Skip to content

Commit

Permalink
feat: add txn subcommand
Browse files Browse the repository at this point in the history
  • Loading branch information
willemneal committed May 7, 2024
1 parent 9d7bee9 commit 4aafe3f
Show file tree
Hide file tree
Showing 9 changed files with 817 additions and 2 deletions.
10 changes: 8 additions & 2 deletions cmd/soroban-cli/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ pub mod global;
pub mod keys;
pub mod network;
pub mod plugin;
pub mod version;

pub mod txn;
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.
Expand Down Expand Up @@ -101,6 +101,7 @@ impl Root {
Cmd::Network(network) => network.run().await?,
Cmd::Version(version) => version.run(),
Cmd::Keys(id) => id.run().await?,
Cmd::Txn(tx) => tx.run().await?,
Cmd::Cache(data) => data.run()?,
};
Ok(())
Expand Down Expand Up @@ -135,6 +136,9 @@ pub enum Cmd {
Network(network::Cmd),
/// Print version information
Version(version::Cmd),
/// Sign, Simulate, and Send transactions
#[command(subcommand)]
Txn(txn::Cmd),
/// Cache for tranasctions and contract specs
#[command(subcommand)]
Cache(cache::Cmd),
Expand All @@ -158,6 +162,8 @@ pub enum Error {
#[error(transparent)]
Network(#[from] network::Error),
#[error(transparent)]
Txn(#[from] txn::Error),
#[error(transparent)]
Cache(#[from] cache::Error),
}

Expand Down
48 changes: 48 additions & 0 deletions cmd/soroban-cli/src/commands/txn/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
use clap::Parser;

pub mod send;
pub mod sign;
pub mod simulate;
pub mod xdr;

use stellar_xdr::cli as xdr_cli;

#[derive(Debug, Parser)]
pub enum Cmd {
/// Add a new identity (keypair, ledger, macOS keychain)
Inspect(xdr_cli::Root),
/// Given an identity return its address (public key)
Sign(sign::Cmd),
/// Submit a transaction to the network
Send(send::Cmd),
/// Simulate a transaction
Simulate(simulate::Cmd),
}

#[derive(thiserror::Error, Debug)]
pub enum Error {
/// An error during the simulation
#[error(transparent)]
Simulate(#[from] simulate::Error),
/// An error during the inspect
#[error(transparent)]
Inspect(#[from] xdr_cli::Error),
/// An error during the sign
#[error(transparent)]
Sign(#[from] sign::Error),
/// An error during the send
#[error(transparent)]
Send(#[from] send::Error),
}

impl Cmd {
pub async fn run(&self) -> Result<(), Error> {
match self {
Cmd::Inspect(cmd) => cmd.run()?,
Cmd::Sign(cmd) => cmd.run().await?,
Cmd::Send(cmd) => cmd.run().await?,
Cmd::Simulate(cmd) => cmd.run().await?,
};
Ok(())
}
}
35 changes: 35 additions & 0 deletions cmd/soroban-cli/src/commands/txn/send.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
use soroban_rpc::GetTransactionResponse;

#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error(transparent)]
XdrArgs(#[from] super::xdr::Error),
#[error(transparent)]
Config(#[from] super::super::config::Error),
#[error(transparent)]
Rpc(#[from] crate::rpc::Error),
}

#[derive(Debug, clap::Parser, Clone)]
#[group(skip)]
pub struct Cmd {
#[clap(flatten)]
pub xdr_args: super::xdr::Args,
#[clap(flatten)]
pub config: super::super::config::Args,
}

impl Cmd {
pub async fn run(&self) -> Result<(), Error> {
let response = self.send().await?;
println!("{response:#?}");
Ok(())
}

pub async fn send(&self) -> Result<GetTransactionResponse, Error> {
let txn_env = self.xdr_args.txn_envelope()?;
let network = self.config.get_network()?;
let client = crate::rpc::Client::new(&network.rpc_url)?;
Ok(client.send_transaction(&txn_env).await?)
}
}
97 changes: 97 additions & 0 deletions cmd/soroban-cli/src/commands/txn/sign.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
use std::io;

// use crossterm::{
// event::{self, Event, KeyCode, KeyEvent, KeyModifiers},
// execute,
// terminal::{self, EnterAlternateScreen, LeaveAlternateScreen},
// };
use soroban_sdk::xdr::{self, Limits, TransactionEnvelope, WriteXdr};

use crate::signer::{self, InMemory, Stellar};

#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error(transparent)]
XdrArgs(#[from] super::xdr::Error),
#[error(transparent)]
Signer(#[from] signer::Error),
#[error(transparent)]
Config(#[from] super::super::config::Error),
#[error(transparent)]
StellarStrkey(#[from] stellar_strkey::DecodeError),
#[error(transparent)]
Xdr(#[from] xdr::Error),
#[error(transparent)]
Io(#[from] io::Error),
#[error("User cancelled signing, perhaps need to add -y")]
UserCancelledSigning,
}

#[derive(Debug, clap::Parser, Clone)]
#[group(skip)]
pub struct Cmd {
/// Confirm that a signature can be signed by the given keypair automatically.
#[arg(long, short = 'y')]
yes: bool,
#[clap(flatten)]
pub xdr_args: super::xdr::Args,
#[clap(flatten)]
pub config: super::super::config::Args,
}

impl Cmd {
#[allow(clippy::unused_async)]
pub async fn run(&self) -> Result<(), Error> {
let envelope = self.sign()?;
println!("{}", envelope.to_xdr_base64(Limits::none())?.trim());
Ok(())
}

pub fn sign(&self) -> Result<TransactionEnvelope, Error> {
let source = &self.config.source_account;
tracing::debug!("signing transaction with source account {}", source);
let txn = self.xdr_args.txn()?;
let key = self.config.key_pair()?;
let address =
stellar_strkey::ed25519::PublicKey::from_payload(key.verifying_key().as_bytes())?;
let in_memory = InMemory {
network_passphrase: self.config.get_network()?.network_passphrase,
keypairs: vec![key],
};
self.prompt_user()?;
Ok(in_memory.sign_txn(txn, &stellar_strkey::Strkey::PublicKeyEd25519(address))?)
}

pub fn prompt_user(&self) -> Result<(), Error> {
if self.yes {
return Ok(());
}
Err(Error::UserCancelledSigning)
// TODO use crossterm to prompt user for confirmation
// // Set up the terminal
// let mut stdout = io::stdout();
// execute!(stdout, EnterAlternateScreen)?;
// terminal::enable_raw_mode()?;

// println!("Press 'y' or 'Y' for yes, any other key for no:");

// loop {
// if let Event::Key(KeyEvent {
// code,
// modifiers: KeyModifiers::NONE,
// ..
// }) = event::read()?
// {
// match code {
// KeyCode::Char('y' | 'Y') => break,
// _ => return Err(Error::UserCancelledSigning),
// }
// }
// }

// // Clean up the terminal
// terminal::disable_raw_mode()?;
// execute!(stdout, LeaveAlternateScreen)?;
// Ok(())
}
}
38 changes: 38 additions & 0 deletions cmd/soroban-cli/src/commands/txn/simulate.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
use soroban_rpc::Assembled;
use soroban_sdk::xdr::{self, WriteXdr};

#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error(transparent)]
XdrArgs(#[from] super::xdr::Error),
#[error(transparent)]
Config(#[from] super::super::config::Error),
#[error(transparent)]
Rpc(#[from] crate::rpc::Error),
#[error(transparent)]
Xdr(#[from] xdr::Error),
}

#[derive(Debug, clap::Parser, Clone)]
#[group(skip)]
pub struct Cmd {
#[clap(flatten)]
pub xdr_args: super::xdr::Args,
#[clap(flatten)]
pub config: super::super::config::Args,
}

impl Cmd {
pub async fn run(&self) -> Result<(), Error> {
let res = self.simulate().await?;
println!("{}", res.transaction().to_xdr_base64(xdr::Limits::none())?);
Ok(())
}

pub async fn simulate(&self) -> Result<Assembled, Error> {
let tx = self.xdr_args.txn()?;
let network = self.config.get_network()?;
let client = crate::rpc::Client::new(&network.rpc_url)?;
Ok(client.create_assembled_transaction(&tx).await?)
}
}
67 changes: 67 additions & 0 deletions cmd/soroban-cli/src/commands/txn/xdr.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
use std::{
io::{stdin, Read},
path::PathBuf,
};

use soroban_env_host::xdr::ReadXdr;
use soroban_sdk::xdr::{Limits, Transaction, TransactionEnvelope};

#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("failed to decode XDR from base64")]
Base64Decode,
#[error("failed to decode XDR from file: {0}")]
FileDecode(PathBuf),
#[error("failed to decode XDR from stdin")]
StdinDecode,
#[error(transparent)]
Io(#[from] std::io::Error),
}

/// XDR input, either base64 encoded or file path and stdin if neither is provided
#[derive(Debug, clap::Args, Clone)]
#[group(skip)]
pub struct Args {
/// Base64 encoded XDR transaction
#[arg(
long = "xdr-base64",
env = "STELLAR_TXN_XDR_BASE64",
conflicts_with = "xdr_file"
)]
pub xdr_base64: Option<String>,
//// File containing Binary encoded data
#[arg(
long = "xdr-file",
env = "STELLAR_TXN_XDR_FILE",
conflicts_with = "xdr_base64"
)]
pub xdr_file: Option<PathBuf>,
}

impl Args {
pub fn xdr<T: ReadXdr>(&self) -> Result<T, Error> {
match (self.xdr_base64.as_ref(), self.xdr_file.as_ref()) {
(Some(xdr_base64), None) => {
T::from_xdr_base64(xdr_base64, Limits::none()).map_err(|_| Error::Base64Decode)
}
(_, Some(xdr_file)) => T::from_xdr(std::fs::read(xdr_file)?, Limits::none())
.map_err(|_| Error::FileDecode(xdr_file.clone())),

_ => {
let mut buf = String::new();
let _ = stdin()
.read_to_string(&mut buf)
.map_err(|_| Error::StdinDecode)?;
T::from_xdr_base64(buf.trim(), Limits::none()).map_err(|_| Error::StdinDecode)
}
}
}

pub fn txn(&self) -> Result<Transaction, Error> {
self.xdr::<Transaction>()
}

pub fn txn_envelope(&self) -> Result<TransactionEnvelope, Error> {
self.xdr::<TransactionEnvelope>()
}
}
1 change: 1 addition & 0 deletions cmd/soroban-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ pub mod commands;
pub mod fee;
pub mod key;
pub mod log;
pub mod signer;
pub mod toid;
pub mod utils;
pub mod wasm;
Expand Down
Loading

0 comments on commit 4aafe3f

Please sign in to comment.