salt-sdk 0.0.0

Salt asset manager Rust SDK
Documentation
pub mod prelude {
	#![allow(unused_imports)]
	pub use crate::Salt;

	pub(crate) use tracing::{debug, error, info, trace, warn};

	pub(crate) use crate::{Error, Result};
	pub(crate) use camino::{Utf8Path, Utf8PathBuf};
}

use std::process::ExitStatus;

use camino::FromPathBufError;
use cli::Command;
use git::Git;
use url::Url;
use which::which;

use crate::prelude::*;

pub struct Salt {
	project_folder: Utf8PathBuf,
	config: SaltConfig,
}

#[derive(Clone, serde::Deserialize)]
pub struct SaltConfig {
	#[serde(rename = "PRIVATE_KEY")]
	pub private_key: String,

	#[serde(rename = "ORCHESTRATION_NETWORK_RPC_NODE_URL")]
	pub orchestration_network_rpc_node: Url,

	#[serde(rename = "BROADCASTING_NETWORK_RPC_NODE_URL")]
	pub broadcasting_network_rpc_node: Url,

	#[serde(rename = "BROADCASTING_NETWORK_ID")]
	pub broadcasting_network_id: String,
}

impl SaltConfig {
	fn iter(self) -> impl IntoIterator<Item = (&'static str, String)> {
		[
			("PRIVATE_KEY", self.private_key),
			(
				"ORCHESTRATION_NETWORK_RPC_NODE_URL",
				self.orchestration_network_rpc_node.to_string(),
			),
			(
				"BROADCASTING_NETWORK_RPC_NODE_URL",
				self.broadcasting_network_rpc_node.to_string(),
			),
			("BROADCASTING_NETWORK_ID", self.broadcasting_network_id),
		]
	}
}

#[derive(thiserror::Error, Debug)]
pub enum Error {
	#[error(
		"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"
	)]
	NoStandardDirectoryFound,

	#[error("{0}")]
	Camino(#[from] FromPathBufError),

	#[error("Executable file doesn't exist")]
	ExecutableFileDoesntExist(Utf8PathBuf),

	#[error("{0}")]
	FailedToExecute(std::io::Error),

	#[error("Subprocess exited badly: {0:?}")]
	SubprocessExited(ExitStatus),

	#[error(
		"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}"
	)]
	Which {
		bin_name: String,
		err_msg: String,
		which: ::which::Error,
	},
}

pub type Result<T> = core::result::Result<T, Error>;

impl Salt {
	fn default_project_path() -> Result<Utf8PathBuf> {
		let dir = dirs::data_local_dir()
			.or(dirs::data_dir())
			.ok_or(Error::NoStandardDirectoryFound)?;
		let path = Utf8PathBuf::try_from(dir)?;
		Ok(path.join("salt-asset-manager"))
	}

	pub fn new(config: SaltConfig) -> Result<Salt> {
		let salt = Salt {
			project_folder: Salt::default_project_path()?,
			config,
		};

		salt.init()?;

		Ok(salt)
	}

	pub fn transaction(
		&self,
		amount: String,
		vault_address: String,
		recipient_address: String,
	) -> Result<()> {
		self.cmd([
			"-amount".into(),
			amount,
			"-vault-address".into(),
			vault_address,
			"-recipient-address".into(),
			recipient_address,
		])?
		.run_and_wait()?;
		Ok(())
	}

	/// git pull && deno install && nu fix.nu
	fn init(&self) -> Result<()> {
		let git = self.git()?;
		git.ensure_latest_branch(
			Url::parse("https://github.com/ActuallyHappening/salt-asset-manager").unwrap(),
			"master",
		)?;

		let deno = Salt::deno()?;
		cli::Command::pure(deno)?
			.with_cwd(self.project_folder.clone())
			.with_args(["install".into()])
			.run_and_wait()?;

		if self.project_folder.join("fix.nu").exists() {
			debug!("Detected fix.nu, running this after deno install");
			// run fix.nu
			let nu = which(
				"nu",
				"required shell to run fix.nu, see https://www.nushell.sh/book/installation.html#package-managers",
			)?;
			cli::Command::pure(nu)?
				.with_cwd(self.project_folder.clone())
				.with_args(["fix.nu".into()])
				.run_and_wait()?;
		}

		info!(
			"Successfully initialized/updated git checkout at {} ready for runtime consumption",
			self.project_folder
		);

		Ok(())
	}

	fn deno() -> Result<Utf8PathBuf> {
		which("deno", "required javascript runtime")
	}

	fn cmd(&self, args: impl IntoIterator<Item = String>) -> Result<Command> {
		let cmd = cli::Command::pure(Salt::deno()?)?.with_cwd(self.project_folder.clone())
			.with_args(
				[
					"run",
					"--unstable-sloppy-imports",
					"-A",
					"src/index.ts",
					"--",
					"-use-cli-only",
				]
				.into_iter()
				.map(String::from),
			)
			.with_args(args)
			.with_envs(self.config.clone().iter());
		Ok(cmd)
	}

	fn git(&self) -> Result<Git> {
		Ok(Git::new(self.project_folder.to_owned())?)
	}
}

mod cli {
	use crate::prelude::*;

	pub struct Command(std::process::Command);

	impl Command {
		pub fn pure(cmd: Utf8PathBuf) -> Result<Command> {
			if !cmd.exists() {
				return Err(Error::ExecutableFileDoesntExist(cmd));
			}
			let mut cmd = std::process::Command::new(cmd);
			cmd.env_clear();
			Ok(Command(cmd))
		}

		pub fn current_dir(&mut self, cwd: Utf8PathBuf) -> &mut Self {
			self.0.current_dir(cwd);
			self
		}

		pub fn with_cwd(mut self, cwd: Utf8PathBuf) -> Self {
			self.current_dir(cwd);
			self
		}

		pub fn with_args(mut self, args: impl IntoIterator<Item = String>) -> Self {
			self.0.args(args);
			self
		}

		pub fn with_envs(mut self, envs: impl IntoIterator<Item = (&'static str, String)>) -> Self {
			self.0.envs(envs);
			self
		}

		pub fn run_and_wait(mut self) -> Result<()> {
			info!("Running command {:?}", self.0);
			let status = self.0.status().map_err(Error::FailedToExecute)?;
			if !status.success() {
				return Err(Error::SubprocessExited(status));
			}
			Ok(())
		}
	}
}

mod git {
	use url::Url;

	use crate::{cli::Command, prelude::*, which::which};

	pub struct Git {
		project_folder: Utf8PathBuf,
	}

	impl Git {
		pub fn new(path: Utf8PathBuf) -> Result<Self> {
			Ok(Self {
				project_folder: path,
			})
		}

		fn cmd(&self) -> Result<Command> {
			let git = which("git", "required runtime dependency")?;
			let cmd = Command::pure(git)?.with_cwd(self.project_folder.to_owned());
			Ok(cmd)
		}

		pub fn ensure_latest_branch(&self, repository_url: Url, branch: &str) -> Result<()> {
			if self.project_folder.exists() {
				// assume already checked out
				self.pull()?;
			} else {
				self.clone(repository_url)?;
			}
			self.checkout(branch)?;
			Ok(())
		}

		fn pull(&self) -> Result<()> {
			debug!("Running `git pull` in directory {}", &self.project_folder);
			self.cmd()?.with_args(["pull".into()]).run_and_wait()
		}

		fn clone(&self, repository_url: Url) -> Result<()> {
			let mut parent_folder = self.project_folder.clone();
			if !parent_folder.pop() {
				panic!("self.project_folder has no parent? Why is the data dir at / ?");
			}
			debug!(
				"Running `git clone {}` in directory {}",
				repository_url, &parent_folder
			);
			self.cmd()?
				.with_cwd(parent_folder)
				.with_args([
					"clone".into(),
					repository_url.to_string(),
					self.project_folder.to_string(),
				])
				.run_and_wait()
		}

		fn checkout(&self, branch: &str) -> Result<()> {
			self.cmd()?
				.with_args(["checkout".into(), branch.into()])
				.run_and_wait()
		}
	}
}

mod which {
	use crate::prelude::*;

	pub fn which(name: &'static str, err_msg: impl Into<String>) -> Result<Utf8PathBuf> {
		let path = ::which::which(name).map_err(|which| Error::Which {
			bin_name: name.to_owned(),
			err_msg: err_msg.into(),
			which,
		})?;
		Ok(Utf8PathBuf::try_from(path)?)
	}
}