diff --git a/Cargo.lock b/Cargo.lock index 5ea1961212..fb98d14e65 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7067,12 +7067,14 @@ dependencies = [ "anyhow", "async-trait", "clap", + "futures", "getrandom 0.2.16", "httpmock", "once_cell", "rain-error-decoding 0.1.0 (git+https://github.com/rainlanguage/rain.error?rev=3d2ed70fb2f7c6156706846e10f163d1e493a8d3)", "rain-interpreter-eval", "rain-math-float", + "rain-metadata 0.0.2-alpha.6", "rain_orderbook_app_settings", "rain_orderbook_bindings", "rain_orderbook_common", diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs index 6cdf43ce5f..38b5b3d70f 100644 --- a/crates/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -7,6 +7,7 @@ pub mod erc20; pub mod fuzz; pub mod local_db; pub mod meta; +pub mod oracle; pub mod parsed_meta; pub mod raindex_client; pub mod rainlang; diff --git a/crates/common/src/oracle.rs b/crates/common/src/oracle.rs new file mode 100644 index 0000000000..7dfbcf95a6 --- /dev/null +++ b/crates/common/src/oracle.rs @@ -0,0 +1,3 @@ +// Re-export oracle types and functions from the quote crate. +// This maintains backward compatibility for code in common that uses oracle functionality. +pub use rain_orderbook_quote::oracle::*; diff --git a/crates/common/src/raindex_client/order_quotes.rs b/crates/common/src/raindex_client/order_quotes.rs index 550cba56ea..67fd0426ea 100644 --- a/crates/common/src/raindex_client/order_quotes.rs +++ b/crates/common/src/raindex_client/order_quotes.rs @@ -120,8 +120,10 @@ impl RaindexOrder { ) -> Result, RaindexError> { let gas_amount = gas.map(|v| v.parse::()).transpose()?; let rpcs = self.get_rpc_urls()?; + let sg_order = self.clone().into_sg_order()?; + let order_quotes = get_order_quotes( - vec![self.clone().into_sg_order()?], + vec![sg_order], block_number, rpcs.iter().map(|s| s.to_string()).collect(), gas_amount, diff --git a/crates/common/src/raindex_client/orders.rs b/crates/common/src/raindex_client/orders.rs index 082563b168..9fb8c7ff4f 100644 --- a/crates/common/src/raindex_client/orders.rs +++ b/crates/common/src/raindex_client/orders.rs @@ -676,6 +676,7 @@ impl RaindexOrder { &rpc_urls, Some(block_number), sell_token, + self.oracle_url(), ) .await } diff --git a/crates/common/src/raindex_client/take_orders/single.rs b/crates/common/src/raindex_client/take_orders/single.rs index 18515d229f..776d896f10 100644 --- a/crates/common/src/raindex_client/take_orders/single.rs +++ b/crates/common/src/raindex_client/take_orders/single.rs @@ -54,6 +54,7 @@ pub fn build_candidate_from_quote( output_io_index, max_output: data.max_output, ratio: data.ratio, + signed_context: vec![], })) } @@ -102,6 +103,7 @@ pub fn estimate_take_order( )) } +#[allow(clippy::too_many_arguments)] pub async fn execute_single_take( candidate: TakeOrderCandidate, mode: ParsedTakeOrdersMode, @@ -110,7 +112,25 @@ pub async fn execute_single_take( rpc_urls: &[Url], block_number: Option, sell_token: Address, + oracle_url: Option, ) -> Result { + // Fetch signed context from oracle if URL provided + let mut candidate = candidate; + if let Some(url) = oracle_url { + let body = crate::oracle::encode_oracle_body( + &candidate.order, + candidate.input_io_index, + candidate.output_io_index, + taker, + ); + match crate::oracle::fetch_signed_context(&url, body).await { + Ok(ctx) => candidate.signed_context = vec![ctx], + Err(e) => { + tracing::warn!("Failed to fetch oracle data from {}: {}", url, e); + } + } + } + let zero = Float::zero()?; if candidate.ratio.gt(price_cap)? { diff --git a/crates/common/src/raindex_client/take_orders/single_tests.rs b/crates/common/src/raindex_client/take_orders/single_tests.rs index b3ace80bc0..a917b1867b 100644 --- a/crates/common/src/raindex_client/take_orders/single_tests.rs +++ b/crates/common/src/raindex_client/take_orders/single_tests.rs @@ -139,6 +139,7 @@ async fn test_single_order_take_happy_path_buy_up_to() { &rpc_urls, None, setup.token1, + None, ) .await .expect("Should succeed with BuyUpTo mode"); @@ -248,6 +249,7 @@ async fn test_single_order_take_happy_path_buy_exact() { &rpc_urls, None, setup.token1, + None, ) .await .expect("Should succeed with BuyExact mode"); @@ -350,6 +352,7 @@ async fn test_single_order_take_happy_path_spend_up_to() { &rpc_urls, None, setup.token1, + None, ) .await .expect("Should succeed with SpendUpTo mode"); @@ -576,6 +579,7 @@ async fn test_single_order_take_buy_exact_insufficient_liquidity() { &rpc_urls, None, setup.token1, + None, ) .await; @@ -667,6 +671,7 @@ async fn test_single_order_take_price_exceeds_cap() { &rpc_urls, None, setup.token1, + None, ) .await; @@ -799,6 +804,7 @@ async fn test_single_order_take_preflight_insufficient_balance() { &rpc_urls, None, setup.token1, + None, ) .await .expect("Should succeed with approval result"); @@ -904,6 +910,7 @@ async fn test_single_order_take_preflight_insufficient_allowance() { &rpc_urls, None, setup.token1, + None, ) .await .expect("Should succeed with approval result"); @@ -1010,6 +1017,7 @@ async fn test_single_order_take_approval_then_ready_flow() { &rpc_urls, None, setup.token1, + None, ) .await .expect("Should succeed with approval result"); @@ -1056,6 +1064,7 @@ async fn test_single_order_take_approval_then_ready_flow() { &rpc_urls, None, setup.token1, + None, ) .await .expect("Should succeed with ready result after approval"); @@ -1168,6 +1177,7 @@ async fn test_single_order_take_calldata_encoding_buy_mode() { &rpc_urls, None, setup.token1, + None, ) .await .expect("Should succeed"); @@ -1275,6 +1285,7 @@ async fn test_single_order_take_expected_spend_calculation() { &rpc_urls, None, setup.token1, + None, ) .await .expect("Should succeed"); @@ -1386,6 +1397,7 @@ async fn test_single_order_take_spend_exact_mode() { &rpc_urls, None, setup.token1, + None, ) .await .expect("Should succeed with SpendExact mode"); @@ -1616,6 +1628,7 @@ async fn test_single_order_take_spend_exact_insufficient_liquidity() { &rpc_urls, None, setup.token1, + None, ) .await; @@ -1709,6 +1722,7 @@ async fn test_single_order_take_calldata_encoding_spend_mode() { &rpc_urls, None, setup.token1, + None, ) .await .expect("Should succeed"); @@ -1825,6 +1839,7 @@ async fn test_single_order_take_expected_receive_calculation() { &rpc_urls, None, setup.token1, + None, ) .await .expect("Should succeed"); diff --git a/crates/common/src/take_orders/candidates.rs b/crates/common/src/take_orders/candidates.rs index e4f62095c5..5f255935ed 100644 --- a/crates/common/src/take_orders/candidates.rs +++ b/crates/common/src/take_orders/candidates.rs @@ -4,7 +4,7 @@ use crate::raindex_client::RaindexError; use alloy::primitives::Address; use futures::StreamExt; use rain_math_float::Float; -use rain_orderbook_bindings::IOrderBookV6::OrderV4; +use rain_orderbook_bindings::IOrderBookV6::{OrderV4, SignedContextV1}; #[cfg(target_family = "wasm")] use std::str::FromStr; @@ -41,6 +41,8 @@ pub struct TakeOrderCandidate { pub output_io_index: u32, pub max_output: Float, pub ratio: Float, + /// Signed context data fetched from the order's oracle endpoint (if any). + pub signed_context: Vec, } fn get_orderbook_address(order: &RaindexOrder) -> Result { @@ -54,22 +56,6 @@ fn get_orderbook_address(order: &RaindexOrder) -> Result } } -fn build_candidates_for_order( - order: &RaindexOrder, - quotes: Vec, - input_token: Address, - output_token: Address, -) -> Result, RaindexError> { - let order_v4: OrderV4 = order.try_into()?; - let orderbook = get_orderbook_address(order)?; - - quotes - .iter() - .map(|quote| try_build_candidate(orderbook, &order_v4, quote, input_token, output_token)) - .collect::, _>>() - .map(|opts| opts.into_iter().flatten().collect()) -} - pub async fn build_take_order_candidates_for_pair( orders: &[RaindexOrder], input_token: Address, @@ -79,7 +65,8 @@ pub async fn build_take_order_candidates_for_pair( ) -> Result, RaindexError> { let gas_string = gas.map(|g| g.to_string()); - let quote_results: Vec> = + // Fetch quotes for each order (oracle context fetched per-pair inside get_quotes) + let results: Vec, RaindexError>> = futures::stream::iter(orders.iter().map(|order| { let gas_string = gas_string.clone(); async move { order.get_quotes(block_number, gas_string).await } @@ -88,14 +75,77 @@ pub async fn build_take_order_candidates_for_pair( .collect() .await; - orders - .iter() - .zip(quote_results) - .map(|(order, quotes_result)| { - build_candidates_for_order(order, quotes_result?, input_token, output_token) - }) - .collect::, _>>() - .map(|vecs| vecs.into_iter().flatten().collect()) + // Build candidates — oracle context for take-order will be fetched per-pair + let mut all_candidates = vec![]; + for (order, quotes_result) in orders.iter().zip(results) { + let quotes = quotes_result?; + let order_v4: OrderV4 = order.try_into()?; + let orderbook = get_orderbook_address(order)?; + let oracle_url = { + #[cfg(target_family = "wasm")] + { + order.oracle_url() + } + #[cfg(not(target_family = "wasm"))] + { + order.oracle_url() + } + }; + + for quote in "es { + let signed_context = match &oracle_url { + Some(url) => { + fetch_oracle_for_pair( + url, + &order_v4, + quote.pair.input_index, + quote.pair.output_index, + Address::ZERO, // counterparty unknown at candidate building time + ) + .await + } + None => vec![], + }; + + if let Some(candidate) = try_build_candidate( + orderbook, + &order_v4, + quote, + input_token, + output_token, + signed_context, + )? { + all_candidates.push(candidate); + } + } + } + + Ok(all_candidates) +} + +/// Fetch signed context from an order's oracle endpoint for a specific IO pair. +/// Returns empty vec if no oracle URL or if fetch fails (best-effort). +async fn fetch_oracle_for_pair( + oracle_url: &str, + order: &OrderV4, + input_io_index: u32, + output_io_index: u32, + counterparty: Address, +) -> Vec { + let body = + crate::oracle::encode_oracle_body(order, input_io_index, output_io_index, counterparty); + match crate::oracle::fetch_signed_context(oracle_url, body).await { + Ok(ctx) => vec![ctx], + Err(e) => { + tracing::warn!( + "Failed to fetch oracle for pair ({}, {}): {}", + input_io_index, + output_io_index, + e + ); + vec![] + } + } } fn try_build_candidate( @@ -104,6 +154,7 @@ fn try_build_candidate( quote: &RaindexOrderQuote, input_token: Address, output_token: Address, + signed_context: Vec, ) -> Result, RaindexError> { let data = match (quote.success, "e.data) { (true, Some(d)) => d, @@ -138,6 +189,7 @@ fn try_build_candidate( output_io_index, max_output: data.max_output, ratio: data.ratio, + signed_context, })) } @@ -263,7 +315,8 @@ mod tests { let f1 = Float::parse("1".to_string()).unwrap(); let quote = make_quote(0, 0, Some(make_quote_value(f1, f1, f1)), true); - let result = try_build_candidate(orderbook, &order, "e, token_b, token_a).unwrap(); + let result = + try_build_candidate(orderbook, &order, "e, token_b, token_a, vec![]).unwrap(); assert!(result.is_none()); } @@ -279,7 +332,8 @@ mod tests { let f1 = Float::parse("1".to_string()).unwrap(); let quote = make_quote(0, 0, Some(make_quote_value(zero, zero, f1)), true); - let result = try_build_candidate(orderbook, &order, "e, token_a, token_b).unwrap(); + let result = + try_build_candidate(orderbook, &order, "e, token_a, token_b, vec![]).unwrap(); assert!(result.is_none()); } @@ -295,7 +349,8 @@ mod tests { let f2 = Float::parse("2".to_string()).unwrap(); let quote = make_quote(0, 0, Some(make_quote_value(f2, f1, f1)), true); - let result = try_build_candidate(orderbook, &order, "e, token_a, token_b).unwrap(); + let result = + try_build_candidate(orderbook, &order, "e, token_a, token_b, vec![]).unwrap(); assert!(result.is_some()); let candidate = result.unwrap(); @@ -314,7 +369,7 @@ mod tests { let order = make_basic_order(token_a, token_b); let quote = make_quote(0, 0, None, false); - let result = try_build_candidate(orderbook, &order, "e, token_a, token_b); + let result = try_build_candidate(orderbook, &order, "e, token_a, token_b, vec![]); assert!( result.is_ok(), @@ -337,8 +392,14 @@ mod tests { let f1 = Float::parse("1".to_string()).unwrap(); let quote_bad_input_index = make_quote(99, 0, Some(make_quote_value(f1, f1, f1)), true); - let result = - try_build_candidate(orderbook, &order, "e_bad_input_index, token_a, token_b); + let result = try_build_candidate( + orderbook, + &order, + "e_bad_input_index, + token_a, + token_b, + vec![], + ); assert!( result.is_ok(), "Out-of-bounds input index must not cause an error" @@ -349,8 +410,14 @@ mod tests { ); let quote_bad_output_index = make_quote(0, 99, Some(make_quote_value(f1, f1, f1)), true); - let result = - try_build_candidate(orderbook, &order, "e_bad_output_index, token_a, token_b); + let result = try_build_candidate( + orderbook, + &order, + "e_bad_output_index, + token_a, + token_b, + vec![], + ); assert!( result.is_ok(), "Out-of-bounds output index must not cause an error" diff --git a/crates/common/src/take_orders/config.rs b/crates/common/src/take_orders/config.rs index 5e6ec8a371..b05b06b39b 100644 --- a/crates/common/src/take_orders/config.rs +++ b/crates/common/src/take_orders/config.rs @@ -2,9 +2,7 @@ use super::simulation::SimulationResult; use crate::raindex_client::RaindexError; use alloy::primitives::{Bytes, U256}; use rain_math_float::Float; -use rain_orderbook_bindings::IOrderBookV6::{ - SignedContextV1, TakeOrderConfigV4, TakeOrdersConfigV5, -}; +use rain_orderbook_bindings::IOrderBookV6::{TakeOrderConfigV4, TakeOrdersConfigV5}; use serde::{Deserialize, Serialize}; use wasm_bindgen_utils::{impl_wasm_traits, prelude::*}; @@ -93,7 +91,7 @@ pub fn build_take_orders_config_from_simulation( order: leg.candidate.order.clone(), inputIOIndex: U256::from(leg.candidate.input_io_index), outputIOIndex: U256::from(leg.candidate.output_io_index), - signedContext: vec![] as Vec, + signedContext: leg.candidate.signed_context.clone(), }) .collect(); diff --git a/crates/common/src/test_helpers.rs b/crates/common/src/test_helpers.rs index 5222a4829e..90b5872c88 100644 --- a/crates/common/src/test_helpers.rs +++ b/crates/common/src/test_helpers.rs @@ -608,6 +608,7 @@ pub mod candidates { output_io_index: 0, max_output, ratio, + signed_context: vec![], } } diff --git a/crates/quote/Cargo.toml b/crates/quote/Cargo.toml index 0a586666a1..0ebe4fa4dd 100644 --- a/crates/quote/Cargo.toml +++ b/crates/quote/Cargo.toml @@ -13,8 +13,10 @@ crate-type = ["rlib"] [dependencies] rain-math-float.workspace = true +rain-metadata = { workspace = true } rain_orderbook_bindings = { workspace = true } rain_orderbook_subgraph_client = { workspace = true } +futures = { workspace = true } rain-error-decoding = { workspace = true } alloy = { workspace = true, features = ["sol-types"] } alloy-ethers-typecast = { workspace = true } diff --git a/crates/quote/src/lib.rs b/crates/quote/src/lib.rs index ad553865ec..8a853cb6d1 100644 --- a/crates/quote/src/lib.rs +++ b/crates/quote/src/lib.rs @@ -6,6 +6,7 @@ mod quote; mod quote_debug; pub mod rpc; +pub mod oracle; mod order_quotes; pub use order_quotes::*; diff --git a/crates/quote/src/oracle.rs b/crates/quote/src/oracle.rs new file mode 100644 index 0000000000..569fdab2ef --- /dev/null +++ b/crates/quote/src/oracle.rs @@ -0,0 +1,276 @@ +use alloy::primitives::{Address, Bytes, FixedBytes, U256}; +use alloy::sol_types::SolValue; +use rain_orderbook_bindings::IOrderBookV6::{OrderV4, SignedContextV1}; +use rain_orderbook_subgraph_client::types::common::SgOrder; +use reqwest::Client; +use serde::{Deserialize, Serialize}; + +/// Error types for oracle fetching +#[derive(Debug, thiserror::Error)] +pub enum OracleError { + #[error("HTTP request failed: {0}")] + RequestFailed(#[from] reqwest::Error), + + #[error("Invalid oracle response: {0}")] + InvalidResponse(String), + + #[error("Invalid URL: {0}")] + InvalidUrl(String), +} + +/// JSON response format from an oracle endpoint. +/// Maps directly to `SignedContextV1` in the orderbook contract. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct OracleResponse { + /// The signer address (EIP-191 signer of the context data) + pub signer: Address, + /// The signed context data as bytes32[] values + pub context: Vec>, + /// The EIP-191 signature over keccak256(abi.encodePacked(context)) + pub signature: Bytes, +} + +impl From for SignedContextV1 { + fn from(resp: OracleResponse) -> Self { + SignedContextV1 { + signer: resp.signer, + context: resp.context, + signature: resp.signature, + } + } +} + +/// Encode the POST body for a single oracle request. +/// +/// The body is `abi.encode(OrderV4, uint256 inputIOIndex, uint256 outputIOIndex, address counterparty)`. +pub fn encode_oracle_body( + order: &OrderV4, + input_io_index: u32, + output_io_index: u32, + counterparty: Address, +) -> Vec { + ( + order.clone(), + U256::from(input_io_index), + U256::from(output_io_index), + counterparty, + ) + .abi_encode() +} + +/// Encode the POST body for a batch oracle request. +/// +/// The body is `abi.encode((OrderV4, uint256 inputIOIndex, uint256 outputIOIndex, address counterparty)[])`. +pub fn encode_oracle_body_batch(requests: Vec<(&OrderV4, u32, u32, Address)>) -> Vec { + let tuples: Vec<_> = requests + .into_iter() + .map(|(order, input_io_index, output_io_index, counterparty)| { + ( + order.clone(), + U256::from(input_io_index), + U256::from(output_io_index), + counterparty, + ) + }) + .collect(); + + tuples.abi_encode() +} + +/// Fetch signed context from an oracle endpoint via POST (single request). +/// +/// The endpoint receives an ABI-encoded body containing the order details +/// that will be used for calculateOrderIO: +/// `abi.encode(OrderV4, uint256 inputIOIndex, uint256 outputIOIndex, address counterparty)` +/// +/// The endpoint must respond with a JSON body matching a single `OracleResponse`. +/// +/// NOTE: This is a legacy function. The batch format is preferred. +pub async fn fetch_signed_context( + url: &str, + body: Vec, +) -> Result { + let builder = Client::builder(); + #[cfg(not(target_family = "wasm"))] + let builder = builder.timeout(std::time::Duration::from_secs(10)); + let client = builder.build()?; + + // For single requests, we still expect a JSON array response but with one item + let response: Vec = client + .post(url) + .header("Content-Type", "application/octet-stream") + .body(body) + .send() + .await? + .error_for_status()? + .json() + .await?; + + if response.len() != 1 { + return Err(OracleError::InvalidResponse(format!( + "Expected 1 response, got {}", + response.len() + ))); + } + + Ok(response.into_iter().next().unwrap().into()) +} + +/// Fetch signed context from an oracle endpoint via POST (batch request). +/// +/// The endpoint receives an ABI-encoded body containing an array of order details: +/// `abi.encode((OrderV4, uint256 inputIOIndex, uint256 outputIOIndex, address counterparty)[])` +/// +/// The endpoint must respond with a JSON array of `OracleResponse` objects. +/// The response array length must match the request array length. +pub async fn fetch_signed_context_batch( + url: &str, + body: Vec, +) -> Result, OracleError> { + let builder = Client::builder(); + #[cfg(not(target_family = "wasm"))] + let builder = builder.timeout(std::time::Duration::from_secs(10)); + let client = builder.build()?; + + let response: Vec = client + .post(url) + .header("Content-Type", "application/octet-stream") + .body(body) + .send() + .await? + .error_for_status()? + .json() + .await?; + + Ok(response.into_iter().map(|resp| resp.into()).collect()) +} + +/// Extract the oracle URL from an SgOrder's meta, if present. +/// +/// Parses the meta bytes and looks for a `RaindexSignedContextOracleV1` entry. +/// Returns `None` if meta is absent, unparseable, or doesn't contain an oracle entry. +pub fn extract_oracle_url(order: &SgOrder) -> Option { + use rain_metadata::types::raindex_signed_context_oracle::RaindexSignedContextOracleV1; + use rain_metadata::RainMetaDocumentV1Item; + + let meta = order.meta.as_ref()?; + let decoded = alloy::hex::decode(&meta.0).ok()?; + let items = RainMetaDocumentV1Item::cbor_decode(&decoded).ok()?; + let oracle = RaindexSignedContextOracleV1::find_in_items(&items).ok()??; + Some(oracle.url().to_string()) +} + +#[cfg(test)] +mod tests { + use super::*; + use alloy::primitives::{address, FixedBytes}; + use rain_orderbook_bindings::IOrderBookV6::{EvaluableV4, OrderV4, IOV2}; + + #[test] + fn test_oracle_response_to_signed_context() { + let ctx_val = FixedBytes::<32>::from([0x2a; 32]); + let response = OracleResponse { + signer: address!("0x1234567890123456789012345678901234567890"), + context: vec![ctx_val], + signature: Bytes::from(vec![0xaa, 0xbb, 0xcc]), + }; + + let signed: SignedContextV1 = response.into(); + assert_eq!( + signed.signer, + address!("0x1234567890123456789012345678901234567890") + ); + assert_eq!(signed.context.len(), 1); + assert_eq!(signed.context[0], ctx_val); + assert_eq!(signed.signature, Bytes::from(vec![0xaa, 0xbb, 0xcc])); + } + + #[test] + fn test_encode_oracle_body_single() { + let order = create_test_order(); + let body = encode_oracle_body( + &order, + 1, + 2, + address!("0x1111111111111111111111111111111111111111"), + ); + assert!(!body.is_empty()); + } + + #[test] + fn test_encode_oracle_body_batch() { + let order1 = create_test_order(); + let order2 = create_test_order(); + + let requests = vec![ + ( + &order1, + 1, + 2, + address!("0x1111111111111111111111111111111111111111"), + ), + ( + &order2, + 3, + 4, + address!("0x2222222222222222222222222222222222222222"), + ), + ]; + + let body = encode_oracle_body_batch(requests); + assert!(!body.is_empty()); + + // Batch encoding should be different from single encoding + let single_body = encode_oracle_body( + &order1, + 1, + 2, + address!("0x1111111111111111111111111111111111111111"), + ); + assert_ne!(body, single_body); + } + + #[tokio::test] + async fn test_fetch_signed_context_invalid_url() { + let result = fetch_signed_context("not-a-url", vec![]).await; + assert!(result.is_err()); + } + + #[tokio::test] + async fn test_fetch_signed_context_unreachable() { + let result = fetch_signed_context("http://127.0.0.1:1/oracle", vec![]).await; + assert!(result.is_err()); + } + + #[tokio::test] + async fn test_fetch_signed_context_batch_invalid_url() { + let result = fetch_signed_context_batch("not-a-url", vec![]).await; + assert!(result.is_err()); + } + + #[tokio::test] + async fn test_fetch_signed_context_batch_unreachable() { + let result = fetch_signed_context_batch("http://127.0.0.1:1/oracle", vec![]).await; + assert!(result.is_err()); + } + + fn create_test_order() -> OrderV4 { + OrderV4 { + owner: address!("0x0000000000000000000000000000000000000000"), + evaluable: EvaluableV4 { + interpreter: address!("0x0000000000000000000000000000000000000000"), + store: address!("0x0000000000000000000000000000000000000000"), + bytecode: Bytes::new(), + }, + validInputs: vec![IOV2 { + token: address!("0x0000000000000000000000000000000000000000"), + vaultId: FixedBytes::<32>::ZERO, + }], + validOutputs: vec![IOV2 { + token: address!("0x0000000000000000000000000000000000000000"), + vaultId: FixedBytes::<32>::ZERO, + }], + nonce: FixedBytes::<32>::ZERO, + } + } +} diff --git a/crates/quote/src/order_quotes.rs b/crates/quote/src/order_quotes.rs index 29a71d69fe..f64b67a92f 100644 --- a/crates/quote/src/order_quotes.rs +++ b/crates/quote/src/order_quotes.rs @@ -38,6 +38,10 @@ pub struct Pair { #[cfg(target_family = "wasm")] impl_wasm_traits!(Pair); +/// Get order quotes, automatically fetching signed oracle context from order meta. +/// +/// For each order, if the meta contains a `RaindexSignedContextOracleV1` entry, +/// the oracle URL is extracted and signed context is fetched per IO pair via POST. pub async fn get_order_quotes( orders: Vec, block_number: Option, @@ -60,6 +64,7 @@ pub async fn get_order_quotes( let mut quote_targets: Vec = Vec::new(); let order_struct: OrderV4 = order.clone().try_into()?; let orderbook = Address::from_str(&order.orderbook.id.0)?; + let oracle_url = crate::oracle::extract_oracle_url(order); for (input_index, input) in order_struct.validInputs.iter().enumerate() { for (output_index, output) in order_struct.validOutputs.iter().enumerate() { @@ -89,13 +94,41 @@ pub async fn get_order_quotes( .unwrap_or("UNKNOWN".to_string()) ); + // Fetch signed oracle context for this pair if oracle URL is present + let signed_context = if let Some(ref url) = oracle_url { + if input.token != output.token { + let body = crate::oracle::encode_oracle_body( + &order_struct, + input_index as u32, + output_index as u32, + Address::ZERO, // counterparty unknown at quote time + ); + match crate::oracle::fetch_signed_context(url, body).await { + Ok(ctx) => vec![ctx], + Err(e) => { + tracing::warn!( + "Failed to fetch oracle for pair ({}, {}): {}", + input_index, + output_index, + e + ); + vec![] + } + } + } else { + vec![] + } + } else { + vec![] + }; + let quote_target = QuoteTarget { orderbook, quote_config: QuoteV2 { order: order_struct.clone(), inputIOIndex: U256::from(input_index), outputIOIndex: U256::from(output_index), - signedContext: vec![], + signedContext: signed_context, }, };