soroban_cli/commands/tx/
edit.rs1use std::{
2 env,
3 fs::{self},
4 io::{stdin, Cursor, IsTerminal, Write},
5 path::PathBuf,
6 process::{self},
7};
8
9use serde_json::json;
10use stellar_xdr::curr;
11
12use crate::{commands::global, print::Print};
13
14fn schema_url() -> String {
15 let ver = stellar_xdr::VERSION.pkg;
16 format!("https://stellar.org/schema/xdr-json/v{ver}/TransactionEnvelope.json")
17}
18
19#[derive(thiserror::Error, Debug)]
20pub enum Error {
21 #[error(transparent)]
22 Io(#[from] std::io::Error),
23
24 #[error(transparent)]
25 StellarXdr(#[from] stellar_xdr::curr::Error),
26
27 #[error(transparent)]
28 SerdeJson(#[from] serde_json::Error),
29
30 #[error(transparent)]
31 Base64Decode(#[from] base64::DecodeError),
32
33 #[error("Editor returned non-zero status")]
34 EditorNonZeroStatus,
35}
36
37#[derive(Debug, clap::Parser, Clone, Default)]
40#[group(skip)]
41pub struct Cmd {}
42
43impl Cmd {
44 pub fn run(&self, global_args: &global::Args) -> Result<(), Error> {
45 let print = Print::new(global_args.quiet);
46 let json: String = if stdin().is_terminal() {
47 default_json()
48 } else {
49 let mut input = String::new();
50 stdin().read_line(&mut input)?;
51 let input = input.trim();
52 xdr_to_json::<curr::TransactionEnvelope>(input)?
53 };
54
55 let path = tmp_file(&json)?;
56 let editor = get_editor();
57
58 open_editor(&print, &editor, &path)?;
59
60 let contents = fs::read_to_string(&path)?;
61 let xdr = json_to_xdr::<curr::TransactionEnvelope>(&contents)?;
62 fs::remove_file(&path)?;
63
64 println!("{xdr}");
65
66 Ok(())
67 }
68}
69
70struct Editor {
71 cmd: String,
72 source: String,
73 args: Vec<String>,
74}
75
76fn tmp_file(contents: &str) -> Result<PathBuf, Error> {
77 let temp_dir = env::current_dir().unwrap_or(env::temp_dir());
78 let file_name = format!("stellar-xdr-{}.json", rand::random::<u64>());
79 let path = temp_dir.join(file_name);
80
81 let mut file = fs::File::create(&path)?;
82 file.write_all(contents.as_bytes())?;
83
84 Ok(path)
85}
86
87fn get_editor() -> Editor {
88 let (source, cmd) = env::var("STELLAR_EDITOR")
89 .map(|val| ("STELLAR_EDITOR", val))
90 .or_else(|_| env::var("EDITOR").map(|val| ("EDITOR", val)))
91 .or_else(|_| env::var("VISUAL").map(|val| ("VISUAL", val)))
92 .unwrap_or_else(|_| ("default", "vim".to_string()));
93
94 let parts: Vec<&str> = cmd.split_whitespace().collect();
95 let cmd = parts[0].to_string();
96 let args = &parts[1..]
97 .iter()
98 .map(|&s| s.to_string())
99 .collect::<Vec<String>>();
100
101 Editor {
102 source: source.to_string(),
103 cmd,
104 args: args.clone(),
105 }
106}
107
108fn open_editor(print: &Print, editor: &Editor, path: &PathBuf) -> Result<(), Error> {
109 print.infoln(format!(
110 "Opening editor with `{source}=\"{cmd}\"`...",
111 source = editor.source,
112 cmd = editor.cmd,
113 ));
114
115 let mut binding = process::Command::new(editor.cmd.clone());
116 let command = binding.args(editor.args.clone()).arg(path);
117
118 #[cfg(unix)]
120 {
121 use fs::File;
122 let tty = File::open("/dev/tty")?;
123 let tty_out = fs::OpenOptions::new().write(true).open("/dev/tty")?;
124 let tty_err = fs::OpenOptions::new().write(true).open("/dev/tty")?;
125
126 command
127 .stdin(tty)
128 .stdout(tty_out)
129 .stderr(tty_err)
130 .env("TERM", "xterm-256color");
131 }
132
133 let status = command.spawn()?.wait()?;
134
135 if status.success() {
136 Ok(())
137 } else {
138 Err(Error::EditorNonZeroStatus)
139 }
140}
141
142fn xdr_to_json<T>(xdr_string: &str) -> Result<String, Error>
143where
144 T: curr::ReadXdr + serde::Serialize,
145{
146 let tx = T::from_xdr_base64(xdr_string, curr::Limits::none())?;
147 let mut schema: serde_json::Value = serde_json::to_value(tx)?;
148 schema["$schema"] = json!(schema_url());
149 let json = serde_json::to_string_pretty(&schema)?;
150
151 Ok(json)
152}
153
154fn json_to_xdr<T>(json_string: &str) -> Result<String, Error>
155where
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}
174
175fn default_json() -> String {
176 let schema_url = schema_url();
177 format!(
178 r#"{{
179 "$schema": "{schema_url}",
180 "tx": {{
181 "tx": {{
182 "source_account": "",
183 "fee": 100,
184 "seq_num": 0,
185 "cond": "none",
186 "memo": "none",
187 "operations": [],
188 "ext": "v0"
189 }},
190 "signatures": []
191 }}
192}}
193"#
194 )
195}