Skip to main content

salt_sdk/
lib.rs

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	/// git pull && deno install && nu fix.nu
128	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			// run fix.nu
144			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				// assume already checked out
261				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}