diff --git a/crates/cli/src/commands/order/compose.rs b/crates/cli/src/commands/order/compose.rs new file mode 100644 index 000000000..4d013e417 --- /dev/null +++ b/crates/cli/src/commands/order/compose.rs @@ -0,0 +1,54 @@ +use crate::execute::Execute; +use crate::output::{output, SupportedOutputEncoding}; +use anyhow::{anyhow, Result}; +use clap::Args; +use rain_orderbook_common::dotrain_order::DotrainOrder; +use std::fs::read_to_string; +use std::path::PathBuf; + +#[derive(Args, Clone)] +pub struct Compose { + #[arg( + short = 'f', + long, + help = "Path to the .rain file specifying the order" + )] + dotrain_file: PathBuf, + + // path to the settings yaml + #[arg( + short = 'c', + long, + help = "Path to the settings yaml file", + default_value = "settings.yml" + )] + settings_file: Option, + + // the name of the scenrio to use + #[arg(short = 's', long, help = "The name of the scenario to use")] + scenario: String, + + // supported encoding + #[arg(short = 'o', long, help = "Output encoding", default_value = "binary")] + encoding: SupportedOutputEncoding, +} + +impl Execute for Compose { + async fn execute(&self) -> Result<()> { + let dotrain = read_to_string(self.dotrain_file.clone()).map_err(|e| anyhow!(e))?; + let settings = match &self.settings_file { + Some(settings_file) => { + Some(read_to_string(settings_file.clone()).map_err(|e| anyhow!(e))?) + } + None => None, + }; + let rainlang = DotrainOrder::new(dotrain, settings) + .await? + .compose_scenario_to_rainlang(self.scenario.clone()) + .await?; + + output(&None, self.encoding.clone(), rainlang.as_bytes())?; + + Ok(()) + } +} diff --git a/crates/cli/src/commands/order/mod.rs b/crates/cli/src/commands/order/mod.rs index 7c6730118..3bf13fb83 100644 --- a/crates/cli/src/commands/order/mod.rs +++ b/crates/cli/src/commands/order/mod.rs @@ -1,4 +1,5 @@ mod add; +mod compose; mod detail; mod list; mod remove; @@ -7,6 +8,7 @@ use crate::execute::Execute; use add::CliOrderAddArgs; use anyhow::Result; use clap::Parser; +use compose::Compose; use detail::CliOrderDetailArgs; use list::CliOrderListArgs; use remove::CliOrderRemoveArgs; @@ -24,6 +26,9 @@ pub enum Order { #[command(about = "Remove an Order", alias = "rm")] Remove(CliOrderRemoveArgs), + + #[command(about = "Compose a .rain order file to Rainlang", alias = "comp")] + Compose(Compose), } impl Execute for Order { @@ -33,6 +38,7 @@ impl Execute for Order { Order::Detail(detail) => detail.execute().await, Order::Create(create) => create.execute().await, Order::Remove(remove) => remove.execute().await, + Order::Compose(compose) => compose.execute().await, } } } diff --git a/crates/cli/src/lib.rs b/crates/cli/src/lib.rs index b97809434..67be6a7d2 100644 --- a/crates/cli/src/lib.rs +++ b/crates/cli/src/lib.rs @@ -5,6 +5,7 @@ use clap::Subcommand; mod commands; mod execute; +mod output; mod status; mod subgraph; mod transaction; diff --git a/crates/cli/src/output.rs b/crates/cli/src/output.rs new file mode 100644 index 000000000..79d954d34 --- /dev/null +++ b/crates/cli/src/output.rs @@ -0,0 +1,29 @@ +use std::io::Write; +use std::path::PathBuf; + +#[derive(clap::ValueEnum, Clone)] +pub enum SupportedOutputEncoding { + Binary, + Hex, +} + +pub fn output( + output_path: &Option, + output_encoding: SupportedOutputEncoding, + bytes: &[u8], +) -> anyhow::Result<()> { + let hex_encoded: String; + let encoded_bytes: &[u8] = match output_encoding { + SupportedOutputEncoding::Binary => bytes, + SupportedOutputEncoding::Hex => { + hex_encoded = alloy_primitives::hex::encode_prefixed(bytes); + hex_encoded.as_bytes() + } + }; + if let Some(output_path) = output_path { + std::fs::write(output_path, encoded_bytes)? + } else { + std::io::stdout().write_all(encoded_bytes)? + } + Ok(()) +} diff --git a/crates/common/src/dotrain_order.rs b/crates/common/src/dotrain_order.rs new file mode 100644 index 000000000..7d4e58dd0 --- /dev/null +++ b/crates/common/src/dotrain_order.rs @@ -0,0 +1,182 @@ +use dotrain::{error::ComposeError, RainDocument}; +use rain_orderbook_app_settings::{ + config_source::{ConfigSource, ConfigSourceError}, + merge::MergeError, + Config, ParseConfigSourceError, +}; +use thiserror::Error; + +use crate::rainlang::compose_to_rainlang; + +pub struct DotrainOrder { + pub config: Config, + pub dotrain: String, +} + +#[derive(Error, Debug)] +pub enum DotrainOrderError { + #[error(transparent)] + ConfigSourceError(#[from] ConfigSourceError), + + #[error(transparent)] + ParseConfigSourceError(#[from] ParseConfigSourceError), + + #[error("Scenario {0} not found")] + ScenarioNotFound(String), + + #[error(transparent)] + ComposeError(#[from] ComposeError), + + #[error(transparent)] + MergeConfigError(#[from] MergeError), +} + +impl DotrainOrder { + pub async fn new(dotrain: String, config: Option) -> Result { + match config { + Some(config) => { + let config_string = ConfigSource::try_from_string(config).await?; + let frontmatter = RainDocument::get_front_matter(&dotrain).unwrap(); + let mut frontmatter_config = + ConfigSource::try_from_string(frontmatter.to_string()).await?; + frontmatter_config.merge(config_string)?; + Ok(Self { + dotrain, + config: frontmatter_config.try_into()?, + }) + } + None => { + let frontmatter = RainDocument::get_front_matter(&dotrain).unwrap(); + let config_string = ConfigSource::try_from_string(frontmatter.to_string()).await?; + let config: Config = config_string.try_into()?; + Ok(Self { dotrain, config }) + } + } + } + + pub async fn compose_scenario_to_rainlang( + &self, + scenario: String, + ) -> Result { + let scenario = self + .config + .scenarios + .get(&scenario) + .ok_or_else(|| DotrainOrderError::ScenarioNotFound(scenario))?; + + Ok(compose_to_rainlang( + self.dotrain.clone(), + scenario.bindings.clone(), + )?) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_config_parse() { + let dotrain = r#" +networks: + polygon: + rpc: https://rpc.ankr.com/polygon + chain-id: 137 + network-id: 137 + currency: MATIC +deployers: + polygon: + address: 0x1234567890123456789012345678901234567890 +scenarios: + polygon: +--- +#calculate-io +_ _: 0 0; +#handle-io +:;"#; + + let dotrain_order = DotrainOrder::new(dotrain.to_string(), None).await.unwrap(); + + assert_eq!( + dotrain_order.config.networks.get("polygon").unwrap().rpc, + "https://rpc.ankr.com/polygon".parse().unwrap() + ); + } + + #[tokio::test] + async fn test_rainlang_from_scenario() { + let dotrain = r#" +networks: + polygon: + rpc: https://rpc.ankr.com/polygon + chain-id: 137 + network-id: 137 + currency: MATIC +deployers: + polygon: + address: 0x1234567890123456789012345678901234567890 +scenarios: + polygon: +--- +#calculate-io +_ _: 0 0; +#handle-io +:;"#; + + let dotrain_order = DotrainOrder::new(dotrain.to_string(), None).await.unwrap(); + + let rainlang = dotrain_order + .compose_scenario_to_rainlang("polygon".to_string()) + .await + .unwrap(); + + assert_eq!( + rainlang, + r#"/* 0. calculate-io */ +_ _: 0 0; + +/* 1. handle-io */ +:;"# + ); + } + + #[tokio::test] + async fn test_config_merge() { + let dotrain = r#" +networks: + polygon: + rpc: https://rpc.ankr.com/polygon + chain-id: 137 + network-id: 137 + currency: MATIC +--- +#calculate-io +_ _: 00; + +#handle-io +:;"#; + + let settings = r#" +networks: + mainnet: + rpc: https://1rpc.io/eth + chain-id: 1 + network-id: 1 + currency: ETH"#; + + let merged_dotrain_order = + DotrainOrder::new(dotrain.to_string(), Some(settings.to_string())) + .await + .unwrap(); + + assert_eq!( + merged_dotrain_order + .config + .networks + .get("mainnet") + .unwrap() + .rpc, + "https://1rpc.io/eth".parse().unwrap() + ); + } +} diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs index 3a57a3d7e..4dcc0f2ea 100644 --- a/crates/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -2,6 +2,7 @@ pub mod add_order; pub mod csv; pub mod deposit; pub mod dotrain_add_order_lsp; +pub mod dotrain_order; pub mod frontmatter; pub mod fuzz; pub mod meta;