mirror of
https://github.com/floor-licker/polyfill-rs.git
synced 2026-07-27 20:47:46 +00:00
fix: align V2 client with official SDK fee behavior
This commit is contained in:
+123
-31
@@ -5,13 +5,11 @@
|
||||
|
||||
use crate::auth::{create_l1_headers, create_l2_headers};
|
||||
use crate::errors::{PolyfillError, Result};
|
||||
use crate::http_config::{
|
||||
create_colocated_client, create_internet_client, prewarm_connections,
|
||||
};
|
||||
use crate::http_config::{create_colocated_client, create_internet_client, prewarm_connections};
|
||||
use crate::types::{
|
||||
CancelOrdersResponse, ClientConfig, ClobMarketInfo, CreateOrderOptions, MarketOrderArgs,
|
||||
OrderArgs, OrderType, PostOrder, PostOrderOptions, PostOrderResponse, SignedOrderRequest,
|
||||
Side,
|
||||
BuilderFeeRateResponse, CancelOrdersResponse, ClientConfig, ClobMarketInfo, CreateOrderOptions,
|
||||
MarketOrderArgs, OrderArgs, OrderType, PostOrder, PostOrderOptions, PostOrderResponse, Side,
|
||||
SignedOrderRequest,
|
||||
};
|
||||
use alloy_primitives::U256;
|
||||
use alloy_signer_local::PrivateKeySigner;
|
||||
@@ -96,7 +94,10 @@ impl ClobClient {
|
||||
});
|
||||
|
||||
let connection_manager = Some(std::sync::Arc::new(
|
||||
crate::connection_manager::ConnectionManager::new(http_client.clone(), host.to_string()),
|
||||
crate::connection_manager::ConnectionManager::new(
|
||||
http_client.clone(),
|
||||
host.to_string(),
|
||||
),
|
||||
));
|
||||
let buffer_pool = std::sync::Arc::new(crate::buffer_pool::BufferPool::new(512 * 1024, 10));
|
||||
|
||||
@@ -455,6 +456,41 @@ impl ClobClient {
|
||||
.map_err(|e| PolyfillError::parse(format!("Failed to parse response: {e}"), None))
|
||||
}
|
||||
|
||||
/// Get V2 builder fee rates for a bytes32 builder code.
|
||||
pub async fn get_builder_fee_rate(&self, builder_code: &str) -> Result<BuilderFeeRateResponse> {
|
||||
crate::orders::validate_bytes32_hex("builder_code", builder_code)?;
|
||||
|
||||
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 endpoint = format!("/fees/builder-fees/{builder_code}");
|
||||
let headers = create_l2_headers::<Value>(signer, api_creds, "GET", &endpoint, None)?;
|
||||
let req = self.create_request_with_headers(Method::GET, &endpoint, headers.into_iter());
|
||||
|
||||
let response = req.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 get builder fee rate".to_string()
|
||||
} else {
|
||||
format!("Failed to get builder fee rate: {body}")
|
||||
};
|
||||
return Err(PolyfillError::api(status, message));
|
||||
}
|
||||
|
||||
response
|
||||
.json::<BuilderFeeRateResponse>()
|
||||
.await
|
||||
.map_err(|e| PolyfillError::parse(format!("Failed to parse response: {e}"), None))
|
||||
}
|
||||
|
||||
fn validate_prices_history_asset_id(asset_id: &str) -> Result<()> {
|
||||
if asset_id.is_empty() {
|
||||
return Err(PolyfillError::validation(
|
||||
@@ -974,12 +1010,21 @@ impl ClobClient {
|
||||
|
||||
if order_args.side == Side::BUY {
|
||||
if let Some(user_balance) = order_args.user_usdc_balance {
|
||||
let market_info = self.get_clob_market_info_for_token(&order_args.token_id).await?;
|
||||
let market_info = self
|
||||
.get_clob_market_info_for_token(&order_args.token_id)
|
||||
.await?;
|
||||
let fee_details = market_info.fd.unwrap_or(crate::types::ClobFeeDetails {
|
||||
r: Decimal::ZERO,
|
||||
e: 0,
|
||||
to: false,
|
||||
});
|
||||
let builder_taker_fee_rate = match order_args.builder_code.as_deref() {
|
||||
Some(code) if code != crate::orders::BYTES32_ZERO => {
|
||||
let rate = self.get_builder_fee_rate(code).await?;
|
||||
Decimal::from(rate.builder_taker_fee_rate_bps) / Decimal::from(10_000_u32)
|
||||
},
|
||||
_ => Decimal::ZERO,
|
||||
};
|
||||
|
||||
order_args.amount = crate::orders::adjust_buy_amount_for_fees(
|
||||
order_args.amount,
|
||||
@@ -987,11 +1032,7 @@ impl ClobClient {
|
||||
user_balance,
|
||||
fee_details.r,
|
||||
fee_details.e,
|
||||
if order_args.builder_code.is_some() {
|
||||
market_info.tbf
|
||||
} else {
|
||||
Decimal::ZERO
|
||||
},
|
||||
builder_taker_fee_rate,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
@@ -1029,6 +1070,17 @@ impl ClobClient {
|
||||
"post_only is not supported for FOK/FAK orders",
|
||||
));
|
||||
}
|
||||
let expiration = order.expiration.parse::<u64>().map_err(|e| {
|
||||
PolyfillError::validation(format!(
|
||||
"Invalid order expiration '{}': {e}",
|
||||
order.expiration
|
||||
))
|
||||
})?;
|
||||
if expiration > 0 && options.order_type != OrderType::GTD {
|
||||
return Err(PolyfillError::validation(
|
||||
"expiration is only supported for GTD orders",
|
||||
));
|
||||
}
|
||||
|
||||
// Owner field must reference the credential principal identifier
|
||||
// to maintain consistency with the authentication context layer
|
||||
@@ -2362,8 +2414,7 @@ mod tests {
|
||||
base_url: base_url.to_string(),
|
||||
chain: 137,
|
||||
private_key: Some(
|
||||
"0x1234567890123456789012345678901234567890123456789012345678901234"
|
||||
.to_string(),
|
||||
"0x1234567890123456789012345678901234567890123456789012345678901234".to_string(),
|
||||
),
|
||||
..ClientConfig::default()
|
||||
})
|
||||
@@ -2382,8 +2433,7 @@ mod tests {
|
||||
base_url: base_url.to_string(),
|
||||
chain: 137,
|
||||
private_key: Some(
|
||||
"0x1234567890123456789012345678901234567890123456789012345678901234"
|
||||
.to_string(),
|
||||
"0x1234567890123456789012345678901234567890123456789012345678901234".to_string(),
|
||||
),
|
||||
api_credentials: Some(api_creds),
|
||||
..ClientConfig::default()
|
||||
@@ -2403,12 +2453,10 @@ mod tests {
|
||||
side: "BUY".to_string(),
|
||||
signature_type: 0,
|
||||
timestamp: "1713916800000".to_string(),
|
||||
metadata:
|
||||
"0x0000000000000000000000000000000000000000000000000000000000000000"
|
||||
.to_string(),
|
||||
builder:
|
||||
"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
.to_string(),
|
||||
metadata: "0x0000000000000000000000000000000000000000000000000000000000000000"
|
||||
.to_string(),
|
||||
builder: "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
.to_string(),
|
||||
signature: "0xdeadbeef".to_string(),
|
||||
}
|
||||
}
|
||||
@@ -2441,8 +2489,7 @@ mod tests {
|
||||
base_url: "https://test.example.com".to_string(),
|
||||
chain: 137,
|
||||
private_key: Some(
|
||||
"0x1234567890123456789012345678901234567890123456789012345678901234"
|
||||
.to_string(),
|
||||
"0x1234567890123456789012345678901234567890123456789012345678901234".to_string(),
|
||||
),
|
||||
api_credentials: Some(api_creds.clone()),
|
||||
..ClientConfig::default()
|
||||
@@ -3121,13 +3168,11 @@ mod tests {
|
||||
.with_header("content-type", "application/json")
|
||||
.with_body(
|
||||
r#"{
|
||||
"c":"0x1111111111111111111111111111111111111111111111111111111111111111",
|
||||
"gst":"ready",
|
||||
"r":{},
|
||||
"t":[{"t":"123","o":"YES"},{"t":"456","o":"NO"}],
|
||||
"mos":"5",
|
||||
"mts":"0.01",
|
||||
"mbf":"0",
|
||||
"tbf":"15",
|
||||
"rfqe":true,
|
||||
"itode":false,
|
||||
"ibce":false,
|
||||
@@ -3143,13 +3188,42 @@ mod tests {
|
||||
let info = client.get_clob_market_info("condition-1").await.unwrap();
|
||||
|
||||
mock.assert_async().await;
|
||||
assert_eq!(
|
||||
info.c.as_deref(),
|
||||
Some("0x1111111111111111111111111111111111111111111111111111111111111111")
|
||||
);
|
||||
assert_eq!(info.t.len(), 2);
|
||||
assert_eq!(info.mos, Decimal::from_str("5").unwrap());
|
||||
assert_eq!(info.mts, Decimal::from_str("0.01").unwrap());
|
||||
assert_eq!(info.tbf, Decimal::from_str("15").unwrap());
|
||||
assert_eq!(info.tbf, Decimal::ZERO);
|
||||
assert_eq!(info.fd.unwrap().e, 2);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_get_builder_fee_rate_uses_v2_endpoint() {
|
||||
let mut server = Server::new_async().await;
|
||||
let builder_code = "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
|
||||
let mock = server
|
||||
.mock("GET", format!("/fees/builder-fees/{builder_code}").as_str())
|
||||
.with_status(200)
|
||||
.with_header("content-type", "application/json")
|
||||
.with_body(
|
||||
r#"{
|
||||
"builder_maker_fee_rate_bps": 5,
|
||||
"builder_taker_fee_rate_bps": 12
|
||||
}"#,
|
||||
)
|
||||
.create_async()
|
||||
.await;
|
||||
|
||||
let client = create_test_client_with_l2_auth(&server.url());
|
||||
let response = client.get_builder_fee_rate(builder_code).await.unwrap();
|
||||
|
||||
mock.assert_async().await;
|
||||
assert_eq!(response.builder_maker_fee_rate_bps, 5);
|
||||
assert_eq!(response.builder_taker_fee_rate_bps, 12);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_post_order_uses_v2_wire_shape_and_typed_response() {
|
||||
let mut server = Server::new_async().await;
|
||||
@@ -3173,7 +3247,7 @@ mod tests {
|
||||
"signature": "0xdeadbeef"
|
||||
},
|
||||
"owner": "test_key",
|
||||
"orderType": "GTC",
|
||||
"orderType": "GTD",
|
||||
"postOnly": true,
|
||||
"deferExec": true
|
||||
})
|
||||
@@ -3201,7 +3275,7 @@ mod tests {
|
||||
.post_order(
|
||||
sample_signed_order(),
|
||||
Some(&PostOrderOptions {
|
||||
order_type: OrderType::GTC,
|
||||
order_type: OrderType::GTD,
|
||||
post_only: true,
|
||||
defer_exec: true,
|
||||
}),
|
||||
@@ -3235,6 +3309,24 @@ mod tests {
|
||||
assert!(matches!(err, PolyfillError::Validation { .. }));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_post_order_rejects_expiration_for_non_gtd() {
|
||||
let client = create_test_client_with_l2_auth("https://test.example.com");
|
||||
let err = client
|
||||
.post_order(
|
||||
sample_signed_order(),
|
||||
Some(&PostOrderOptions {
|
||||
order_type: OrderType::GTC,
|
||||
post_only: false,
|
||||
defer_exec: false,
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
assert!(matches!(err, PolyfillError::Validation { .. }));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_cancel_endpoints_parse_typed_responses() {
|
||||
let mut server = Server::new_async().await;
|
||||
|
||||
@@ -186,6 +186,16 @@ pub mod deserializers {
|
||||
serde::de::Error::custom("Expected decimal as string/number, got null/empty string")
|
||||
})
|
||||
}
|
||||
|
||||
/// Deserialize a Decimal from string/number/null, defaulting missing-ish values to zero.
|
||||
pub fn decimal_from_string_or_zero<'de, D>(
|
||||
deserializer: D,
|
||||
) -> std::result::Result<Decimal, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
Ok(optional_decimal_from_string(deserializer)?.unwrap_or(Decimal::ZERO))
|
||||
}
|
||||
}
|
||||
|
||||
/// Raw API response types for efficient parsing
|
||||
|
||||
+88
-42
@@ -5,20 +5,20 @@
|
||||
|
||||
use crate::auth::{sign_order_message, SignedOrderMessage};
|
||||
use crate::errors::{PolyfillError, Result};
|
||||
use crate::types::{CreateOrderOptions, MarketOrderArgs, OrderArgs, OrderType, Side, SignedOrderRequest};
|
||||
use crate::types::{
|
||||
CreateOrderOptions, MarketOrderArgs, OrderArgs, OrderType, Side, SignedOrderRequest,
|
||||
};
|
||||
use alloy_primitives::{Address, B256, U256};
|
||||
use alloy_signer_local::PrivateKeySigner;
|
||||
use rand::Rng;
|
||||
use rust_decimal::Decimal;
|
||||
use rust_decimal::prelude::{FromPrimitive, ToPrimitive};
|
||||
use rust_decimal::RoundingStrategy::{AwayFromZero, MidpointTowardZero, ToZero};
|
||||
use std::collections::HashMap;
|
||||
use std::str::FromStr;
|
||||
use std::sync::LazyLock;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
pub const BYTES32_ZERO: &str =
|
||||
"0x0000000000000000000000000000000000000000000000000000000000000000";
|
||||
pub const BYTES32_ZERO: &str = "0x0000000000000000000000000000000000000000000000000000000000000000";
|
||||
|
||||
/// Signature types for orders
|
||||
#[derive(Copy, Clone)]
|
||||
@@ -133,7 +133,7 @@ fn parse_round_config(tick_size: Decimal) -> Result<&'static RoundConfig> {
|
||||
.ok_or_else(|| PolyfillError::validation(format!("Unsupported tick size {tick_size}")))
|
||||
}
|
||||
|
||||
fn validate_bytes32_hex(field: &str, value: &str) -> Result<()> {
|
||||
pub(crate) fn validate_bytes32_hex(field: &str, value: &str) -> Result<()> {
|
||||
if value == BYTES32_ZERO {
|
||||
return Ok(());
|
||||
}
|
||||
@@ -176,39 +176,47 @@ pub fn adjust_buy_amount_for_fees(
|
||||
user_usdc_balance: Decimal,
|
||||
fee_rate: Decimal,
|
||||
fee_exponent: u32,
|
||||
builder_taker_fee_rate_bps: Decimal,
|
||||
builder_taker_fee_rate: Decimal,
|
||||
) -> Result<Decimal> {
|
||||
let price_f64 = price
|
||||
.to_f64()
|
||||
.ok_or_else(|| PolyfillError::validation(format!("Invalid price {price}")))?;
|
||||
let amount_f64 = amount
|
||||
.to_f64()
|
||||
.ok_or_else(|| PolyfillError::validation(format!("Invalid amount {amount}")))?;
|
||||
let user_balance_f64 = user_usdc_balance.to_f64().ok_or_else(|| {
|
||||
PolyfillError::validation(format!("Invalid user_usdc_balance {user_usdc_balance}"))
|
||||
})?;
|
||||
let fee_rate_f64 = fee_rate
|
||||
.to_f64()
|
||||
.ok_or_else(|| PolyfillError::validation(format!("Invalid fee rate {fee_rate}")))?;
|
||||
let builder_rate_f64 = builder_taker_fee_rate_bps
|
||||
.to_f64()
|
||||
.ok_or_else(|| PolyfillError::validation(format!(
|
||||
"Invalid builder taker fee rate {builder_taker_fee_rate_bps}"
|
||||
)))?
|
||||
/ 10_000.0;
|
||||
if price <= Decimal::ZERO {
|
||||
return Err(PolyfillError::validation(
|
||||
"Market buy fee adjustment requires a positive price",
|
||||
));
|
||||
}
|
||||
|
||||
let platform_fee_rate = fee_rate_f64 * (price_f64 * (1.0 - price_f64)).powi(fee_exponent as i32);
|
||||
let platform_fee = (amount_f64 / price_f64) * platform_fee_rate;
|
||||
let total_cost = amount_f64 + platform_fee + amount_f64 * builder_rate_f64;
|
||||
let base = price * (Decimal::ONE - price);
|
||||
let base_f64: f64 = base
|
||||
.try_into()
|
||||
.map_err(|_| PolyfillError::validation(format!("Invalid fee base {base}")))?;
|
||||
let exp_f64: f64 = Decimal::from(fee_exponent)
|
||||
.try_into()
|
||||
.map_err(|_| PolyfillError::validation(format!("Invalid fee exponent {fee_exponent}")))?;
|
||||
let platform_fee_rate = fee_rate
|
||||
* Decimal::try_from(base_f64.powf(exp_f64)).map_err(|_| {
|
||||
PolyfillError::validation(format!(
|
||||
"Invalid platform fee rate for price {price} and exponent {fee_exponent}"
|
||||
))
|
||||
})?;
|
||||
|
||||
let adjusted = if user_balance_f64 <= total_cost {
|
||||
user_balance_f64 / (1.0 + platform_fee_rate / price_f64 + builder_rate_f64)
|
||||
let platform_fee = amount / price * platform_fee_rate;
|
||||
let total_cost = amount + platform_fee + amount * builder_taker_fee_rate;
|
||||
|
||||
let raw = if user_usdc_balance <= total_cost {
|
||||
let divisor = Decimal::ONE + platform_fee_rate / price + builder_taker_fee_rate;
|
||||
user_usdc_balance / divisor
|
||||
} else {
|
||||
amount_f64
|
||||
amount
|
||||
};
|
||||
|
||||
Decimal::from_f64(adjusted)
|
||||
.ok_or_else(|| PolyfillError::validation("Adjusted market buy amount is out of range"))
|
||||
let adjusted = raw.trunc_with_scale(6);
|
||||
if adjusted.is_zero() {
|
||||
return Err(PolyfillError::validation(format!(
|
||||
"user_usdc_balance {user_usdc_balance} too small to cover fees at price {price}; \
|
||||
fee-adjusted amount truncated to zero"
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(adjusted)
|
||||
}
|
||||
|
||||
impl OrderBuilder {
|
||||
@@ -290,7 +298,8 @@ impl OrderBuilder {
|
||||
match side {
|
||||
Side::BUY => {
|
||||
let raw_maker_amt = amount.round_dp_with_strategy(round_config.size, ToZero);
|
||||
let raw_taker_amt = self.fix_amount_rounding(raw_maker_amt / raw_price, round_config);
|
||||
let raw_taker_amt =
|
||||
self.fix_amount_rounding(raw_maker_amt / raw_price, round_config);
|
||||
|
||||
(
|
||||
decimal_to_token_u32(raw_maker_amt),
|
||||
@@ -299,7 +308,8 @@ impl OrderBuilder {
|
||||
},
|
||||
Side::SELL => {
|
||||
let raw_maker_amt = amount.round_dp_with_strategy(round_config.size, ToZero);
|
||||
let raw_taker_amt = self.fix_amount_rounding(raw_maker_amt * raw_price, round_config);
|
||||
let raw_taker_amt =
|
||||
self.fix_amount_rounding(raw_maker_amt * raw_price, round_config);
|
||||
|
||||
(
|
||||
decimal_to_token_u32(raw_maker_amt),
|
||||
@@ -357,9 +367,9 @@ impl OrderBuilder {
|
||||
));
|
||||
}
|
||||
|
||||
let tick_size = options.tick_size.ok_or_else(|| {
|
||||
PolyfillError::validation("Cannot create order without tick size")
|
||||
})?;
|
||||
let tick_size = options
|
||||
.tick_size
|
||||
.ok_or_else(|| PolyfillError::validation("Cannot create order without tick size"))?;
|
||||
let round_config = parse_round_config(tick_size)?;
|
||||
|
||||
let (maker_amount, taker_amount) =
|
||||
@@ -396,9 +406,9 @@ impl OrderBuilder {
|
||||
order_args: &OrderArgs,
|
||||
options: &CreateOrderOptions,
|
||||
) -> Result<SignedOrderRequest> {
|
||||
let tick_size = options.tick_size.ok_or_else(|| {
|
||||
PolyfillError::validation("Cannot create order without tick size")
|
||||
})?;
|
||||
let tick_size = options
|
||||
.tick_size
|
||||
.ok_or_else(|| PolyfillError::validation("Cannot create order without tick size"))?;
|
||||
let round_config = parse_round_config(tick_size)?;
|
||||
|
||||
let (maker_amount, taker_amount) = self.get_order_amounts(
|
||||
@@ -541,8 +551,14 @@ mod tests {
|
||||
fn test_get_contract_config() {
|
||||
// Test Polygon mainnet
|
||||
let config = get_contract_config(137, false).expect("polygon config");
|
||||
assert_eq!(config.exchange, "0xE111180000d2663C0091e4f400237545B87B996B");
|
||||
assert_eq!(config.collateral, "0xC011a7E12a19f7B1f670d46F03B03f3342E82DFB");
|
||||
assert_eq!(
|
||||
config.exchange,
|
||||
"0xE111180000d2663C0091e4f400237545B87B996B"
|
||||
);
|
||||
assert_eq!(
|
||||
config.collateral,
|
||||
"0xC011a7E12a19f7B1f670d46F03B03f3342E82DFB"
|
||||
);
|
||||
assert_eq!(
|
||||
config.conditional_tokens,
|
||||
"0x4D97DCd97eC945f40cF65F87097ACe5EA0476045"
|
||||
@@ -641,6 +657,36 @@ mod tests {
|
||||
assert!(!order.timestamp.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_adjust_buy_amount_for_fees_uses_builder_rate_decimal() {
|
||||
let adjusted = adjust_buy_amount_for_fees(
|
||||
Decimal::from_str("100").unwrap(),
|
||||
Decimal::from_str("0.5").unwrap(),
|
||||
Decimal::from_str("100").unwrap(),
|
||||
Decimal::ZERO,
|
||||
0,
|
||||
Decimal::from_str("0.01").unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(adjusted, Decimal::from_str("99.009900").unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_adjust_buy_amount_for_fees_rejects_zero_after_truncation() {
|
||||
let err = adjust_buy_amount_for_fees(
|
||||
Decimal::from_str("1").unwrap(),
|
||||
Decimal::from_str("0.5").unwrap(),
|
||||
Decimal::from_str("0.0000009").unwrap(),
|
||||
Decimal::ZERO,
|
||||
0,
|
||||
Decimal::ZERO,
|
||||
)
|
||||
.unwrap_err();
|
||||
|
||||
assert!(matches!(err, PolyfillError::Validation { .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_market_order_amounts_differ_for_buy_and_sell() {
|
||||
let builder = test_builder();
|
||||
|
||||
+35
-5
@@ -669,6 +669,7 @@ pub struct ClobTokenInfo {
|
||||
/// Fee details returned by `GET /clob-markets/{condition_id}`.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct ClobFeeDetails {
|
||||
#[serde(deserialize_with = "crate::decode::deserializers::decimal_from_string")]
|
||||
pub r: Decimal,
|
||||
pub e: u32,
|
||||
#[serde(default)]
|
||||
@@ -678,17 +679,33 @@ pub struct ClobFeeDetails {
|
||||
/// CLOB market info returned by `GET /clob-markets/{condition_id}`.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct ClobMarketInfo {
|
||||
#[serde(default)]
|
||||
pub c: Option<String>,
|
||||
#[serde(default)]
|
||||
pub gst: Option<String>,
|
||||
#[serde(default)]
|
||||
pub r: serde_json::Value,
|
||||
#[serde(default)]
|
||||
pub t: Vec<ClobTokenInfo>,
|
||||
#[serde(deserialize_with = "crate::decode::deserializers::decimal_from_string")]
|
||||
#[serde(
|
||||
default,
|
||||
deserialize_with = "crate::decode::deserializers::decimal_from_string_or_zero"
|
||||
)]
|
||||
pub mos: Decimal,
|
||||
#[serde(deserialize_with = "crate::decode::deserializers::decimal_from_string")]
|
||||
#[serde(
|
||||
default,
|
||||
deserialize_with = "crate::decode::deserializers::decimal_from_string_or_zero"
|
||||
)]
|
||||
pub mts: Decimal,
|
||||
#[serde(deserialize_with = "crate::decode::deserializers::decimal_from_string")]
|
||||
#[serde(
|
||||
default,
|
||||
deserialize_with = "crate::decode::deserializers::decimal_from_string_or_zero"
|
||||
)]
|
||||
pub mbf: Decimal,
|
||||
#[serde(deserialize_with = "crate::decode::deserializers::decimal_from_string")]
|
||||
#[serde(
|
||||
default,
|
||||
deserialize_with = "crate::decode::deserializers::decimal_from_string_or_zero"
|
||||
)]
|
||||
pub tbf: Decimal,
|
||||
#[serde(default)]
|
||||
pub rfqe: bool,
|
||||
@@ -700,10 +717,23 @@ pub struct ClobMarketInfo {
|
||||
pub nr: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub fd: Option<ClobFeeDetails>,
|
||||
#[serde(default, deserialize_with = "crate::decode::deserializers::optional_number_from_string")]
|
||||
#[serde(
|
||||
default,
|
||||
deserialize_with = "crate::decode::deserializers::optional_number_from_string"
|
||||
)]
|
||||
pub oas: Option<u64>,
|
||||
}
|
||||
|
||||
/// Builder fee response returned by `GET /fees/builder-fees/{builder_code}`.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BuilderFeeRateResponse {
|
||||
#[serde(alias = "builder_maker_fee_rate_bps")]
|
||||
pub builder_maker_fee_rate_bps: u32,
|
||||
#[serde(alias = "builder_taker_fee_rate_bps")]
|
||||
pub builder_taker_fee_rate_bps: u32,
|
||||
}
|
||||
|
||||
/// Market information
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Market {
|
||||
|
||||
@@ -31,7 +31,10 @@ fn bootstrap_client(private_key: &str) -> ClobClient {
|
||||
.expect("failed to build bootstrap client")
|
||||
}
|
||||
|
||||
fn authenticated_client(private_key: String, api_credentials: polyfill_rs::ApiCredentials) -> ClobClient {
|
||||
fn authenticated_client(
|
||||
private_key: String,
|
||||
api_credentials: polyfill_rs::ApiCredentials,
|
||||
) -> ClobClient {
|
||||
ClobClient::from_config(ClientConfig {
|
||||
base_url: HOST.to_string(),
|
||||
chain: CHAIN_ID,
|
||||
|
||||
@@ -8,7 +8,9 @@
|
||||
#![cfg(feature = "stream")]
|
||||
|
||||
use futures::StreamExt;
|
||||
use polyfill_rs::{ClientConfig, ClobClient, OrderBookManager, WebSocketStream, WsBookUpdateProcessor};
|
||||
use polyfill_rs::{
|
||||
ClientConfig, ClobClient, OrderBookManager, WebSocketStream, WsBookUpdateProcessor,
|
||||
};
|
||||
use std::env;
|
||||
use std::time::Duration;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user