diff --git a/cmd/soroban-cli/src/rpc/mod.rs b/cmd/soroban-cli/src/rpc/mod.rs index 0047ffab2..132ae0481 100644 --- a/cmd/soroban-cli/src/rpc/mod.rs +++ b/cmd/soroban-cli/src/rpc/mod.rs @@ -5,14 +5,14 @@ use jsonrpsee_core::{self, client::ClientT, rpc_params}; use jsonrpsee_http_client::{HeaderMap, HttpClient, HttpClientBuilder}; use serde_aux::prelude::{deserialize_default_from_null, deserialize_number_from_string}; use sha2::{Digest, Sha256}; +use soroban_env_host::xdr::DepthLimitedRead; use soroban_env_host::xdr::{ self, AccountEntry, AccountId, ContractDataEntry, DiagnosticEvent, Error as XdrError, ExpirationEntry, LedgerEntryData, LedgerFootprint, LedgerKey, LedgerKeyAccount, - LedgerKeyExpiration, PublicKey, ReadXdr, SequenceNumber, SorobanAuthorizationEntry, - SorobanResources, Transaction, TransactionEnvelope, TransactionMeta, TransactionMetaV3, - TransactionResult, TransactionV1Envelope, Uint256, VecM, WriteXdr, + LedgerKeyExpiration, PublicKey, ReadXdr, SorobanAuthorizationEntry, SorobanResources, + SorobanTransactionData, Transaction, TransactionEnvelope, TransactionMeta, TransactionMetaV3, + TransactionResult, Uint256, VecM, WriteXdr, }; -use soroban_env_host::xdr::{DepthLimitedRead, SorobanAuthorizedFunction}; use soroban_sdk::token; use std::{ fmt::Display, @@ -23,10 +23,9 @@ use termcolor::{Color, ColorChoice, StandardStream, WriteColor}; use termcolor_output::colored; use tokio::time::sleep; -use crate::utils::{self, contract_spec}; +use crate::utils::contract_spec; -mod transaction; -use transaction::{assemble, build_restore_txn, sign_soroban_authorizations}; +mod txn; const VERSION: Option<&str> = option_env!("CARGO_PKG_VERSION"); @@ -123,7 +122,7 @@ pub struct SendTransactionResponse { } #[derive(serde::Deserialize, serde::Serialize, Debug)] -pub struct GetTransactionResponse { +pub struct GetTransactionResponseRaw { pub status: String, #[serde( rename = "envelopeXdr", @@ -142,6 +141,33 @@ pub struct GetTransactionResponse { // TODO: add ledger info and application order } +#[derive(serde::Deserialize, serde::Serialize, Debug)] +pub struct GetTransactionResponse { + pub status: String, + pub envelope: Option, + pub result: Option, + pub result_meta: Option, +} + +impl TryInto for GetTransactionResponseRaw { + type Error = xdr::Error; + + fn try_into(self) -> Result { + Ok(GetTransactionResponse { + status: self.status, + envelope: self + .envelope_xdr + .map(ReadXdr::from_xdr_base64) + .transpose()?, + result: self.result_xdr.map(ReadXdr::from_xdr_base64).transpose()?, + result_meta: self + .result_meta_xdr + .map(ReadXdr::from_xdr_base64) + .transpose()?, + }) + } +} + #[derive(serde::Deserialize, serde::Serialize, Debug)] pub struct LedgerEntryResult { pub key: String, @@ -202,12 +228,18 @@ pub struct Cost { } #[derive(serde::Deserialize, serde::Serialize, Debug)] -pub struct SimulateHostFunctionResult { +pub struct SimulateHostFunctionResultRaw { #[serde(deserialize_with = "deserialize_default_from_null")] pub auth: Vec, pub xdr: String, } +#[derive(Debug)] +pub struct SimulateHostFunctionResult { + pub auth: Vec, + pub xdr: xdr::ScVal, +} + #[derive(serde::Deserialize, serde::Serialize, Debug, Default)] pub struct SimulateTransactionResponse { #[serde( @@ -219,7 +251,7 @@ pub struct SimulateTransactionResponse { #[serde(default)] pub cost: Cost, #[serde(skip_serializing_if = "Vec::is_empty", default)] - pub results: Vec, + pub results: Vec, #[serde(rename = "transactionData", default)] pub transaction_data: String, #[serde( @@ -243,6 +275,37 @@ pub struct SimulateTransactionResponse { pub error: Option, } +impl SimulateTransactionResponse { + pub fn results(&self) -> Result, Error> { + self.results + .iter() + .map(|r| { + Ok(SimulateHostFunctionResult { + auth: r + .auth + .iter() + .map(|a| Ok(SorobanAuthorizationEntry::from_xdr_base64(a)?)) + .collect::>()?, + xdr: xdr::ScVal::from_xdr_base64(&r.xdr)?, + }) + }) + .collect() + } + + pub fn events(&self) -> Result, Error> { + self.events + .iter() + .map(|e| Ok(DiagnosticEvent::from_xdr_base64(e)?)) + .collect() + } + + pub fn transaction_data(&self) -> Result { + Ok(SorobanTransactionData::from_xdr_base64( + &self.transaction_data, + )?) + } +} + #[derive(serde::Deserialize, serde::Serialize, Debug, Default)] pub struct RestorePreamble { #[serde(rename = "transactionData")] @@ -557,7 +620,7 @@ soroban config identity fund {address} --helper-url "# tx: &TransactionEnvelope, ) -> Result<(TransactionResult, TransactionMeta, Vec), Error> { let client = self.client()?; - tracing::trace!(?tx); + tracing::trace!("Sending:\n{tx:#?}"); let SendTransactionResponse { hash, error_result_xdr, @@ -566,7 +629,9 @@ soroban config identity fund {address} --helper-url "# } = client .request("sendTransaction", rpc_params![tx.to_xdr_base64()?]) .await - .map_err(|err| Error::TransactionSubmissionFailed(format!("{err:#?}")))?; + .map_err(|err| { + Error::TransactionSubmissionFailed(format!("No status yet:\n {err:#?}")) + })?; if status == "ERROR" { let error = error_result_xdr @@ -579,7 +644,7 @@ soroban config identity fund {address} --helper-url "# .map_err(|_| Error::InvalidResponse) }) .map(|r| r.result); - tracing::error!(?error); + tracing::error!("TXN failed:\n {error:#?}"); return Err(Error::TransactionSubmissionFailed(format!("{:#?}", error?))); } // even if status == "success" we need to query the transaction status in order to get the result @@ -587,27 +652,27 @@ soroban config identity fund {address} --helper-url "# // Poll the transaction status let start = Instant::now(); loop { - let response = self.get_transaction(&hash).await?; + let response: GetTransactionResponse = self.get_transaction(&hash).await?.try_into()?; match response.status.as_str() { "SUCCESS" => { // TODO: the caller should probably be printing this - tracing::trace!(?response); - let result = TransactionResult::from_xdr_base64( - response.result_xdr.clone().ok_or(Error::MissingResult)?, - )?; - let meta = TransactionMeta::from_xdr_base64( - response - .result_meta_xdr - .clone() - .ok_or(Error::MissingResult)?, - )?; + tracing::trace!("{response:#?}"); + let GetTransactionResponse { + result, + result_meta, + .. + } = response; + let meta = result_meta.ok_or(Error::MissingResult)?; let events = extract_events(&meta); - return Ok((result, meta, events)); + return Ok((result.ok_or(Error::MissingResult)?, meta, events)); } "FAILED" => { - tracing::error!(?response); + tracing::error!("{response:#?}"); // TODO: provide a more elaborate error - return Err(Error::TransactionSubmissionFailed(format!("{response:#?}"))); + return Err(Error::TransactionSubmissionFailed(format!( + "{:#?}", + response.result + ))); } "NOT_FOUND" => (), _ => { @@ -627,13 +692,13 @@ soroban config identity fund {address} --helper-url "# &self, tx: &TransactionEnvelope, ) -> Result { - tracing::trace!(?tx); + tracing::trace!("Simulating:\n{tx:#?}"); let base64_tx = tx.to_xdr_base64()?; let response: SimulateTransactionResponse = self .client()? .request("simulateTransaction", rpc_params![base64_tx]) .await?; - tracing::trace!(?response); + tracing::trace!("Simulation response:\n {response:#?}"); match response.error { None => Ok(response), Some(e) => { @@ -643,31 +708,6 @@ soroban config identity fund {address} --helper-url "# } } - // Simulate a transaction, then assemble the result of the simulation into the envelope, so it - // is ready for sending to the network. - pub async fn prepare_transaction( - &self, - tx: &Transaction, - ) -> Result<(Transaction, Option, Vec), Error> { - tracing::trace!(?tx); - let sim_response = self - .simulate_transaction(&TransactionEnvelope::Tx(TransactionV1Envelope { - tx: tx.clone(), - signatures: VecM::default(), - })) - .await?; - let events = sim_response - .events - .iter() - .map(DiagnosticEvent::from_xdr_base64) - .collect::, _>>()?; - Ok(( - assemble(tx, &sim_response)?, - sim_response.restore_preamble, - events, - )) - } - pub async fn prepare_and_send_transaction( &self, tx_without_preflight: &Transaction, @@ -677,68 +717,19 @@ soroban config identity fund {address} --helper-url "# log_events: Option, log_resources: Option, ) -> Result<(TransactionResult, TransactionMeta, Vec), Error> { - let GetLatestLedgerResponse { sequence, .. } = self.get_latest_ledger().await?; - let (mut unsigned_tx, restore_preamble, events) = - self.prepare_transaction(tx_without_preflight).await?; - if let Some(restore) = restore_preamble { - // Build and submit the restore transaction - self.send_transaction(&utils::sign_transaction( - source_key, - &build_restore_txn(&unsigned_tx, &restore)?, - network_passphrase, - )?) + let txn = txn::Assembled::new(tx_without_preflight, self).await?; + let seq_num = txn.sim_res().latest_ledger + 60; //5 min; + let authorized = txn + .handle_restore(self, source_key, network_passphrase) + .await? + .authorize(self, source_key, signers, seq_num, network_passphrase) .await?; - // Increment the original txn's seq_num so it doesn't conflict - unsigned_tx.seq_num = SequenceNumber(unsigned_tx.seq_num.0 + 1); - } - let (part_signed_tx, signed_auth_entries) = sign_soroban_authorizations( - &unsigned_tx, - source_key, - signers, - sequence + 60, // ~5 minutes of ledgers - network_passphrase, - )?; - let (fee_ready_txn, events) = if signed_auth_entries.is_empty() - || (signed_auth_entries.len() == 1 - && matches!( - signed_auth_entries[0].root_invocation.function, - SorobanAuthorizedFunction::CreateContractHostFn(_) - )) { - (part_signed_tx, events) - } else { - // re-simulate to calculate the new fees - let (tx, _, events) = self.prepare_transaction(&part_signed_tx).await?; - (tx, events) - }; - - // Try logging stuff if requested - if let Transaction { - ext: xdr::TransactionExt::V1(xdr::SorobanTransactionData { resources, .. }), - .. - } = fee_ready_txn.clone() - { - if let Some(log) = log_events { - if let xdr::Operation { - body: - xdr::OperationBody::InvokeHostFunction(xdr::InvokeHostFunctionOp { - auth, .. - }), - .. - } = &fee_ready_txn.operations[0] - { - log(&resources.footprint, &[auth.clone()], &events); - } - } - if let Some(log) = log_resources { - log(&resources); - } - } - - let tx = utils::sign_transaction(source_key, &fee_ready_txn, network_passphrase)?; + authorized.log(log_events, log_resources)?; + let tx = authorized.sign(source_key, network_passphrase)?; self.send_transaction(&tx).await } - pub async fn get_transaction(&self, tx_id: &str) -> Result { + pub async fn get_transaction(&self, tx_id: &str) -> Result { Ok(self .client()? .request("getTransaction", rpc_params![tx_id]) diff --git a/cmd/soroban-cli/src/rpc/transaction.rs b/cmd/soroban-cli/src/rpc/txn.rs similarity index 71% rename from cmd/soroban-cli/src/rpc/transaction.rs rename to cmd/soroban-cli/src/rpc/txn.rs index f2ed5eeee..b59d85144 100644 --- a/cmd/soroban-cli/src/rpc/transaction.rs +++ b/cmd/soroban-cli/src/rpc/txn.rs @@ -1,14 +1,161 @@ use ed25519_dalek::Signer; use sha2::{Digest, Sha256}; use soroban_env_host::xdr::{ - AccountId, ExtensionPoint, Hash, HashIdPreimage, HashIdPreimageSorobanAuthorization, Memo, - Operation, OperationBody, Preconditions, PublicKey, ReadXdr, RestoreFootprintOp, ScAddress, - ScMap, ScSymbol, ScVal, SorobanAddressCredentials, SorobanAuthorizationEntry, - SorobanCredentials, SorobanTransactionData, Transaction, TransactionExt, Uint256, VecM, - WriteXdr, + self, AccountId, DecoratedSignature, ExtensionPoint, Hash, HashIdPreimage, + HashIdPreimageSorobanAuthorization, InvokeHostFunctionOp, Memo, Operation, OperationBody, + Preconditions, PublicKey, ReadXdr, RestoreFootprintOp, ScAddress, ScMap, ScSymbol, ScVal, + Signature, SignatureHint, SorobanAddressCredentials, SorobanAuthorizationEntry, + SorobanAuthorizedFunction, SorobanCredentials, SorobanResources, SorobanTransactionData, + Transaction, TransactionEnvelope, TransactionExt, TransactionSignaturePayload, + TransactionSignaturePayloadTaggedTransaction, TransactionV1Envelope, Uint256, VecM, WriteXdr, }; -use crate::rpc::{Error, RestorePreamble, SimulateTransactionResponse}; +use crate::rpc::{Client, Error, RestorePreamble, SimulateTransactionResponse}; + +use super::{LogEvents, LogResources}; + +pub struct Assembled { + txn: Transaction, + sim_res: SimulateTransactionResponse, +} + +impl Assembled { + pub async fn new(txn: &Transaction, client: &Client) -> Result { + let sim_res = Self::simulate(txn, client).await?; + let txn = assemble(txn, &sim_res)?; + Ok(Self { txn, sim_res }) + } + + pub fn hash(&self, network_passphrase: &str) -> Result<[u8; 32], xdr::Error> { + let signature_payload = TransactionSignaturePayload { + network_id: Hash(Sha256::digest(network_passphrase).into()), + tagged_transaction: TransactionSignaturePayloadTaggedTransaction::Tx(self.txn.clone()), + }; + Ok(Sha256::digest(signature_payload.to_xdr()?).into()) + } + + pub fn sign( + self, + key: &ed25519_dalek::SigningKey, + network_passphrase: &str, + ) -> Result { + let tx = self.txn(); + let tx_hash = self.hash(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()?, + })) + } + + pub async fn simulate( + tx: &Transaction, + client: &Client, + ) -> Result { + client + .simulate_transaction(&TransactionEnvelope::Tx(TransactionV1Envelope { + tx: tx.clone(), + signatures: VecM::default(), + })) + .await + } + + pub async fn handle_restore( + self, + client: &Client, + source_key: &ed25519_dalek::SigningKey, + network_passphrase: &str, + ) -> Result { + if let Some(restore_preamble) = &self.sim_res.restore_preamble { + // Build and submit the restore transaction + client + .send_transaction( + &Assembled::new(&restore(self.txn(), restore_preamble)?, client) + .await? + .sign(source_key, network_passphrase)?, + ) + .await?; + Ok(self.bump_seq_num()) + } else { + Ok(self) + } + } + + pub fn txn(&self) -> &Transaction { + &self.txn + } + + pub fn sim_res(&self) -> &SimulateTransactionResponse { + &self.sim_res + } + + pub async fn authorize( + self, + client: &Client, + source_key: &ed25519_dalek::SigningKey, + signers: &[ed25519_dalek::SigningKey], + seq_num: u32, + network_passphrase: &str, + ) -> Result { + if let Some(txn) = sign_soroban_authorizations( + self.txn(), + source_key, + signers, + seq_num, + network_passphrase, + )? { + Self::new(&txn, client).await + } else { + Ok(self) + } + } + + pub fn bump_seq_num(mut self) -> Self { + self.txn.seq_num.0 += 1; + self + } + + pub fn auth(&self) -> VecM { + self.txn + .operations + .get(0) + .and_then(|op| match op.body { + OperationBody::InvokeHostFunction(ref body) => (matches!( + body.auth.get(0).map(|x| &x.root_invocation.function), + Some(&SorobanAuthorizedFunction::ContractFn(_)) + )) + .then_some(body.auth.clone()), + _ => None, + }) + .unwrap_or_default() + } + + pub fn log( + &self, + log_events: Option, + log_resources: Option, + ) -> Result<(), Error> { + if let TransactionExt::V1(SorobanTransactionData { + resources: resources @ SorobanResources { footprint, .. }, + .. + }) = &self.txn.ext + { + if let Some(log) = log_resources { + log(resources); + } + if let Some(log) = log_events { + log(footprint, &[self.auth()], &self.sim_res.events()?); + }; + } + Ok(()) + } +} // Apply the result of a simulateTransaction onto a transaction envelope, preparing it for // submission to the network. @@ -28,7 +175,7 @@ pub fn assemble( }); } - let transaction_data = SorobanTransactionData::from_xdr_base64(&simulation.transaction_data)?; + let transaction_data = simulation.transaction_data()?; let mut op = tx.operations[0].clone(); if let OperationBody::InvokeHostFunction(ref mut body) = &mut op.body { @@ -74,23 +221,34 @@ pub fn assemble( // 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( +fn sign_soroban_authorizations( raw: &Transaction, source_key: &ed25519_dalek::SigningKey, signers: &[ed25519_dalek::SigningKey], signature_expiration_ledger: u32, network_passphrase: &str, -) -> Result<(Transaction, Vec), Error> { +) -> Result, Error> { let mut tx = raw.clone(); + let mut op = match tx.operations.as_slice() { + [op @ Operation { + body: OperationBody::InvokeHostFunction(InvokeHostFunctionOp { auth, .. }), + .. + }] if matches!( + auth.get(0).map(|x| &x.root_invocation.function), + Some(&SorobanAuthorizedFunction::ContractFn(_)) + ) => + { + op.clone() + } + _ => return Ok(None), + }; - if tx.operations.len() != 1 { - // This must not be an invokeHostFunction operation, so nothing to do - return Ok((tx, Vec::new())); - } - - let mut op = tx.operations[0].clone(); - let OperationBody::InvokeHostFunction(ref mut body) = &mut op.body else { - return Ok((tx, Vec::new())); + let Operation { + body: OperationBody::InvokeHostFunction(ref mut body), + .. + } = op + else { + return Ok(None); }; let network_id = Hash(Sha256::digest(network_passphrase.as_bytes()).into()); @@ -100,6 +258,7 @@ pub fn sign_soroban_authorizations( let signed_auths = body .auth + .as_slice() .iter() .map(|raw_auth| { let mut auth = raw_auth.clone(); @@ -153,12 +312,12 @@ pub fn sign_soroban_authorizations( }) .collect::, Error>>()?; - body.auth = signed_auths.clone().try_into()?; + body.auth = signed_auths.try_into()?; tx.operations = vec![op].try_into()?; - Ok((tx, signed_auths)) + Ok(Some(tx)) } -pub fn sign_soroban_authorization_entry( +fn sign_soroban_authorization_entry( raw: &SorobanAuthorizationEntry, signer: &ed25519_dalek::SigningKey, signature_expiration_ledger: u32, @@ -218,12 +377,8 @@ pub fn sign_soroban_authorization_entry( Ok(auth) } -pub fn build_restore_txn( - parent: &Transaction, - restore: &RestorePreamble, -) -> Result { - let transaction_data = - SorobanTransactionData::from_xdr_base64(restore.transaction_data.clone())?; +pub fn restore(parent: &Transaction, restore: &RestorePreamble) -> Result { + let transaction_data = SorobanTransactionData::from_xdr_base64(&restore.transaction_data)?; let fee = u32::try_from(restore.min_resource_fee) .map_err(|_| Error::LargeFee(restore.min_resource_fee))?; Ok(Transaction { @@ -251,7 +406,7 @@ pub fn build_restore_txn( mod tests { use super::*; - use super::super::SimulateHostFunctionResult; + use super::super::SimulateHostFunctionResultRaw; use soroban_env_host::xdr::{ self, AccountId, ChangeTrustAsset, ChangeTrustOp, ExtensionPoint, Hash, HostFunction, InvokeContractArgs, InvokeHostFunctionOp, LedgerFootprint, Memo, MuxedAccount, Operation, @@ -303,7 +458,7 @@ mod tests { SimulateTransactionResponse { min_resource_fee: 115, latest_ledger: 3, - results: vec![SimulateHostFunctionResult { + results: vec![SimulateHostFunctionResultRaw { auth: vec![fn_auth.to_xdr_base64().unwrap()], xdr: ScVal::U32(0).to_xdr_base64().unwrap(), }],