1pub mod prelude {
2 #![allow(unused_imports)]
3 pub use crate::Salt;
4
5 pub(crate) use tracing::{debug, error, info, trace, warn};
6
7 pub(crate) use crate::{Error, Result};
8 pub(crate) use camino::{Utf8Path, Utf8PathBuf};
9}
10
11use std::process::ExitStatus;
12
13use camino::FromPathBufError;
14use cli::Command;
15use git::Git;
16use url::Url;
17use which::which;
18
19use crate::prelude::*;
20
21pub struct Salt {
22 project_folder: Utf8PathBuf,
23 config: SaltConfig,
24}
25
26#[derive(Clone, serde::Deserialize)]
27pub struct SaltConfig {
28 #[serde(rename = "PRIVATE_KEY")]
29 pub private_key: String,
30
31 #[serde(rename = "ORCHESTRATION_NETWORK_RPC_NODE_URL")]
32 pub orchestration_network_rpc_node: Url,
33
34 #[serde(rename = "BROADCASTING_NETWORK_RPC_NODE_URL")]
35 pub broadcasting_network_rpc_node: Url,
36
37 #[serde(rename = "BROADCASTING_NETWORK_ID")]
38 pub broadcasting_network_id: String,
39}
40
41impl SaltConfig {
42 fn iter(self) -> impl IntoIterator<Item = (&'static str, String)> {
43 [
44 ("PRIVATE_KEY", self.private_key),
45 (
46 "ORCHESTRATION_NETWORK_RPC_NODE_URL",
47 self.orchestration_network_rpc_node.to_string(),
48 ),
49 (
50 "BROADCASTING_NETWORK_RPC_NODE_URL",
51 self.broadcasting_network_rpc_node.to_string(),
52 ),
53 ("BROADCASTING_NETWORK_ID", self.broadcasting_network_id),
54 ]
55 }
56}
57
58#[derive(thiserror::Error, Debug)]
59pub enum Error {
60 #[error(
61 "Couldn't find appropriate default director: https://docs.rs/dirs/latest/dirs/fn.data_dir.html or https://docs.rs/dirs/latest/dirs/fn.data_local_dir.html"
62 )]
63 NoStandardDirectoryFound,
64
65 #[error("{0}")]
66 Camino(#[from] FromPathBufError),
67
68 #[error("Executable file doesn't exist")]
69 ExecutableFileDoesntExist(Utf8PathBuf),
70
71 #[error("{0}")]
72 FailedToExecute(std::io::Error),
73
74 #[error("Subprocess exited badly: {0:?}")]
75 SubprocessExited(ExitStatus),
76
77 #[error(
78 "Expected `{bin_name}` binary to be in PATH environment variable or finable with which https://docs.rs/which/latest/which/fn.which.html ({err_msg}): {which}"
79 )]
80 Which {
81 bin_name: String,
82 err_msg: String,
83 which: ::which::Error,
84 },
85}
86
87pub type Result<T> = core::result::Result<T, Error>;
88
89impl Salt {
90 fn default_project_path() -> Result<Utf8PathBuf> {
91 let dir = dirs::data_local_dir()
92 .or(dirs::data_dir())
93 .ok_or(Error::NoStandardDirectoryFound)?;
94 let path = Utf8PathBuf::try_from(dir)?;
95 Ok(path.join("salt-asset-manager"))
96 }
97
98 pub fn new(config: SaltConfig) -> Result<Salt> {
99 let salt = Salt {
100 project_folder: Salt::default_project_path()?,
101 config,
102 };
103
104 salt.init()?;
105
106 Ok(salt)
107 }
108
109 pub fn transaction(
110 &self,
111 amount: String,
112 vault_address: String,
113 recipient_address: String,
114 ) -> Result<()> {
115 self.cmd([
116 "-amount".into(),
117 amount,
118 "-vault-address".into(),
119 vault_address,
120 "-recipient-address".into(),
121 recipient_address,
122 ])?
123 .run_and_wait()?;
124 Ok(())
125 }
126
127 fn init(&self) -> Result<()> {
129 let git = self.git()?;
130 git.ensure_latest_branch(
131 Url::parse("https://github.com/ActuallyHappening/salt-asset-manager").unwrap(),
132 "master",
133 )?;
134
135 let deno = Salt::deno()?;
136 cli::Command::pure(deno)?
137 .with_cwd(self.project_folder.clone())
138 .with_args(["install".into()])
139 .run_and_wait()?;
140
141 if self.project_folder.join("fix.nu").exists() {
142 debug!("Detected fix.nu, running this after deno install");
143 let nu = which(
145 "nu",
146 "required shell to run fix.nu, see https://www.nushell.sh/book/installation.html#package-managers",
147 )?;
148 cli::Command::pure(nu)?
149 .with_cwd(self.project_folder.clone())
150 .with_args(["fix.nu".into()])
151 .run_and_wait()?;
152 }
153
154 info!(
155 "Successfully initialized/updated git checkout at {} ready for runtime consumption",
156 self.project_folder
157 );
158
159 Ok(())
160 }
161
162 fn deno() -> Result<Utf8PathBuf> {
163 which("deno", "required javascript runtime")
164 }
165
166 fn cmd(&self, args: impl IntoIterator<Item = String>) -> Result<Command> {
167 let cmd = cli::Command::pure(Salt::deno()?)?.with_cwd(self.project_folder.clone())
168 .with_args(
169 [
170 "run",
171 "--unstable-sloppy-imports",
172 "-A",
173 "src/index.ts",
174 "--",
175 "-use-cli-only",
176 ]
177 .into_iter()
178 .map(String::from),
179 )
180 .with_args(args)
181 .with_envs(self.config.clone().iter());
182 Ok(cmd)
183 }
184
185 fn git(&self) -> Result<Git> {
186 Ok(Git::new(self.project_folder.to_owned())?)
187 }
188}
189
190mod cli {
191 use crate::prelude::*;
192
193 pub struct Command(std::process::Command);
194
195 impl Command {
196 pub fn pure(cmd: Utf8PathBuf) -> Result<Command> {
197 if !cmd.exists() {
198 return Err(Error::ExecutableFileDoesntExist(cmd));
199 }
200 let mut cmd = std::process::Command::new(cmd);
201 cmd.env_clear();
202 Ok(Command(cmd))
203 }
204
205 pub fn current_dir(&mut self, cwd: Utf8PathBuf) -> &mut Self {
206 self.0.current_dir(cwd);
207 self
208 }
209
210 pub fn with_cwd(mut self, cwd: Utf8PathBuf) -> Self {
211 self.current_dir(cwd);
212 self
213 }
214
215 pub fn with_args(mut self, args: impl IntoIterator<Item = String>) -> Self {
216 self.0.args(args);
217 self
218 }
219
220 pub fn with_envs(mut self, envs: impl IntoIterator<Item = (&'static str, String)>) -> Self {
221 self.0.envs(envs);
222 self
223 }
224
225 pub fn run_and_wait(mut self) -> Result<()> {
226 info!("Running command {:?}", self.0);
227 let status = self.0.status().map_err(Error::FailedToExecute)?;
228 if !status.success() {
229 return Err(Error::SubprocessExited(status));
230 }
231 Ok(())
232 }
233 }
234}
235
236mod git {
237 use url::Url;
238
239 use crate::{cli::Command, prelude::*, which::which};
240
241 pub struct Git {
242 project_folder: Utf8PathBuf,
243 }
244
245 impl Git {
246 pub fn new(path: Utf8PathBuf) -> Result<Self> {
247 Ok(Self {
248 project_folder: path,
249 })
250 }
251
252 fn cmd(&self) -> Result<Command> {
253 let git = which("git", "required runtime dependency")?;
254 let cmd = Command::pure(git)?.with_cwd(self.project_folder.to_owned());
255 Ok(cmd)
256 }
257
258 pub fn ensure_latest_branch(&self, repository_url: Url, branch: &str) -> Result<()> {
259 if self.project_folder.exists() {
260 self.pull()?;
262 } else {
263 self.clone(repository_url)?;
264 }
265 self.checkout(branch)?;
266 Ok(())
267 }
268
269 fn pull(&self) -> Result<()> {
270 debug!("Running `git pull` in directory {}", &self.project_folder);
271 self.cmd()?.with_args(["pull".into()]).run_and_wait()
272 }
273
274 fn clone(&self, repository_url: Url) -> Result<()> {
275 let mut parent_folder = self.project_folder.clone();
276 if !parent_folder.pop() {
277 panic!("self.project_folder has no parent? Why is the data dir at / ?");
278 }
279 debug!(
280 "Running `git clone {}` in directory {}",
281 repository_url, &parent_folder
282 );
283 self.cmd()?
284 .with_cwd(parent_folder)
285 .with_args([
286 "clone".into(),
287 repository_url.to_string(),
288 self.project_folder.to_string(),
289 ])
290 .run_and_wait()
291 }
292
293 fn checkout(&self, branch: &str) -> Result<()> {
294 self.cmd()?
295 .with_args(["checkout".into(), branch.into()])
296 .run_and_wait()
297 }
298 }
299}
300
301mod which {
302 use crate::prelude::*;
303
304 pub fn which(name: &'static str, err_msg: impl Into<String>) -> Result<Utf8PathBuf> {
305 let path = ::which::which(name).map_err(|which| Error::Which {
306 bin_name: name.to_owned(),
307 err_msg: err_msg.into(),
308 which,
309 })?;
310 Ok(Utf8PathBuf::try_from(path)?)
311 }
312}