diff --git a/crates/quote/src/cli/input.rs b/crates/quote/src/cli/input.rs index 197751049..23c22c98c 100644 --- a/crates/quote/src/cli/input.rs +++ b/crates/quote/src/cli/input.rs @@ -37,6 +37,55 @@ pub struct Input { ], )] pub target: Option>, + + /// A Quote Spec input that takes exactly 4 values + #[arg( + long, + num_args = 4, + value_names = [ + "ORDERBOOK_ADDRESS", + "INPUT_IO_INDEX", + "OUTPUT_IO_INDEX", + "ORDER_HASH" + ], + )] + pub spec: Option>, +} + +/// Determines the variants of parsed json input +#[derive(Debug, Clone, PartialEq)] +pub enum InputContentType { + /// quote specs that need to read their order details from a subgraph before a quote call + Spec(BatchQuoteSpec), + // ready to quote targets that have all the details for a quote call + Target(BatchQuoteTarget), +} + +impl Input { + /// Reads the input content from the provided source + pub fn read_content(&self) -> anyhow::Result { + let mut inputs_count = 0; + if self.input.is_some() { + inputs_count += 1; + } + if self.target.is_some() { + inputs_count += 1; + } + if self.spec.is_some() { + inputs_count += 1; + } + if inputs_count > 1 { + Err(anyhow::anyhow!("conflicting inputs")) + } else if let Some(v) = &self.input { + Ok(InputContentType::Spec(v.clone())) + } else if let Some(targets) = &self.target { + Ok(InputContentType::Target(targets.try_into()?)) + } else if let Some(specs) = &self.spec { + Ok(InputContentType::Spec(specs.try_into()?)) + } else { + Err(anyhow::anyhow!("expected at least one input")) + } + } } /// Parse and validates the input hex string bytes into [BatchQuoteSpec] @@ -49,33 +98,24 @@ pub fn parse_input(value: &str) -> anyhow::Result { let mut start_index = 0; let mut end_index = 54; while let Some(bytes_piece) = bytes.get(start_index..end_index) { - let orderbook = bytes_piece - .get(..20) - .map(Address::from_slice) - .ok_or(anyhow::anyhow!("missing orderbook address"))?; - let input_io_index = bytes_piece - .get(20..21) - .map(|v| v[0]) - .ok_or(anyhow::anyhow!("missing input IO index"))?; - let output_io_index = bytes_piece - .get(21..22) - .map(|v| v[0]) - .ok_or(anyhow::anyhow!("missing output IO index"))?; - let order_hash = bytes_piece - .get(22..) - .map(|v| { - let mut bytes32: [u8; 32] = [0; 32]; - bytes32.copy_from_slice(v); - U256::from_be_bytes(bytes32) - }) - .ok_or(anyhow::anyhow!("missing order hash"))?; - batch_quote_sepcs.0.push(QuoteSpec { - order_hash, - input_io_index, - output_io_index, signed_context: vec![], - orderbook, + orderbook: bytes_piece + .get(..20) + .map(Address::from_slice) + .ok_or(anyhow::anyhow!("missing orderbook address"))?, + input_io_index: bytes_piece + .get(20..21) + .map(|v| v[0]) + .ok_or(anyhow::anyhow!("missing input IO index"))?, + output_io_index: bytes_piece + .get(21..22) + .map(|v| v[0]) + .ok_or(anyhow::anyhow!("missing output IO index"))?, + order_hash: bytes_piece + .get(22..) + .map(U256::from_be_slice) + .ok_or(anyhow::anyhow!("missing order hash"))?, }); start_index += 54; end_index += 54; @@ -83,24 +123,6 @@ pub fn parse_input(value: &str) -> anyhow::Result { Ok(batch_quote_sepcs) } -// a binding struct for Quote -struct CliQuoteTarget<'a> { - pub order: &'a str, - pub input_io_index: &'a str, - pub output_io_index: &'a str, -} -impl<'a> TryFrom> for Quote { - type Error = anyhow::Error; - fn try_from(value: CliQuoteTarget<'a>) -> Result { - Ok(Self { - inputIOIndex: U256::from_str(value.input_io_index)?, - outputIOIndex: U256::from_str(value.output_io_index)?, - signedContext: vec![], - order: OrderV3::abi_decode(&decode(value.order)?, true)?, - }) - } -} - // tries to map an array of strings into a BatchQuoteTarget impl TryFrom<&Vec> for BatchQuoteTarget { type Error = anyhow::Error; @@ -111,14 +133,14 @@ impl TryFrom<&Vec> for BatchQuoteTarget { if let Some(input_io_index_str) = iter.next() { if let Some(output_io_index_str) = iter.next() { if let Some(order_bytes_str) = iter.next() { - let cli_quote_target = CliQuoteTarget { - order: order_bytes_str, - input_io_index: input_io_index_str, - output_io_index: output_io_index_str, - }; batch_quote_target.0.push(QuoteTarget { orderbook: Address::from_hex(orderbook_str)?, - quote_config: cli_quote_target.try_into()?, + quote_config: Quote { + signedContext: vec![], + inputIOIndex: U256::from_str(input_io_index_str)?, + outputIOIndex: U256::from_str(output_io_index_str)?, + order: OrderV3::abi_decode(&decode(order_bytes_str)?, true)?, + }, }); } else { return Err(anyhow::anyhow!("missing order bytes")); @@ -137,28 +159,37 @@ impl TryFrom<&Vec> for BatchQuoteTarget { } } -/// Determines the variants of parsed json input -#[derive(Debug, Clone, PartialEq)] -pub enum InputContentType { - /// quote specs that need to read their order details from a subgraph before a quote call - Spec(BatchQuoteSpec), - // ready to quote targets that have all the details for a quote call - Target(BatchQuoteTarget), -} - -impl Input { - /// Reads the input content from the provided source - pub fn read_content(&self) -> anyhow::Result { - if self.input.is_some() && self.target.is_some() { - Err(anyhow::anyhow!("conflicting inputs")) - } else if let Some(v) = &self.input { - Ok(InputContentType::Spec(v.clone())) - } else if let Some(targets) = &self.target { - let batch_quote_target = targets.try_into()?; - Ok(InputContentType::Target(batch_quote_target)) - } else { - Err(anyhow::anyhow!("expected at least one input")) +// tries to map an array of strings into a BatchQuoteSpec +impl TryFrom<&Vec> for BatchQuoteSpec { + type Error = anyhow::Error; + fn try_from(value: &Vec) -> Result { + let mut batch_quote_specs = BatchQuoteSpec::default(); + let mut iter = value.iter(); + while let Some(orderbook_str) = iter.next() { + if let Some(input_io_index_str) = iter.next() { + if let Some(output_io_index_str) = iter.next() { + if let Some(order_hash_str) = iter.next() { + batch_quote_specs.0.push(QuoteSpec { + signed_context: vec![], + orderbook: Address::from_hex(orderbook_str)?, + order_hash: U256::from_str(order_hash_str)?, + input_io_index: input_io_index_str.parse()?, + output_io_index: output_io_index_str.parse()?, + }); + } else { + return Err(anyhow::anyhow!("missing order hash")); + } + } else { + return Err(anyhow::anyhow!("missing output IO index")); + } + } else { + return Err(anyhow::anyhow!("missing input IO index")); + } + } + if batch_quote_specs.0.is_empty() { + return Err(anyhow::anyhow!("missing '--spec' values")); } + Ok(batch_quote_specs) } } @@ -301,6 +332,68 @@ mod tests { assert_eq!(result, "missing output IO index"); } + #[test] + fn test_try_from_vec_string_for_batch_quote_spec() { + // valid targets + let input_index = 8u8; + let output_index = 9u8; + let orderbook1 = Address::random(); + let orderbook2 = Address::random(); + let order_hash1 = [1u8; 32]; + let order_hash2 = [2u8; 32]; + + let specs_str = vec![ + encode_prefixed(orderbook1.0), + input_index.to_string(), + output_index.to_string(), + encode_prefixed(order_hash1), + encode_prefixed(orderbook2.0), + input_index.to_string(), + output_index.to_string(), + encode_prefixed(order_hash2), + ]; + + let result: BatchQuoteSpec = (&specs_str).try_into().unwrap(); + let expected = BatchQuoteSpec(vec![ + QuoteSpec { + orderbook: orderbook1, + input_io_index: input_index, + output_io_index: output_index, + signed_context: vec![], + order_hash: U256::from_be_bytes(order_hash1), + }, + QuoteSpec { + orderbook: orderbook2, + input_io_index: input_index, + output_io_index: output_index, + signed_context: vec![], + order_hash: U256::from_be_bytes(order_hash2), + }, + ]); + assert_eq!(result, expected); + + // invalid targets + let specs_str = vec![ + encode_prefixed(orderbook1.0), + input_index.to_string(), + output_index.to_string(), + encode_prefixed([1u8; 32]), + encode_prefixed(orderbook2.0), + input_index.to_string(), + output_index.to_string(), + ]; + let result = std::convert::TryInto::::try_into(&specs_str) + .expect_err("expected error") + .to_string(); + assert_eq!(result, "missing order hash"); + + let specs_str = vec![encode_prefixed(orderbook1.0), input_index.to_string()]; + let result = std::convert::TryInto::::try_into(&specs_str) + .expect_err("expected error") + .to_string(); + assert_eq!(result, "missing output IO index"); + } + #[test] fn test_read_content() { let orderbook = Address::random(); @@ -318,6 +411,7 @@ mod tests { let input = Input { input: Some(specs.clone()), target: None, + spec: None, }; matches!(input.read_content().unwrap(), InputContentType::Spec(_)); @@ -330,12 +424,27 @@ mod tests { let input = Input { input: None, target: Some(targets_str.clone()), + spec: None, }; matches!(input.read_content().unwrap(), InputContentType::Target(_)); + let specs_str = vec![ + encode_prefixed(orderbook.0), + input_io_index.to_string(), + output_io_index.to_string(), + encode_prefixed([1u8; 32]), + ]; + let input = Input { + input: None, + spec: Some(specs_str.clone()), + target: None, + }; + matches!(input.read_content().unwrap(), InputContentType::Spec(_)); + let input = Input { input: None, target: None, + spec: None, }; assert_eq!( input @@ -348,6 +457,7 @@ mod tests { let input = Input { input: Some(specs), target: Some(targets_str), + spec: None, }; assert_eq!( input diff --git a/crates/quote/src/cli/mod.rs b/crates/quote/src/cli/mod.rs index 893fa27f2..f62fa8d2c 100644 --- a/crates/quote/src/cli/mod.rs +++ b/crates/quote/src/cli/mod.rs @@ -262,6 +262,7 @@ mod tests { pretty: true, input: Input { target: None, + spec: None, input: Some(BatchQuoteSpec(vec![ QuoteSpec::default(), QuoteSpec::default(), @@ -276,7 +277,7 @@ mod tests { } #[tokio::test] - async fn test_run_ok_input_bytes() { + async fn test_run_ok_spec_inputs() { let rpc_server = MockServer::start_async().await; let rpc_url = rpc_server.url("/rpc"); let sg_url = rpc_server.url("/sg"); @@ -312,14 +313,18 @@ mod tests { ..Default::default() }; let order_hash_bytes = keccak256(order.abi_encode()).0; - let order_id_u256 = U256::from_be_bytes(order_hash_bytes); - let order_id = encode_prefixed(order_hash_bytes); + let order_hash_u256 = U256::from_be_bytes(order_hash_bytes); + let order_hash = encode_prefixed(order_hash_bytes); + let mut order_id = vec![]; + order_id.extend_from_slice(orderbook.as_ref()); + order_id.extend_from_slice(&order_hash_bytes); + let order_id = encode_prefixed(keccak256(order_id)); let retrun_sg_data = serde_json::json!({ "data": { "orders": [{ "id": order_id, "orderBytes": encode_prefixed(order.abi_encode()), - "orderHash": order_id, + "orderHash": order_hash, "owner": encode_prefixed(order.owner), "outputs": [{ "id": encode_prefixed(Address::random().0.0), @@ -362,9 +367,10 @@ mod tests { then.json_body_obj(&retrun_sg_data); }); + // input bytes let batch_quote_specs = BatchQuoteSpec(vec![ QuoteSpec { - order_hash: order_id_u256, + order_hash: order_hash_u256, input_io_index: 0, output_io_index: 0, signed_context: vec![], @@ -382,6 +388,7 @@ mod tests { pretty: false, input: Input { target: None, + spec: None, input: Some(batch_quote_specs), }, }; @@ -393,6 +400,40 @@ mod tests { QuoterResultInner::Error(FailedQuote::NonExistent.to_string()), ]); assert_eq!(result, expected); + + // specs input + let specs_str = vec![ + encode_prefixed(orderbook.0), + 0.to_string(), + 0.to_string(), + encode_prefixed(order_hash_bytes), + encode_prefixed(orderbook.0), + 0.to_string(), + 0.to_string(), + encode_prefixed([0u8; 32]), + ]; + let cli = Quoter { + output: None, + rpc: Url::parse(&rpc_url).unwrap(), + subgraph: Some(Url::parse(&sg_url).unwrap()), + block_number: None, + multicall_address: None, + no_stdout: true, + pretty: false, + input: Input { + target: None, + input: None, + spec: Some(specs_str), + }, + }; + + // run + let result = cli.run().await.unwrap(); + let expected = QuoterResult(vec![ + QuoterResultInner::Ok(OrderQuoteValue::default()), + QuoterResultInner::Error(FailedQuote::NonExistent.to_string()), + ]); + assert_eq!(result, expected); } #[tokio::test] @@ -421,6 +462,7 @@ mod tests { pretty: false, input: Input { input: None, + spec: None, target: Some(targets_str), }, }; diff --git a/crates/quote/src/quote.rs b/crates/quote/src/quote.rs index 52704e387..f7148bd66 100644 --- a/crates/quote/src/quote.rs +++ b/crates/quote/src/quote.rs @@ -4,12 +4,13 @@ use crate::{ }; use alloy_primitives::{ hex::{decode, encode_prefixed}, - Address, U256, + keccak256, Address, B256, U256, }; use alloy_sol_types::SolValue; use rain_orderbook_bindings::IOrderBookV4::{quoteReturn, OrderV3, Quote, SignedContextV1}; use rain_orderbook_subgraph_client::{ types::{order_detail::Bytes, Id}, + utils::make_order_id, OrderbookSubgraphClient, }; use serde::{Deserialize, Serialize}; @@ -44,6 +45,17 @@ pub struct QuoteTarget { } impl QuoteTarget { + /// Get the order hash of self + pub fn get_order_hash(&self) -> B256 { + keccak256(self.quote_config.order.abi_encode()) + } + + /// Get subgraph represented "order_id" of self + /// which is keccak256 of orderbook address concated with order hash + pub fn get_id(&self) -> B256 { + make_order_id(self.orderbook, self.get_order_hash().into()) + } + /// Quotes the target on the given rpc url pub async fn do_quote( &self, @@ -92,6 +104,12 @@ pub struct QuoteSpec { } impl QuoteSpec { + /// Get subgraph represented "order_id" of self + /// which is keccak256 of orderbook address concated with order hash + pub fn get_id(&self) -> B256 { + make_order_id(self.orderbook, self.order_hash) + } + /// Given a subgraph will fetch the order details and returns the /// respective quote target pub async fn get_quote_target_from_subgraph( @@ -101,7 +119,7 @@ impl QuoteSpec { let url = Url::from_str(subgraph_url)?; let sg_client = OrderbookSubgraphClient::new(url); let order_detail = sg_client - .order_detail(Id::new(encode_prefixed(self.order_hash.to_be_bytes_vec()))) + .order_detail(Id::new(encode_prefixed(self.get_id()))) .await?; Ok(QuoteTarget { @@ -156,7 +174,7 @@ impl BatchQuoteSpec { .batch_order_detail( self.0 .iter() - .map(|v| Bytes(encode_prefixed(v.order_hash.to_be_bytes_vec()))) + .map(|v| Bytes(encode_prefixed(v.get_id()))) .collect(), ) .await?; @@ -167,10 +185,7 @@ impl BatchQuoteSpec { .map(|target| { orders_details .iter() - .find(|order_detail| { - order_detail.order_hash.0 - == encode_prefixed(target.order_hash.to_be_bytes_vec()) - }) + .find(|order_detail| order_detail.id.0 == encode_prefixed(target.get_id())) .and_then(|order_detail| { Some(QuoteTarget { orderbook: target.orderbook, @@ -254,12 +269,16 @@ mod tests { ..Default::default() }; let order_hash_bytes = keccak256(order.abi_encode()).0; - let order_id_u256 = U256::from_be_bytes(order_hash_bytes); - let order_id = encode_prefixed(order_hash_bytes); + let order_hash_u256 = U256::from_be_bytes(order_hash_bytes); + let order_hash = encode_prefixed(order_hash_bytes); + let mut id = vec![]; + id.extend_from_slice(orderbook.as_ref()); + id.extend_from_slice(&order_hash_bytes); + let order_id = encode_prefixed(keccak256(id)); let order_json = json!({ "id": order_id, "orderBytes": encode_prefixed(order.abi_encode()), - "orderHash": order_id, + "orderHash": order_hash, "owner": encode_prefixed(order.owner), "outputs": [{ "id": encode_prefixed(Address::random().0.0), @@ -308,11 +327,11 @@ mod tests { } }) }; - (orderbook, order, order_id_u256, retrun_sg_data) + (orderbook, order, order_hash_u256, retrun_sg_data) } #[tokio::test] - async fn test_get_quote_target_from_subgraph() { + async fn test_get_quote_spec_from_subgraph() { let rpc_server = MockServer::start_async().await; let (orderbook, order, order_id_u256, retrun_sg_data) = get_test_data(false); @@ -349,7 +368,7 @@ mod tests { } #[tokio::test] - async fn test_get_batch_quote_target_from_subgraph() { + async fn test_get_batch_quote_spec_from_subgraph() { let rpc_server = MockServer::start_async().await; let (orderbook, order, order_id_u256, retrun_sg_data) = get_test_data(true); @@ -386,7 +405,7 @@ mod tests { } #[tokio::test] - async fn test_quote_specifier() { + async fn test_quote_spec_do_quote() { let rpc_server = MockServer::start_async().await; let (orderbook, _, order_id_u256, retrun_sg_data) = get_test_data(false); @@ -445,7 +464,7 @@ mod tests { } #[tokio::test] - async fn test_quote_batch_specifier() { + async fn test_quote_batch_spec_do_quote() { let rpc_server = MockServer::start_async().await; let (orderbook, _, order_id_u256, retrun_sg_data) = get_test_data(true); @@ -517,7 +536,7 @@ mod tests { } #[tokio::test] - async fn test_quote() { + async fn test_quote_target_do_quote() { let rpc_server = MockServer::start_async().await; let (orderbook, order, _, _) = get_test_data(false); @@ -564,7 +583,7 @@ mod tests { } #[tokio::test] - async fn test_batch_quote() { + async fn test_batch_quote_target_do_quote() { let rpc_server = MockServer::start_async().await; let (orderbook, order, _, _) = get_test_data(true); diff --git a/crates/subgraph/src/utils/mod.rs b/crates/subgraph/src/utils/mod.rs index 72ae0ac33..33e69147e 100644 --- a/crates/subgraph/src/utils/mod.rs +++ b/crates/subgraph/src/utils/mod.rs @@ -1,5 +1,7 @@ +mod order_id; mod slice_list; mod u256; +pub use order_id::*; pub use slice_list::*; pub use u256::*; diff --git a/crates/subgraph/src/utils/order_id.rs b/crates/subgraph/src/utils/order_id.rs new file mode 100644 index 000000000..077cb63f7 --- /dev/null +++ b/crates/subgraph/src/utils/order_id.rs @@ -0,0 +1,27 @@ +use alloy_primitives::{keccak256, Address, B256, U256}; + +/// Builds the subgraph represented order ID, given an orderbook address and an order hash +/// An order ID on subgraph is keccak256 of concated orderbook address + order hash +pub fn make_order_id(orderbook: Address, order_hash: U256) -> B256 { + let mut id_bytes = vec![]; + id_bytes.extend_from_slice(orderbook.as_ref()); + id_bytes.extend_from_slice(&B256::from(order_hash).0); + keccak256(id_bytes) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_make_order_id() { + let bytes = [4u8; 52]; + let result = make_order_id( + Address::from_slice(&bytes[..20]), + U256::from_be_slice(&bytes[20..]), + ); + let expected = keccak256(bytes); + + assert_eq!(result, expected) + } +}