From 73101de5034a7e02f6f1408a2f661579abd38aca Mon Sep 17 00:00:00 2001 From: Willem Wyndham Date: Thu, 22 Feb 2024 14:07:47 -0500 Subject: [PATCH] feat: initial pass at creating a human readable preview of the transaction --- .../src/commands/contract/invoke.rs | 2 + cmd/soroban-cli/src/lib.rs | 3 +- cmd/soroban-cli/src/rpc.rs | 132 ++++++++++++++++++ 3 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 cmd/soroban-cli/src/rpc.rs diff --git a/cmd/soroban-cli/src/commands/contract/invoke.rs b/cmd/soroban-cli/src/commands/contract/invoke.rs index cd42d818f..7e7d317be 100644 --- a/cmd/soroban-cli/src/commands/contract/invoke.rs +++ b/cmd/soroban-cli/src/commands/contract/invoke.rs @@ -28,6 +28,7 @@ use super::super::{ config::{self, locator}, events, }; +use crate::rpc::preview_txn; use crate::commands::NetworkRunnable; use crate::{commands::global, rpc, Pwd}; use soroban_spec_tools::{contract, Spec}; @@ -336,6 +337,7 @@ impl NetworkRunnable for Cmd { )?; let txn = client.create_assembled_transaction(&tx).await?; let txn = self.fee.apply_to_assembled_txn(txn); + println!("TXN:\n{}", preview_txn(txn.transaction())); let (return_value, events) = if self.is_view() { ( txn.sim_response().results()?[0].xdr.clone(), diff --git a/cmd/soroban-cli/src/lib.rs b/cmd/soroban-cli/src/lib.rs index ef443853b..3117f847a 100644 --- a/cmd/soroban-cli/src/lib.rs +++ b/cmd/soroban-cli/src/lib.rs @@ -3,13 +3,14 @@ clippy::must_use_candidate, clippy::missing_panics_doc )] -pub(crate) use soroban_rpc as rpc; + use std::path::Path; pub mod commands; pub mod fee; pub mod key; pub mod log; +pub mod rpc; pub mod toid; pub mod utils; pub mod wasm; diff --git a/cmd/soroban-cli/src/rpc.rs b/cmd/soroban-cli/src/rpc.rs new file mode 100644 index 000000000..6b3f3d19a --- /dev/null +++ b/cmd/soroban-cli/src/rpc.rs @@ -0,0 +1,132 @@ +pub(crate) use soroban_rpc::*; +use soroban_sdk::xdr; + +pub(crate) fn preview_txn(txn: &xdr::Transaction) -> String { + let source_account = txn.source_account.to_string(); + let fee = txn.fee; + let operations = txn + .operations + .iter() + .map(preview_operation) + .collect::>() + .join("\n"); + format!("source_account: {source_account}\nfee: {fee}\noperations:\n{operations}") +} + +pub(crate) fn preview_operation(op: &xdr::Operation) -> String { + use soroban_sdk::xdr::OperationBody; + let _source_account = op.source_account.as_ref().map(ToString::to_string); + + match &op.body { + OperationBody::CreateAccount(_) => todo!(), + OperationBody::Payment(_) => todo!(), + OperationBody::PathPaymentStrictReceive(_) => todo!(), + OperationBody::ManageSellOffer(_) => todo!(), + OperationBody::CreatePassiveSellOffer(_) => todo!(), + OperationBody::SetOptions(_) => todo!(), + OperationBody::ChangeTrust(_) => todo!(), + OperationBody::AllowTrust(_) => todo!(), + OperationBody::AccountMerge(_) => todo!(), + OperationBody::Inflation => todo!(), + OperationBody::ManageData(_) => todo!(), + OperationBody::BumpSequence(_) => todo!(), + OperationBody::ManageBuyOffer(_) => todo!(), + OperationBody::PathPaymentStrictSend(_) => todo!(), + OperationBody::CreateClaimableBalance(_) => todo!(), + OperationBody::ClaimClaimableBalance(_) => todo!(), + OperationBody::BeginSponsoringFutureReserves(_) => todo!(), + OperationBody::EndSponsoringFutureReserves => todo!(), + OperationBody::RevokeSponsorship(_) => todo!(), + OperationBody::Clawback(_) => todo!(), + OperationBody::ClawbackClaimableBalance(_) => todo!(), + OperationBody::SetTrustLineFlags(_) => todo!(), + OperationBody::LiquidityPoolDeposit(_) => todo!(), + OperationBody::LiquidityPoolWithdraw(_) => todo!(), + OperationBody::InvokeHostFunction(op) => preview_invoke(op), + OperationBody::ExtendFootprintTtl(_) => todo!(), + OperationBody::RestoreFootprint(_) => todo!(), + } +} + +fn preview_invoke(invoke: &xdr::InvokeHostFunctionOp) -> String { + let host_function = preview_host_function(&invoke.host_function); + let auth = invoke + .auth + .iter() + .map(preview_auth_entry) + .collect::>() + .join("\n"); + format!("host_function:\n{host_function}\nauth:\n{auth}") +} + +fn preview_host_function(host_function: &xdr::HostFunction) -> String { + match host_function { + xdr::HostFunction::InvokeContract(args) => preview_invoke_contract_args(args, 1), + xdr::HostFunction::CreateContract(_) => todo!(), + xdr::HostFunction::UploadContractWasm(_) => todo!(), + } +} + +fn preview_invoke_contract_args(args: &xdr::InvokeContractArgs, indention: u8) -> String { + let xdr::InvokeContractArgs { + contract_address, + function_name, + args, + } = args; + let contract_id = match contract_address { + xdr::ScAddress::Account(_) => todo!(), + xdr::ScAddress::Contract(_) => strkey_from_sc_address(contract_address), + }; + let function_name = function_name.to_string(); + let args = args.iter().map(|x| format!("{x:?}")).collect::>(); + let indent = " ".repeat(indention as usize); + format!("{indent}contract_id: {contract_id}\n{indent}function_name: {function_name}\n{indent}args:\n{indent}{args:?}") +} + +fn preview_auth_entry(entry: &xdr::SorobanAuthorizationEntry) -> String { + let xdr::SorobanAuthorizationEntry { + credentials, + root_invocation, + } = entry; + let function = preview_authorized_function(root_invocation, 1); + let account_id = if let xdr::SorobanCredentials::Address(account_id) = &credentials { + strkey_from_sc_address(&account_id.address) + } else { + "SourceAccount".to_string() + }; + + format!("credentials: {account_id}\n root_invocation:\n{function}") +} + +fn preview_authorized_function( + function: &xdr::SorobanAuthorizedInvocation, + indention: u8, +) -> String { + let contract_args = match &function.function { + xdr::SorobanAuthorizedFunction::ContractFn(args) => { + preview_invoke_contract_args(args, indention) + } + xdr::SorobanAuthorizedFunction::CreateContractHostFn(_) => todo!(), + }; + let mut sub_invocations = String::new(); + let preview_authorized_function = |x| preview_authorized_function(x, indention + 2); + if !function.sub_invocations.is_empty() { + sub_invocations = function + .sub_invocations + .iter() + .map(preview_authorized_function) + .collect::>() + .join("\n"); + } + let indent = " ".repeat(indention as usize); + format!("{indent}contract_args: {contract_args}\n{indent}sub_invocations:\n{sub_invocations}") +} + +fn strkey_from_sc_address(addr: &xdr::ScAddress) -> String { + match addr { + xdr::ScAddress::Account(xdr::AccountId(xdr::PublicKey::PublicKeyTypeEd25519( + xdr::Uint256(bytes), + ))) => stellar_strkey::ed25519::PublicKey(*bytes).to_string(), + xdr::ScAddress::Contract(contract) => stellar_strkey::Contract(contract.0).to_string(), + } +}