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
14const SCHEMA_URL: &str =
15 "https://github.com/stellar/rs-stellar-xdr/raw/main/xdr-json/curr/TransactionEnvelope.json";
16
17#[derive(thiserror::Error, Debug)]
18pub enum Error {
19 #[error(transparent)]
20 Io(#[from] std::io::Error),
21
22 #[error(transparent)]
23 StellarXdr(#[from] stellar_xdr::curr::Error),
24
25 #[error(transparent)]
26 SerdeJson(#[from] serde_json::Error),
27
28 #[error(transparent)]
29 Base64Decode(#[from] base64::DecodeError),
30
31 #[error("Editor returned non-zero status")]
32 EditorNonZeroStatus,
33}
34
35#[derive(Debug, clap::Parser, Clone, Default)]
38#[group(skip)]
39pub struct Cmd {}
40
41impl 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 default_json()
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
68struct Editor {
69 cmd: String,
70 source: String,
71 args: Vec<String>,
72}
73
74fn 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
85fn 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
106fn 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 #[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
140fn xdr_to_json<T>(xdr_string: &str) -> Result<String, Error>
141where
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!(SCHEMA_URL);
147 let json = serde_json::to_string_pretty(&schema)?;
148
149 Ok(json)
150}
151
152fn json_to_xdr<T>(json_string: &str) -> Result<String, Error>
153where
154 T: serde::de::DeserializeOwned + curr::WriteXdr,
155{
156 let mut schema: serde_json::Value = serde_json::from_str(json_string)?;
157
158 if let Some(obj) = schema.as_object_mut() {
159 obj.remove("$schema");
160 }
161
162 let json_string = serde_json::to_string(&schema)?;
163
164 let value: T = serde_json::from_str(json_string.as_str())?;
165 let mut data = Vec::new();
166 let cursor = Cursor::new(&mut data);
167 let mut limit = curr::Limited::new(cursor, curr::Limits::none());
168 value.write_xdr(&mut limit)?;
169
170 Ok(value.to_xdr_base64(curr::Limits::none())?)
171}
172
173fn default_json() -> String {
174 format!(
175 r#"{{
176 "$schema": "{SCHEMA_URL}",
177 "tx": {{
178 "tx": {{
179 "source_account": "",
180 "fee": 100,
181 "seq_num": 0,
182 "cond": "none",
183 "memo": "none",
184 "operations": [],
185 "ext": "v0"
186 }},
187 "signatures": []
188 }}
189}}
190"#
191 )
192}