Skip to content

Commit ada1a20

Browse files
authored
Merge pull request #10 from Polymarket/v2
chore(release): 0.5.0
2 parents e4b08b9 + 672ea09 commit ada1a20

6 files changed

Lines changed: 85 additions & 89 deletions

File tree

Cargo.lock

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

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[package]
22
name = "polymarket_client_sdk_v2"
33
description = "Polymarket CLOB (Central Limit Order Book) API client SDK"
4-
version = "0.4.4"
4+
version = "0.5.0"
55
authors = [
66
"Polymarket Engineering <engineering@polymarket.com>",
77
"Chaz Byrnes <chaz@polymarket.com>",

README.md

Lines changed: 42 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ Add the crate to your `Cargo.toml`:
4848

4949
```toml
5050
[dependencies]
51-
polymarket_client_sdk_v2 = "0.4"
51+
polymarket_client_sdk_v2 = "0.5"
5252
```
5353

5454
or
@@ -83,7 +83,7 @@ Enable features in your `Cargo.toml`:
8383

8484
```toml
8585
[dependencies]
86-
polymarket_client_sdk_v2 = { version = "0.3", features = ["ws", "data"] }
86+
polymarket_client_sdk_v2 = { version = "0.5", features = ["ws", "data"] }
8787
```
8888

8989
## Re-exported Types
@@ -226,7 +226,7 @@ The **signature_type** parameter tells the system how to verify your signatures:
226226
- `signature_type=2`: Browser wallet proxy signatures (when using a proxy contract, not direct wallet connections)
227227
- `signature_type=3`: EIP-1271 smart contract wallet signatures (**V2 orders only**)
228228

229-
See [SignatureType](src/clob/types/mod.rs#L182) for more information.
229+
See [`SignatureType`](src/clob/types/mod.rs) for more information.
230230

231231
##### Place a market order
232232

@@ -303,38 +303,54 @@ async fn main() -> anyhow::Result<()> {
303303
}
304304
```
305305

306-
##### V1 and V2 Orders
306+
##### V1 and V2 protocols
307307

308-
The SDK supports both V1 (legacy) and V2 exchange contracts. **V2 is the default.** V2 orders add `timestamp`,
309-
`metadata`, and `builder` fields, and remove the V1-only `taker`, `nonce`, and `feeRateBps` fields. The EIP-712
310-
domain version is `"2"` for V2 orders.
308+
The SDK supports both V1 (legacy) and V2 exchange contracts. Protocol is **auto-detected** on
309+
the first order build via `GET /version` and cached for the lifetime of the `Client`. You pick
310+
the protocol by pointing the client at the corresponding host:
311+
312+
| Protocol | Host | Collateral | EIP-712 domain version |
313+
|----------|-----------------------------------|--------------|------------------------|
314+
| V2 | `https://clob-v2.polymarket.com` | pUSD | `"2"` |
315+
| V1 | `https://clob.polymarket.com` | USDC.e | `"1"` |
316+
317+
V2 orders add `timestamp`, `metadata`, and `builder` fields. V1 orders use `taker`, `nonce`,
318+
and `feeRateBps` instead. The order builder exposes both sets of fields — the ones that don't
319+
apply to the detected protocol are silently ignored at build time, so you can write one
320+
code-path that works against either server.
321+
322+
V2-specific builder fields (ignored when the server runs V1):
311323

312324
```rust,ignore
313-
use alloy::primitives::B256;
314-
use polymarket_client_sdk_v2::clob::types::OrderVersion;
325+
use polymarket_client_sdk_v2::types::B256;
315326
316-
// V2 order (default) — with metadata and builder attribution
317327
let order = client
318328
.limit_order()
319329
.token_id("<token-id>")
320330
.size(Decimal::ONE_HUNDRED)
321331
.price(dec!(0.5))
322332
.side(Side::Buy)
323-
.metadata(B256::ZERO) // optional: 32 bytes of custom metadata
324-
.builder_code(B256::ZERO) // optional: builder fee attribution
325-
.defer_exec(false) // optional: defer execution
333+
.metadata(B256::ZERO) // 32 bytes of custom metadata
334+
.builder_code(B256::ZERO) // builder fee attribution
335+
.defer_exec(false) // defer execution
326336
.build()
327337
.await?;
338+
```
339+
340+
V1-specific builder fields (ignored when the server runs V2):
341+
342+
```rust,ignore
343+
use polymarket_client_sdk_v2::types::Address;
328344
329-
// V1 order (legacy) — explicitly opt in
330345
let order = client
331346
.limit_order()
332-
.version(OrderVersion::V1)
333347
.token_id("<token-id>")
334348
.size(Decimal::ONE_HUNDRED)
335349
.price(dec!(0.5))
336350
.side(Side::Buy)
337-
.nonce(0) // V1-only field
351+
.taker(Address::ZERO) // explicit taker; default zero = public order
352+
.nonce(0) // on-chain cancel nonce; default 0
353+
.fee_rate_bps(0) // must match the market rate when set
338354
.build()
339355
.await?;
340356
```
@@ -383,26 +399,31 @@ async fn main() -> anyhow::Result<()> {
383399
Real-time orderbook and user event streaming. Requires the `ws` feature.
384400

385401
```toml
386-
polymarket_client_sdk_v2 = { version = "0.3", features = ["ws"] }
402+
polymarket_client_sdk_v2 = { version = "0.5", features = ["ws"] }
387403
```
388404

389405
```rust,ignore
406+
use std::str::FromStr as _;
407+
390408
use futures::StreamExt as _;
391409
use polymarket_client_sdk_v2::clob::ws::Client;
410+
use polymarket_client_sdk_v2::types::U256;
392411
393412
#[tokio::main]
394413
async fn main() -> anyhow::Result<()> {
395414
let client = Client::default();
396415
397-
// Subscribe to orderbook updates for specific assets
398-
let asset_ids = vec!["<asset-id>".to_owned()];
416+
// Subscribe to orderbook updates for specific assets.
417+
let asset_ids = vec![U256::from_str("<asset-id>")?];
399418
let stream = client.subscribe_orderbook(asset_ids)?;
400419
let mut stream = Box::pin(stream);
401420
402421
while let Some(book_result) = stream.next().await {
403422
let book = book_result?;
404-
println!("Orderbook update for {}: {} bids, {} asks",
405-
book.asset_id, book.bids.len(), book.asks.len());
423+
println!(
424+
"Orderbook update for {}: {} bids, {} asks",
425+
book.asset_id, book.bids.len(), book.asks.len()
426+
);
406427
}
407428
Ok(())
408429
}

src/clob/client.rs

Lines changed: 13 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,13 @@ use crate::clob::types::response::{
3838
ApiKeysResponse, BalanceAllowanceResponse, BanStatusResponse, BuilderApiKeyResponse,
3939
BuilderFeeRateResponse, BuilderTradeResponse, CancelOrdersResponse, ClobMarketInfoResponse,
4040
CurrentRewardResponse, FeeInfo, FeeRateResponse, GeoblockResponse, HeartbeatResponse,
41-
LastTradePriceResponse, LastTradesPricesResponse, MarketResponse, MarketRewardResponse,
42-
MidpointResponse, MidpointsResponse, NegRiskResponse, NotificationResponse, OpenOrderResponse,
43-
OrderBookSummaryResponse, OrderScoringResponse, OrdersScoringResponse, Page, PostOrderResponse,
44-
PriceHistoryResponse, PriceResponse, PricesResponse, ReadonlyApiKeyResponse,
45-
RewardsPercentagesResponse, SimplifiedMarketResponse, SpreadResponse, SpreadsResponse,
46-
TickSizeResponse, TotalUserEarningResponse, TradeResponse, UserEarningResponse,
47-
UserRewardsEarningResponse,
41+
LastTradePriceResponse, LastTradesPricesResponse, MarketByTokenResponse, MarketResponse,
42+
MarketRewardResponse, MidpointResponse, MidpointsResponse, NegRiskResponse,
43+
NotificationResponse, OpenOrderResponse, OrderBookSummaryResponse, OrderScoringResponse,
44+
OrdersScoringResponse, Page, PostOrderResponse, PriceHistoryResponse, PriceResponse,
45+
PricesResponse, ReadonlyApiKeyResponse, RewardsPercentagesResponse, SimplifiedMarketResponse,
46+
SpreadResponse, SpreadsResponse, TickSizeResponse, TotalUserEarningResponse, TradeResponse,
47+
UserEarningResponse, UserRewardsEarningResponse,
4848
};
4949
#[cfg(feature = "rfq")]
5050
use crate::clob::types::{
@@ -744,23 +744,6 @@ impl<S: State> Client<S> {
744744
crate::request(&self.inner.client, request, None).await
745745
}
746746

747-
/// Retrieves prices for all available market outcome tokens.
748-
///
749-
/// Returns the current best bid and ask prices for every active token
750-
/// in the system. This is useful for getting a complete market overview.
751-
///
752-
/// # Errors
753-
///
754-
/// Returns an error if the request fails.
755-
pub async fn all_prices(&self) -> Result<PricesResponse> {
756-
let request = self
757-
.client()
758-
.request(Method::GET, format!("{}prices", self.host()))
759-
.build()?;
760-
761-
crate::request(&self.inner.client, request, None).await
762-
}
763-
764747
/// Retrieves historical price data for a market outcome token.
765748
///
766749
/// Returns time-series price data over a specified time range or interval.
@@ -1322,11 +1305,10 @@ impl<S: State> Client<S> {
13221305
*cid
13231306
} else {
13241307
let market = self.market_by_token(token_id).await?;
1325-
let cid = market.condition_id.ok_or_else(|| {
1326-
Error::validation(format!("market for token {token_id} has no condition_id"))
1327-
})?;
1328-
self.inner.token_condition_map.insert(token_id, cid);
1329-
cid
1308+
self.inner
1309+
.token_condition_map
1310+
.insert(token_id, market.condition_id);
1311+
market.condition_id
13301312
};
13311313
self.clob_market_info(&condition_id.to_string()).await?;
13321314
Ok(())
@@ -1352,7 +1334,7 @@ impl<S: State> Client<S> {
13521334
/// # Errors
13531335
///
13541336
/// Returns an error if the request fails or the token ID is invalid.
1355-
pub async fn market_by_token(&self, token_id: U256) -> Result<MarketResponse> {
1337+
pub async fn market_by_token(&self, token_id: U256) -> Result<MarketByTokenResponse> {
13561338
let request = self
13571339
.client()
13581340
.request(
@@ -2173,7 +2155,7 @@ impl<K: Kind> Client<Authenticated<K>> {
21732155
&self,
21742156
request: &UserRewardsEarningRequest,
21752157
next_cursor: Option<String>,
2176-
) -> Result<Vec<UserRewardsEarningResponse>> {
2158+
) -> Result<Page<UserRewardsEarningResponse>> {
21772159
let params = request.query_params(next_cursor.as_deref());
21782160
let request = self
21792161
.client()

src/clob/types/response.rs

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ use bon::Builder;
99
use chrono::{DateTime, NaiveDate, Utc};
1010
use serde::{Deserialize, Deserializer, Serialize};
1111
use serde_with::{
12-
DefaultOnError, DefaultOnNull, NoneAsEmptyString, TimestampMilliSeconds, TimestampSeconds,
13-
TryFromInto, serde_as,
12+
DefaultOnError, DefaultOnNull, DisplayFromStr, NoneAsEmptyString, TimestampMilliSeconds,
13+
TimestampSeconds, TryFromInto, serde_as,
1414
};
1515
use sha2::{Digest as _, Sha256};
1616
use uuid::Uuid;
@@ -172,6 +172,21 @@ pub struct LastTradesPricesResponse {
172172
pub side: Side,
173173
}
174174

175+
/// Response from `GET /markets-by-token/{token_id}`. This endpoint returns a minimal
176+
/// market descriptor — just the condition ID and the two outcome token IDs — not a full
177+
/// [`MarketResponse`]. Used to resolve `token_id -> condition_id` before fetching the
178+
/// full clob-market info.
179+
#[non_exhaustive]
180+
#[serde_as]
181+
#[derive(Debug, Clone, Deserialize, Builder, PartialEq)]
182+
pub struct MarketByTokenResponse {
183+
pub condition_id: B256,
184+
#[serde_as(as = "DisplayFromStr")]
185+
pub primary_token_id: U256,
186+
#[serde_as(as = "DisplayFromStr")]
187+
pub secondary_token_id: U256,
188+
}
189+
175190
#[expect(
176191
clippy::struct_excessive_bools,
177192
reason = "The current API has these fields, so we have to capture this"

tests/clob.rs

Lines changed: 11 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -211,33 +211,6 @@ mod unauthenticated {
211211
Ok(())
212212
}
213213

214-
#[tokio::test]
215-
async fn all_prices_should_succeed() -> anyhow::Result<()> {
216-
let server = MockServer::start();
217-
let client = Client::new(&server.base_url(), Config::default())?;
218-
219-
let mock = server.mock(|when, then| {
220-
when.method(httpmock::Method::GET).path("/prices");
221-
then.status(StatusCode::OK)
222-
.json_body(json!({ token_1().to_string(): { "BUY": 0.5, "SELL": 0.6 } }));
223-
});
224-
225-
let response = client.all_prices().await?;
226-
227-
let mut price_map = HashMap::new();
228-
let mut side_map = HashMap::new();
229-
side_map.insert(Side::Buy, dec!(0.5));
230-
side_map.insert(Side::Sell, dec!(0.6));
231-
price_map.insert(token_1(), side_map);
232-
233-
let expected = PricesResponse::builder().prices(price_map).build();
234-
235-
assert_eq!(response, expected);
236-
mock.assert();
237-
238-
Ok(())
239-
}
240-
241214
#[tokio::test]
242215
async fn price_history_with_interval_should_succeed() -> anyhow::Result<()> {
243216
let server = MockServer::start();
@@ -2518,8 +2491,8 @@ mod authenticated {
25182491
.query_param("position", "")
25192492
.query_param("no_competition", "false")
25202493
.query_param("signature_type", (SignatureType::Eoa as u8).to_string());
2521-
then.status(StatusCode::OK).json_body(json!(
2522-
[
2494+
then.status(StatusCode::OK).json_body(json!({
2495+
"data": [
25232496
{
25242497
"condition_id": "0x0000000000000000000000000000000000000000000000000000000c00d00123",
25252498
"question": "Will BTC be above $50k on December 31, 2025?",
@@ -2574,8 +2547,11 @@ mod authenticated {
25742547
}
25752548
]
25762549
}
2577-
]
2578-
));
2550+
],
2551+
"next_cursor": "LTE=",
2552+
"limit": 500,
2553+
"count": 1
2554+
}));
25792555
});
25802556

25812557
let request = UserRewardsEarningRequest::builder()
@@ -2585,7 +2561,7 @@ mod authenticated {
25852561
.user_earnings_and_markets_config(&request, None)
25862562
.await?;
25872563

2588-
let expected = vec![
2564+
let expected_entries = vec![
25892565
UserRewardsEarningResponse::builder()
25902566
.condition_id(b256!(
25912567
"0000000000000000000000000000000000000000000000000000000c00d00123"
@@ -2644,7 +2620,9 @@ mod authenticated {
26442620
.build(),
26452621
];
26462622

2647-
assert_eq!(response, expected);
2623+
assert_eq!(response.data, expected_entries);
2624+
assert_eq!(response.next_cursor, "LTE=");
2625+
assert_eq!(response.count, 1);
26482626
mock.assert();
26492627

26502628
Ok(())

0 commit comments

Comments
 (0)