diff --git a/cmd/crates/stellar-ledger/src/lib.rs b/cmd/crates/stellar-ledger/src/lib.rs index 5b1a72f74..76fbdec24 100644 --- a/cmd/crates/stellar-ledger/src/lib.rs +++ b/cmd/crates/stellar-ledger/src/lib.rs @@ -114,7 +114,7 @@ impl AsRef> for NativeSigner { impl TryFrom<(String, u32)> for NativeSigner { type Error = LedgerError; - fn try_from((network_passphrase, hd_path): (String, u32)) -> Result { + fn try_from((network_passphrase, hd_path): (String, u32)) -> Result { Ok(Self(LedgerSigner { network_passphrase, transport: transport_native_hid()?, @@ -138,6 +138,13 @@ where Self::get_public_key_with_display_flag(self, hd_path, false).await } + pub fn get_public_key_sync( + &self, + index: u32, + ) -> Result { + block_on(self.get_public_key(index)) + } + /// Get the device app's configuration /// # Errors /// Returns an error if there is an issue with connecting with the device or getting the config from the device @@ -152,7 +159,7 @@ where self.send_command_to_ledger(command).await } - /// Sign a Stellar transaction hash with the account on the Ledger device + /// Sign a Stellar transaction hash with the account on the Ledger device /// based on impl from [https://github.com/LedgerHQ/ledger-live/blob/develop/libs/ledgerjs/packages/hw-app-str/src/Str.ts#L166](https://github.com/LedgerHQ/ledger-live/blob/develop/libs/ledgerjs/packages/hw-app-str/src/Str.ts#L166) /// # Errors /// Returns an error if there is an issue with connecting with the device or signing the given tx on the device. Or, if the device has not enabled hash signing diff --git a/cmd/soroban-cli/src/commands/contract/install.rs b/cmd/soroban-cli/src/commands/contract/install.rs index 7208e1a93..f04def508 100644 --- a/cmd/soroban-cli/src/commands/contract/install.rs +++ b/cmd/soroban-cli/src/commands/contract/install.rs @@ -121,13 +121,12 @@ impl NetworkRunnable for Cmd { let key = config.key_pair()?; // Get the account sequence number - let public_strkey = - stellar_strkey::ed25519::PublicKey(key.verifying_key().to_bytes()).to_string(); - let account_details = client.get_account(&public_strkey).await?; + let public_strkey = stellar_strkey::ed25519::PublicKey(key.verifying_key().to_bytes()); + let account_details = client.get_account(&public_strkey.to_string()).await?; let sequence: i64 = account_details.seq_num.into(); let (tx_without_preflight, hash) = - build_install_contract_code_tx(&contract, sequence + 1, self.fee.fee, &key)?; + build_install_contract_code_tx(&contract, sequence + 1, self.fee.fee, &public_strkey)?; if self.fee.build_only { return Ok(TxnResult::from_xdr(&tx_without_preflight)?); @@ -223,14 +222,12 @@ pub(crate) fn build_install_contract_code_tx( source_code: &[u8], sequence: i64, fee: u32, - key: &ed25519_dalek::SigningKey, + key: &stellar_strkey::ed25519::PublicKey, ) -> Result<(Transaction, Hash), XdrError> { let hash = utils::contract_hash(source_code)?; let op = Operation { - source_account: Some(MuxedAccount::Ed25519(Uint256( - key.verifying_key().to_bytes(), - ))), + source_account: None, body: OperationBody::InvokeHostFunction(InvokeHostFunctionOp { host_function: HostFunction::UploadContractWasm(source_code.try_into()?), auth: VecM::default(), @@ -238,7 +235,7 @@ pub(crate) fn build_install_contract_code_tx( }; let tx = Transaction { - source_account: MuxedAccount::Ed25519(Uint256(key.verifying_key().to_bytes())), + source_account: MuxedAccount::Ed25519(Uint256(key.0)), fee, seq_num: SequenceNumber(sequence), cond: Preconditions::None, @@ -260,8 +257,14 @@ mod tests { b"foo", 300, 1, - &utils::parse_secret_key("SBFGFF27Y64ZUGFAIG5AMJGQODZZKV2YQKAVUUN4HNE24XZXD2OEUVUP") - .unwrap(), + &stellar_strkey::ed25519::PublicKey( + *utils::parse_secret_key( + "SBFGFF27Y64ZUGFAIG5AMJGQODZZKV2YQKAVUUN4HNE24XZXD2OEUVUP", + ) + .unwrap() + .verifying_key() + .as_bytes(), + ), ); assert!(result.is_ok()); diff --git a/cmd/soroban-cli/src/commands/contract/invoke.rs b/cmd/soroban-cli/src/commands/contract/invoke.rs index db3b4eb3f..547f1768b 100644 --- a/cmd/soroban-cli/src/commands/contract/invoke.rs +++ b/cmd/soroban-cli/src/commands/contract/invoke.rs @@ -204,9 +204,10 @@ impl Cmd { let mut s = val.next().unwrap().to_string_lossy().to_string(); if matches!(i.type_, ScSpecTypeDef::Address) { let cmd = crate::commands::keys::address::Cmd { - name: s.clone(), + name: Some(s.clone()), hd_path: Some(0), locator: config.locator.clone(), + use_ledger: false, }; if let Ok(address) = cmd.public_key() { s = address.to_string(); diff --git a/cmd/soroban-cli/src/commands/keys/address.rs b/cmd/soroban-cli/src/commands/keys/address.rs index d13381b49..44a793706 100644 --- a/cmd/soroban-cli/src/commands/keys/address.rs +++ b/cmd/soroban-cli/src/commands/keys/address.rs @@ -13,13 +13,21 @@ pub enum Error { #[error(transparent)] StrKey(#[from] stellar_strkey::DecodeError), + #[error(transparent)] + Ledger(#[from] stellar_ledger::LedgerError), + #[error("Invalid HD path index {0}")] + UsizeConversionError(usize), } #[derive(Debug, clap::Parser, Clone)] #[group(skip)] pub struct Cmd { /// Name of identity to lookup, default test identity used if not provided - pub name: String, + pub name: Option, + + /// Use Ledger + #[arg(long, short = 'l')] + pub use_ledger: bool, /// If identity is a seed phrase use this hd path, default is 0 #[arg(long)] @@ -35,15 +43,34 @@ impl Cmd { Ok(()) } + pub fn name(&self) -> &str { + self.name.as_deref().unwrap_or("default") + } + pub fn private_key(&self) -> Result { Ok(self .locator - .read_identity(&self.name)? + .read_identity(self.name())? .key_pair(self.hd_path)?) } + pub fn hd_path(&self) -> Result { + let hd_path = &self.hd_path.unwrap_or_default(); + (*hd_path) + .try_into() + .map_err(|_| Error::UsizeConversionError(*hd_path)) + } + pub fn public_key(&self) -> Result { - if let Ok(key) = stellar_strkey::ed25519::PublicKey::from_string(&self.name) { + if self.use_ledger { + let signer: stellar_ledger::NativeSigner = ( + String::new(), + self.hd_path.unwrap_or_default().try_into().unwrap(), + ) + .try_into()?; + return Ok(signer.as_ref().get_public_key_sync(self.hd_path()?)?); + } + if let Ok(key) = stellar_strkey::ed25519::PublicKey::from_string(self.name()) { Ok(key) } else { Ok(stellar_strkey::ed25519::PublicKey::from_payload( diff --git a/cmd/soroban-cli/src/commands/mod.rs b/cmd/soroban-cli/src/commands/mod.rs index df28339bd..4eeeb53e2 100644 --- a/cmd/soroban-cli/src/commands/mod.rs +++ b/cmd/soroban-cli/src/commands/mod.rs @@ -13,8 +13,8 @@ pub mod keys; pub mod network; pub mod plugin; pub mod txn; -pub mod version; pub mod txn_result; +pub mod version; pub const HEADING_RPC: &str = "Options (RPC)"; const ABOUT: &str = "Build, deploy, & interact with contracts; set identities to sign with; configure networks; generate keys; and more. diff --git a/cmd/soroban-cli/src/commands/txn/sign.rs b/cmd/soroban-cli/src/commands/txn/sign.rs index 092cac5fe..c764fc977 100644 --- a/cmd/soroban-cli/src/commands/txn/sign.rs +++ b/cmd/soroban-cli/src/commands/txn/sign.rs @@ -1,12 +1,13 @@ use std::io; +use soroban_rpc::Client; // use crossterm::{ // event::{self, Event, KeyCode, KeyEvent, KeyModifiers}, // execute, // terminal::{self, EnterAlternateScreen, LeaveAlternateScreen}, // }; use soroban_sdk::xdr::{ - self, Limits, MuxedAccount, Transaction, TransactionEnvelope, Uint256, WriteXdr, + self, Limits, MuxedAccount, SequenceNumber, Transaction, TransactionEnvelope, Uint256, WriteXdr, }; use stellar_ledger::{LedgerError, NativeSigner}; use stellar_strkey::Strkey; @@ -31,6 +32,8 @@ pub enum Error { UserCancelledSigning, #[error(transparent)] Ledger(#[from] LedgerError), + #[error(transparent)] + Rpc(#[from] soroban_rpc::Error), } #[derive(Debug, clap::Parser, Clone)] @@ -43,7 +46,6 @@ pub struct Cmd { pub xdr_args: super::xdr::Args, #[clap(flatten)] pub config: super::super::config::Args, - #[arg(long, value_enum, default_value = "file")] pub signer: SignerType, } @@ -63,8 +65,6 @@ impl Cmd { } pub async fn sign(&self) -> Result { - let source = &self.config.source_account; - tracing::debug!("signing transaction with source account {}", source); let txn = self.xdr_args.txn()?; match self.signer { SignerType::File => self.sign_file(txn).await, @@ -105,7 +105,7 @@ impl Cmd { // Ok(()) } - pub async fn sign_file(&self, txn: Transaction) -> Result { + pub async fn sign_file(&self, mut txn: Transaction) -> Result { let key = self.config.key_pair()?; let address = stellar_strkey::ed25519::PublicKey::from_payload(key.verifying_key().as_bytes())?; @@ -113,6 +113,8 @@ impl Cmd { network_passphrase: self.config.get_network()?.network_passphrase, keypairs: vec![key], }; + let client = Client::new(&self.config.get_network()?.rpc_url)?; + txn.seq_num = SequenceNumber(client.get_account(&address.to_string()).await?.seq_num.0 + 1); self.prompt_user()?; Ok(in_memory .sign_txn(txn, &Strkey::PublicKeyEd25519(address)) @@ -130,7 +132,10 @@ impl Cmd { (self.config.get_network()?.network_passphrase, index).try_into()?; let key = signer.as_ref().get_public_key(index).await.unwrap(); let account = Strkey::PublicKeyEd25519(key); + let client = Client::new(&self.config.get_network()?.rpc_url)?; + txn.seq_num = SequenceNumber(client.get_account(&account.to_string()).await?.seq_num.0 + 1); txn.source_account = MuxedAccount::Ed25519(Uint256(key.0)); + eprintln!("Account {account}"); let bx_signer = Box::new(signer); Ok(bx_signer.sign_txn(txn, &account).await.unwrap()) } diff --git a/cmd/soroban-cli/src/lib.rs b/cmd/soroban-cli/src/lib.rs index 9a9900605..5f44d6b8b 100644 --- a/cmd/soroban-cli/src/lib.rs +++ b/cmd/soroban-cli/src/lib.rs @@ -8,13 +8,12 @@ use std::path::Path; pub(crate) use soroban_env_host::xdr; pub(crate) use soroban_rpc as rpc; - pub mod commands; pub mod fee; pub mod key; pub mod log; -pub mod toid; pub mod signer; +pub mod toid; pub mod utils; pub mod wasm; diff --git a/cmd/soroban-cli/src/signer.rs b/cmd/soroban-cli/src/signer.rs index d72ce5459..89b484bed 100644 --- a/cmd/soroban-cli/src/signer.rs +++ b/cmd/soroban-cli/src/signer.rs @@ -9,6 +9,7 @@ use soroban_env_host::xdr::{ TransactionEnvelope, TransactionSignaturePayload, TransactionSignaturePayloadTaggedTransaction, TransactionV1Envelope, Uint256, WriteXdr, }; +use soroban_sdk::xdr::BytesM; use stellar_ledger::{LedgerSigner, NativeSigner}; #[derive(thiserror::Error, Debug)] @@ -17,6 +18,8 @@ pub enum Error { Xdr(#[from] xdr::Error), #[error("Error signing transaction {address}")] MissingSignerForAddress { address: String }, + #[error(transparent)] + Ledger(#[from] stellar_ledger::LedgerError), } fn requires_auth(txn: &Transaction) -> Option { @@ -53,15 +56,67 @@ pub trait Stellar { source_account: &stellar_strkey::Strkey, ) -> impl std::future::Future> + Send; + async fn sign_blob( + &self, + data: &[u8], + source_account: &stellar_strkey::Strkey, + ) -> Result, Error>; + /// Sign a Soroban authorization entry with the given address /// # Errors /// Returns an error if the address is not found - fn sign_soroban_authorization_entry( + async fn sign_soroban_authorization_entry( &self, unsigned_entry: &SorobanAuthorizationEntry, signature_expiration_ledger: u32, - address: &[u8; 32], - ) -> impl std::future::Future> + Send; + address: &stellar_strkey::ed25519::PublicKey, + ) -> Result { + let mut auth = unsigned_entry.clone(); + let SorobanAuthorizationEntry { + credentials: SorobanCredentials::Address(ref mut credentials), + .. + } = auth + else { + // Doesn't need special signing + return Ok(auth); + }; + let SorobanAddressCredentials { nonce, .. } = credentials; + + let preimage = HashIdPreimage::SorobanAuthorization(HashIdPreimageSorobanAuthorization { + network_id: self.network_hash(), + invocation: auth.root_invocation.clone(), + nonce: *nonce, + signature_expiration_ledger, + }) + .to_xdr(Limits::none())?; + + let payload = Sha256::digest(preimage); + let signature = self + .sign_blob( + &payload, + &stellar_strkey::Strkey::PublicKeyEd25519(*address), + ) + .await?; + + let map = ScMap::sorted_from(vec![ + ( + ScVal::Symbol(ScSymbol("public_key".try_into()?)), + ScVal::Bytes(address.0.to_vec().try_into().map_err(Error::Xdr)?), + ), + ( + ScVal::Symbol(ScSymbol("signature".try_into()?)), + ScVal::Bytes(signature.try_into().map_err(Error::Xdr)?), + ), + ]) + .map_err(Error::Xdr)?; + credentials.signature = ScVal::Vec(Some( + vec![ScVal::Map(Some(map))].try_into().map_err(Error::Xdr)?, + )); + credentials.signature_expiration_ledger = signature_expiration_ledger; + auth.credentials = SorobanCredentials::Address(credentials.clone()); + + Ok(auth) + } /// Sign a Stellar transaction with the given source account /// This is a default implementation that signs the transaction hash and returns a decorated signature @@ -106,7 +161,7 @@ pub trait Stellar { }; let mut auths = body.auth.to_vec(); - for auth in auths.iter_mut() { + for auth in &mut auths { *auth = self .maybe_sign_soroban_authorization_entry(auth, signature_expiration_ledger) .await?; @@ -132,7 +187,10 @@ pub trait Stellar { // See if we have a signer for this authorizationEntry // If not, then we Error let needle = match address { - ScAddress::Account(AccountId(PublicKey::PublicKeyTypeEd25519(Uint256(ref a)))) => a, + ScAddress::Account(AccountId(PublicKey::PublicKeyTypeEd25519(Uint256(a)))) => { + {} + stellar_strkey::ed25519::PublicKey(*a) + } ScAddress::Contract(Hash(c)) => { // This address is for a contract. This means we're using a custom // smart-contract account. Currently the CLI doesn't support that yet. @@ -145,7 +203,7 @@ pub trait Stellar { self.sign_soroban_authorization_entry( unsigned_entry, signature_expiration_ledger, - needle, + &needle, ) .await } else { @@ -189,6 +247,16 @@ impl Stellar for InMemory { } } + async fn sign_blob( + &self, + data: &[u8], + source_account: &stellar_strkey::Strkey, + ) -> Result, Error> { + let source_account = self.get_key(source_account)?; + let sig = source_account.sign(data); + Ok(sig.to_bytes().to_vec()) + } + async fn sign_txn_hash( &self, txn: [u8; 32], @@ -207,69 +275,6 @@ impl Stellar for InMemory { }) } - async fn sign_soroban_authorization_entry( - &self, - unsigned_entry: &SorobanAuthorizationEntry, - signature_expiration_ledger: u32, - signer: &[u8; 32], - ) -> Result { - let mut auth = unsigned_entry.clone(); - let SorobanAuthorizationEntry { - credentials: SorobanCredentials::Address(ref mut credentials), - .. - } = auth - else { - // Doesn't need special signing - return Ok(auth); - }; - let SorobanAddressCredentials { nonce, .. } = credentials; - - let preimage = HashIdPreimage::SorobanAuthorization(HashIdPreimageSorobanAuthorization { - network_id: self.network_hash(), - invocation: auth.root_invocation.clone(), - nonce: *nonce, - signature_expiration_ledger, - }) - .to_xdr(Limits::none())?; - - let strkey = stellar_strkey::ed25519::PublicKey(*signer); - let payload = Sha256::digest(preimage); - let signer = self.get_key(&stellar_strkey::Strkey::PublicKeyEd25519(strkey))?; - let signature = signer.sign(&payload); - - let map = ScMap::sorted_from(vec![ - ( - ScVal::Symbol(ScSymbol("public_key".try_into()?)), - ScVal::Bytes( - signer - .verifying_key() - .to_bytes() - .to_vec() - .try_into() - .map_err(Error::Xdr)?, - ), - ), - ( - ScVal::Symbol(ScSymbol("signature".try_into()?)), - ScVal::Bytes( - signature - .to_bytes() - .to_vec() - .try_into() - .map_err(Error::Xdr)?, - ), - ), - ]) - .map_err(Error::Xdr)?; - credentials.signature = ScVal::Vec(Some( - vec![ScVal::Map(Some(map))].try_into().map_err(Error::Xdr)?, - )); - credentials.signature_expiration_ledger = signature_expiration_ledger; - auth.credentials = SorobanCredentials::Address(credentials.clone()); - - Ok(auth) - } - fn network_hash(&self) -> xdr::Hash { xdr::Hash(Sha256::digest(self.network_passphrase.as_bytes()).into()) } @@ -279,7 +284,11 @@ impl Stellar for Box { type Init = u32; fn new(network_passphrase: &str, options: Option) -> Self { - Box::new((network_passphrase.to_owned(), options.unwrap_or_default()).into()) + Box::new( + (network_passphrase.to_owned(), options.unwrap_or_default()) + .try_into() + .unwrap(), + ) } fn network_hash(&self) -> xdr::Hash { @@ -287,37 +296,40 @@ impl Stellar for Box { self.as_ref().as_ref().network_hash() } + async fn sign_blob( + &self, + data: &[u8], + _source_account: &stellar_strkey::Strkey, + ) -> Result, Error> { + let index = self.as_ref().as_ref().hd_path.clone(); + Ok(self.as_ref().as_ref().sign_blob(index, data).await?) + } + async fn sign_txn_hash( &self, txn: [u8; 32], - _source_account: &stellar_strkey::Strkey, + source_account: &stellar_strkey::Strkey, ) -> Result { let index = self.as_ref().as_ref().hd_path.clone(); - let mut res = self + let res = self .as_ref() .as_ref() .sign_transaction_hash(index, &txn) .await .unwrap(); - println!("{}", base64::encode(&res)); - println!("{}", res.len()); - println!("{:#?}", Signature::from_xdr(&res, Limits::none())); - - todo!("Need to figure out how to get Signature"); - let source_account = self.as_ref().as_ref().get_public_key(0).await.unwrap(); - // 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, - // }) - } - - async fn sign_soroban_authorization_entry( - &self, - unsigned_entry: &SorobanAuthorizationEntry, - signature_expiration_ledger: u32, - address: &[u8; 32], - ) -> Result { - todo!() + let sig_bytes = res.try_into().unwrap(); // FIXME: handle error + let bytes = match source_account { + stellar_strkey::Strkey::PublicKeyEd25519(d) => d.0, + stellar_strkey::Strkey::PrivateKeyEd25519(_) => todo!(), + stellar_strkey::Strkey::PreAuthTx(_) => todo!(), + stellar_strkey::Strkey::HashX(_) => todo!(), + stellar_strkey::Strkey::MuxedAccountEd25519(_) => todo!(), + stellar_strkey::Strkey::SignedPayloadEd25519(_) => todo!(), + stellar_strkey::Strkey::Contract(_) => todo!(), + }; + Ok(DecoratedSignature { + hint: SignatureHint(bytes[28..].try_into().unwrap()), + signature: Signature(sig_bytes), + }) } }