soroban_cli/config/
data.rs

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