1#![cfg_attr(doc, doc = include_str!("../README.md"))]
2
3pub mod auth;
4#[cfg(feature = "bridge")]
5pub mod bridge;
6pub mod clob;
7#[cfg(feature = "data")]
8pub mod data;
9pub mod error;
10#[cfg(feature = "gamma")]
11pub mod gamma;
12#[cfg(feature = "rtds")]
13pub mod rtds;
14pub(crate) mod serde_helpers;
15pub mod types;
16#[cfg(any(feature = "ws", feature = "rtds"))]
17pub mod ws;
18
19use std::fmt::Write as _;
20
21use alloy::primitives::ChainId;
22use alloy::primitives::{B256, b256, keccak256};
23use phf::phf_map;
24use reqwest::header::HeaderMap;
25use reqwest::{Request, StatusCode};
26use serde::Serialize;
27use serde::de::DeserializeOwned;
28
29use crate::error::Error;
30use crate::types::{Address, address};
31
32pub type Result<T> = std::result::Result<T, Error>;
33
34pub const POLYGON: ChainId = 137;
36
37pub const AMOY: ChainId = 80002;
39
40pub const PRIVATE_KEY_VAR: &str = "POLYMARKET_PRIVATE_KEY";
41
42pub(crate) type Timestamp = i64;
44
45static CONFIG: phf::Map<ChainId, ContractConfig> = phf_map! {
46 137_u64 => ContractConfig {
47 exchange: address!("0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E"),
48 collateral: address!("0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174"),
49 conditional_tokens: address!("0x4D97DCd97eC945f40cF65F87097ACe5EA0476045"),
50 neg_risk_adapter: None,
51 },
52 80002_u64 => ContractConfig {
53 exchange: address!("0xdFE02Eb6733538f8Ea35D585af8DE5958AD99E40"),
54 collateral: address!("0x9c4e1703476e875070ee25b56a58b008cfb8fa78"),
55 conditional_tokens: address!("0x69308FB512518e39F9b16112fA8d994F4e2Bf8bB"),
56 neg_risk_adapter: None,
57 },
58};
59
60static NEG_RISK_CONFIG: phf::Map<ChainId, ContractConfig> = phf_map! {
61 137_u64 => ContractConfig {
62 exchange: address!("0xC5d563A36AE78145C45a50134d48A1215220f80a"),
63 collateral: address!("0x2791bca1f2de4661ed88a30c99a7a9449aa84174"),
64 conditional_tokens: address!("0x4D97DCd97eC945f40cF65F87097ACe5EA0476045"),
65 neg_risk_adapter: Some(address!("0xd91E80cF2E7be2e162c6513ceD06f1dD0dA35296")),
66 },
67 80002_u64 => ContractConfig {
68 exchange: address!("0xd91E80cF2E7be2e162c6513ceD06f1dD0dA35296"),
69 collateral: address!("0x9c4e1703476e875070ee25b56a58b008cfb8fa78"),
70 conditional_tokens: address!("0x69308FB512518e39F9b16112fA8d994F4e2Bf8bB"),
71 neg_risk_adapter: Some(address!("0xd91E80cF2E7be2e162c6513ceD06f1dD0dA35296")),
72 },
73};
74
75static WALLET_CONFIG: phf::Map<ChainId, WalletContractConfig> = phf_map! {
78 137_u64 => WalletContractConfig {
79 proxy_factory: Some(address!("0xaB45c5A4B0c941a2F231C04C3f49182e1A254052")),
80 safe_factory: address!("0xaacFeEa03eb1561C4e67d661e40682Bd20E3541b"),
81 },
82 80002_u64 => WalletContractConfig {
83 proxy_factory: None,
85 safe_factory: address!("0xaacFeEa03eb1561C4e67d661e40682Bd20E3541b"),
86 },
87};
88
89const PROXY_INIT_CODE_HASH: B256 =
91 b256!("0xd21df8dc65880a8606f09fe0ce3df9b8869287ab0b058be05aa9e8af6330a00b");
92
93const SAFE_INIT_CODE_HASH: B256 =
95 b256!("0x2bce2127ff07fb632d16c8347c4ebf501f4841168bed00d9e6ef715ddb6fcecf");
96
97#[non_exhaustive]
99#[derive(Debug)]
100pub struct ContractConfig {
101 pub exchange: Address,
102 pub collateral: Address,
103 pub conditional_tokens: Address,
104 pub neg_risk_adapter: Option<Address>,
107}
108
109#[non_exhaustive]
111#[derive(Debug)]
112pub struct WalletContractConfig {
113 pub proxy_factory: Option<Address>,
116 pub safe_factory: Address,
118}
119
120#[must_use]
122pub fn contract_config(chain_id: ChainId, is_neg_risk: bool) -> Option<&'static ContractConfig> {
123 if is_neg_risk {
124 NEG_RISK_CONFIG.get(&chain_id)
125 } else {
126 CONFIG.get(&chain_id)
127 }
128}
129
130#[must_use]
132pub fn wallet_contract_config(chain_id: ChainId) -> Option<&'static WalletContractConfig> {
133 WALLET_CONFIG.get(&chain_id)
134}
135
136#[must_use]
149pub fn derive_proxy_wallet(eoa_address: Address, chain_id: ChainId) -> Option<Address> {
150 let config = wallet_contract_config(chain_id)?;
151 let factory = config.proxy_factory?;
152
153 let salt = keccak256(eoa_address);
155
156 Some(factory.create2(salt, PROXY_INIT_CODE_HASH))
157}
158
159#[must_use]
172pub fn derive_safe_wallet(eoa_address: Address, chain_id: ChainId) -> Option<Address> {
173 let config = wallet_contract_config(chain_id)?;
174 let factory = config.safe_factory;
175
176 let mut padded = [0_u8; 32];
179 padded[12..].copy_from_slice(eoa_address.as_slice());
180 let salt = keccak256(padded);
181
182 Some(factory.create2(salt, SAFE_INIT_CODE_HASH))
183}
184
185pub trait ToQueryParams: Serialize {
190 fn query_params(&self, next_cursor: Option<&str>) -> String {
196 let mut params = serde_urlencoded::to_string(self)
197 .inspect_err(|e| {
198 #[cfg(not(feature = "tracing"))]
199 let _: &serde_urlencoded::ser::Error = e;
200
201 #[cfg(feature = "tracing")]
202 tracing::error!("Unable to convert to URL-encoded string {e:?}");
203 })
204 .unwrap_or_default();
205
206 if let Some(cursor) = next_cursor {
207 if !params.is_empty() {
208 params.push('&');
209 }
210 let _ = write!(params, "next_cursor={cursor}");
211 }
212
213 if params.is_empty() {
214 String::new()
215 } else {
216 format!("?{params}")
217 }
218 }
219}
220
221impl<T: Serialize> ToQueryParams for T {}
222
223#[cfg_attr(
224 feature = "tracing",
225 tracing::instrument(
226 level = "debug",
227 skip(client, request, headers),
228 fields(
229 method = %request.method(),
230 path = request.url().path(),
231 status_code
232 )
233 )
234)]
235async fn request<Response: DeserializeOwned>(
236 client: &reqwest::Client,
237 mut request: Request,
238 headers: Option<HeaderMap>,
239) -> Result<Response> {
240 let method = request.method().clone();
241 let path = request.url().path().to_owned();
242
243 if let Some(h) = headers {
244 *request.headers_mut() = h;
245 }
246
247 let response = client.execute(request).await?;
248 let status_code = response.status();
249
250 #[cfg(feature = "tracing")]
251 tracing::Span::current().record("status_code", status_code.as_u16());
252
253 if !status_code.is_success() {
254 let message = response.text().await.unwrap_or_default();
255
256 #[cfg(feature = "tracing")]
257 tracing::warn!(
258 status = %status_code,
259 method = %method,
260 path = %path,
261 message = %message,
262 "API request failed"
263 );
264
265 return Err(Error::status(status_code, method, path, message));
266 }
267
268 let json_value = response.json::<serde_json::Value>().await?;
269 let response_data: Option<Response> = serde_helpers::deserialize_with_warnings(json_value)?;
270
271 if let Some(response) = response_data {
272 Ok(response)
273 } else {
274 #[cfg(feature = "tracing")]
275 tracing::warn!(method = %method, path = %path, "API resource not found");
276 Err(Error::status(
277 StatusCode::NOT_FOUND,
278 method,
279 path,
280 "Unable to find requested resource",
281 ))
282 }
283}
284
285#[cfg(test)]
286mod tests {
287 use super::*;
288
289 #[test]
290 fn config_contains_80002() {
291 let cfg = contract_config(AMOY, false).expect("missing config");
292 assert_eq!(
293 cfg.exchange,
294 address!("0xdFE02Eb6733538f8Ea35D585af8DE5958AD99E40")
295 );
296 }
297
298 #[test]
299 fn config_contains_80002_neg() {
300 let cfg = contract_config(AMOY, true).expect("missing config");
301 assert_eq!(
302 cfg.exchange,
303 address!("0xd91e80cf2e7be2e162c6513ced06f1dd0da35296")
304 );
305 }
306
307 #[test]
308 fn wallet_contract_config_polygon() {
309 let cfg = wallet_contract_config(POLYGON).expect("missing config");
310 assert_eq!(
311 cfg.proxy_factory,
312 Some(address!("0xaB45c5A4B0c941a2F231C04C3f49182e1A254052"))
313 );
314 assert_eq!(
315 cfg.safe_factory,
316 address!("0xaacFeEa03eb1561C4e67d661e40682Bd20E3541b")
317 );
318 }
319
320 #[test]
321 fn wallet_contract_config_amoy() {
322 let cfg = wallet_contract_config(AMOY).expect("missing config");
323 assert_eq!(cfg.proxy_factory, None);
325 assert_eq!(
326 cfg.safe_factory,
327 address!("0xaacFeEa03eb1561C4e67d661e40682Bd20E3541b")
328 );
329 }
330
331 #[test]
332 fn derive_safe_wallet_polygon() {
333 let eoa = address!("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266");
335 let safe_addr = derive_safe_wallet(eoa, POLYGON).expect("derivation failed");
336
337 assert_eq!(
339 safe_addr,
340 address!("0xd93b25Cb943D14d0d34FBAf01fc93a0F8b5f6e47")
341 );
342 }
343
344 #[test]
345 fn derive_proxy_wallet_polygon() {
346 let eoa = address!("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266");
348 let proxy_addr = derive_proxy_wallet(eoa, POLYGON).expect("derivation failed");
349
350 assert_eq!(
352 proxy_addr,
353 address!("0x365f0cA36ae1F641E02Fe3b7743673DA42A13a70")
354 );
355 }
356
357 #[test]
358 fn derive_proxy_wallet_amoy_not_supported() {
359 let eoa = address!("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266");
360 assert!(derive_proxy_wallet(eoa, AMOY).is_none());
362 }
363
364 #[test]
365 fn derive_safe_wallet_amoy() {
366 let eoa = address!("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266");
367 let safe_addr = derive_safe_wallet(eoa, AMOY).expect("derivation failed");
369
370 assert_eq!(
372 safe_addr,
373 address!("0xd93b25Cb943D14d0d34FBAf01fc93a0F8b5f6e47")
374 );
375 }
376
377 #[test]
378 fn derive_wallet_unsupported_chain() {
379 let eoa = address!("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266");
380 assert!(derive_proxy_wallet(eoa, 1).is_none());
382 assert!(derive_safe_wallet(eoa, 1).is_none());
383 }
384}