diff --git a/Cargo.lock b/Cargo.lock index ae6408d09..c539264f5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -878,6 +878,31 @@ version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +[[package]] +name = "crossterm" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" +dependencies = [ + "bitflags 2.6.0", + "crossterm_winapi", + "libc", + "mio 0.8.11", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + [[package]] name = "crunchy" version = "0.2.2" @@ -3290,6 +3315,18 @@ dependencies = [ "adler", ] +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.48.0", +] + [[package]] name = "mio" version = "1.0.1" @@ -4545,6 +4582,27 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signal-hook" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" +dependencies = [ + "libc", + "mio 0.8.11", + "signal-hook", +] + [[package]] name = "signal-hook-registry" version = "1.4.2" @@ -4690,6 +4748,7 @@ dependencies = [ "clap-markdown", "clap_complete", "crate-git-revision 0.0.4", + "crossterm", "csv", "directories", "dirs", @@ -5536,7 +5595,7 @@ dependencies = [ "backtrace", "bytes", "libc", - "mio", + "mio 1.0.1", "parking_lot", "pin-project-lite", "signal-hook-registry", diff --git a/FULL_HELP_DOCS.md b/FULL_HELP_DOCS.md index b7480cb5e..99577b5d6 100644 --- a/FULL_HELP_DOCS.md +++ b/FULL_HELP_DOCS.md @@ -116,11 +116,13 @@ Get Id of builtin Soroban Asset Contract. Deprecated, use `stellar contract id a ###### **Options:** * `--asset ` — ID of the Stellar classic asset to wrap, e.g. "USDC:G...5" +* `--source-account ` — Account where the final transaction originates from. If no `--sign-with-*` flag is passed, passed key will also be used to sign the transaction. Can be an identity (`--source alice`), a secret key (`--source SC36…`), or a seed phrase (`--source "kite urban…"`) +* `--sign-with-key ` — Sign with a local key. Can be an identity (--sign-with-key alice), a secret key (--sign-with-key SC36…), or a seed phrase (--sign-with-key "kite urban…"). If using seed phrase, `--hd-path` defaults to the `0` path +* `--hd-path ` — If using a seed phrase to sign, sets which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` +* `--yes` — If one of `--sign-with-*` flags is provided, don't ask to confirm to sign a transaction * `--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 signs the final transaction. Alias `source`. Can be an identity (--source alice), a secret key (--source SC36…), or a seed phrase (--source "kite urban…") -* `--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 "." @@ -135,11 +137,13 @@ Deploy builtin Soroban Asset Contract ###### **Options:** * `--asset ` — ID of the Stellar classic asset to wrap, e.g. "USDC:G...5" +* `--source-account ` — Account where the final transaction originates from. If no `--sign-with-*` flag is passed, passed key will also be used to sign the transaction. Can be an identity (`--source alice`), a secret key (`--source SC36…`), or a seed phrase (`--source "kite urban…"`) +* `--sign-with-key ` — Sign with a local key. Can be an identity (--sign-with-key alice), a secret key (--sign-with-key SC36…), or a seed phrase (--sign-with-key "kite urban…"). If using seed phrase, `--hd-path` defaults to the `0` path +* `--hd-path ` — If using a seed phrase to sign, sets which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` +* `--yes` — If one of `--sign-with-*` flags is provided, don't ask to confirm to sign a transaction * `--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 signs the final transaction. Alias `source`. Can be an identity (--source alice), a secret key (--source SC36…), or a seed phrase (--source "kite urban…") -* `--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 "." * `--fee ` — fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm @@ -360,11 +364,13 @@ If no keys are specified the contract itself is extended. - `temporary`: Temporary +* `--source-account ` — Account where the final transaction originates from. If no `--sign-with-*` flag is passed, passed key will also be used to sign the transaction. Can be an identity (`--source alice`), a secret key (`--source SC36…`), or a seed phrase (`--source "kite urban…"`) +* `--sign-with-key ` — Sign with a local key. Can be an identity (--sign-with-key alice), a secret key (--sign-with-key SC36…), or a seed phrase (--sign-with-key "kite urban…"). If using seed phrase, `--hd-path` defaults to the `0` path +* `--hd-path ` — If using a seed phrase to sign, sets which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` +* `--yes` — If one of `--sign-with-*` flags is provided, don't ask to confirm to sign a transaction * `--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 signs the final transaction. Alias `source`. Can be an identity (--source alice), a secret key (--source SC36…), or a seed phrase (--source "kite urban…") -* `--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 "." * `--fee ` — fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm @@ -388,11 +394,13 @@ Deploy a wasm contract * `--wasm ` — WASM file to deploy * `--wasm-hash ` — Hash of the already installed/deployed WASM file * `--salt ` — Custom salt 32-byte salt for the token id +* `--source-account ` — Account where the final transaction originates from. If no `--sign-with-*` flag is passed, passed key will also be used to sign the transaction. Can be an identity (`--source alice`), a secret key (`--source SC36…`), or a seed phrase (`--source "kite urban…"`) +* `--sign-with-key ` — Sign with a local key. Can be an identity (--sign-with-key alice), a secret key (--sign-with-key SC36…), or a seed phrase (--sign-with-key "kite urban…"). If using seed phrase, `--hd-path` defaults to the `0` path +* `--hd-path ` — If using a seed phrase to sign, sets which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` +* `--yes` — If one of `--sign-with-*` flags is provided, don't ask to confirm to sign a transaction * `--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 signs the final transaction. Alias `source`. Can be an identity (--source alice), a secret key (--source SC36…), or a seed phrase (--source "kite urban…") -* `--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 "." * `--fee ` — fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm @@ -449,11 +457,13 @@ Deploy builtin Soroban Asset Contract ###### **Options:** * `--asset ` — ID of the Stellar classic asset to wrap, e.g. "USDC:G...5" +* `--source-account ` — Account where the final transaction originates from. If no `--sign-with-*` flag is passed, passed key will also be used to sign the transaction. Can be an identity (`--source alice`), a secret key (`--source SC36…`), or a seed phrase (`--source "kite urban…"`) +* `--sign-with-key ` — Sign with a local key. Can be an identity (--sign-with-key alice), a secret key (--sign-with-key SC36…), or a seed phrase (--sign-with-key "kite urban…"). If using seed phrase, `--hd-path` defaults to the `0` path +* `--hd-path ` — If using a seed phrase to sign, sets which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` +* `--yes` — If one of `--sign-with-*` flags is provided, don't ask to confirm to sign a transaction * `--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 signs the final transaction. Alias `source`. Can be an identity (--source alice), a secret key (--source SC36…), or a seed phrase (--source "kite urban…") -* `--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 "." @@ -468,11 +478,13 @@ Deploy normal Wasm Contract ###### **Options:** * `--salt ` — ID of the Soroban contract +* `--source-account ` — Account where the final transaction originates from. If no `--sign-with-*` flag is passed, passed key will also be used to sign the transaction. Can be an identity (`--source alice`), a secret key (`--source SC36…`), or a seed phrase (`--source "kite urban…"`) +* `--sign-with-key ` — Sign with a local key. Can be an identity (--sign-with-key alice), a secret key (--sign-with-key SC36…), or a seed phrase (--sign-with-key "kite urban…"). If using seed phrase, `--hd-path` defaults to the `0` path +* `--hd-path ` — If using a seed phrase to sign, sets which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` +* `--yes` — If one of `--sign-with-*` flags is provided, don't ask to confirm to sign a transaction * `--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 signs the final transaction. Alias `source`. Can be an identity (--source alice), a secret key (--source SC36…), or a seed phrase (--source "kite urban…") -* `--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 "." @@ -666,11 +678,13 @@ Install a WASM file to the ledger without creating a contract instance ###### **Options:** +* `--source-account ` — Account where the final transaction originates from. If no `--sign-with-*` flag is passed, passed key will also be used to sign the transaction. Can be an identity (`--source alice`), a secret key (`--source SC36…`), or a seed phrase (`--source "kite urban…"`) +* `--sign-with-key ` — Sign with a local key. Can be an identity (--sign-with-key alice), a secret key (--sign-with-key SC36…), or a seed phrase (--sign-with-key "kite urban…"). If using seed phrase, `--hd-path` defaults to the `0` path +* `--hd-path ` — If using a seed phrase to sign, sets which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` +* `--yes` — If one of `--sign-with-*` flags is provided, don't ask to confirm to sign a transaction * `--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 signs the final transaction. Alias `source`. Can be an identity (--source alice), a secret key (--source SC36…), or a seed phrase (--source "kite urban…") -* `--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 "." * `--fee ` — fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm @@ -705,11 +719,13 @@ stellar contract invoke ... -- --help * `--id ` — Contract ID to invoke * `--is-view` — View the result simulating and do not sign and submit transaction. Deprecated use `--send=no` +* `--source-account ` — Account where the final transaction originates from. If no `--sign-with-*` flag is passed, passed key will also be used to sign the transaction. Can be an identity (`--source alice`), a secret key (`--source SC36…`), or a seed phrase (`--source "kite urban…"`) +* `--sign-with-key ` — Sign with a local key. Can be an identity (--sign-with-key alice), a secret key (--sign-with-key SC36…), or a seed phrase (--sign-with-key "kite urban…"). If using seed phrase, `--hd-path` defaults to the `0` path +* `--hd-path ` — If using a seed phrase to sign, sets which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` +* `--yes` — If one of `--sign-with-*` flags is provided, don't ask to confirm to sign a transaction * `--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 signs the final transaction. Alias `source`. Can be an identity (--source alice), a secret key (--source SC36…), or a seed phrase (--source "kite urban…") -* `--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 "." * `--fee ` — fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm @@ -719,6 +735,10 @@ stellar contract invoke ... -- --help * `--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 +* `--auth-expires-in-ledgers ` — Number of ledgers from current ledger before the signed auth entry expires. Default 60 ~ 5 minutes + + Default value: `60` +* `--auth-expires-at-ledger ` — Ledger number when signed auth entry expires * `--send ` — Whether or not to send a transaction Default value: `default` @@ -782,11 +802,13 @@ Print the current value of a contract-data ledger entry - `temporary`: Temporary +* `--source-account ` — Account where the final transaction originates from. If no `--sign-with-*` flag is passed, passed key will also be used to sign the transaction. Can be an identity (`--source alice`), a secret key (`--source SC36…`), or a seed phrase (`--source "kite urban…"`) +* `--sign-with-key ` — Sign with a local key. Can be an identity (--sign-with-key alice), a secret key (--sign-with-key SC36…), or a seed phrase (--sign-with-key "kite urban…"). If using seed phrase, `--hd-path` defaults to the `0` path +* `--hd-path ` — If using a seed phrase to sign, sets which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` +* `--yes` — If one of `--sign-with-*` flags is provided, don't ask to confirm to sign a transaction * `--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 signs the final transaction. Alias `source`. Can be an identity (--source alice), a secret key (--source SC36…), or a seed phrase (--source "kite urban…") -* `--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 "." @@ -819,11 +841,13 @@ If no keys are specificed the contract itself is restored. * `--ledgers-to-extend ` — Number of ledgers to extend the entry * `--ttl-ledger-only` — Only print the new Time To Live ledger +* `--source-account ` — Account where the final transaction originates from. If no `--sign-with-*` flag is passed, passed key will also be used to sign the transaction. Can be an identity (`--source alice`), a secret key (`--source SC36…`), or a seed phrase (`--source "kite urban…"`) +* `--sign-with-key ` — Sign with a local key. Can be an identity (--sign-with-key alice), a secret key (--sign-with-key SC36…), or a seed phrase (--sign-with-key "kite urban…"). If using seed phrase, `--hd-path` defaults to the `0` path +* `--hd-path ` — If using a seed phrase to sign, sets which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` +* `--yes` — If one of `--sign-with-*` flags is provided, don't ask to confirm to sign a transaction * `--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 signs the final transaction. Alias `source`. Can be an identity (--source alice), a secret key (--source SC36…), or a seed phrase (--source "kite urban…") -* `--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 "." * `--fee ` — fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm @@ -1291,6 +1315,8 @@ Sign, Simulate, and Send transactions * `simulate` — Simulate a transaction envelope from stdin * `hash` — Calculate the hash of a transaction envelope from stdin +* `sign` — Sign a transaction envolope appending the signature to the envelope +* `send` — Send a transaction envelope to the network @@ -1302,11 +1328,13 @@ Simulate a transaction envelope from stdin ###### **Options:** +* `--source-account ` — Account where the final transaction originates from. If no `--sign-with-*` flag is passed, passed key will also be used to sign the transaction. Can be an identity (`--source alice`), a secret key (`--source SC36…`), or a seed phrase (`--source "kite urban…"`) +* `--sign-with-key ` — Sign with a local key. Can be an identity (--sign-with-key alice), a secret key (--sign-with-key SC36…), or a seed phrase (--sign-with-key "kite urban…"). If using seed phrase, `--hd-path` defaults to the `0` path +* `--hd-path ` — If using a seed phrase to sign, sets which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` +* `--yes` — If one of `--sign-with-*` flags is provided, don't ask to confirm to sign a transaction * `--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 signs the final transaction. Alias `source`. Can be an identity (--source alice), a secret key (--source SC36…), or a seed phrase (--source "kite urban…") -* `--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 "." @@ -1326,6 +1354,41 @@ Calculate the hash of a transaction envelope from stdin +## `stellar tx sign` + +Sign a transaction envolope appending the signature to the envelope + +**Usage:** `stellar tx sign [OPTIONS]` + +###### **Options:** + +* `--sign-with-key ` — Sign with a local key. Can be an identity (--sign-with-key alice), a secret key (--sign-with-key SC36…), or a seed phrase (--sign-with-key "kite urban…"). If using seed phrase, `--hd-path` defaults to the `0` path +* `--hd-path ` — If using a seed phrase to sign, sets which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` +* `--yes` — If one of `--sign-with-*` flags is provided, don't ask to confirm to sign a transaction +* `--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 +* `--global` — Use global config +* `--config-dir ` — Location of config directory, default is "." + + + +## `stellar tx send` + +Send a transaction envelope to the network + +**Usage:** `stellar tx send [OPTIONS]` + +###### **Options:** + +* `--rpc-url ` — RPC server endpoint +* `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server +* `--network ` — Name of network to use from config +* `--global` — Use global config +* `--config-dir ` — Location of config directory, default is "." + + + ## `stellar xdr` Decode and encode XDR diff --git a/cmd/crates/soroban-test/src/lib.rs b/cmd/crates/soroban-test/src/lib.rs index 544e2d59e..44e14204e 100644 --- a/cmd/crates/soroban-test/src/lib.rs +++ b/cmd/crates/soroban-test/src/lib.rs @@ -31,7 +31,7 @@ use fs_extra::dir::CopyOptions; use soroban_cli::{ commands::{contract::invoke, global, keys, NetworkRunnable}, - config::{self, network}, + config::{self, network, sign_with}, CommandParser, }; @@ -222,17 +222,20 @@ impl TestEnv { pub fn clone_config(&self, account: &str) -> config::Args { let config_dir = Some(self.dir().to_path_buf()); config::Args { - network: network::Args { - rpc_url: Some(self.rpc_url.clone()), - network_passphrase: Some(LOCAL_NETWORK_PASSPHRASE.to_string()), - network: None, - }, source_account: account.to_string(), - locator: config::locator::Args { - global: false, - config_dir, + sign_with: sign_with::Args { + network: network::Args { + rpc_url: Some(self.rpc_url.clone()), + network_passphrase: Some(LOCAL_NETWORK_PASSPHRASE.to_string()), + network: None, + }, + locator: config::locator::Args { + global: false, + config_dir, + }, + yes: true, + ..Default::default() }, - hd_path: None, } } @@ -243,9 +246,10 @@ impl TestEnv { account: &str, ) -> Result { let config = self.clone_config(account); + eprintln!("Running with config: {config:#?}"); cmd.run_against_rpc_server( Some(&global::Args { - locator: config.locator.clone(), + locator: config.sign_with.locator.clone(), filter_logs: Vec::default(), quiet: false, verbose: false, @@ -264,9 +268,10 @@ impl TestEnv { } /// Returns the public key corresponding to the test keys's `hd_path` - pub fn test_address(&self, hd_path: usize) -> String { + pub async fn test_address(&self, hd_path: usize) -> String { self.cmd::(&format!("--hd-path={hd_path}")) .public_key() + .await .unwrap() .to_string() } diff --git a/cmd/crates/soroban-test/tests/it/integration/custom_types.rs b/cmd/crates/soroban-test/tests/it/integration/custom_types.rs index 6cdb61192..b427029c1 100644 --- a/cmd/crates/soroban-test/tests/it/integration/custom_types.rs +++ b/cmd/crates/soroban-test/tests/it/integration/custom_types.rs @@ -9,7 +9,13 @@ use super::util::invoke_with_roundtrip; fn invoke_custom(e: &TestEnv, id: &str, func: &str) -> assert_cmd::Command { let mut s = e.new_assert_cmd("contract"); - s.arg("invoke").arg("--id").arg(id).arg("--").arg(func); + s.env("RUST_LOG", "soroban_cli::log::diagnostic_event=off") + .arg("invoke") + .arg("--id") + .arg(id) + .arg("--is-view") + .arg("--") + .arg(func); s } 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 20b680997..e335c79fa 100644 --- a/cmd/crates/soroban-test/tests/it/integration/hello_world.rs +++ b/cmd/crates/soroban-test/tests/it/integration/hello_world.rs @@ -219,6 +219,7 @@ async fn invoke_auth_with_different_test_account_fail(sandbox: &TestEnv, id: &st "--hd-path=0", "--id", id, + "--fee=1000000", "--", "auth", &format!("--addr={addr}"), @@ -227,8 +228,8 @@ async fn invoke_auth_with_different_test_account_fail(sandbox: &TestEnv, id: &st .await; let e = res.unwrap_err(); assert!( - matches!(e, contract::invoke::Error::Config(_)), - "Expected config error got {e:?}" + matches!(e, contract::invoke::Error::Rpc(_)), + "Expected rpc error got {e:?}" ); } diff --git a/cmd/crates/soroban-test/tests/it/integration/tx.rs b/cmd/crates/soroban-test/tests/it/integration/tx.rs index bcb880b18..b877536b1 100644 --- a/cmd/crates/soroban-test/tests/it/integration/tx.rs +++ b/cmd/crates/soroban-test/tests/it/integration/tx.rs @@ -1,10 +1,10 @@ use soroban_sdk::xdr::{Limits, ReadXdr, TransactionEnvelope, WriteXdr}; use soroban_test::{AssertExt, TestEnv}; -use crate::integration::util::{deploy_contract, DeployKind, HELLO_WORLD}; +use crate::integration::util::{deploy_contract, deploy_hello, DeployKind, HELLO_WORLD}; #[tokio::test] -async fn txn_simulate() { +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; @@ -49,3 +49,49 @@ async fn txn_hash() { assert_eq!(hash.trim(), expected_hash); } + +#[tokio::test] +async fn send() { + let sandbox = &TestEnv::new(); + sandbox + .new_assert_cmd("contract") + .arg("install") + .args(["--wasm", HELLO_WORLD.path().as_os_str().to_str().unwrap()]) + .assert() + .success(); + + let xdr_base64 = deploy_contract(sandbox, HELLO_WORLD, DeployKind::SimOnly).await; + println!("{xdr_base64}"); + let tx_env = TransactionEnvelope::from_xdr_base64(&xdr_base64, Limits::none()).unwrap(); + let tx_env = sign_manually(sandbox, &tx_env); + + println!( + "Transaction to send:\n{}", + tx_env.to_xdr_base64(Limits::none()).unwrap() + ); + + let assembled_str = sandbox + .new_assert_cmd("tx") + .arg("send") + .write_stdin(tx_env.to_xdr_base64(Limits::none()).unwrap()) + .assert() + .success() + .stdout_as_str(); + println!("Transaction sent: {assembled_str}"); +} + +fn sign_manually(sandbox: &TestEnv, tx_env: &TransactionEnvelope) -> TransactionEnvelope { + TransactionEnvelope::from_xdr_base64( + sandbox + .new_assert_cmd("tx") + .arg("sign") + .arg("--sign-with-key=test") + .arg("--yes") + .write_stdin(tx_env.to_xdr_base64(Limits::none()).unwrap().as_bytes()) + .assert() + .success() + .stdout_as_str(), + Limits::none(), + ) + .unwrap() +} diff --git a/cmd/soroban-cli/Cargo.toml b/cmd/soroban-cli/Cargo.toml index d9ced8aa2..d6ef5d90c 100644 --- a/cmd/soroban-cli/Cargo.toml +++ b/cmd/soroban-cli/Cargo.toml @@ -50,6 +50,7 @@ soroban-ledger-snapshot = { workspace = true } stellar-strkey = { workspace = true } soroban-sdk = { workspace = true } soroban-rpc = { workspace = true } + clap = { workspace = true, features = [ "derive", "env", @@ -113,7 +114,7 @@ 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" @@ -121,6 +122,7 @@ flate2 = "1.0.30" bytesize = "1.3.0" humantime = "2.1.0" phf = { version = "0.11.2", features = ["macros"] } +crossterm = "0.27.0" semver = "1.0.0" glob = "0.3.1" diff --git a/cmd/soroban-cli/src/commands/contract/arg_parsing.rs b/cmd/soroban-cli/src/commands/contract/arg_parsing.rs index 4a8af47e2..772a3e2f9 100644 --- a/cmd/soroban-cli/src/commands/contract/arg_parsing.rs +++ b/cmd/soroban-cli/src/commands/contract/arg_parsing.rs @@ -5,16 +5,17 @@ use std::fmt::Debug; use std::path::PathBuf; use clap::value_parser; -use ed25519_dalek::SigningKey; use heck::ToKebabCase; +use crate::commands::txn_result::TxnResult; +use crate::config::secret::StellarSigner; +use crate::config::{self}; +use crate::signer::{self, Stellar}; use soroban_env_host::xdr::{ self, Hash, InvokeContractArgs, ScAddress, ScSpecEntry, ScSpecFunctionV0, ScSpecTypeDef, ScVal, ScVec, }; - -use crate::commands::txn_result::TxnResult; -use crate::config::{self}; +use soroban_sdk::xdr::ScSpecFunctionInputV0; use soroban_spec_tools::Spec; #[derive(thiserror::Error, Debug)] @@ -43,14 +44,16 @@ pub enum Error { MissingArgument(String), #[error("")] MissingFileArg(PathBuf), + #[error(transparent)] + Signer(#[from] signer::Error), } -pub fn build_host_function_parameters( +pub async fn build_host_function_parameters( contract_id: &stellar_strkey::Contract, slop: &[OsString], spec_entries: &[ScSpecEntry], config: &config::Args, -) -> Result<(String, Spec, InvokeContractArgs, Vec), Error> { +) -> Result<(String, Spec, InvokeContractArgs, Vec), Error> { let spec = Spec(Some(spec_entries.to_vec())); let mut cmd = clap::Command::new(contract_id.to_string()) .no_binary_name(true) @@ -70,57 +73,15 @@ pub fn build_host_function_parameters( let func = spec.find_function(function)?; // create parsed_args in same order as the inputs to func - let mut signers: Vec = vec![]; - let parsed_args = func - .inputs - .iter() - .map(|i| { - let name = i.name.to_utf8_string()?; - if let Some(mut val) = matches_.get_raw(&name) { - let mut s = val.next().unwrap().to_string_lossy().to_string(); - if matches!(i.type_, ScSpecTypeDef::Address) { - let cmd = crate::commands::keys::address::Cmd { - name: s.clone(), - hd_path: Some(0), - locator: config.locator.clone(), - }; - if let Ok(address) = cmd.public_key() { - s = address.to_string(); - } - if let Ok(key) = cmd.private_key() { - signers.push(key); - } - } - spec.from_string(&s, &i.type_) - .map_err(|error| Error::CannotParseArg { arg: name, error }) - } else if matches!(i.type_, ScSpecTypeDef::Option(_)) { - Ok(ScVal::Void) - } else if let Some(arg_path) = matches_.get_one::(&fmt_arg_file_name(&name)) { - if matches!(i.type_, ScSpecTypeDef::Bytes | ScSpecTypeDef::BytesN(_)) { - Ok(ScVal::try_from( - &std::fs::read(arg_path) - .map_err(|_| Error::MissingFileArg(arg_path.clone()))?, - ) - .map_err(|()| Error::CannotParseArg { - arg: name.clone(), - error: soroban_spec_tools::Error::Unknown, - })?) - } else { - let file_contents = std::fs::read_to_string(arg_path) - .map_err(|_| Error::MissingFileArg(arg_path.clone()))?; - tracing::debug!( - "file {arg_path:?}, has contents:\n{file_contents}\nAnd type {:#?}\n{}", - i.type_, - file_contents.len() - ); - spec.from_string(&file_contents, &i.type_) - .map_err(|error| Error::CannotParseArg { arg: name, error }) - } - } else { - Err(Error::MissingArgument(name)) - } - }) - .collect::, Error>>()?; + let mut signers: Vec = vec![]; + let mut parsed_args: Vec = Vec::new(); + for i in func.inputs.iter() { + let (val, signer) = parse_arg(i, matches_, config, &spec).await?; + parsed_args.push(val); + if let Some(signer) = signer { + signers.push(signer); + } + } let contract_address_arg = ScAddress::Contract(Hash(contract_id.0)); let function_symbol_arg = function @@ -145,6 +106,58 @@ pub fn build_host_function_parameters( Ok((function.clone(), spec, invoke_args, signers)) } +pub async fn parse_arg( + input: &ScSpecFunctionInputV0, + matches_: &clap::ArgMatches, + config: &config::Args, + spec: &Spec, +) -> Result<(ScVal, Option), Error> { + let mut signer: Option = None; + let name = input.name.to_utf8_string()?; + let sc_val = if let Some(mut val) = matches_.get_raw(&name) { + let mut s = val.next().unwrap().to_string_lossy().to_string(); + if matches!(input.type_, ScSpecTypeDef::Address) { + // Currently we only support local keys, same as input for --sign-with-key`, for signing auth entries. + if let Ok(signer_) = config + .sign_with + .locator + .account(&s) + .and_then(|signer| Ok(signer.signer(config.sign_with.hd_path, false)?)) + { + s = signer_.get_public_key().await?.to_string(); + signer = Some(signer_); + } + } + spec.from_string(&s, &input.type_) + .map_err(|error| Error::CannotParseArg { arg: name, error })? + } else if matches!(input.type_, ScSpecTypeDef::Option(_)) { + ScVal::Void + } else if let Some(arg_path) = matches_.get_one::(&fmt_arg_file_name(&name)) { + if matches!(input.type_, ScSpecTypeDef::Bytes | ScSpecTypeDef::BytesN(_)) { + ScVal::try_from( + &std::fs::read(arg_path).map_err(|_| Error::MissingFileArg(arg_path.clone()))?, + ) + .map_err(|()| Error::CannotParseArg { + arg: name.clone(), + error: soroban_spec_tools::Error::Unknown, + })? + } else { + let file_contents = std::fs::read_to_string(arg_path) + .map_err(|_| Error::MissingFileArg(arg_path.clone()))?; + tracing::debug!( + "file {arg_path:?}, has contents:\n{file_contents}\nAnd type {:#?}\n{}", + input.type_, + file_contents.len() + ); + spec.from_string(&file_contents, &input.type_) + .map_err(|error| Error::CannotParseArg { arg: name, error })? + } + } else { + return Err(Error::MissingArgument(name)); + }; + Ok((sc_val, signer)) +} + fn build_custom_cmd(name: &str, spec: &Spec) -> Result { let func = spec .find_function(name) diff --git a/cmd/soroban-cli/src/commands/contract/deploy/asset.rs b/cmd/soroban-cli/src/commands/contract/deploy/asset.rs index 51baa55e9..d0abea606 100644 --- a/cmd/soroban-cli/src/commands/contract/deploy/asset.rs +++ b/cmd/soroban-cli/src/commands/contract/deploy/asset.rs @@ -97,13 +97,10 @@ impl NetworkRunnable for Cmd { client .verify_network_passphrase(Some(&network.network_passphrase)) .await?; - let key = config.key_pair()?; - // Get the account sequence number - let public_strkey = - stellar_strkey::ed25519::PublicKey(key.verifying_key().to_bytes()).to_string(); + let public_strkey = config.source_account().await?; // TODO: use symbols for the method names (both here and in serve) - let account_details = client.get_account(&public_strkey).await?; + let account_details = client.get_account(&public_strkey.to_string()).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); @@ -113,7 +110,7 @@ impl NetworkRunnable for Cmd { sequence + 1, self.fee.fee, network_passphrase, - &key, + &public_strkey, )?; if self.fee.build_only { return Ok(TxnResult::Txn(tx)); @@ -124,7 +121,7 @@ impl NetworkRunnable for Cmd { return Ok(TxnResult::Txn(txn)); } let get_txn_resp = client - .send_transaction_polling(&self.config.sign_with_local_key(txn).await?) + .send_transaction_polling(&self.config.sign(txn).await?) .await? .try_into()?; if args.map_or(true, |a| !a.no_cache) { @@ -141,7 +138,7 @@ fn build_wrap_token_tx( sequence: i64, fee: u32, _network_passphrase: &str, - key: &ed25519_dalek::SigningKey, + key: &stellar_strkey::ed25519::PublicKey, ) -> Result { let contract = ScAddress::Contract(contract_id.clone()); let mut read_write = vec![ @@ -180,7 +177,7 @@ fn build_wrap_token_tx( }; Ok(Transaction { - source_account: MuxedAccount::Ed25519(Uint256(key.verifying_key().to_bytes())), + source_account: MuxedAccount::Ed25519(Uint256(key.0)), 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 74491249f..15ce97624 100644 --- a/cmd/soroban-cli/src/commands/contract/deploy/wasm.rs +++ b/cmd/soroban-cli/src/commands/contract/deploy/wasm.rs @@ -123,19 +123,12 @@ impl Cmd { .run_against_rpc_server(Some(global_args), None) .await? .to_envelope(); - match res { + match &res { TxnEnvelopeResult::TxnEnvelope(tx) => println!("{}", tx.to_xdr_base64(Limits::none())?), TxnEnvelopeResult::Res(contract) => { - let network = self.config.get_network()?; - - if let Some(alias) = self.alias.clone() { - self.config.locator.save_contract_id( - &network.network_passphrase, - &contract, - &alias, - )?; + if let Some(alias) = self.alias.as_deref() { + self.config.save_contract_id(contract, alias)?; } - println!("{contract}"); } } @@ -212,13 +205,9 @@ impl NetworkRunnable for Cmd { client .verify_network_passphrase(Some(&network.network_passphrase)) .await?; - let key = config.key_pair()?; - // Get the account sequence number - let public_strkey = - stellar_strkey::ed25519::PublicKey(key.verifying_key().to_bytes()).to_string(); - - let account_details = client.get_account(&public_strkey).await?; + let public_strkey = config.source_account().await?; + let account_details = client.get_account(&public_strkey.to_string()).await?; let sequence: i64 = account_details.seq_num.into(); let (txn, contract_id) = build_create_contract_tx( wasm_hash, @@ -226,7 +215,7 @@ impl NetworkRunnable for Cmd { self.fee.fee, &network.network_passphrase, salt, - &key, + &public_strkey, )?; if self.fee.build_only { @@ -248,7 +237,7 @@ impl NetworkRunnable for Cmd { print.log_transaction(&txn, &network, true)?; let get_txn_resp = client - .send_transaction_polling(&config.sign_with_local_key(txn).await?) + .send_transaction_polling(&config.sign(txn).await?) .await? .try_into()?; @@ -274,11 +263,9 @@ fn build_create_contract_tx( fee: u32, network_passphrase: &str, salt: [u8; 32], - key: &ed25519_dalek::SigningKey, + key: &stellar_strkey::ed25519::PublicKey, ) -> Result<(Transaction, Hash), Error> { - let source_account = AccountId(PublicKey::PublicKeyTypeEd25519( - key.verifying_key().to_bytes().into(), - )); + let source_account = AccountId(PublicKey::PublicKeyTypeEd25519(key.0.into())); let contract_id_preimage = ContractIdPreimage::Address(ContractIdPreimageFromAddress { address: ScAddress::Account(source_account), @@ -297,7 +284,7 @@ fn build_create_contract_tx( }), }; let tx = Transaction { - source_account: MuxedAccount::Ed25519(Uint256(key.verifying_key().to_bytes())), + source_account: MuxedAccount::Ed25519(Uint256(key.0)), fee, seq_num: SequenceNumber(sequence), cond: Preconditions::None, @@ -311,6 +298,7 @@ fn build_create_contract_tx( #[cfg(test)] mod tests { + use super::*; #[test] @@ -325,8 +313,12 @@ mod tests { 1, "Public Global Stellar Network ; September 2015", [0u8; 32], - &utils::parse_secret_key("SBFGFF27Y64ZUGFAIG5AMJGQODZZKV2YQKAVUUN4HNE24XZXD2OEUVUP") - .unwrap(), + &stellar_strkey::ed25519::PublicKey( + utils::parse_secret_key("SBFGFF27Y64ZUGFAIG5AMJGQODZZKV2YQKAVUUN4HNE24XZXD2OEUVUP") + .unwrap() + .verifying_key() + .to_bytes(), + ), ); assert!(result.is_ok()); diff --git a/cmd/soroban-cli/src/commands/contract/extend.rs b/cmd/soroban-cli/src/commands/contract/extend.rs index 5901c71b1..a2499f415 100644 --- a/cmd/soroban-cli/src/commands/contract/extend.rs +++ b/cmd/soroban-cli/src/commands/contract/extend.rs @@ -130,19 +130,15 @@ impl NetworkRunnable for Cmd { let config = config.unwrap_or(&self.config); let network = config.get_network()?; tracing::trace!(?network); - let keys = self.key.parse_keys(&config.locator, &network)?; + let keys = self.key.parse_keys(&config.sign_with.locator, &network)?; let client = Client::new(&network.rpc_url)?; - let key = config.key_pair()?; + let public_key = config.source_account().await?; let extend_to = self.ledgers_to_extend(); - - // Get the account sequence number - let public_strkey = - stellar_strkey::ed25519::PublicKey(key.verifying_key().to_bytes()).to_string(); - let account_details = client.get_account(&public_strkey).await?; + let account_details = client.get_account(&public_key.to_string()).await?; let sequence: i64 = account_details.seq_num.into(); let tx = Transaction { - source_account: MuxedAccount::Ed25519(Uint256(key.verifying_key().to_bytes())), + source_account: MuxedAccount::Ed25519(Uint256(public_key.0)), fee: self.fee.fee, seq_num: SequenceNumber(sequence + 1), cond: Preconditions::None, @@ -178,7 +174,7 @@ impl NetworkRunnable for Cmd { .transaction() .clone(); let res = client - .send_transaction_polling(&config.sign_with_local_key(tx).await?) + .send_transaction_polling(&config.sign(tx).await?) .await?; if args.map_or(true, |a| !a.no_cache) { data::write(res.clone().try_into()?, &network.rpc_uri()?)?; diff --git a/cmd/soroban-cli/src/commands/contract/id.rs b/cmd/soroban-cli/src/commands/contract/id.rs index bb8744d51..f07fa8df6 100644 --- a/cmd/soroban-cli/src/commands/contract/id.rs +++ b/cmd/soroban-cli/src/commands/contract/id.rs @@ -18,10 +18,10 @@ pub enum Error { } impl Cmd { - pub fn run(&self) -> Result<(), Error> { + pub async fn run(&self) -> Result<(), Error> { match &self { Cmd::Asset(asset) => asset.run()?, - Cmd::Wasm(wasm) => wasm.run()?, + Cmd::Wasm(wasm) => wasm.run().await?, } Ok(()) } diff --git a/cmd/soroban-cli/src/commands/contract/id/wasm.rs b/cmd/soroban-cli/src/commands/contract/id/wasm.rs index 14824b145..4afe14463 100644 --- a/cmd/soroban-cli/src/commands/contract/id/wasm.rs +++ b/cmd/soroban-cli/src/commands/contract/id/wasm.rs @@ -29,13 +29,12 @@ pub enum Error { CannotParseSalt(String), } impl Cmd { - pub fn run(&self) -> Result<(), Error> { + pub async fn run(&self) -> Result<(), Error> { let salt: [u8; 32] = soroban_spec_tools::utils::padded_hex_from_str(&self.salt, 32) .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 contract_id_preimage = contract_preimage(&self.config.source_account().await?, salt); let contract_id = get_contract_id( contract_id_preimage.clone(), &self.config.get_network()?.network_passphrase, @@ -46,8 +45,11 @@ impl Cmd { } } -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), diff --git a/cmd/soroban-cli/src/commands/contract/install.rs b/cmd/soroban-cli/src/commands/contract/install.rs index 98c866271..ce21bf5ba 100644 --- a/cmd/soroban-cli/src/commands/contract/install.rs +++ b/cmd/soroban-cli/src/commands/contract/install.rs @@ -125,17 +125,12 @@ impl NetworkRunnable for Cmd { tracing::warn!("the deployed smart contract {path} was built with Soroban Rust SDK v{rs_sdk_ver}, a release candidate version not intended for use with the Stellar Public Network", path = self.wasm.wasm.display()); } } - - let key = config.key_pair()?; - + let public_strkey = config.source_account().await?; // Get the account sequence number - let public_strkey = - stellar_strkey::ed25519::PublicKey(key.verifying_key().to_bytes()).to_string(); - let account_details = client.get_account(&public_strkey).await?; + let account_details = client.get_account(&public_strkey.to_string()).await?; let sequence: i64 = account_details.seq_num.into(); - let (tx_without_preflight, hash) = - build_install_contract_code_tx(&contract, sequence + 1, self.fee.fee, &key)?; + build_install_contract_code_tx(&contract, sequence + 1, self.fee.fee, &public_strkey)?; if self.fee.build_only { return Ok(TxnResult::Txn(tx_without_preflight)); @@ -189,7 +184,7 @@ impl NetworkRunnable for Cmd { print.globeln("Submitting install transaction…"); let txn_resp = client - .send_transaction_polling(&self.config.sign_with_local_key(txn).await?) + .send_transaction_polling(&self.config.sign(txn).await?) .await?; if args.map_or(true, |a| !a.no_cache) { @@ -255,14 +250,12 @@ pub(crate) fn build_install_contract_code_tx( source_code: &[u8], sequence: i64, fee: u32, - key: &ed25519_dalek::SigningKey, + key: &stellar_strkey::ed25519::PublicKey, ) -> Result<(Transaction, Hash), XdrError> { let hash = utils::contract_hash(source_code)?; let op = Operation { - source_account: Some(MuxedAccount::Ed25519(Uint256( - key.verifying_key().to_bytes(), - ))), + source_account: Some(MuxedAccount::Ed25519(Uint256(key.0))), body: OperationBody::InvokeHostFunction(InvokeHostFunctionOp { host_function: HostFunction::UploadContractWasm(source_code.try_into()?), auth: VecM::default(), @@ -270,7 +263,7 @@ pub(crate) fn build_install_contract_code_tx( }; let tx = Transaction { - source_account: MuxedAccount::Ed25519(Uint256(key.verifying_key().to_bytes())), + source_account: MuxedAccount::Ed25519(Uint256(key.0)), fee, seq_num: SequenceNumber(sequence), cond: Preconditions::None, @@ -292,8 +285,12 @@ mod tests { b"foo", 300, 1, - &utils::parse_secret_key("SBFGFF27Y64ZUGFAIG5AMJGQODZZKV2YQKAVUUN4HNE24XZXD2OEUVUP") - .unwrap(), + &stellar_strkey::ed25519::PublicKey( + utils::parse_secret_key("SBFGFF27Y64ZUGFAIG5AMJGQODZZKV2YQKAVUUN4HNE24XZXD2OEUVUP") + .unwrap() + .verifying_key() + .to_bytes(), + ), ); assert!(result.is_ok()); diff --git a/cmd/soroban-cli/src/commands/contract/invoke.rs b/cmd/soroban-cli/src/commands/contract/invoke.rs index 293867c76..816431c14 100644 --- a/cmd/soroban-cli/src/commands/contract/invoke.rs +++ b/cmd/soroban-cli/src/commands/contract/invoke.rs @@ -28,6 +28,8 @@ use crate::commands::txn_result::{TxnEnvelopeResult, TxnResult}; use crate::commands::NetworkRunnable; use crate::get_spec::{self, get_remote_contract_spec}; use crate::print; +use crate::signer::{self, auth::sign_soroban_authorizations}; +// use crate::signer; use crate::{ commands::global, config::{self, data, locator, network}, @@ -55,6 +57,8 @@ pub struct Cmd { pub config: config::Args, #[command(flatten)] pub fee: crate::fee::Args, + #[command(flatten)] + pub auth: crate::commands::tx::auth::Args, /// Whether or not to send a transaction #[arg(long, value_enum, default_value_t, env = "STELLAR_SEND")] pub send: Send, @@ -127,6 +131,8 @@ pub enum Error { #[error(transparent)] GetSpecError(#[from] get_spec::Error), #[error(transparent)] + Signer(#[from] signer::Error), + #[error(transparent)] ArgParsing(#[from] arg_parsing::Error), } @@ -201,13 +207,15 @@ impl NetworkRunnable for Cmd { tracing::trace!(?network); let contract_id = self .config + .sign_with .locator .resolve_contract_id(&self.contract_id, &network.network_passphrase)?; let spec_entries = self.spec_entries()?; if let Some(spec_entries) = &spec_entries { // For testing wasm arg parsing - let _ = build_host_function_parameters(&contract_id, &self.slop, spec_entries, config)?; + let _ = build_host_function_parameters(&contract_id, &self.slop, spec_entries, config) + .await?; } let client = rpc::Client::new(&network.rpc_url)?; let account_details = if self.is_view { @@ -216,20 +224,18 @@ impl NetworkRunnable for Cmd { client .verify_network_passphrase(Some(&network.network_passphrase)) .await?; - let key = config.key_pair()?; + let key = config.source_account().await?; // Get the account sequence number - let public_strkey = - stellar_strkey::ed25519::PublicKey(key.verifying_key().to_bytes()).to_string(); - client.get_account(&public_strkey).await? + client.get_account(&key.to_string()).await? }; let sequence: i64 = account_details.seq_num.into(); let AccountId(PublicKey::PublicKeyTypeEd25519(account_id)) = account_details.account_id; let spec_entries = get_remote_contract_spec( &contract_id.0, - &config.locator, - &config.network, + &config.sign_with.locator, + &config.sign_with.network, global_args, Some(config), ) @@ -238,7 +244,7 @@ impl NetworkRunnable for Cmd { // Get the ledger footprint let (function, spec, host_function_params, signers) = - build_host_function_parameters(&contract_id, &self.slop, &spec_entries, config)?; + build_host_function_parameters(&contract_id, &self.slop, &spec_entries, config).await?; let tx = build_invoke_contract_tx( host_function_params.clone(), sequence + 1, @@ -263,11 +269,17 @@ impl NetworkRunnable for Cmd { let global::Args { no_cache, .. } = global_args.cloned().unwrap_or_default(); // Need to sign all auth entries let mut txn = txn.transaction().clone(); - if let Some(tx) = config.sign_soroban_authorizations(&txn, &signers).await? { - txn = tx; + let expriation_ledger = self.auth.expiration_ledger(&client).await?; + for signer in &signers { + if let Some(tx) = + sign_soroban_authorizations(signer, &txn, &network, expriation_ledger) + .await? + { + txn = tx; + } } let res = client - .send_transaction_polling(&config.sign_with_local_key(txn).await?) + .send_transaction_polling(&config.sign(txn).await?) .await?; if !no_cache { data::write(res.clone().try_into()?, &network.rpc_uri()?)?; diff --git a/cmd/soroban-cli/src/commands/contract/mod.rs b/cmd/soroban-cli/src/commands/contract/mod.rs index 761784de9..2997bcea3 100644 --- a/cmd/soroban-cli/src/commands/contract/mod.rs +++ b/cmd/soroban-cli/src/commands/contract/mod.rs @@ -144,7 +144,7 @@ impl Cmd { Cmd::Extend(extend) => extend.run().await?, Cmd::Alias(alias) => alias.run(global_args)?, Cmd::Deploy(deploy) => deploy.run(global_args).await?, - Cmd::Id(id) => id.run()?, + Cmd::Id(id) => id.run().await?, Cmd::Info(info) => info.run().await?, Cmd::Init(init) => init.run(global_args)?, Cmd::Inspect(inspect) => inspect.run()?, diff --git a/cmd/soroban-cli/src/commands/contract/read.rs b/cmd/soroban-cli/src/commands/contract/read.rs index 3cb253bb1..8398a281b 100644 --- a/cmd/soroban-cli/src/commands/contract/read.rs +++ b/cmd/soroban-cli/src/commands/contract/read.rs @@ -186,7 +186,7 @@ impl NetworkRunnable for Cmd { let network = config.get_network()?; tracing::trace!(?network); let client = Client::new(&network.rpc_url)?; - let keys = self.key.parse_keys(&config.locator, &network)?; + let keys = self.key.parse_keys(&config.sign_with.locator, &network)?; Ok(client.get_full_ledger_entries(&keys).await?) } } diff --git a/cmd/soroban-cli/src/commands/contract/restore.rs b/cmd/soroban-cli/src/commands/contract/restore.rs index e3e8e65ae..d3cdad6d0 100644 --- a/cmd/soroban-cli/src/commands/contract/restore.rs +++ b/cmd/soroban-cli/src/commands/contract/restore.rs @@ -133,18 +133,16 @@ impl NetworkRunnable for Cmd { let config = config.unwrap_or(&self.config); let network = config.get_network()?; tracing::trace!(?network); - let entry_keys = self.key.parse_keys(&config.locator, &network)?; + let entry_keys = self.key.parse_keys(&config.sign_with.locator, &network)?; let client = Client::new(&network.rpc_url)?; - let key = config.key_pair()?; // Get the account sequence number - let public_strkey = - stellar_strkey::ed25519::PublicKey(key.verifying_key().to_bytes()).to_string(); - let account_details = client.get_account(&public_strkey).await?; + let public_strkey = config.source_account().await?; + let account_details = client.get_account(&public_strkey.to_string()).await?; let sequence: i64 = account_details.seq_num.into(); let tx = Transaction { - source_account: MuxedAccount::Ed25519(Uint256(key.verifying_key().to_bytes())), + source_account: MuxedAccount::Ed25519(Uint256(public_strkey.0)), fee: self.fee.fee, seq_num: SequenceNumber(sequence + 1), cond: Preconditions::None, @@ -174,7 +172,7 @@ impl NetworkRunnable for Cmd { return Ok(TxnResult::Txn(tx)); } let res = client - .send_transaction_polling(&config.sign_with_local_key(tx).await?) + .send_transaction_polling(&config.sign(tx).await?) .await?; if args.map_or(true, |a| !a.no_cache) { data::write(res.clone().try_into()?, &network.rpc_uri()?)?; diff --git a/cmd/soroban-cli/src/commands/keys/add.rs b/cmd/soroban-cli/src/commands/keys/add.rs index d8f528bae..98aad4a9a 100644 --- a/cmd/soroban-cli/src/commands/keys/add.rs +++ b/cmd/soroban-cli/src/commands/keys/add.rs @@ -28,6 +28,6 @@ impl Cmd { pub fn run(&self) -> Result<(), Error> { Ok(self .config_locator - .write_identity(&self.name, &self.secrets.read_secret()?)?) + .write_identity(&self.name, &self.secrets.kind()?)?) } } diff --git a/cmd/soroban-cli/src/commands/keys/address.rs b/cmd/soroban-cli/src/commands/keys/address.rs index d13381b49..766e948eb 100644 --- a/cmd/soroban-cli/src/commands/keys/address.rs +++ b/cmd/soroban-cli/src/commands/keys/address.rs @@ -30,25 +30,16 @@ pub struct Cmd { } impl Cmd { - pub fn run(&self) -> Result<(), Error> { - println!("{}", self.public_key()?); + pub async fn run(&self) -> Result<(), Error> { + println!("{}", self.public_key().await?); Ok(()) } - pub fn private_key(&self) -> Result { + pub async fn public_key(&self) -> Result { Ok(self .locator - .read_identity(&self.name)? - .key_pair(self.hd_path)?) - } - - pub fn public_key(&self) -> Result { - if let Ok(key) = stellar_strkey::ed25519::PublicKey::from_string(&self.name) { - Ok(key) - } else { - Ok(stellar_strkey::ed25519::PublicKey::from_payload( - self.private_key()?.verifying_key().as_bytes(), - )?) - } + .account(&self.name)? + .public_key(self.hd_path) + .await?) } } diff --git a/cmd/soroban-cli/src/commands/keys/fund.rs b/cmd/soroban-cli/src/commands/keys/fund.rs index d7100c6cb..9660f4241 100644 --- a/cmd/soroban-cli/src/commands/keys/fund.rs +++ b/cmd/soroban-cli/src/commands/keys/fund.rs @@ -24,7 +24,7 @@ pub struct Cmd { impl Cmd { pub async fn run(&self) -> Result<(), Error> { - let addr = self.address.public_key()?; + let addr = self.address.public_key().await?; self.network .get(&self.address.locator)? .fund_address(&addr) diff --git a/cmd/soroban-cli/src/commands/keys/generate.rs b/cmd/soroban-cli/src/commands/keys/generate.rs index 16e6a7fdb..cd4071de4 100644 --- a/cmd/soroban-cli/src/commands/keys/generate.rs +++ b/cmd/soroban-cli/src/commands/keys/generate.rs @@ -62,7 +62,7 @@ impl Cmd { }; self.config_locator.write_identity(&self.name, &secret)?; if !self.no_fund { - let addr = secret.public_key(self.hd_path)?; + let addr = secret.public_key(self.hd_path).await?; let network = self.network.get(&self.config_locator)?; network .fund_address(&addr) diff --git a/cmd/soroban-cli/src/commands/keys/mod.rs b/cmd/soroban-cli/src/commands/keys/mod.rs index 42814092f..7ddb2c34d 100644 --- a/cmd/soroban-cli/src/commands/keys/mod.rs +++ b/cmd/soroban-cli/src/commands/keys/mod.rs @@ -51,7 +51,7 @@ impl Cmd { pub async fn run(&self) -> Result<(), Error> { match self { Cmd::Add(cmd) => cmd.run()?, - Cmd::Address(cmd) => cmd.run()?, + Cmd::Address(cmd) => cmd.run().await?, Cmd::Fund(cmd) => cmd.run().await?, Cmd::Generate(cmd) => cmd.run().await?, Cmd::Ls(cmd) => cmd.run()?, diff --git a/cmd/soroban-cli/src/commands/tx/auth.rs b/cmd/soroban-cli/src/commands/tx/auth.rs new file mode 100644 index 000000000..7c8989a1c --- /dev/null +++ b/cmd/soroban-cli/src/commands/tx/auth.rs @@ -0,0 +1,33 @@ +use clap::arg; + +use crate::rpc::{self, Client}; + +#[derive(Debug, clap::Args, Clone)] +#[group(skip)] +pub struct Args { + /// Number of ledgers from current ledger before the signed auth entry expires. Default 60 ~ 5 minutes. + #[arg(long, default_value = "60")] + pub auth_expires_in_ledgers: u32, + /// Ledger number when signed auth entry expires. + #[arg(long, conflicts_with = "auth_expires_in_ledgers")] + pub auth_expires_at_ledger: Option, +} + +impl Args { + pub async fn expiration_ledger(&self, client: &Client) -> Result { + if let Some(ledger) = self.auth_expires_at_ledger { + return Ok(ledger); + } + let current_ledger = client.get_latest_ledger().await?.sequence; + Ok(current_ledger + self.auth_expires_in_ledgers) + } +} + +impl Default for Args { + fn default() -> Self { + Self { + auth_expires_in_ledgers: 60, + auth_expires_at_ledger: None, + } + } +} diff --git a/cmd/soroban-cli/src/commands/tx/hash.rs b/cmd/soroban-cli/src/commands/tx/hash.rs index 8d8ec6d82..96748d4e2 100644 --- a/cmd/soroban-cli/src/commands/tx/hash.rs +++ b/cmd/soroban-cli/src/commands/tx/hash.rs @@ -1,13 +1,17 @@ use hex; -use crate::{commands::global, config::network, utils::transaction_hash}; +use crate::{ + commands::global, + config::network, + signer::{self, transaction_hash}, +}; #[derive(thiserror::Error, Debug)] pub enum Error { #[error(transparent)] TxEnvelopeFromStdin(#[from] super::xdr::Error), #[error(transparent)] - XdrToBase64(#[from] soroban_env_host::xdr::Error), + Signer(#[from] signer::Error), #[error(transparent)] Config(#[from] network::Error), } diff --git a/cmd/soroban-cli/src/commands/tx/mod.rs b/cmd/soroban-cli/src/commands/tx/mod.rs index 59f07228a..d5d36ead6 100644 --- a/cmd/soroban-cli/src/commands/tx/mod.rs +++ b/cmd/soroban-cli/src/commands/tx/mod.rs @@ -2,7 +2,10 @@ use clap::Parser; use super::global; +pub mod auth; pub mod hash; +pub mod send; +pub mod sign; pub mod simulate; pub mod xdr; @@ -12,16 +15,22 @@ pub enum Cmd { Simulate(simulate::Cmd), /// Calculate the hash of a transaction envelope from stdin Hash(hash::Cmd), + /// Sign a transaction envolope appending the signature to the envelope + Sign(sign::Cmd), + /// Send a transaction envelope to the network + Send(send::Cmd), } #[derive(thiserror::Error, Debug)] pub enum Error { - /// An error during the simulation #[error(transparent)] Simulate(#[from] simulate::Error), - /// An error during hash calculation #[error(transparent)] Hash(#[from] hash::Error), + #[error(transparent)] + Send(#[from] send::Error), + #[error(transparent)] + Sign(#[from] sign::Error), } impl Cmd { @@ -29,6 +38,8 @@ impl Cmd { match self { Cmd::Simulate(cmd) => cmd.run(global_args).await?, Cmd::Hash(cmd) => cmd.run(global_args)?, + Cmd::Sign(cmd) => cmd.run().await?, + Cmd::Send(cmd) => cmd.run(global_args).await?, }; Ok(()) } diff --git a/cmd/soroban-cli/src/commands/tx/send.rs b/cmd/soroban-cli/src/commands/tx/send.rs new file mode 100644 index 000000000..069434609 --- /dev/null +++ b/cmd/soroban-cli/src/commands/tx/send.rs @@ -0,0 +1,61 @@ +use async_trait::async_trait; +use soroban_rpc::GetTransactionResponse; + +use crate::commands::{global, NetworkRunnable}; +use crate::config::{self, locator, network}; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error(transparent)] + XdrArgs(#[from] super::xdr::Error), + #[error(transparent)] + Network(#[from] network::Error), + #[error(transparent)] + Locator(#[from] locator::Error), + #[error(transparent)] + Config(#[from] config::Error), + #[error(transparent)] + Rpc(#[from] crate::rpc::Error), + #[error(transparent)] + SerdeJson(#[from] serde_json::Error), +} + +#[derive(Debug, clap::Parser, Clone)] +#[group(skip)] +/// Command to send a transaction envelope to the network +/// e.g. `cat file.txt | soroban tx send` +pub struct Cmd { + #[clap(flatten)] + pub network: network::Args, + #[clap(flatten)] + pub locator: locator::Args, +} + +impl Cmd { + pub async fn run(&self, global_args: &global::Args) -> Result<(), Error> { + let response = self.run_against_rpc_server(Some(global_args), None).await?; + println!("{}", serde_json::to_string_pretty(&response)?); + Ok(()) + } +} + +#[async_trait] +impl NetworkRunnable for Cmd { + type Error = Error; + + type Result = GetTransactionResponse; + async fn run_against_rpc_server( + &self, + _: Option<&global::Args>, + config: Option<&config::Args>, + ) -> Result { + let network = if let Some(config) = config { + config.get_network()? + } else { + self.network.get(&self.locator)? + }; + let client = crate::rpc::Client::new(&network.rpc_url)?; + let tx_env = super::xdr::tx_envelope_from_stdin()?; + Ok(client.send_transaction_polling(&tx_env).await?) + } +} diff --git a/cmd/soroban-cli/src/commands/tx/sign.rs b/cmd/soroban-cli/src/commands/tx/sign.rs new file mode 100644 index 000000000..3f0b90139 --- /dev/null +++ b/cmd/soroban-cli/src/commands/tx/sign.rs @@ -0,0 +1,35 @@ +use crate::{ + config::sign_with, + xdr::{self, Limits, TransactionEnvelope, WriteXdr}, +}; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error(transparent)] + XdrArgs(#[from] super::xdr::Error), + #[error(transparent)] + SignWith(#[from] sign_with::Error), + #[error(transparent)] + Xdr(#[from] xdr::Error), +} + +#[derive(Debug, clap::Parser, Clone)] +#[group(skip)] +pub struct Cmd { + #[command(flatten)] + pub sign_with: sign_with::Args, +} + +impl Cmd { + #[allow(clippy::unused_async)] + pub async fn run(&self) -> Result<(), Error> { + let txn_env = super::xdr::tx_envelope_from_stdin()?; + let envelope = self.sign_tx_env(txn_env).await?; + println!("{}", envelope.to_xdr_base64(Limits::none())?.trim()); + Ok(()) + } + + pub async fn sign_tx_env(&self, tx: TransactionEnvelope) -> Result { + Ok(self.sign_with.sign_txn_env(tx).await?) + } +} diff --git a/cmd/soroban-cli/src/config/locator.rs b/cmd/soroban-cli/src/config/locator.rs index 86d1004f6..a6394ed9c 100644 --- a/cmd/soroban-cli/src/config/locator.rs +++ b/cmd/soroban-cli/src/config/locator.rs @@ -217,6 +217,14 @@ impl Args { KeyType::Identity.read_with_global(name, &self.local_config()?) } + pub fn account(&self, account_str: &str) -> Result { + if let Ok(signer) = account_str.parse::() { + Ok(signer) + } else { + self.read_identity(account_str) + } + } + pub fn read_network(&self, name: &str) -> Result { let res = KeyType::Network.read_with_global(name, &self.local_config()?); if let Err(Error::ConfigMissing(_, _)) = &res { diff --git a/cmd/soroban-cli/src/config/mod.rs b/cmd/soroban-cli/src/config/mod.rs index ef286f6a0..129a374fb 100644 --- a/cmd/soroban-cli/src/config/mod.rs +++ b/cmd/soroban-cli/src/config/mod.rs @@ -1,35 +1,33 @@ use std::path::PathBuf; use clap::{arg, command}; +use secret::StellarSigner; use serde::{Deserialize, Serialize}; -use soroban_rpc::Client; +use crate::signer; +use crate::xdr::{Transaction, TransactionEnvelope}; +use crate::Pwd; -use crate::{ - signer, - xdr::{Transaction, TransactionEnvelope}, - Pwd, -}; - -use self::{network::Network, secret::Secret}; +use self::network::Network; pub mod alias; pub mod data; pub mod locator; pub mod network; pub mod secret; +pub mod sign_with; pub mod upgrade_check; #[derive(thiserror::Error, Debug)] pub enum Error { #[error(transparent)] - Network(#[from] network::Error), + SignWith(#[from] sign_with::Error), #[error(transparent)] Secret(#[from] secret::Error), #[error(transparent)] - Config(#[from] locator::Error), + Network(#[from] network::Error), #[error(transparent)] - Rpc(#[from] soroban_rpc::Error), + Locator(#[from] locator::Error), #[error(transparent)] Signer(#[from] signer::Error), } @@ -37,79 +35,76 @@ pub enum Error { #[derive(Debug, clap::Args, Clone, Default)] #[group(skip)] pub struct Args { - #[command(flatten)] - pub network: network::Args, - #[arg(long, visible_alias = "source", env = "STELLAR_ACCOUNT")] - /// Account that signs the final transaction. Alias `source`. Can be an identity (--source alice), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). + /// Account where the final transaction originates from. + /// If no `--sign-with-*` flag is passed, passed key will also be used to sign the transaction. + /// Can be an identity (`--source alice`), a secret key (`--source SC36…`), or a seed phrase (`--source "kite urban…"`) pub source_account: String, - #[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` - pub hd_path: Option, - #[command(flatten)] - pub locator: locator::Args, + pub sign_with: sign_with::Args, } impl Args { - pub fn key_pair(&self) -> Result { - let key = self.account(&self.source_account)?; - Ok(key.key_pair(self.hd_path)?) + pub fn signer(&self) -> Result { + let (account, prompt) = self.sign_with.sign_with_key.as_ref().map_or_else( + || (&self.source_account, false), + |s| (s, !self.sign_with.yes), + ); + Ok(self + .sign_with + .locator + .account(account)? + .signer(self.sign_with.hd_path, prompt)?) } - pub async fn sign_with_local_key(&self, tx: Transaction) -> Result { - self.sign(tx).await + pub async fn source_account(&self) -> Result { + Ok(self + .sign_with + .locator + .account(&self.source_account)? + .public_key(self.sign_with.hd_path) + .await?) } - #[allow(clippy::unused_async)] pub async fn sign(&self, tx: Transaction) -> Result { - let key = self.key_pair()?; - let Network { - network_passphrase, .. - } = &self.get_network()?; - Ok(signer::sign_tx(&key, &tx, network_passphrase)?) + Ok(self + .sign_with + .sign_tx_env_with_signer(&self.signer()?, tx.into()) + .await?) } - pub async fn sign_soroban_authorizations( - &self, - tx: &Transaction, - signers: &[ed25519_dalek::SigningKey], - ) -> Result, Error> { - let network = self.get_network()?; - let source_key = self.key_pair()?; - let client = Client::new(&network.rpc_url)?; - let latest_ledger = client.get_latest_ledger().await?.sequence; - let seq_num = latest_ledger + 60; // ~ 5 min - Ok(signer::sign_soroban_authorizations( - tx, - &source_key, - signers, - seq_num, - &network.network_passphrase, - )?) + pub fn get_network(&self) -> Result { + Ok(self.sign_with.get_network()?) } - 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 config_dir(&self) -> Result { + Ok(self.sign_with.config_dir()?) } - pub fn get_network(&self) -> Result { - Ok(self.network.get(&self.locator)?) + pub fn resolve_contract_id( + &self, + contract_id: &str, + ) -> Result { + Ok(self + .sign_with + .locator + .resolve_contract_id(contract_id, &self.get_network()?.network_passphrase)?) } - pub fn config_dir(&self) -> Result { - Ok(self.locator.config_dir()?) + pub fn save_contract_id(&self, contract_id: &str, alias: &str) -> Result<(), Error> { + self.sign_with.locator.save_contract_id( + &self.get_network()?.network_passphrase, + contract_id, + alias, + )?; + Ok(()) } } impl Pwd for Args { fn set_pwd(&mut self, pwd: &std::path::Path) { - self.locator.set_pwd(pwd); + self.sign_with.locator.set_pwd(pwd); } } diff --git a/cmd/soroban-cli/src/config/secret.rs b/cmd/soroban-cli/src/config/secret.rs index b5b1dd747..0418a8bf5 100644 --- a/cmd/soroban-cli/src/config/secret.rs +++ b/cmd/soroban-cli/src/config/secret.rs @@ -3,7 +3,10 @@ use serde::{Deserialize, Serialize}; use std::{io::Write, str::FromStr}; use stellar_strkey::ed25519::{PrivateKey, PublicKey}; -use crate::utils; +use crate::{ + signer::{self, LocalKey, Stellar}, + utils, +}; #[derive(thiserror::Error, Debug)] pub enum Error { @@ -21,6 +24,8 @@ pub enum Error { Ed25519(#[from] ed25519_dalek::SignatureError), #[error("Invalid address {0}")] InvalidAddress(String), + #[error(transparent)] + Stellar(#[from] signer::Error), } #[derive(Debug, clap::Args, Clone)] @@ -36,7 +41,7 @@ pub struct Args { } impl Args { - pub fn read_secret(&self) -> Result { + pub fn kind(&self) -> Result { if let Ok(secret_key) = std::env::var("SOROBAN_SECRET_KEY") { Ok(Secret::SecretKey { secret_key }) } else if self.secret_key { @@ -113,11 +118,17 @@ impl Secret { }) } - pub fn public_key(&self, index: Option) -> Result { - let key = self.key_pair(index)?; - Ok(stellar_strkey::ed25519::PublicKey::from_payload( - key.verifying_key().as_bytes(), - )?) + pub async fn public_key(&self, index: Option) -> Result { + let key = self.signer(index, true)?; + Ok(key.get_public_key().await?) + } + + pub fn signer(&self, index: Option, prompt: bool) -> Result { + match self { + Secret::SecretKey { .. } | Secret::SeedPhrase { .. } => Ok(StellarSigner::Local( + LocalKey::new(self.key_pair(index)?, prompt), + )), + } } pub fn key_pair(&self, index: Option) -> Result { @@ -140,6 +151,25 @@ impl Secret { } } +pub enum StellarSigner { + Local(LocalKey), +} + +#[async_trait::async_trait] +impl Stellar for StellarSigner { + async fn get_public_key(&self) -> Result { + match self { + StellarSigner::Local(signer) => signer.get_public_key().await, + } + } + + async fn sign_blob(&self, blob: &[u8]) -> Result, signer::Error> { + match self { + StellarSigner::Local(signer) => signer.sign_blob(blob).await, + } + } +} + fn read_password() -> Result { std::io::stdout().flush().map_err(|_| Error::PasswordRead)?; rpassword::read_password().map_err(|_| Error::PasswordRead) diff --git a/cmd/soroban-cli/src/config/sign_with.rs b/cmd/soroban-cli/src/config/sign_with.rs new file mode 100644 index 000000000..c31580eef --- /dev/null +++ b/cmd/soroban-cli/src/config/sign_with.rs @@ -0,0 +1,98 @@ +use std::path::PathBuf; + +use crate::{ + signer::{self, sign_txn_env, Stellar}, + xdr::TransactionEnvelope, +}; +use clap::arg; + +use super::{ + locator, + network::{self, Network}, + secret::{self, StellarSigner}, +}; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error(transparent)] + Network(#[from] network::Error), + #[error(transparent)] + Signer(#[from] signer::Error), + #[error(transparent)] + Secret(#[from] secret::Error), + #[error(transparent)] + Locator(#[from] locator::Error), + #[error(transparent)] + Rpc(#[from] soroban_rpc::Error), + #[error("No sign with key provided")] + NoSignWithKey, +} + +#[derive(Debug, clap::Args, Clone, Default)] +#[group(skip)] +pub struct Args { + /// Sign with a local key. Can be an identity (--sign-with-key alice), a secret key (--sign-with-key SC36…), or a seed phrase (--sign-with-key "kite urban…"). If using seed phrase, `--hd-path` defaults to the `0` path. + #[arg( + long, + conflicts_with = "sign_with_lab", + env = "STELLAR_SIGN_WITH_SECRET" + )] + pub sign_with_key: Option, + /// Sign with labratory + #[arg( + long, + conflicts_with = "sign_with_key", + env = "STELLAR_SIGN_WITH_LABRATORY", + hide = true + )] + pub sign_with_lab: bool, + + #[arg(long, conflicts_with = "sign_with_lab")] + /// If using a seed phrase to sign, sets which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` + pub hd_path: Option, + + /// If one of `--sign-with-*` flags is provided, don't ask to confirm to sign a transaction + #[arg(long)] + pub yes: bool, + + #[command(flatten)] + pub network: network::Args, + + #[command(flatten)] + pub locator: locator::Args, +} + +impl Args { + pub fn signer(&self) -> Result { + let account = self.sign_with_key.as_deref().ok_or(Error::NoSignWithKey)?; + Ok(self + .locator + .account(account)? + .signer(self.hd_path, !self.yes)?) + } + + pub async fn sign_txn_env( + &self, + tx: TransactionEnvelope, + ) -> Result { + let signer = self.signer()?; + self.sign_tx_env_with_signer(&signer, tx).await + } + + pub async fn sign_tx_env_with_signer( + &self, + signer: &(impl Stellar + std::marker::Sync), + tx_env: TransactionEnvelope, + ) -> Result { + let network = self.get_network()?; + Ok(sign_txn_env(signer, tx_env, &network).await?) + } + + pub fn get_network(&self) -> Result { + Ok(self.network.get(&self.locator)?) + } + + pub fn config_dir(&self) -> Result { + Ok(self.locator.config_dir()?) + } +} diff --git a/cmd/soroban-cli/src/signer.rs b/cmd/soroban-cli/src/signer.rs index 580a61a5e..4f4a3b885 100644 --- a/cmd/soroban-cli/src/signer.rs +++ b/cmd/soroban-cli/src/signer.rs @@ -1,15 +1,19 @@ +use crossterm::event::{read, Event, KeyCode}; use ed25519_dalek::ed25519::signature::Signer; use sha2::{Digest, Sha256}; -use soroban_env_host::xdr::{ - self, AccountId, DecoratedSignature, Hash, HashIdPreimage, HashIdPreimageSorobanAuthorization, - InvokeHostFunctionOp, Limits, Operation, OperationBody, PublicKey, ScAddress, ScMap, ScSymbol, - ScVal, Signature, SignatureHint, SorobanAddressCredentials, SorobanAuthorizationEntry, - SorobanAuthorizedFunction, SorobanCredentials, Transaction, TransactionEnvelope, - TransactionSignaturePayload, TransactionSignaturePayloadTaggedTransaction, - TransactionV1Envelope, Uint256, WriteXdr, +use crate::{ + config::network::Network, + xdr::{ + self, DecoratedSignature, InvokeHostFunctionOp, Limits, Operation, OperationBody, + Signature, SignatureHint, SorobanAuthorizedFunction, Transaction, TransactionEnvelope, + TransactionSignaturePayload, TransactionSignaturePayloadTaggedTransaction, + TransactionV1Envelope, WriteXdr, + }, }; +pub mod auth; + #[derive(thiserror::Error, Debug)] pub enum Error { #[error("Contract addresses are not supported to sign auth entries {address}")] @@ -19,14 +23,16 @@ pub enum Error { #[error("Missing signing key for account {address}")] MissingSignerForAddress { address: String }, #[error(transparent)] - TryFromSlice(#[from] std::array::TryFromSliceError), - #[error("User cancelled signing, perhaps need to add -y")] - UserCancelledSigning, - #[error(transparent)] Xdr(#[from] xdr::Error), + #[error(transparent)] + Rpc(#[from] crate::rpc::Error), + #[error("User cancelled signing, perhaps need to remove --check")] + UserCancelledSigning, + #[error("Only Transaction envelope V1 type is supported")] + UnsupportedTransactionEnvelopeType, } -fn requires_auth(txn: &Transaction) -> Option { +pub fn extract_auth_operation(txn: &Transaction) -> Option { let [op @ Operation { body: OperationBody::InvokeHostFunction(InvokeHostFunctionOp { auth, .. }), .. @@ -41,177 +47,126 @@ fn requires_auth(txn: &Transaction) -> Option { .then(move || op.clone()) } -// Use the given source_key and signers, to sign all SorobanAuthorizationEntry's in the given -// transaction. If unable to sign, return an error. -pub fn sign_soroban_authorizations( - raw: &Transaction, - source_key: &ed25519_dalek::SigningKey, - signers: &[ed25519_dalek::SigningKey], - signature_expiration_ledger: u32, - network_passphrase: &str, -) -> Result, Error> { - let mut tx = raw.clone(); - let Some(mut op) = requires_auth(&tx) else { - return Ok(None); - }; - - let Operation { - body: OperationBody::InvokeHostFunction(ref mut body), - .. - } = op - else { - return Ok(None); +/// Calculate the hash of a Transaction +pub fn transaction_hash(txn: &Transaction, network_passphrase: &str) -> Result<[u8; 32], Error> { + let signature_payload = TransactionSignaturePayload { + network_id: hash(network_passphrase), + tagged_transaction: TransactionSignaturePayloadTaggedTransaction::Tx(txn.clone()), }; + let hash = Sha256::digest(signature_payload.to_xdr(Limits::none())?).into(); + Ok(hash) +} - let network_id = Hash(Sha256::digest(network_passphrase.as_bytes()).into()); - - let verification_key = source_key.verifying_key(); - let source_address = verification_key.as_bytes(); - - let signed_auths = body - .auth - .as_slice() - .iter() - .map(|raw_auth| { - let mut auth = raw_auth.clone(); - let SorobanAuthorizationEntry { - credentials: SorobanCredentials::Address(ref mut credentials), - .. - } = auth - else { - // Doesn't need special signing - return Ok(auth); - }; - let SorobanAddressCredentials { ref address, .. } = credentials; - - // See if we have a signer for this authorizationEntry - // If not, then we Error - let needle = match address { - ScAddress::Account(AccountId(PublicKey::PublicKeyTypeEd25519(Uint256(ref a)))) => a, - ScAddress::Contract(Hash(c)) => { - // This address is for a contract. This means we're using a custom - // smart-contract account. Currently the CLI doesn't support that yet. - return Err(Error::MissingSignerForAddress { - address: stellar_strkey::Strkey::Contract(stellar_strkey::Contract(*c)) - .to_string(), - }); - } - }; - let signer = if let Some(s) = signers - .iter() - .find(|s| needle == s.verifying_key().as_bytes()) - { - s - } else if needle == source_address { - // This is the source address, so we can sign it - source_key - } else { - // We don't have a signer for this address - return Err(Error::MissingSignerForAddress { - address: stellar_strkey::Strkey::PublicKeyEd25519( - stellar_strkey::ed25519::PublicKey(*needle), - ) - .to_string(), - }); - }; - - sign_soroban_authorization_entry( - raw_auth, - signer, - signature_expiration_ledger, - &network_id, - ) +/// A trait for signing Stellar transactions and Soroban authorization entries +#[async_trait::async_trait] +pub trait Stellar { + /// Currently only supports ed25519 keys + async fn get_public_key(&self) -> Result; + + /// Sign an abritatry byte array + async fn sign_blob(&self, blob: &[u8]) -> Result, Error>; + + /// Sign a transaction hash with the given source account + /// # Errors + /// Returns an error if the source account is not found + async fn sign_txn_hash(&self, txn: [u8; 32]) -> Result { + let source_account = self.get_public_key().await?; + eprintln!( + "{} about to sign hash: {}", + source_account.to_string(), + hex::encode(txn) + ); + let tx_signature = self.sign_blob(&txn).await?; + Ok(DecoratedSignature { + // TODO: remove this unwrap. It's safe because we know the length of the array + hint: SignatureHint(source_account.0[28..].try_into().unwrap()), + signature: Signature(tx_signature.try_into()?), }) - .collect::, Error>>()?; + } + + /// Sign a Stellar transaction with the given source account + /// This is a default implementation that signs the transaction hash and returns a decorated signature + /// + /// Todo: support signing the transaction directly. + /// # Errors + /// Returns an error if the source account is not found + async fn sign_txn( + &self, + txn: &Transaction, + Network { + network_passphrase, .. + }: &Network, + ) -> Result { + let hash = transaction_hash(txn, network_passphrase)?; + self.sign_txn_hash(hash).await + } +} - body.auth = signed_auths.try_into()?; - tx.operations = vec![op].try_into()?; - Ok(Some(tx)) +pub async fn sign_txn_env( + signer: &(impl Stellar + std::marker::Sync), + txn_env: TransactionEnvelope, + network: &Network, +) -> Result { + match txn_env { + TransactionEnvelope::Tx(TransactionV1Envelope { tx, signatures }) => { + let decorated_signature = signer.sign_txn(&tx, network).await?; + let mut sigs = signatures.to_vec(); + sigs.push(decorated_signature); + Ok(TransactionEnvelope::Tx(TransactionV1Envelope { + tx, + signatures: sigs.try_into()?, + })) + } + _ => Err(Error::UnsupportedTransactionEnvelopeType), + } } -fn sign_soroban_authorization_entry( - raw: &SorobanAuthorizationEntry, - signer: &ed25519_dalek::SigningKey, - signature_expiration_ledger: u32, - network_id: &Hash, -) -> Result { - let mut auth = raw.clone(); - let SorobanAuthorizationEntry { - credentials: SorobanCredentials::Address(ref mut credentials), - .. - } = auth - else { - // Doesn't need special signing - return Ok(auth); - }; - let SorobanAddressCredentials { nonce, .. } = credentials; - - let preimage = HashIdPreimage::SorobanAuthorization(HashIdPreimageSorobanAuthorization { - network_id: network_id.clone(), - invocation: auth.root_invocation.clone(), - nonce: *nonce, - signature_expiration_ledger, - }) - .to_xdr(Limits::none())?; - - let payload = Sha256::digest(preimage); - let signature = signer.sign(&payload); - - let map = ScMap::sorted_from(vec![ - ( - ScVal::Symbol(ScSymbol("public_key".try_into()?)), - ScVal::Bytes( - signer - .verifying_key() - .to_bytes() - .to_vec() - .try_into() - .map_err(Error::Xdr)?, - ), - ), - ( - ScVal::Symbol(ScSymbol("signature".try_into()?)), - ScVal::Bytes( - signature - .to_bytes() - .to_vec() - .try_into() - .map_err(Error::Xdr)?, - ), - ), - ]) - .map_err(Error::Xdr)?; - credentials.signature = ScVal::Vec(Some( - vec![ScVal::Map(Some(map))].try_into().map_err(Error::Xdr)?, - )); - credentials.signature_expiration_ledger = signature_expiration_ledger; - auth.credentials = SorobanCredentials::Address(credentials.clone()); - Ok(auth) +pub(crate) fn hash(network_passphrase: &str) -> xdr::Hash { + xdr::Hash(Sha256::digest(network_passphrase.as_bytes()).into()) } -pub fn sign_tx( - key: &ed25519_dalek::SigningKey, - tx: &Transaction, - network_passphrase: &str, -) -> Result { - let tx_hash = hash(tx, network_passphrase)?; - let tx_signature = key.sign(&tx_hash); +pub struct LocalKey { + key: ed25519_dalek::SigningKey, + prompt: bool, +} - let decorated_signature = DecoratedSignature { - hint: SignatureHint(key.verifying_key().to_bytes()[28..].try_into()?), - signature: Signature(tx_signature.to_bytes().try_into()?), - }; +impl LocalKey { + pub fn new(key: ed25519_dalek::SigningKey, prompt: bool) -> Self { + Self { key, prompt } + } +} - Ok(TransactionEnvelope::Tx(TransactionV1Envelope { - tx: tx.clone(), - signatures: [decorated_signature].try_into()?, - })) +#[async_trait::async_trait] +impl Stellar for LocalKey { + async fn sign_blob(&self, data: &[u8]) -> Result, Error> { + if self.prompt { + eprintln!("Press 'y' or 'Y' for yes, any other key for no:"); + match read_key() { + 'y' | 'Y' => { + eprintln!("Signing now..."); + } + _ => return Err(Error::UserCancelledSigning), + }; + } + let sig = self.key.sign(data); + Ok(sig.to_bytes().to_vec()) + } + + async fn get_public_key(&self) -> Result { + Ok(stellar_strkey::ed25519::PublicKey( + self.key.verifying_key().to_bytes(), + )) + } } -pub fn 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()), - }; - Ok(Sha256::digest(signature_payload.to_xdr(Limits::none())?).into()) +pub fn read_key() -> char { + loop { + if let Event::Key(key) = read().unwrap() { + match key.code { + KeyCode::Char(c) => return c, + KeyCode::Esc => return '\x1b', // escape key + _ => (), + } + } + } } diff --git a/cmd/soroban-cli/src/signer/auth.rs b/cmd/soroban-cli/src/signer/auth.rs new file mode 100644 index 000000000..e404aff8f --- /dev/null +++ b/cmd/soroban-cli/src/signer/auth.rs @@ -0,0 +1,141 @@ +use sha2::{Digest, Sha256}; + +use crate::{ + config::network::Network, + xdr::{ + self, AccountId, Hash, HashIdPreimage, HashIdPreimageSorobanAuthorization, Limits, + OperationBody, PublicKey, ScAddress, ScMap, ScSymbol, ScVal, SorobanAddressCredentials, + SorobanAuthorizationEntry, SorobanCredentials, Transaction, Uint256, WriteXdr, + }, +}; + +use super::{extract_auth_operation, hash, Error, Stellar}; + +/// Sign a Soroban authorization entries for a given transaction and set the expiration ledger +/// # Errors +/// Returns an error if the address is not found +pub async fn sign_soroban_authorizations( + signer: &impl Stellar, + raw: &Transaction, + network: &Network, + expiration_ledger: u32, +) -> Result, Error> { + let mut tx = raw.clone(); + let Some(mut op) = extract_auth_operation(&tx) else { + return Ok(None); + }; + + let xdr::Operation { + body: OperationBody::InvokeHostFunction(ref mut body), + .. + } = op + else { + return Ok(None); + }; + let mut auths = body.auth.to_vec(); + for auth in &mut auths { + *auth = maybe_sign_soroban_authorization_entry(signer, auth, network, expiration_ledger) + .await?; + } + body.auth = auths.try_into()?; + tx.operations = [op].try_into()?; + Ok(Some(tx)) +} + +/// Sign a Soroban authorization entry if the address is public key +/// # Errors +/// Returns an error if the address in entry is a contract +pub async fn maybe_sign_soroban_authorization_entry( + signer: &impl Stellar, + unsigned_entry: &SorobanAuthorizationEntry, + network: &Network, + expiration_ledger: u32, +) -> Result { + if let SorobanAuthorizationEntry { + credentials: SorobanCredentials::Address(SorobanAddressCredentials { address, .. }), + .. + } = unsigned_entry + { + // See if we have a signer for this authorizationEntry + // If not, then we Error + let key = match address { + ScAddress::Account(AccountId(PublicKey::PublicKeyTypeEd25519(Uint256(a)))) => { + stellar_strkey::ed25519::PublicKey(*a) + } + ScAddress::Contract(Hash(c)) => { + // This address is for a contract. This means we're using a custom + // smart-contract account. Currently the CLI doesn't support that yet. + return Err(Error::MissingSignerForAddress { + address: stellar_strkey::Strkey::Contract(stellar_strkey::Contract(*c)) + .to_string(), + }); + } + }; + if key == signer.get_public_key().await? { + return sign_soroban_authorization_entry( + signer, + unsigned_entry, + network, + expiration_ledger, + ) + .await; + } + } + Ok(unsigned_entry.clone()) +} + +/// Sign a Soroban authorization entry with the given address +/// # Errors +/// Returns an error if the address is not found +pub async fn sign_soroban_authorization_entry( + signer: &impl Stellar, + unsigned_entry: &SorobanAuthorizationEntry, + Network { + network_passphrase, .. + }: &Network, + expiration_ledger: u32, +) -> Result { + let address = signer.get_public_key().await?; + let mut auth = unsigned_entry.clone(); + let SorobanAuthorizationEntry { + credentials: SorobanCredentials::Address(ref mut credentials), + .. + } = auth + else { + // Doesn't need special signing + return Ok(auth); + }; + let SorobanAddressCredentials { + nonce, + signature_expiration_ledger, + .. + } = credentials; + + *signature_expiration_ledger = expiration_ledger; + + let preimage = HashIdPreimage::SorobanAuthorization(HashIdPreimageSorobanAuthorization { + network_id: hash(network_passphrase), + invocation: auth.root_invocation.clone(), + nonce: *nonce, + signature_expiration_ledger: *signature_expiration_ledger, + }) + .to_xdr(Limits::none())?; + + let payload = Sha256::digest(preimage); + let signature = signer.sign_blob(&payload).await?; + + let map = ScMap::sorted_from(vec![ + ( + ScVal::Symbol(ScSymbol("public_key".try_into()?)), + ScVal::Bytes(address.0.to_vec().try_into()?), + ), + ( + ScVal::Symbol(ScSymbol("signature".try_into()?)), + ScVal::Bytes(signature.try_into()?), + ), + ])?; + credentials.signature = ScVal::Vec(Some(vec![ScVal::Map(Some(map))].try_into()?)); + auth.credentials = SorobanCredentials::Address(credentials.clone()); + + Ok(auth) +} diff --git a/cmd/soroban-cli/src/utils.rs b/cmd/soroban-cli/src/utils.rs index f8ebb8b37..f5827f75b 100644 --- a/cmd/soroban-cli/src/utils.rs +++ b/cmd/soroban-cli/src/utils.rs @@ -1,13 +1,11 @@ -use ed25519_dalek::Signer; use phf::phf_map; use sha2::{Digest, Sha256}; use stellar_strkey::ed25519::PrivateKey; use soroban_env_host::xdr::{ - Asset, ContractIdPreimage, DecoratedSignature, Error as XdrError, Hash, HashIdPreimage, - HashIdPreimageContractId, Limits, ScMap, ScMapEntry, ScVal, Signature, SignatureHint, - Transaction, TransactionEnvelope, TransactionSignaturePayload, - TransactionSignaturePayloadTaggedTransaction, TransactionV1Envelope, WriteXdr, + Asset, ContractIdPreimage, Error as XdrError, Hash, HashIdPreimage, HashIdPreimageContractId, + Limits, ScMap, ScMapEntry, ScVal, Transaction, TransactionSignaturePayload, + TransactionSignaturePayloadTaggedTransaction, WriteXdr, }; pub use soroban_spec_tools::contract as contract_spec; @@ -49,28 +47,6 @@ pub fn explorer_url_for_contract(network: &Network, contract_id: &str) -> Option .map(|base_url| format!("{base_url}/contract/{contract_id}")) } -/// # Errors -/// -/// Might return an error -pub fn sign_transaction( - key: &ed25519_dalek::SigningKey, - tx: &Transaction, - network_passphrase: &str, -) -> Result { - let tx_hash = transaction_hash(tx, network_passphrase)?; - let tx_signature = key.sign(&tx_hash); - - let decorated_signature = DecoratedSignature { - hint: SignatureHint(key.verifying_key().to_bytes()[28..].try_into()?), - signature: Signature(tx_signature.to_bytes().try_into()?), - }; - - Ok(TransactionEnvelope::Tx(TransactionV1Envelope { - tx: tx.clone(), - signatures: vec![decorated_signature].try_into()?, - })) -} - /// # Errors /// /// Might return an error