From bf88e3692129d92e17ee67bab9885a4714f3d7f0 Mon Sep 17 00:00:00 2001 From: Willem Wyndham Date: Tue, 10 Sep 2024 15:25:56 -0400 Subject: [PATCH 01/23] feat: `tx sign` and new traits for signing The new `sign` subcommand allows for signing a passed transaction envelope. And can choose a different source_account than the signing key's corresponding public key. Additionally, `signer::{Transaction, TransactionHash, Blob}` traits which simplifies the interface. Using blanket implementations, any type that implements `Blob`, will implement, `TransactionHash` and any type that implements `TransactionHash` implements `Transaction`, which uses the hash. This will allow for types to opt in to how they want to sign the transaction. --- Cargo.lock | 48 +++++ .../soroban-test/tests/it/integration/tx.rs | 56 +++++- cmd/soroban-cli/Cargo.toml | 1 + cmd/soroban-cli/src/commands/tx/mod.rs | 6 + cmd/soroban-cli/src/commands/tx/sign.rs | 35 ++++ cmd/soroban-cli/src/config/locator.rs | 8 + cmd/soroban-cli/src/config/mod.rs | 1 + cmd/soroban-cli/src/config/secret.rs | 28 ++- cmd/soroban-cli/src/config/sign_with.rs | 114 +++++++++++ cmd/soroban-cli/src/signer.rs | 15 +- cmd/soroban-cli/src/signer/types.rs | 183 ++++++++++++++++++ 11 files changed, 486 insertions(+), 9 deletions(-) create mode 100644 cmd/soroban-cli/src/commands/tx/sign.rs create mode 100644 cmd/soroban-cli/src/config/sign_with.rs create mode 100644 cmd/soroban-cli/src/signer/types.rs diff --git a/Cargo.lock b/Cargo.lock index ae6408d09..d42253195 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.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" +dependencies = [ + "bitflags 2.6.0", + "crossterm_winapi", + "mio", + "parking_lot", + "rustix 0.38.34", + "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" @@ -3298,6 +3323,7 @@ checksum = "4569e456d394deccd22ce1c1913e6ea0e54519f577285001215d33557431afe4" dependencies = [ "hermit-abi 0.3.9", "libc", + "log", "wasi", "windows-sys 0.52.0", ] @@ -4545,6 +4571,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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + [[package]] name = "signal-hook-registry" version = "1.4.2" @@ -4690,6 +4737,7 @@ dependencies = [ "clap-markdown", "clap_complete", "crate-git-revision 0.0.4", + "crossterm", "csv", "directories", "dirs", diff --git a/cmd/crates/soroban-test/tests/it/integration/tx.rs b/cmd/crates/soroban-test/tests/it/integration/tx.rs index bcb880b18..4be244b18 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,55 @@ 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 tx_env = super::xdr::tx_envelope_from_stdin()?; + let rpc_result = send_manually(sandbox, &tx_env).await; + + println!("Transaction sent: {rpc_result}"); +} + +async fn send_manually(sandbox: &TestEnv, tx_env: &TransactionEnvelope) -> String { + let client = crate::rpc::Client::new(&network.rpc_url).unwrap(); + let res = client + .send_transaction_polling(tx_env) + .await + .unwrap() + .to_string(); + serde_json::to_string_pretty(&res).unwrap() +} + +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..a916dc714 100644 --- a/cmd/soroban-cli/Cargo.toml +++ b/cmd/soroban-cli/Cargo.toml @@ -123,6 +123,7 @@ humantime = "2.1.0" phf = { version = "0.11.2", features = ["macros"] } semver = "1.0.0" glob = "0.3.1" +crossterm = "0.28.1" # For hyper-tls [target.'cfg(unix)'.dependencies] diff --git a/cmd/soroban-cli/src/commands/tx/mod.rs b/cmd/soroban-cli/src/commands/tx/mod.rs index 59f07228a..a64417127 100644 --- a/cmd/soroban-cli/src/commands/tx/mod.rs +++ b/cmd/soroban-cli/src/commands/tx/mod.rs @@ -3,6 +3,7 @@ use clap::Parser; use super::global; pub mod hash; +pub mod sign; pub mod simulate; pub mod xdr; @@ -12,6 +13,8 @@ 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), } #[derive(thiserror::Error, Debug)] @@ -22,6 +25,8 @@ pub enum Error { /// An error during hash calculation #[error(transparent)] Hash(#[from] hash::Error), + #[error(transparent)] + Sign(#[from] sign::Error), } impl Cmd { @@ -29,6 +34,7 @@ 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?, }; Ok(()) } 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..86055a101 100644 --- a/cmd/soroban-cli/src/config/mod.rs +++ b/cmd/soroban-cli/src/config/mod.rs @@ -18,6 +18,7 @@ pub mod data; pub mod locator; pub mod network; pub mod secret; +pub mod sign_with; pub mod upgrade_check; #[derive(thiserror::Error, Debug)] diff --git a/cmd/soroban-cli/src/config/secret.rs b/cmd/soroban-cli/src/config/secret.rs index b5b1dd747..fcba3ed77 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}, + 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)] @@ -120,6 +125,14 @@ impl Secret { )?) } + 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 { Ok(utils::into_signing_key(&self.private_key(index)?)) } @@ -140,6 +153,19 @@ impl Secret { } } +pub enum StellarSigner { + Local(LocalKey), +} + +#[async_trait::async_trait] +impl signer::Blob for StellarSigner { + async fn sign_blob(&self, blob: &[u8]) -> Result, signer::types::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..355739c92 --- /dev/null +++ b/cmd/soroban-cli/src/config/sign_with.rs @@ -0,0 +1,114 @@ +use std::path::PathBuf; + +use crate::{ + signer::{ + self, + types::{sign_txn_env, Transaction}, + }, + xdr::TransactionEnvelope, +}; +use clap::arg; +use stellar_strkey::ed25519::PublicKey; + +use super::{ + locator, + network::{self, Network}, + secret::{self, Secret}, +}; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error(transparent)] + Network(#[from] network::Error), + #[error(transparent)] + Signer(#[from] signer::types::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, + #[error(transparent)] + StrKey(#[from] stellar_strkey::DecodeError), +} + +#[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, + + /// Source account of the transaction. By default will be the account that signs the transaction. + #[arg(long, visible_alias = "source")] + pub source_account: Option, +} + +impl Args { + pub fn secret(&self) -> Result { + let account = self.sign_with_key.as_deref().ok_or(Error::NoSignWithKey)?; + Ok(self.locator.account(account)?) + } + + pub async fn sign_txn_env( + &self, + tx: TransactionEnvelope, + ) -> Result { + let secret = self.secret()?; + let signer = secret.signer(self.hd_path, !self.yes)?; + let source_account = if let Some(source_account) = self.source_account.as_deref() { + stellar_strkey::ed25519::PublicKey::from_string(source_account)? + } else { + secret.public_key(self.hd_path)? + }; + + self.sign_tx_env_with_signer(&signer, &source_account, tx) + .await + } + + pub async fn sign_tx_env_with_signer( + &self, + signer: &(impl Transaction + std::marker::Sync), + source_account: &PublicKey, + tx_env: TransactionEnvelope, + ) -> Result { + let network = self.get_network()?; + Ok(sign_txn_env(signer, source_account, 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..99dc3038f 100644 --- a/cmd/soroban-cli/src/signer.rs +++ b/cmd/soroban-cli/src/signer.rs @@ -5,11 +5,14 @@ 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, + SorobanAuthorizedFunction, SorobanCredentials, TransactionEnvelope, TransactionSignaturePayload, TransactionSignaturePayloadTaggedTransaction, TransactionV1Envelope, Uint256, WriteXdr, }; +pub mod types; +pub use types::{Blob, LocalKey, Transaction, TransactionHash}; + #[derive(thiserror::Error, Debug)] pub enum Error { #[error("Contract addresses are not supported to sign auth entries {address}")] @@ -26,7 +29,7 @@ pub enum Error { Xdr(#[from] xdr::Error), } -fn requires_auth(txn: &Transaction) -> Option { +fn requires_auth(txn: &xdr::Transaction) -> Option { let [op @ Operation { body: OperationBody::InvokeHostFunction(InvokeHostFunctionOp { auth, .. }), .. @@ -44,12 +47,12 @@ fn requires_auth(txn: &Transaction) -> Option { // 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, + raw: &xdr::Transaction, source_key: &ed25519_dalek::SigningKey, signers: &[ed25519_dalek::SigningKey], signature_expiration_ledger: u32, network_passphrase: &str, -) -> Result, Error> { +) -> Result, Error> { let mut tx = raw.clone(); let Some(mut op) = requires_auth(&tx) else { return Ok(None); @@ -191,7 +194,7 @@ fn sign_soroban_authorization_entry( pub fn sign_tx( key: &ed25519_dalek::SigningKey, - tx: &Transaction, + tx: &xdr::Transaction, network_passphrase: &str, ) -> Result { let tx_hash = hash(tx, network_passphrase)?; @@ -208,7 +211,7 @@ pub fn sign_tx( })) } -pub fn hash(tx: &Transaction, network_passphrase: &str) -> Result<[u8; 32], xdr::Error> { +pub fn hash(tx: &xdr::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()), diff --git a/cmd/soroban-cli/src/signer/types.rs b/cmd/soroban-cli/src/signer/types.rs new file mode 100644 index 000000000..0f619015e --- /dev/null +++ b/cmd/soroban-cli/src/signer/types.rs @@ -0,0 +1,183 @@ +use crossterm::event::{read, Event, KeyCode}; +use ed25519_dalek::ed25519::signature::Signer; +use sha2::{Digest, Sha256}; + +use crate::{ + config::network::Network, + xdr::{ + self, DecoratedSignature, Limits, Signature, SignatureHint, TransactionEnvelope, + TransactionSignaturePayload, TransactionSignaturePayloadTaggedTransaction, + TransactionV1Envelope, WriteXdr, + }, +}; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("Contract addresses are not supported to sign auth entries {address}")] + ContractAddressAreNotSupported { address: String }, + #[error(transparent)] + Ed25519(#[from] ed25519_dalek::SignatureError), + #[error("Missing signing key for account {address}")] + MissingSignerForAddress { address: String }, + #[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, +} + +/// Calculate the hash of a Transaction +pub fn transaction_hash( + txn: &xdr::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) +} + +/// A trait for signing arbitrary byte arrays +#[async_trait::async_trait] +pub trait Blob { + /// Sign an abritatry byte array + async fn sign_blob(&self, blob: &[u8]) -> Result, Error>; +} + +#[async_trait::async_trait] +pub trait TransactionHash { + /// 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, + source_account: &stellar_strkey::ed25519::PublicKey, + txn: [u8; 32], + ) -> Result; +} +#[async_trait::async_trait] +impl TransactionHash for T +where + T: Blob + Send + Sync, +{ + async fn sign_txn_hash( + &self, + source_account: &stellar_strkey::ed25519::PublicKey, + txn: [u8; 32], + ) -> Result { + 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()?), + }) + } +} + +/// A trait for signing Stellar transactions and Soroban authorization entries +#[async_trait::async_trait] +pub trait Transaction { + /// 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, + source_account: &stellar_strkey::ed25519::PublicKey, + txn: &xdr::Transaction, + network: &Network, + ) -> Result; +} + +#[async_trait::async_trait] +impl Transaction for T +where + T: TransactionHash + Send + Sync, +{ + async fn sign_txn( + &self, + source_account: &stellar_strkey::ed25519::PublicKey, + txn: &xdr::Transaction, + Network { + network_passphrase, .. + }: &Network, + ) -> Result { + let hash = transaction_hash(txn, network_passphrase)?; + self.sign_txn_hash(source_account, hash).await + } +} +pub async fn sign_txn_env( + signer: &(impl Transaction + std::marker::Sync), + source_account: &stellar_strkey::ed25519::PublicKey, + txn_env: TransactionEnvelope, + network: &Network, +) -> Result { + match txn_env { + TransactionEnvelope::Tx(TransactionV1Envelope { tx, signatures }) => { + let decorated_signature = signer.sign_txn(source_account, &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), + } +} + +pub(crate) fn hash(network_passphrase: &str) -> xdr::Hash { + xdr::Hash(Sha256::digest(network_passphrase.as_bytes()).into()) +} + +pub struct LocalKey { + key: ed25519_dalek::SigningKey, + prompt: bool, +} + +impl LocalKey { + pub fn new(key: ed25519_dalek::SigningKey, prompt: bool) -> Self { + Self { key, prompt } + } +} + +#[async_trait::async_trait] +impl Blob 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()) + } +} + +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 + _ => (), + } + } + } +} From df588644b34964e5a5de6f158bc8324ec59dee65 Mon Sep 17 00:00:00 2001 From: Willem Wyndham Date: Tue, 10 Sep 2024 15:36:26 -0400 Subject: [PATCH 02/23] refactor(tx): test --- FULL_HELP_DOCS.md | 21 +++++++++++++++++++ .../soroban-test/tests/it/integration/tx.rs | 9 ++------ 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/FULL_HELP_DOCS.md b/FULL_HELP_DOCS.md index b7480cb5e..4a2821d59 100644 --- a/FULL_HELP_DOCS.md +++ b/FULL_HELP_DOCS.md @@ -1291,6 +1291,7 @@ 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 @@ -1326,6 +1327,26 @@ 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 "." +* `--source-account ` — Source account of the transaction. By default will be the account that signs the transaction + + + ## `stellar xdr` Decode and encode XDR diff --git a/cmd/crates/soroban-test/tests/it/integration/tx.rs b/cmd/crates/soroban-test/tests/it/integration/tx.rs index 4be244b18..5acbff9a9 100644 --- a/cmd/crates/soroban-test/tests/it/integration/tx.rs +++ b/cmd/crates/soroban-test/tests/it/integration/tx.rs @@ -70,19 +70,14 @@ async fn send() { tx_env.to_xdr_base64(Limits::none()).unwrap() ); - let tx_env = super::xdr::tx_envelope_from_stdin()?; let rpc_result = send_manually(sandbox, &tx_env).await; println!("Transaction sent: {rpc_result}"); } async fn send_manually(sandbox: &TestEnv, tx_env: &TransactionEnvelope) -> String { - let client = crate::rpc::Client::new(&network.rpc_url).unwrap(); - let res = client - .send_transaction_polling(tx_env) - .await - .unwrap() - .to_string(); + let client = soroban_rpc::Client::new(&sandbox.rpc_url).unwrap(); + let res = client.send_transaction_polling(tx_env).await.unwrap(); serde_json::to_string_pretty(&res).unwrap() } From a0cc592d1bbabd5f42728d9c41b65b94be1efaa8 Mon Sep 17 00:00:00 2001 From: Willem Wyndham Date: Thu, 12 Sep 2024 10:41:39 -0400 Subject: [PATCH 03/23] fix: simplify sign_with --- FULL_HELP_DOCS.md | 4 +-- cmd/soroban-cli/src/commands/tx/sign.rs | 15 ++++++-- cmd/soroban-cli/src/config/sign_with.rs | 48 ++++++++++--------------- 3 files changed, 34 insertions(+), 33 deletions(-) diff --git a/FULL_HELP_DOCS.md b/FULL_HELP_DOCS.md index 4a2821d59..ecd5c97cf 100644 --- a/FULL_HELP_DOCS.md +++ b/FULL_HELP_DOCS.md @@ -1331,19 +1331,19 @@ Calculate the hash of a transaction envelope from stdin Sign a transaction envolope appending the signature to the envelope -**Usage:** `stellar tx sign [OPTIONS]` +**Usage:** `stellar tx sign [OPTIONS] --source-account ` ###### **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 +* `--source-account ` — Account that signs the transaction. Alias `source`. Can be an identity (--source alice), a secret key (--source SC36…), or a seed phrase (--source "kite urban…") * `--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 "." -* `--source-account ` — Source account of the transaction. By default will be the account that signs the transaction diff --git a/cmd/soroban-cli/src/commands/tx/sign.rs b/cmd/soroban-cli/src/commands/tx/sign.rs index 3f0b90139..1047a91f7 100644 --- a/cmd/soroban-cli/src/commands/tx/sign.rs +++ b/cmd/soroban-cli/src/commands/tx/sign.rs @@ -1,5 +1,5 @@ use crate::{ - config::sign_with, + config::{locator, network, sign_with}, xdr::{self, Limits, TransactionEnvelope, WriteXdr}, }; @@ -8,6 +8,10 @@ pub enum Error { #[error(transparent)] XdrArgs(#[from] super::xdr::Error), #[error(transparent)] + Network(#[from] network::Error), + #[error(transparent)] + Locator(#[from] locator::Error), + #[error(transparent)] SignWith(#[from] sign_with::Error), #[error(transparent)] Xdr(#[from] xdr::Error), @@ -18,6 +22,10 @@ pub enum Error { pub struct Cmd { #[command(flatten)] pub sign_with: sign_with::Args, + #[command(flatten)] + pub network: network::Args, + #[command(flatten)] + pub locator: locator::Args, } impl Cmd { @@ -30,6 +38,9 @@ impl Cmd { } pub async fn sign_tx_env(&self, tx: TransactionEnvelope) -> Result { - Ok(self.sign_with.sign_txn_env(tx).await?) + Ok(self + .sign_with + .sign_txn_env(tx, &self.locator, &self.network.get(&self.locator)?) + .await?) } } diff --git a/cmd/soroban-cli/src/config/sign_with.rs b/cmd/soroban-cli/src/config/sign_with.rs index 355739c92..98338d493 100644 --- a/cmd/soroban-cli/src/config/sign_with.rs +++ b/cmd/soroban-cli/src/config/sign_with.rs @@ -1,5 +1,3 @@ -use std::path::PathBuf; - use crate::{ signer::{ self, @@ -61,36 +59,30 @@ pub struct Args { #[arg(long)] pub yes: bool, - #[command(flatten)] - pub network: network::Args, - - #[command(flatten)] - pub locator: locator::Args, - - /// Source account of the transaction. By default will be the account that signs the transaction. - #[arg(long, visible_alias = "source")] - pub source_account: Option, + /// Account that signs the transaction. Alias `source`. Can be an identity (--source alice), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). + #[arg(long, visible_alias = "source", env = "STELLAR_ACCOUNT")] + pub source_account: String, } impl Args { - pub fn secret(&self) -> Result { - let account = self.sign_with_key.as_deref().ok_or(Error::NoSignWithKey)?; - Ok(self.locator.account(account)?) + pub fn secret(&self, locator: &locator::Args) -> Result { + let account = self + .sign_with_key + .as_deref() + .unwrap_or(&self.source_account); + Ok(locator.account(account)?) } pub async fn sign_txn_env( &self, tx: TransactionEnvelope, + locator: &locator::Args, + network: &Network, ) -> Result { - let secret = self.secret()?; + let secret = self.secret(locator)?; let signer = secret.signer(self.hd_path, !self.yes)?; - let source_account = if let Some(source_account) = self.source_account.as_deref() { - stellar_strkey::ed25519::PublicKey::from_string(source_account)? - } else { - secret.public_key(self.hd_path)? - }; - - self.sign_tx_env_with_signer(&signer, &source_account, tx) + let source_account = self.source_account(locator)?; + self.sign_tx_env_with_signer(&signer, &source_account, tx, network) .await } @@ -99,16 +91,14 @@ impl Args { signer: &(impl Transaction + std::marker::Sync), source_account: &PublicKey, tx_env: TransactionEnvelope, + network: &Network, ) -> Result { - let network = self.get_network()?; Ok(sign_txn_env(signer, source_account, 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()?) + pub fn source_account(&self, locator: &locator::Args) -> Result { + Ok(locator + .account(&self.source_account)? + .public_key(self.hd_path)?) } } From c705666a0b165fd54e600c7bd6b5dc702c3d8ea4 Mon Sep 17 00:00:00 2001 From: Willem Wyndham Date: Mon, 16 Sep 2024 09:36:16 -0400 Subject: [PATCH 04/23] Apply suggestions from code review Co-authored-by: Leigh McCulloch <351529+leighmcculloch@users.noreply.github.com> --- cmd/soroban-cli/src/config/sign_with.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/soroban-cli/src/config/sign_with.rs b/cmd/soroban-cli/src/config/sign_with.rs index 98338d493..b0eb0900d 100644 --- a/cmd/soroban-cli/src/config/sign_with.rs +++ b/cmd/soroban-cli/src/config/sign_with.rs @@ -39,14 +39,14 @@ pub struct Args { #[arg( long, conflicts_with = "sign_with_lab", - env = "STELLAR_SIGN_WITH_SECRET" + env = "STELLAR_SIGN_WITH_KEY" )] pub sign_with_key: Option, /// Sign with labratory #[arg( long, conflicts_with = "sign_with_key", - env = "STELLAR_SIGN_WITH_LABRATORY", + env = "STELLAR_SIGN_WITH_LAB", hide = true )] pub sign_with_lab: bool, From 00aaedc9dbb10edf4b5f404fc1cfbe423f385186 Mon Sep 17 00:00:00 2001 From: Willem Wyndham Date: Tue, 17 Sep 2024 15:06:47 -0400 Subject: [PATCH 05/23] fix: address PR review Remove Blob trait. Add to TransactionHash trait to include hint. Move print to top level signer and fix test. Also remove the prompt for this PR since the sign command is already approval for signing. --- Cargo.lock | 48 ---------- FULL_HELP_DOCS.md | 4 +- .../soroban-test/tests/it/integration/tx.rs | 9 +- cmd/soroban-cli/Cargo.toml | 1 - cmd/soroban-cli/src/commands/tx/hash.rs | 2 +- cmd/soroban-cli/src/commands/tx/mod.rs | 2 +- cmd/soroban-cli/src/commands/tx/sign.rs | 13 ++- cmd/soroban-cli/src/config/secret.rs | 49 ++++++++--- cmd/soroban-cli/src/config/sign_with.rs | 36 ++------ cmd/soroban-cli/src/print.rs | 3 +- cmd/soroban-cli/src/signer.rs | 2 +- cmd/soroban-cli/src/signer/types.rs | 87 +++++-------------- cmd/soroban-cli/src/utils.rs | 40 +-------- 13 files changed, 85 insertions(+), 211 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4cfcf436b..788fc8e59 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -878,31 +878,6 @@ version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" -[[package]] -name = "crossterm" -version = "0.28.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" -dependencies = [ - "bitflags 2.6.0", - "crossterm_winapi", - "mio", - "parking_lot", - "rustix 0.38.34", - "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" @@ -3323,7 +3298,6 @@ checksum = "4569e456d394deccd22ce1c1913e6ea0e54519f577285001215d33557431afe4" dependencies = [ "hermit-abi 0.3.9", "libc", - "log", "wasi", "windows-sys 0.52.0", ] @@ -4571,27 +4545,6 @@ 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.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" -dependencies = [ - "libc", - "mio", - "signal-hook", -] - [[package]] name = "signal-hook-registry" version = "1.4.2" @@ -4737,7 +4690,6 @@ dependencies = [ "clap-markdown", "clap_complete", "crate-git-revision 0.0.4", - "crossterm", "csv", "directories", "dirs", diff --git a/FULL_HELP_DOCS.md b/FULL_HELP_DOCS.md index ecd5c97cf..b0ecf7514 100644 --- a/FULL_HELP_DOCS.md +++ b/FULL_HELP_DOCS.md @@ -1331,14 +1331,12 @@ Calculate the hash of a transaction envelope from stdin Sign a transaction envolope appending the signature to the envelope -**Usage:** `stellar tx sign [OPTIONS] --source-account ` +**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 -* `--source-account ` — Account that signs the transaction. Alias `source`. Can be an identity (--source alice), a secret key (--source SC36…), or a seed phrase (--source "kite urban…") * `--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 diff --git a/cmd/crates/soroban-test/tests/it/integration/tx.rs b/cmd/crates/soroban-test/tests/it/integration/tx.rs index 5acbff9a9..1eab10684 100644 --- a/cmd/crates/soroban-test/tests/it/integration/tx.rs +++ b/cmd/crates/soroban-test/tests/it/integration/tx.rs @@ -1,3 +1,4 @@ +use soroban_rpc::GetTransactionResponse; use soroban_sdk::xdr::{Limits, ReadXdr, TransactionEnvelope, WriteXdr}; use soroban_test::{AssertExt, TestEnv}; @@ -71,14 +72,12 @@ async fn send() { ); let rpc_result = send_manually(sandbox, &tx_env).await; - - println!("Transaction sent: {rpc_result}"); + assert_eq!(rpc_result.status, "SUCCESS"); } -async fn send_manually(sandbox: &TestEnv, tx_env: &TransactionEnvelope) -> String { +async fn send_manually(sandbox: &TestEnv, tx_env: &TransactionEnvelope) -> GetTransactionResponse { let client = soroban_rpc::Client::new(&sandbox.rpc_url).unwrap(); - let res = client.send_transaction_polling(tx_env).await.unwrap(); - serde_json::to_string_pretty(&res).unwrap() + client.send_transaction_polling(tx_env).await.unwrap() } fn sign_manually(sandbox: &TestEnv, tx_env: &TransactionEnvelope) -> TransactionEnvelope { diff --git a/cmd/soroban-cli/Cargo.toml b/cmd/soroban-cli/Cargo.toml index 67b3f9be4..1f381eb42 100644 --- a/cmd/soroban-cli/Cargo.toml +++ b/cmd/soroban-cli/Cargo.toml @@ -123,7 +123,6 @@ humantime = "2.1.0" phf = { version = "0.11.2", features = ["macros"] } semver = "1.0.0" glob = "0.3.1" -crossterm = "0.28.1" # For hyper-tls [target.'cfg(unix)'.dependencies] diff --git a/cmd/soroban-cli/src/commands/tx/hash.rs b/cmd/soroban-cli/src/commands/tx/hash.rs index 8d8ec6d82..dfffb623e 100644 --- a/cmd/soroban-cli/src/commands/tx/hash.rs +++ b/cmd/soroban-cli/src/commands/tx/hash.rs @@ -1,6 +1,6 @@ use hex; -use crate::{commands::global, config::network, utils::transaction_hash}; +use crate::{commands::global, config::network, signer::types::transaction_hash}; #[derive(thiserror::Error, Debug)] pub enum Error { diff --git a/cmd/soroban-cli/src/commands/tx/mod.rs b/cmd/soroban-cli/src/commands/tx/mod.rs index a64417127..1f57cb79f 100644 --- a/cmd/soroban-cli/src/commands/tx/mod.rs +++ b/cmd/soroban-cli/src/commands/tx/mod.rs @@ -34,7 +34,7 @@ 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::Sign(cmd) => cmd.run(global_args).await?, }; Ok(()) } diff --git a/cmd/soroban-cli/src/commands/tx/sign.rs b/cmd/soroban-cli/src/commands/tx/sign.rs index 1047a91f7..913422b34 100644 --- a/cmd/soroban-cli/src/commands/tx/sign.rs +++ b/cmd/soroban-cli/src/commands/tx/sign.rs @@ -1,4 +1,5 @@ use crate::{ + commands::global, config::{locator, network, sign_with}, xdr::{self, Limits, TransactionEnvelope, WriteXdr}, }; @@ -30,17 +31,21 @@ pub struct Cmd { impl Cmd { #[allow(clippy::unused_async)] - pub async fn run(&self) -> Result<(), Error> { + pub async fn run(&self, global_args: &global::Args) -> Result<(), Error> { let txn_env = super::xdr::tx_envelope_from_stdin()?; - let envelope = self.sign_tx_env(txn_env).await?; + let envelope = self.sign_tx_env(txn_env, global_args.quiet).await?; println!("{}", envelope.to_xdr_base64(Limits::none())?.trim()); Ok(()) } - pub async fn sign_tx_env(&self, tx: TransactionEnvelope) -> Result { + pub async fn sign_tx_env( + &self, + tx: TransactionEnvelope, + quiet: bool, + ) -> Result { Ok(self .sign_with - .sign_txn_env(tx, &self.locator, &self.network.get(&self.locator)?) + .sign_txn_env(tx, &self.locator, &self.network.get(&self.locator)?, quiet) .await?) } } diff --git a/cmd/soroban-cli/src/config/secret.rs b/cmd/soroban-cli/src/config/secret.rs index fcba3ed77..0293c056a 100644 --- a/cmd/soroban-cli/src/config/secret.rs +++ b/cmd/soroban-cli/src/config/secret.rs @@ -3,11 +3,16 @@ use serde::{Deserialize, Serialize}; use std::{io::Write, str::FromStr}; use stellar_strkey::ed25519::{PrivateKey, PublicKey}; +use crate::print::Print; +use crate::signer::types::transaction_hash; +use crate::xdr::{self, DecoratedSignature}; use crate::{ signer::{self, LocalKey}, utils, }; +use super::network::Network; + #[derive(thiserror::Error, Debug)] pub enum Error { #[error("invalid secret key")] @@ -125,12 +130,21 @@ impl Secret { )?) } - 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 signer( + &self, + index: Option, + prompt: bool, + quiet: bool, + ) -> Result { + let kind = match self { + Secret::SecretKey { .. } | Secret::SeedPhrase { .. } => { + SignerKind::Local(LocalKey::new(self.key_pair(index)?, prompt)) + } + }; + Ok(StellarSigner { + kind, + printer: Print::new(quiet), + }) } pub fn key_pair(&self, index: Option) -> Result { @@ -153,15 +167,28 @@ impl Secret { } } -pub enum StellarSigner { +pub struct StellarSigner { + kind: SignerKind, + printer: Print, +} + +pub enum SignerKind { Local(LocalKey), } #[async_trait::async_trait] -impl signer::Blob for StellarSigner { - async fn sign_blob(&self, blob: &[u8]) -> Result, signer::types::Error> { - match self { - StellarSigner::Local(signer) => signer.sign_blob(blob).await, +impl signer::Transaction for StellarSigner { + async fn sign_txn( + &self, + txn: &xdr::Transaction, + network: &Network, + ) -> Result { + let tx_hash = transaction_hash(txn, &network.network_passphrase)?; + let hex_hash = hex::encode(tx_hash); + self.printer + .infoln(format!("Signing transaction with hash: {hex_hash}")); + match &self.kind { + SignerKind::Local(key) => key.sign_txn(txn, network).await, } } } diff --git a/cmd/soroban-cli/src/config/sign_with.rs b/cmd/soroban-cli/src/config/sign_with.rs index b0eb0900d..a2bc796bc 100644 --- a/cmd/soroban-cli/src/config/sign_with.rs +++ b/cmd/soroban-cli/src/config/sign_with.rs @@ -6,7 +6,6 @@ use crate::{ xdr::TransactionEnvelope, }; use clap::arg; -use stellar_strkey::ed25519::PublicKey; use super::{ locator, @@ -36,11 +35,7 @@ pub enum Error { #[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_KEY" - )] + #[arg(long, conflicts_with = "sign_with_lab", env = "STELLAR_SIGN_WITH_KEY")] pub sign_with_key: Option, /// Sign with labratory #[arg( @@ -54,22 +49,11 @@ pub struct Args { #[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, - - /// Account that signs the transaction. Alias `source`. Can be an identity (--source alice), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). - #[arg(long, visible_alias = "source", env = "STELLAR_ACCOUNT")] - pub source_account: String, } impl Args { pub fn secret(&self, locator: &locator::Args) -> Result { - let account = self - .sign_with_key - .as_deref() - .unwrap_or(&self.source_account); + let account = self.sign_with_key.as_deref().ok_or(Error::NoSignWithKey)?; Ok(locator.account(account)?) } @@ -78,27 +62,19 @@ impl Args { tx: TransactionEnvelope, locator: &locator::Args, network: &Network, + quiet: bool, ) -> Result { let secret = self.secret(locator)?; - let signer = secret.signer(self.hd_path, !self.yes)?; - let source_account = self.source_account(locator)?; - self.sign_tx_env_with_signer(&signer, &source_account, tx, network) - .await + let signer = secret.signer(self.hd_path, false, quiet)?; + self.sign_tx_env_with_signer(&signer, tx, network).await } pub async fn sign_tx_env_with_signer( &self, signer: &(impl Transaction + std::marker::Sync), - source_account: &PublicKey, tx_env: TransactionEnvelope, network: &Network, ) -> Result { - Ok(sign_txn_env(signer, source_account, tx_env, &network).await?) - } - - pub fn source_account(&self, locator: &locator::Args) -> Result { - Ok(locator - .account(&self.source_account)? - .public_key(self.hd_path)?) + Ok(sign_txn_env(signer, tx_env, network).await?) } } diff --git a/cmd/soroban-cli/src/print.rs b/cmd/soroban-cli/src/print.rs index f772eb649..2a95267d0 100644 --- a/cmd/soroban-cli/src/print.rs +++ b/cmd/soroban-cli/src/print.rs @@ -3,8 +3,7 @@ use std::{env, fmt::Display}; use soroban_env_host::xdr::{Error as XdrError, Transaction}; use crate::{ - config::network::Network, - utils::{explorer_url_for_transaction, transaction_hash}, + config::network::Network, signer::types::transaction_hash, utils::explorer_url_for_transaction, }; const TERMS: &[&str] = &["Apple_Terminal", "vscode"]; diff --git a/cmd/soroban-cli/src/signer.rs b/cmd/soroban-cli/src/signer.rs index 99dc3038f..4f66a93c4 100644 --- a/cmd/soroban-cli/src/signer.rs +++ b/cmd/soroban-cli/src/signer.rs @@ -11,7 +11,7 @@ use soroban_env_host::xdr::{ }; pub mod types; -pub use types::{Blob, LocalKey, Transaction, TransactionHash}; +pub use types::{LocalKey, Transaction, TransactionHash}; #[derive(thiserror::Error, Debug)] pub enum Error { diff --git a/cmd/soroban-cli/src/signer/types.rs b/cmd/soroban-cli/src/signer/types.rs index 0f619015e..12ba97ddd 100644 --- a/cmd/soroban-cli/src/signer/types.rs +++ b/cmd/soroban-cli/src/signer/types.rs @@ -1,4 +1,3 @@ -use crossterm::event::{read, Event, KeyCode}; use ed25519_dalek::ed25519::signature::Signer; use sha2::{Digest, Sha256}; @@ -33,7 +32,7 @@ pub enum Error { pub fn transaction_hash( txn: &xdr::Transaction, network_passphrase: &str, -) -> Result<[u8; 32], Error> { +) -> Result<[u8; 32], xdr::Error> { let signature_payload = TransactionSignaturePayload { network_id: hash(network_passphrase), tagged_transaction: TransactionSignaturePayloadTaggedTransaction::Tx(txn.clone()), @@ -42,46 +41,15 @@ pub fn transaction_hash( Ok(hash) } -/// A trait for signing arbitrary byte arrays -#[async_trait::async_trait] -pub trait Blob { - /// Sign an abritatry byte array - async fn sign_blob(&self, blob: &[u8]) -> Result, Error>; -} - #[async_trait::async_trait] pub trait TransactionHash { - /// Sign a transaction hash with the given source account + /// Sign a transaction hash with the given signer /// # Errors /// Returns an error if the source account is not found - async fn sign_txn_hash( - &self, - source_account: &stellar_strkey::ed25519::PublicKey, - txn: [u8; 32], - ) -> Result; -} -#[async_trait::async_trait] -impl TransactionHash for T -where - T: Blob + Send + Sync, -{ - async fn sign_txn_hash( - &self, - source_account: &stellar_strkey::ed25519::PublicKey, - txn: [u8; 32], - ) -> Result { - 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()?), - }) - } + async fn sign_txn_hash(&self, txn: [u8; 32]) -> Result; + + /// Return the signature hint required for a `DecoratedSignature`` + fn hint(&self) -> SignatureHint; } /// A trait for signing Stellar transactions and Soroban authorization entries @@ -95,7 +63,6 @@ pub trait Transaction { /// Returns an error if the source account is not found async fn sign_txn( &self, - source_account: &stellar_strkey::ed25519::PublicKey, txn: &xdr::Transaction, network: &Network, ) -> Result; @@ -108,25 +75,25 @@ where { async fn sign_txn( &self, - source_account: &stellar_strkey::ed25519::PublicKey, txn: &xdr::Transaction, Network { network_passphrase, .. }: &Network, ) -> Result { let hash = transaction_hash(txn, network_passphrase)?; - self.sign_txn_hash(source_account, hash).await + let hint = self.hint(); + let signature = self.sign_txn_hash(hash).await?; + Ok(DecoratedSignature { hint, signature }) } } pub async fn sign_txn_env( signer: &(impl Transaction + std::marker::Sync), - source_account: &stellar_strkey::ed25519::PublicKey, txn_env: TransactionEnvelope, network: &Network, ) -> Result { match txn_env { TransactionEnvelope::Tx(TransactionV1Envelope { tx, signatures }) => { - let decorated_signature = signer.sign_txn(source_account, &tx, network).await?; + let decorated_signature = signer.sign_txn(&tx, network).await?; let mut sigs = signatures.to_vec(); sigs.push(decorated_signature); Ok(TransactionEnvelope::Tx(TransactionV1Envelope { @@ -144,6 +111,7 @@ pub(crate) fn hash(network_passphrase: &str) -> xdr::Hash { pub struct LocalKey { key: ed25519_dalek::SigningKey, + #[allow(dead_code)] prompt: bool, } @@ -154,30 +122,17 @@ impl LocalKey { } #[async_trait::async_trait] -impl Blob 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()) +impl TransactionHash for LocalKey { + async fn sign_txn_hash(&self, txn: [u8; 32]) -> Result { + let sig = self.key.sign(&txn); + Ok(Signature(sig.to_bytes().to_vec().try_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 - _ => (), - } - } + fn hint(&self) -> SignatureHint { + SignatureHint( + self.key.verifying_key().to_bytes()[28..] + .try_into() + .unwrap(), + ) } } diff --git a/cmd/soroban-cli/src/utils.rs b/cmd/soroban-cli/src/utils.rs index f8ebb8b37..75a18d5e1 100644 --- a/cmd/soroban-cli/src/utils.rs +++ b/cmd/soroban-cli/src/utils.rs @@ -1,13 +1,10 @@ -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, WriteXdr, }; pub use soroban_spec_tools::contract as contract_spec; @@ -21,17 +18,6 @@ pub fn contract_hash(contract: &[u8]) -> Result { Ok(Hash(Sha256::digest(contract).into())) } -/// # Errors -/// -/// Might return an error -pub fn transaction_hash(tx: &Transaction, network_passphrase: &str) -> Result<[u8; 32], XdrError> { - 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()) -} - static EXPLORERS: phf::Map<&'static str, &'static str> = phf_map! { "Test SDF Network ; September 2015" => "https://stellar.expert/explorer/testnet", "Public Global Stellar Network ; September 2015" => "https://stellar.expert/explorer/public", @@ -49,28 +35,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 From 00d989adb93704ad88f09221b5a9146b73fc6a83 Mon Sep 17 00:00:00 2001 From: Willem Wyndham Date: Wed, 18 Sep 2024 13:35:22 -0400 Subject: [PATCH 06/23] fix: remove unneeded arg --- cmd/crates/soroban-test/tests/it/integration/tx.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/cmd/crates/soroban-test/tests/it/integration/tx.rs b/cmd/crates/soroban-test/tests/it/integration/tx.rs index 1eab10684..7d7b65116 100644 --- a/cmd/crates/soroban-test/tests/it/integration/tx.rs +++ b/cmd/crates/soroban-test/tests/it/integration/tx.rs @@ -86,7 +86,6 @@ fn sign_manually(sandbox: &TestEnv, tx_env: &TransactionEnvelope) -> Transaction .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() From c36f788f0645e9aaff12eeadaed28dc36765ce75 Mon Sep 17 00:00:00 2001 From: Leigh McCulloch <351529+leighmcculloch@users.noreply.github.com> Date: Mon, 23 Sep 2024 10:41:58 +1000 Subject: [PATCH 07/23] rename Transaction trait to SignTx and collapse traits --- cmd/soroban-cli/src/commands/tx/sign.rs | 2 +- cmd/soroban-cli/src/config/secret.rs | 6 +- cmd/soroban-cli/src/config/sign_with.rs | 18 +---- cmd/soroban-cli/src/signer.rs | 2 +- cmd/soroban-cli/src/signer/types.rs | 91 +++++++++---------------- 5 files changed, 41 insertions(+), 78 deletions(-) diff --git a/cmd/soroban-cli/src/commands/tx/sign.rs b/cmd/soroban-cli/src/commands/tx/sign.rs index 913422b34..b3ae0433b 100644 --- a/cmd/soroban-cli/src/commands/tx/sign.rs +++ b/cmd/soroban-cli/src/commands/tx/sign.rs @@ -45,7 +45,7 @@ impl Cmd { ) -> Result { Ok(self .sign_with - .sign_txn_env(tx, &self.locator, &self.network.get(&self.locator)?, quiet) + .sign_tx_env(tx, &self.locator, &self.network.get(&self.locator)?, quiet) .await?) } } diff --git a/cmd/soroban-cli/src/config/secret.rs b/cmd/soroban-cli/src/config/secret.rs index 0293c056a..a4b1069c5 100644 --- a/cmd/soroban-cli/src/config/secret.rs +++ b/cmd/soroban-cli/src/config/secret.rs @@ -177,8 +177,8 @@ pub enum SignerKind { } #[async_trait::async_trait] -impl signer::Transaction for StellarSigner { - async fn sign_txn( +impl signer::SignTx for StellarSigner { + async fn sign_tx( &self, txn: &xdr::Transaction, network: &Network, @@ -188,7 +188,7 @@ impl signer::Transaction for StellarSigner { self.printer .infoln(format!("Signing transaction with hash: {hex_hash}")); match &self.kind { - SignerKind::Local(key) => key.sign_txn(txn, network).await, + SignerKind::Local(key) => key.sign_tx(txn, network).await, } } } diff --git a/cmd/soroban-cli/src/config/sign_with.rs b/cmd/soroban-cli/src/config/sign_with.rs index a2bc796bc..ca2918b04 100644 --- a/cmd/soroban-cli/src/config/sign_with.rs +++ b/cmd/soroban-cli/src/config/sign_with.rs @@ -1,8 +1,5 @@ use crate::{ - signer::{ - self, - types::{sign_txn_env, Transaction}, - }, + signer::{self, types::sign_tx_env}, xdr::TransactionEnvelope, }; use clap::arg; @@ -57,7 +54,7 @@ impl Args { Ok(locator.account(account)?) } - pub async fn sign_txn_env( + pub async fn sign_tx_env( &self, tx: TransactionEnvelope, locator: &locator::Args, @@ -66,15 +63,6 @@ impl Args { ) -> Result { let secret = self.secret(locator)?; let signer = secret.signer(self.hd_path, false, quiet)?; - self.sign_tx_env_with_signer(&signer, tx, network).await - } - - pub async fn sign_tx_env_with_signer( - &self, - signer: &(impl Transaction + std::marker::Sync), - tx_env: TransactionEnvelope, - network: &Network, - ) -> Result { - Ok(sign_txn_env(signer, tx_env, network).await?) + Ok(sign_tx_env(&signer, tx, network).await?) } } diff --git a/cmd/soroban-cli/src/signer.rs b/cmd/soroban-cli/src/signer.rs index 4f66a93c4..b2f205a63 100644 --- a/cmd/soroban-cli/src/signer.rs +++ b/cmd/soroban-cli/src/signer.rs @@ -11,7 +11,7 @@ use soroban_env_host::xdr::{ }; pub mod types; -pub use types::{LocalKey, Transaction, TransactionHash}; +pub use types::{LocalKey, SignTx}; #[derive(thiserror::Error, Debug)] pub enum Error { diff --git a/cmd/soroban-cli/src/signer/types.rs b/cmd/soroban-cli/src/signer/types.rs index 12ba97ddd..b756ccf81 100644 --- a/cmd/soroban-cli/src/signer/types.rs +++ b/cmd/soroban-cli/src/signer/types.rs @@ -41,59 +41,14 @@ pub fn transaction_hash( Ok(hash) } -#[async_trait::async_trait] -pub trait TransactionHash { - /// Sign a transaction hash with the given signer - /// # Errors - /// Returns an error if the source account is not found - async fn sign_txn_hash(&self, txn: [u8; 32]) -> Result; - - /// Return the signature hint required for a `DecoratedSignature`` - fn hint(&self) -> SignatureHint; -} - -/// A trait for signing Stellar transactions and Soroban authorization entries -#[async_trait::async_trait] -pub trait Transaction { - /// 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: &xdr::Transaction, - network: &Network, - ) -> Result; -} - -#[async_trait::async_trait] -impl Transaction for T -where - T: TransactionHash + Send + Sync, -{ - async fn sign_txn( - &self, - txn: &xdr::Transaction, - Network { - network_passphrase, .. - }: &Network, - ) -> Result { - let hash = transaction_hash(txn, network_passphrase)?; - let hint = self.hint(); - let signature = self.sign_txn_hash(hash).await?; - Ok(DecoratedSignature { hint, signature }) - } -} -pub async fn sign_txn_env( - signer: &(impl Transaction + std::marker::Sync), +pub async fn sign_tx_env( + signer: &(impl SignTx + 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 decorated_signature = signer.sign_tx(&tx, network).await?; let mut sigs = signatures.to_vec(); sigs.push(decorated_signature); Ok(TransactionEnvelope::Tx(TransactionV1Envelope { @@ -105,10 +60,26 @@ pub async fn sign_txn_env( } } -pub(crate) fn hash(network_passphrase: &str) -> xdr::Hash { +fn hash(network_passphrase: &str) -> xdr::Hash { xdr::Hash(Sha256::digest(network_passphrase.as_bytes()).into()) } +/// A trait for signing Stellar transactions and Soroban authorization entries +#[async_trait::async_trait] +pub trait SignTx { + /// 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_tx( + &self, + txn: &xdr::Transaction, + network: &Network, + ) -> Result; +} + pub struct LocalKey { key: ed25519_dalek::SigningKey, #[allow(dead_code)] @@ -122,17 +93,21 @@ impl LocalKey { } #[async_trait::async_trait] -impl TransactionHash for LocalKey { - async fn sign_txn_hash(&self, txn: [u8; 32]) -> Result { - let sig = self.key.sign(&txn); - Ok(Signature(sig.to_bytes().to_vec().try_into()?)) - } - - fn hint(&self) -> SignatureHint { - SignatureHint( +impl SignTx for LocalKey { + async fn sign_tx( + &self, + txn: &xdr::Transaction, + Network { + network_passphrase, .. + }: &Network, + ) -> Result { + let hash = transaction_hash(txn, network_passphrase)?; + let hint = SignatureHint( self.key.verifying_key().to_bytes()[28..] .try_into() .unwrap(), - ) + ); + let signature = Signature(self.key.sign(&hash).to_bytes().to_vec().try_into()?); + Ok(DecoratedSignature { hint, signature }) } } From 1470057fda1034e7ee699636a8765865836465e2 Mon Sep 17 00:00:00 2001 From: Leigh McCulloch <351529+leighmcculloch@users.noreply.github.com> Date: Mon, 23 Sep 2024 10:48:47 +1000 Subject: [PATCH 08/23] rename account to key --- cmd/soroban-cli/src/config/locator.rs | 6 +++--- cmd/soroban-cli/src/config/sign_with.rs | 10 +++------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/cmd/soroban-cli/src/config/locator.rs b/cmd/soroban-cli/src/config/locator.rs index a6394ed9c..bc167c977 100644 --- a/cmd/soroban-cli/src/config/locator.rs +++ b/cmd/soroban-cli/src/config/locator.rs @@ -217,11 +217,11 @@ 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::() { + pub fn key(&self, key_or_name: &str) -> Result { + if let Ok(signer) = key_or_name.parse::() { Ok(signer) } else { - self.read_identity(account_str) + self.read_identity(key_or_name) } } diff --git a/cmd/soroban-cli/src/config/sign_with.rs b/cmd/soroban-cli/src/config/sign_with.rs index ca2918b04..b86047a1c 100644 --- a/cmd/soroban-cli/src/config/sign_with.rs +++ b/cmd/soroban-cli/src/config/sign_with.rs @@ -7,7 +7,7 @@ use clap::arg; use super::{ locator, network::{self, Network}, - secret::{self, Secret}, + secret, }; #[derive(thiserror::Error, Debug)] @@ -49,11 +49,6 @@ pub struct Args { } impl Args { - pub fn secret(&self, locator: &locator::Args) -> Result { - let account = self.sign_with_key.as_deref().ok_or(Error::NoSignWithKey)?; - Ok(locator.account(account)?) - } - pub async fn sign_tx_env( &self, tx: TransactionEnvelope, @@ -61,7 +56,8 @@ impl Args { network: &Network, quiet: bool, ) -> Result { - let secret = self.secret(locator)?; + let key_or_name = self.sign_with_key.as_deref().ok_or(Error::NoSignWithKey)?; + let secret = locator.key(key_or_name)?; let signer = secret.signer(self.hd_path, false, quiet)?; Ok(sign_tx_env(&signer, tx, network).await?) } From 70837be350afb4a3f9944a41164ec401eb67ade4 Mon Sep 17 00:00:00 2001 From: Leigh McCulloch <351529+leighmcculloch@users.noreply.github.com> Date: Mon, 23 Sep 2024 10:50:23 +1000 Subject: [PATCH 09/23] update docs --- FULL_HELP_DOCS.md | 4 ++-- cmd/soroban-cli/src/commands/tx/mod.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/FULL_HELP_DOCS.md b/FULL_HELP_DOCS.md index b0ecf7514..9ba32c35a 100644 --- a/FULL_HELP_DOCS.md +++ b/FULL_HELP_DOCS.md @@ -1291,7 +1291,7 @@ 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 +* `sign` — Sign a transaction envelope appending the signature to the envelope @@ -1329,7 +1329,7 @@ Calculate the hash of a transaction envelope from stdin ## `stellar tx sign` -Sign a transaction envolope appending the signature to the envelope +Sign a transaction envelope appending the signature to the envelope **Usage:** `stellar tx sign [OPTIONS]` diff --git a/cmd/soroban-cli/src/commands/tx/mod.rs b/cmd/soroban-cli/src/commands/tx/mod.rs index 1f57cb79f..5f0f90c4c 100644 --- a/cmd/soroban-cli/src/commands/tx/mod.rs +++ b/cmd/soroban-cli/src/commands/tx/mod.rs @@ -13,7 +13,7 @@ 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 a transaction envelope appending the signature to the envelope Sign(sign::Cmd), } From bf7f88dd787ef3e19c90d2db9774cc85f7e9c7bf Mon Sep 17 00:00:00 2001 From: Leigh McCulloch <351529+leighmcculloch@users.noreply.github.com> Date: Mon, 23 Sep 2024 10:54:15 +1000 Subject: [PATCH 10/23] flatten layer of one liner --- cmd/soroban-cli/src/commands/tx/sign.rs | 27 +++++++++++-------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/cmd/soroban-cli/src/commands/tx/sign.rs b/cmd/soroban-cli/src/commands/tx/sign.rs index b3ae0433b..bae7cad70 100644 --- a/cmd/soroban-cli/src/commands/tx/sign.rs +++ b/cmd/soroban-cli/src/commands/tx/sign.rs @@ -1,7 +1,7 @@ use crate::{ commands::global, config::{locator, network, sign_with}, - xdr::{self, Limits, TransactionEnvelope, WriteXdr}, + xdr::{self, Limits, WriteXdr}, }; #[derive(thiserror::Error, Debug)] @@ -32,20 +32,17 @@ pub struct Cmd { impl Cmd { #[allow(clippy::unused_async)] pub async fn run(&self, global_args: &global::Args) -> Result<(), Error> { - let txn_env = super::xdr::tx_envelope_from_stdin()?; - let envelope = self.sign_tx_env(txn_env, global_args.quiet).await?; - println!("{}", envelope.to_xdr_base64(Limits::none())?.trim()); - Ok(()) - } - - pub async fn sign_tx_env( - &self, - tx: TransactionEnvelope, - quiet: bool, - ) -> Result { - Ok(self + let tx_env = super::xdr::tx_envelope_from_stdin()?; + let tx_env_signed = self .sign_with - .sign_tx_env(tx, &self.locator, &self.network.get(&self.locator)?, quiet) - .await?) + .sign_tx_env( + tx_env, + &self.locator, + &self.network.get(&self.locator)?, + global_args.quiet, + ) + .await?; + println!("{}", tx_env_signed.to_xdr_base64(Limits::none())?.trim()); + Ok(()) } } From c2a7cc2b55039e8cabb69ee8f7d811c2ee5e1969 Mon Sep 17 00:00:00 2001 From: Leigh McCulloch <351529+leighmcculloch@users.noreply.github.com> Date: Mon, 23 Sep 2024 11:22:13 +1000 Subject: [PATCH 11/23] undo move transaction_hash, remove trait --- cmd/soroban-cli/src/commands/tx/hash.rs | 2 +- cmd/soroban-cli/src/config/secret.rs | 4 +-- cmd/soroban-cli/src/print.rs | 2 +- cmd/soroban-cli/src/signer.rs | 16 +++-------- cmd/soroban-cli/src/signer/types.rs | 37 ++++--------------------- cmd/soroban-cli/src/utils.rs | 14 +++++++++- 6 files changed, 26 insertions(+), 49 deletions(-) diff --git a/cmd/soroban-cli/src/commands/tx/hash.rs b/cmd/soroban-cli/src/commands/tx/hash.rs index dfffb623e..8d8ec6d82 100644 --- a/cmd/soroban-cli/src/commands/tx/hash.rs +++ b/cmd/soroban-cli/src/commands/tx/hash.rs @@ -1,6 +1,6 @@ use hex; -use crate::{commands::global, config::network, signer::types::transaction_hash}; +use crate::{commands::global, config::network, utils::transaction_hash}; #[derive(thiserror::Error, Debug)] pub enum Error { diff --git a/cmd/soroban-cli/src/config/secret.rs b/cmd/soroban-cli/src/config/secret.rs index a4b1069c5..78877faf3 100644 --- a/cmd/soroban-cli/src/config/secret.rs +++ b/cmd/soroban-cli/src/config/secret.rs @@ -4,7 +4,7 @@ use std::{io::Write, str::FromStr}; use stellar_strkey::ed25519::{PrivateKey, PublicKey}; use crate::print::Print; -use crate::signer::types::transaction_hash; +use crate::utils::transaction_hash; use crate::xdr::{self, DecoratedSignature}; use crate::{ signer::{self, LocalKey}, @@ -188,7 +188,7 @@ impl signer::SignTx for StellarSigner { self.printer .infoln(format!("Signing transaction with hash: {hex_hash}")); match &self.kind { - SignerKind::Local(key) => key.sign_tx(txn, network).await, + SignerKind::Local(key) => key.sign_tx_hash(tx_hash), } } } diff --git a/cmd/soroban-cli/src/print.rs b/cmd/soroban-cli/src/print.rs index 2a95267d0..5b98687bd 100644 --- a/cmd/soroban-cli/src/print.rs +++ b/cmd/soroban-cli/src/print.rs @@ -3,7 +3,7 @@ use std::{env, fmt::Display}; use soroban_env_host::xdr::{Error as XdrError, Transaction}; use crate::{ - config::network::Network, signer::types::transaction_hash, utils::explorer_url_for_transaction, + config::network::Network, utils::explorer_url_for_transaction, utils::transaction_hash, }; const TERMS: &[&str] = &["Apple_Terminal", "vscode"]; diff --git a/cmd/soroban-cli/src/signer.rs b/cmd/soroban-cli/src/signer.rs index b2f205a63..d56e2ab1d 100644 --- a/cmd/soroban-cli/src/signer.rs +++ b/cmd/soroban-cli/src/signer.rs @@ -5,12 +5,12 @@ 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, TransactionEnvelope, - TransactionSignaturePayload, TransactionSignaturePayloadTaggedTransaction, - TransactionV1Envelope, Uint256, WriteXdr, + SorobanAuthorizedFunction, SorobanCredentials, TransactionEnvelope, TransactionV1Envelope, + Uint256, WriteXdr, }; pub mod types; +use crate::utils::transaction_hash; pub use types::{LocalKey, SignTx}; #[derive(thiserror::Error, Debug)] @@ -197,7 +197,7 @@ pub fn sign_tx( tx: &xdr::Transaction, network_passphrase: &str, ) -> Result { - let tx_hash = hash(tx, network_passphrase)?; + let tx_hash = transaction_hash(tx, network_passphrase)?; let tx_signature = key.sign(&tx_hash); let decorated_signature = DecoratedSignature { @@ -210,11 +210,3 @@ pub fn sign_tx( signatures: [decorated_signature].try_into()?, })) } - -pub fn hash(tx: &xdr::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()) -} diff --git a/cmd/soroban-cli/src/signer/types.rs b/cmd/soroban-cli/src/signer/types.rs index b756ccf81..e44935b31 100644 --- a/cmd/soroban-cli/src/signer/types.rs +++ b/cmd/soroban-cli/src/signer/types.rs @@ -1,12 +1,10 @@ use ed25519_dalek::ed25519::signature::Signer; -use sha2::{Digest, Sha256}; use crate::{ config::network::Network, xdr::{ - self, DecoratedSignature, Limits, Signature, SignatureHint, TransactionEnvelope, - TransactionSignaturePayload, TransactionSignaturePayloadTaggedTransaction, - TransactionV1Envelope, WriteXdr, + self, DecoratedSignature, Signature, SignatureHint, TransactionEnvelope, + TransactionV1Envelope, }, }; @@ -28,19 +26,6 @@ pub enum Error { UnsupportedTransactionEnvelopeType, } -/// Calculate the hash of a Transaction -pub fn transaction_hash( - txn: &xdr::Transaction, - network_passphrase: &str, -) -> Result<[u8; 32], xdr::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) -} - pub async fn sign_tx_env( signer: &(impl SignTx + std::marker::Sync), txn_env: TransactionEnvelope, @@ -60,10 +45,6 @@ pub async fn sign_tx_env( } } -fn hash(network_passphrase: &str) -> xdr::Hash { - xdr::Hash(Sha256::digest(network_passphrase.as_bytes()).into()) -} - /// A trait for signing Stellar transactions and Soroban authorization entries #[async_trait::async_trait] pub trait SignTx { @@ -92,22 +73,14 @@ impl LocalKey { } } -#[async_trait::async_trait] -impl SignTx for LocalKey { - async fn sign_tx( - &self, - txn: &xdr::Transaction, - Network { - network_passphrase, .. - }: &Network, - ) -> Result { - let hash = transaction_hash(txn, network_passphrase)?; +impl LocalKey { + pub fn sign_tx_hash(&self, tx_hash: [u8; 32]) -> Result { let hint = SignatureHint( self.key.verifying_key().to_bytes()[28..] .try_into() .unwrap(), ); - let signature = Signature(self.key.sign(&hash).to_bytes().to_vec().try_into()?); + let signature = Signature(self.key.sign(&tx_hash).to_bytes().to_vec().try_into()?); Ok(DecoratedSignature { hint, signature }) } } diff --git a/cmd/soroban-cli/src/utils.rs b/cmd/soroban-cli/src/utils.rs index 75a18d5e1..f5827f75b 100644 --- a/cmd/soroban-cli/src/utils.rs +++ b/cmd/soroban-cli/src/utils.rs @@ -4,7 +4,8 @@ use stellar_strkey::ed25519::PrivateKey; use soroban_env_host::xdr::{ Asset, ContractIdPreimage, Error as XdrError, Hash, HashIdPreimage, HashIdPreimageContractId, - Limits, ScMap, ScMapEntry, ScVal, WriteXdr, + Limits, ScMap, ScMapEntry, ScVal, Transaction, TransactionSignaturePayload, + TransactionSignaturePayloadTaggedTransaction, WriteXdr, }; pub use soroban_spec_tools::contract as contract_spec; @@ -18,6 +19,17 @@ pub fn contract_hash(contract: &[u8]) -> Result { Ok(Hash(Sha256::digest(contract).into())) } +/// # Errors +/// +/// Might return an error +pub fn transaction_hash(tx: &Transaction, network_passphrase: &str) -> Result<[u8; 32], XdrError> { + 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()) +} + static EXPLORERS: phf::Map<&'static str, &'static str> = phf_map! { "Test SDF Network ; September 2015" => "https://stellar.expert/explorer/testnet", "Public Global Stellar Network ; September 2015" => "https://stellar.expert/explorer/public", From df299e80e26fc317775e2a9ee6a5d156c26951f1 Mon Sep 17 00:00:00 2001 From: Leigh McCulloch <351529+leighmcculloch@users.noreply.github.com> Date: Mon, 23 Sep 2024 11:24:07 +1000 Subject: [PATCH 12/23] undo change --- cmd/soroban-cli/src/signer.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/cmd/soroban-cli/src/signer.rs b/cmd/soroban-cli/src/signer.rs index d56e2ab1d..d38b8e29a 100644 --- a/cmd/soroban-cli/src/signer.rs +++ b/cmd/soroban-cli/src/signer.rs @@ -5,8 +5,8 @@ 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, TransactionEnvelope, TransactionV1Envelope, - Uint256, WriteXdr, + SorobanAuthorizedFunction, SorobanCredentials, Transaction, TransactionEnvelope, + TransactionV1Envelope, Uint256, WriteXdr, }; pub mod types; @@ -29,7 +29,7 @@ pub enum Error { Xdr(#[from] xdr::Error), } -fn requires_auth(txn: &xdr::Transaction) -> Option { +fn requires_auth(txn: &Transaction) -> Option { let [op @ Operation { body: OperationBody::InvokeHostFunction(InvokeHostFunctionOp { auth, .. }), .. @@ -47,12 +47,12 @@ fn requires_auth(txn: &xdr::Transaction) -> Option { // 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: &xdr::Transaction, + raw: &Transaction, source_key: &ed25519_dalek::SigningKey, signers: &[ed25519_dalek::SigningKey], signature_expiration_ledger: u32, network_passphrase: &str, -) -> Result, Error> { +) -> Result, Error> { let mut tx = raw.clone(); let Some(mut op) = requires_auth(&tx) else { return Ok(None); @@ -194,7 +194,7 @@ fn sign_soroban_authorization_entry( pub fn sign_tx( key: &ed25519_dalek::SigningKey, - tx: &xdr::Transaction, + tx: &Transaction, network_passphrase: &str, ) -> Result { let tx_hash = transaction_hash(tx, network_passphrase)?; From 9beed0502903dbae2ec88f420d62e688a5fa05f8 Mon Sep 17 00:00:00 2001 From: Leigh McCulloch <351529+leighmcculloch@users.noreply.github.com> Date: Mon, 23 Sep 2024 12:15:20 +1000 Subject: [PATCH 13/23] rename Stellar error to Signer --- cmd/soroban-cli/src/config/secret.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/soroban-cli/src/config/secret.rs b/cmd/soroban-cli/src/config/secret.rs index 78877faf3..e2578c81b 100644 --- a/cmd/soroban-cli/src/config/secret.rs +++ b/cmd/soroban-cli/src/config/secret.rs @@ -30,7 +30,7 @@ pub enum Error { #[error("Invalid address {0}")] InvalidAddress(String), #[error(transparent)] - Stellar(#[from] signer::Error), + Signer(#[from] signer::Error), } #[derive(Debug, clap::Args, Clone)] From d3e22ccb2703b85657c42a4b0e66d44fe41654ee Mon Sep 17 00:00:00 2001 From: Leigh McCulloch <351529+leighmcculloch@users.noreply.github.com> Date: Mon, 23 Sep 2024 12:42:30 +1000 Subject: [PATCH 14/23] move StellarSigner and eliminate trait --- cmd/soroban-cli/src/config/secret.rs | 32 +------------------ cmd/soroban-cli/src/signer.rs | 2 +- cmd/soroban-cli/src/signer/types.rs | 47 +++++++++++++++++----------- 3 files changed, 31 insertions(+), 50 deletions(-) diff --git a/cmd/soroban-cli/src/config/secret.rs b/cmd/soroban-cli/src/config/secret.rs index e2578c81b..6b76e7418 100644 --- a/cmd/soroban-cli/src/config/secret.rs +++ b/cmd/soroban-cli/src/config/secret.rs @@ -4,15 +4,11 @@ use std::{io::Write, str::FromStr}; use stellar_strkey::ed25519::{PrivateKey, PublicKey}; use crate::print::Print; -use crate::utils::transaction_hash; -use crate::xdr::{self, DecoratedSignature}; use crate::{ - signer::{self, LocalKey}, + signer::{self, LocalKey, SignerKind, StellarSigner}, utils, }; -use super::network::Network; - #[derive(thiserror::Error, Debug)] pub enum Error { #[error("invalid secret key")] @@ -167,32 +163,6 @@ impl Secret { } } -pub struct StellarSigner { - kind: SignerKind, - printer: Print, -} - -pub enum SignerKind { - Local(LocalKey), -} - -#[async_trait::async_trait] -impl signer::SignTx for StellarSigner { - async fn sign_tx( - &self, - txn: &xdr::Transaction, - network: &Network, - ) -> Result { - let tx_hash = transaction_hash(txn, &network.network_passphrase)?; - let hex_hash = hex::encode(tx_hash); - self.printer - .infoln(format!("Signing transaction with hash: {hex_hash}")); - match &self.kind { - SignerKind::Local(key) => key.sign_tx_hash(tx_hash), - } - } -} - 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/signer.rs b/cmd/soroban-cli/src/signer.rs index d38b8e29a..9097b4a5f 100644 --- a/cmd/soroban-cli/src/signer.rs +++ b/cmd/soroban-cli/src/signer.rs @@ -11,7 +11,7 @@ use soroban_env_host::xdr::{ pub mod types; use crate::utils::transaction_hash; -pub use types::{LocalKey, SignTx}; +pub use types::{LocalKey, SignerKind, StellarSigner}; #[derive(thiserror::Error, Debug)] pub enum Error { diff --git a/cmd/soroban-cli/src/signer/types.rs b/cmd/soroban-cli/src/signer/types.rs index e44935b31..73bff030c 100644 --- a/cmd/soroban-cli/src/signer/types.rs +++ b/cmd/soroban-cli/src/signer/types.rs @@ -2,6 +2,8 @@ use ed25519_dalek::ed25519::signature::Signer; use crate::{ config::network::Network, + print::Print, + utils::transaction_hash, xdr::{ self, DecoratedSignature, Signature, SignatureHint, TransactionEnvelope, TransactionV1Envelope, @@ -26,14 +28,39 @@ pub enum Error { UnsupportedTransactionEnvelopeType, } +pub struct StellarSigner { + pub kind: SignerKind, + pub printer: Print, +} + +pub enum SignerKind { + Local(LocalKey), +} + +impl StellarSigner { + pub fn sign_tx( + &self, + txn: &xdr::Transaction, + network: &Network, + ) -> Result { + let tx_hash = transaction_hash(txn, &network.network_passphrase)?; + let hex_hash = hex::encode(tx_hash); + self.printer + .infoln(format!("Signing transaction with hash: {hex_hash}")); + match &self.kind { + SignerKind::Local(key) => key.sign_tx_hash(tx_hash), + } + } +} + pub async fn sign_tx_env( - signer: &(impl SignTx + std::marker::Sync), + signer: &StellarSigner, txn_env: TransactionEnvelope, network: &Network, ) -> Result { match txn_env { TransactionEnvelope::Tx(TransactionV1Envelope { tx, signatures }) => { - let decorated_signature = signer.sign_tx(&tx, network).await?; + let decorated_signature = signer.sign_tx(&tx, network)?; let mut sigs = signatures.to_vec(); sigs.push(decorated_signature); Ok(TransactionEnvelope::Tx(TransactionV1Envelope { @@ -45,22 +72,6 @@ pub async fn sign_tx_env( } } -/// A trait for signing Stellar transactions and Soroban authorization entries -#[async_trait::async_trait] -pub trait SignTx { - /// 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_tx( - &self, - txn: &xdr::Transaction, - network: &Network, - ) -> Result; -} - pub struct LocalKey { key: ed25519_dalek::SigningKey, #[allow(dead_code)] From ded7eba52ccd60747da826619833c6c613812a2d Mon Sep 17 00:00:00 2001 From: Leigh McCulloch <351529+leighmcculloch@users.noreply.github.com> Date: Mon, 23 Sep 2024 13:15:43 +1000 Subject: [PATCH 15/23] handle instead of panic on error --- cmd/soroban-cli/src/signer/types.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/cmd/soroban-cli/src/signer/types.rs b/cmd/soroban-cli/src/signer/types.rs index 73bff030c..610674637 100644 --- a/cmd/soroban-cli/src/signer/types.rs +++ b/cmd/soroban-cli/src/signer/types.rs @@ -86,11 +86,7 @@ impl LocalKey { impl LocalKey { pub fn sign_tx_hash(&self, tx_hash: [u8; 32]) -> Result { - let hint = SignatureHint( - self.key.verifying_key().to_bytes()[28..] - .try_into() - .unwrap(), - ); + let hint = SignatureHint(self.key.verifying_key().to_bytes()[28..].try_into()?); let signature = Signature(self.key.sign(&tx_hash).to_bytes().to_vec().try_into()?); Ok(DecoratedSignature { hint, signature }) } From 015435b96d85e1d4b96c657d33f00afcdb6d9efe Mon Sep 17 00:00:00 2001 From: Leigh McCulloch <351529+leighmcculloch@users.noreply.github.com> Date: Mon, 23 Sep 2024 14:43:35 +1000 Subject: [PATCH 16/23] move types into signer and replace existing signing logic --- cmd/soroban-cli/src/commands/tx/sign.rs | 15 ++-- cmd/soroban-cli/src/config/mod.rs | 14 ++-- cmd/soroban-cli/src/config/sign_with.rs | 11 ++- cmd/soroban-cli/src/signer.rs | 88 ++++++++++++++++++----- cmd/soroban-cli/src/signer/types.rs | 93 ------------------------- 5 files changed, 88 insertions(+), 133 deletions(-) delete mode 100644 cmd/soroban-cli/src/signer/types.rs diff --git a/cmd/soroban-cli/src/commands/tx/sign.rs b/cmd/soroban-cli/src/commands/tx/sign.rs index bae7cad70..f5987ab34 100644 --- a/cmd/soroban-cli/src/commands/tx/sign.rs +++ b/cmd/soroban-cli/src/commands/tx/sign.rs @@ -33,15 +33,12 @@ impl Cmd { #[allow(clippy::unused_async)] pub async fn run(&self, global_args: &global::Args) -> Result<(), Error> { let tx_env = super::xdr::tx_envelope_from_stdin()?; - let tx_env_signed = self - .sign_with - .sign_tx_env( - tx_env, - &self.locator, - &self.network.get(&self.locator)?, - global_args.quiet, - ) - .await?; + let tx_env_signed = self.sign_with.sign_tx_env( + tx_env, + &self.locator, + &self.network.get(&self.locator)?, + global_args.quiet, + )?; println!("{}", tx_env_signed.to_xdr_base64(Limits::none())?.trim()); Ok(()) } diff --git a/cmd/soroban-cli/src/config/mod.rs b/cmd/soroban-cli/src/config/mod.rs index 86055a101..c0dc2d694 100644 --- a/cmd/soroban-cli/src/config/mod.rs +++ b/cmd/soroban-cli/src/config/mod.rs @@ -4,9 +4,11 @@ use clap::{arg, command}; use serde::{Deserialize, Serialize}; use soroban_rpc::Client; +use soroban_sdk::xdr::{TransactionV1Envelope, VecM}; use crate::{ - signer, + print::Print, + signer::{self, LocalKey, SignerKind, StellarSigner}, xdr::{Transaction, TransactionEnvelope}, Pwd, }; @@ -66,10 +68,12 @@ impl Args { #[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)?) + let network = &self.get_network()?; + let signer = StellarSigner { + kind: SignerKind::Local(LocalKey::new(key, false)), + printer: Print::new(false), + }; + Ok(signer.sign_tx(tx, network)?) } pub async fn sign_soroban_authorizations( diff --git a/cmd/soroban-cli/src/config/sign_with.rs b/cmd/soroban-cli/src/config/sign_with.rs index b86047a1c..c4f4e6c95 100644 --- a/cmd/soroban-cli/src/config/sign_with.rs +++ b/cmd/soroban-cli/src/config/sign_with.rs @@ -1,7 +1,4 @@ -use crate::{ - signer::{self, types::sign_tx_env}, - xdr::TransactionEnvelope, -}; +use crate::{signer, xdr::TransactionEnvelope}; use clap::arg; use super::{ @@ -15,7 +12,7 @@ pub enum Error { #[error(transparent)] Network(#[from] network::Error), #[error(transparent)] - Signer(#[from] signer::types::Error), + Signer(#[from] signer::Error), #[error(transparent)] Secret(#[from] secret::Error), #[error(transparent)] @@ -49,7 +46,7 @@ pub struct Args { } impl Args { - pub async fn sign_tx_env( + pub fn sign_tx_env( &self, tx: TransactionEnvelope, locator: &locator::Args, @@ -59,6 +56,6 @@ impl Args { let key_or_name = self.sign_with_key.as_deref().ok_or(Error::NoSignWithKey)?; let secret = locator.key(key_or_name)?; let signer = secret.signer(self.hd_path, false, quiet)?; - Ok(sign_tx_env(&signer, tx, network).await?) + Ok(signer.sign_tx_env(tx, network)?) } } diff --git a/cmd/soroban-cli/src/signer.rs b/cmd/soroban-cli/src/signer.rs index 9097b4a5f..eae9fae30 100644 --- a/cmd/soroban-cli/src/signer.rs +++ b/cmd/soroban-cli/src/signer.rs @@ -6,12 +6,10 @@ use soroban_env_host::xdr::{ InvokeHostFunctionOp, Limits, Operation, OperationBody, PublicKey, ScAddress, ScMap, ScSymbol, ScVal, Signature, SignatureHint, SorobanAddressCredentials, SorobanAuthorizationEntry, SorobanAuthorizedFunction, SorobanCredentials, Transaction, TransactionEnvelope, - TransactionV1Envelope, Uint256, WriteXdr, + TransactionV1Envelope, Uint256, VecM, WriteXdr, }; -pub mod types; -use crate::utils::transaction_hash; -pub use types::{LocalKey, SignerKind, StellarSigner}; +use crate::{config::network::Network, print::Print, utils::transaction_hash}; #[derive(thiserror::Error, Debug)] pub enum Error { @@ -27,6 +25,8 @@ pub enum Error { UserCancelledSigning, #[error(transparent)] Xdr(#[from] xdr::Error), + #[error("Only Transaction envelope V1 type is supported")] + UnsupportedTransactionEnvelopeType, } fn requires_auth(txn: &Transaction) -> Option { @@ -192,21 +192,71 @@ fn sign_soroban_authorization_entry( Ok(auth) } -pub fn sign_tx( - 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); +pub struct StellarSigner { + pub kind: SignerKind, + pub printer: Print, +} - let decorated_signature = DecoratedSignature { - hint: SignatureHint(key.verifying_key().to_bytes()[28..].try_into()?), - signature: Signature(tx_signature.to_bytes().try_into()?), - }; +pub enum SignerKind { + Local(LocalKey), +} + +impl StellarSigner { + pub fn sign_tx( + &self, + tx: Transaction, + network: &Network, + ) -> Result { + let tx_env = TransactionEnvelope::Tx(TransactionV1Envelope { + tx, + signatures: VecM::default(), + }); + self.sign_tx_env(tx_env, network) + } + + pub fn sign_tx_env( + &self, + tx_env: TransactionEnvelope, + network: &Network, + ) -> Result { + match tx_env { + TransactionEnvelope::Tx(TransactionV1Envelope { tx, signatures }) => { + let tx_hash = transaction_hash(&tx, &network.network_passphrase)?; + self.printer.infoln(format!( + "Signing transaction with hash: {}", + hex::encode(tx_hash) + )); + let decorated_signature = match &self.kind { + SignerKind::Local(key) => key.sign_tx_hash(tx_hash)?, + }; + let mut sigs = signatures.into_vec(); + sigs.push(decorated_signature); + Ok(TransactionEnvelope::Tx(TransactionV1Envelope { + tx, + signatures: sigs.try_into()?, + })) + } + _ => Err(Error::UnsupportedTransactionEnvelopeType), + } + } +} + +pub struct LocalKey { + key: ed25519_dalek::SigningKey, + #[allow(dead_code)] + prompt: bool, +} + +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()?, - })) +impl LocalKey { + pub fn sign_tx_hash(&self, tx_hash: [u8; 32]) -> Result { + let hint = SignatureHint(self.key.verifying_key().to_bytes()[28..].try_into()?); + let signature = Signature(self.key.sign(&tx_hash).to_bytes().to_vec().try_into()?); + Ok(DecoratedSignature { hint, signature }) + } } diff --git a/cmd/soroban-cli/src/signer/types.rs b/cmd/soroban-cli/src/signer/types.rs deleted file mode 100644 index 610674637..000000000 --- a/cmd/soroban-cli/src/signer/types.rs +++ /dev/null @@ -1,93 +0,0 @@ -use ed25519_dalek::ed25519::signature::Signer; - -use crate::{ - config::network::Network, - print::Print, - utils::transaction_hash, - xdr::{ - self, DecoratedSignature, Signature, SignatureHint, TransactionEnvelope, - TransactionV1Envelope, - }, -}; - -#[derive(thiserror::Error, Debug)] -pub enum Error { - #[error("Contract addresses are not supported to sign auth entries {address}")] - ContractAddressAreNotSupported { address: String }, - #[error(transparent)] - Ed25519(#[from] ed25519_dalek::SignatureError), - #[error("Missing signing key for account {address}")] - MissingSignerForAddress { address: String }, - #[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, -} - -pub struct StellarSigner { - pub kind: SignerKind, - pub printer: Print, -} - -pub enum SignerKind { - Local(LocalKey), -} - -impl StellarSigner { - pub fn sign_tx( - &self, - txn: &xdr::Transaction, - network: &Network, - ) -> Result { - let tx_hash = transaction_hash(txn, &network.network_passphrase)?; - let hex_hash = hex::encode(tx_hash); - self.printer - .infoln(format!("Signing transaction with hash: {hex_hash}")); - match &self.kind { - SignerKind::Local(key) => key.sign_tx_hash(tx_hash), - } - } -} - -pub async fn sign_tx_env( - signer: &StellarSigner, - txn_env: TransactionEnvelope, - network: &Network, -) -> Result { - match txn_env { - TransactionEnvelope::Tx(TransactionV1Envelope { tx, signatures }) => { - let decorated_signature = signer.sign_tx(&tx, network)?; - let mut sigs = signatures.to_vec(); - sigs.push(decorated_signature); - Ok(TransactionEnvelope::Tx(TransactionV1Envelope { - tx, - signatures: sigs.try_into()?, - })) - } - _ => Err(Error::UnsupportedTransactionEnvelopeType), - } -} - -pub struct LocalKey { - key: ed25519_dalek::SigningKey, - #[allow(dead_code)] - prompt: bool, -} - -impl LocalKey { - pub fn new(key: ed25519_dalek::SigningKey, prompt: bool) -> Self { - Self { key, prompt } - } -} - -impl LocalKey { - pub fn sign_tx_hash(&self, tx_hash: [u8; 32]) -> Result { - let hint = SignatureHint(self.key.verifying_key().to_bytes()[28..].try_into()?); - let signature = Signature(self.key.sign(&tx_hash).to_bytes().to_vec().try_into()?); - Ok(DecoratedSignature { hint, signature }) - } -} From baf7036cee7506d7b5cdcf1d56de1ebc0aa8069e Mon Sep 17 00:00:00 2001 From: Leigh McCulloch <351529+leighmcculloch@users.noreply.github.com> Date: Mon, 23 Sep 2024 21:44:22 +1000 Subject: [PATCH 17/23] address clippy --- cmd/soroban-cli/src/config/mod.rs | 5 ++--- cmd/soroban-cli/src/config/secret.rs | 11 +++-------- cmd/soroban-cli/src/signer.rs | 7 ++++--- 3 files changed, 9 insertions(+), 14 deletions(-) diff --git a/cmd/soroban-cli/src/config/mod.rs b/cmd/soroban-cli/src/config/mod.rs index c0dc2d694..a109ea845 100644 --- a/cmd/soroban-cli/src/config/mod.rs +++ b/cmd/soroban-cli/src/config/mod.rs @@ -4,11 +4,10 @@ use clap::{arg, command}; use serde::{Deserialize, Serialize}; use soroban_rpc::Client; -use soroban_sdk::xdr::{TransactionV1Envelope, VecM}; use crate::{ print::Print, - signer::{self, LocalKey, SignerKind, StellarSigner}, + signer::{self, LocalKey, Signer, SignerKind}, xdr::{Transaction, TransactionEnvelope}, Pwd, }; @@ -69,7 +68,7 @@ impl Args { pub async fn sign(&self, tx: Transaction) -> Result { let key = self.key_pair()?; let network = &self.get_network()?; - let signer = StellarSigner { + let signer = Signer { kind: SignerKind::Local(LocalKey::new(key, false)), printer: Print::new(false), }; diff --git a/cmd/soroban-cli/src/config/secret.rs b/cmd/soroban-cli/src/config/secret.rs index 6b76e7418..c7c5ebbcc 100644 --- a/cmd/soroban-cli/src/config/secret.rs +++ b/cmd/soroban-cli/src/config/secret.rs @@ -5,7 +5,7 @@ use stellar_strkey::ed25519::{PrivateKey, PublicKey}; use crate::print::Print; use crate::{ - signer::{self, LocalKey, SignerKind, StellarSigner}, + signer::{self, LocalKey, Signer, SignerKind}, utils, }; @@ -126,18 +126,13 @@ impl Secret { )?) } - pub fn signer( - &self, - index: Option, - prompt: bool, - quiet: bool, - ) -> Result { + pub fn signer(&self, index: Option, prompt: bool, quiet: bool) -> Result { let kind = match self { Secret::SecretKey { .. } | Secret::SeedPhrase { .. } => { SignerKind::Local(LocalKey::new(self.key_pair(index)?, prompt)) } }; - Ok(StellarSigner { + Ok(Signer { kind, printer: Print::new(quiet), }) diff --git a/cmd/soroban-cli/src/signer.rs b/cmd/soroban-cli/src/signer.rs index eae9fae30..b05593513 100644 --- a/cmd/soroban-cli/src/signer.rs +++ b/cmd/soroban-cli/src/signer.rs @@ -1,4 +1,4 @@ -use ed25519_dalek::ed25519::signature::Signer; +use ed25519_dalek::ed25519::signature::Signer as _; use sha2::{Digest, Sha256}; use soroban_env_host::xdr::{ @@ -192,16 +192,17 @@ fn sign_soroban_authorization_entry( Ok(auth) } -pub struct StellarSigner { +pub struct Signer { pub kind: SignerKind, pub printer: Print, } +#[allow(clippy::module_name_repetitions)] pub enum SignerKind { Local(LocalKey), } -impl StellarSigner { +impl Signer { pub fn sign_tx( &self, tx: Transaction, From f3fbff7e00efcc6f3320712dc5abb7ffd86d9f97 Mon Sep 17 00:00:00 2001 From: Leigh McCulloch <351529+leighmcculloch@users.noreply.github.com> Date: Mon, 23 Sep 2024 04:45:32 -0700 Subject: [PATCH 18/23] lab.stellar.org --- cmd/soroban-cli/src/config/sign_with.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/soroban-cli/src/config/sign_with.rs b/cmd/soroban-cli/src/config/sign_with.rs index c4f4e6c95..089e35c14 100644 --- a/cmd/soroban-cli/src/config/sign_with.rs +++ b/cmd/soroban-cli/src/config/sign_with.rs @@ -31,7 +31,7 @@ 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_KEY")] pub sign_with_key: Option, - /// Sign with labratory + /// Sign with https://lab.stellar.org #[arg( long, conflicts_with = "sign_with_key", From d56fa098929b6e2978ca60460dc9939147ffa146 Mon Sep 17 00:00:00 2001 From: Leigh McCulloch <351529+leighmcculloch@users.noreply.github.com> Date: Mon, 23 Sep 2024 22:05:14 +1000 Subject: [PATCH 19/23] clippy --- cmd/soroban-cli/src/config/sign_with.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/soroban-cli/src/config/sign_with.rs b/cmd/soroban-cli/src/config/sign_with.rs index 089e35c14..7a028baf7 100644 --- a/cmd/soroban-cli/src/config/sign_with.rs +++ b/cmd/soroban-cli/src/config/sign_with.rs @@ -31,7 +31,7 @@ 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_KEY")] pub sign_with_key: Option, - /// Sign with https://lab.stellar.org + /// Sign with #[arg( long, conflicts_with = "sign_with_key", From 25102a079533235d54ab4296db24cdcbb8479619 Mon Sep 17 00:00:00 2001 From: Leigh McCulloch <351529+leighmcculloch@users.noreply.github.com> Date: Tue, 24 Sep 2024 10:53:18 +1000 Subject: [PATCH 20/23] remove unnecessary trim --- cmd/soroban-cli/src/commands/tx/sign.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/soroban-cli/src/commands/tx/sign.rs b/cmd/soroban-cli/src/commands/tx/sign.rs index f5987ab34..92fef7417 100644 --- a/cmd/soroban-cli/src/commands/tx/sign.rs +++ b/cmd/soroban-cli/src/commands/tx/sign.rs @@ -39,7 +39,7 @@ impl Cmd { &self.network.get(&self.locator)?, global_args.quiet, )?; - println!("{}", tx_env_signed.to_xdr_base64(Limits::none())?.trim()); + println!("{}", tx_env_signed.to_xdr_base64(Limits::none())?); Ok(()) } } From 02245e7dd4905217b6fedba5e59871cec81c0abe Mon Sep 17 00:00:00 2001 From: Leigh McCulloch <351529+leighmcculloch@users.noreply.github.com> Date: Tue, 24 Sep 2024 12:04:02 +1000 Subject: [PATCH 21/23] fix e2e test run and tweak --- Makefile | 4 +- .../soroban-test/tests/it/integration/tx.rs | 49 +++++++------------ 2 files changed, 20 insertions(+), 33 deletions(-) diff --git a/Makefile b/Makefile index 7e307b16c..6d55aacc4 100644 --- a/Makefile +++ b/Makefile @@ -15,6 +15,8 @@ endif REPOSITORY_BRANCH := "$(shell git rev-parse --abbrev-ref HEAD)" BUILD_TIMESTAMP ?= $(shell date '+%Y-%m-%dT%H:%M:%S') +SOROBAN_PORT?=8000 + # The following works around incompatibility between the rust and the go linkers - # the rust would generate an object file with min-version of 13.0 where-as the go # compiler would generate a binary compatible with 12.3 and up. To align these @@ -53,7 +55,7 @@ test: build-test cargo test e2e-test: - cargo test --test it -- --ignored + cargo test --features it --test it -- integration check: cargo clippy --all-targets diff --git a/cmd/crates/soroban-test/tests/it/integration/tx.rs b/cmd/crates/soroban-test/tests/it/integration/tx.rs index 7d7b65116..c3d2cebd6 100644 --- a/cmd/crates/soroban-test/tests/it/integration/tx.rs +++ b/cmd/crates/soroban-test/tests/it/integration/tx.rs @@ -1,8 +1,7 @@ -use soroban_rpc::GetTransactionResponse; use soroban_sdk::xdr::{Limits, ReadXdr, TransactionEnvelope, WriteXdr}; use soroban_test::{AssertExt, TestEnv}; -use crate::integration::util::{deploy_contract, deploy_hello, DeployKind, HELLO_WORLD}; +use crate::integration::util::{deploy_contract, DeployKind, HELLO_WORLD}; #[tokio::test] async fn simulate() { @@ -52,7 +51,7 @@ async fn txn_hash() { } #[tokio::test] -async fn send() { +async fn build_simulate_sign_send() { let sandbox = &TestEnv::new(); sandbox .new_assert_cmd("contract") @@ -61,36 +60,22 @@ async fn send() { .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); + let tx_simulated = deploy_contract(sandbox, HELLO_WORLD, DeployKind::SimOnly).await; + dbg!("{tx_simulated}"); - println!( - "Transaction to send:\n{}", - tx_env.to_xdr_base64(Limits::none()).unwrap() - ); - - let rpc_result = send_manually(sandbox, &tx_env).await; - assert_eq!(rpc_result.status, "SUCCESS"); -} + let tx_signed = sandbox + .new_assert_cmd("tx") + .arg("sign") + .arg("--sign-with-key=test") + .write_stdin(tx_simulated.as_bytes()) + .assert() + .success() + .stdout_as_str(); + dbg!("{tx_signed}"); -async fn send_manually(sandbox: &TestEnv, tx_env: &TransactionEnvelope) -> GetTransactionResponse { + // TODO: Replace with calling tx send when that command is added. + let tx_signed = TransactionEnvelope::from_xdr_base64(tx_signed, Limits::none()).unwrap(); let client = soroban_rpc::Client::new(&sandbox.rpc_url).unwrap(); - client.send_transaction_polling(tx_env).await.unwrap() -} - -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") - .write_stdin(tx_env.to_xdr_base64(Limits::none()).unwrap().as_bytes()) - .assert() - .success() - .stdout_as_str(), - Limits::none(), - ) - .unwrap() + let rpc_result = client.send_transaction_polling(&tx_signed).await.unwrap(); + assert_eq!(rpc_result.status, "SUCCESS"); } From 376c4645ece43ee228397fb33acb28f5058fe110 Mon Sep 17 00:00:00 2001 From: Leigh McCulloch <351529+leighmcculloch@users.noreply.github.com> Date: Tue, 24 Sep 2024 12:11:22 +1000 Subject: [PATCH 22/23] remove deadcode --- cmd/soroban-cli/src/config/mod.rs | 2 +- cmd/soroban-cli/src/config/secret.rs | 5 +++-- cmd/soroban-cli/src/config/sign_with.rs | 2 +- cmd/soroban-cli/src/signer.rs | 16 +++------------- 4 files changed, 8 insertions(+), 17 deletions(-) diff --git a/cmd/soroban-cli/src/config/mod.rs b/cmd/soroban-cli/src/config/mod.rs index a109ea845..48eb6dbe8 100644 --- a/cmd/soroban-cli/src/config/mod.rs +++ b/cmd/soroban-cli/src/config/mod.rs @@ -69,7 +69,7 @@ impl Args { let key = self.key_pair()?; let network = &self.get_network()?; let signer = Signer { - kind: SignerKind::Local(LocalKey::new(key, false)), + kind: SignerKind::Local(LocalKey { key }), printer: Print::new(false), }; Ok(signer.sign_tx(tx, network)?) diff --git a/cmd/soroban-cli/src/config/secret.rs b/cmd/soroban-cli/src/config/secret.rs index c7c5ebbcc..ff1fb7b72 100644 --- a/cmd/soroban-cli/src/config/secret.rs +++ b/cmd/soroban-cli/src/config/secret.rs @@ -126,10 +126,11 @@ impl Secret { )?) } - pub fn signer(&self, index: Option, prompt: bool, quiet: bool) -> Result { + pub fn signer(&self, index: Option, quiet: bool) -> Result { let kind = match self { Secret::SecretKey { .. } | Secret::SeedPhrase { .. } => { - SignerKind::Local(LocalKey::new(self.key_pair(index)?, prompt)) + let key = self.key_pair(index)?; + SignerKind::Local(LocalKey { key }) } }; Ok(Signer { diff --git a/cmd/soroban-cli/src/config/sign_with.rs b/cmd/soroban-cli/src/config/sign_with.rs index 7a028baf7..d7b124f1f 100644 --- a/cmd/soroban-cli/src/config/sign_with.rs +++ b/cmd/soroban-cli/src/config/sign_with.rs @@ -55,7 +55,7 @@ impl Args { ) -> Result { let key_or_name = self.sign_with_key.as_deref().ok_or(Error::NoSignWithKey)?; let secret = locator.key(key_or_name)?; - let signer = secret.signer(self.hd_path, false, quiet)?; + let signer = secret.signer(self.hd_path, quiet)?; Ok(signer.sign_tx_env(tx, network)?) } } diff --git a/cmd/soroban-cli/src/signer.rs b/cmd/soroban-cli/src/signer.rs index b05593513..36ae55053 100644 --- a/cmd/soroban-cli/src/signer.rs +++ b/cmd/soroban-cli/src/signer.rs @@ -223,10 +223,8 @@ impl Signer { match tx_env { TransactionEnvelope::Tx(TransactionV1Envelope { tx, signatures }) => { let tx_hash = transaction_hash(&tx, &network.network_passphrase)?; - self.printer.infoln(format!( - "Signing transaction with hash: {}", - hex::encode(tx_hash) - )); + self.printer + .infoln(format!("Signing transaction: {}", hex::encode(tx_hash),)); let decorated_signature = match &self.kind { SignerKind::Local(key) => key.sign_tx_hash(tx_hash)?, }; @@ -243,15 +241,7 @@ impl Signer { } pub struct LocalKey { - key: ed25519_dalek::SigningKey, - #[allow(dead_code)] - prompt: bool, -} - -impl LocalKey { - pub fn new(key: ed25519_dalek::SigningKey, prompt: bool) -> Self { - Self { key, prompt } - } + pub key: ed25519_dalek::SigningKey, } impl LocalKey { From 1db4b5a95c09dba84e7cf140cec1e07b295944e0 Mon Sep 17 00:00:00 2001 From: Leigh McCulloch <351529+leighmcculloch@users.noreply.github.com> Date: Tue, 24 Sep 2024 12:33:34 +1000 Subject: [PATCH 23/23] remove sign with lab --- cmd/soroban-cli/src/config/sign_with.rs | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/cmd/soroban-cli/src/config/sign_with.rs b/cmd/soroban-cli/src/config/sign_with.rs index d7b124f1f..685945194 100644 --- a/cmd/soroban-cli/src/config/sign_with.rs +++ b/cmd/soroban-cli/src/config/sign_with.rs @@ -29,18 +29,10 @@ pub enum Error { #[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_KEY")] + #[arg(long, env = "STELLAR_SIGN_WITH_KEY")] pub sign_with_key: Option, - /// Sign with - #[arg( - long, - conflicts_with = "sign_with_key", - env = "STELLAR_SIGN_WITH_LAB", - hide = true - )] - pub sign_with_lab: bool, - #[arg(long, conflicts_with = "sign_with_lab")] + #[arg(long, requires = "sign_with_key")] /// 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, }