Skip to content

Commit ab21059

Browse files
committed
feat: add missing utility endpoints
1 parent a61aef8 commit ab21059

7 files changed

Lines changed: 905 additions & 7 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ serde_json = "1.0.149"
6868
serde_path_to_error = { version = "0.1", optional = true }
6969
serde_repr = "0.1.20"
7070
serde_with = { version = "3.18.0", features = ["chrono_0_4", "json"] }
71+
sha1 = "0.10.6"
7172
sha2 = "0.10.9"
7273
strum_macros = "0.28.0"
7374
tokio = { version = "1.50.0", features = ["rt-multi-thread", "macros"], optional = true }

specs/scope/feature-parity-py-clob-client-v2.md

Lines changed: 336 additions & 0 deletions
Large diffs are not rendered by default.

src/clob/client.rs

Lines changed: 184 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,13 @@ use crate::clob::types::request::{
3636
};
3737
use crate::clob::types::response::{
3838
ApiKeysResponse, BalanceAllowanceResponse, BanStatusResponse, BuilderApiKeyResponse,
39-
BuilderTradeResponse, CancelOrdersResponse, CurrentRewardResponse, FeeRateResponse,
40-
GeoblockResponse, HeartbeatResponse, LastTradePriceResponse, LastTradesPricesResponse,
41-
MarketResponse, MarketRewardResponse, MidpointResponse, MidpointsResponse, NegRiskResponse,
42-
NotificationResponse, OpenOrderResponse, OrderBookSummaryResponse, OrderScoringResponse,
43-
OrdersScoringResponse, Page, PostOrderResponse, PriceHistoryResponse, PriceResponse,
44-
PricesResponse, RewardsPercentagesResponse, SimplifiedMarketResponse, SpreadResponse,
39+
BuilderFeeRateResponse, BuilderTradeResponse, CancelOrdersResponse, ClobMarketInfoResponse,
40+
CurrentRewardResponse, FeeRateResponse, GeoblockResponse, HeartbeatResponse,
41+
LastTradePriceResponse, LastTradesPricesResponse, MarketResponse, MarketRewardResponse,
42+
MidpointResponse, MidpointsResponse, NegRiskResponse, NotificationResponse,
43+
OpenOrderResponse, OrderBookSummaryResponse, OrderScoringResponse, OrdersScoringResponse,
44+
Page, PostOrderResponse, PriceHistoryResponse, PriceResponse, PricesResponse,
45+
ReadonlyApiKeyResponse, RewardsPercentagesResponse, SimplifiedMarketResponse, SpreadResponse,
4546
SpreadsResponse, TickSizeResponse, TotalUserEarningResponse, TradeResponse,
4647
UserEarningResponse, UserRewardsEarningResponse,
4748
};
@@ -54,7 +55,7 @@ use crate::clob::types::{
5455
};
5556
use crate::clob::types::{SignableOrder, SignatureType, SignedOrder, TickSize};
5657
use crate::error::{Error, Kind as ErrorKind, Synchronization};
57-
use crate::types::Address;
58+
use crate::types::{Address, B256};
5859
use crate::{
5960
AMOY, POLYGON, Result, Timestamp, ToQueryParams as _, auth, contract_config,
6061
derive_proxy_wallet, derive_safe_wallet,
@@ -222,6 +223,7 @@ impl<S: Signer, K: Kind> AuthenticationBuilder<'_, S, K> {
222223
tick_sizes: inner.tick_sizes,
223224
neg_risk: inner.neg_risk,
224225
fee_rate_bps: inner.fee_rate_bps,
226+
builder_fee_rates: inner.builder_fee_rates,
225227
funder,
226228
signature_type: self.signature_type.unwrap_or(SignatureType::Eoa),
227229
salt_generator: self.salt_generator.unwrap_or(generate_seed),
@@ -405,6 +407,8 @@ struct ClientInner<S: State> {
405407
neg_risk: DashMap<U256, bool>,
406408
/// Local cache representing the fee rate in basis points per token ID
407409
fee_rate_bps: DashMap<U256, u32>,
410+
/// Local cache of builder fee rates per builder code
411+
builder_fee_rates: DashMap<B256, BuilderFeeRateResponse>,
408412
/// The funder for this [`ClientInner`]. If funder is present, then `signature_type` cannot
409413
/// be [`SignatureType::Eoa`]. Conversely, if funder is absent, then `signature_type` cannot be
410414
/// [`SignatureType::Proxy`] or [`SignatureType::GnosisSafe`].
@@ -513,6 +517,7 @@ impl<S: State> Client<S> {
513517
self.inner.tick_sizes.clear();
514518
self.inner.fee_rate_bps.clear();
515519
self.inner.neg_risk.clear();
520+
self.inner.builder_fee_rates.clear();
516521
}
517522

518523
/// Pre-populates the tick size cache for a token, avoiding the HTTP call.
@@ -1170,6 +1175,60 @@ impl<S: State> Client<S> {
11701175
}
11711176
}
11721177

1178+
/// Returns combined CLOB market info for a condition ID.
1179+
///
1180+
/// This endpoint (`GET /clob-markets/{condition_id}`) returns tick size, neg risk,
1181+
/// fee rate, and token data in a single call. It also populates the local caches
1182+
/// for tick size, neg risk, and fee rate for all tokens in the market.
1183+
///
1184+
/// # Errors
1185+
///
1186+
/// Returns an error if the HTTP request fails or the response cannot be parsed.
1187+
pub async fn clob_market_info(
1188+
&self,
1189+
condition_id: &str,
1190+
) -> Result<ClobMarketInfoResponse> {
1191+
let request = self
1192+
.client()
1193+
.request(
1194+
Method::GET,
1195+
format!("{}clob-markets/{condition_id}", self.host()),
1196+
)
1197+
.build()?;
1198+
1199+
let response: ClobMarketInfoResponse =
1200+
crate::request(&self.inner.client, request, None).await?;
1201+
1202+
// Prime local caches from the response
1203+
for token in &response.tokens {
1204+
self.inner.tick_sizes.insert(token.token_id, response.min_tick_size);
1205+
self.inner.neg_risk.insert(token.token_id, response.neg_risk);
1206+
self.inner.fee_rate_bps.insert(token.token_id, response.fee_rate_bps);
1207+
}
1208+
1209+
Ok(response)
1210+
}
1211+
1212+
/// Looks up a market by token ID.
1213+
///
1214+
/// This is used internally for cache priming when the condition ID for a token
1215+
/// is not yet known.
1216+
///
1217+
/// # Errors
1218+
///
1219+
/// Returns an error if the HTTP request fails or the response cannot be parsed.
1220+
pub async fn market_by_token(&self, token_id: U256) -> Result<MarketResponse> {
1221+
let request = self
1222+
.client()
1223+
.request(
1224+
Method::GET,
1225+
format!("{}markets-by-token/{token_id}", self.host()),
1226+
)
1227+
.build()?;
1228+
1229+
crate::request(&self.inner.client, request, None).await
1230+
}
1231+
11731232
fn client(&self) -> &ReqwestClient {
11741233
&self.inner.client
11751234
}
@@ -1227,6 +1286,7 @@ impl Client<Unauthenticated> {
12271286
tick_sizes: DashMap::new(),
12281287
neg_risk: DashMap::new(),
12291288
fee_rate_bps: DashMap::new(),
1289+
builder_fee_rates: DashMap::new(),
12301290
state: Unauthenticated,
12311291
funder: None,
12321292
signature_type: SignatureType::Eoa,
@@ -1339,6 +1399,7 @@ impl<K: Kind> Client<Authenticated<K>> {
13391399
tick_sizes: inner.tick_sizes,
13401400
neg_risk: inner.neg_risk,
13411401
fee_rate_bps: inner.fee_rate_bps,
1402+
builder_fee_rates: inner.builder_fee_rates,
13421403
// Reset the order parameters that were previously stored on the client
13431404
funder: None,
13441405
signature_type: SignatureType::Eoa,
@@ -1992,6 +2053,121 @@ impl<K: Kind> Client<Authenticated<K>> {
19922053
crate::request(&self.inner.client, request, Some(headers)).await
19932054
}
19942055

2056+
/// Creates a new read-only API key.
2057+
///
2058+
/// Read-only keys can access account data but cannot place or cancel orders.
2059+
///
2060+
/// # Errors
2061+
///
2062+
/// Returns an error if the HTTP request fails or the response cannot be parsed.
2063+
pub async fn create_readonly_api_key(&self) -> Result<ReadonlyApiKeyResponse> {
2064+
let request = self
2065+
.client()
2066+
.request(Method::POST, format!("{}auth/readonly-api-key", self.host()))
2067+
.build()?;
2068+
let headers = self.create_headers(&request).await?;
2069+
2070+
crate::request(&self.inner.client, request, Some(headers)).await
2071+
}
2072+
2073+
/// Lists all read-only API keys for the authenticated user.
2074+
///
2075+
/// # Errors
2076+
///
2077+
/// Returns an error if the HTTP request fails or the response cannot be parsed.
2078+
pub async fn readonly_api_keys(&self) -> Result<Vec<ReadonlyApiKeyResponse>> {
2079+
let request = self
2080+
.client()
2081+
.request(Method::GET, format!("{}auth/readonly-api-keys", self.host()))
2082+
.build()?;
2083+
let headers = self.create_headers(&request).await?;
2084+
2085+
crate::request(&self.inner.client, request, Some(headers)).await
2086+
}
2087+
2088+
/// Deletes a read-only API key.
2089+
///
2090+
/// # Errors
2091+
///
2092+
/// Returns an error if the HTTP request fails or the key cannot be deleted.
2093+
pub async fn delete_readonly_api_key(&self, key: &str) -> Result<()> {
2094+
let mut request = self
2095+
.client()
2096+
.request(
2097+
Method::DELETE,
2098+
format!("{}auth/readonly-api-key", self.host()),
2099+
)
2100+
.json(&serde_json::json!({ "key": key }))
2101+
.build()?;
2102+
let headers = self.create_headers(&request).await?;
2103+
2104+
*request.headers_mut() = headers;
2105+
self.inner.client.execute(request).await?;
2106+
2107+
Ok(())
2108+
}
2109+
2110+
/// Gets pre-migration orders (legacy V1 orders) for the authenticated user.
2111+
///
2112+
/// # Errors
2113+
///
2114+
/// Returns an error if the HTTP request fails or the response cannot be parsed.
2115+
pub async fn pre_migration_orders(
2116+
&self,
2117+
next_cursor: Option<String>,
2118+
) -> Result<Page<OpenOrderResponse>> {
2119+
let cursor = next_cursor
2120+
.map(|c| format!("?next_cursor={c}"))
2121+
.unwrap_or_default();
2122+
2123+
let request = self
2124+
.client()
2125+
.request(
2126+
Method::GET,
2127+
format!("{}data/pre-migration-orders{cursor}", self.host()),
2128+
)
2129+
.build()?;
2130+
let headers = self.create_headers(&request).await?;
2131+
2132+
crate::request(&self.inner.client, request, Some(headers)).await
2133+
}
2134+
2135+
/// Gets the builder fee rate for a given builder code, with local caching.
2136+
///
2137+
/// The server returns fee rates in basis points. The response contains the raw
2138+
/// BPS values; callers should divide by 10000 to get decimal rates.
2139+
///
2140+
/// Results are cached per builder code. Use [`Client::invalidate_internal_caches`]
2141+
/// to clear.
2142+
///
2143+
/// # Errors
2144+
///
2145+
/// Returns an error if the HTTP request fails or the response cannot be parsed.
2146+
pub async fn builder_fee_rate(
2147+
&self,
2148+
builder_code: B256,
2149+
) -> Result<BuilderFeeRateResponse> {
2150+
if let Some(cached) = self.inner.builder_fee_rates.get(&builder_code) {
2151+
return Ok(cached.clone());
2152+
}
2153+
2154+
let request = self
2155+
.client()
2156+
.request(
2157+
Method::GET,
2158+
format!("{}fees/builder-fees/{builder_code}", self.host()),
2159+
)
2160+
.build()?;
2161+
let headers = self.create_headers(&request).await?;
2162+
2163+
let response: BuilderFeeRateResponse =
2164+
crate::request(&self.inner.client, request, Some(headers)).await?;
2165+
2166+
self.inner.builder_fee_rates.insert(builder_code, response.clone());
2167+
2168+
Ok(response)
2169+
}
2170+
19952171
/// Creates a new Builder API key for order attribution.
19962172
///
19972173
/// Builder API keys allow you to attribute orders to your builder account,
@@ -2202,6 +2378,7 @@ impl Client<Authenticated<Normal>> {
22022378
tick_sizes: inner.tick_sizes,
22032379
neg_risk: inner.neg_risk,
22042380
fee_rate_bps: inner.fee_rate_bps,
2381+
builder_fee_rates: inner.builder_fee_rates,
22052382
funder: inner.funder,
22062383
signature_type: inner.signature_type,
22072384
salt_generator: inner.salt_generator,

src/clob/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@
155155
pub mod client;
156156
pub mod order_builder;
157157
pub mod types;
158+
pub mod utilities;
158159
#[cfg(feature = "ws")]
159160
pub mod ws;
160161

src/clob/types/response.rs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -707,6 +707,67 @@ pub struct Page<T> {
707707
pub count: u64,
708708
}
709709

710+
/// Response from creating or listing read-only API keys.
711+
#[non_exhaustive]
712+
#[derive(Clone, Debug, Deserialize, Builder, PartialEq)]
713+
#[serde(rename_all = "camelCase")]
714+
pub struct ReadonlyApiKeyResponse {
715+
pub key: String,
716+
#[serde(default)]
717+
pub created_at: Option<DateTime<Utc>>,
718+
}
719+
720+
/// Response from the CLOB market info endpoint (`/clob-markets/{condition_id}`).
721+
///
722+
/// Provides combined market configuration data that can be used to prime local caches.
723+
#[non_exhaustive]
724+
#[serde_as]
725+
#[derive(Clone, Debug, Deserialize, Builder, PartialEq)]
726+
#[serde(rename_all = "camelCase")]
727+
pub struct ClobMarketInfoResponse {
728+
/// The market condition ID.
729+
#[serde_as(as = "NoneAsEmptyString")]
730+
#[serde(default)]
731+
pub condition_id: Option<B256>,
732+
/// Tokens in this market.
733+
#[serde(default)]
734+
#[serde_as(deserialize_as = "DefaultOnNull")]
735+
pub tokens: Vec<Token>,
736+
/// Minimum tick size for price increments.
737+
#[serde_as(as = "TryFromInto<Decimal>")]
738+
pub min_tick_size: TickSize,
739+
/// Whether this market uses the `NegRisk` adapter.
740+
pub neg_risk: bool,
741+
/// Trading fee in basis points.
742+
#[serde(default)]
743+
pub fee_rate_bps: u32,
744+
/// Fee exponent for the platform fee formula.
745+
#[serde(default)]
746+
pub fee_exponent: Option<u32>,
747+
/// Maker base fee.
748+
#[serde(default)]
749+
pub maker_base_fee: Option<Decimal>,
750+
/// Taker base fee.
751+
#[serde(default)]
752+
pub taker_base_fee: Option<Decimal>,
753+
}
754+
755+
/// Builder fee rates for a given builder code.
756+
///
757+
/// The server returns rates in basis points; the client converts to decimal rates
758+
/// by dividing by 10000.
759+
#[non_exhaustive]
760+
#[derive(Clone, Debug, Deserialize, Builder, PartialEq)]
761+
#[serde(rename_all = "camelCase")]
762+
pub struct BuilderFeeRateResponse {
763+
/// Builder maker fee rate in basis points.
764+
#[serde(alias = "builder_maker_fee_rate_bps")]
765+
pub builder_maker_fee_rate_bps: u32,
766+
/// Builder taker fee rate in basis points.
767+
#[serde(alias = "builder_taker_fee_rate_bps")]
768+
pub builder_taker_fee_rate_bps: u32,
769+
}
770+
710771
/// Response from creating an RFQ request.
711772
#[cfg(feature = "rfq")]
712773
#[non_exhaustive]

0 commit comments

Comments
 (0)