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 fn project_dir() -> Result<directories::ProjectDirs, Error> {
26    let dir = if let Ok(data_home) = std::env::var("STELLAR_DATA_HOME") {
27        ProjectDirs::from_path(std::path::PathBuf::from(data_home))
28    } else if let Ok(data_home) = std::env::var("XDG_DATA_HOME") {
29        ProjectDirs::from_path(std::path::PathBuf::from(data_home).join("stellar-cli"))
30    } else {
31        ProjectDirs::from("org", "stellar", "stellar-cli")
32    };
33
34    dir.ok_or(Error::FailedToFindProjectDirs)
35}
36
37#[allow(clippy::module_name_repetitions)]
38pub fn data_local_dir() -> Result<std::path::PathBuf, Error> {
39    Ok(project_dir()?.data_local_dir().to_path_buf())
40}
41
42pub fn actions_dir() -> Result<std::path::PathBuf, Error> {
43    let dir = data_local_dir()?.join("actions");
44    std::fs::create_dir_all(&dir)?;
45    Ok(dir)
46}
47
48pub fn spec_dir() -> Result<std::path::PathBuf, Error> {
49    let dir = data_local_dir()?.join("spec");
50    std::fs::create_dir_all(&dir)?;
51    Ok(dir)
52}
53
54pub fn bucket_dir() -> Result<std::path::PathBuf, Error> {
55    let dir = data_local_dir()?.join("bucket");
56    std::fs::create_dir_all(&dir)?;
57    Ok(dir)
58}
59
60pub fn write(action: Action, rpc_url: &Url) -> Result<ulid::Ulid, Error> {
61    let data = Data {
62        action,
63        rpc_url: rpc_url.to_string(),
64    };
65    let id = ulid::Ulid::new();
66    let file = actions_dir()?.join(id.to_string()).with_extension("json");
67    std::fs::write(file, serde_json::to_string(&data)?)?;
68    Ok(id)
69}
70
71pub fn read(id: &ulid::Ulid) -> Result<(Action, Url), Error> {
72    let file = actions_dir()?.join(id.to_string()).with_extension("json");
73    let data: Data = serde_json::from_str(&std::fs::read_to_string(file)?)?;
74    Ok((data.action, Url::from_str(&data.rpc_url)?))
75}
76
77pub fn write_spec(hash: &str, spec_entries: &[xdr::ScSpecEntry]) -> Result<(), Error> {
78    let file = spec_dir()?.join(hash);
79    tracing::trace!("writing spec to {:?}", file);
80    let mut contents: Vec<u8> = Vec::new();
81    for entry in spec_entries {
82        contents.extend(entry.to_xdr(xdr::Limits::none())?);
83    }
84    std::fs::write(file, contents)?;
85    Ok(())
86}
87
88pub fn read_spec(hash: &str) -> Result<Vec<xdr::ScSpecEntry>, Error> {
89    let file = spec_dir()?.join(hash);
90    tracing::trace!("reading spec from {:?}", file);
91    Ok(soroban_spec::read::parse_raw(&std::fs::read(file)?)?)
92}
93
94pub fn list_ulids() -> Result<Vec<ulid::Ulid>, Error> {
95    let dir = actions_dir()?;
96    let mut list = std::fs::read_dir(dir)?
97        .map(|entry| {
98            entry
99                .map(|e| e.file_name().into_string().unwrap())
100                .map_err(Error::from)
101        })
102        .collect::<Result<Vec<String>, Error>>()?;
103    list.sort();
104    Ok(list
105        .iter()
106        .map(|s| ulid::Ulid::from_str(s.trim_end_matches(".json")))
107        .collect::<Result<Vec<_>, _>>()?)
108}
109
110pub fn list_actions() -> Result<Vec<DatedAction>, Error> {
111    list_ulids()?
112        .into_iter()
113        .rev()
114        .map(|id| {
115            let (action, uri) = read(&id)?;
116            Ok(DatedAction(id, action, uri))
117        })
118        .collect::<Result<Vec<_>, Error>>()
119}
120
121pub struct DatedAction(ulid::Ulid, Action, Url);
122
123impl std::fmt::Display for DatedAction {
124    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
125        let (id, a, uri) = (&self.0, &self.1, &self.2);
126        let datetime = to_datatime(id).format("%b %d %H:%M");
127        let status = match a {
128            Action::Simulate { response } => response
129                .error
130                .as_ref()
131                .map_or_else(|| "SUCCESS".to_string(), |_| "ERROR".to_string()),
132            Action::Send { response } => response.status.clone(),
133        };
134        write!(f, "{id} {} {status} {datetime} {uri} ", a.type_str(),)
135    }
136}
137
138impl DatedAction {}
139
140fn to_datatime(id: &ulid::Ulid) -> chrono::DateTime<chrono::Utc> {
141    chrono::DateTime::from_timestamp_millis(id.timestamp_ms().try_into().unwrap()).unwrap()
142}
143
144#[derive(Serialize, Deserialize)]
145#[serde(rename_all = "snake_case")]
146struct Data {
147    action: Action,
148    rpc_url: String,
149}
150
151#[derive(Serialize, Deserialize, Clone)]
152#[serde(rename_all = "snake_case")]
153pub enum Action {
154    Simulate {
155        response: SimulateTransactionResponse,
156    },
157    Send {
158        response: GetTransactionResponseRaw,
159    },
160}
161
162impl Action {
163    pub fn type_str(&self) -> String {
164        match self {
165            Action::Simulate { .. } => "Simulate",
166            Action::Send { .. } => "Send    ",
167        }
168        .to_string()
169    }
170}
171
172impl From<SimulateTransactionResponse> for Action {
173    fn from(response: SimulateTransactionResponse) -> Self {
174        Self::Simulate { response }
175    }
176}
177
178impl TryFrom<GetTransactionResponse> for Action {
179    type Error = xdr::Error;
180    fn try_from(res: GetTransactionResponse) -> Result<Self, Self::Error> {
181        Ok(Self::Send {
182            response: GetTransactionResponseRaw {
183                status: res.status,
184                ledger: res.ledger,
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}