mirror of
https://github.com/floor-licker/polyfill-rs.git
synced 2026-08-08 10:17:46 +00:00
feat(client): use typed HTTP response models instead of serde_json::Value
This commit is contained in:
+74
-57
@@ -14,7 +14,6 @@ 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;
|
||||
@@ -623,18 +622,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 +647,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 +809,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 +938,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 +959,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 +985,7 @@ impl ClobClient {
|
||||
&self,
|
||||
order: SignedOrderRequest,
|
||||
order_type: OrderType,
|
||||
) -> Result<Value> {
|
||||
) -> Result<crate::types::PostOrderResponse> {
|
||||
let signer = self
|
||||
.signer
|
||||
.as_ref()
|
||||
@@ -1029,7 +1014,7 @@ impl ClobClient {
|
||||
return Err(PolyfillError::api(status, message));
|
||||
}
|
||||
|
||||
Ok(response.json::<Value>().await?)
|
||||
Ok(response.json::<crate::types::PostOrderResponse>().await?)
|
||||
}
|
||||
|
||||
/// Post multiple orders to the exchange in a single request
|
||||
@@ -1037,7 +1022,7 @@ impl ClobClient {
|
||||
&self,
|
||||
orders: Vec<SignedOrderRequest>,
|
||||
order_type: OrderType,
|
||||
) -> Result<Value> {
|
||||
) -> Result<Vec<crate::types::PostOrderResponse>> {
|
||||
if orders.is_empty() {
|
||||
return Err(PolyfillError::validation("orders cannot be empty"));
|
||||
}
|
||||
@@ -1076,7 +1061,9 @@ impl ClobClient {
|
||||
return Err(PolyfillError::api(status, message));
|
||||
}
|
||||
|
||||
Ok(response.json::<Value>().await?)
|
||||
Ok(response
|
||||
.json::<Vec<crate::types::PostOrderResponse>>()
|
||||
.await?)
|
||||
}
|
||||
|
||||
/// Create and post an order in one call with an explicit order type
|
||||
@@ -1084,13 +1071,16 @@ impl ClobClient {
|
||||
&self,
|
||||
order_args: &OrderArgs,
|
||||
order_type: OrderType,
|
||||
) -> Result<Value> {
|
||||
) -> Result<crate::types::PostOrderResponse> {
|
||||
let order = self.create_order(order_args, None, None, None).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<Value> {
|
||||
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
|
||||
}
|
||||
@@ -1100,7 +1090,7 @@ impl ClobClient {
|
||||
&self,
|
||||
order_args: &[OrderArgs],
|
||||
order_type: OrderType,
|
||||
) -> Result<Value> {
|
||||
) -> Result<Vec<crate::types::PostOrderResponse>> {
|
||||
if order_args.is_empty() {
|
||||
return Err(PolyfillError::validation("order_args cannot be empty"));
|
||||
}
|
||||
@@ -1114,13 +1104,16 @@ impl ClobClient {
|
||||
}
|
||||
|
||||
/// Create and post multiple orders in one call (defaults to GTC)
|
||||
pub async fn create_and_post_orders(&self, order_args: &[OrderArgs]) -> Result<Value> {
|
||||
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()
|
||||
@@ -1143,11 +1136,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()
|
||||
@@ -1168,11 +1166,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()
|
||||
@@ -1182,7 +1182,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());
|
||||
|
||||
@@ -1194,7 +1194,9 @@ impl ClobClient {
|
||||
));
|
||||
}
|
||||
|
||||
Ok(response.json::<Value>().await?)
|
||||
Ok(response
|
||||
.json::<crate::types::CancelOrdersResponse>()
|
||||
.await?)
|
||||
}
|
||||
|
||||
/// Get open orders with optional filtering
|
||||
@@ -1292,7 +1294,7 @@ impl ClobClient {
|
||||
&self,
|
||||
trade_params: Option<&crate::types::TradeParams>,
|
||||
next_cursor: Option<&str>,
|
||||
) -> Result<Vec<Value>> {
|
||||
) -> Result<Vec<crate::types::TradeResponse>> {
|
||||
let signer = self
|
||||
.signer
|
||||
.as_ref()
|
||||
@@ -1348,7 +1350,14 @@ impl ClobClient {
|
||||
next_cursor = new_cursor;
|
||||
|
||||
let results = resp["data"].clone();
|
||||
output.push(results);
|
||||
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,
|
||||
)
|
||||
})?;
|
||||
output.extend(trades);
|
||||
}
|
||||
|
||||
Ok(output)
|
||||
@@ -1646,7 +1655,7 @@ impl ClobClient {
|
||||
&self,
|
||||
market: Option<&str>,
|
||||
asset_id: Option<&str>,
|
||||
) -> Result<Value> {
|
||||
) -> Result<crate::types::CancelOrdersResponse> {
|
||||
let signer = self
|
||||
.signer
|
||||
.as_ref()
|
||||
@@ -1680,7 +1689,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))
|
||||
}
|
||||
@@ -1802,12 +1811,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
|
||||
@@ -2357,9 +2366,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
|
||||
@@ -2715,7 +2726,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")
|
||||
@@ -2738,6 +2749,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")]
|
||||
@@ -2980,7 +2993,7 @@ mod tests {
|
||||
.mock("POST", "/orders")
|
||||
.with_status(200)
|
||||
.with_header("content-type", "application/json")
|
||||
.with_body(r#"{"success":true,"orderIDs":["a","b"]}"#)
|
||||
.with_body(r#"[{"success":true,"orderID":"a"},{"success":true,"orderID":"b"}]"#)
|
||||
.create_async()
|
||||
.await;
|
||||
|
||||
@@ -2996,24 +3009,28 @@ mod tests {
|
||||
|
||||
mock.assert_async().await;
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap()["success"], true);
|
||||
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;
|
||||
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 orders = (0..16).map(|_| sample_signed_order()).collect::<Vec<_>>();
|
||||
|
||||
let result = client.post_orders(orders, crate::types::OrderType::GTC).await;
|
||||
let result = client
|
||||
.post_orders(orders, crate::types::OrderType::GTC)
|
||||
.await;
|
||||
assert!(matches!(result, Err(PolyfillError::Validation { .. })));
|
||||
}
|
||||
|
||||
@@ -3036,7 +3053,7 @@ mod tests {
|
||||
|
||||
mock.assert_async().await;
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap()["success"], true);
|
||||
assert!(result.unwrap().success);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
@@ -3058,7 +3075,7 @@ mod tests {
|
||||
|
||||
mock.assert_async().await;
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap()[0]["success"], true);
|
||||
assert!(result.unwrap()[0].success);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
@@ -3222,7 +3239,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;
|
||||
|
||||
|
||||
@@ -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,
|
||||
@@ -143,6 +148,8 @@ pub use crate::types::{
|
||||
Token,
|
||||
TokenPrice,
|
||||
TradeParams,
|
||||
TradeResponse,
|
||||
TraderSide,
|
||||
WssAuth,
|
||||
WssChannelType,
|
||||
WssSubscription,
|
||||
|
||||
+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");
|
||||
}
|
||||
}
|
||||
|
||||
+159
-7
@@ -191,6 +191,21 @@ 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())
|
||||
}
|
||||
}
|
||||
|
||||
impl Side {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
@@ -526,6 +541,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,
|
||||
}
|
||||
|
||||
@@ -1108,6 +1125,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 {
|
||||
@@ -1256,24 +1398,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,
|
||||
}
|
||||
|
||||
@@ -1282,6 +1429,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,
|
||||
@@ -1376,7 +1528,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).
|
||||
|
||||
@@ -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