soroban_cli/config/
data.rs1use crate::rpc::{GetTransactionResponse, GetTransactionResponseRaw, SimulateTransactionResponse};
2use directories::ProjectDirs;
3use serde::{Deserialize, Serialize};
4use std::str::FromStr;
5use url::Url;
6
7use crate::xdr::{self, WriteXdr};
8
9#[derive(thiserror::Error, Debug)]
10pub enum Error {
11 #[error("Failed to find project directories")]
12 FailedToFindProjectDirs,
13 #[error(transparent)]
14 Io(#[from] std::io::Error),
15 #[error(transparent)]
16 SerdeJson(#[from] serde_json::Error),
17 #[error(transparent)]
18 InvalidUrl(#[from] url::ParseError),
19 #[error(transparent)]
20 Ulid(#[from] ulid::DecodeError),
21 #[error(transparent)]
22 Xdr(#[from] xdr::Error),
23}
24
25pub const XDG_DATA_HOME: &str = "XDG_DATA_HOME";
26
27pub fn project_dir() -> Result<directories::ProjectDirs, Error> {
28 std::env::var(XDG_DATA_HOME)
29 .map_or_else(
30 |_| ProjectDirs::from("org", "stellar", "stellar-cli"),
31 |data_home| {
32 ProjectDirs::from_path(std::path::PathBuf::from(data_home).join("stellar-cli"))
33 },
34 )
35 .ok_or(Error::FailedToFindProjectDirs)
36}
37
38#[allow(clippy::module_name_repetitions)]
39pub fn data_local_dir() -> Result<std::path::PathBuf, Error> {
40 Ok(project_dir()?.data_local_dir().to_path_buf())
41}
42
43pub fn actions_dir() -> Result<std::path::PathBuf, Error> {
44 let dir = data_local_dir()?.join("actions");
45 std::fs::create_dir_all(&dir)?;
46 Ok(dir)
47}
48
49pub fn spec_dir() -> Result<std::path::PathBuf, Error> {
50 let dir = data_local_dir()?.join("spec");
51 std::fs::create_dir_all(&dir)?;
52 Ok(dir)
53}
54
55pub fn bucket_dir() -> Result<std::path::PathBuf, Error> {
56 let dir = data_local_dir()?.join("bucket");
57 std::fs::create_dir_all(&dir)?;
58 Ok(dir)
59}
60
61pub fn write(action: Action, rpc_url: &Url) -> Result<ulid::Ulid, Error> {
62 let data = Data {
63 action,
64 rpc_url: rpc_url.to_string(),
65 };
66 let id = ulid::Ulid::new();
67 let file = actions_dir()?.join(id.to_string()).with_extension("json");
68 std::fs::write(file, serde_json::to_string(&data)?)?;
69 Ok(id)
70}
71
72pub fn read(id: &ulid::Ulid) -> Result<(Action, Url), Error> {
73 let file = actions_dir()?.join(id.to_string()).with_extension("json");
74 let data: Data = serde_json::from_str(&std::fs::read_to_string(file)?)?;
75 Ok((data.action, Url::from_str(&data.rpc_url)?))
76}
77
78pub fn write_spec(hash: &str, spec_entries: &[xdr::ScSpecEntry]) -> Result<(), Error> {
79 let file = spec_dir()?.join(hash);
80 tracing::trace!("writing spec to {:?}", file);
81 let mut contents: Vec<u8> = Vec::new();
82 for entry in spec_entries {
83 contents.extend(entry.to_xdr(xdr::Limits::none())?);
84 }
85 std::fs::write(file, contents)?;
86 Ok(())
87}
88
89pub fn read_spec(hash: &str) -> Result<Vec<xdr::ScSpecEntry>, Error> {
90 let file = spec_dir()?.join(hash);
91 tracing::trace!("reading spec from {:?}", file);
92 Ok(soroban_spec::read::parse_raw(&std::fs::read(file)?)?)
93}
94
95pub fn list_ulids() -> Result<Vec<ulid::Ulid>, Error> {
96 let dir = actions_dir()?;
97 let mut list = std::fs::read_dir(dir)?
98 .map(|entry| {
99 entry
100 .map(|e| e.file_name().into_string().unwrap())
101 .map_err(Error::from)
102 })
103 .collect::<Result<Vec<String>, Error>>()?;
104 list.sort();
105 Ok(list
106 .iter()
107 .map(|s| ulid::Ulid::from_str(s.trim_end_matches(".json")))
108 .collect::<Result<Vec<_>, _>>()?)
109}
110
111pub fn list_actions() -> Result<Vec<DatedAction>, Error> {
112 list_ulids()?
113 .into_iter()
114 .rev()
115 .map(|id| {
116 let (action, uri) = read(&id)?;
117 Ok(DatedAction(id, action, uri))
118 })
119 .collect::<Result<Vec<_>, Error>>()
120}
121
122pub struct DatedAction(ulid::Ulid, Action, Url);
123
124impl std::fmt::Display for DatedAction {
125 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
126 let (id, a, uri) = (&self.0, &self.1, &self.2);
127 let datetime = to_datatime(id).format("%b %d %H:%M");
128 let status = match a {
129 Action::Simulate { response } => response
130 .error
131 .as_ref()
132 .map_or_else(|| "SUCCESS".to_string(), |_| "ERROR".to_string()),
133 Action::Send { response } => response.status.to_string(),
134 };
135 write!(f, "{id} {} {status} {datetime} {uri} ", a.type_str(),)
136 }
137}
138
139impl DatedAction {}
140
141fn to_datatime(id: &ulid::Ulid) -> chrono::DateTime<chrono::Utc> {
142 chrono::DateTime::from_timestamp_millis(id.timestamp_ms().try_into().unwrap()).unwrap()
143}
144
145#[derive(Serialize, Deserialize)]
146#[serde(rename_all = "snake_case")]
147struct Data {
148 action: Action,
149 rpc_url: String,
150}
151
152#[derive(Serialize, Deserialize, Clone)]
153#[serde(rename_all = "snake_case")]
154pub enum Action {
155 Simulate {
156 response: SimulateTransactionResponse,
157 },
158 Send {
159 response: GetTransactionResponseRaw,
160 },
161}
162
163impl Action {
164 pub fn type_str(&self) -> String {
165 match self {
166 Action::Simulate { .. } => "Simulate",
167 Action::Send { .. } => "Send ",
168 }
169 .to_string()
170 }
171}
172
173impl From<SimulateTransactionResponse> for Action {
174 fn from(response: SimulateTransactionResponse) -> Self {
175 Self::Simulate { response }
176 }
177}
178
179impl TryFrom<GetTransactionResponse> for Action {
180 type Error = xdr::Error;
181 fn try_from(res: GetTransactionResponse) -> Result<Self, Self::Error> {
182 Ok(Self::Send {
183 response: GetTransactionResponseRaw {
184 status: res.status,
185 envelope_xdr: res.envelope.as_ref().map(to_xdr).transpose()?,
186 result_xdr: res.result.as_ref().map(to_xdr).transpose()?,
187 result_meta_xdr: res.result_meta.as_ref().map(to_xdr).transpose()?,
188 events: None,
189 },
190 })
191 }
192}
193
194fn to_xdr(data: &impl WriteXdr) -> Result<String, xdr::Error> {
195 data.to_xdr_base64(xdr::Limits::none())
196}
197
198#[cfg(test)]
199mod test {
200 use super::*;
201
202 #[test]
203 fn test_write_read() {
204 let t = assert_fs::TempDir::new().unwrap();
205 std::env::set_var(XDG_DATA_HOME, t.path().to_str().unwrap());
206 let rpc_uri = Url::from_str("http://localhost:8000").unwrap();
207 let sim = SimulateTransactionResponse::default();
208 let original_action: Action = sim.into();
209
210 let id = write(original_action.clone(), &rpc_uri.clone()).unwrap();
211 let (action, new_rpc_uri) = read(&id).unwrap();
212 assert_eq!(rpc_uri, new_rpc_uri);
213 match (action, original_action) {
214 (Action::Simulate { response: a }, Action::Simulate { response: b }) => {
215 assert_eq!(a.min_resource_fee, b.min_resource_fee);
216 }
217 _ => panic!("Action mismatch"),
218 }
219 }
220}