diff --git a/Cargo.lock b/Cargo.lock index dd27464da..ce9ea6cf3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6306,6 +6306,7 @@ dependencies = [ "chrono", "clap", "comfy-table", + "httpmock", "rain_orderbook_app_settings", "rain_orderbook_bindings", "rain_orderbook_common", diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index b3c5efb89..77ee8e665 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -22,7 +22,6 @@ reqwest = { workspace = true } rust-bigint = { workspace = true } serde = { workspace = true } serde_bytes = { workspace = true } -# tokio = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true, features = ['env-filter'] } comfy-table = { workspace = true } @@ -32,4 +31,7 @@ chrono = { workspace = true } tokio = { workspace = true, features = ["full"] } [target.'cfg(target_family = "wasm")'.dependencies] -tokio = { workspace = true, features = ["sync", "macros", "io-util", "rt", "time"] } \ No newline at end of file +tokio = { workspace = true, features = ["sync", "macros", "io-util", "rt", "time"] } + +[dev-dependencies] +httpmock = "0.7.0" diff --git a/crates/cli/src/commands/order/calldata.rs b/crates/cli/src/commands/order/calldata.rs new file mode 100644 index 000000000..e7fcd1e79 --- /dev/null +++ b/crates/cli/src/commands/order/calldata.rs @@ -0,0 +1,250 @@ +use crate::execute::Execute; +use crate::output::{output, SupportedOutputEncoding}; +use alloy::sol_types::SolCall; +use anyhow::{anyhow, Result}; +use clap::Parser; +use rain_orderbook_common::add_order::AddOrderArgs; +use rain_orderbook_common::dotrain_order::DotrainOrder; +use std::fs::read_to_string; +use std::ops::Deref; +use std::path::PathBuf; + +#[derive(Parser, Clone)] +pub struct AddOrderCalldata { + #[arg( + short = 'f', + long, + help = "Path to the .rain file specifying the order" + )] + dotrain_file: PathBuf, + + #[arg(short = 'c', long, help = "Path to the settings yaml file")] + settings_file: Option, + + #[arg(short = 'e', long, help = "Deployment key to select from frontmatter")] + deployment: String, + + #[arg(short = 'o', long, help = "Output encoding", default_value = "binary")] + encoding: SupportedOutputEncoding, +} + +impl Execute for AddOrderCalldata { + 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 order = DotrainOrder::new(dotrain, settings).await?; + let dotrain_string = order.dotrain.clone(); + + let config_deployment = order + .config + .deployments + .get(&self.deployment) + .ok_or(anyhow!("specified deployment is undefined!"))?; + + let add_order_args = + AddOrderArgs::new_from_deployment(dotrain_string, config_deployment.deref().clone()) + .await; + + let add_order_calldata = add_order_args? + .try_into_call(config_deployment.scenario.deployer.network.rpc.to_string()) + .await? + .abi_encode(); + + output(&None, self.encoding.clone(), &add_order_calldata)?; + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloy::primitives::{hex::encode_prefixed, Address, Bytes, B256}; + use alloy::sol_types::SolValue; + use alloy_ethers_typecast::rpc::Response; + use clap::CommandFactory; + use httpmock::MockServer; + use std::str::FromStr; + + #[test] + fn verify_cli() { + AddOrderCalldata::command().debug_assert(); + } + + #[test] + fn test_cli_args() { + let dotrain_file = PathBuf::from_str("./some/dotrain_file.rain").unwrap(); + let settings_file = PathBuf::from_str("./some/settings_file.rain").unwrap(); + let deployment_str = "some-deployment"; + let output_str = "hex"; + + let cmd = AddOrderCalldata::command(); + let result = cmd + .try_get_matches_from(vec![ + "cmd", + "-f", + dotrain_file.to_str().unwrap(), + "-c", + settings_file.to_str().unwrap(), + "-e", + deployment_str, + "-o", + output_str, + ]) + .unwrap(); + assert_eq!( + result.get_one::("dotrain_file"), + Some(&dotrain_file) + ); + assert_eq!( + result.get_one::("settings_file"), + Some(&settings_file) + ); + assert_eq!( + result.get_one::("deployment"), + Some(&deployment_str.to_string()) + ); + assert_eq!( + result.get_one::("encoding"), + Some(&SupportedOutputEncoding::Hex) + ); + } + + #[tokio::test] + async fn test_execute() { + let rpc_server = MockServer::start_async().await; + let dotrain = format!( + " +networks: + some-network: + rpc: {} + chain-id: 123 + network-id: 123 + currency: ETH + +subgraphs: + some-sg: https://www.some-sg.com + +deployers: + some-deployer: + network: some-network + address: 0xF14E09601A47552De6aBd3A0B165607FaFd2B5Ba + +orderbooks: + some-orderbook: + address: 0xc95A5f8eFe14d7a20BD2E5BAFEC4E71f8Ce0B9A6 + network: some-network + subgraph: some-sg + +tokens: + token1: + network: some-network + address: 0xc2132d05d31c914a87c6611c10748aeb04b58e8f + decimals: 6 + label: T1 + symbol: T1 + token2: + network: some-network + address: 0x8f3cf7ad23cd3cadbd9735aff958023239c6a063 + decimals: 18 + label: T2 + symbol: T2 + +scenarios: + some-scenario: + network: some-network + deployer: some-deployer + +orders: + some-order: + inputs: + - token: token1 + vault-id: 1 + outputs: + - token: token2 + vault-id: 1 + deployer: some-deployer + orderbook: some-orderbook + +deployments: + some-deployment: + scenario: some-scenario + order: some-order +--- +#calculate-io +_ _: 0 0; +#handle-io +:; +#post-add-order +:;", + rpc_server.url("/rpc").as_str() + ); + + let dotrain_path = "./test_dotrain1.rain"; + std::fs::write(dotrain_path, dotrain).unwrap(); + + // mock rpc response data + // mock iInterpreter() call + rpc_server.mock(|when, then| { + when.path("/rpc").body_contains("0xf0cfdd37"); + then.body( + Response::new_success( + 1, + &B256::left_padding_from(Address::random().as_slice()).to_string(), + ) + .to_json_string() + .unwrap(), + ); + }); + // mock iStore() call + rpc_server.mock(|when, then| { + when.path("/rpc").body_contains("0xc19423bc"); + then.body( + Response::new_success( + 2, + &B256::left_padding_from(Address::random().as_slice()).to_string(), + ) + .to_json_string() + .unwrap(), + ); + }); + // mock iParser() call + rpc_server.mock(|when, then| { + when.path("/rpc").body_contains("0x24376855"); + then.body( + Response::new_success( + 3, + &B256::left_padding_from(Address::random().as_slice()).to_string(), + ) + .to_json_string() + .unwrap(), + ); + }); + // mock parse2() call + rpc_server.mock(|when, then| { + when.path("/rpc").body_contains("0xa3869e14"); + then.body( + Response::new_success(4, &encode_prefixed(Bytes::from(vec![1, 2]).abi_encode())) + .to_json_string() + .unwrap(), + ); + }); + + let add_order_calldata = AddOrderCalldata { + dotrain_file: dotrain_path.into(), + settings_file: None, + deployment: "some-deployment".to_string(), + encoding: SupportedOutputEncoding::Hex, + }; + // should succeed without err + add_order_calldata.execute().await.unwrap(); + + // remove test file + std::fs::remove_file(dotrain_path).unwrap(); + } +} diff --git a/crates/cli/src/commands/order/mod.rs b/crates/cli/src/commands/order/mod.rs index 3bf13fb83..d644a2eed 100644 --- a/crates/cli/src/commands/order/mod.rs +++ b/crates/cli/src/commands/order/mod.rs @@ -1,14 +1,19 @@ mod add; +mod calldata; mod compose; mod detail; mod list; +mod orderbook_address; mod remove; +use crate::commands::order::orderbook_address::OrderbookAddress; use crate::execute::Execute; use add::CliOrderAddArgs; use anyhow::Result; +use calldata::AddOrderCalldata; use clap::Parser; use compose::Compose; + use detail::CliOrderDetailArgs; use list::CliOrderListArgs; use remove::CliOrderRemoveArgs; @@ -29,6 +34,18 @@ pub enum Order { #[command(about = "Compose a .rain order file to Rainlang", alias = "comp")] Compose(Compose), + + #[command( + about = "Generate calldata for addOrder from a composition", + alias = "call" + )] + Calldata(AddOrderCalldata), + + #[command( + about = "Get the orderbook address for a given order", + alias = "ob-addr" + )] + OrderbookAddress(OrderbookAddress), } impl Execute for Order { @@ -39,6 +56,8 @@ impl Execute for Order { Order::Create(create) => create.execute().await, Order::Remove(remove) => remove.execute().await, Order::Compose(compose) => compose.execute().await, + Order::Calldata(calldata) => calldata.execute().await, + Order::OrderbookAddress(orderbook_address) => orderbook_address.execute().await, } } } diff --git a/crates/cli/src/commands/order/orderbook_address.rs b/crates/cli/src/commands/order/orderbook_address.rs new file mode 100644 index 000000000..d1299e428 --- /dev/null +++ b/crates/cli/src/commands/order/orderbook_address.rs @@ -0,0 +1,218 @@ +use crate::execute::Execute; +use crate::output::{output, SupportedOutputEncoding}; +use anyhow::{anyhow, Result}; +use clap::Parser; +use rain_orderbook_app_settings::Config; +use rain_orderbook_common::dotrain_order::DotrainOrder; +use std::fs::read_to_string; +use std::path::PathBuf; + +#[derive(Parser, Clone)] +pub struct OrderbookAddress { + #[arg( + short = 'f', + long, + help = "Path to the .rain file specifying the order" + )] + dotrain_file: PathBuf, + + #[arg(short = 'c', long, help = "Path to the settings yaml file")] + settings_file: Option, + + #[arg(short = 'e', long, help = "Deployment key to select from frontmatter")] + deployment: String, + + #[arg(short = 'o', long, help = "Output encoding", default_value = "binary")] + encoding: SupportedOutputEncoding, +} + +impl Execute for OrderbookAddress { + 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 order = DotrainOrder::new(dotrain, settings).await?; + let order_config: Config = order.clone().config; + let deployment_ref = order_config + .deployments + .get(&self.deployment) + .ok_or(anyhow!("specified deployment is undefined!"))?; + + let orderbook_address = if let Some(v) = &deployment_ref.order.orderbook { + v.address + } else { + let network_name = &deployment_ref.scenario.deployer.network.name; + order_config + .orderbooks + .iter() + .find(|(k, v)| *k == network_name || v.network.name == *network_name) + .ok_or(anyhow!("specified orderbook is undefined!"))? + .1 + .address + }; + output(&None, self.encoding.clone(), orderbook_address.as_slice())?; + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use clap::CommandFactory; + use std::str::FromStr; + + #[test] + fn verify_cli() { + OrderbookAddress::command().debug_assert(); + } + + #[test] + fn test_cli_args() { + let dotrain_file = PathBuf::from_str("./some/dotrain_file.rain").unwrap(); + let settings_file = PathBuf::from_str("./some/settings_file.rain").unwrap(); + let deployment_str = "some-deployment"; + let output_str = "hex"; + + let cmd = OrderbookAddress::command(); + let result = cmd.get_matches_from(vec![ + "cmd", + "-f", + dotrain_file.to_str().unwrap(), + "-c", + settings_file.to_str().unwrap(), + "-e", + deployment_str, + "-o", + output_str, + ]); + assert_eq!( + result.get_one::("dotrain_file"), + Some(&dotrain_file) + ); + assert_eq!( + result.get_one::("settings_file"), + Some(&settings_file) + ); + assert_eq!( + result.get_one::("deployment"), + Some(&deployment_str.to_string()) + ); + assert_eq!( + result.get_one::("encoding"), + Some(&SupportedOutputEncoding::Hex) + ); + } + + fn get_test_dotrain(orderbook_key: &str) -> String { + format!( + " +networks: + some-network: + rpc: https://some-url.com + chain-id: 123 + network-id: 123 + currency: ETH + +subgraphs: + some-sg: https://www.some-sg.com + +deployers: + some-deployer: + network: some-network + address: 0xF14E09601A47552De6aBd3A0B165607FaFd2B5Ba + +orderbooks: + {}: + address: 0xc95A5f8eFe14d7a20BD2E5BAFEC4E71f8Ce0B9A6 + network: some-network + subgraph: some-sg + +tokens: + token1: + network: some-network + address: 0xc2132d05d31c914a87c6611c10748aeb04b58e8f + decimals: 6 + label: T1 + symbol: T1 + token2: + network: some-network + address: 0x8f3cf7ad23cd3cadbd9735aff958023239c6a063 + decimals: 18 + label: T2 + symbol: T2 + +scenarios: + some-scenario: + network: some-network + deployer: some-deployer + +orders: + some-order: + inputs: + - token: token1 + vault-id: 1 + outputs: + - token: token2 + vault-id: 1 + deployer: some-deployer + +deployments: + some-deployment: + scenario: some-scenario + order: some-order +--- +#calculate-io +_ _: 0 0; +#handle-io +:; +#post-add-order +:;", + orderbook_key + ) + } + + #[tokio::test] + async fn test_execute_diff_name() { + let dotrain = get_test_dotrain("some-orderbook"); + + let dotrain_path = "./test_dotrain2.rain"; + std::fs::write(dotrain_path, dotrain).unwrap(); + + let orderbook_adress = OrderbookAddress { + dotrain_file: dotrain_path.into(), + settings_file: None, + deployment: "some-deployment".to_string(), + encoding: SupportedOutputEncoding::Hex, + }; + // should succeed without err + orderbook_adress.execute().await.unwrap(); + + // remove test file + std::fs::remove_file(dotrain_path).unwrap(); + } + + #[tokio::test] + async fn test_execute_same_name() { + let dotrain = get_test_dotrain("some-network"); + + let dotrain_path = "./test_dotrain3.rain"; + std::fs::write(dotrain_path, dotrain).unwrap(); + + let orderbook_adress = OrderbookAddress { + dotrain_file: dotrain_path.into(), + settings_file: None, + deployment: "some-deployment".to_string(), + encoding: SupportedOutputEncoding::Hex, + }; + // should succeed without err + orderbook_adress.execute().await.unwrap(); + + // remove test file + std::fs::remove_file(dotrain_path).unwrap(); + } +} diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 36941b1f7..182a66354 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -31,3 +31,14 @@ async fn main() -> Result<()> { let cli = Cli::parse(); cli.orderbook.execute().await } + +#[cfg(test)] +mod tests { + use super::*; + use clap::CommandFactory; + + #[test] + fn verify_cli() { + Cli::command().debug_assert(); + } +} diff --git a/crates/cli/src/output.rs b/crates/cli/src/output.rs index 137fe54a9..adcefa1da 100644 --- a/crates/cli/src/output.rs +++ b/crates/cli/src/output.rs @@ -1,7 +1,7 @@ use std::io::Write; use std::path::PathBuf; -#[derive(clap::ValueEnum, Clone)] +#[derive(Debug, clap::ValueEnum, Clone, PartialEq)] pub enum SupportedOutputEncoding { Binary, Hex,