mirror of
https://github.com/floor-licker/polyfill-rs.git
synced 2026-07-27 20:47:46 +00:00
feat:implement rfq endpoints
This commit is contained in:
+810
-6
@@ -76,13 +76,20 @@ impl ClobClient {
|
||||
// Benchmarked optimal configuration: 512KB stream window
|
||||
// Results: 309.3ms vs 349ms baseline (11.4% improvement)
|
||||
let optimized_client = reqwest::ClientBuilder::new()
|
||||
// Avoid reading OS proxy settings (can be slow and/or unavailable in some sandboxed envs)
|
||||
.no_proxy()
|
||||
.http2_adaptive_window(true)
|
||||
.http2_initial_stream_window_size(512 * 1024) // 512KB - empirically optimal
|
||||
.tcp_nodelay(true)
|
||||
.pool_max_idle_per_host(10)
|
||||
.pool_idle_timeout(std::time::Duration::from_secs(90))
|
||||
.build()
|
||||
.unwrap_or_else(|_| Client::new());
|
||||
.unwrap_or_else(|_| {
|
||||
reqwest::ClientBuilder::new()
|
||||
.no_proxy()
|
||||
.build()
|
||||
.expect("Failed to build reqwest client")
|
||||
});
|
||||
|
||||
// Initialize DNS cache and pre-warm it
|
||||
let dns_cache = tokio::runtime::Handle::try_current().ok().and_then(|_| {
|
||||
@@ -134,7 +141,12 @@ impl ClobClient {
|
||||
|
||||
/// Create a client optimized for co-located environments
|
||||
pub fn new_colocated(host: &str) -> Self {
|
||||
let http_client = create_colocated_client().unwrap_or_else(|_| Client::new());
|
||||
let http_client = create_colocated_client().unwrap_or_else(|_| {
|
||||
reqwest::ClientBuilder::new()
|
||||
.no_proxy()
|
||||
.build()
|
||||
.expect("Failed to build reqwest client")
|
||||
});
|
||||
|
||||
let connection_manager = Some(std::sync::Arc::new(
|
||||
crate::connection_manager::ConnectionManager::new(
|
||||
@@ -159,7 +171,12 @@ impl ClobClient {
|
||||
|
||||
/// Create a client optimized for internet connections
|
||||
pub fn new_internet(host: &str) -> Self {
|
||||
let http_client = create_internet_client().unwrap_or_else(|_| Client::new());
|
||||
let http_client = create_internet_client().unwrap_or_else(|_| {
|
||||
reqwest::ClientBuilder::new()
|
||||
.no_proxy()
|
||||
.build()
|
||||
.expect("Failed to build reqwest client")
|
||||
});
|
||||
|
||||
let connection_manager = Some(std::sync::Arc::new(
|
||||
crate::connection_manager::ConnectionManager::new(
|
||||
@@ -190,7 +207,12 @@ impl ClobClient {
|
||||
|
||||
let order_builder = crate::orders::OrderBuilder::new(signer.clone(), None, None);
|
||||
|
||||
let http_client = create_optimized_client().unwrap_or_else(|_| Client::new());
|
||||
let http_client = create_optimized_client().unwrap_or_else(|_| {
|
||||
reqwest::ClientBuilder::new()
|
||||
.no_proxy()
|
||||
.build()
|
||||
.expect("Failed to build reqwest client")
|
||||
});
|
||||
|
||||
// Initialize infrastructure modules
|
||||
let dns_cache = None; // Skip DNS cache for simplicity in this constructor
|
||||
@@ -228,7 +250,12 @@ impl ClobClient {
|
||||
|
||||
let order_builder = crate::orders::OrderBuilder::new(signer.clone(), None, None);
|
||||
|
||||
let http_client = create_optimized_client().unwrap_or_else(|_| Client::new());
|
||||
let http_client = create_optimized_client().unwrap_or_else(|_| {
|
||||
reqwest::ClientBuilder::new()
|
||||
.no_proxy()
|
||||
.build()
|
||||
.expect("Failed to build reqwest client")
|
||||
});
|
||||
|
||||
// Initialize infrastructure modules
|
||||
let dns_cache = None; // Skip DNS cache for simplicity in this constructor
|
||||
@@ -610,6 +637,30 @@ impl ClobClient {
|
||||
Ok(tick_size)
|
||||
}
|
||||
|
||||
/// Get maker fee rate (in bps) for a token
|
||||
pub async fn get_fee_rate_bps(&self, token_id: &str) -> Result<u32> {
|
||||
let response = self
|
||||
.http_client
|
||||
.get(format!("{}/fee-rate", self.base_url))
|
||||
.query(&[("token_id", token_id)])
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(PolyfillError::api(
|
||||
response.status().as_u16(),
|
||||
"Failed to get fee rate",
|
||||
));
|
||||
}
|
||||
|
||||
let fee_rate: crate::types::FeeRateResponse = response
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None))?;
|
||||
Ok(fee_rate.fee_rate_bps)
|
||||
}
|
||||
|
||||
/// Create a new API key
|
||||
pub async fn create_api_key(&self, nonce: Option<U256>) -> Result<ApiCreds> {
|
||||
let signer = self
|
||||
@@ -1722,6 +1773,389 @@ impl ClobClient {
|
||||
.map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None))
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// RFQ (Market Maker) endpoints
|
||||
// ============================================================================
|
||||
|
||||
/// Create an RFQ request.
|
||||
pub async fn create_rfq_request(
|
||||
&self,
|
||||
request: &crate::types::RfqCreateRequest,
|
||||
) -> Result<crate::types::RfqCreateRequestResponse> {
|
||||
let signer = self
|
||||
.signer
|
||||
.as_ref()
|
||||
.ok_or_else(|| PolyfillError::auth("Signer not set"))?;
|
||||
let api_creds = self
|
||||
.api_creds
|
||||
.as_ref()
|
||||
.ok_or_else(|| PolyfillError::auth("API credentials not set"))?;
|
||||
|
||||
let method = Method::POST;
|
||||
let endpoint = "/rfq/request";
|
||||
let headers =
|
||||
create_l2_headers(signer, api_creds, method.as_str(), endpoint, Some(request))?;
|
||||
|
||||
let response = self
|
||||
.create_request_with_headers(method, endpoint, headers.into_iter())
|
||||
.json(request)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(PolyfillError::api(
|
||||
response.status().as_u16(),
|
||||
"Failed to create RFQ request",
|
||||
));
|
||||
}
|
||||
|
||||
response
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None))
|
||||
}
|
||||
|
||||
/// Cancel an RFQ request.
|
||||
pub async fn cancel_rfq_request(&self, request_id: &str) -> Result<()> {
|
||||
let signer = self
|
||||
.signer
|
||||
.as_ref()
|
||||
.ok_or_else(|| PolyfillError::auth("Signer not set"))?;
|
||||
let api_creds = self
|
||||
.api_creds
|
||||
.as_ref()
|
||||
.ok_or_else(|| PolyfillError::auth("API credentials not set"))?;
|
||||
|
||||
let method = Method::DELETE;
|
||||
let endpoint = "/rfq/request";
|
||||
let body = crate::types::RfqCancelRequest {
|
||||
request_id: request_id.to_string(),
|
||||
};
|
||||
let headers = create_l2_headers(signer, api_creds, method.as_str(), endpoint, Some(&body))?;
|
||||
|
||||
let response = self
|
||||
.create_request_with_headers(method, endpoint, headers.into_iter())
|
||||
.json(&body)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(PolyfillError::api(
|
||||
response.status().as_u16(),
|
||||
"Failed to cancel RFQ request",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get RFQ requests (requester).
|
||||
pub async fn get_rfq_requests(
|
||||
&self,
|
||||
params: Option<&crate::types::RfqRequestsParams>,
|
||||
) -> Result<crate::types::RfqListResponse<crate::types::RfqRequestData>> {
|
||||
let signer = self
|
||||
.signer
|
||||
.as_ref()
|
||||
.ok_or_else(|| PolyfillError::auth("Signer not set"))?;
|
||||
let api_creds = self
|
||||
.api_creds
|
||||
.as_ref()
|
||||
.ok_or_else(|| PolyfillError::auth("API credentials not set"))?;
|
||||
|
||||
let method = Method::GET;
|
||||
let endpoint = "/rfq/data/requests";
|
||||
let headers =
|
||||
create_l2_headers::<Value>(signer, api_creds, method.as_str(), endpoint, None)?;
|
||||
|
||||
let query_params = params.cloned().unwrap_or_default().to_query_params();
|
||||
|
||||
let response = self
|
||||
.create_request_with_headers(method, endpoint, headers.into_iter())
|
||||
.query(&query_params)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(PolyfillError::api(
|
||||
response.status().as_u16(),
|
||||
"Failed to get RFQ requests",
|
||||
));
|
||||
}
|
||||
|
||||
response
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None))
|
||||
}
|
||||
|
||||
/// Create an RFQ quote.
|
||||
pub async fn create_rfq_quote(
|
||||
&self,
|
||||
quote: &crate::types::RfqCreateQuote,
|
||||
) -> Result<crate::types::RfqCreateQuoteResponse> {
|
||||
let signer = self
|
||||
.signer
|
||||
.as_ref()
|
||||
.ok_or_else(|| PolyfillError::auth("Signer not set"))?;
|
||||
let api_creds = self
|
||||
.api_creds
|
||||
.as_ref()
|
||||
.ok_or_else(|| PolyfillError::auth("API credentials not set"))?;
|
||||
|
||||
let method = Method::POST;
|
||||
let endpoint = "/rfq/quote";
|
||||
let headers = create_l2_headers(signer, api_creds, method.as_str(), endpoint, Some(quote))?;
|
||||
|
||||
let response = self
|
||||
.create_request_with_headers(method, endpoint, headers.into_iter())
|
||||
.json(quote)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(PolyfillError::api(
|
||||
response.status().as_u16(),
|
||||
"Failed to create RFQ quote",
|
||||
));
|
||||
}
|
||||
|
||||
response
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None))
|
||||
}
|
||||
|
||||
/// Cancel an RFQ quote.
|
||||
pub async fn cancel_rfq_quote(&self, quote_id: &str) -> Result<()> {
|
||||
let signer = self
|
||||
.signer
|
||||
.as_ref()
|
||||
.ok_or_else(|| PolyfillError::auth("Signer not set"))?;
|
||||
let api_creds = self
|
||||
.api_creds
|
||||
.as_ref()
|
||||
.ok_or_else(|| PolyfillError::auth("API credentials not set"))?;
|
||||
|
||||
let method = Method::DELETE;
|
||||
let endpoint = "/rfq/quote";
|
||||
let body = crate::types::RfqCancelQuote {
|
||||
quote_id: quote_id.to_string(),
|
||||
};
|
||||
let headers = create_l2_headers(signer, api_creds, method.as_str(), endpoint, Some(&body))?;
|
||||
|
||||
let response = self
|
||||
.create_request_with_headers(method, endpoint, headers.into_iter())
|
||||
.json(&body)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(PolyfillError::api(
|
||||
response.status().as_u16(),
|
||||
"Failed to cancel RFQ quote",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get quotes for the requester.
|
||||
pub async fn get_rfq_requester_quotes(
|
||||
&self,
|
||||
params: Option<&crate::types::RfqQuotesParams>,
|
||||
) -> Result<crate::types::RfqListResponse<crate::types::RfqQuoteData>> {
|
||||
let signer = self
|
||||
.signer
|
||||
.as_ref()
|
||||
.ok_or_else(|| PolyfillError::auth("Signer not set"))?;
|
||||
let api_creds = self
|
||||
.api_creds
|
||||
.as_ref()
|
||||
.ok_or_else(|| PolyfillError::auth("API credentials not set"))?;
|
||||
|
||||
let method = Method::GET;
|
||||
let endpoint = "/rfq/data/requester/quotes";
|
||||
let headers =
|
||||
create_l2_headers::<Value>(signer, api_creds, method.as_str(), endpoint, None)?;
|
||||
|
||||
let query_params = params.cloned().unwrap_or_default().to_query_params();
|
||||
|
||||
let response = self
|
||||
.create_request_with_headers(method, endpoint, headers.into_iter())
|
||||
.query(&query_params)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(PolyfillError::api(
|
||||
response.status().as_u16(),
|
||||
"Failed to get RFQ requester quotes",
|
||||
));
|
||||
}
|
||||
|
||||
response
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None))
|
||||
}
|
||||
|
||||
/// Get quotes for the quoter.
|
||||
pub async fn get_rfq_quoter_quotes(
|
||||
&self,
|
||||
params: Option<&crate::types::RfqQuotesParams>,
|
||||
) -> Result<crate::types::RfqListResponse<crate::types::RfqQuoteData>> {
|
||||
let signer = self
|
||||
.signer
|
||||
.as_ref()
|
||||
.ok_or_else(|| PolyfillError::auth("Signer not set"))?;
|
||||
let api_creds = self
|
||||
.api_creds
|
||||
.as_ref()
|
||||
.ok_or_else(|| PolyfillError::auth("API credentials not set"))?;
|
||||
|
||||
let method = Method::GET;
|
||||
let endpoint = "/rfq/data/quoter/quotes";
|
||||
let headers =
|
||||
create_l2_headers::<Value>(signer, api_creds, method.as_str(), endpoint, None)?;
|
||||
|
||||
let query_params = params.cloned().unwrap_or_default().to_query_params();
|
||||
|
||||
let response = self
|
||||
.create_request_with_headers(method, endpoint, headers.into_iter())
|
||||
.query(&query_params)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(PolyfillError::api(
|
||||
response.status().as_u16(),
|
||||
"Failed to get RFQ quoter quotes",
|
||||
));
|
||||
}
|
||||
|
||||
response
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None))
|
||||
}
|
||||
|
||||
/// Get best quote for a request.
|
||||
pub async fn get_rfq_best_quote(&self, request_id: &str) -> Result<crate::types::RfqQuoteData> {
|
||||
let signer = self
|
||||
.signer
|
||||
.as_ref()
|
||||
.ok_or_else(|| PolyfillError::auth("Signer not set"))?;
|
||||
let api_creds = self
|
||||
.api_creds
|
||||
.as_ref()
|
||||
.ok_or_else(|| PolyfillError::auth("API credentials not set"))?;
|
||||
|
||||
let method = Method::GET;
|
||||
let endpoint = "/rfq/data/best-quote";
|
||||
let headers =
|
||||
create_l2_headers::<Value>(signer, api_creds, method.as_str(), endpoint, None)?;
|
||||
|
||||
let response = self
|
||||
.create_request_with_headers(method, endpoint, headers.into_iter())
|
||||
.query(&[("requestId", request_id)])
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(PolyfillError::api(
|
||||
response.status().as_u16(),
|
||||
"Failed to get RFQ best quote",
|
||||
));
|
||||
}
|
||||
|
||||
response
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None))
|
||||
}
|
||||
|
||||
/// Accept the best quote and post the resulting order.
|
||||
pub async fn accept_rfq_quote(
|
||||
&self,
|
||||
body: &crate::types::RfqOrderExecutionRequest,
|
||||
) -> Result<()> {
|
||||
let signer = self
|
||||
.signer
|
||||
.as_ref()
|
||||
.ok_or_else(|| PolyfillError::auth("Signer not set"))?;
|
||||
let api_creds = self
|
||||
.api_creds
|
||||
.as_ref()
|
||||
.ok_or_else(|| PolyfillError::auth("API credentials not set"))?;
|
||||
|
||||
let method = Method::POST;
|
||||
let endpoint = "/rfq/request/accept";
|
||||
let headers = create_l2_headers(signer, api_creds, method.as_str(), endpoint, Some(body))?;
|
||||
|
||||
let response = self
|
||||
.create_request_with_headers(method, endpoint, headers.into_iter())
|
||||
.json(body)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(PolyfillError::api(
|
||||
response.status().as_u16(),
|
||||
"Failed to accept RFQ quote",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Approve the accepted quote's order (Quoter).
|
||||
pub async fn approve_rfq_order(
|
||||
&self,
|
||||
body: &crate::types::RfqOrderExecutionRequest,
|
||||
) -> Result<crate::types::RfqApproveOrderResponse> {
|
||||
let signer = self
|
||||
.signer
|
||||
.as_ref()
|
||||
.ok_or_else(|| PolyfillError::auth("Signer not set"))?;
|
||||
let api_creds = self
|
||||
.api_creds
|
||||
.as_ref()
|
||||
.ok_or_else(|| PolyfillError::auth("API credentials not set"))?;
|
||||
|
||||
let method = Method::POST;
|
||||
let endpoint = "/rfq/quote/approve";
|
||||
let headers = create_l2_headers(signer, api_creds, method.as_str(), endpoint, Some(body))?;
|
||||
|
||||
let response = self
|
||||
.create_request_with_headers(method, endpoint, headers.into_iter())
|
||||
.json(body)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(PolyfillError::api(
|
||||
response.status().as_u16(),
|
||||
"Failed to approve RFQ order",
|
||||
));
|
||||
}
|
||||
|
||||
response
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None))
|
||||
}
|
||||
|
||||
/// Get sampling markets with pagination
|
||||
pub async fn get_sampling_markets(
|
||||
&self,
|
||||
@@ -1860,10 +2294,14 @@ pub type PolyfillClient = ClobClient;
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{ClobClient, OrderArgs as ClientOrderArgs};
|
||||
use crate::types::{PricesHistoryInterval, Side};
|
||||
use crate::types::{
|
||||
PricesHistoryInterval, RfqCreateQuote, RfqCreateRequest, RfqOrderExecutionRequest,
|
||||
RfqQuotesParams, RfqRequestsParams, Side,
|
||||
};
|
||||
use crate::{ApiCredentials, PolyfillError};
|
||||
use mockito::{Matcher, Server};
|
||||
use rust_decimal::Decimal;
|
||||
use serde_json::json;
|
||||
use std::str::FromStr;
|
||||
use tokio;
|
||||
|
||||
@@ -1879,6 +2317,22 @@ mod tests {
|
||||
)
|
||||
}
|
||||
|
||||
fn create_test_client_with_l2_auth(base_url: &str) -> ClobClient {
|
||||
let api_creds = ApiCredentials {
|
||||
api_key: "test_key".to_string(),
|
||||
// URL-safe base64 so HMAC header generation succeeds.
|
||||
secret: "dGVzdF9zZWNyZXRfa2V5XzEyMzQ1".to_string(),
|
||||
passphrase: "test_passphrase".to_string(),
|
||||
};
|
||||
|
||||
ClobClient::with_l2_headers(
|
||||
base_url,
|
||||
"0x1234567890123456789012345678901234567890123456789012345678901234",
|
||||
137,
|
||||
api_creds,
|
||||
)
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_client_creation() {
|
||||
let client = create_test_client("https://test.example.com");
|
||||
@@ -2572,4 +3026,354 @@ mod tests {
|
||||
assert_eq!(default_args.size, Decimal::ZERO);
|
||||
assert_eq!(default_args.side, Side::BUY);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_get_fee_rate_bps_success() {
|
||||
let mut server = Server::new_async().await;
|
||||
let mock = server
|
||||
.mock("GET", "/fee-rate")
|
||||
.match_query(Matcher::UrlEncoded("token_id".into(), "123".into()))
|
||||
.with_status(200)
|
||||
.with_header("content-type", "application/json")
|
||||
.with_body(r#"{"fee_rate_bps":1000}"#)
|
||||
.create_async()
|
||||
.await;
|
||||
|
||||
let client = create_test_client(&server.url());
|
||||
let rate = client.get_fee_rate_bps("123").await.unwrap();
|
||||
|
||||
mock.assert_async().await;
|
||||
assert_eq!(rate, 1000);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_rfq_endpoints_happy_path() {
|
||||
let mut server = Server::new_async().await;
|
||||
|
||||
// create_rfq_request
|
||||
let create_request = RfqCreateRequest {
|
||||
asset_in: "some_asset_in".to_string(),
|
||||
asset_out: "some_asset_out".to_string(),
|
||||
amount_in: "100".to_string(),
|
||||
amount_out: "200".to_string(),
|
||||
user_type: 0,
|
||||
};
|
||||
let create_request_mock = server
|
||||
.mock("POST", "/rfq/request")
|
||||
.match_body(Matcher::JsonString(
|
||||
json!({
|
||||
"assetIn": "some_asset_in",
|
||||
"assetOut": "some_asset_out",
|
||||
"amountIn": "100",
|
||||
"amountOut": "200",
|
||||
"userType": 0
|
||||
})
|
||||
.to_string(),
|
||||
))
|
||||
.with_status(200)
|
||||
.with_header("content-type", "application/json")
|
||||
.with_body(r#"{"requestId":"req123","expiry":1744936318}"#)
|
||||
.create_async()
|
||||
.await;
|
||||
|
||||
// cancel_rfq_request
|
||||
let cancel_request_mock = server
|
||||
.mock("DELETE", "/rfq/request")
|
||||
.match_body(Matcher::JsonString(r#"{"requestId":"req123"}"#.to_string()))
|
||||
.with_status(200)
|
||||
.with_body("OK")
|
||||
.create_async()
|
||||
.await;
|
||||
|
||||
// get_rfq_requests
|
||||
let rfq_requests_mock = server
|
||||
.mock("GET", "/rfq/data/requests")
|
||||
.match_query(Matcher::AllOf(vec![
|
||||
Matcher::UrlEncoded("offset".into(), "MA==".into()),
|
||||
Matcher::UrlEncoded("limit".into(), "10".into()),
|
||||
Matcher::UrlEncoded("state".into(), "active".into()),
|
||||
Matcher::UrlEncoded("requestIds[]".into(), "req123".into()),
|
||||
Matcher::UrlEncoded("markets[]".into(), "some_market".into()),
|
||||
]))
|
||||
.with_status(200)
|
||||
.with_header("content-type", "application/json")
|
||||
.with_body(
|
||||
r#"{
|
||||
"data": [{
|
||||
"requestId": "req123",
|
||||
"userAddress": "0xabc",
|
||||
"proxyAddress": "0xdef",
|
||||
"condition": "some_condition_id",
|
||||
"token": "some_token_id",
|
||||
"complement": "some_complement",
|
||||
"side": "BUY",
|
||||
"sizeIn": 100,
|
||||
"sizeOut": 200,
|
||||
"price": 0.5,
|
||||
"state": "active",
|
||||
"expiry": 1744936318
|
||||
}],
|
||||
"next_cursor": "MA==",
|
||||
"limit": 10,
|
||||
"count": 1
|
||||
}"#,
|
||||
)
|
||||
.create_async()
|
||||
.await;
|
||||
|
||||
// create_rfq_quote
|
||||
let create_quote = RfqCreateQuote {
|
||||
request_id: "req123".to_string(),
|
||||
asset_in: "some_asset_in".to_string(),
|
||||
asset_out: "some_asset_out".to_string(),
|
||||
amount_in: "100".to_string(),
|
||||
amount_out: "200".to_string(),
|
||||
user_type: 0,
|
||||
};
|
||||
let create_quote_mock = server
|
||||
.mock("POST", "/rfq/quote")
|
||||
.match_body(Matcher::JsonString(
|
||||
json!({
|
||||
"requestId": "req123",
|
||||
"assetIn": "some_asset_in",
|
||||
"assetOut": "some_asset_out",
|
||||
"amountIn": "100",
|
||||
"amountOut": "200",
|
||||
"userType": 0
|
||||
})
|
||||
.to_string(),
|
||||
))
|
||||
.with_status(200)
|
||||
.with_header("content-type", "application/json")
|
||||
.with_body(r#"{"quoteId":"q123"}"#)
|
||||
.create_async()
|
||||
.await;
|
||||
|
||||
// cancel_rfq_quote
|
||||
let cancel_quote_mock = server
|
||||
.mock("DELETE", "/rfq/quote")
|
||||
.match_body(Matcher::JsonString(r#"{"quoteId":"q123"}"#.to_string()))
|
||||
.with_status(200)
|
||||
.with_body("OK")
|
||||
.create_async()
|
||||
.await;
|
||||
|
||||
// get_rfq_requester_quotes
|
||||
let requester_quotes_mock = server
|
||||
.mock("GET", "/rfq/data/requester/quotes")
|
||||
.match_query(Matcher::AllOf(vec![
|
||||
Matcher::UrlEncoded("offset".into(), "MA==".into()),
|
||||
Matcher::UrlEncoded("limit".into(), "10".into()),
|
||||
Matcher::UrlEncoded("state".into(), "active".into()),
|
||||
Matcher::UrlEncoded("quoteIds[]".into(), "q123".into()),
|
||||
Matcher::UrlEncoded("requestIds[]".into(), "req123".into()),
|
||||
]))
|
||||
.with_status(200)
|
||||
.with_header("content-type", "application/json")
|
||||
.with_body(
|
||||
r#"{
|
||||
"data": [{
|
||||
"quoteId": "q123",
|
||||
"requestId": "req123",
|
||||
"userAddress": "0xabc",
|
||||
"proxyAddress": "0xdef",
|
||||
"condition": "some_condition_id",
|
||||
"token": "some_token_id",
|
||||
"complement": "some_complement",
|
||||
"side": "BUY",
|
||||
"sizeIn": 100,
|
||||
"sizeOut": 200,
|
||||
"price": 0.5,
|
||||
"matchType": "matched",
|
||||
"state": "active"
|
||||
}],
|
||||
"next_cursor": "MA==",
|
||||
"limit": 10,
|
||||
"count": 1
|
||||
}"#,
|
||||
)
|
||||
.create_async()
|
||||
.await;
|
||||
|
||||
// get_rfq_quoter_quotes
|
||||
let quoter_quotes_mock = server
|
||||
.mock("GET", "/rfq/data/quoter/quotes")
|
||||
.with_status(200)
|
||||
.with_header("content-type", "application/json")
|
||||
.with_body(
|
||||
r#"{
|
||||
"data": [],
|
||||
"next_cursor": "MA==",
|
||||
"limit": 10,
|
||||
"count": 0
|
||||
}"#,
|
||||
)
|
||||
.create_async()
|
||||
.await;
|
||||
|
||||
// get_rfq_best_quote
|
||||
let best_quote_mock = server
|
||||
.mock("GET", "/rfq/data/best-quote")
|
||||
.match_query(Matcher::UrlEncoded("requestId".into(), "req123".into()))
|
||||
.with_status(200)
|
||||
.with_header("content-type", "application/json")
|
||||
.with_body(
|
||||
r#"{
|
||||
"quoteId": "q123",
|
||||
"requestId": "req123",
|
||||
"userAddress": "0xabc",
|
||||
"proxyAddress": "0xdef",
|
||||
"condition": "some_condition_id",
|
||||
"token": "some_token_id",
|
||||
"complement": "some_complement",
|
||||
"side": "BUY",
|
||||
"sizeIn": 100,
|
||||
"sizeOut": 200,
|
||||
"price": 0.5,
|
||||
"matchType": "matched",
|
||||
"state": "active"
|
||||
}"#,
|
||||
)
|
||||
.create_async()
|
||||
.await;
|
||||
|
||||
// accept_rfq_quote
|
||||
let exec = RfqOrderExecutionRequest {
|
||||
request_id: "req123".to_string(),
|
||||
quote_id: "q123".to_string(),
|
||||
maker: "0xmaker".to_string(),
|
||||
signer: "0xsigner".to_string(),
|
||||
taker: "0xtaker".to_string(),
|
||||
expiration: 1_740_000_000,
|
||||
nonce: "123".to_string(),
|
||||
fee_rate_bps: "1000".to_string(),
|
||||
side: "BUY".to_string(),
|
||||
token_id: "123".to_string(),
|
||||
maker_amount: "100".to_string(),
|
||||
taker_amount: "200".to_string(),
|
||||
signature_type: 2,
|
||||
signature: "0xsig".to_string(),
|
||||
salt: 42,
|
||||
owner: "owner".to_string(),
|
||||
};
|
||||
|
||||
let accept_mock = server
|
||||
.mock("POST", "/rfq/request/accept")
|
||||
.match_body(Matcher::JsonString(
|
||||
json!({
|
||||
"requestId": "req123",
|
||||
"quoteId": "q123",
|
||||
"maker": "0xmaker",
|
||||
"signer": "0xsigner",
|
||||
"taker": "0xtaker",
|
||||
"expiration": 1740000000,
|
||||
"nonce": "123",
|
||||
"feeRateBps": "1000",
|
||||
"side": "BUY",
|
||||
"tokenId": "123",
|
||||
"makerAmount": "100",
|
||||
"takerAmount": "200",
|
||||
"signatureType": 2,
|
||||
"signature": "0xsig",
|
||||
"salt": 42,
|
||||
"owner": "owner"
|
||||
})
|
||||
.to_string(),
|
||||
))
|
||||
.with_status(200)
|
||||
.with_body("OK")
|
||||
.create_async()
|
||||
.await;
|
||||
|
||||
// approve_rfq_order
|
||||
let approve_mock = server
|
||||
.mock("POST", "/rfq/quote/approve")
|
||||
.match_body(Matcher::JsonString(
|
||||
json!({
|
||||
"requestId": "req123",
|
||||
"quoteId": "q123",
|
||||
"maker": "0xmaker",
|
||||
"signer": "0xsigner",
|
||||
"taker": "0xtaker",
|
||||
"expiration": 1740000000,
|
||||
"nonce": "123",
|
||||
"feeRateBps": "1000",
|
||||
"side": "BUY",
|
||||
"tokenId": "123",
|
||||
"makerAmount": "100",
|
||||
"takerAmount": "200",
|
||||
"signatureType": 2,
|
||||
"signature": "0xsig",
|
||||
"salt": 42,
|
||||
"owner": "owner"
|
||||
})
|
||||
.to_string(),
|
||||
))
|
||||
.with_status(200)
|
||||
.with_header("content-type", "application/json")
|
||||
.with_body(r#"{"tradeIds":["t1","t2"]}"#)
|
||||
.create_async()
|
||||
.await;
|
||||
|
||||
let client = create_test_client_with_l2_auth(&server.url());
|
||||
|
||||
let created = client.create_rfq_request(&create_request).await.unwrap();
|
||||
assert_eq!(created.request_id, "req123");
|
||||
assert_eq!(created.expiry, 1_744_936_318);
|
||||
create_request_mock.assert_async().await;
|
||||
|
||||
client.cancel_rfq_request("req123").await.unwrap();
|
||||
cancel_request_mock.assert_async().await;
|
||||
|
||||
let params = RfqRequestsParams {
|
||||
offset: Some("MA==".to_string()),
|
||||
limit: Some(10),
|
||||
state: Some("active".to_string()),
|
||||
request_ids: vec!["req123".to_string()],
|
||||
markets: vec!["some_market".to_string()],
|
||||
..Default::default()
|
||||
};
|
||||
let requests = client.get_rfq_requests(Some(¶ms)).await.unwrap();
|
||||
assert_eq!(requests.data.len(), 1);
|
||||
assert_eq!(requests.data[0].request_id, "req123");
|
||||
rfq_requests_mock.assert_async().await;
|
||||
|
||||
let quote = client.create_rfq_quote(&create_quote).await.unwrap();
|
||||
assert_eq!(quote.quote_id, "q123");
|
||||
create_quote_mock.assert_async().await;
|
||||
|
||||
client.cancel_rfq_quote("q123").await.unwrap();
|
||||
cancel_quote_mock.assert_async().await;
|
||||
|
||||
let quote_params = RfqQuotesParams {
|
||||
offset: Some("MA==".to_string()),
|
||||
limit: Some(10),
|
||||
state: Some("active".to_string()),
|
||||
quote_ids: vec!["q123".to_string()],
|
||||
request_ids: vec!["req123".to_string()],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let requester_quotes = client
|
||||
.get_rfq_requester_quotes(Some("e_params))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(requester_quotes.data.len(), 1);
|
||||
requester_quotes_mock.assert_async().await;
|
||||
|
||||
let quoter_quotes = client.get_rfq_quoter_quotes(None).await.unwrap();
|
||||
assert_eq!(quoter_quotes.data.len(), 0);
|
||||
quoter_quotes_mock.assert_async().await;
|
||||
|
||||
let best = client.get_rfq_best_quote("req123").await.unwrap();
|
||||
assert_eq!(best.quote_id, "q123");
|
||||
best_quote_mock.assert_async().await;
|
||||
|
||||
client.accept_rfq_quote(&exec).await.unwrap();
|
||||
accept_mock.assert_async().await;
|
||||
|
||||
let approved = client.approve_rfq_order(&exec).await.unwrap();
|
||||
assert_eq!(approved.trade_ids, vec!["t1".to_string(), "t2".to_string()]);
|
||||
approve_mock.assert_async().await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,14 +100,14 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_connection_manager_creation() {
|
||||
let client = Client::new();
|
||||
let client = reqwest::ClientBuilder::new().no_proxy().build().unwrap();
|
||||
let manager = ConnectionManager::new(client, "https://clob.polymarket.com".to_string());
|
||||
assert!(!manager.is_running());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_keepalive_start_stop() {
|
||||
let client = Client::new();
|
||||
let client = reqwest::ClientBuilder::new().no_proxy().build().unwrap();
|
||||
let manager = ConnectionManager::new(client, "https://clob.polymarket.com".to_string());
|
||||
|
||||
manager.start_keepalive(Duration::from_secs(30)).await;
|
||||
|
||||
@@ -173,6 +173,19 @@ pub mod deserializers {
|
||||
_ => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
/// Deserialize a Decimal from string/number.
|
||||
///
|
||||
/// - `""` => error
|
||||
/// - invalid values => error
|
||||
pub fn decimal_from_string<'de, D>(deserializer: D) -> std::result::Result<Decimal, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
optional_decimal_from_string(deserializer)?.ok_or_else(|| {
|
||||
serde::de::Error::custom("Expected decimal as string/number, got null/empty string")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Raw API response types for efficient parsing
|
||||
|
||||
@@ -102,6 +102,7 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "requires external DNS/network access"]
|
||||
async fn test_dns_cache_resolve() {
|
||||
let cache = DnsCache::new().await.unwrap();
|
||||
let ips = cache.resolve("clob.polymarket.com").await.unwrap();
|
||||
@@ -109,6 +110,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "requires external DNS/network access"]
|
||||
async fn test_dns_cache_prewarm() {
|
||||
let cache = DnsCache::new().await.unwrap();
|
||||
cache.prewarm("clob.polymarket.com").await.unwrap();
|
||||
@@ -116,6 +118,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "requires external DNS/network access"]
|
||||
async fn test_dns_cache_clear() {
|
||||
let cache = DnsCache::new().await.unwrap();
|
||||
cache.prewarm("clob.polymarket.com").await.unwrap();
|
||||
|
||||
@@ -26,6 +26,8 @@ pub async fn prewarm_connections(client: &Client, base_url: &str) -> Result<(),
|
||||
/// Benchmarked configuration: 309.3ms vs 349ms baseline (11.4% faster)
|
||||
pub fn create_optimized_client() -> Result<Client, reqwest::Error> {
|
||||
ClientBuilder::new()
|
||||
// Avoid reading OS proxy settings (can be slow and/or unavailable in some sandboxed envs)
|
||||
.no_proxy()
|
||||
// Connection pooling optimizations - aggressive reuse
|
||||
.pool_max_idle_per_host(10) // Keep connections alive
|
||||
.pool_idle_timeout(Duration::from_secs(90)) // Longer reuse window
|
||||
@@ -49,6 +51,8 @@ pub fn create_optimized_client() -> Result<Client, reqwest::Error> {
|
||||
/// (even more aggressive settings for when you're close to the exchange)
|
||||
pub fn create_colocated_client() -> Result<Client, reqwest::Error> {
|
||||
ClientBuilder::new()
|
||||
// Avoid reading OS proxy settings (can be slow and/or unavailable in some sandboxed envs)
|
||||
.no_proxy()
|
||||
// More aggressive connection pooling
|
||||
.pool_max_idle_per_host(20) // More connections
|
||||
.pool_idle_timeout(Duration::from_secs(60)) // Longer reuse
|
||||
@@ -78,6 +82,8 @@ pub fn create_colocated_client() -> Result<Client, reqwest::Error> {
|
||||
/// (more conservative settings for internet connections)
|
||||
pub fn create_internet_client() -> Result<Client, reqwest::Error> {
|
||||
ClientBuilder::new()
|
||||
// Avoid reading OS proxy settings (can be slow and/or unavailable in some sandboxed envs)
|
||||
.no_proxy()
|
||||
// Conservative connection pooling
|
||||
.pool_max_idle_per_host(5)
|
||||
.pool_idle_timeout(Duration::from_secs(90))
|
||||
|
||||
+14
@@ -99,6 +99,7 @@ pub use crate::types::{
|
||||
BookParams,
|
||||
ClientConfig,
|
||||
ClientResult,
|
||||
FeeRateResponse,
|
||||
FillEvent,
|
||||
Market,
|
||||
MarketSnapshot,
|
||||
@@ -120,6 +121,19 @@ pub use crate::types::{
|
||||
PricesHistoryInterval,
|
||||
PricesHistoryResponse,
|
||||
Rewards,
|
||||
RfqApproveOrderResponse,
|
||||
RfqCancelQuote,
|
||||
RfqCancelRequest,
|
||||
RfqCreateQuote,
|
||||
RfqCreateQuoteResponse,
|
||||
RfqCreateRequest,
|
||||
RfqCreateRequestResponse,
|
||||
RfqListResponse,
|
||||
RfqOrderExecutionRequest,
|
||||
RfqQuoteData,
|
||||
RfqQuotesParams,
|
||||
RfqRequestData,
|
||||
RfqRequestsParams,
|
||||
Side,
|
||||
SimplifiedMarket,
|
||||
SimplifiedMarketsResponse,
|
||||
|
||||
+280
@@ -1367,6 +1367,286 @@ pub struct Rewards {
|
||||
pub reward_epoch: Option<Decimal>,
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// CLOB API: Fee Rate + RFQ (Market Maker) Types
|
||||
// ============================================================================
|
||||
|
||||
/// Fee rate in basis points for a given token.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct FeeRateResponse {
|
||||
pub fee_rate_bps: u32,
|
||||
}
|
||||
|
||||
/// Create RFQ request (Requester).
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RfqCreateRequest {
|
||||
pub asset_in: String,
|
||||
pub asset_out: String,
|
||||
pub amount_in: String,
|
||||
pub amount_out: String,
|
||||
pub user_type: u8,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RfqCreateRequestResponse {
|
||||
pub request_id: String,
|
||||
pub expiry: u64,
|
||||
}
|
||||
|
||||
/// Cancel RFQ request (Requester).
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RfqCancelRequest {
|
||||
pub request_id: String,
|
||||
}
|
||||
|
||||
/// RFQ request list query parameters.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct RfqRequestsParams {
|
||||
pub offset: Option<String>,
|
||||
pub limit: Option<u32>,
|
||||
pub state: Option<String>,
|
||||
pub request_ids: Vec<String>,
|
||||
pub markets: Vec<String>,
|
||||
pub size_min: Option<Decimal>,
|
||||
pub size_max: Option<Decimal>,
|
||||
pub size_usdc_min: Option<Decimal>,
|
||||
pub size_usdc_max: Option<Decimal>,
|
||||
pub price_min: Option<Decimal>,
|
||||
pub price_max: Option<Decimal>,
|
||||
pub sort_by: Option<String>,
|
||||
pub sort_dir: Option<String>,
|
||||
}
|
||||
|
||||
impl RfqRequestsParams {
|
||||
pub fn to_query_params(&self) -> Vec<(String, String)> {
|
||||
let mut params = Vec::new();
|
||||
|
||||
if let Some(x) = &self.offset {
|
||||
params.push(("offset".to_string(), x.clone()));
|
||||
}
|
||||
if let Some(x) = self.limit {
|
||||
params.push(("limit".to_string(), x.to_string()));
|
||||
}
|
||||
if let Some(x) = &self.state {
|
||||
params.push(("state".to_string(), x.clone()));
|
||||
}
|
||||
for x in &self.request_ids {
|
||||
params.push(("requestIds[]".to_string(), x.clone()));
|
||||
}
|
||||
for x in &self.markets {
|
||||
params.push(("markets[]".to_string(), x.clone()));
|
||||
}
|
||||
|
||||
if let Some(x) = self.size_min {
|
||||
params.push(("sizeMin".to_string(), x.to_string()));
|
||||
}
|
||||
if let Some(x) = self.size_max {
|
||||
params.push(("sizeMax".to_string(), x.to_string()));
|
||||
}
|
||||
if let Some(x) = self.size_usdc_min {
|
||||
params.push(("sizeUsdcMin".to_string(), x.to_string()));
|
||||
}
|
||||
if let Some(x) = self.size_usdc_max {
|
||||
params.push(("sizeUsdcMax".to_string(), x.to_string()));
|
||||
}
|
||||
if let Some(x) = self.price_min {
|
||||
params.push(("priceMin".to_string(), x.to_string()));
|
||||
}
|
||||
if let Some(x) = self.price_max {
|
||||
params.push(("priceMax".to_string(), x.to_string()));
|
||||
}
|
||||
|
||||
if let Some(x) = &self.sort_by {
|
||||
params.push(("sortBy".to_string(), x.clone()));
|
||||
}
|
||||
if let Some(x) = &self.sort_dir {
|
||||
params.push(("sortDir".to_string(), x.clone()));
|
||||
}
|
||||
|
||||
params
|
||||
}
|
||||
}
|
||||
|
||||
/// RFQ request data.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RfqRequestData {
|
||||
pub request_id: String,
|
||||
pub user_address: String,
|
||||
pub proxy_address: String,
|
||||
pub condition: String,
|
||||
pub token: String,
|
||||
pub complement: String,
|
||||
pub side: Side,
|
||||
#[serde(deserialize_with = "crate::decode::deserializers::decimal_from_string")]
|
||||
pub size_in: Decimal,
|
||||
#[serde(deserialize_with = "crate::decode::deserializers::decimal_from_string")]
|
||||
pub size_out: Decimal,
|
||||
#[serde(deserialize_with = "crate::decode::deserializers::decimal_from_string")]
|
||||
pub price: Decimal,
|
||||
pub state: String,
|
||||
pub expiry: u64,
|
||||
}
|
||||
|
||||
/// Create RFQ quote (Quoter).
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RfqCreateQuote {
|
||||
pub request_id: String,
|
||||
pub asset_in: String,
|
||||
pub asset_out: String,
|
||||
pub amount_in: String,
|
||||
pub amount_out: String,
|
||||
pub user_type: u8,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RfqCreateQuoteResponse {
|
||||
pub quote_id: String,
|
||||
}
|
||||
|
||||
/// Cancel RFQ quote (Quoter).
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RfqCancelQuote {
|
||||
pub quote_id: String,
|
||||
}
|
||||
|
||||
/// RFQ quote list query parameters.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct RfqQuotesParams {
|
||||
pub offset: Option<String>,
|
||||
pub limit: Option<u32>,
|
||||
pub state: Option<String>,
|
||||
pub quote_ids: Vec<String>,
|
||||
pub request_ids: Vec<String>,
|
||||
pub markets: Vec<String>,
|
||||
pub size_min: Option<Decimal>,
|
||||
pub size_max: Option<Decimal>,
|
||||
pub size_usdc_min: Option<Decimal>,
|
||||
pub size_usdc_max: Option<Decimal>,
|
||||
pub price_min: Option<Decimal>,
|
||||
pub price_max: Option<Decimal>,
|
||||
pub sort_by: Option<String>,
|
||||
pub sort_dir: Option<String>,
|
||||
}
|
||||
|
||||
impl RfqQuotesParams {
|
||||
pub fn to_query_params(&self) -> Vec<(String, String)> {
|
||||
let mut params = Vec::new();
|
||||
|
||||
if let Some(x) = &self.offset {
|
||||
params.push(("offset".to_string(), x.clone()));
|
||||
}
|
||||
if let Some(x) = self.limit {
|
||||
params.push(("limit".to_string(), x.to_string()));
|
||||
}
|
||||
if let Some(x) = &self.state {
|
||||
params.push(("state".to_string(), x.clone()));
|
||||
}
|
||||
for x in &self.quote_ids {
|
||||
params.push(("quoteIds[]".to_string(), x.clone()));
|
||||
}
|
||||
for x in &self.request_ids {
|
||||
params.push(("requestIds[]".to_string(), x.clone()));
|
||||
}
|
||||
for x in &self.markets {
|
||||
params.push(("markets[]".to_string(), x.clone()));
|
||||
}
|
||||
|
||||
if let Some(x) = self.size_min {
|
||||
params.push(("sizeMin".to_string(), x.to_string()));
|
||||
}
|
||||
if let Some(x) = self.size_max {
|
||||
params.push(("sizeMax".to_string(), x.to_string()));
|
||||
}
|
||||
if let Some(x) = self.size_usdc_min {
|
||||
params.push(("sizeUsdcMin".to_string(), x.to_string()));
|
||||
}
|
||||
if let Some(x) = self.size_usdc_max {
|
||||
params.push(("sizeUsdcMax".to_string(), x.to_string()));
|
||||
}
|
||||
if let Some(x) = self.price_min {
|
||||
params.push(("priceMin".to_string(), x.to_string()));
|
||||
}
|
||||
if let Some(x) = self.price_max {
|
||||
params.push(("priceMax".to_string(), x.to_string()));
|
||||
}
|
||||
|
||||
if let Some(x) = &self.sort_by {
|
||||
params.push(("sortBy".to_string(), x.clone()));
|
||||
}
|
||||
if let Some(x) = &self.sort_dir {
|
||||
params.push(("sortDir".to_string(), x.clone()));
|
||||
}
|
||||
|
||||
params
|
||||
}
|
||||
}
|
||||
|
||||
/// RFQ quote data.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RfqQuoteData {
|
||||
pub quote_id: String,
|
||||
pub request_id: String,
|
||||
pub user_address: String,
|
||||
pub proxy_address: String,
|
||||
pub condition: String,
|
||||
pub token: String,
|
||||
pub complement: String,
|
||||
pub side: Side,
|
||||
#[serde(deserialize_with = "crate::decode::deserializers::decimal_from_string")]
|
||||
pub size_in: Decimal,
|
||||
#[serde(deserialize_with = "crate::decode::deserializers::decimal_from_string")]
|
||||
pub size_out: Decimal,
|
||||
#[serde(deserialize_with = "crate::decode::deserializers::decimal_from_string")]
|
||||
pub price: Decimal,
|
||||
pub match_type: String,
|
||||
pub state: String,
|
||||
}
|
||||
|
||||
/// Generic RFQ list response wrapper.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RfqListResponse<T> {
|
||||
pub data: Vec<T>,
|
||||
pub next_cursor: Option<String>,
|
||||
pub limit: u32,
|
||||
pub count: u32,
|
||||
}
|
||||
|
||||
/// RFQ order execution request (used for both accept + approve).
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RfqOrderExecutionRequest {
|
||||
pub request_id: String,
|
||||
pub quote_id: String,
|
||||
pub maker: String,
|
||||
pub signer: String,
|
||||
pub taker: String,
|
||||
pub expiration: u64,
|
||||
pub nonce: String,
|
||||
pub fee_rate_bps: String,
|
||||
pub side: String,
|
||||
pub token_id: String,
|
||||
pub maker_amount: String,
|
||||
pub taker_amount: String,
|
||||
pub signature_type: u8,
|
||||
pub signature: String,
|
||||
pub salt: u64,
|
||||
pub owner: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RfqApproveOrderResponse {
|
||||
pub trade_ids: Vec<String>,
|
||||
}
|
||||
|
||||
// For compatibility with reference implementation
|
||||
pub type ClientResult<T> = anyhow::Result<T>;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user