Skip to content

Commit 8ba5008

Browse files
authored
Merge pull request #33 from Polymarket/suhail/deposit-wallet-clob
feat: add deposit wallet order support
2 parents ed32ab4 + 4c5e10c commit 8ba5008

9 files changed

Lines changed: 423 additions & 52 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: 6 additions & 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.5.1"
4+
version = "0.6.0-canary.1"
55
authors = [
66
"Polymarket Engineering <engineering@polymarket.com>",
77
"Chaz Byrnes <chaz@polymarket.com>",
@@ -205,6 +205,11 @@ name = "gtc_limit_buy"
205205
path = "examples/clob/orders/gtc_limit_buy.rs"
206206
required-features = ["clob"]
207207

208+
[[example]]
209+
name = "gtc_limit_buy_deposit_wallet"
210+
path = "examples/clob/orders/gtc_limit_buy_deposit_wallet.rs"
211+
required-features = ["clob"]
212+
208213
[[example]]
209214
name = "gtc_limit_sell"
210215
path = "examples/clob/orders/gtc_limit_sell.rs"

README.md

Lines changed: 14 additions & 3 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.5"
51+
polymarket_client_sdk_v2 = "=0.6.0-canary.1"
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.5", features = ["ws", "data"] }
86+
polymarket_client_sdk_v2 = { version = "=0.6.0-canary.1", features = ["ws", "data"] }
8787
```
8888

8989
## Re-exported Types
@@ -228,6 +228,17 @@ The **signature_type** parameter tells the system how to verify your signatures:
228228

229229
See [`SignatureType`](src/clob/types/mod.rs) for more information.
230230

231+
For deposit wallets, pass the deployed deposit wallet as the funder and use `SignatureType::Poly1271`:
232+
233+
```rust,ignore
234+
let client = Client::new("https://clob-v2.polymarket.com", Config::default())?
235+
.authentication_builder(&signer)
236+
.funder(deposit_wallet)
237+
.signature_type(SignatureType::Poly1271)
238+
.authenticate()
239+
.await?;
240+
```
241+
231242
##### Place a market order
232243

233244
```rust,ignore
@@ -399,7 +410,7 @@ async fn main() -> anyhow::Result<()> {
399410
Real-time orderbook and user event streaming. Requires the `ws` feature.
400411

401412
```toml
402-
polymarket_client_sdk_v2 = { version = "0.5", features = ["ws"] }
413+
polymarket_client_sdk_v2 = { version = "=0.6.0-canary.1", features = ["ws"] }
403414
```
404415

405416
```rust,ignore
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
#![allow(
2+
clippy::print_stdout,
3+
reason = "Examples print their results to stdout"
4+
)]
5+
6+
//! Place a resting GTC limit buy from a deployed deposit wallet.
7+
8+
use std::str::FromStr as _;
9+
10+
use alloy::signers::Signer as _;
11+
use alloy::signers::local::LocalSigner;
12+
use polymarket_client_sdk_v2::clob::types::{OrderType, Side, SignatureType};
13+
use polymarket_client_sdk_v2::clob::{Client, Config};
14+
use polymarket_client_sdk_v2::types::{Address, Decimal, U256};
15+
use polymarket_client_sdk_v2::{POLYGON, PRIVATE_KEY_VAR};
16+
17+
#[tokio::main]
18+
async fn main() -> anyhow::Result<()> {
19+
let host =
20+
std::env::var("CLOB_API_URL").unwrap_or_else(|_| "https://clob-v2.polymarket.com".into());
21+
let token_id = U256::from_str(&std::env::var("TOKEN_ID")?)?;
22+
let deposit_wallet = Address::from_str(&std::env::var("DEPOSIT_WALLET")?)?;
23+
let price =
24+
Decimal::from_str(&std::env::var("ORDER_PRICE").unwrap_or_else(|_| "0.4".to_owned()))?;
25+
let size =
26+
Decimal::from_str(&std::env::var("ORDER_SIZE").unwrap_or_else(|_| "100".to_owned()))?;
27+
let signer =
28+
LocalSigner::from_str(&std::env::var(PRIVATE_KEY_VAR)?)?.with_chain_id(Some(POLYGON));
29+
30+
let client = Client::new(&host, Config::default())?
31+
.authentication_builder(&signer)
32+
.funder(deposit_wallet)
33+
.signature_type(SignatureType::Poly1271)
34+
.authenticate()
35+
.await?;
36+
37+
let resp = client
38+
.limit_order()
39+
.token_id(token_id)
40+
.side(Side::Buy)
41+
.price(price)
42+
.size(size)
43+
.order_type(OrderType::GTC)
44+
.build_sign_and_post(&signer)
45+
.await?;
46+
47+
println!("order_id={} status={}", resp.order_id, resp.status);
48+
Ok(())
49+
}

src/clob/client.rs

Lines changed: 104 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@ use std::sync::atomic::{AtomicU32, Ordering};
77
use std::time::Duration;
88

99
use alloy::dyn_abi::Eip712Domain;
10-
use alloy::primitives::U256;
10+
use alloy::primitives::{Signature, U256, keccak256};
1111
use alloy::signers::Signer;
1212
use alloy::sol_types::SolStruct as _;
13+
use alloy::sol_types::SolValue as _;
1314
use async_stream::try_stream;
1415
use bon::Builder;
1516
use chrono::{NaiveDate, Utc};
@@ -54,7 +55,8 @@ use crate::clob::types::{
5455
RfqRequestsRequest,
5556
};
5657
use crate::clob::types::{
57-
Amount, OrderPayload, OrderType, Side, SignableOrder, SignatureType, SignedOrder, TickSize,
58+
Amount, OrderPayload, OrderSignature, OrderType, Side, SignableOrder, SignatureType,
59+
SignedOrder, TickSize,
5860
};
5961
use crate::error::{Error, Kind as ErrorKind, Synchronization};
6062
use crate::types::{Address, B256, Decimal};
@@ -66,11 +68,42 @@ use crate::{
6668
const ORDER_NAME: Option<Cow<'static, str>> = Some(Cow::Borrowed("Polymarket CTF Exchange"));
6769
const VERSION_V1: Option<Cow<'static, str>> = Some(Cow::Borrowed("1"));
6870
const VERSION_V2: Option<Cow<'static, str>> = Some(Cow::Borrowed("2"));
71+
const DEPOSIT_WALLET_NAME: &str = "DepositWallet";
72+
const DEPOSIT_WALLET_VERSION: &str = "1";
73+
const ORDER_TYPE_STRING: &str = concat!(
74+
"Order(uint256 salt,address maker,address signer,uint256 tokenId,",
75+
"uint256 makerAmount,uint256 takerAmount,uint8 side,uint8 signatureType,",
76+
"uint256 timestamp,bytes32 metadata,bytes32 builder)"
77+
);
78+
const SOLADY_TYPE_STRING: &str = concat!(
79+
"TypedDataSign(Order contents,string name,string version,uint256 chainId,",
80+
"address verifyingContract,bytes32 salt)",
81+
"Order(uint256 salt,address maker,address signer,uint256 tokenId,",
82+
"uint256 makerAmount,uint256 takerAmount,uint8 side,uint8 signatureType,",
83+
"uint256 timestamp,bytes32 metadata,bytes32 builder)"
84+
);
6985

7086
const TERMINAL_CURSOR: &str = "LTE="; // base64("-1")
7187

7288
pub(crate) const ORDER_VERSION_MISMATCH_ERROR: &str = "order_version_mismatch";
7389

90+
fn push_hex(out: &mut String, bytes: &[u8]) {
91+
const LUT: &[u8; 16] = b"0123456789abcdef";
92+
out.reserve(bytes.len() * 2);
93+
for byte in bytes {
94+
out.push(LUT[(byte >> 4) as usize] as char);
95+
out.push(LUT[(byte & 0x0f) as usize] as char);
96+
}
97+
}
98+
99+
fn signature_hex_no_prefix(signature: &Signature) -> String {
100+
let signature = signature.to_string();
101+
signature
102+
.strip_prefix("0x")
103+
.unwrap_or(&signature)
104+
.to_owned()
105+
}
106+
74107
/// The type used to build a request to authenticate the inner [`Client<Unauthorized>`]. Calling
75108
/// `authenticate` on this will elevate that inner `client` into an [`Client<Authenticated<K>>`].
76109
pub struct AuthenticationBuilder<'signer, S: Signer, K: Kind = Normal> {
@@ -87,8 +120,8 @@ pub struct AuthenticationBuilder<'signer, S: Signer, K: Kind = Normal> {
87120
/// headers for different types of authentication, e.g. Builder.
88121
kind: K,
89122
/// The optional [`Address`] used to represent the funder for this `client`. If a funder is set
90-
/// then `signature_type` must match `Some(SignatureType::Proxy | Signature::GnosisSafe)`. Conversely,
91-
/// if funder is not set, then `signature_type` must be `Some(SignatureType::Eoa)`.
123+
/// then `signature_type` must match `Some(SignatureType::Proxy | SignatureType::GnosisSafe | SignatureType::Poly1271)`.
124+
/// Conversely, if funder is not set, then `signature_type` must be `Some(SignatureType::Eoa)`.
92125
funder: Option<Address>,
93126
/// The optional [`SignatureType`], see `funder` for more information.
94127
signature_type: Option<SignatureType>,
@@ -179,9 +212,18 @@ impl<S: Signer, K: Kind> AuthenticationBuilder<'_, S, K> {
179212
"Cannot have a funder address with a {sig} signature type"
180213
)));
181214
}
215+
(None, Some(SignatureType::Poly1271)) => {
216+
return Err(Error::validation(
217+
"A deposit wallet funder address is required with a Poly1271 signature type",
218+
));
219+
}
182220
(
183221
Some(Address::ZERO),
184-
Some(sig @ (SignatureType::Proxy | SignatureType::GnosisSafe)),
222+
Some(
223+
sig @ (SignatureType::Proxy
224+
| SignatureType::GnosisSafe
225+
| SignatureType::Poly1271),
226+
),
185227
) => {
186228
return Err(Error::validation(format!(
187229
"Cannot have a zero funder address with a {sig} signature type"
@@ -913,8 +955,8 @@ impl<S: State> Client<S> {
913955
Ok(response)
914956
}
915957

916-
/// Resolves the V1 `feeRateBps` to apply to an order. Mirrors the TS client's
917-
/// `_resolveFeeRateBps`: fetches the market rate via [`Self::fee_rate_bps`] and,
958+
/// Resolves the V1 `feeRateBps` to apply to an order: fetches the market rate via
959+
/// [`Self::fee_rate_bps`] and,
918960
/// when the caller supplied an override, validates that it matches.
919961
///
920962
/// # Errors
@@ -1714,9 +1756,15 @@ impl<K: Kind> Client<Authenticated<K>> {
17141756
verifying_contract: Some(exchange),
17151757
..Eip712Domain::default()
17161758
};
1717-
signer
1718-
.sign_hash(&p.order.eip712_signing_hash(&domain))
1719-
.await?
1759+
if p.order.signatureType == SignatureType::Poly1271 as u8 {
1760+
self.sign_poly1271_order(signer, &p.order, &domain, chain_id)
1761+
.await?
1762+
} else {
1763+
signer
1764+
.sign_hash(&p.order.eip712_signing_hash(&domain))
1765+
.await?
1766+
.into()
1767+
}
17201768
}
17211769
OrderPayload::V1(p) => {
17221770
let domain = Eip712Domain {
@@ -1729,6 +1777,7 @@ impl<K: Kind> Client<Authenticated<K>> {
17291777
signer
17301778
.sign_hash(&p.order.eip712_signing_hash(&domain))
17311779
.await?
1780+
.into()
17321781
}
17331782
};
17341783

@@ -1742,6 +1791,51 @@ impl<K: Kind> Client<Authenticated<K>> {
17421791
})
17431792
}
17441793

1794+
async fn sign_poly1271_order<S: Signer>(
1795+
&self,
1796+
signer: &S,
1797+
order: &crate::clob::types::OrderV2,
1798+
app_domain: &Eip712Domain,
1799+
chain_id: u64,
1800+
) -> Result<OrderSignature> {
1801+
let contents_hash = order.eip712_hash_struct();
1802+
let app_domain_separator = app_domain.hash_struct();
1803+
1804+
let typed_data_sign_struct_hash = keccak256(
1805+
(
1806+
keccak256(SOLADY_TYPE_STRING.as_bytes()),
1807+
contents_hash,
1808+
keccak256(DEPOSIT_WALLET_NAME.as_bytes()),
1809+
keccak256(DEPOSIT_WALLET_VERSION.as_bytes()),
1810+
U256::from(chain_id),
1811+
order.signer,
1812+
B256::ZERO,
1813+
)
1814+
.abi_encode(),
1815+
);
1816+
1817+
let mut digest_input = [0_u8; 66];
1818+
digest_input[0] = 0x19;
1819+
digest_input[1] = 0x01;
1820+
digest_input[2..34].copy_from_slice(app_domain_separator.as_slice());
1821+
digest_input[34..66].copy_from_slice(typed_data_sign_struct_hash.as_slice());
1822+
let digest = keccak256(digest_input);
1823+
1824+
let inner_signature = signer.sign_hash(&digest).await?;
1825+
let mut wrapped =
1826+
String::with_capacity(2 + 130 + 64 + 64 + (ORDER_TYPE_STRING.len() * 2) + 4);
1827+
wrapped.push_str("0x");
1828+
wrapped.push_str(&signature_hex_no_prefix(&inner_signature));
1829+
push_hex(&mut wrapped, app_domain_separator.as_slice());
1830+
push_hex(&mut wrapped, contents_hash.as_slice());
1831+
push_hex(&mut wrapped, ORDER_TYPE_STRING.as_bytes());
1832+
let contents_type_len =
1833+
u16::try_from(ORDER_TYPE_STRING.len()).expect("order type string length fits in u16");
1834+
push_hex(&mut wrapped, &contents_type_len.to_be_bytes());
1835+
1836+
Ok(OrderSignature::Wrapped(wrapped))
1837+
}
1838+
17451839
/// Posts a signed order to the orderbook.
17461840
///
17471841
/// Submits a single limit or market order that has been signed with the

src/clob/order_builder.rs

Lines changed: 32 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,15 @@ impl<OrderKind, K: AuthKind> OrderBuilder<OrderKind, K> {
158158
) -> Result<OrderPayload> {
159159
let version = self.client.resolve_version(false).await?;
160160
let maker = self.funder.unwrap_or(self.signer);
161+
let signer = if matches!(self.signature_type, SignatureType::Poly1271) {
162+
self.funder.ok_or_else(|| {
163+
Error::validation(
164+
"A deposit wallet funder address is required with a Poly1271 signature type",
165+
)
166+
})?
167+
} else {
168+
self.signer
169+
};
161170

162171
match version {
163172
1 => {
@@ -194,7 +203,7 @@ impl<OrderKind, K: AuthKind> OrderBuilder<OrderKind, K> {
194203
OrderV2 {
195204
salt: U256::from(salt),
196205
maker,
197-
signer: self.signer,
206+
signer,
198207
tokenId: token_id,
199208
makerAmount: U256::from(maker_amount),
200209
takerAmount: U256::from(taker_amount),
@@ -378,19 +387,17 @@ impl<K: AuthKind> OrderBuilder<Limit, K> {
378387
let order = self.build().await?;
379388
let signed = client.sign(signer, order).await?;
380389
let result = client.post_order(signed).await;
381-
if let Err(err) = &result {
382-
if let Some(status) = err.downcast_ref::<crate::error::Status>() {
383-
if status
384-
.message
385-
.contains(crate::clob::client::ORDER_VERSION_MISMATCH_ERROR)
386-
{
387-
let after_version = client.resolve_version(false).await.unwrap_or(0);
388-
if after_version != before_version {
389-
let order = retry.build().await?;
390-
let signed = client.sign(signer, order).await?;
391-
return client.post_order(signed).await;
392-
}
393-
}
390+
if let Err(err) = &result
391+
&& let Some(status) = err.downcast_ref::<crate::error::Status>()
392+
&& status
393+
.message
394+
.contains(crate::clob::client::ORDER_VERSION_MISMATCH_ERROR)
395+
{
396+
let after_version = client.resolve_version(false).await.unwrap_or(0);
397+
if after_version != before_version {
398+
let order = retry.build().await?;
399+
let signed = client.sign(signer, order).await?;
400+
return client.post_order(signed).await;
394401
}
395402
}
396403
result
@@ -633,19 +640,17 @@ impl<K: AuthKind> OrderBuilder<Market, K> {
633640
let order = self.build().await?;
634641
let signed = client.sign(signer, order).await?;
635642
let result = client.post_order(signed).await;
636-
if let Err(err) = &result {
637-
if let Some(status) = err.downcast_ref::<crate::error::Status>() {
638-
if status
639-
.message
640-
.contains(crate::clob::client::ORDER_VERSION_MISMATCH_ERROR)
641-
{
642-
let after_version = client.resolve_version(false).await.unwrap_or(0);
643-
if after_version != before_version {
644-
let order = retry.build().await?;
645-
let signed = client.sign(signer, order).await?;
646-
return client.post_order(signed).await;
647-
}
648-
}
643+
if let Err(err) = &result
644+
&& let Some(status) = err.downcast_ref::<crate::error::Status>()
645+
&& status
646+
.message
647+
.contains(crate::clob::client::ORDER_VERSION_MISMATCH_ERROR)
648+
{
649+
let after_version = client.resolve_version(false).await.unwrap_or(0);
650+
if after_version != before_version {
651+
let order = retry.build().await?;
652+
let signed = client.sign(signer, order).await?;
653+
return client.post_order(signed).await;
649654
}
650655
}
651656
result

0 commit comments

Comments
 (0)