fix: align V2 client with official SDK fee behavior

This commit is contained in:
floor-licker
2026-04-27 22:36:57 -03:00
parent e78ae17286
commit e5994d04dd
6 changed files with 263 additions and 80 deletions
+123 -31
View File
@@ -5,13 +5,11 @@
use crate::auth::{create_l1_headers, create_l2_headers}; use crate::auth::{create_l1_headers, create_l2_headers};
use crate::errors::{PolyfillError, Result}; use crate::errors::{PolyfillError, Result};
use crate::http_config::{ use crate::http_config::{create_colocated_client, create_internet_client, prewarm_connections};
create_colocated_client, create_internet_client, prewarm_connections,
};
use crate::types::{ use crate::types::{
CancelOrdersResponse, ClientConfig, ClobMarketInfo, CreateOrderOptions, MarketOrderArgs, BuilderFeeRateResponse, CancelOrdersResponse, ClientConfig, ClobMarketInfo, CreateOrderOptions,
OrderArgs, OrderType, PostOrder, PostOrderOptions, PostOrderResponse, SignedOrderRequest, MarketOrderArgs, OrderArgs, OrderType, PostOrder, PostOrderOptions, PostOrderResponse, Side,
Side, SignedOrderRequest,
}; };
use alloy_primitives::U256; use alloy_primitives::U256;
use alloy_signer_local::PrivateKeySigner; use alloy_signer_local::PrivateKeySigner;
@@ -96,7 +94,10 @@ impl ClobClient {
}); });
let connection_manager = Some(std::sync::Arc::new( 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)); 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)) .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<()> { fn validate_prices_history_asset_id(asset_id: &str) -> Result<()> {
if asset_id.is_empty() { if asset_id.is_empty() {
return Err(PolyfillError::validation( return Err(PolyfillError::validation(
@@ -974,12 +1010,21 @@ impl ClobClient {
if order_args.side == Side::BUY { if order_args.side == Side::BUY {
if let Some(user_balance) = order_args.user_usdc_balance { 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 { let fee_details = market_info.fd.unwrap_or(crate::types::ClobFeeDetails {
r: Decimal::ZERO, r: Decimal::ZERO,
e: 0, e: 0,
to: false, 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 = crate::orders::adjust_buy_amount_for_fees(
order_args.amount, order_args.amount,
@@ -987,11 +1032,7 @@ impl ClobClient {
user_balance, user_balance,
fee_details.r, fee_details.r,
fee_details.e, fee_details.e,
if order_args.builder_code.is_some() { builder_taker_fee_rate,
market_info.tbf
} else {
Decimal::ZERO
},
)?; )?;
} }
} }
@@ -1029,6 +1070,17 @@ impl ClobClient {
"post_only is not supported for FOK/FAK orders", "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 // Owner field must reference the credential principal identifier
// to maintain consistency with the authentication context layer // to maintain consistency with the authentication context layer
@@ -2362,8 +2414,7 @@ mod tests {
base_url: base_url.to_string(), base_url: base_url.to_string(),
chain: 137, chain: 137,
private_key: Some( private_key: Some(
"0x1234567890123456789012345678901234567890123456789012345678901234" "0x1234567890123456789012345678901234567890123456789012345678901234".to_string(),
.to_string(),
), ),
..ClientConfig::default() ..ClientConfig::default()
}) })
@@ -2382,8 +2433,7 @@ mod tests {
base_url: base_url.to_string(), base_url: base_url.to_string(),
chain: 137, chain: 137,
private_key: Some( private_key: Some(
"0x1234567890123456789012345678901234567890123456789012345678901234" "0x1234567890123456789012345678901234567890123456789012345678901234".to_string(),
.to_string(),
), ),
api_credentials: Some(api_creds), api_credentials: Some(api_creds),
..ClientConfig::default() ..ClientConfig::default()
@@ -2403,12 +2453,10 @@ mod tests {
side: "BUY".to_string(), side: "BUY".to_string(),
signature_type: 0, signature_type: 0,
timestamp: "1713916800000".to_string(), timestamp: "1713916800000".to_string(),
metadata: metadata: "0x0000000000000000000000000000000000000000000000000000000000000000"
"0x0000000000000000000000000000000000000000000000000000000000000000" .to_string(),
.to_string(), builder: "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
builder: .to_string(),
"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
.to_string(),
signature: "0xdeadbeef".to_string(), signature: "0xdeadbeef".to_string(),
} }
} }
@@ -2441,8 +2489,7 @@ mod tests {
base_url: "https://test.example.com".to_string(), base_url: "https://test.example.com".to_string(),
chain: 137, chain: 137,
private_key: Some( private_key: Some(
"0x1234567890123456789012345678901234567890123456789012345678901234" "0x1234567890123456789012345678901234567890123456789012345678901234".to_string(),
.to_string(),
), ),
api_credentials: Some(api_creds.clone()), api_credentials: Some(api_creds.clone()),
..ClientConfig::default() ..ClientConfig::default()
@@ -3121,13 +3168,11 @@ mod tests {
.with_header("content-type", "application/json") .with_header("content-type", "application/json")
.with_body( .with_body(
r#"{ r#"{
"c":"0x1111111111111111111111111111111111111111111111111111111111111111",
"gst":"ready", "gst":"ready",
"r":{},
"t":[{"t":"123","o":"YES"},{"t":"456","o":"NO"}], "t":[{"t":"123","o":"YES"},{"t":"456","o":"NO"}],
"mos":"5", "mos":"5",
"mts":"0.01", "mts":"0.01",
"mbf":"0",
"tbf":"15",
"rfqe":true, "rfqe":true,
"itode":false, "itode":false,
"ibce":false, "ibce":false,
@@ -3143,13 +3188,42 @@ mod tests {
let info = client.get_clob_market_info("condition-1").await.unwrap(); let info = client.get_clob_market_info("condition-1").await.unwrap();
mock.assert_async().await; mock.assert_async().await;
assert_eq!(
info.c.as_deref(),
Some("0x1111111111111111111111111111111111111111111111111111111111111111")
);
assert_eq!(info.t.len(), 2); assert_eq!(info.t.len(), 2);
assert_eq!(info.mos, Decimal::from_str("5").unwrap()); assert_eq!(info.mos, Decimal::from_str("5").unwrap());
assert_eq!(info.mts, Decimal::from_str("0.01").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); 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")] #[tokio::test(flavor = "multi_thread")]
async fn test_post_order_uses_v2_wire_shape_and_typed_response() { async fn test_post_order_uses_v2_wire_shape_and_typed_response() {
let mut server = Server::new_async().await; let mut server = Server::new_async().await;
@@ -3173,7 +3247,7 @@ mod tests {
"signature": "0xdeadbeef" "signature": "0xdeadbeef"
}, },
"owner": "test_key", "owner": "test_key",
"orderType": "GTC", "orderType": "GTD",
"postOnly": true, "postOnly": true,
"deferExec": true "deferExec": true
}) })
@@ -3201,7 +3275,7 @@ mod tests {
.post_order( .post_order(
sample_signed_order(), sample_signed_order(),
Some(&PostOrderOptions { Some(&PostOrderOptions {
order_type: OrderType::GTC, order_type: OrderType::GTD,
post_only: true, post_only: true,
defer_exec: true, defer_exec: true,
}), }),
@@ -3235,6 +3309,24 @@ mod tests {
assert!(matches!(err, PolyfillError::Validation { .. })); 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")] #[tokio::test(flavor = "multi_thread")]
async fn test_cancel_endpoints_parse_typed_responses() { async fn test_cancel_endpoints_parse_typed_responses() {
let mut server = Server::new_async().await; let mut server = Server::new_async().await;
+10
View File
@@ -186,6 +186,16 @@ pub mod deserializers {
serde::de::Error::custom("Expected decimal as string/number, got null/empty string") 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 /// Raw API response types for efficient parsing
+88 -42
View File
@@ -5,20 +5,20 @@
use crate::auth::{sign_order_message, SignedOrderMessage}; use crate::auth::{sign_order_message, SignedOrderMessage};
use crate::errors::{PolyfillError, Result}; 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_primitives::{Address, B256, U256};
use alloy_signer_local::PrivateKeySigner; use alloy_signer_local::PrivateKeySigner;
use rand::Rng; use rand::Rng;
use rust_decimal::Decimal; use rust_decimal::Decimal;
use rust_decimal::prelude::{FromPrimitive, ToPrimitive};
use rust_decimal::RoundingStrategy::{AwayFromZero, MidpointTowardZero, ToZero}; use rust_decimal::RoundingStrategy::{AwayFromZero, MidpointTowardZero, ToZero};
use std::collections::HashMap; use std::collections::HashMap;
use std::str::FromStr; use std::str::FromStr;
use std::sync::LazyLock; use std::sync::LazyLock;
use std::time::{SystemTime, UNIX_EPOCH}; use std::time::{SystemTime, UNIX_EPOCH};
pub const BYTES32_ZERO: &str = pub const BYTES32_ZERO: &str = "0x0000000000000000000000000000000000000000000000000000000000000000";
"0x0000000000000000000000000000000000000000000000000000000000000000";
/// Signature types for orders /// Signature types for orders
#[derive(Copy, Clone)] #[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}"))) .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 { if value == BYTES32_ZERO {
return Ok(()); return Ok(());
} }
@@ -176,39 +176,47 @@ pub fn adjust_buy_amount_for_fees(
user_usdc_balance: Decimal, user_usdc_balance: Decimal,
fee_rate: Decimal, fee_rate: Decimal,
fee_exponent: u32, fee_exponent: u32,
builder_taker_fee_rate_bps: Decimal, builder_taker_fee_rate: Decimal,
) -> Result<Decimal> { ) -> Result<Decimal> {
let price_f64 = price if price <= Decimal::ZERO {
.to_f64() return Err(PolyfillError::validation(
.ok_or_else(|| PolyfillError::validation(format!("Invalid price {price}")))?; "Market buy fee adjustment requires a positive 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;
let platform_fee_rate = fee_rate_f64 * (price_f64 * (1.0 - price_f64)).powi(fee_exponent as i32); let base = price * (Decimal::ONE - price);
let platform_fee = (amount_f64 / price_f64) * platform_fee_rate; let base_f64: f64 = base
let total_cost = amount_f64 + platform_fee + amount_f64 * builder_rate_f64; .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 { let platform_fee = amount / price * platform_fee_rate;
user_balance_f64 / (1.0 + platform_fee_rate / price_f64 + builder_rate_f64) 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 { } else {
amount_f64 amount
}; };
Decimal::from_f64(adjusted) let adjusted = raw.trunc_with_scale(6);
.ok_or_else(|| PolyfillError::validation("Adjusted market buy amount is out of range")) 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 { impl OrderBuilder {
@@ -290,7 +298,8 @@ impl OrderBuilder {
match side { match side {
Side::BUY => { Side::BUY => {
let raw_maker_amt = amount.round_dp_with_strategy(round_config.size, ToZero); 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), decimal_to_token_u32(raw_maker_amt),
@@ -299,7 +308,8 @@ impl OrderBuilder {
}, },
Side::SELL => { Side::SELL => {
let raw_maker_amt = amount.round_dp_with_strategy(round_config.size, ToZero); 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), decimal_to_token_u32(raw_maker_amt),
@@ -357,9 +367,9 @@ impl OrderBuilder {
)); ));
} }
let tick_size = options.tick_size.ok_or_else(|| { let tick_size = options
PolyfillError::validation("Cannot create order without tick size") .tick_size
})?; .ok_or_else(|| PolyfillError::validation("Cannot create order without tick size"))?;
let round_config = parse_round_config(tick_size)?; let round_config = parse_round_config(tick_size)?;
let (maker_amount, taker_amount) = let (maker_amount, taker_amount) =
@@ -396,9 +406,9 @@ impl OrderBuilder {
order_args: &OrderArgs, order_args: &OrderArgs,
options: &CreateOrderOptions, options: &CreateOrderOptions,
) -> Result<SignedOrderRequest> { ) -> Result<SignedOrderRequest> {
let tick_size = options.tick_size.ok_or_else(|| { let tick_size = options
PolyfillError::validation("Cannot create order without tick size") .tick_size
})?; .ok_or_else(|| PolyfillError::validation("Cannot create order without tick size"))?;
let round_config = parse_round_config(tick_size)?; let round_config = parse_round_config(tick_size)?;
let (maker_amount, taker_amount) = self.get_order_amounts( let (maker_amount, taker_amount) = self.get_order_amounts(
@@ -541,8 +551,14 @@ mod tests {
fn test_get_contract_config() { fn test_get_contract_config() {
// Test Polygon mainnet // Test Polygon mainnet
let config = get_contract_config(137, false).expect("polygon config"); let config = get_contract_config(137, false).expect("polygon config");
assert_eq!(config.exchange, "0xE111180000d2663C0091e4f400237545B87B996B"); assert_eq!(
assert_eq!(config.collateral, "0xC011a7E12a19f7B1f670d46F03B03f3342E82DFB"); config.exchange,
"0xE111180000d2663C0091e4f400237545B87B996B"
);
assert_eq!(
config.collateral,
"0xC011a7E12a19f7B1f670d46F03B03f3342E82DFB"
);
assert_eq!( assert_eq!(
config.conditional_tokens, config.conditional_tokens,
"0x4D97DCd97eC945f40cF65F87097ACe5EA0476045" "0x4D97DCd97eC945f40cF65F87097ACe5EA0476045"
@@ -641,6 +657,36 @@ mod tests {
assert!(!order.timestamp.is_empty()); 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] #[test]
fn test_market_order_amounts_differ_for_buy_and_sell() { fn test_market_order_amounts_differ_for_buy_and_sell() {
let builder = test_builder(); let builder = test_builder();
+35 -5
View File
@@ -669,6 +669,7 @@ pub struct ClobTokenInfo {
/// Fee details returned by `GET /clob-markets/{condition_id}`. /// Fee details returned by `GET /clob-markets/{condition_id}`.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ClobFeeDetails { pub struct ClobFeeDetails {
#[serde(deserialize_with = "crate::decode::deserializers::decimal_from_string")]
pub r: Decimal, pub r: Decimal,
pub e: u32, pub e: u32,
#[serde(default)] #[serde(default)]
@@ -678,17 +679,33 @@ pub struct ClobFeeDetails {
/// CLOB market info returned by `GET /clob-markets/{condition_id}`. /// CLOB market info returned by `GET /clob-markets/{condition_id}`.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ClobMarketInfo { pub struct ClobMarketInfo {
#[serde(default)]
pub c: Option<String>,
#[serde(default)] #[serde(default)]
pub gst: Option<String>, pub gst: Option<String>,
#[serde(default)]
pub r: serde_json::Value, pub r: serde_json::Value,
#[serde(default)]
pub t: Vec<ClobTokenInfo>, 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, 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, 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, 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, pub tbf: Decimal,
#[serde(default)] #[serde(default)]
pub rfqe: bool, pub rfqe: bool,
@@ -700,10 +717,23 @@ pub struct ClobMarketInfo {
pub nr: Option<bool>, pub nr: Option<bool>,
#[serde(default)] #[serde(default)]
pub fd: Option<ClobFeeDetails>, 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>, 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 /// Market information
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Market { pub struct Market {
+4 -1
View File
@@ -31,7 +31,10 @@ fn bootstrap_client(private_key: &str) -> ClobClient {
.expect("failed to build bootstrap client") .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 { ClobClient::from_config(ClientConfig {
base_url: HOST.to_string(), base_url: HOST.to_string(),
chain: CHAIN_ID, chain: CHAIN_ID,
+3 -1
View File
@@ -8,7 +8,9 @@
#![cfg(feature = "stream")] #![cfg(feature = "stream")]
use futures::StreamExt; use futures::StreamExt;
use polyfill_rs::{ClientConfig, ClobClient, OrderBookManager, WebSocketStream, WsBookUpdateProcessor}; use polyfill_rs::{
ClientConfig, ClobClient, OrderBookManager, WebSocketStream, WsBookUpdateProcessor,
};
use std::env; use std::env;
use std::time::Duration; use std::time::Duration;