Skip to content

Commit f558f0c

Browse files
authored
Add tx edit. (#1893)
1 parent eccf9cb commit f558f0c

File tree

7 files changed

+199
-8
lines changed

7 files changed

+199
-8
lines changed

FULL_HELP_DOCS.md

+9
Original file line numberDiff line numberDiff line change
@@ -1515,6 +1515,7 @@ Sign, Simulate, and Send transactions
15151515

15161516
###### **Subcommands:**
15171517

1518+
* `edit` — Edit a transaction envelope from stdin. This command respects the environment variables `STELLAR_EDITOR`, `EDITOR` and `VISUAL`, in that order
15181519
* `hash` — Calculate the hash of a transaction envelope
15191520
* `new` — Create a new transaction
15201521
* `operation` — Manipulate the operations in a transaction, including adding new operations
@@ -1524,6 +1525,14 @@ Sign, Simulate, and Send transactions
15241525

15251526

15261527

1528+
## `stellar tx edit`
1529+
1530+
Edit a transaction envelope from stdin. This command respects the environment variables `STELLAR_EDITOR`, `EDITOR` and `VISUAL`, in that order
1531+
1532+
**Usage:** `stellar tx edit`
1533+
1534+
1535+
15271536
## `stellar tx hash`
15281537

15291538
Calculate the hash of a transaction envelope

cmd/crates/soroban-test/build.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ fn set_protocol_features() {
1515
println!("cargo:rustc-cfg=feature=\"version_gte_23\"");
1616
}
1717

18-
if major_version < 23 && !std::env::var("CARGO_FEATURE_VERSION_GTE_23").is_ok() {
18+
if major_version < 23 && std::env::var("CARGO_FEATURE_VERSION_GTE_23").is_err() {
1919
println!("cargo:rustc-cfg=feature=\"version_lt_23\"");
2020
}
2121
}

cmd/soroban-cli/build.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ fn set_protocol_features() {
1616
println!("cargo:rustc-cfg=feature=\"version_gte_23\"");
1717
}
1818

19-
if major_version < 23 && !std::env::var("CARGO_FEATURE_VERSION_GTE_23").is_ok() {
19+
if major_version < 23 && std::env::var("CARGO_FEATURE_VERSION_GTE_23").is_err() {
2020
println!("cargo:rustc-cfg=feature=\"version_lt_23\"");
2121
}
2222
}
+173
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
use std::{
2+
env,
3+
fs::{self},
4+
io::{stdin, Cursor, IsTerminal, Write},
5+
path::PathBuf,
6+
process::{self},
7+
};
8+
9+
use serde_json::json;
10+
use stellar_xdr::curr;
11+
12+
use crate::{commands::global, print::Print};
13+
14+
#[derive(thiserror::Error, Debug)]
15+
pub enum Error {
16+
#[error(transparent)]
17+
Io(#[from] std::io::Error),
18+
19+
#[error(transparent)]
20+
StellarXdr(#[from] stellar_xdr::curr::Error),
21+
22+
#[error(transparent)]
23+
SerdeJson(#[from] serde_json::Error),
24+
25+
#[error(transparent)]
26+
Base64Decode(#[from] base64::DecodeError),
27+
28+
#[error("Editor returned non-zero status")]
29+
EditorNonZeroStatus,
30+
31+
#[error("No stdin detected")]
32+
NoStdin,
33+
}
34+
35+
// Command to edit the transaction
36+
/// e.g. `stellar tx new manage-data --data-name hello --build-only | stellar tx edit`
37+
#[derive(Debug, clap::Parser, Clone, Default)]
38+
#[group(skip)]
39+
pub struct Cmd {}
40+
41+
impl Cmd {
42+
pub fn run(&self, global_args: &global::Args) -> Result<(), Error> {
43+
let print = Print::new(global_args.quiet);
44+
let json: String = if stdin().is_terminal() {
45+
return Err(Error::NoStdin);
46+
} else {
47+
let mut input = String::new();
48+
stdin().read_line(&mut input)?;
49+
let input = input.trim();
50+
xdr_to_json::<curr::TransactionEnvelope>(input)?
51+
};
52+
53+
let path = tmp_file(&json)?;
54+
let editor = get_editor();
55+
56+
open_editor(&print, &editor, &path)?;
57+
58+
let contents = fs::read_to_string(&path)?;
59+
let xdr = json_to_xdr::<curr::TransactionEnvelope>(&contents)?;
60+
fs::remove_file(&path)?;
61+
62+
println!("{xdr}");
63+
64+
Ok(())
65+
}
66+
}
67+
68+
struct Editor {
69+
cmd: String,
70+
source: String,
71+
args: Vec<String>,
72+
}
73+
74+
fn tmp_file(contents: &str) -> Result<PathBuf, Error> {
75+
let temp_dir = env::current_dir().unwrap_or(env::temp_dir());
76+
let file_name = format!("stellar-xdr-{}.json", rand::random::<u64>());
77+
let path = temp_dir.join(file_name);
78+
79+
let mut file = fs::File::create(&path)?;
80+
file.write_all(contents.as_bytes())?;
81+
82+
Ok(path)
83+
}
84+
85+
fn get_editor() -> Editor {
86+
let (source, cmd) = env::var("STELLAR_EDITOR")
87+
.map(|val| ("STELLAR_EDITOR", val))
88+
.or_else(|_| env::var("EDITOR").map(|val| ("EDITOR", val)))
89+
.or_else(|_| env::var("VISUAL").map(|val| ("VISUAL", val)))
90+
.unwrap_or_else(|_| ("default", "vim".to_string()));
91+
92+
let parts: Vec<&str> = cmd.split_whitespace().collect();
93+
let cmd = parts[0].to_string();
94+
let args = &parts[1..]
95+
.iter()
96+
.map(|&s| s.to_string())
97+
.collect::<Vec<String>>();
98+
99+
Editor {
100+
source: source.to_string(),
101+
cmd,
102+
args: args.clone(),
103+
}
104+
}
105+
106+
fn open_editor(print: &Print, editor: &Editor, path: &PathBuf) -> Result<(), Error> {
107+
print.infoln(format!(
108+
"Opening editor with `{source}=\"{cmd}\"`...",
109+
source = editor.source,
110+
cmd = editor.cmd,
111+
));
112+
113+
let mut binding = process::Command::new(editor.cmd.clone());
114+
let command = binding.args(editor.args.clone()).arg(path);
115+
116+
// Windows doesn't have devices like /dev/tty.
117+
#[cfg(unix)]
118+
{
119+
use fs::File;
120+
let tty = File::open("/dev/tty")?;
121+
let tty_out = fs::OpenOptions::new().write(true).open("/dev/tty")?;
122+
let tty_err = fs::OpenOptions::new().write(true).open("/dev/tty")?;
123+
124+
command
125+
.stdin(tty)
126+
.stdout(tty_out)
127+
.stderr(tty_err)
128+
.env("TERM", "xterm-256color");
129+
}
130+
131+
let status = command.spawn()?.wait()?;
132+
133+
if status.success() {
134+
Ok(())
135+
} else {
136+
Err(Error::EditorNonZeroStatus)
137+
}
138+
}
139+
140+
fn xdr_to_json<T>(xdr_string: &str) -> Result<String, Error>
141+
where
142+
T: curr::ReadXdr + serde::Serialize,
143+
{
144+
let tx = T::from_xdr_base64(xdr_string, curr::Limits::none())?;
145+
let mut schema: serde_json::Value = serde_json::to_value(tx)?;
146+
schema["$schema"] = json!(
147+
"https://github.com/stellar/rs-stellar-xdr/raw/main/xdr-json/curr/TransactionEnvelope.json"
148+
);
149+
let json = serde_json::to_string_pretty(&schema)?;
150+
151+
Ok(json)
152+
}
153+
154+
fn json_to_xdr<T>(json_string: &str) -> Result<String, Error>
155+
where
156+
T: serde::de::DeserializeOwned + curr::WriteXdr,
157+
{
158+
let mut schema: serde_json::Value = serde_json::from_str(json_string)?;
159+
160+
if let Some(obj) = schema.as_object_mut() {
161+
obj.remove("$schema");
162+
}
163+
164+
let json_string = serde_json::to_string(&schema)?;
165+
166+
let value: T = serde_json::from_str(json_string.as_str())?;
167+
let mut data = Vec::new();
168+
let cursor = Cursor::new(&mut data);
169+
let mut limit = curr::Limited::new(cursor, curr::Limits::none());
170+
value.write_xdr(&mut limit)?;
171+
172+
Ok(value.to_xdr_base64(curr::Limits::none())?)
173+
}

cmd/soroban-cli/src/commands/tx/mod.rs

+7
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use super::global;
22

33
pub mod args;
4+
pub mod edit;
45
pub mod hash;
56
pub mod help;
67
pub mod new;
@@ -14,6 +15,9 @@ pub use args::Args;
1415

1516
#[derive(Debug, clap::Subcommand)]
1617
pub enum Cmd {
18+
/// Edit a transaction envelope from stdin. This command respects the environment variables
19+
/// `STELLAR_EDITOR`, `EDITOR` and `VISUAL`, in that order.
20+
Edit(edit::Cmd),
1721
/// Calculate the hash of a transaction envelope
1822
Hash(hash::Cmd),
1923
/// Create a new transaction
@@ -37,6 +41,8 @@ pub enum Error {
3741
#[error(transparent)]
3842
New(#[from] new::Error),
3943
#[error(transparent)]
44+
Edit(#[from] edit::Error),
45+
#[error(transparent)]
4046
Op(#[from] op::Error),
4147
#[error(transparent)]
4248
Send(#[from] send::Error),
@@ -53,6 +59,7 @@ impl Cmd {
5359
match self {
5460
Cmd::Hash(cmd) => cmd.run(global_args)?,
5561
Cmd::New(cmd) => cmd.run(global_args).await?,
62+
Cmd::Edit(cmd) => cmd.run(global_args)?,
5663
Cmd::Operation(cmd) => cmd.run(global_args).await?,
5764
Cmd::Send(cmd) => cmd.run(global_args).await?,
5865
Cmd::Sign(cmd) => cmd.run(global_args).await?,

cmd/soroban-cli/src/commands/tx/send.rs

+7-5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::{print::Print, utils::transaction_hash};
1+
use crate::print::Print;
22
use async_trait::async_trait;
33
use soroban_rpc::GetTransactionResponse;
44
use std::ffi::OsString;
@@ -8,6 +8,8 @@ use crate::{
88
config::{self, locator, network},
99
};
1010

11+
use stellar_xdr::curr;
12+
1113
#[derive(thiserror::Error, Debug)]
1214
pub enum Error {
1315
#[error(transparent)]
@@ -20,6 +22,8 @@ pub enum Error {
2022
Rpc(#[from] crate::rpc::Error),
2123
#[error(transparent)]
2224
SerdeJson(#[from] serde_json::Error),
25+
#[error("xdr processing error: {0}")]
26+
Xdr(#[from] curr::Error),
2327
}
2428

2529
#[derive(Debug, clap::Parser, Clone)]
@@ -62,11 +66,9 @@ impl NetworkRunnable for Cmd {
6266
let client = network.rpc_client()?;
6367
let tx_env = super::xdr::tx_envelope_from_input(&self.tx_xdr)?;
6468

65-
if let Ok(Ok(hash)) = super::xdr::unwrap_envelope_v1(tx_env.clone())
66-
.map(|tx| transaction_hash(&tx, &network.network_passphrase))
67-
{
69+
if let Ok(txn) = super::xdr::unwrap_envelope_v1(tx_env.clone()) {
6870
let print = Print::new(globals.map_or(false, |g| g.quiet));
69-
print.infoln(format!("Transaction Hash: {}", hex::encode(hash)));
71+
print.log_transaction(&txn, &network, true)?;
7072
}
7173

7274
Ok(client.send_transaction_polling(&tx_env).await?)

cmd/soroban-cli/src/commands/tx/xdr.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ pub fn tx_envelope_from_input(input: &Option<OsString>) -> Result<TransactionEnv
3030
&mut Cursor::new(input.clone().into_encoded_bytes())
3131
}
3232
} else {
33-
print::Print::new(false).infoln("waiting for transaction input...");
33+
print::Print::new(false).infoln("Waiting for transaction input...");
3434
&mut stdin()
3535
};
3636

0 commit comments

Comments
 (0)