mirror of
https://github.com/floor-licker/polyfill-rs.git
synced 2026-08-17 22:48:07 +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
|
// Benchmarked optimal configuration: 512KB stream window
|
||||||
// Results: 309.3ms vs 349ms baseline (11.4% improvement)
|
// Results: 309.3ms vs 349ms baseline (11.4% improvement)
|
||||||
let optimized_client = reqwest::ClientBuilder::new()
|
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_adaptive_window(true)
|
||||||
.http2_initial_stream_window_size(512 * 1024) // 512KB - empirically optimal
|
.http2_initial_stream_window_size(512 * 1024) // 512KB - empirically optimal
|
||||||
.tcp_nodelay(true)
|
.tcp_nodelay(true)
|
||||||
.pool_max_idle_per_host(10)
|
.pool_max_idle_per_host(10)
|
||||||
.pool_idle_timeout(std::time::Duration::from_secs(90))
|
.pool_idle_timeout(std::time::Duration::from_secs(90))
|
||||||
.build()
|
.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
|
// Initialize DNS cache and pre-warm it
|
||||||
let dns_cache = tokio::runtime::Handle::try_current().ok().and_then(|_| {
|
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
|
/// Create a client optimized for co-located environments
|
||||||
pub fn new_colocated(host: &str) -> Self {
|
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(
|
let connection_manager = Some(std::sync::Arc::new(
|
||||||
crate::connection_manager::ConnectionManager::new(
|
crate::connection_manager::ConnectionManager::new(
|
||||||
@@ -159,7 +171,12 @@ impl ClobClient {
|
|||||||
|
|
||||||
/// Create a client optimized for internet connections
|
/// Create a client optimized for internet connections
|
||||||
pub fn new_internet(host: &str) -> Self {
|
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(
|
let connection_manager = Some(std::sync::Arc::new(
|
||||||
crate::connection_manager::ConnectionManager::new(
|
crate::connection_manager::ConnectionManager::new(
|
||||||
@@ -190,7 +207,12 @@ impl ClobClient {
|
|||||||
|
|
||||||
let order_builder = crate::orders::OrderBuilder::new(signer.clone(), None, None);
|
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
|
// Initialize infrastructure modules
|
||||||
let dns_cache = None; // Skip DNS cache for simplicity in this constructor
|
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 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
|
// Initialize infrastructure modules
|
||||||
let dns_cache = None; // Skip DNS cache for simplicity in this constructor
|
let dns_cache = None; // Skip DNS cache for simplicity in this constructor
|
||||||
@@ -610,6 +637,30 @@ impl ClobClient {
|
|||||||
Ok(tick_size)
|
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
|
/// Create a new API key
|
||||||
pub async fn create_api_key(&self, nonce: Option<U256>) -> Result<ApiCreds> {
|
pub async fn create_api_key(&self, nonce: Option<U256>) -> Result<ApiCreds> {
|
||||||
let signer = self
|
let signer = self
|
||||||
@@ -1722,6 +1773,389 @@ impl ClobClient {
|
|||||||
.map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None))
|
.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
|
/// Get sampling markets with pagination
|
||||||
pub async fn get_sampling_markets(
|
pub async fn get_sampling_markets(
|
||||||
&self,
|
&self,
|
||||||
@@ -1860,10 +2294,14 @@ pub type PolyfillClient = ClobClient;
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{ClobClient, OrderArgs as ClientOrderArgs};
|
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 crate::{ApiCredentials, PolyfillError};
|
||||||
use mockito::{Matcher, Server};
|
use mockito::{Matcher, Server};
|
||||||
use rust_decimal::Decimal;
|
use rust_decimal::Decimal;
|
||||||
|
use serde_json::json;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use tokio;
|
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")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
async fn test_client_creation() {
|
async fn test_client_creation() {
|
||||||
let client = create_test_client("https://test.example.com");
|
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.size, Decimal::ZERO);
|
||||||
assert_eq!(default_args.side, Side::BUY);
|
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]
|
#[tokio::test]
|
||||||
async fn test_connection_manager_creation() {
|
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());
|
let manager = ConnectionManager::new(client, "https://clob.polymarket.com".to_string());
|
||||||
assert!(!manager.is_running());
|
assert!(!manager.is_running());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_keepalive_start_stop() {
|
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());
|
let manager = ConnectionManager::new(client, "https://clob.polymarket.com".to_string());
|
||||||
|
|
||||||
manager.start_keepalive(Duration::from_secs(30)).await;
|
manager.start_keepalive(Duration::from_secs(30)).await;
|
||||||
|
|||||||
@@ -173,6 +173,19 @@ pub mod deserializers {
|
|||||||
_ => Ok(None),
|
_ => 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
|
/// Raw API response types for efficient parsing
|
||||||
|
|||||||
@@ -102,6 +102,7 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
#[ignore = "requires external DNS/network access"]
|
||||||
async fn test_dns_cache_resolve() {
|
async fn test_dns_cache_resolve() {
|
||||||
let cache = DnsCache::new().await.unwrap();
|
let cache = DnsCache::new().await.unwrap();
|
||||||
let ips = cache.resolve("clob.polymarket.com").await.unwrap();
|
let ips = cache.resolve("clob.polymarket.com").await.unwrap();
|
||||||
@@ -109,6 +110,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
#[ignore = "requires external DNS/network access"]
|
||||||
async fn test_dns_cache_prewarm() {
|
async fn test_dns_cache_prewarm() {
|
||||||
let cache = DnsCache::new().await.unwrap();
|
let cache = DnsCache::new().await.unwrap();
|
||||||
cache.prewarm("clob.polymarket.com").await.unwrap();
|
cache.prewarm("clob.polymarket.com").await.unwrap();
|
||||||
@@ -116,6 +118,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
#[ignore = "requires external DNS/network access"]
|
||||||
async fn test_dns_cache_clear() {
|
async fn test_dns_cache_clear() {
|
||||||
let cache = DnsCache::new().await.unwrap();
|
let cache = DnsCache::new().await.unwrap();
|
||||||
cache.prewarm("clob.polymarket.com").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)
|
/// Benchmarked configuration: 309.3ms vs 349ms baseline (11.4% faster)
|
||||||
pub fn create_optimized_client() -> Result<Client, reqwest::Error> {
|
pub fn create_optimized_client() -> Result<Client, reqwest::Error> {
|
||||||
ClientBuilder::new()
|
ClientBuilder::new()
|
||||||
|
// Avoid reading OS proxy settings (can be slow and/or unavailable in some sandboxed envs)
|
||||||
|
.no_proxy()
|
||||||
// Connection pooling optimizations - aggressive reuse
|
// Connection pooling optimizations - aggressive reuse
|
||||||
.pool_max_idle_per_host(10) // Keep connections alive
|
.pool_max_idle_per_host(10) // Keep connections alive
|
||||||
.pool_idle_timeout(Duration::from_secs(90)) // Longer reuse window
|
.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)
|
/// (even more aggressive settings for when you're close to the exchange)
|
||||||
pub fn create_colocated_client() -> Result<Client, reqwest::Error> {
|
pub fn create_colocated_client() -> Result<Client, reqwest::Error> {
|
||||||
ClientBuilder::new()
|
ClientBuilder::new()
|
||||||
|
// Avoid reading OS proxy settings (can be slow and/or unavailable in some sandboxed envs)
|
||||||
|
.no_proxy()
|
||||||
// More aggressive connection pooling
|
// More aggressive connection pooling
|
||||||
.pool_max_idle_per_host(20) // More connections
|
.pool_max_idle_per_host(20) // More connections
|
||||||
.pool_idle_timeout(Duration::from_secs(60)) // Longer reuse
|
.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)
|
/// (more conservative settings for internet connections)
|
||||||
pub fn create_internet_client() -> Result<Client, reqwest::Error> {
|
pub fn create_internet_client() -> Result<Client, reqwest::Error> {
|
||||||
ClientBuilder::new()
|
ClientBuilder::new()
|
||||||
|
// Avoid reading OS proxy settings (can be slow and/or unavailable in some sandboxed envs)
|
||||||
|
.no_proxy()
|
||||||
// Conservative connection pooling
|
// Conservative connection pooling
|
||||||
.pool_max_idle_per_host(5)
|
.pool_max_idle_per_host(5)
|
||||||
.pool_idle_timeout(Duration::from_secs(90))
|
.pool_idle_timeout(Duration::from_secs(90))
|
||||||
|
|||||||
+14
@@ -99,6 +99,7 @@ pub use crate::types::{
|
|||||||
BookParams,
|
BookParams,
|
||||||
ClientConfig,
|
ClientConfig,
|
||||||
ClientResult,
|
ClientResult,
|
||||||
|
FeeRateResponse,
|
||||||
FillEvent,
|
FillEvent,
|
||||||
Market,
|
Market,
|
||||||
MarketSnapshot,
|
MarketSnapshot,
|
||||||
@@ -120,6 +121,19 @@ pub use crate::types::{
|
|||||||
PricesHistoryInterval,
|
PricesHistoryInterval,
|
||||||
PricesHistoryResponse,
|
PricesHistoryResponse,
|
||||||
Rewards,
|
Rewards,
|
||||||
|
RfqApproveOrderResponse,
|
||||||
|
RfqCancelQuote,
|
||||||
|
RfqCancelRequest,
|
||||||
|
RfqCreateQuote,
|
||||||
|
RfqCreateQuoteResponse,
|
||||||
|
RfqCreateRequest,
|
||||||
|
RfqCreateRequestResponse,
|
||||||
|
RfqListResponse,
|
||||||
|
RfqOrderExecutionRequest,
|
||||||
|
RfqQuoteData,
|
||||||
|
RfqQuotesParams,
|
||||||
|
RfqRequestData,
|
||||||
|
RfqRequestsParams,
|
||||||
Side,
|
Side,
|
||||||
SimplifiedMarket,
|
SimplifiedMarket,
|
||||||
SimplifiedMarketsResponse,
|
SimplifiedMarketsResponse,
|
||||||
|
|||||||
+280
@@ -1367,6 +1367,286 @@ pub struct Rewards {
|
|||||||
pub reward_epoch: Option<Decimal>,
|
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
|
// For compatibility with reference implementation
|
||||||
pub type ClientResult<T> = anyhow::Result<T>;
|
pub type ClientResult<T> = anyhow::Result<T>;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user