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