From eb0495580b2b8eaca5c46942d99dddfa932d7cf0 Mon Sep 17 00:00:00 2001 From: Willem Wyndham Date: Mon, 24 Jun 2024 16:12:23 -0400 Subject: [PATCH] feat: add signer to other commands and ban keys named "ledger" --- FULL_HELP_DOCS.md | 100 +++++++++++- cmd/crates/soroban-test/src/lib.rs | 4 +- cmd/crates/soroban-test/tests/it/config.rs | 30 ++++ cmd/crates/soroban-test/tests/it/util.rs | 6 +- .../src/commands/config/locator.rs | 40 ++++- cmd/soroban-cli/src/commands/config/mod.rs | 68 ++++---- cmd/soroban-cli/src/commands/config/secret.rs | 85 +++++++--- .../src/commands/contract/deploy/asset.rs | 2 +- .../src/commands/contract/deploy/wasm.rs | 2 +- .../src/commands/contract/extend.rs | 2 +- .../src/commands/contract/install.rs | 2 +- .../src/commands/contract/invoke.rs | 149 ++++++++++-------- .../src/commands/contract/restore.rs | 2 +- cmd/soroban-cli/src/commands/keys/add.rs | 2 +- cmd/soroban-cli/src/commands/keys/address.rs | 21 +-- cmd/soroban-cli/src/commands/keys/fund.rs | 2 +- cmd/soroban-cli/src/commands/keys/generate.rs | 11 +- cmd/soroban-cli/src/commands/keys/mod.rs | 2 +- 18 files changed, 373 insertions(+), 157 deletions(-) diff --git a/FULL_HELP_DOCS.md b/FULL_HELP_DOCS.md index 0842b6d98e..ad1aa34352 100644 --- a/FULL_HELP_DOCS.md +++ b/FULL_HELP_DOCS.md @@ -58,6 +58,8 @@ This document contains the help content for the `stellar` command-line program. * [`stellar version`↴](#stellar-version) * [`stellar tx`↴](#stellar-tx) * [`stellar tx simulate`↴](#stellar-tx-simulate) +* [`stellar tx sign`↴](#stellar-tx-sign) +* [`stellar tx send`↴](#stellar-tx-send) * [`stellar cache`↴](#stellar-cache) * [`stellar cache clean`↴](#stellar-cache-clean) * [`stellar cache path`↴](#stellar-cache-path) @@ -217,6 +219,10 @@ Get Id of builtin Soroban Asset Contract. Deprecated, use `stellar contract id a Possible values: `true`, `false` * `--config-dir ` — Location of config directory, default is "." +* `--check` — Check with user before signature. Eventually this will be replaced with `--yes`, which does the opposite and will force a check without --yes + + Possible values: `true`, `false` + @@ -239,6 +245,10 @@ Deploy builtin Soroban Asset Contract Possible values: `true`, `false` * `--config-dir ` — Location of config directory, default is "." +* `--check` — Check with user before signature. Eventually this will be replaced with `--yes`, which does the opposite and will force a check without --yes + + Possible values: `true`, `false` + * `--fee ` — fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm Default value: `100` @@ -398,6 +408,10 @@ If no keys are specified the contract itself is extended. Possible values: `true`, `false` * `--config-dir ` — Location of config directory, default is "." +* `--check` — Check with user before signature. Eventually this will be replaced with `--yes`, which does the opposite and will force a check without --yes + + Possible values: `true`, `false` + * `--fee ` — fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm Default value: `100` @@ -438,6 +452,10 @@ Deploy a wasm contract Possible values: `true`, `false` * `--config-dir ` — Location of config directory, default is "." +* `--check` — Check with user before signature. Eventually this will be replaced with `--yes`, which does the opposite and will force a check without --yes + + Possible values: `true`, `false` + * `--fee ` — fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm Default value: `100` @@ -517,6 +535,10 @@ Deploy builtin Soroban Asset Contract Possible values: `true`, `false` * `--config-dir ` — Location of config directory, default is "." +* `--check` — Check with user before signature. Eventually this will be replaced with `--yes`, which does the opposite and will force a check without --yes + + Possible values: `true`, `false` + @@ -539,6 +561,10 @@ Deploy normal Wasm Contract Possible values: `true`, `false` * `--config-dir ` — Location of config directory, default is "." +* `--check` — Check with user before signature. Eventually this will be replaced with `--yes`, which does the opposite and will force a check without --yes + + Possible values: `true`, `false` + @@ -611,6 +637,10 @@ Install a WASM file to the ledger without creating a contract instance Possible values: `true`, `false` * `--config-dir ` — Location of config directory, default is "." +* `--check` — Check with user before signature. Eventually this will be replaced with `--yes`, which does the opposite and will force a check without --yes + + Possible values: `true`, `false` + * `--fee ` — fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm Default value: `100` @@ -668,6 +698,10 @@ stellar contract invoke ... -- --help Possible values: `true`, `false` * `--config-dir ` — Location of config directory, default is "." +* `--check` — Check with user before signature. Eventually this will be replaced with `--yes`, which does the opposite and will force a check without --yes + + Possible values: `true`, `false` + * `--fee ` — fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm Default value: `100` @@ -745,6 +779,10 @@ Print the current value of a contract-data ledger entry Possible values: `true`, `false` * `--config-dir ` — Location of config directory, default is "." +* `--check` — Check with user before signature. Eventually this will be replaced with `--yes`, which does the opposite and will force a check without --yes + + Possible values: `true`, `false` + @@ -788,6 +826,10 @@ If no keys are specificed the contract itself is restored. Possible values: `true`, `false` * `--config-dir ` — Location of config directory, default is "." +* `--check` — Check with user before signature. Eventually this will be replaced with `--yes`, which does the opposite and will force a check without --yes + + Possible values: `true`, `false` + * `--fee ` — fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm Default value: `100` @@ -1436,6 +1478,8 @@ Sign, Simulate, and Send transactions ###### **Subcommands:** * `simulate` — Simulate a transaction envelope from stdin +* `sign` — Sign a transaction with a ledger or local key +* `send` — Send a transaction envelope to the network @@ -1450,13 +1494,67 @@ 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 signs the final transaction. Alias `source`. Can be an identity (--source alice), a secret key (--source SC36…), or a seed phrase (--source "kite urban…") +* `--source-account ` — Account that signs the final transaction. Alias `source`. Can be an identity (--source alice), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). Default: `identity generate --default-seed` +* `--hd-path ` — If using a seed phrase, which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` +* `--global` — Use global config + + Possible values: `true`, `false` + +* `--config-dir ` — Location of config directory, default is "." +* `--check` — Check with user before signature. Eventually this will be replaced with `--yes`, which does the opposite and will force a check without --yes + + Possible values: `true`, `false` + + + + +## `stellar tx sign` + +Sign a transaction with a ledger or local key + +**Usage:** `stellar tx sign [OPTIONS] --source-account ` + +###### **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 +* `--source-account ` — Account that signs the final transaction. Alias `source`. Can be an identity (--source alice), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). Default: `identity generate --default-seed` * `--hd-path ` — If using a seed phrase, which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` * `--global` — Use global config Possible values: `true`, `false` * `--config-dir ` — Location of config directory, default is "." +* `--check` — Check with user before signature. Eventually this will be replaced with `--yes`, which does the opposite and will force a check without --yes + + Possible values: `true`, `false` + + + + +## `stellar tx send` + +Send a transaction envelope to the network + +**Usage:** `stellar tx send [OPTIONS] --source-account ` + +###### **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 +* `--source-account ` — Account that signs the final transaction. Alias `source`. Can be an identity (--source alice), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). Default: `identity generate --default-seed` +* `--hd-path ` — If using a seed phrase, which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` +* `--global` — Use global config + + Possible values: `true`, `false` + +* `--config-dir ` — Location of config directory, default is "." +* `--check` — Check with user before signature. Eventually this will be replaced with `--yes`, which does the opposite and will force a check without --yes + + Possible values: `true`, `false` + diff --git a/cmd/crates/soroban-test/src/lib.rs b/cmd/crates/soroban-test/src/lib.rs index 8df6a97c08..9f45decbaf 100644 --- a/cmd/crates/soroban-test/src/lib.rs +++ b/cmd/crates/soroban-test/src/lib.rs @@ -232,6 +232,7 @@ impl TestEnv { config_dir, }, hd_path: None, + check: false, } } @@ -263,9 +264,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/config.rs b/cmd/crates/soroban-test/tests/it/config.rs index dd33713aa1..598f65ec05 100644 --- a/cmd/crates/soroban-test/tests/it/config.rs +++ b/cmd/crates/soroban-test/tests/it/config.rs @@ -153,6 +153,36 @@ fn read_key() { .stdout(predicates::str::contains("test_id\n")); } +#[test] +fn cannot_generate_ledger_key() { + let sandbox = TestEnv::default(); + sandbox + .new_assert_cmd("keys") + .arg("generate") + .arg("ledger") + .assert() + .stdout("") + .stderr("error: Cannot name a Key ledger\n") + .failure(); +} + +#[test] +fn cannot_add_ledger_key() { + let sandbox = TestEnv::default(); + sandbox + .new_assert_cmd("keys") + .env( + "SOROBAN_SECRET_KEY", + "SDIY6AQQ75WMD4W46EYB7O6UYMHOCGQHLAQGQTKHDX4J2DYQCHVCQYFD", + ) + .arg("add") + .arg("ledger") + .assert() + .stdout("") + .stderr("error: Cannot name a Key ledger\n") + .failure(); +} + #[test] fn generate_key() { let sandbox = TestEnv::default(); diff --git a/cmd/crates/soroban-test/tests/it/util.rs b/cmd/crates/soroban-test/tests/it/util.rs index 6bf5f8c225..b09c9657ec 100644 --- a/cmd/crates/soroban-test/tests/it/util.rs +++ b/cmd/crates/soroban-test/tests/it/util.rs @@ -1,7 +1,7 @@ use std::path::Path; use soroban_cli::commands::{ - config::{locator::KeyType, secret::Secret}, + config::{locator::KeyType, secret::SignerKind}, contract, }; use soroban_test::{TestEnv, Wasm, TEST_ACCOUNT}; @@ -17,10 +17,10 @@ pub enum SecretKind { #[allow(clippy::needless_pass_by_value)] pub fn add_key(dir: &Path, name: &str, kind: SecretKind, data: &str) { let secret = match kind { - SecretKind::Seed => Secret::SeedPhrase { + SecretKind::Seed => SignerKind::SeedPhrase { seed_phrase: data.to_string(), }, - SecretKind::Key => Secret::SecretKey { + SecretKind::Key => SignerKind::SecretKey { secret_key: data.to_string(), }, }; diff --git a/cmd/soroban-cli/src/commands/config/locator.rs b/cmd/soroban-cli/src/commands/config/locator.rs index d32ff5e1d2..f18bc3acab 100644 --- a/cmd/soroban-cli/src/commands/config/locator.rs +++ b/cmd/soroban-cli/src/commands/config/locator.rs @@ -13,7 +13,7 @@ use stellar_strkey::DecodeError; use crate::{utils::find_config_dir, Pwd}; -use super::{alias, network::Network, secret::Secret}; +use super::{alias, network::Network, secret::SignerKind}; #[derive(thiserror::Error, Debug)] pub enum Error { @@ -65,6 +65,10 @@ pub enum Error { CannotAccessConfigDir, #[error("cannot parse contract ID {0}: {1}")] CannotParseContractId(String, DecodeError), + #[error("Incorrect Key name")] + IncorrectKeyName, + #[error("Cannot name a Key ledger")] + LedgerKeyName, } #[derive(Debug, clap::Args, Default, Clone)] @@ -98,6 +102,27 @@ impl Display for Location { } } +pub struct KeyName(String); + +impl std::ops::Deref for KeyName { + type Target = str; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl FromStr for KeyName { + type Err = Error; + + fn from_str(s: &str) -> Result { + if s == "ledger" { + return Err(Error::LedgerKeyName); + } + Ok(KeyName(s.to_string())) + } +} + impl AsRef for Location { fn as_ref(&self) -> &Path { match self { @@ -144,7 +169,7 @@ impl Args { ) } - pub fn write_identity(&self, name: &str, secret: &Secret) -> Result<(), Error> { + pub fn write_identity(&self, name: &KeyName, secret: &SignerKind) -> Result<(), Error> { KeyType::Identity.write(name, secret, &self.config_dir()?) } @@ -197,10 +222,19 @@ impl Args { }) .collect::>()) } - pub fn read_identity(&self, name: &str) -> Result { + + pub fn read_identity(&self, name: &str) -> Result { KeyType::Identity.read_with_global(name, &self.local_config()?) } + pub fn account(&self, account_str: &str) -> Result { + if let Ok(secret) = self.read_identity(account_str) { + Ok(secret) + } else { + Ok(account_str.parse::()?) + } + } + 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/commands/config/mod.rs b/cmd/soroban-cli/src/commands/config/mod.rs index bb55103c12..5c54a5c46a 100644 --- a/cmd/soroban-cli/src/commands/config/mod.rs +++ b/cmd/soroban-cli/src/commands/config/mod.rs @@ -1,17 +1,15 @@ use std::path::PathBuf; use clap::{arg, command}; +use secret::StellarSigner; use serde::{Deserialize, Serialize}; +use stellar_strkey::ed25519::PublicKey; -use soroban_rpc::Client; +use crate::signer; +use crate::xdr::{Transaction, TransactionEnvelope}; +use crate::{signer::Stellar, Pwd}; -use crate::{ - signer, - xdr::{Transaction, TransactionEnvelope}, - Pwd, -}; - -use self::{network::Network, secret::Secret}; +use self::network::Network; use super::{keys, network}; @@ -52,52 +50,54 @@ pub struct Args { #[command(flatten)] pub locator: locator::Args, + + /// Check with user before signature. Eventually this will be replaced with `--yes`, which does the opposite and will force a check without --yes + #[arg(long)] + pub check: bool, } impl Args { + pub fn signer(&self) -> Result { + Ok(self + .locator + .account(&self.source_account)? + .signer(self.hd_path, self.check)?) + } + pub fn key_pair(&self) -> Result { - let key = self.account(&self.source_account)?; + let key = self.locator.account(&self.source_account)?; Ok(key.key_pair(self.hd_path)?) } - pub async fn sign_with_local_key(&self, tx: Transaction) -> Result { - self.sign(tx).await + pub async fn public_key(&self) -> Result { + Ok(self.signer()?.get_public_key().await?) } - #[allow(clippy::unused_async)] pub async fn sign(&self, tx: Transaction) -> Result { - let key = self.key_pair()?; + let signer = self.signer()?; + self.sign_with_signer(&signer, tx).await + } + + pub async fn sign_with_signer( + &self, + signer: &impl Stellar, + tx: Transaction, + ) -> Result { let Network { network_passphrase, .. } = &self.get_network()?; - Ok(signer::sign_tx(&key, &tx, network_passphrase)?) + Ok(signer.sign_txn(tx, network_passphrase).await?) } pub async fn sign_soroban_authorizations( &self, + signer: &impl Stellar, 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 account(&self, account_str: &str) -> Result { - if let Ok(secret) = self.locator.read_identity(account_str) { - Ok(secret) - } else { - Ok(account_str.parse::()?) - } + Ok(signer + .sign_soroban_authorizations(tx, &network.network_passphrase) + .await?) } pub fn get_network(&self) -> Result { diff --git a/cmd/soroban-cli/src/commands/config/secret.rs b/cmd/soroban-cli/src/commands/config/secret.rs index 17a921c783..b28c1338d1 100644 --- a/cmd/soroban-cli/src/commands/config/secret.rs +++ b/cmd/soroban-cli/src/commands/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, native, Ledger, LocalKey, Stellar}, + utils, +}; #[derive(thiserror::Error, Debug)] pub enum Error { @@ -21,6 +24,10 @@ pub enum Error { Ed25519(#[from] ed25519_dalek::SignatureError), #[error("Invalid address {0}")] InvalidAddress(String), + #[error("Ledger does not reveal secret key")] + LedgerDoesNotRevealSecretKey, + #[error(transparent)] + Stellar(#[from] signer::Error), } #[derive(Debug, clap::Args, Clone)] @@ -36,16 +43,16 @@ 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 }) + Ok(SignerKind::SecretKey { secret_key }) } else if self.secret_key { println!("Type a secret key: "); let secret_key = read_password()?; let secret_key = PrivateKey::from_string(&secret_key) .map_err(|_| Error::InvalidSecretKey)? .to_string(); - Ok(Secret::SecretKey { secret_key }) + Ok(SignerKind::SecretKey { secret_key }) } else if self.seed_phrase { println!("Type a 12 word seed phrase: "); let seed_phrase = read_password()?; @@ -54,7 +61,7 @@ impl Args { // let len = seed_phrase.len(); // return Err(Error::InvalidSeedPhrase { len }); // } - Ok(Secret::SeedPhrase { + Ok(SignerKind::SeedPhrase { seed_phrase: seed_phrase .into_iter() .map(ToString::to_string) @@ -69,55 +76,72 @@ impl Args { #[derive(Debug, Serialize, Deserialize)] #[serde(untagged)] -pub enum Secret { +pub enum SignerKind { SecretKey { secret_key: String }, SeedPhrase { seed_phrase: String }, + Ledger, } -impl FromStr for Secret { +impl FromStr for SignerKind { type Err = Error; fn from_str(s: &str) -> Result { if PrivateKey::from_string(s).is_ok() { - Ok(Secret::SecretKey { + Ok(SignerKind::SecretKey { secret_key: s.to_string(), }) } else if sep5::SeedPhrase::from_str(s).is_ok() { - Ok(Secret::SeedPhrase { + Ok(SignerKind::SeedPhrase { seed_phrase: s.to_string(), }) + } else if s == "ledger" { + Ok(SignerKind::Ledger) } else { Err(Error::InvalidAddress(s.to_string())) } } } -impl From for Secret { +impl From for SignerKind { fn from(value: PrivateKey) -> Self { - Secret::SecretKey { + SignerKind::SecretKey { secret_key: value.to_string(), } } } -impl Secret { +impl SignerKind { pub fn private_key(&self, index: Option) -> Result { Ok(match self { - Secret::SecretKey { secret_key } => PrivateKey::from_string(secret_key)?, - Secret::SeedPhrase { seed_phrase } => PrivateKey::from_payload( + SignerKind::SecretKey { secret_key } => PrivateKey::from_string(secret_key)?, + SignerKind::SeedPhrase { seed_phrase } => PrivateKey::from_payload( &sep5::SeedPhrase::from_str(seed_phrase)? .from_path_index(index.unwrap_or_default(), None)? .private() .0, )?, + SignerKind::Ledger => panic!("Ledger does not reveal secret key"), }) } - 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 { + SignerKind::SecretKey { .. } | SignerKind::SeedPhrase { .. } => Ok(StellarSigner::Local( + LocalKey::new(self.key_pair(index)?, prompt), + )), + SignerKind::Ledger => { + let hd_path: u32 = index + .unwrap_or_default() + .try_into() + .expect("uszie bigger than u32"); + Ok(StellarSigner::Ledger(native(hd_path)?)) + } + } } pub fn key_pair(&self, index: Option) -> Result { @@ -132,7 +156,7 @@ impl Secret { }? .seed_phrase .into_phrase(); - Ok(Secret::SeedPhrase { seed_phrase }) + Ok(SignerKind::SeedPhrase { seed_phrase }) } pub fn test_seed_phrase() -> Result { @@ -140,6 +164,27 @@ impl Secret { } } +pub enum StellarSigner { + Local(LocalKey), + Ledger(Ledger), +} + +impl Stellar for StellarSigner { + async fn get_public_key(&self) -> Result { + match self { + StellarSigner::Local(signer) => signer.get_public_key().await, + StellarSigner::Ledger(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, + StellarSigner::Ledger(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/commands/contract/deploy/asset.rs b/cmd/soroban-cli/src/commands/contract/deploy/asset.rs index e912d2b981..ab46a911d7 100644 --- a/cmd/soroban-cli/src/commands/contract/deploy/asset.rs +++ b/cmd/soroban-cli/src/commands/contract/deploy/asset.rs @@ -124,7 +124,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) { diff --git a/cmd/soroban-cli/src/commands/contract/deploy/wasm.rs b/cmd/soroban-cli/src/commands/contract/deploy/wasm.rs index 36bfb3f5c6..d3b1d1c0db 100644 --- a/cmd/soroban-cli/src/commands/contract/deploy/wasm.rs +++ b/cmd/soroban-cli/src/commands/contract/deploy/wasm.rs @@ -231,7 +231,7 @@ impl NetworkRunnable for Cmd { return Ok(TxnResult::Txn(txn)); } 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()?; if global_args.map_or(true, |a| !a.no_cache) { diff --git a/cmd/soroban-cli/src/commands/contract/extend.rs b/cmd/soroban-cli/src/commands/contract/extend.rs index a6a0929eb9..6be0c99102 100644 --- a/cmd/soroban-cli/src/commands/contract/extend.rs +++ b/cmd/soroban-cli/src/commands/contract/extend.rs @@ -177,7 +177,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/install.rs b/cmd/soroban-cli/src/commands/contract/install.rs index d7ebef16d3..04d159e367 100644 --- a/cmd/soroban-cli/src/commands/contract/install.rs +++ b/cmd/soroban-cli/src/commands/contract/install.rs @@ -171,7 +171,7 @@ impl NetworkRunnable for Cmd { return Ok(TxnResult::Txn(txn)); } 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) { data::write(txn_resp.clone().try_into().unwrap(), &network.rpc_uri()?)?; diff --git a/cmd/soroban-cli/src/commands/contract/invoke.rs b/cmd/soroban-cli/src/commands/contract/invoke.rs index 537bcf9eb5..4901e410cf 100644 --- a/cmd/soroban-cli/src/commands/contract/invoke.rs +++ b/cmd/soroban-cli/src/commands/contract/invoke.rs @@ -7,7 +7,7 @@ use std::str::FromStr; use std::{fmt::Debug, fs, io}; use clap::{arg, command, value_parser, Parser}; -use ed25519_dalek::SigningKey; + use heck::ToKebabCase; use soroban_env_host::{ @@ -21,15 +21,18 @@ use soroban_env_host::{ HostError, }; +use soroban_sdk::xdr::ScSpecFunctionInputV0; use soroban_spec::read::FromWasmError; use super::super::{ config::{self, locator}, events, }; +use crate::commands::config::secret::StellarSigner; use crate::commands::txn_result::{TxnEnvelopeResult, TxnResult}; use crate::commands::NetworkRunnable; use crate::get_spec::{self, get_remote_contract_spec}; +use crate::signer::{self, Stellar}; use crate::{ commands::{config::data, global, network}, rpc, Pwd, @@ -146,6 +149,8 @@ pub enum Error { Network(#[from] network::Error), #[error(transparent)] GetSpecError(#[from] get_spec::Error), + #[error(transparent)] + Signer(#[from] signer::Error), } impl From for Error { @@ -163,12 +168,12 @@ impl Cmd { std::env::var("SYSTEM_TEST_VERBOSE_OUTPUT").as_deref() == Ok("true") } - fn build_host_function_parameters( + async fn build_host_function_parameters( &self, contract_id: [u8; 32], 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(self.contract_id.clone()) .no_binary_name(true) @@ -188,59 +193,15 @@ impl Cmd { 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) = self.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)); let function_symbol_arg = function @@ -265,6 +226,58 @@ impl Cmd { Ok((function.clone(), spec, invoke_args, signers)) } + pub async fn parse_arg( + &self, + 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) { + if let Ok(signer_) = config + .locator + .read_identity(&s) + .and_then(|signer| Ok(signer.signer(config.hd_path, config.check)?)) + { + 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)) + } + pub async fn run(&self, global_args: &global::Args) -> Result<(), Error> { let res = self.invoke(global_args).await?.to_envelope(); match res { @@ -317,7 +330,9 @@ impl NetworkRunnable for Cmd { let spec_entries = self.spec_entries()?; if let Some(spec_entries) = &spec_entries { // For testing wasm arg parsing - let _ = self.build_host_function_parameters(contract_id, spec_entries, config)?; + let _ = self + .build_host_function_parameters(contract_id, spec_entries, config) + .await?; } let client = rpc::Client::new(&network.rpc_url)?; let account_details = if self.is_view { @@ -326,12 +341,10 @@ impl NetworkRunnable for Cmd { client .verify_network_passphrase(Some(&network.network_passphrase)) .await?; - let key = config.key_pair()?; + let key = config.public_key().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; @@ -347,8 +360,9 @@ impl NetworkRunnable for Cmd { .map_err(Error::from)?; // Get the ledger footprint - let (function, spec, host_function_params, signers) = - self.build_host_function_parameters(contract_id, &spec_entries, config)?; + let (function, spec, host_function_params, signers) = self + .build_host_function_parameters(contract_id, &spec_entries, config) + .await?; let tx = build_invoke_contract_tx( host_function_params.clone(), sequence + 1, @@ -376,13 +390,14 @@ impl NetworkRunnable for Cmd { let mut txn = txn.transaction().clone(); // let auth = auth_entries(&txn); // crate::log::auth(&[auth]); - - if let Some(tx) = config.sign_soroban_authorizations(&txn, &signers).await? { - txn = tx; + for signer in &signers { + if let Some(tx) = config.sign_soroban_authorizations(signer, &txn).await? { + txn = tx; + } } // log_auth_cost_and_footprint(resources(&txn)); 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/restore.rs b/cmd/soroban-cli/src/commands/contract/restore.rs index e38a45b311..6994deb893 100644 --- a/cmd/soroban-cli/src/commands/contract/restore.rs +++ b/cmd/soroban-cli/src/commands/contract/restore.rs @@ -174,7 +174,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 2868c7371a..37f13dfcb5 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.parse()?, &self.secrets.kind()?)?) } } diff --git a/cmd/soroban-cli/src/commands/keys/address.rs b/cmd/soroban-cli/src/commands/keys/address.rs index d13381b49d..766e948ebe 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 b6c088f135..a312064fc1 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 159191dc30..6db46d7306 100644 --- a/cmd/soroban-cli/src/commands/keys/generate.rs +++ b/cmd/soroban-cli/src/commands/keys/generate.rs @@ -4,7 +4,7 @@ use crate::commands::network; use super::super::config::{ locator, - secret::{self, Secret}, + secret::{self, SignerKind}, }; #[derive(thiserror::Error, Debug)] @@ -53,18 +53,19 @@ pub struct Cmd { impl Cmd { pub async fn run(&self) -> Result<(), Error> { let seed_phrase = if self.default_seed { - Secret::test_seed_phrase() + SignerKind::test_seed_phrase() } else { - Secret::from_seed(self.seed.as_deref()) + SignerKind::from_seed(self.seed.as_deref()) }?; let secret = if self.as_secret { seed_phrase.private_key(self.hd_path)?.into() } else { seed_phrase }; - self.config_locator.write_identity(&self.name, &secret)?; + self.config_locator + .write_identity(&self.name.parse()?, &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 42814092f8..7ddb2c34db 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()?,