From 357fffb038a2f19fbce58e335e9e62dc8a970313 Mon Sep 17 00:00:00 2001 From: findolor Date: Tue, 22 Oct 2024 21:22:08 +0300 Subject: [PATCH] refactor: move order quotes logic --- crates/quote/src/error.rs | 6 +- crates/quote/src/lib.rs | 3 + crates/quote/src/order_quotes.rs | 357 ++++++++++++++++++ .../src-tauri/src/commands/order_quote.rs | 335 +--------------- 4 files changed, 369 insertions(+), 332 deletions(-) create mode 100644 crates/quote/src/order_quotes.rs diff --git a/crates/quote/src/error.rs b/crates/quote/src/error.rs index 61b7996e8..1780f07db 100644 --- a/crates/quote/src/error.rs +++ b/crates/quote/src/error.rs @@ -1,7 +1,9 @@ use alloy::primitives::{hex::FromHexError, U256}; use alloy_ethers_typecast::transaction::ReadableClientError; use rain_error_decoding::{AbiDecodeFailedErrors, AbiDecodedErrorType}; -use rain_orderbook_subgraph_client::OrderbookSubgraphClientError; +use rain_orderbook_subgraph_client::{ + types::order_detail_traits::OrderDetailError, OrderbookSubgraphClientError, +}; use thiserror::Error; use url::ParseError; @@ -34,6 +36,8 @@ pub enum Error { #[error(transparent)] FromHexError(#[from] FromHexError), #[error(transparent)] + OrderDetailError(#[from] OrderDetailError), + #[error(transparent)] AlloySolTypesError(#[from] alloy::sol_types::Error), #[cfg(target_family = "wasm")] #[error(transparent)] diff --git a/crates/quote/src/lib.rs b/crates/quote/src/lib.rs index d4773957a..23d9b8182 100644 --- a/crates/quote/src/lib.rs +++ b/crates/quote/src/lib.rs @@ -9,6 +9,9 @@ pub mod rpc; #[cfg(target_family = "wasm")] pub mod js_api; +mod order_quotes; +pub use order_quotes::*; + pub use quote::*; #[cfg(not(target_family = "wasm"))] diff --git a/crates/quote/src/order_quotes.rs b/crates/quote/src/order_quotes.rs new file mode 100644 index 000000000..fe2b0c7b7 --- /dev/null +++ b/crates/quote/src/order_quotes.rs @@ -0,0 +1,357 @@ +use std::str::FromStr; + +use crate::{ + error::Error, + quote::{BatchQuoteTarget, QuoteTarget}, + OrderQuoteValue, +}; +use alloy::primitives::{Address, U256}; +use alloy_ethers_typecast::transaction::ReadableClient; +use rain_orderbook_bindings::IOrderBookV4::{OrderV3, Quote}; +use rain_orderbook_subgraph_client::types::common::Order; +use serde::{Deserialize, Serialize}; +use typeshare::typeshare; + +#[typeshare] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Default)] +pub struct BatchOrderQuotesResponse { + pub pair: Pair, + #[typeshare(typescript(type = "string"))] + pub block_number: U256, + pub data: Option, + pub success: bool, + pub error: Option, +} + +#[typeshare] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Default)] +pub struct Pair { + pub pair_name: String, + pub input_index: u32, + pub output_index: u32, +} + +pub async fn get_order_quotes( + orders: Vec, + block_number: Option, + rpc_url: String, +) -> Result, Error> { + let mut results: Vec = Vec::new(); + + for order in &orders { + let mut pairs: Vec = Vec::new(); + let mut quote_targets: Vec = Vec::new(); + let order_struct: OrderV3 = order.clone().try_into()?; + let orderbook = Address::from_str(&order.orderbook.id.0)?; + + for (input_index, input) in order_struct.validInputs.iter().enumerate() { + for (output_index, output) in order_struct.validOutputs.iter().enumerate() { + let pair_name = format!( + "{}/{}", + order + .inputs + .iter() + .find_map(|v| { + Address::from_str(&v.token.address.0).ok().and_then(|add| { + add.eq(&input.token).then_some( + v.token.symbol.clone().unwrap_or("UNKNOWN".to_string()), + ) + }) + }) + .unwrap_or("UNKNOWN".to_string()), + order + .outputs + .iter() + .find_map(|v| { + Address::from_str(&v.token.address.0).ok().and_then(|add| { + add.eq(&output.token).then_some( + v.token.symbol.clone().unwrap_or("UNKNOWN".to_string()), + ) + }) + }) + .unwrap_or("UNKNOWN".to_string()) + ); + + let quote_target = QuoteTarget { + orderbook, + quote_config: Quote { + order: order_struct.clone(), + inputIOIndex: U256::from(input_index), + outputIOIndex: U256::from(output_index), + signedContext: vec![], + }, + }; + + if input.token != output.token { + pairs.push(Pair { + pair_name, + input_index: input_index as u32, + output_index: output_index as u32, + }); + quote_targets.push(quote_target); + } + } + } + + let req_block_number = block_number.unwrap_or( + ReadableClient::new_from_url(rpc_url.clone())? + .get_block_number() + .await?, + ); + + let quote_values = BatchQuoteTarget(quote_targets) + .do_quote(&rpc_url, Some(req_block_number), None) + .await; + + if let Ok(quote_values) = quote_values { + for (quote_value_result, pair) in quote_values.into_iter().zip(pairs) { + match quote_value_result { + Ok(quote_value) => { + results.push(BatchOrderQuotesResponse { + pair, + block_number: U256::from(req_block_number), + success: true, + data: Some(quote_value), + error: None, + }); + } + Err(e) => { + results.push(BatchOrderQuotesResponse { + pair, + block_number: U256::from(req_block_number), + success: false, + data: None, + error: Some(e.to_string()), + }); + } + } + } + } else if let Err(e) = quote_values { + for pair in pairs { + results.push(BatchOrderQuotesResponse { + pair, + block_number: U256::from(req_block_number), + success: false, + data: None, + error: Some(e.to_string()), + }); + } + } + } + + Ok(results) +} + +#[cfg(test)] +mod tests { + use super::*; + use alloy::{ + hex::encode_prefixed, + primitives::B256, + providers::Provider, + sol_types::{SolCall, SolValue}, + }; + use rain_orderbook_common::{add_order::AddOrderArgs, dotrain_order::DotrainOrder}; + use rain_orderbook_subgraph_client::types::common::{BigInt, Bytes, Erc20, Orderbook, Vault}; + use rain_orderbook_test_fixtures::LocalEvm; + + #[tokio::test] + async fn test_get_order_quotes() { + let mut local_evm = LocalEvm::new().await; + + let owner = local_evm.signer_wallets[0].default_signer().address(); + let token1 = local_evm + .deploy_new_token("Token1", "Token1", 18, U256::MAX, owner) + .await; + let token2 = local_evm + .deploy_new_token("Token2", "Token2", 18, U256::MAX, owner) + .await; + let orderbook = &local_evm.orderbook; + + let dotrain = format!( + r#" +networks: + some-key: + rpc: {rpc_url} + chain-id: 123 + network-id: 123 + currency: ETH +deployers: + some-key: + address: {deployer} +tokens: + t2: + network: some-key + address: {token2} + decimals: 18 + label: Token2 + symbol: Token2 + t1: + network: some-key + address: {token1} + decimals: 18 + label: Token1 + symbol: token1 +orderbook: + some-key: + address: {orderbook} +orders: + some-key: + inputs: + - token: t1 + - token: t2 + outputs: + - token: t1 + vault-id: 0x01 + - token: t2 + vault-id: 0x01 +scenarios: + some-key: +deployments: + some-key: + scenario: some-key + order: some-key +--- +#calculate-io +/* use io addresses in context as calculate-io maxoutput and ratio */ +amount price: context<3 0>() context<4 0>(); +#handle-add-order +:; +#handle-io +:; +"#, + rpc_url = local_evm.url(), + orderbook = orderbook.address(), + deployer = local_evm.deployer.address(), + token1 = token1.address(), + token2 = token2.address(), + ); + + let order = DotrainOrder::new(dotrain.clone(), None).await.unwrap(); + let deployment = order.config().deployments["some-key"].as_ref().clone(); + let calldata = AddOrderArgs::new_from_deployment(dotrain, deployment) + .await + .unwrap() + .try_into_call(local_evm.url()) + .await + .unwrap() + .abi_encode(); + + // add order + let order = encode_prefixed( + local_evm + .add_order(&calldata, owner) + .await + .0 + .order + .abi_encode(), + ); + // deposit in token1 and token2 vaults + // deposit MAX so we can get token addresses as the quote result + local_evm + .deposit(owner, *token1.address(), U256::MAX, U256::from(1)) + .await; + local_evm + .deposit(owner, *token2.address(), U256::MAX, U256::from(1)) + .await; + + let vault1 = Vault { + id: Bytes(B256::random().to_string()), + token: Erc20 { + id: Bytes(token1.address().to_string()), + address: Bytes(token1.address().to_string()), + name: Some("Token1".to_string()), + symbol: Some("Token1".to_string()), + decimals: Some(BigInt(18.to_string())), + }, + balance: BigInt("123".to_string()), + vault_id: BigInt(B256::random().to_string()), + owner: Bytes(local_evm.anvil.addresses()[0].to_string()), + orderbook: Orderbook { + id: Bytes(orderbook.address().to_string()), + }, + orders_as_input: vec![], + orders_as_output: vec![], + balance_changes: vec![], + }; + let vault2 = Vault { + id: Bytes(B256::random().to_string()), + token: Erc20 { + id: Bytes(token2.address().to_string()), + address: Bytes(token2.address().to_string()), + name: Some("Token2".to_string()), + symbol: Some("Token2".to_string()), + decimals: Some(BigInt(6.to_string())), + }, + balance: BigInt("123".to_string()), + vault_id: BigInt(B256::random().to_string()), + owner: Bytes(local_evm.anvil.addresses()[0].to_string()), + orderbook: Orderbook { + id: Bytes(orderbook.address().to_string()), + }, + orders_as_input: vec![], + orders_as_output: vec![], + balance_changes: vec![], + }; + + // does not follow the actual original order's io order + let inputs = vec![vault2.clone(), vault1.clone()]; + let outputs = vec![vault2.clone(), vault1.clone()]; + + let order = Order { + id: Bytes(B256::random().to_string()), + orderbook: Orderbook { + id: Bytes(orderbook.address().to_string()), + }, + order_bytes: Bytes(order), + order_hash: Bytes(B256::random().to_string()), + owner: Bytes(local_evm.anvil.addresses()[0].to_string()), + outputs, + inputs, + active: true, + add_events: vec![], + meta: None, + timestamp_added: BigInt(0.to_string()), + trades: vec![], + }; + + let result = get_order_quotes(vec![order], None, local_evm.url()) + .await + .unwrap(); + + let token1_as_u256 = U256::from_str(&token1.address().to_string()).unwrap(); + let token2_as_u256 = U256::from_str(&token2.address().to_string()).unwrap(); + let block_number = U256::from(local_evm.provider.get_block_number().await.unwrap()); + let expected = vec![ + BatchOrderQuotesResponse { + pair: Pair { + pair_name: "Token1/Token2".to_string(), + input_index: 0, + output_index: 1, + }, + block_number, + data: Some(OrderQuoteValue { + max_output: token1_as_u256, + ratio: token2_as_u256, + }), + success: true, + error: None, + }, + BatchOrderQuotesResponse { + pair: Pair { + pair_name: "Token2/Token1".to_string(), + input_index: 1, + output_index: 0, + }, + block_number, + data: Some(OrderQuoteValue { + max_output: token2_as_u256, + ratio: token1_as_u256, + }), + success: true, + error: None, + }, + ]; + assert_eq!(result, expected); + } +} diff --git a/tauri-app/src-tauri/src/commands/order_quote.rs b/tauri-app/src-tauri/src/commands/order_quote.rs index 1c4ba36ed..361368cb9 100644 --- a/tauri-app/src-tauri/src/commands/order_quote.rs +++ b/tauri-app/src-tauri/src/commands/order_quote.rs @@ -1,34 +1,11 @@ use crate::error::CommandResult; use alloy::primitives::{Address, U256}; -use alloy_ethers_typecast::transaction::ReadableClient; -use rain_orderbook_bindings::IOrderBookV4::{OrderV3, Quote}; +use rain_orderbook_bindings::IOrderBookV4::Quote; use rain_orderbook_common::fuzz::{RainEvalResults, RainEvalResultsTable}; use rain_orderbook_quote::{ - BatchQuoteTarget, NewQuoteDebugger, OrderQuoteValue, QuoteDebugger, QuoteTarget, + get_order_quotes, BatchOrderQuotesResponse, NewQuoteDebugger, QuoteDebugger, QuoteTarget, }; use rain_orderbook_subgraph_client::types::common::*; -use serde::{Deserialize, Serialize}; -use std::str::FromStr; -use typeshare::typeshare; - -#[typeshare] -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Default)] -pub struct BatchOrderQuotesResponse { - pub pair: Pair, - #[typeshare(typescript(type = "string"))] - pub block_number: U256, - pub data: Option, - pub success: bool, - pub error: Option, -} - -#[typeshare] -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Default)] -pub struct Pair { - pub pair_name: String, - pub input_index: u32, - pub output_index: u32, -} #[tauri::command] pub async fn batch_order_quotes( @@ -36,110 +13,7 @@ pub async fn batch_order_quotes( block_number: Option, rpc_url: String, ) -> CommandResult> { - let mut results: Vec = Vec::new(); - - for order in &orders { - let mut pairs: Vec = Vec::new(); - let mut quote_targets: Vec = Vec::new(); - let order_struct: OrderV3 = order.clone().try_into()?; - let orderbook = Address::from_str(&order.orderbook.id.0)?; - - for (input_index, input) in order_struct.validInputs.iter().enumerate() { - for (output_index, output) in order_struct.validOutputs.iter().enumerate() { - let pair_name = format!( - "{}/{}", - order - .inputs - .iter() - .find_map(|v| { - Address::from_str(&v.token.address.0).ok().and_then(|add| { - add.eq(&input.token).then_some( - v.token.symbol.clone().unwrap_or("UNKNOWN".to_string()), - ) - }) - }) - .unwrap_or("UNKNOWN".to_string()), - order - .outputs - .iter() - .find_map(|v| { - Address::from_str(&v.token.address.0).ok().and_then(|add| { - add.eq(&output.token).then_some( - v.token.symbol.clone().unwrap_or("UNKNOWN".to_string()), - ) - }) - }) - .unwrap_or("UNKNOWN".to_string()) - ); - - let quote_target = QuoteTarget { - orderbook, - quote_config: Quote { - order: order_struct.clone(), - inputIOIndex: U256::from(input_index), - outputIOIndex: U256::from(output_index), - signedContext: vec![], - }, - }; - - if input.token != output.token { - pairs.push(Pair { - pair_name, - input_index: input_index as u32, - output_index: output_index as u32, - }); - quote_targets.push(quote_target); - } - } - } - - let req_block_number = block_number.unwrap_or( - ReadableClient::new_from_url(rpc_url.clone())? - .get_block_number() - .await?, - ); - - let quote_values = BatchQuoteTarget(quote_targets) - .do_quote(&rpc_url, Some(req_block_number), None) - .await; - - if let Ok(quote_values) = quote_values { - for (quote_value_result, pair) in quote_values.into_iter().zip(pairs) { - match quote_value_result { - Ok(quote_value) => { - results.push(BatchOrderQuotesResponse { - pair, - block_number: U256::from(req_block_number), - success: true, - data: Some(quote_value), - error: None, - }); - } - Err(e) => { - results.push(BatchOrderQuotesResponse { - pair, - block_number: U256::from(req_block_number), - success: false, - data: None, - error: Some(e.to_string()), - }); - } - } - } - } else if let Err(e) = quote_values { - for pair in pairs { - results.push(BatchOrderQuotesResponse { - pair, - block_number: U256::from(req_block_number), - success: false, - data: None, - error: Some(e.to_string()), - }); - } - } - } - - Ok(results) + Ok(get_order_quotes(orders, block_number, rpc_url).await?) } #[tauri::command] @@ -184,8 +58,7 @@ mod tests { use super::*; use alloy::{ hex::encode_prefixed, - primitives::{utils::parse_ether, B256}, - providers::Provider, + primitives::utils::parse_ether, sol_types::{SolCall, SolValue}, }; use rain_orderbook_common::{add_order::AddOrderArgs, dotrain_order::DotrainOrder}; @@ -315,204 +188,4 @@ amount price: 16 52; [parse_ether("16").unwrap(), parse_ether("52").unwrap()] ); } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_batch_order_quotes_block() { - let mut local_evm = LocalEvm::new().await; - - let owner = local_evm.signer_wallets[0].default_signer().address(); - let token1 = local_evm - .deploy_new_token("Token1", "Token1", 18, U256::MAX, owner) - .await; - let token2 = local_evm - .deploy_new_token("Token2", "Token2", 18, U256::MAX, owner) - .await; - let orderbook = &local_evm.orderbook; - - let dotrain = format!( - r#" -networks: - some-key: - rpc: {rpc_url} - chain-id: 123 - network-id: 123 - currency: ETH -deployers: - some-key: - address: {deployer} -tokens: - t2: - network: some-key - address: {token2} - decimals: 18 - label: Token2 - symbol: Token2 - t1: - network: some-key - address: {token1} - decimals: 18 - label: Token1 - symbol: token1 -orderbook: - some-key: - address: {orderbook} -orders: - some-key: - inputs: - - token: t1 - - token: t2 - outputs: - - token: t1 - vault-id: 0x01 - - token: t2 - vault-id: 0x01 -scenarios: - some-key: -deployments: - some-key: - scenario: some-key - order: some-key ---- -#calculate-io -/* use io addresses in context as calculate-io maxoutput and ratio */ -amount price: context<3 0>() context<4 0>(); -#handle-add-order -:; -#handle-io -:; -"#, - rpc_url = local_evm.url(), - orderbook = orderbook.address(), - deployer = local_evm.deployer.address(), - token1 = token1.address(), - token2 = token2.address(), - ); - - let order = DotrainOrder::new(dotrain.clone(), None).await.unwrap(); - let deployment = order.config().deployments["some-key"].as_ref().clone(); - let calldata = AddOrderArgs::new_from_deployment(dotrain, deployment) - .await - .unwrap() - .try_into_call(local_evm.url()) - .await - .unwrap() - .abi_encode(); - - // add order - let order = encode_prefixed( - local_evm - .add_order(&calldata, owner) - .await - .0 - .order - .abi_encode(), - ); - // deposit in token1 and token2 vaults - // deposit MAX so we can get token addresses as the quote result - local_evm - .deposit(owner, *token1.address(), U256::MAX, U256::from(1)) - .await; - local_evm - .deposit(owner, *token2.address(), U256::MAX, U256::from(1)) - .await; - - let vault1 = Vault { - id: Bytes(B256::random().to_string()), - token: Erc20 { - id: Bytes(token1.address().to_string()), - address: Bytes(token1.address().to_string()), - name: Some("Token1".to_string()), - symbol: Some("Token1".to_string()), - decimals: Some(BigInt(18.to_string())), - }, - balance: BigInt("123".to_string()), - vault_id: BigInt(B256::random().to_string()), - owner: Bytes(local_evm.anvil.addresses()[0].to_string()), - orderbook: Orderbook { - id: Bytes(orderbook.address().to_string()), - }, - orders_as_input: vec![], - orders_as_output: vec![], - balance_changes: vec![], - }; - let vault2 = Vault { - id: Bytes(B256::random().to_string()), - token: Erc20 { - id: Bytes(token2.address().to_string()), - address: Bytes(token2.address().to_string()), - name: Some("Token2".to_string()), - symbol: Some("Token2".to_string()), - decimals: Some(BigInt(6.to_string())), - }, - balance: BigInt("123".to_string()), - vault_id: BigInt(B256::random().to_string()), - owner: Bytes(local_evm.anvil.addresses()[0].to_string()), - orderbook: Orderbook { - id: Bytes(orderbook.address().to_string()), - }, - orders_as_input: vec![], - orders_as_output: vec![], - balance_changes: vec![], - }; - - // does not follow the actual original order's io order - let inputs = vec![vault2.clone(), vault1.clone()]; - let outputs = vec![vault2.clone(), vault1.clone()]; - - let order = Order { - id: Bytes(B256::random().to_string()), - orderbook: Orderbook { - id: Bytes(orderbook.address().to_string()), - }, - order_bytes: Bytes(order), - order_hash: Bytes(B256::random().to_string()), - owner: Bytes(local_evm.anvil.addresses()[0].to_string()), - outputs, - inputs, - active: true, - add_events: vec![], - meta: None, - timestamp_added: BigInt(0.to_string()), - trades: vec![], - }; - - let result = batch_order_quotes(vec![order], None, local_evm.url()) - .await - .unwrap(); - - let token1_as_u256 = U256::from_str(&token1.address().to_string()).unwrap(); - let token2_as_u256 = U256::from_str(&token2.address().to_string()).unwrap(); - let block_number = U256::from(local_evm.provider.get_block_number().await.unwrap()); - let expected = vec![ - BatchOrderQuotesResponse { - pair: Pair { - pair_name: "Token1/Token2".to_string(), - input_index: 0, - output_index: 1, - }, - block_number, - data: Some(OrderQuoteValue { - max_output: token1_as_u256, - ratio: token2_as_u256, - }), - success: true, - error: None, - }, - BatchOrderQuotesResponse { - pair: Pair { - pair_name: "Token2/Token1".to_string(), - input_index: 1, - output_index: 0, - }, - block_number, - data: Some(OrderQuoteValue { - max_output: token2_as_u256, - ratio: token1_as_u256, - }), - success: true, - error: None, - }, - ]; - assert_eq!(result, expected); - } }