mirror of
https://github.com/floor-licker/polyfill-rs.git
synced 2026-08-15 05:28:07 +00:00
Merge pull request #14 from onsails/batch
Batch orders, GTC/GTD/FOK/FAK order types, typed responses, proxy-wallet support, per-page pagination
This commit is contained in:
+9
-1
@@ -623,11 +623,19 @@ impl PolyfillDemo {
|
||||
side: Side::BUY,
|
||||
size: dec!(50.0),
|
||||
price: dec!(0.75),
|
||||
status: Some("MATCHED".to_string()),
|
||||
status: TradeMessageStatus::Matched,
|
||||
msg_type: None,
|
||||
last_update: None,
|
||||
matchtime: None,
|
||||
timestamp: None,
|
||||
outcome: None,
|
||||
owner: None,
|
||||
trade_owner: None,
|
||||
taker_order_id: None,
|
||||
maker_orders: vec![],
|
||||
fee_rate_bps: None,
|
||||
transaction_hash: None,
|
||||
trader_side: None,
|
||||
}),
|
||||
];
|
||||
|
||||
|
||||
+403
-152
@@ -9,15 +9,13 @@ use crate::http_config::{
|
||||
create_colocated_client, create_internet_client, create_optimized_client, prewarm_connections,
|
||||
};
|
||||
use crate::types::{OrderOptions, PostOrder, SignedOrderRequest};
|
||||
use alloy_primitives::U256;
|
||||
use alloy_primitives::{Address, U256};
|
||||
use alloy_signer_local::PrivateKeySigner;
|
||||
use reqwest::header::HeaderName;
|
||||
use reqwest::Client;
|
||||
use reqwest::{Method, RequestBuilder};
|
||||
use rust_decimal::prelude::FromPrimitive;
|
||||
use rust_decimal::Decimal;
|
||||
use serde_json::Value;
|
||||
use std::str::FromStr;
|
||||
|
||||
// Re-export types for compatibility
|
||||
pub use crate::types::{ApiCredentials as ApiCreds, OrderType, Side};
|
||||
@@ -238,17 +236,23 @@ impl ClobClient {
|
||||
}
|
||||
|
||||
/// Create a client with L2 headers (for API key authentication)
|
||||
///
|
||||
/// `sig_type` and `funder` are optional parameters for proxy wallet support.
|
||||
/// When using a Polymarket proxy wallet, pass `SigType::PolyProxy` and the
|
||||
/// proxy wallet address as `funder`.
|
||||
pub fn with_l2_headers(
|
||||
host: &str,
|
||||
private_key: &str,
|
||||
chain_id: u64,
|
||||
api_creds: ApiCreds,
|
||||
sig_type: Option<crate::orders::SigType>,
|
||||
funder: Option<Address>,
|
||||
) -> Self {
|
||||
let signer = private_key
|
||||
.parse::<PrivateKeySigner>()
|
||||
.expect("Invalid private key");
|
||||
|
||||
let order_builder = crate::orders::OrderBuilder::new(signer.clone(), None, None);
|
||||
let order_builder = crate::orders::OrderBuilder::new(signer.clone(), sig_type, funder);
|
||||
|
||||
let http_client = create_optimized_client().unwrap_or_else(|_| {
|
||||
reqwest::ClientBuilder::new()
|
||||
@@ -623,18 +627,8 @@ impl ClobClient {
|
||||
));
|
||||
}
|
||||
|
||||
let tick_size_response: Value = response.json().await?;
|
||||
let tick_size = tick_size_response["minimum_tick_size"]
|
||||
.as_str()
|
||||
.and_then(|s| Decimal::from_str(s).ok())
|
||||
.or_else(|| {
|
||||
tick_size_response["minimum_tick_size"]
|
||||
.as_f64()
|
||||
.map(|f| Decimal::from_f64(f).unwrap_or(Decimal::ZERO))
|
||||
})
|
||||
.ok_or_else(|| PolyfillError::parse("Invalid tick size format", None))?;
|
||||
|
||||
Ok(tick_size)
|
||||
let tick_size_response: TickSizeResponse = response.json().await?;
|
||||
Ok(tick_size_response.minimum_tick_size)
|
||||
}
|
||||
|
||||
/// Get maker fee rate (in bps) for a token
|
||||
@@ -658,7 +652,7 @@ impl ClobClient {
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None))?;
|
||||
Ok(fee_rate.fee_rate_bps)
|
||||
Ok(fee_rate.base_fee)
|
||||
}
|
||||
|
||||
/// Create a new API key
|
||||
@@ -820,12 +814,8 @@ impl ClobClient {
|
||||
));
|
||||
}
|
||||
|
||||
let neg_risk_response: Value = response.json().await?;
|
||||
let neg_risk = neg_risk_response["neg_risk"]
|
||||
.as_bool()
|
||||
.ok_or_else(|| PolyfillError::parse("Invalid neg risk format", None))?;
|
||||
|
||||
Ok(neg_risk)
|
||||
let neg_risk_response: NegRiskResponse = response.json().await?;
|
||||
Ok(neg_risk_response.neg_risk)
|
||||
}
|
||||
|
||||
/// Resolve tick size for an order
|
||||
@@ -953,7 +943,7 @@ impl ClobClient {
|
||||
.collect(),
|
||||
};
|
||||
|
||||
order_builder.calculate_market_price(&levels, amount)
|
||||
order_builder.calculate_market_price(side, &levels, amount)
|
||||
}
|
||||
|
||||
/// Create a market order
|
||||
@@ -974,7 +964,7 @@ impl ClobClient {
|
||||
|
||||
let extras = extras.unwrap_or_default();
|
||||
let price = self
|
||||
.calculate_market_price(&order_args.token_id, Side::BUY, order_args.amount)
|
||||
.calculate_market_price(&order_args.token_id, order_args.side, order_args.amount)
|
||||
.await?;
|
||||
|
||||
if !self.is_price_in_range(
|
||||
@@ -1000,7 +990,7 @@ impl ClobClient {
|
||||
&self,
|
||||
order: SignedOrderRequest,
|
||||
order_type: OrderType,
|
||||
) -> Result<Value> {
|
||||
) -> Result<crate::types::PostOrderResponse> {
|
||||
let signer = self
|
||||
.signer
|
||||
.as_ref()
|
||||
@@ -1029,17 +1019,106 @@ impl ClobClient {
|
||||
return Err(PolyfillError::api(status, message));
|
||||
}
|
||||
|
||||
Ok(response.json::<Value>().await?)
|
||||
Ok(response.json::<crate::types::PostOrderResponse>().await?)
|
||||
}
|
||||
|
||||
/// Create and post an order in one call
|
||||
pub async fn create_and_post_order(&self, order_args: &OrderArgs) -> Result<Value> {
|
||||
/// Post multiple orders to the exchange in a single request
|
||||
pub async fn post_orders(
|
||||
&self,
|
||||
orders: Vec<SignedOrderRequest>,
|
||||
order_type: OrderType,
|
||||
) -> Result<Vec<crate::types::PostOrderResponse>> {
|
||||
if orders.is_empty() {
|
||||
return Err(PolyfillError::validation("orders cannot be empty"));
|
||||
}
|
||||
if orders.len() > 15 {
|
||||
return Err(PolyfillError::validation(
|
||||
"orders cannot exceed 15 items per batch",
|
||||
));
|
||||
}
|
||||
|
||||
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 body: Vec<PostOrder> = orders
|
||||
.into_iter()
|
||||
.map(|order| PostOrder::new(order, api_creds.api_key.clone(), order_type))
|
||||
.collect();
|
||||
|
||||
let headers = create_l2_headers(signer, api_creds, "POST", "/orders", Some(&body))?;
|
||||
let req = self.create_request_with_headers(Method::POST, "/orders", headers.into_iter());
|
||||
|
||||
let response = req.json(&body).send().await?;
|
||||
if !response.status().is_success() {
|
||||
let status = response.status().as_u16();
|
||||
let body = response.text().await.unwrap_or_default();
|
||||
let message = if body.is_empty() {
|
||||
"Failed to post orders".to_string()
|
||||
} else {
|
||||
format!("Failed to post orders: {}", body)
|
||||
};
|
||||
return Err(PolyfillError::api(status, message));
|
||||
}
|
||||
|
||||
Ok(response
|
||||
.json::<Vec<crate::types::PostOrderResponse>>()
|
||||
.await?)
|
||||
}
|
||||
|
||||
/// Create and post an order in one call with an explicit order type
|
||||
pub async fn create_and_post_order_with_type(
|
||||
&self,
|
||||
order_args: &OrderArgs,
|
||||
order_type: OrderType,
|
||||
) -> Result<crate::types::PostOrderResponse> {
|
||||
let order = self.create_order(order_args, None, None, None).await?;
|
||||
self.post_order(order, OrderType::GTC).await
|
||||
self.post_order(order, order_type).await
|
||||
}
|
||||
|
||||
/// Create and post an order in one call (defaults to GTC)
|
||||
pub async fn create_and_post_order(
|
||||
&self,
|
||||
order_args: &OrderArgs,
|
||||
) -> Result<crate::types::PostOrderResponse> {
|
||||
self.create_and_post_order_with_type(order_args, OrderType::GTC)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Create and post multiple orders in one call with an explicit order type
|
||||
pub async fn create_and_post_orders_with_type(
|
||||
&self,
|
||||
order_args: &[OrderArgs],
|
||||
order_type: OrderType,
|
||||
) -> Result<Vec<crate::types::PostOrderResponse>> {
|
||||
if order_args.is_empty() {
|
||||
return Err(PolyfillError::validation("order_args cannot be empty"));
|
||||
}
|
||||
|
||||
let mut orders = Vec::with_capacity(order_args.len());
|
||||
for args in order_args {
|
||||
orders.push(self.create_order(args, None, None, None).await?);
|
||||
}
|
||||
|
||||
self.post_orders(orders, order_type).await
|
||||
}
|
||||
|
||||
/// Create and post multiple orders in one call (defaults to GTC)
|
||||
pub async fn create_and_post_orders(
|
||||
&self,
|
||||
order_args: &[OrderArgs],
|
||||
) -> Result<Vec<crate::types::PostOrderResponse>> {
|
||||
self.create_and_post_orders_with_type(order_args, OrderType::GTC)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Cancel an order
|
||||
pub async fn cancel(&self, order_id: &str) -> Result<Value> {
|
||||
pub async fn cancel(&self, order_id: &str) -> Result<crate::types::CancelOrdersResponse> {
|
||||
let signer = self
|
||||
.signer
|
||||
.as_ref()
|
||||
@@ -1062,11 +1141,16 @@ impl ClobClient {
|
||||
));
|
||||
}
|
||||
|
||||
Ok(response.json::<Value>().await?)
|
||||
Ok(response
|
||||
.json::<crate::types::CancelOrdersResponse>()
|
||||
.await?)
|
||||
}
|
||||
|
||||
/// Cancel multiple orders
|
||||
pub async fn cancel_orders(&self, order_ids: &[String]) -> Result<Value> {
|
||||
pub async fn cancel_orders(
|
||||
&self,
|
||||
order_ids: &[String],
|
||||
) -> Result<crate::types::CancelOrdersResponse> {
|
||||
let signer = self
|
||||
.signer
|
||||
.as_ref()
|
||||
@@ -1087,11 +1171,13 @@ impl ClobClient {
|
||||
));
|
||||
}
|
||||
|
||||
Ok(response.json::<Value>().await?)
|
||||
Ok(response
|
||||
.json::<crate::types::CancelOrdersResponse>()
|
||||
.await?)
|
||||
}
|
||||
|
||||
/// Cancel all orders
|
||||
pub async fn cancel_all(&self) -> Result<Value> {
|
||||
pub async fn cancel_all(&self) -> Result<crate::types::CancelOrdersResponse> {
|
||||
let signer = self
|
||||
.signer
|
||||
.as_ref()
|
||||
@@ -1101,7 +1187,7 @@ impl ClobClient {
|
||||
.as_ref()
|
||||
.ok_or_else(|| PolyfillError::auth("API credentials not set"))?;
|
||||
|
||||
let headers = create_l2_headers::<Value>(signer, api_creds, "DELETE", "/cancel-all", None)?;
|
||||
let headers = create_l2_headers::<()>(signer, api_creds, "DELETE", "/cancel-all", None)?;
|
||||
let req =
|
||||
self.create_request_with_headers(Method::DELETE, "/cancel-all", headers.into_iter());
|
||||
|
||||
@@ -1113,22 +1199,20 @@ impl ClobClient {
|
||||
));
|
||||
}
|
||||
|
||||
Ok(response.json::<Value>().await?)
|
||||
Ok(response
|
||||
.json::<crate::types::CancelOrdersResponse>()
|
||||
.await?)
|
||||
}
|
||||
|
||||
/// Get open orders with optional filtering
|
||||
/// Fetch a single page of orders.
|
||||
///
|
||||
/// This retrieves all open orders for the authenticated user. You can filter by:
|
||||
/// - Order ID (exact match)
|
||||
/// - Asset/Token ID (all orders for a specific token)
|
||||
/// - Market ID (all orders for a specific market)
|
||||
///
|
||||
/// The response includes order status, fill information, and timestamps.
|
||||
pub async fn get_orders(
|
||||
/// Returns `(page_data, next_cursor)`. `next_cursor` is `None` when there are
|
||||
/// no more pages.
|
||||
pub async fn get_orders_page(
|
||||
&self,
|
||||
params: Option<&crate::types::OpenOrderParams>,
|
||||
next_cursor: Option<&str>,
|
||||
) -> Result<Vec<crate::types::OpenOrder>> {
|
||||
) -> Result<(Vec<crate::types::OpenOrder>, Option<String>)> {
|
||||
let signer = self
|
||||
.signer
|
||||
.as_ref()
|
||||
@@ -1148,55 +1232,148 @@ impl ClobClient {
|
||||
Some(p) => p.to_query_params(),
|
||||
};
|
||||
|
||||
let mut next_cursor = next_cursor.unwrap_or("MA==").to_string(); // INITIAL_CURSOR
|
||||
let cursor = next_cursor.unwrap_or("MA==");
|
||||
|
||||
let req = self
|
||||
.http_client
|
||||
.request(method.clone(), format!("{}{}", self.base_url, endpoint))
|
||||
.query(&query_params)
|
||||
.query(&[("next_cursor", cursor)]);
|
||||
|
||||
let r = headers
|
||||
.into_iter()
|
||||
.fold(req, |r, (k, v)| r.header(HeaderName::from_static(k), v));
|
||||
|
||||
let resp = r
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?
|
||||
.json::<Value>()
|
||||
.await
|
||||
.map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None))?;
|
||||
|
||||
let new_cursor = resp["next_cursor"]
|
||||
.as_str()
|
||||
.ok_or_else(|| PolyfillError::parse("Failed to parse next cursor".to_string(), None))?
|
||||
.to_owned();
|
||||
|
||||
let results = resp["data"].clone();
|
||||
let orders =
|
||||
serde_json::from_value::<Vec<crate::types::OpenOrder>>(results).map_err(|e| {
|
||||
PolyfillError::parse(
|
||||
format!("Failed to parse data from order response: {}", e),
|
||||
None,
|
||||
)
|
||||
})?;
|
||||
|
||||
let next = if new_cursor == "LTE=" {
|
||||
None
|
||||
} else {
|
||||
Some(new_cursor)
|
||||
};
|
||||
|
||||
Ok((orders, next))
|
||||
}
|
||||
|
||||
/// Get open orders with optional filtering
|
||||
///
|
||||
/// This retrieves all open orders for the authenticated user. You can filter by:
|
||||
/// - Order ID (exact match)
|
||||
/// - Asset/Token ID (all orders for a specific token)
|
||||
/// - Market ID (all orders for a specific market)
|
||||
///
|
||||
/// The response includes order status, fill information, and timestamps.
|
||||
pub async fn get_orders(
|
||||
&self,
|
||||
params: Option<&crate::types::OpenOrderParams>,
|
||||
next_cursor: Option<&str>,
|
||||
) -> Result<Vec<crate::types::OpenOrder>> {
|
||||
let mut cursor = next_cursor.map(|s| s.to_owned());
|
||||
let mut output = Vec::new();
|
||||
|
||||
while next_cursor != "LTE=" {
|
||||
// END_CURSOR
|
||||
let req = self
|
||||
.http_client
|
||||
.request(method.clone(), format!("{}{}", self.base_url, endpoint))
|
||||
.query(&query_params)
|
||||
.query(&[("next_cursor", &next_cursor)]);
|
||||
|
||||
let r = headers
|
||||
.clone()
|
||||
.into_iter()
|
||||
.fold(req, |r, (k, v)| r.header(HeaderName::from_static(k), v));
|
||||
|
||||
let resp = r
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?
|
||||
.json::<Value>()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
PolyfillError::parse(format!("Failed to parse response: {}", e), None)
|
||||
})?;
|
||||
|
||||
let new_cursor = resp["next_cursor"]
|
||||
.as_str()
|
||||
.ok_or_else(|| {
|
||||
PolyfillError::parse("Failed to parse next cursor".to_string(), None)
|
||||
})?
|
||||
.to_owned();
|
||||
|
||||
next_cursor = new_cursor;
|
||||
|
||||
let results = resp["data"].clone();
|
||||
let orders =
|
||||
serde_json::from_value::<Vec<crate::types::OpenOrder>>(results).map_err(|e| {
|
||||
PolyfillError::parse(
|
||||
format!("Failed to parse data from order response: {}", e),
|
||||
None,
|
||||
)
|
||||
})?;
|
||||
output.extend(orders);
|
||||
loop {
|
||||
let (page, next) = self.get_orders_page(params, cursor.as_deref()).await?;
|
||||
output.extend(page);
|
||||
match next {
|
||||
Some(c) => cursor = Some(c),
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
/// Fetch a single page of trades.
|
||||
///
|
||||
/// Returns `(page_data, next_cursor)`. `next_cursor` is `None` when there are
|
||||
/// no more pages.
|
||||
pub async fn get_trades_page(
|
||||
&self,
|
||||
params: Option<&crate::types::TradeParams>,
|
||||
next_cursor: Option<&str>,
|
||||
) -> Result<(Vec<crate::types::TradeResponse>, Option<String>)> {
|
||||
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 = "/data/trades";
|
||||
let headers =
|
||||
create_l2_headers::<Value>(signer, api_creds, method.as_str(), endpoint, None)?;
|
||||
|
||||
let query_params = match params {
|
||||
None => Vec::new(),
|
||||
Some(p) => p.to_query_params(),
|
||||
};
|
||||
|
||||
let cursor = next_cursor.unwrap_or("MA==");
|
||||
|
||||
let req = self
|
||||
.http_client
|
||||
.request(method.clone(), format!("{}{}", self.base_url, endpoint))
|
||||
.query(&query_params)
|
||||
.query(&[("next_cursor", cursor)]);
|
||||
|
||||
let r = headers
|
||||
.into_iter()
|
||||
.fold(req, |r, (k, v)| r.header(HeaderName::from_static(k), v));
|
||||
|
||||
let resp = r
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?
|
||||
.json::<Value>()
|
||||
.await
|
||||
.map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None))?;
|
||||
|
||||
let new_cursor = resp["next_cursor"]
|
||||
.as_str()
|
||||
.ok_or_else(|| PolyfillError::parse("Failed to parse next cursor".to_string(), None))?
|
||||
.to_owned();
|
||||
|
||||
let results = resp["data"].clone();
|
||||
let trades =
|
||||
serde_json::from_value::<Vec<crate::types::TradeResponse>>(results).map_err(|e| {
|
||||
PolyfillError::parse(
|
||||
format!("Failed to parse data from trades response: {}", e),
|
||||
None,
|
||||
)
|
||||
})?;
|
||||
|
||||
let next = if new_cursor == "LTE=" {
|
||||
None
|
||||
} else {
|
||||
Some(new_cursor)
|
||||
};
|
||||
|
||||
Ok((trades, next))
|
||||
}
|
||||
|
||||
/// Get trade history with optional filtering
|
||||
///
|
||||
/// This retrieves historical trades for the authenticated user. You can filter by:
|
||||
@@ -1211,63 +1388,19 @@ impl ClobClient {
|
||||
&self,
|
||||
trade_params: Option<&crate::types::TradeParams>,
|
||||
next_cursor: Option<&str>,
|
||||
) -> Result<Vec<Value>> {
|
||||
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 = "/data/trades";
|
||||
let headers =
|
||||
create_l2_headers::<Value>(signer, api_creds, method.as_str(), endpoint, None)?;
|
||||
|
||||
let query_params = match trade_params {
|
||||
None => Vec::new(),
|
||||
Some(p) => p.to_query_params(),
|
||||
};
|
||||
|
||||
let mut next_cursor = next_cursor.unwrap_or("MA==").to_string(); // INITIAL_CURSOR
|
||||
) -> Result<Vec<crate::types::TradeResponse>> {
|
||||
let mut cursor = next_cursor.map(|s| s.to_owned());
|
||||
let mut output = Vec::new();
|
||||
|
||||
while next_cursor != "LTE=" {
|
||||
// END_CURSOR
|
||||
let req = self
|
||||
.http_client
|
||||
.request(method.clone(), format!("{}{}", self.base_url, endpoint))
|
||||
.query(&query_params)
|
||||
.query(&[("next_cursor", &next_cursor)]);
|
||||
|
||||
let r = headers
|
||||
.clone()
|
||||
.into_iter()
|
||||
.fold(req, |r, (k, v)| r.header(HeaderName::from_static(k), v));
|
||||
|
||||
let resp = r
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?
|
||||
.json::<Value>()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
PolyfillError::parse(format!("Failed to parse response: {}", e), None)
|
||||
})?;
|
||||
|
||||
let new_cursor = resp["next_cursor"]
|
||||
.as_str()
|
||||
.ok_or_else(|| {
|
||||
PolyfillError::parse("Failed to parse next cursor".to_string(), None)
|
||||
})?
|
||||
.to_owned();
|
||||
|
||||
next_cursor = new_cursor;
|
||||
|
||||
let results = resp["data"].clone();
|
||||
output.push(results);
|
||||
loop {
|
||||
let (page, next) = self
|
||||
.get_trades_page(trade_params, cursor.as_deref())
|
||||
.await?;
|
||||
output.extend(page);
|
||||
match next {
|
||||
Some(c) => cursor = Some(c),
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
|
||||
Ok(output)
|
||||
@@ -1565,7 +1698,7 @@ impl ClobClient {
|
||||
&self,
|
||||
market: Option<&str>,
|
||||
asset_id: Option<&str>,
|
||||
) -> Result<Value> {
|
||||
) -> Result<crate::types::CancelOrdersResponse> {
|
||||
let signer = self
|
||||
.signer
|
||||
.as_ref()
|
||||
@@ -1599,7 +1732,7 @@ impl ClobClient {
|
||||
.map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
|
||||
|
||||
response
|
||||
.json::<Value>()
|
||||
.json::<crate::types::CancelOrdersResponse>()
|
||||
.await
|
||||
.map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None))
|
||||
}
|
||||
@@ -1721,12 +1854,12 @@ impl ClobClient {
|
||||
.await
|
||||
.map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
|
||||
|
||||
let result: Value = response
|
||||
let result: crate::types::OrderScoringResponse = response
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None))?;
|
||||
|
||||
Ok(result["scoring"].as_bool().unwrap_or(false))
|
||||
Ok(result.scoring)
|
||||
}
|
||||
|
||||
/// Check if multiple orders are scoring
|
||||
@@ -2276,9 +2409,11 @@ impl ClobClient {
|
||||
|
||||
// Re-export types from the canonical location in types.rs
|
||||
pub use crate::types::{
|
||||
ExtraOrderArgs, Market, MarketOrderArgs, MarketsResponse, MidpointResponse, NegRiskResponse,
|
||||
OrderBookSummary, OrderSummary, PriceResponse, PricesHistoryInterval, PricesHistoryResponse,
|
||||
Rewards, SpreadResponse, TickSizeResponse, Token,
|
||||
CancelOrdersResponse, ExtraOrderArgs, MakerOrder, Market, MarketOrderArgs, MarketsResponse,
|
||||
MidpointResponse, NegRiskResponse, OrderBookSummary, OrderScoringResponse, OrderSummary,
|
||||
PostOrderResponse, PriceHistoryPoint, PriceResponse, PricesHistoryInterval,
|
||||
PricesHistoryResponse, Rewards, SpreadResponse, TickSizeResponse, Token, TradeResponse,
|
||||
TraderSide,
|
||||
};
|
||||
|
||||
// Compatibility types that need to stay in client.rs
|
||||
@@ -2330,9 +2465,29 @@ mod tests {
|
||||
"0x1234567890123456789012345678901234567890123456789012345678901234",
|
||||
137,
|
||||
api_creds,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
fn sample_signed_order() -> crate::types::SignedOrderRequest {
|
||||
crate::types::SignedOrderRequest {
|
||||
salt: 1,
|
||||
maker: "0x0000000000000000000000000000000000000000".to_string(),
|
||||
signer: "0x0000000000000000000000000000000000000000".to_string(),
|
||||
taker: "0x0000000000000000000000000000000000000000".to_string(),
|
||||
token_id: "123".to_string(),
|
||||
maker_amount: "100".to_string(),
|
||||
taker_amount: "50".to_string(),
|
||||
expiration: "0".to_string(),
|
||||
nonce: "0".to_string(),
|
||||
fee_rate_bps: "0".to_string(),
|
||||
side: "BUY".to_string(),
|
||||
signature_type: 0,
|
||||
signature: "0xdeadbeef".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_client_creation() {
|
||||
let client = create_test_client("https://test.example.com");
|
||||
@@ -2362,6 +2517,8 @@ mod tests {
|
||||
"0x1234567890123456789012345678901234567890123456789012345678901234",
|
||||
137,
|
||||
api_creds.clone(),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
assert_eq!(client.base_url, "https://test.example.com");
|
||||
@@ -2616,7 +2773,7 @@ mod tests {
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_get_prices_history_interval_success() {
|
||||
let mut server = Server::new_async().await;
|
||||
let mock_response = r#"{"history":[{"t":1}]}"#;
|
||||
let mock_response = r#"{"history":[{"t":1,"p":"0.52"}]}"#;
|
||||
|
||||
let mock = server
|
||||
.mock("GET", "/prices-history")
|
||||
@@ -2639,6 +2796,8 @@ mod tests {
|
||||
|
||||
mock.assert_async().await;
|
||||
assert_eq!(response.history.len(), 1);
|
||||
assert_eq!(response.history[0].t, 1);
|
||||
assert_eq!(response.history[0].p, Decimal::from_str("0.52").unwrap());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
@@ -2874,6 +3033,98 @@ mod tests {
|
||||
assert_eq!(timestamp, 1234567890);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_post_orders_batch_success() {
|
||||
let mut server = Server::new_async().await;
|
||||
let mock = server
|
||||
.mock("POST", "/orders")
|
||||
.with_status(200)
|
||||
.with_header("content-type", "application/json")
|
||||
.with_body(r#"[{"success":true,"orderID":"a"},{"success":true,"orderID":"b"}]"#)
|
||||
.create_async()
|
||||
.await;
|
||||
|
||||
let client = create_test_client_with_l2_auth(&server.url());
|
||||
let signed_order = sample_signed_order();
|
||||
|
||||
let result = client
|
||||
.post_orders(
|
||||
vec![signed_order.clone(), signed_order],
|
||||
crate::types::OrderType::GTC,
|
||||
)
|
||||
.await;
|
||||
|
||||
mock.assert_async().await;
|
||||
assert!(result.is_ok());
|
||||
let orders = result.unwrap();
|
||||
assert_eq!(orders.len(), 2);
|
||||
assert!(orders.iter().all(|o| o.success));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_post_orders_batch_empty_validation() {
|
||||
let client = create_test_client_with_l2_auth("https://test.example.com");
|
||||
let result = client
|
||||
.post_orders(Vec::new(), crate::types::OrderType::GTC)
|
||||
.await;
|
||||
assert!(matches!(result, Err(PolyfillError::Validation { .. })));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_post_orders_batch_too_many_validation() {
|
||||
let client = create_test_client_with_l2_auth("https://test.example.com");
|
||||
let orders = (0..16).map(|_| sample_signed_order()).collect::<Vec<_>>();
|
||||
|
||||
let result = client
|
||||
.post_orders(orders, crate::types::OrderType::GTC)
|
||||
.await;
|
||||
assert!(matches!(result, Err(PolyfillError::Validation { .. })));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_post_order_supports_fak() {
|
||||
let mut server = Server::new_async().await;
|
||||
let mock = server
|
||||
.mock("POST", "/order")
|
||||
.match_body(Matcher::Regex(r#""orderType":"FAK""#.to_string()))
|
||||
.with_status(200)
|
||||
.with_header("content-type", "application/json")
|
||||
.with_body(r#"{"success":true,"orderID":"a"}"#)
|
||||
.create_async()
|
||||
.await;
|
||||
|
||||
let client = create_test_client_with_l2_auth(&server.url());
|
||||
let result = client
|
||||
.post_order(sample_signed_order(), crate::types::OrderType::FAK)
|
||||
.await;
|
||||
|
||||
mock.assert_async().await;
|
||||
assert!(result.is_ok());
|
||||
assert!(result.unwrap().success);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_post_orders_supports_fak() {
|
||||
let mut server = Server::new_async().await;
|
||||
let mock = server
|
||||
.mock("POST", "/orders")
|
||||
.match_body(Matcher::Regex(r#""orderType":"FAK""#.to_string()))
|
||||
.with_status(200)
|
||||
.with_header("content-type", "application/json")
|
||||
.with_body(r#"[{"success":true,"orderID":"a"}]"#)
|
||||
.create_async()
|
||||
.await;
|
||||
|
||||
let client = create_test_client_with_l2_auth(&server.url());
|
||||
let result = client
|
||||
.post_orders(vec![sample_signed_order()], crate::types::OrderType::FAK)
|
||||
.await;
|
||||
|
||||
mock.assert_async().await;
|
||||
assert!(result.is_ok());
|
||||
assert!(result.unwrap()[0].success);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_create_or_derive_api_key() {
|
||||
let mut server = Server::new_async().await;
|
||||
@@ -3035,7 +3286,7 @@ mod tests {
|
||||
.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}"#)
|
||||
.with_body(r#"{"base_fee":1000}"#)
|
||||
.create_async()
|
||||
.await;
|
||||
|
||||
|
||||
+149
@@ -663,4 +663,153 @@ mod tests {
|
||||
let results: Vec<serde_json::Value> = decoder.parse_json_stream(data).unwrap();
|
||||
assert_eq!(results.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_trade_message_full_payload() {
|
||||
use crate::types::{StreamMessage, TradeMessageStatus, TradeMessageType, TraderSide};
|
||||
|
||||
let json = r#"{
|
||||
"event_type": "trade",
|
||||
"id": "trade-001",
|
||||
"market": "0xabc123",
|
||||
"asset_id": "asset-xyz",
|
||||
"side": "BUY",
|
||||
"size": "100.5",
|
||||
"price": "0.65",
|
||||
"status": "MATCHED",
|
||||
"type": "TRADE",
|
||||
"last_update": "1700000000",
|
||||
"match_time": "1700000001",
|
||||
"timestamp": "1700000002",
|
||||
"outcome": "Yes",
|
||||
"owner": "owner-key-123",
|
||||
"trade_owner": "trader-key-456",
|
||||
"taker_order_id": "taker-order-789",
|
||||
"maker_orders": [
|
||||
{
|
||||
"order_id": "maker-order-1",
|
||||
"owner": "maker-owner-1",
|
||||
"matched_amount": "50.25",
|
||||
"price": "0.65",
|
||||
"asset_id": "asset-xyz",
|
||||
"outcome": "Yes"
|
||||
}
|
||||
],
|
||||
"fee_rate_bps": "2.5",
|
||||
"transaction_hash": "0xdeadbeef",
|
||||
"trader_side": "TAKER"
|
||||
}"#;
|
||||
|
||||
let msgs = parse_stream_messages(json).unwrap();
|
||||
assert_eq!(msgs.len(), 1);
|
||||
|
||||
let StreamMessage::Trade(trade) = &msgs[0] else {
|
||||
panic!("expected Trade variant");
|
||||
};
|
||||
|
||||
assert_eq!(trade.id, "trade-001");
|
||||
assert_eq!(trade.market, "0xabc123");
|
||||
assert_eq!(trade.asset_id, "asset-xyz");
|
||||
assert_eq!(trade.side, Side::BUY);
|
||||
assert_eq!(trade.size, Decimal::from_str("100.5").unwrap());
|
||||
assert_eq!(trade.price, Decimal::from_str("0.65").unwrap());
|
||||
assert_eq!(trade.status, TradeMessageStatus::Matched);
|
||||
assert_eq!(trade.msg_type, Some(TradeMessageType::Trade));
|
||||
assert_eq!(trade.last_update, Some(1700000000));
|
||||
assert_eq!(trade.matchtime, Some(1700000001));
|
||||
assert_eq!(trade.timestamp, Some(1700000002));
|
||||
assert_eq!(trade.outcome.as_deref(), Some("Yes"));
|
||||
assert_eq!(trade.owner.as_deref(), Some("owner-key-123"));
|
||||
assert_eq!(trade.trade_owner.as_deref(), Some("trader-key-456"));
|
||||
assert_eq!(trade.taker_order_id.as_deref(), Some("taker-order-789"));
|
||||
assert_eq!(trade.maker_orders.len(), 1);
|
||||
assert_eq!(trade.maker_orders[0].order_id, "maker-order-1");
|
||||
assert_eq!(trade.fee_rate_bps, Some(Decimal::from_str("2.5").unwrap()));
|
||||
assert_eq!(trade.transaction_hash.as_deref(), Some("0xdeadbeef"));
|
||||
assert_eq!(trade.trader_side, Some(TraderSide::Taker));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_trade_message_minimal_payload() {
|
||||
use crate::types::StreamMessage;
|
||||
|
||||
// Only mandatory fields — all new optional fields absent.
|
||||
let json = r#"{
|
||||
"event_type": "trade",
|
||||
"id": "trade-minimal",
|
||||
"market": "0xdef",
|
||||
"asset_id": "asset-min",
|
||||
"side": "SELL",
|
||||
"size": "10",
|
||||
"price": "0.50"
|
||||
}"#;
|
||||
|
||||
let msgs = parse_stream_messages(json).unwrap();
|
||||
assert_eq!(msgs.len(), 1);
|
||||
|
||||
let StreamMessage::Trade(trade) = &msgs[0] else {
|
||||
panic!("expected Trade variant");
|
||||
};
|
||||
|
||||
assert_eq!(trade.id, "trade-minimal");
|
||||
assert_eq!(trade.side, Side::SELL);
|
||||
// All optional fields should be None / empty / default.
|
||||
assert!(trade.msg_type.is_none());
|
||||
assert!(trade.outcome.is_none());
|
||||
assert!(trade.owner.is_none());
|
||||
assert!(trade.trade_owner.is_none());
|
||||
assert!(trade.taker_order_id.is_none());
|
||||
assert!(trade.maker_orders.is_empty());
|
||||
assert!(trade.fee_rate_bps.is_none());
|
||||
assert!(trade.transaction_hash.is_none());
|
||||
assert!(trade.trader_side.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_trade_message_status_lifecycle() {
|
||||
use crate::types::{StreamMessage, TradeMessageStatus};
|
||||
|
||||
for (status_str, expected) in [
|
||||
("MATCHED", TradeMessageStatus::Matched),
|
||||
("matched", TradeMessageStatus::Matched),
|
||||
("Matched", TradeMessageStatus::Matched),
|
||||
("MINED", TradeMessageStatus::Mined),
|
||||
("mined", TradeMessageStatus::Mined),
|
||||
("CONFIRMED", TradeMessageStatus::Confirmed),
|
||||
("confirmed", TradeMessageStatus::Confirmed),
|
||||
] {
|
||||
let json = format!(
|
||||
r#"{{
|
||||
"event_type": "trade",
|
||||
"id": "t1", "market": "m", "asset_id": "a",
|
||||
"side": "BUY", "size": "1", "price": "0.5",
|
||||
"status": "{status_str}"
|
||||
}}"#
|
||||
);
|
||||
let msgs = parse_stream_messages(&json).unwrap();
|
||||
let StreamMessage::Trade(trade) = &msgs[0] else {
|
||||
panic!("expected Trade");
|
||||
};
|
||||
assert_eq!(trade.status, expected, "failed for status_str={status_str}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_trade_message_null_maker_orders() {
|
||||
use crate::types::StreamMessage;
|
||||
|
||||
// API sometimes sends `null` instead of `[]`.
|
||||
let json = r#"{
|
||||
"event_type": "trade",
|
||||
"id": "t1", "market": "m", "asset_id": "a",
|
||||
"side": "BUY", "size": "1", "price": "0.5",
|
||||
"maker_orders": null
|
||||
}"#;
|
||||
|
||||
let msgs = parse_stream_messages(json).unwrap();
|
||||
let StreamMessage::Trade(trade) = &msgs[0] else {
|
||||
panic!("expected Trade");
|
||||
};
|
||||
assert!(trade.maker_orders.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
+14
@@ -97,10 +97,12 @@ pub use crate::types::{
|
||||
BatchPriceRequest,
|
||||
BatchPriceResponse,
|
||||
BookParams,
|
||||
CancelOrdersResponse,
|
||||
ClientConfig,
|
||||
ClientResult,
|
||||
FeeRateResponse,
|
||||
FillEvent,
|
||||
MakerOrder,
|
||||
Market,
|
||||
MarketSnapshot,
|
||||
MarketsResponse,
|
||||
@@ -114,9 +116,12 @@ pub use crate::types::{
|
||||
OrderBookSummary,
|
||||
OrderDelta,
|
||||
OrderRequest,
|
||||
OrderScoringResponse,
|
||||
OrderStatus,
|
||||
OrderSummary,
|
||||
OrderType,
|
||||
PostOrderResponse,
|
||||
PriceHistoryPoint,
|
||||
PriceResponse,
|
||||
PricesHistoryInterval,
|
||||
PricesHistoryResponse,
|
||||
@@ -142,7 +147,12 @@ pub use crate::types::{
|
||||
TickSizeResponse,
|
||||
Token,
|
||||
TokenPrice,
|
||||
TradeMessage,
|
||||
TradeMessageStatus,
|
||||
TradeMessageType,
|
||||
TradeParams,
|
||||
TradeResponse,
|
||||
TraderSide,
|
||||
WssAuth,
|
||||
WssChannelType,
|
||||
WssSubscription,
|
||||
@@ -151,6 +161,10 @@ pub use crate::types::{
|
||||
// Re-export client
|
||||
pub use crate::client::{ClobClient, PolyfillClient};
|
||||
|
||||
// Re-export order signing types (for proxy wallet support)
|
||||
pub use crate::orders::SigType;
|
||||
pub use alloy_primitives::Address;
|
||||
|
||||
// Re-export compatibility types (for easy migration from polymarket-rs-client)
|
||||
pub use crate::client::OrderArgs;
|
||||
|
||||
|
||||
+94
-13
@@ -193,32 +193,48 @@ impl OrderBuilder {
|
||||
/// Get order amounts for a market order
|
||||
fn get_market_order_amounts(
|
||||
&self,
|
||||
side: Side,
|
||||
amount: Decimal,
|
||||
price: Decimal,
|
||||
round_config: &RoundConfig,
|
||||
) -> (u32, u32) {
|
||||
let raw_maker_amt = amount.round_dp_with_strategy(round_config.size, ToZero);
|
||||
let raw_price = price.round_dp_with_strategy(round_config.price, MidpointTowardZero);
|
||||
|
||||
let raw_taker_amt = raw_maker_amt / raw_price;
|
||||
let raw_taker_amt = self.fix_amount_rounding(raw_taker_amt, round_config);
|
||||
|
||||
(
|
||||
decimal_to_token_u32(raw_maker_amt),
|
||||
decimal_to_token_u32(raw_taker_amt),
|
||||
)
|
||||
match side {
|
||||
Side::BUY => {
|
||||
let raw_maker_amt = amount.round_dp_with_strategy(round_config.size, ToZero);
|
||||
let raw_taker_amt = raw_maker_amt / raw_price;
|
||||
let raw_taker_amt = self.fix_amount_rounding(raw_taker_amt, round_config);
|
||||
(
|
||||
decimal_to_token_u32(raw_maker_amt),
|
||||
decimal_to_token_u32(raw_taker_amt),
|
||||
)
|
||||
},
|
||||
Side::SELL => {
|
||||
let raw_maker_amt = amount.round_dp_with_strategy(round_config.size, ToZero);
|
||||
let raw_taker_amt = raw_maker_amt * raw_price;
|
||||
let raw_taker_amt = self.fix_amount_rounding(raw_taker_amt, round_config);
|
||||
(
|
||||
decimal_to_token_u32(raw_maker_amt),
|
||||
decimal_to_token_u32(raw_taker_amt),
|
||||
)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate market price from order book levels
|
||||
pub fn calculate_market_price(
|
||||
&self,
|
||||
side: Side,
|
||||
positions: &[crate::types::BookLevel],
|
||||
amount_to_match: Decimal,
|
||||
) -> Result<Decimal> {
|
||||
let mut sum = Decimal::ZERO;
|
||||
|
||||
for level in positions {
|
||||
sum += level.size * level.price;
|
||||
sum += match side {
|
||||
Side::BUY => level.size * level.price,
|
||||
Side::SELL => level.size,
|
||||
};
|
||||
if sum >= amount_to_match {
|
||||
return Ok(level.price);
|
||||
}
|
||||
@@ -246,8 +262,12 @@ impl OrderBuilder {
|
||||
.tick_size
|
||||
.ok_or_else(|| PolyfillError::validation("Cannot create order without tick size"))?;
|
||||
|
||||
let (maker_amount, taker_amount) =
|
||||
self.get_market_order_amounts(order_args.amount, price, &ROUNDING_CONFIG[&tick_size]);
|
||||
let (maker_amount, taker_amount) = self.get_market_order_amounts(
|
||||
order_args.side,
|
||||
order_args.amount,
|
||||
price,
|
||||
&ROUNDING_CONFIG[&tick_size],
|
||||
);
|
||||
|
||||
let neg_risk = options
|
||||
.neg_risk
|
||||
@@ -262,7 +282,7 @@ impl OrderBuilder {
|
||||
|
||||
self.build_signed_order(
|
||||
order_args.token_id.clone(),
|
||||
Side::BUY,
|
||||
order_args.side,
|
||||
chain_id,
|
||||
exchange_address,
|
||||
maker_amount,
|
||||
@@ -437,4 +457,65 @@ mod tests {
|
||||
assert!(seed < u64::MAX);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_calculate_market_price_respects_side_amount_semantics() {
|
||||
let signer: PrivateKeySigner =
|
||||
"0x1234567890123456789012345678901234567890123456789012345678901234"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let builder = OrderBuilder::new(signer, None, None);
|
||||
|
||||
let levels = vec![
|
||||
crate::types::BookLevel {
|
||||
price: Decimal::from_str("0.50").unwrap(),
|
||||
size: Decimal::from_str("10").unwrap(),
|
||||
},
|
||||
crate::types::BookLevel {
|
||||
price: Decimal::from_str("0.55").unwrap(),
|
||||
size: Decimal::from_str("10").unwrap(),
|
||||
},
|
||||
];
|
||||
|
||||
// BUY amounts are quote-denominated: need 6 USDC -> first level (10 * 0.50 = 5) is not enough.
|
||||
let buy_price = builder
|
||||
.calculate_market_price(Side::BUY, &levels, Decimal::from_str("6").unwrap())
|
||||
.unwrap();
|
||||
assert_eq!(buy_price, Decimal::from_str("0.55").unwrap());
|
||||
|
||||
// SELL amounts are base-denominated: need 6 tokens -> first level (size 10) is enough.
|
||||
let sell_price = builder
|
||||
.calculate_market_price(Side::SELL, &levels, Decimal::from_str("6").unwrap())
|
||||
.unwrap();
|
||||
assert_eq!(sell_price, Decimal::from_str("0.50").unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_market_order_uses_input_side() {
|
||||
let signer: PrivateKeySigner =
|
||||
"0x1234567890123456789012345678901234567890123456789012345678901234"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let builder = OrderBuilder::new(signer, None, None);
|
||||
|
||||
let order = builder
|
||||
.create_market_order(
|
||||
137,
|
||||
&MarketOrderArgs {
|
||||
token_id: "123".to_string(),
|
||||
side: Side::SELL,
|
||||
amount: Decimal::from_str("5").unwrap(),
|
||||
},
|
||||
Decimal::from_str("0.40").unwrap(),
|
||||
&ExtraOrderArgs::default(),
|
||||
&OrderOptions {
|
||||
tick_size: Some(Decimal::from_str("0.01").unwrap()),
|
||||
neg_risk: Some(false),
|
||||
fee_rate_bps: None,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(order.side, "SELL");
|
||||
}
|
||||
}
|
||||
|
||||
+241
-10
@@ -191,6 +191,51 @@ pub enum Side {
|
||||
SELL = 1,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "UPPERCASE")]
|
||||
pub enum TraderSide {
|
||||
Taker,
|
||||
Maker,
|
||||
#[serde(untagged)]
|
||||
Unknown(String),
|
||||
}
|
||||
|
||||
impl Default for TraderSide {
|
||||
fn default() -> Self {
|
||||
Self::Unknown("UNKNOWN".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Trade lifecycle status (Matched → Mined → Confirmed).
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum TradeMessageStatus {
|
||||
#[serde(alias = "matched", alias = "MATCHED")]
|
||||
Matched,
|
||||
#[serde(alias = "mined", alias = "MINED")]
|
||||
Mined,
|
||||
#[serde(alias = "confirmed", alias = "CONFIRMED")]
|
||||
Confirmed,
|
||||
/// Forward-compatible catch-all for unknown statuses.
|
||||
#[serde(untagged)]
|
||||
Unknown(String),
|
||||
}
|
||||
|
||||
impl Default for TradeMessageStatus {
|
||||
fn default() -> Self {
|
||||
Self::Unknown("UNKNOWN".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Trade message type discriminator.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum TradeMessageType {
|
||||
#[serde(alias = "trade", alias = "TRADE")]
|
||||
Trade,
|
||||
/// Forward-compatible catch-all.
|
||||
#[serde(untagged)]
|
||||
Unknown(String),
|
||||
}
|
||||
|
||||
impl Side {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
@@ -214,6 +259,7 @@ pub enum OrderType {
|
||||
#[default]
|
||||
GTC,
|
||||
FOK,
|
||||
FAK,
|
||||
GTD,
|
||||
}
|
||||
|
||||
@@ -222,6 +268,7 @@ impl OrderType {
|
||||
match self {
|
||||
OrderType::GTC => "GTC",
|
||||
OrderType::FOK => "FOK",
|
||||
OrderType::FAK => "FAK",
|
||||
OrderType::GTD => "GTD",
|
||||
}
|
||||
}
|
||||
@@ -524,6 +571,8 @@ impl Default for ExtraOrderArgs {
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MarketOrderArgs {
|
||||
pub token_id: String,
|
||||
pub side: Side,
|
||||
/// Quote amount for buys, base token amount for sells.
|
||||
pub amount: Decimal,
|
||||
}
|
||||
|
||||
@@ -889,7 +938,7 @@ pub struct EventMessage {
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
/// User trade execution message.
|
||||
/// User trade execution message (authenticated WebSocket channel).
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TradeMessage {
|
||||
pub id: String,
|
||||
@@ -898,10 +947,11 @@ pub struct TradeMessage {
|
||||
pub side: Side,
|
||||
pub size: Decimal,
|
||||
pub price: Decimal,
|
||||
/// Trade lifecycle status (Matched → Mined → Confirmed).
|
||||
#[serde(default)]
|
||||
pub status: Option<String>,
|
||||
pub status: TradeMessageStatus,
|
||||
#[serde(rename = "type", default)]
|
||||
pub msg_type: Option<String>,
|
||||
pub msg_type: Option<TradeMessageType>,
|
||||
#[serde(
|
||||
default,
|
||||
deserialize_with = "crate::decode::deserializers::optional_number_from_string"
|
||||
@@ -918,6 +968,36 @@ pub struct TradeMessage {
|
||||
deserialize_with = "crate::decode::deserializers::optional_number_from_string"
|
||||
)]
|
||||
pub timestamp: Option<u64>,
|
||||
/// Outcome (e.g. "Yes" / "No").
|
||||
#[serde(default)]
|
||||
pub outcome: Option<String>,
|
||||
/// API key of the event owner.
|
||||
#[serde(default)]
|
||||
pub owner: Option<String>,
|
||||
/// API key of the trade owner.
|
||||
#[serde(default)]
|
||||
pub trade_owner: Option<String>,
|
||||
/// Taker order ID.
|
||||
#[serde(default)]
|
||||
pub taker_order_id: Option<String>,
|
||||
/// Maker order details.
|
||||
#[serde(
|
||||
default,
|
||||
deserialize_with = "crate::decode::deserializers::vec_from_null"
|
||||
)]
|
||||
pub maker_orders: Vec<MakerOrder>,
|
||||
/// Fee rate in basis points.
|
||||
#[serde(
|
||||
default,
|
||||
deserialize_with = "crate::decode::deserializers::optional_decimal_from_string"
|
||||
)]
|
||||
pub fee_rate_bps: Option<Decimal>,
|
||||
/// On-chain transaction hash.
|
||||
#[serde(default)]
|
||||
pub transaction_hash: Option<String>,
|
||||
/// Whether user was maker or taker.
|
||||
#[serde(default)]
|
||||
pub trader_side: Option<TraderSide>,
|
||||
}
|
||||
|
||||
/// User order update message.
|
||||
@@ -1106,6 +1186,131 @@ pub struct OpenOrder {
|
||||
pub created_at: u64,
|
||||
}
|
||||
|
||||
/// Response from posting a single order.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PostOrderResponse {
|
||||
#[serde(default)]
|
||||
pub error_msg: Option<String>,
|
||||
#[serde(
|
||||
default,
|
||||
deserialize_with = "crate::decode::deserializers::optional_decimal_from_string_default_on_error"
|
||||
)]
|
||||
pub making_amount: Option<Decimal>,
|
||||
#[serde(
|
||||
default,
|
||||
deserialize_with = "crate::decode::deserializers::optional_decimal_from_string_default_on_error"
|
||||
)]
|
||||
pub taking_amount: Option<Decimal>,
|
||||
#[serde(rename = "orderID")]
|
||||
pub order_id: String,
|
||||
#[serde(default)]
|
||||
pub status: Option<String>,
|
||||
pub success: bool,
|
||||
#[serde(default, alias = "transactionsHashes")]
|
||||
pub transaction_hashes: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub trade_ids: Vec<String>,
|
||||
}
|
||||
|
||||
/// Response from cancel endpoints.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CancelOrdersResponse {
|
||||
#[serde(
|
||||
default,
|
||||
deserialize_with = "crate::decode::deserializers::vec_from_null"
|
||||
)]
|
||||
pub canceled: Vec<String>,
|
||||
#[serde(default, alias = "not_canceled")]
|
||||
pub not_canceled: std::collections::HashMap<String, String>,
|
||||
}
|
||||
|
||||
/// Trade item returned by `/data/trades`.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct TradeResponse {
|
||||
pub id: String,
|
||||
#[serde(default)]
|
||||
pub taker_order_id: Option<String>,
|
||||
pub market: String,
|
||||
pub asset_id: String,
|
||||
pub side: Side,
|
||||
#[serde(deserialize_with = "crate::decode::deserializers::decimal_from_string")]
|
||||
pub size: Decimal,
|
||||
#[serde(
|
||||
default,
|
||||
deserialize_with = "crate::decode::deserializers::optional_decimal_from_string"
|
||||
)]
|
||||
pub fee_rate_bps: Option<Decimal>,
|
||||
#[serde(deserialize_with = "crate::decode::deserializers::decimal_from_string")]
|
||||
pub price: Decimal,
|
||||
#[serde(default)]
|
||||
pub status: Option<String>,
|
||||
#[serde(
|
||||
default,
|
||||
deserialize_with = "crate::decode::deserializers::optional_number_from_string"
|
||||
)]
|
||||
pub match_time: Option<u64>,
|
||||
#[serde(
|
||||
default,
|
||||
deserialize_with = "crate::decode::deserializers::optional_number_from_string"
|
||||
)]
|
||||
pub last_update: Option<u64>,
|
||||
#[serde(default)]
|
||||
pub outcome: Option<String>,
|
||||
#[serde(
|
||||
default,
|
||||
deserialize_with = "crate::decode::deserializers::optional_number_from_string"
|
||||
)]
|
||||
pub bucket_index: Option<u64>,
|
||||
#[serde(default)]
|
||||
pub owner: Option<String>,
|
||||
#[serde(default)]
|
||||
pub maker_address: Option<String>,
|
||||
#[serde(
|
||||
default,
|
||||
deserialize_with = "crate::decode::deserializers::vec_from_null"
|
||||
)]
|
||||
pub maker_orders: Vec<MakerOrder>,
|
||||
#[serde(default)]
|
||||
pub transaction_hash: Option<String>,
|
||||
#[serde(default)]
|
||||
pub trader_side: TraderSide,
|
||||
#[serde(default, alias = "err_msg")]
|
||||
pub error_msg: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct MakerOrder {
|
||||
pub order_id: String,
|
||||
#[serde(default)]
|
||||
pub owner: Option<String>,
|
||||
#[serde(default)]
|
||||
pub maker_address: Option<String>,
|
||||
#[serde(
|
||||
default,
|
||||
deserialize_with = "crate::decode::deserializers::optional_decimal_from_string"
|
||||
)]
|
||||
pub matched_amount: Option<Decimal>,
|
||||
#[serde(
|
||||
default,
|
||||
deserialize_with = "crate::decode::deserializers::optional_decimal_from_string"
|
||||
)]
|
||||
pub price: Option<Decimal>,
|
||||
#[serde(
|
||||
default,
|
||||
deserialize_with = "crate::decode::deserializers::optional_decimal_from_string"
|
||||
)]
|
||||
pub fee_rate_bps: Option<Decimal>,
|
||||
#[serde(default)]
|
||||
pub asset_id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub outcome: Option<String>,
|
||||
#[serde(default)]
|
||||
pub side: Option<Side>,
|
||||
}
|
||||
|
||||
/// Balance allowance information
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct BalanceAllowance {
|
||||
@@ -1254,24 +1459,29 @@ impl PricesHistoryInterval {
|
||||
}
|
||||
}
|
||||
|
||||
/// Raw response from `/prices-history`.
|
||||
/// A single price-history datapoint from `/prices-history`.
|
||||
///
|
||||
/// We intentionally keep `history` entries as `serde_json::Value` because the upstream API has
|
||||
/// no stable public schema here and currently may return empty history for many markets.
|
||||
/// Mirrors the official Polymarket SDK shape (`t`, `p`).
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PriceHistoryPoint {
|
||||
pub t: i64,
|
||||
#[serde(deserialize_with = "crate::decode::deserializers::decimal_from_string")]
|
||||
pub p: Decimal,
|
||||
}
|
||||
|
||||
/// Response from `/prices-history`.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PricesHistoryResponse {
|
||||
pub history: Vec<serde_json::Value>,
|
||||
pub history: Vec<PriceHistoryPoint>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct SpreadResponse {
|
||||
#[serde(with = "rust_decimal::serde::str")]
|
||||
pub spread: Decimal,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct TickSizeResponse {
|
||||
#[serde(with = "rust_decimal::serde::str")]
|
||||
pub minimum_tick_size: Decimal,
|
||||
}
|
||||
|
||||
@@ -1280,6 +1490,11 @@ pub struct NegRiskResponse {
|
||||
pub neg_risk: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct OrderScoringResponse {
|
||||
pub scoring: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct BookParams {
|
||||
pub token_id: String,
|
||||
@@ -1374,7 +1589,7 @@ pub struct Rewards {
|
||||
/// Fee rate in basis points for a given token.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct FeeRateResponse {
|
||||
pub fee_rate_bps: u32,
|
||||
pub base_fee: u32,
|
||||
}
|
||||
|
||||
/// Create RFQ request (Requester).
|
||||
@@ -1657,3 +1872,19 @@ pub type Result<T> = std::result::Result<T, crate::errors::PolyfillError>;
|
||||
pub type ApiCreds = ApiCredentials;
|
||||
pub type CreateOrderOptions = OrderOptions;
|
||||
pub type OrderArgs = OrderRequest;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::OrderType;
|
||||
|
||||
#[test]
|
||||
fn test_order_type_fak_serde_and_string() {
|
||||
assert_eq!(OrderType::FAK.as_str(), "FAK");
|
||||
|
||||
let json = serde_json::to_string(&OrderType::FAK).unwrap();
|
||||
assert_eq!(json, "\"FAK\"");
|
||||
|
||||
let parsed: OrderType = serde_json::from_str("\"FAK\"").unwrap();
|
||||
assert_eq!(parsed, OrderType::FAK);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,9 +112,9 @@ async fn test_real_api_authenticated_order_flow() {
|
||||
println!("PASS: Order posted successfully!");
|
||||
|
||||
// Step 5: Cancel the order
|
||||
if let Some(order_id) = response.get("orderID").and_then(|v| v.as_str()) {
|
||||
println!("Step 5: Canceling order {}...", order_id);
|
||||
let cancel_result = client.cancel(order_id).await;
|
||||
if !response.order_id.is_empty() {
|
||||
println!("Step 5: Canceling order {}...", response.order_id);
|
||||
let cancel_result = client.cancel(&response.order_id).await;
|
||||
assert!(
|
||||
cancel_result.is_ok(),
|
||||
"Failed to cancel order: {:?}",
|
||||
|
||||
@@ -41,9 +41,9 @@ async fn test_post_order_authentication() {
|
||||
println!(" Response: {:?}", response);
|
||||
|
||||
// Try to cancel it if we got an order ID
|
||||
if let Some(order_id) = response.get("orderID").and_then(|v| v.as_str()) {
|
||||
if !response.order_id.is_empty() {
|
||||
println!("\nStep 3: Canceling order...");
|
||||
match client.cancel(order_id).await {
|
||||
match client.cancel(&response.order_id).await {
|
||||
Ok(_) => println!("Order canceled successfully"),
|
||||
Err(e) => println!("Cancel failed (order might have expired): {:?}", e),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user