polymarket-apis 0.3.6__py3-none-any.whl → 0.3.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,7 +10,7 @@ This package provides a comprehensive interface to Polymarket's APIs including:
10
10
  - GraphQL API for flexible data queries
11
11
  """
12
12
 
13
- __version__ = "0.3.6"
13
+ __version__ = "0.3.7"
14
14
  __author__ = "Razvan Gheorghe"
15
15
  __email__ = "razvan@gheorghe.me"
16
16
 
@@ -1,7 +1,7 @@
1
1
  import json
2
2
  import logging
3
3
  from datetime import UTC, datetime, timedelta
4
- from typing import Literal, Optional
4
+ from typing import Literal, cast
5
5
  from urllib.parse import urljoin
6
6
 
7
7
  import httpx
@@ -94,7 +94,7 @@ class PolymarketClobClient:
94
94
  self,
95
95
  private_key: str,
96
96
  address: EthAddress,
97
- creds: Optional[ApiCreds] = None,
97
+ creds: ApiCreds | None = None,
98
98
  chain_id: Literal[137, 80002] = POLYGON,
99
99
  signature_type: Literal[0, 1, 2] = 1,
100
100
  # 0 - EOA wallet, 1 - Proxy wallet, 2 - Gnosis Safe wallet
@@ -112,9 +112,9 @@ class PolymarketClobClient:
112
112
  self.creds = creds if creds else self.create_or_derive_api_creds()
113
113
 
114
114
  # local cache
115
- self.__tick_sizes = {}
116
- self.__neg_risk = {}
117
- self.__fee_rates = {}
115
+ self.__tick_sizes: dict[str, TickSize] = {}
116
+ self.__neg_risk: dict[str, bool] = {}
117
+ self.__fee_rates: dict[str, int] = {}
118
118
 
119
119
  def _build_url(self, endpoint: str) -> str:
120
120
  return urljoin(self.base_url, endpoint)
@@ -124,19 +124,19 @@ class PolymarketClobClient:
124
124
  response.raise_for_status()
125
125
  return response.json()
126
126
 
127
- def create_api_creds(self, nonce: Optional[int] = None) -> ApiCreds:
127
+ def create_api_creds(self, nonce: int | None = None) -> ApiCreds:
128
128
  headers = create_level_1_headers(self.signer, nonce)
129
129
  response = self.client.post(self._build_url(CREATE_API_KEY), headers=headers)
130
130
  response.raise_for_status()
131
131
  return ApiCreds(**response.json())
132
132
 
133
- def derive_api_key(self, nonce: Optional[int] = None) -> ApiCreds:
133
+ def derive_api_key(self, nonce: int | None = None) -> ApiCreds:
134
134
  headers = create_level_1_headers(self.signer, nonce)
135
135
  response = self.client.get(self._build_url(DERIVE_API_KEY), headers=headers)
136
136
  response.raise_for_status()
137
137
  return ApiCreds(**response.json())
138
138
 
139
- def create_or_derive_api_creds(self, nonce: Optional[int] = None) -> ApiCreds:
139
+ def create_or_derive_api_creds(self, nonce: int | None = None) -> ApiCreds:
140
140
  try:
141
141
  return self.create_api_creds(nonce)
142
142
  except HTTPStatusError:
@@ -172,7 +172,9 @@ class PolymarketClobClient:
172
172
  params = {"token_id": token_id}
173
173
  response = self.client.get(self._build_url(GET_TICK_SIZE), params=params)
174
174
  response.raise_for_status()
175
- self.__tick_sizes[token_id] = str(response.json()["minimum_tick_size"])
175
+ self.__tick_sizes[token_id] = cast(
176
+ "TickSize", str(response.json()["minimum_tick_size"])
177
+ )
176
178
 
177
179
  return self.__tick_sizes[token_id]
178
180
 
@@ -202,7 +204,7 @@ class PolymarketClobClient:
202
204
  def __resolve_tick_size(
203
205
  self,
204
206
  token_id: str,
205
- tick_size: TickSize = None,
207
+ tick_size: TickSize | None = None,
206
208
  ) -> TickSize:
207
209
  min_tick_size = self.get_tick_size(token_id)
208
210
  if tick_size is not None:
@@ -216,7 +218,7 @@ class PolymarketClobClient:
216
218
  def __resolve_fee_rate(
217
219
  self,
218
220
  token_id: str,
219
- user_fee_rate: Optional[int] = None,
221
+ user_fee_rate: int | None = None,
220
222
  ) -> int:
221
223
  market_fee_rate_bps = self.get_fee_rate_bps(token_id)
222
224
  # If both fee rate on the market and the user supplied fee rate are non-zero, validate that they match
@@ -362,11 +364,11 @@ class PolymarketClobClient:
362
364
  "max": 2,
363
365
  }
364
366
 
365
- if fidelity < min_fidelities.get(interval):
366
- msg = f"invalid filters: minimum 'fidelity' for '{interval}' range is {min_fidelities.get(interval)}"
367
+ if fidelity < min_fidelities[interval]:
368
+ msg = f"invalid filters: minimum fidelity' for '{interval}' range is {min_fidelities.get(interval)}"
367
369
  raise ValueError(msg)
368
370
 
369
- params = {
371
+ params: dict[str, int | str] = {
370
372
  "market": token_id,
371
373
  "interval": interval,
372
374
  "fidelity": fidelity,
@@ -378,8 +380,8 @@ class PolymarketClobClient:
378
380
  def get_history(
379
381
  self,
380
382
  token_id: str,
381
- start_time: Optional[datetime] = None,
382
- end_time: Optional[datetime] = None,
383
+ start_time: datetime | None = None,
384
+ end_time: datetime | None = None,
383
385
  fidelity: int = 2, # resolution in minutes
384
386
  ) -> PriceHistory:
385
387
  """Get the price history of a token between a selected date range of max 15 days or from start_time to now."""
@@ -395,7 +397,7 @@ class PolymarketClobClient:
395
397
  msg = "'start_time' - 'end_time' range cannot exceed 15 days. Remove 'end_time' to get prices up to now or set a shorter range."
396
398
  raise ValueError(msg)
397
399
 
398
- params = {
400
+ params: dict[str, int | str] = {
399
401
  "market": token_id,
400
402
  "fidelity": fidelity,
401
403
  }
@@ -417,9 +419,9 @@ class PolymarketClobClient:
417
419
 
418
420
  def get_orders(
419
421
  self,
420
- order_id: Optional[str] = None,
421
- condition_id: Optional[Keccak256] = None,
422
- token_id: Optional[str] = None,
422
+ order_id: str | None = None,
423
+ condition_id: Keccak256 | None = None,
424
+ token_id: str | None = None,
423
425
  next_cursor: str = "MA==",
424
426
  ) -> list[OpenOrder]:
425
427
  """Gets your active orders, filtered by order_id, condition_id, token_id."""
@@ -448,7 +450,7 @@ class PolymarketClobClient:
448
450
  return results
449
451
 
450
452
  def create_order(
451
- self, order_args: OrderArgs, options: Optional[PartialCreateOrderOptions] = None
453
+ self, order_args: OrderArgs, options: PartialCreateOrderOptions | None = None
452
454
  ) -> SignedOrder:
453
455
  """Creates and signs an order."""
454
456
  # add resolve_order_options, or similar
@@ -483,7 +485,7 @@ class PolymarketClobClient:
483
485
 
484
486
  def post_order(
485
487
  self, order: SignedOrder, order_type: OrderType = OrderType.GTC
486
- ) -> Optional[OrderPostResponse]:
488
+ ) -> OrderPostResponse | None:
487
489
  """Posts a SignedOrder."""
488
490
  body = order_to_json(order, self.creds.key, order_type)
489
491
  headers = create_level_2_headers(
@@ -505,18 +507,19 @@ class PolymarketClobClient:
505
507
  logger.warning(msg)
506
508
  error_json = exc.response.json()
507
509
  print("Details:", error_json["error"])
510
+ return None
508
511
 
509
512
  def create_and_post_order(
510
513
  self,
511
514
  order_args: OrderArgs,
512
- options: Optional[PartialCreateOrderOptions] = None,
515
+ options: PartialCreateOrderOptions | None = None,
513
516
  order_type: OrderType = OrderType.GTC,
514
517
  ) -> OrderPostResponse | None:
515
518
  """Utility function to create and publish an order."""
516
519
  order = self.create_order(order_args, options)
517
520
  return self.post_order(order=order, order_type=order_type)
518
521
 
519
- def post_orders(self, args: list[PostOrdersArgs]):
522
+ def post_orders(self, args: list[PostOrdersArgs]) -> list[OrderPostResponse] | None:
520
523
  """Posts multiple SignedOrders at once."""
521
524
  body = [
522
525
  order_to_json(arg.order, self.creds.key, arg.order_type) for arg in args
@@ -549,12 +552,13 @@ class PolymarketClobClient:
549
552
  logger.warning(msg)
550
553
  error_json = exc.response.json()
551
554
  print("Details:", error_json["error"])
555
+ return None
552
556
  else:
553
557
  return order_responses
554
558
 
555
559
  def create_and_post_orders(
556
560
  self, args: list[OrderArgs], order_types: list[OrderType]
557
- ) -> list[OrderPostResponse]:
561
+ ) -> list[OrderPostResponse] | None:
558
562
  """Utility function to create and publish multiple orders at once."""
559
563
  return self.post_orders(
560
564
  [
@@ -597,7 +601,7 @@ class PolymarketClobClient:
597
601
  def create_market_order(
598
602
  self,
599
603
  order_args: MarketOrderArgs,
600
- options: Optional[PartialCreateOrderOptions] = None,
604
+ options: PartialCreateOrderOptions | None = None,
601
605
  ):
602
606
  """Creates and signs a market order."""
603
607
  tick_size = self.__resolve_tick_size(
@@ -640,7 +644,7 @@ class PolymarketClobClient:
640
644
  def create_and_post_market_order(
641
645
  self,
642
646
  order_args: MarketOrderArgs,
643
- options: Optional[PartialCreateOrderOptions] = None,
647
+ options: PartialCreateOrderOptions | None = None,
644
648
  order_type: OrderType = OrderType.FOK,
645
649
  ) -> OrderPostResponse | None:
646
650
  """Utility function to create and publish a market order."""
@@ -678,7 +682,7 @@ class PolymarketClobClient:
678
682
  "DELETE",
679
683
  self._build_url(CANCEL_ORDERS),
680
684
  headers=headers,
681
- data=json.dumps(body).encode("utf-8"),
685
+ content=json.dumps(body).encode("utf-8"),
682
686
  )
683
687
  response.raise_for_status()
684
688
  return OrderCancelResponse(**response.json())
@@ -739,12 +743,12 @@ class PolymarketClobClient:
739
743
 
740
744
  def get_trades(
741
745
  self,
742
- condition_id: Optional[Keccak256] = None,
743
- token_id: Optional[str] = None,
744
- trade_id: Optional[str] = None,
745
- before: Optional[datetime] = None,
746
- after: Optional[datetime] = None,
747
- address: Optional[EthAddress] = None,
746
+ condition_id: Keccak256 | None = None,
747
+ token_id: str | None = None,
748
+ trade_id: str | None = None,
749
+ before: datetime | None = None,
750
+ after: datetime | None = None,
751
+ address: EthAddress | None = None,
748
752
  next_cursor="MA==",
749
753
  ) -> list[PolygonTrade]:
750
754
  """Fetches the trade history for a user."""
@@ -778,7 +782,7 @@ class PolymarketClobClient:
778
782
 
779
783
  return results
780
784
 
781
- def get_total_rewards(self, date: Optional[datetime] = None) -> DailyEarnedReward:
785
+ def get_total_rewards(self, date: datetime | None = None) -> DailyEarnedReward:
782
786
  """Get the total rewards earned on a given date (seems to only hold the 6 most recent data points)."""
783
787
  if date is None:
784
788
  date = datetime.now(UTC)
@@ -807,20 +811,19 @@ class PolymarketClobClient:
807
811
 
808
812
  def get_reward_markets(
809
813
  self,
810
- query: Optional[str] = None,
811
- sort_by: Optional[
812
- Literal[
813
- "market",
814
- "max_spread",
815
- "min_size",
816
- "rate_per_day",
817
- "spread",
818
- "price",
819
- "earnings",
820
- "earning_percentage",
821
- ]
822
- ] = "market",
823
- sort_direction: Optional[Literal["ASC", "DESC"]] = None,
814
+ query: str | None = None,
815
+ sort_by: Literal[
816
+ "market",
817
+ "max_spread",
818
+ "min_size",
819
+ "rate_per_day",
820
+ "spread",
821
+ "price",
822
+ "earnings",
823
+ "earning_percentage",
824
+ ]
825
+ | None = "market",
826
+ sort_direction: Literal["ASC", "DESC"] | None = None,
824
827
  show_favorites: bool = False,
825
828
  ) -> list[RewardMarket]:
826
829
  """
@@ -837,7 +840,7 @@ class PolymarketClobClient:
837
840
  """
838
841
  results = []
839
842
  desc = {"ASC": False, "DESC": True}
840
- params = {
843
+ params: dict[str, bool | str] = {
841
844
  "authenticationType": "magic",
842
845
  "showFavorites": show_favorites,
843
846
  }
@@ -222,7 +222,11 @@ class PolymarketDataClient:
222
222
  min_balance: int = 1,
223
223
  ) -> list[HolderResponse]:
224
224
  """Takes in a condition_id and returns top holders for each corresponding token_id."""
225
- params = {"market": condition_id, "limit": limit, "min_balance": min_balance}
225
+ params: dict[str, int | str] = {
226
+ "market": condition_id,
227
+ "limit": limit,
228
+ "min_balance": min_balance,
229
+ }
226
230
  response = self.client.get(self._build_url("/holders"), params=params)
227
231
  response.raise_for_status()
228
232
  return [HolderResponse(**holder_data) for holder_data in response.json()]
@@ -332,7 +336,7 @@ class PolymarketDataClient:
332
336
  window: Literal["1d", "7d", "30d", "all"] = "all",
333
337
  ):
334
338
  """Get a user's overall profit or volume in the last day, week, month or all."""
335
- params = {
339
+ params: dict[str, int | str] = {
336
340
  "address": user,
337
341
  "window": window,
338
342
  "limit": 1,
@@ -104,29 +104,29 @@ class PolymarketGammaClient:
104
104
 
105
105
  def get_markets(
106
106
  self,
107
- limit: Optional[int] = None,
108
- offset: Optional[int] = None,
109
- order: Optional[str] = None,
107
+ limit: int | None = None,
108
+ offset: int | None = None,
109
+ order: str | None = None,
110
110
  ascending: bool = True,
111
- archived: Optional[bool] = None,
112
- active: Optional[bool] = None,
113
- closed: Optional[bool] = None,
114
- slugs: Optional[list[str]] = None,
115
- market_ids: Optional[list[int]] = None,
116
- token_ids: Optional[list[str]] = None,
117
- condition_ids: Optional[list[str]] = None,
118
- tag_id: Optional[int] = None,
119
- related_tags: Optional[bool] = False,
120
- liquidity_num_min: Optional[float] = None,
121
- liquidity_num_max: Optional[float] = None,
122
- volume_num_min: Optional[float] = None,
123
- volume_num_max: Optional[float] = None,
124
- start_date_min: Optional[datetime] = None,
125
- start_date_max: Optional[datetime] = None,
126
- end_date_min: Optional[datetime] = None,
127
- end_date_max: Optional[datetime] = None,
111
+ archived: bool | None = None,
112
+ active: bool | None = None,
113
+ closed: bool | None = None,
114
+ slugs: list[str] | None = None,
115
+ market_ids: list[int] | None = None,
116
+ token_ids: list[str] | None = None,
117
+ condition_ids: list[str] | None = None,
118
+ tag_id: int | None = None,
119
+ related_tags: bool | None = False,
120
+ liquidity_num_min: float | None = None,
121
+ liquidity_num_max: float | None = None,
122
+ volume_num_min: float | None = None,
123
+ volume_num_max: float | None = None,
124
+ start_date_min: datetime | None = None,
125
+ start_date_max: datetime | None = None,
126
+ end_date_min: datetime | None = None,
127
+ end_date_max: datetime | None = None,
128
128
  ) -> list[GammaMarket]:
129
- params = {}
129
+ params: dict[str, float | int | list[int] | str | list[str] | bool] = {}
130
130
  if limit:
131
131
  params["limit"] = limit
132
132
  if offset:
@@ -1,10 +1,15 @@
1
1
  from json import load
2
2
  from pathlib import Path
3
- from typing import Literal, Optional
3
+ from typing import Literal
4
4
 
5
5
  from web3 import Web3
6
+ from web3.constants import MAX_INT
6
7
  from web3.exceptions import ContractCustomError
7
- from web3.middleware import ExtraDataToPOAMiddleware, SignAndSendRawMiddlewareBuilder
8
+ from web3.middleware import (
9
+ ExtraDataToPOAMiddleware,
10
+ SignAndSendRawMiddlewareBuilder,
11
+ )
12
+ from web3.types import ChecksumAddress, TxParams, Wei
8
13
 
9
14
  from ..types.common import EthAddress, Keccak256
10
15
  from ..utilities.config import get_contract_config
@@ -36,9 +41,10 @@ class PolymarketWeb3Client:
36
41
  chain_id: Literal[137, 80002] = POLYGON,
37
42
  ):
38
43
  self.w3 = Web3(Web3.HTTPProvider("https://polygon-rpc.com"))
39
- self.w3.middleware_onion.inject(ExtraDataToPOAMiddleware, layer=0)
44
+ self.w3.middleware_onion.inject(ExtraDataToPOAMiddleware, layer=0) # type: ignore[arg-type]
40
45
  self.w3.middleware_onion.inject(
41
- SignAndSendRawMiddlewareBuilder.build(private_key), layer=0
46
+ SignAndSendRawMiddlewareBuilder.build(private_key), # type: ignore[arg-type]
47
+ layer=0,
42
48
  )
43
49
 
44
50
  self.account = self.w3.eth.account.from_key(private_key)
@@ -71,19 +77,25 @@ class PolymarketWeb3Client:
71
77
  self.neg_risk_exchange_address, self.neg_risk_exchange_abi
72
78
  )
73
79
 
74
- self.neg_risk_adapter_address = "0xd91E80cF2E7be2e162c6513ceD06f1dD0dA35296"
80
+ self.neg_risk_adapter_address = Web3.to_checksum_address(
81
+ "0xd91E80cF2E7be2e162c6513ceD06f1dD0dA35296"
82
+ )
75
83
  self.neg_risk_adapter_abi = _load_abi("NegRiskAdapter")
76
84
  self.neg_risk_adapter = self.contract(
77
85
  self.neg_risk_adapter_address, self.neg_risk_adapter_abi
78
86
  )
79
87
 
80
- self.proxy_factory_address = "0xaB45c5A4B0c941a2F231C04C3f49182e1A254052"
88
+ self.proxy_factory_address = Web3.to_checksum_address(
89
+ "0xaB45c5A4B0c941a2F231C04C3f49182e1A254052"
90
+ )
81
91
  self.proxy_factory_abi = _load_abi("ProxyWalletFactory")
82
92
  self.proxy_factory = self.contract(
83
93
  self.proxy_factory_address, self.proxy_factory_abi
84
94
  )
85
95
 
86
- self.safe_proxy_factory_address = "0xaacFeEa03eb1561C4e67d661e40682Bd20E3541b"
96
+ self.safe_proxy_factory_address = Web3.to_checksum_address(
97
+ "0xaacFeEa03eb1561C4e67d661e40682Bd20E3541b"
98
+ )
87
99
  self.safe_proxy_factory_abi = _load_abi("SafeProxyFactory")
88
100
  self.safe_proxy_factory = self.contract(
89
101
  self.safe_proxy_factory_address, self.safe_proxy_factory_abi
@@ -99,6 +111,18 @@ class PolymarketWeb3Client:
99
111
  self.safe_abi = _load_abi("Safe")
100
112
  self.safe = self.contract(self.address, self.safe_abi)
101
113
 
114
+ def _encode_usdc_approve(self, address: ChecksumAddress) -> str:
115
+ return self.usdc.encode_abi(
116
+ abi_element_identifier="approve",
117
+ args=[address, int(MAX_INT, base=16)],
118
+ )
119
+
120
+ def _encode_condition_tokens_approve(self, address: ChecksumAddress) -> str:
121
+ return self.conditional_tokens.encode_abi(
122
+ abi_element_identifier="setApprovalForAll",
123
+ args=[address, True],
124
+ )
125
+
102
126
  def _encode_split(self, condition_id: Keccak256, amount: int) -> str:
103
127
  return self.conditional_tokens.encode_abi(
104
128
  abi_element_identifier="splitPosition",
@@ -133,6 +157,139 @@ class PolymarketWeb3Client:
133
157
  args=[neg_risk_market_id, index_set, amount],
134
158
  )
135
159
 
160
+ def _build_transaction(self) -> TxParams:
161
+ """Build base transaction parameters."""
162
+ nonce = self.w3.eth.get_transaction_count(self.account.address)
163
+
164
+ current_gas_price: int = self.w3.eth.gas_price
165
+ adjusted_gas_price = Wei(int(current_gas_price * 1.05))
166
+
167
+ transaction: TxParams = {
168
+ "nonce": nonce,
169
+ "gasPrice": adjusted_gas_price,
170
+ "gas": 1000000,
171
+ "from": self.account.address,
172
+ }
173
+
174
+ return transaction
175
+
176
+ def _set_collateral_approval(self, spender: ChecksumAddress) -> str:
177
+ data = self._encode_usdc_approve(address=spender)
178
+ transaction = self._build_transaction()
179
+ txn_data: TxParams | None = None
180
+
181
+ match self.signature_type:
182
+ case 0:
183
+ txn_data = self.usdc.functions.approve(
184
+ spender, int(MAX_INT, base=16)
185
+ ).build_transaction(transaction=transaction)
186
+ case 1:
187
+ proxy_txn = {
188
+ "typeCode": 1,
189
+ "to": self.usdc_address,
190
+ "value": 0,
191
+ "data": data,
192
+ }
193
+ txn_data = self.proxy_factory.functions.proxy(
194
+ [proxy_txn]
195
+ ).build_transaction(transaction=transaction)
196
+ case 2:
197
+ safe_nonce = self.safe.functions.nonce().call()
198
+ safe_txn = {
199
+ "to": self.usdc_address,
200
+ "data": data,
201
+ "operation": 0, # 1 for delegatecall, 0 for call
202
+ "value": 0,
203
+ }
204
+ packed_sig = sign_safe_transaction(
205
+ self.account, self.safe, safe_txn, safe_nonce
206
+ )
207
+ txn_data = self.safe.functions.execTransaction(
208
+ safe_txn["to"],
209
+ safe_txn["value"],
210
+ safe_txn["data"],
211
+ safe_txn.get("operation", 0),
212
+ 0, # safeTxGas
213
+ 0, # baseGas
214
+ 0, # gasPrice
215
+ ADDRESS_ZERO, # gasToken
216
+ ADDRESS_ZERO, # refundReceiver
217
+ packed_sig,
218
+ ).build_transaction(transaction=transaction)
219
+
220
+ signed_txn = self.account.sign_transaction(txn_data)
221
+ tx_hash = self.w3.eth.send_raw_transaction(signed_txn.raw_transaction)
222
+ tx_hash_hex = tx_hash.hex()
223
+
224
+ print(f"Txn hash: 0x{tx_hash_hex}")
225
+
226
+ # Wait for transaction to be mined
227
+ self.w3.eth.wait_for_transaction_receipt(tx_hash)
228
+
229
+ print("Done!")
230
+
231
+ return f"0x{tx_hash_hex}"
232
+
233
+ def _set_conditional_tokens_approval(self, spender: ChecksumAddress) -> str:
234
+ data = self._encode_condition_tokens_approve(address=spender)
235
+ transaction = self._build_transaction()
236
+ txn_data: TxParams | None = None
237
+
238
+ match self.signature_type:
239
+ case 0:
240
+ txn_data = self.conditional_tokens.functions.setApprovalForAll(
241
+ spender, True
242
+ ).build_transaction(transaction=transaction)
243
+ case 1:
244
+ proxy_txn = {
245
+ "typeCode": 1,
246
+ "to": self.conditional_tokens_address,
247
+ "value": 0,
248
+ "data": data,
249
+ }
250
+ txn_data = self.proxy_factory.functions.proxy(
251
+ [proxy_txn]
252
+ ).build_transaction(transaction=transaction)
253
+ case 2:
254
+ safe_nonce = self.safe.functions.nonce().call()
255
+ safe_txn = {
256
+ "to": self.conditional_tokens_address,
257
+ "data": data,
258
+ "operation": 0, # 1 for delegatecall, 0 for call
259
+ "value": 0,
260
+ }
261
+ packed_sig = sign_safe_transaction(
262
+ self.account,
263
+ self.safe,
264
+ safe_txn,
265
+ safe_nonce,
266
+ )
267
+ txn_data = self.safe.functions.execTransaction(
268
+ safe_txn["to"],
269
+ safe_txn["value"],
270
+ safe_txn["data"],
271
+ safe_txn.get("operation", 0),
272
+ 0, # safeTxGas
273
+ 0, # baseGas
274
+ 0, # gasPrice
275
+ ADDRESS_ZERO, # gasToken
276
+ ADDRESS_ZERO, # refundReceiver
277
+ packed_sig,
278
+ ).build_transaction(transaction=transaction)
279
+
280
+ signed_txn = self.account.sign_transaction(txn_data)
281
+ tx_hash = self.w3.eth.send_raw_transaction(signed_txn.raw_transaction)
282
+ tx_hash_hex = tx_hash.hex()
283
+
284
+ print(f"Txn hash: 0x{tx_hash_hex}")
285
+
286
+ # Wait for transaction to be mined
287
+ self.w3.eth.wait_for_transaction_receipt(tx_hash)
288
+
289
+ print("Done!")
290
+
291
+ return f"0x{tx_hash_hex}"
292
+
136
293
  def contract(self, address, abi):
137
294
  return self.w3.eth.contract(
138
295
  address=Web3.to_checksum_address(address),
@@ -163,7 +320,7 @@ class PolymarketWeb3Client:
163
320
  return float(balance_res / 1e6)
164
321
 
165
322
  def get_token_balance(
166
- self, token_id: str, address: Optional[EthAddress] = None
323
+ self, token_id: str, address: EthAddress | None = None
167
324
  ) -> float:
168
325
  """Get the token balance of the given address."""
169
326
  if not address:
@@ -173,7 +330,7 @@ class PolymarketWeb3Client:
173
330
  ).call()
174
331
  return float(balance_res / 1e6)
175
332
 
176
- def get_token_complement(self, token_id: str) -> Optional[str]:
333
+ def get_token_complement(self, token_id: str) -> str | None:
177
334
  """Get the complement of the given token."""
178
335
  try:
179
336
  return str(
@@ -191,6 +348,8 @@ class PolymarketWeb3Client:
191
348
  raise ContractCustomError(
192
349
  msg,
193
350
  ) from e2
351
+ return None
352
+ return None
194
353
 
195
354
  def get_condition_id_neg_risk(self, question_id: Keccak256) -> Keccak256:
196
355
  """
@@ -205,9 +364,41 @@ class PolymarketWeb3Client:
205
364
  + self.neg_risk_adapter.functions.getConditionId(question_id).call().hex()
206
365
  )
207
366
 
367
+ def set_all_approvals(self) -> None:
368
+ """Sets both collateral and conditional tokens approvals."""
369
+ print("Approving ConditionalTokens as spender on USDC")
370
+ self._set_collateral_approval(
371
+ spender=self.conditional_tokens_address,
372
+ )
373
+ print("Approving CTFExchange as spender on USDC")
374
+ self._set_collateral_approval(
375
+ spender=self.exchange_address,
376
+ )
377
+ print("Approving NegRiskCtfExchange as spender on USDC")
378
+ self._set_collateral_approval(
379
+ spender=self.neg_risk_exchange_address,
380
+ )
381
+ print("Approving NegRiskAdapter as spender on USDC")
382
+ self._set_collateral_approval(
383
+ spender=self.neg_risk_adapter_address,
384
+ )
385
+ print("Approving CTFExchange as spender on ConditionalTokens")
386
+ self._set_conditional_tokens_approval(
387
+ spender=self.exchange_address,
388
+ )
389
+ print("Approving NegRiskCtfExchange as spender on ConditionalTokens")
390
+ self._set_conditional_tokens_approval(
391
+ spender=self.neg_risk_exchange_address,
392
+ )
393
+ print("Approving NegRiskAdapter as spender on ConditionalTokens")
394
+ self._set_conditional_tokens_approval(
395
+ spender=self.neg_risk_adapter_address,
396
+ )
397
+ print("All approvals set!")
398
+
208
399
  def split_position(
209
400
  self, condition_id: Keccak256, amount: float, neg_risk: bool = True
210
- ):
401
+ ) -> str:
211
402
  """Splits usdc into two complementary positions of equal size."""
212
403
  amount = int(amount * 1e6)
213
404
  data = self._encode_split(condition_id, amount)
@@ -216,13 +407,8 @@ class PolymarketWeb3Client:
216
407
  if neg_risk
217
408
  else self.conditional_tokens_address
218
409
  )
219
- nonce = self.w3.eth.get_transaction_count(self.account.address)
220
- transaction = {
221
- "nonce": nonce,
222
- "gasPrice": int(1.05 * self.w3.eth.gas_price),
223
- "gas": 1000000,
224
- "from": self.account.address,
225
- }
410
+ transaction = self._build_transaction()
411
+ txn_data: TxParams | None = None
226
412
 
227
413
  match self.signature_type:
228
414
  case 0:
@@ -282,9 +468,11 @@ class PolymarketWeb3Client:
282
468
 
283
469
  print("Done!")
284
470
 
471
+ return f"0x{tx_hash_hex}"
472
+
285
473
  def merge_position(
286
474
  self, condition_id: Keccak256, amount: float, neg_risk: bool = True
287
- ):
475
+ ) -> str:
288
476
  """Merges two complementary positions into usdc."""
289
477
  amount = int(amount * 1e6)
290
478
  data = self._encode_merge(condition_id, amount)
@@ -293,13 +481,8 @@ class PolymarketWeb3Client:
293
481
  if neg_risk
294
482
  else self.conditional_tokens_address
295
483
  )
296
- nonce = self.w3.eth.get_transaction_count(self.account.address)
297
- transaction = {
298
- "nonce": nonce,
299
- "gasPrice": int(1.05 * self.w3.eth.gas_price),
300
- "gas": 1000000,
301
- "from": self.account.address,
302
- }
484
+ transaction = self._build_transaction()
485
+ txn_data: TxParams | None = None
303
486
 
304
487
  match self.signature_type:
305
488
  case 0:
@@ -360,9 +543,11 @@ class PolymarketWeb3Client:
360
543
 
361
544
  print("Done!")
362
545
 
546
+ return f"0x{tx_hash_hex}"
547
+
363
548
  def redeem_position(
364
549
  self, condition_id: Keccak256, amounts: list[float], neg_risk: bool = True
365
- ):
550
+ ) -> str:
366
551
  """
367
552
  Redeem a position into usdc.
368
553
 
@@ -381,13 +566,8 @@ class PolymarketWeb3Client:
381
566
  if neg_risk
382
567
  else self.conditional_tokens_address
383
568
  )
384
- nonce = self.w3.eth.get_transaction_count(self.account.address)
385
- transaction = {
386
- "nonce": nonce,
387
- "gasPrice": int(1.05 * self.w3.eth.gas_price),
388
- "gas": 1000000,
389
- "from": self.account.address,
390
- }
569
+ transaction = self._build_transaction()
570
+ txn_data: TxParams | None = None
391
571
 
392
572
  match self.signature_type:
393
573
  case 0:
@@ -451,24 +631,21 @@ class PolymarketWeb3Client:
451
631
 
452
632
  print("Done!")
453
633
 
634
+ return f"0x{tx_hash_hex}"
635
+
454
636
  def convert_positions(
455
637
  self,
456
638
  question_ids: list[Keccak256],
457
639
  amount: float,
458
- ):
640
+ ) -> str:
459
641
  amount = int(amount * 1e6)
460
642
  neg_risk_market_id = question_ids[0][:-2] + "00"
461
643
  data = self._encode_convert(
462
644
  neg_risk_market_id, get_index_set(question_ids), amount
463
645
  )
464
646
  to = self.neg_risk_adapter_address
465
- nonce = self.w3.eth.get_transaction_count(self.account.address)
466
- transaction = {
467
- "nonce": nonce,
468
- "gasPrice": int(1.05 * self.w3.eth.gas_price),
469
- "gas": 1000000,
470
- "from": self.account.address,
471
- }
647
+ transaction = self._build_transaction()
648
+ txn_data: TxParams | None = None
472
649
 
473
650
  match self.signature_type:
474
651
  case 0:
@@ -524,3 +701,5 @@ class PolymarketWeb3Client:
524
701
  self.w3.eth.wait_for_transaction_receipt(tx_hash)
525
702
 
526
703
  print("Done!")
704
+
705
+ return f"0x{tx_hash_hex}"
@@ -221,7 +221,7 @@ class ClobMarket(BaseModel):
221
221
  @classmethod
222
222
  def validate_neg_risk_fields(
223
223
  cls, value: str, handler: ValidatorFunctionWrapHandler, info: ValidationInfo
224
- ) -> Optional[str]:
224
+ ) -> str | None:
225
225
  try:
226
226
  return handler(value)
227
227
  except ValidationError as e:
@@ -238,6 +238,7 @@ class ClobMarket(BaseModel):
238
238
  f" Question: {info.data.get('question')}; Market slug: {info.data.get('market_slug')} \n"
239
239
  )
240
240
  logger.warning(msg)
241
+ return None
241
242
 
242
243
  @field_validator("condition_id", "question_id", mode="wrap")
243
244
  @classmethod
@@ -323,8 +324,8 @@ class DropNotificationParams(BaseModel):
323
324
 
324
325
 
325
326
  class OrderSummary(BaseModel):
326
- price: Optional[float] = None
327
- size: Optional[float] = None
327
+ price: float
328
+ size: float
328
329
 
329
330
 
330
331
  class PriceLevel(OrderSummary):
@@ -505,7 +506,7 @@ class OrderPostResponse(BaseModel):
505
506
  order_id: Union[Keccak256, Literal[""]] = Field(alias="orderID")
506
507
  taking_amount: str = Field(alias="takingAmount")
507
508
  making_amount: str = Field(alias="makingAmount")
508
- status: str = Literal["live", "matched", "delayed"]
509
+ status: Literal["live", "matched", "delayed"]
509
510
  success: bool
510
511
 
511
512
 
@@ -19,7 +19,11 @@ def parse_flexible_datetime(v: str | datetime) -> datetime:
19
19
  return datetime.fromtimestamp(0, tz=UTC)
20
20
 
21
21
  if isinstance(v, str):
22
- return parser.parse(v)
22
+ parsed = parser.parse(v)
23
+ if not isinstance(parsed, datetime):
24
+ msg = f"Failed to parse '{v}' as datetime, got {type(parsed)}"
25
+ raise TypeError(msg)
26
+ return parsed
23
27
  return v
24
28
 
25
29
 
@@ -1,3 +1,5 @@
1
+ from typing import Literal
2
+
1
3
  # Access levels
2
4
  L0 = 0
3
5
  L1 = 1
@@ -17,9 +19,8 @@ L2_AUTH_UNAVAILABLE = "API Credentials are needed to interact with this endpoint
17
19
  ADDRESS_ZERO = "0x0000000000000000000000000000000000000000"
18
20
  HASH_ZERO = "0x0000000000000000000000000000000000000000000000000000000000000000"
19
21
 
20
- AMOY = 80002
21
- POLYGON = 137
22
-
22
+ AMOY: Literal[80002] = 80002
23
+ POLYGON: Literal[137] = 137
23
24
  END_CURSOR = "LTE="
24
25
 
25
26
  BUY = "BUY"
@@ -221,10 +221,10 @@ class OrderBuilder:
221
221
  msg = "No ask orders available"
222
222
  raise LiquidityError(msg)
223
223
 
224
- sum = 0
224
+ amount = 0.0
225
225
  for p in reversed(asks):
226
- sum += float(p.size) * float(p.price)
227
- if sum >= amount_to_match:
226
+ amount += float(p.size) * float(p.price)
227
+ if amount >= amount_to_match:
228
228
  return float(p.price)
229
229
 
230
230
  if order_type == OrderType.FOK:
@@ -245,10 +245,10 @@ class OrderBuilder:
245
245
  msg = "No bid orders available"
246
246
  raise LiquidityError(msg)
247
247
 
248
- sum = 0
248
+ amount = 0.0
249
249
  for p in reversed(bids):
250
- sum += float(p.size)
251
- if sum >= amount_to_match:
250
+ amount += float(p.size)
251
+ if amount >= amount_to_match:
252
252
  return float(p.price)
253
253
 
254
254
  if order_type == OrderType.FOK:
@@ -11,7 +11,7 @@ class Signer:
11
11
  raise ValueError(msg)
12
12
 
13
13
  self.private_key = private_key
14
- self.account = Account.from_key(private_key)
14
+ self.account = Account.from_key(private_key) # type: ignore[misc]
15
15
  self.chain_id = chain_id
16
16
 
17
17
  def address(self):
@@ -22,4 +22,4 @@ class Signer:
22
22
 
23
23
  def sign(self, message_hash):
24
24
  """Signs a message hash."""
25
- return Account.unsafe_sign_hash(message_hash, self.private_key).signature.hex()
25
+ return Account.unsafe_sign_hash(message_hash, self.private_key).signature.hex() # type: ignore[misc]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: polymarket-apis
3
- Version: 0.3.6
3
+ Version: 0.3.7
4
4
  Summary: Unified Polymarket APIs with Pydantic data validation - Clob, Gamma, Data, Web3, Websockets, GraphQL clients.
5
5
  Project-URL: repository, https://github.com/qualiaenjoyer/polymarket-apis
6
6
  Author-email: Razvan Gheorghe <razvan@gheorghe.me>
@@ -155,7 +155,10 @@ flowchart LR
155
155
  - get top users on the profit/volume leaderboards (at most 100) for a recent window (1d, 7d, 30d, all)
156
156
 
157
157
  ### PolymarketWeb3Client - Blockchain related operations
158
- - #### Supporting both Email/Magic wallets (signature_type=1) and Safe/Gnosis wallets (signature_type=2)
158
+ - #### Supporting EOA(signature_type=0), Email/Magic wallets (signature_type=1) and Safe/Gnosis wallets (signature_type=2)
159
+ - #### Approvals
160
+ - set approvals for all needed usdc and conditional token spenders (needed for full trading functionality)
161
+ - Safe/Gnosis wallets need to be deployed beforehand, either via "Enable Trading" pop-up on the web UI or by calling createProxy on the SafeWalletFactory contract (wip)
159
162
  - #### Balance
160
163
  - get usdc balance by user address
161
164
  - get token balance by `token_id` and user address
@@ -1,31 +1,31 @@
1
- polymarket_apis/__init__.py,sha256=qovYOBPNsQVc792R8UIVlGIrU-p-2DKclTosLQZbhT4,1126
1
+ polymarket_apis/__init__.py,sha256=OzhWc-PqnHoGyrcRwclQG2mLGSu0cz7QLuhx-jenzgk,1126
2
2
  polymarket_apis/clients/__init__.py,sha256=ruMvFEA4HNkbWEYqbnrCuYXR4PUwkV1XWG0w63we-LA,759
3
- polymarket_apis/clients/clob_client.py,sha256=gt3EqqMYOYB0_hI8HByp0flfKRsUkzL9gpQZJxX_r5k,32359
4
- polymarket_apis/clients/data_client.py,sha256=xHZcjZMKpzPD8kipPc1qN08q7jWPqN3Py1hD28MQ2EQ,13151
5
- polymarket_apis/clients/gamma_client.py,sha256=tBR_z03c3wh1_sxFbjNx54DcrFEjFB0TmJiOYNUc_pk,27632
3
+ polymarket_apis/clients/clob_client.py,sha256=Eo5zXYtI_Ct0EByzjJnH6h7FLHOqheXDujBA57CtINw,32485
4
+ polymarket_apis/clients/data_client.py,sha256=0--2W6_DZtC7cRud8OYS7bH2heWzn2NwvcUVK3PyEwU,13242
5
+ polymarket_apis/clients/gamma_client.py,sha256=iDfuaClhRK2Y5v8ZA03Qbne3CnIM4JJolHo-qUrQV78,27633
6
6
  polymarket_apis/clients/graphql_client.py,sha256=KgjxbXNWEXp82ZEz464in5mCn1PydnZqWq-g11xu9YU,1839
7
- polymarket_apis/clients/web3_client.py,sha256=KR48E9rVKfwqo6BCtGKQZ6VYyeRTiKPKMlI3sv0gjpk,19843
7
+ polymarket_apis/clients/web3_client.py,sha256=Xsk1APsnMDRu9daLKZnf6LW41LdocuRV9rqjKnZjYmw,26400
8
8
  polymarket_apis/clients/websockets_client.py,sha256=gaGcsjqp1ZRyxrL6EWvZ9XaTv7koTPDzlcShj0u0B2A,7737
9
9
  polymarket_apis/types/__init__.py,sha256=cyrAPNX0ty0wkMwZqB0lNA7n3JoOJCSeTpclmamMh9k,3258
10
- polymarket_apis/types/clob_types.py,sha256=PmD_afx7zowX0cFJ39PQQ4jywGtwNjLfoke1H7iZZnI,11744
11
- polymarket_apis/types/common.py,sha256=zFo10eBj4_3alOhYSilCm6Dh5ZkzhrdR9nuRSDQ_od8,1107
10
+ polymarket_apis/types/clob_types.py,sha256=WZs370ni2B2UsLW-yIPGA2C-z8dfbywX26vADuu2YFc,11725
11
+ polymarket_apis/types/common.py,sha256=OYvn-BIbLM5YHNiZCi9ttqbqfPjRdvFhBE192EsBIdQ,1284
12
12
  polymarket_apis/types/data_types.py,sha256=vBElvARw8LkH2xXwJkVErpQo0FkXJA3b44hKD5clSOI,5729
13
13
  polymarket_apis/types/gamma_types.py,sha256=MoIUI6csqEp_vij7mkFi2IlIA99c5UNzI3zODSONWzQ,30641
14
14
  polymarket_apis/types/websockets_types.py,sha256=XGTpfix2rFFQg-97ZkPagS_tHJJpSnB5mCMf_sT2MOI,11180
15
15
  polymarket_apis/utilities/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
16
  polymarket_apis/utilities/config.py,sha256=kQymQRy9fVg5jt8CcQJxsSgIZFbfjPx2q_gNnZI5b24,2449
17
- polymarket_apis/utilities/constants.py,sha256=rGX-XXu5tLy2yUurFQCQNRtSvZPPcRAUCmjlqVtTAvY,580
17
+ polymarket_apis/utilities/constants.py,sha256=TzFVtN8zNgvEa4yjLWjpvPYsAi1LLV5oPEnHlbwxSDQ,637
18
18
  polymarket_apis/utilities/endpoints.py,sha256=bxZyrJBPbVauWc-eR0RMh6KDqU-SmO_3LfQwVMNJ6vE,1235
19
19
  polymarket_apis/utilities/exceptions.py,sha256=nLVkwGNkX8mBhOi3L3lLEJ5UCPd5OBjl2f7kcct3K3A,368
20
20
  polymarket_apis/utilities/headers.py,sha256=Cc5WEnIBLYAgfwvmCXRBwA2zUYME8fDy4PbwlwlB6Oo,1510
21
21
  polymarket_apis/utilities/order_builder/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
- polymarket_apis/utilities/order_builder/builder.py,sha256=1SXgJlKlutRqMA1iBtZcw5K6CWI379d4wXZ5pdNgs0M,8454
22
+ polymarket_apis/utilities/order_builder/builder.py,sha256=dyxKhMeNjGyHXEIFBBZhWwI8TaQQcE_ErQ4sF6BcMX0,8476
23
23
  polymarket_apis/utilities/order_builder/helpers.py,sha256=2-UYDLntYOXjyN0ZDQI13jW2zwXwIODEK-smBRFTYRk,1943
24
24
  polymarket_apis/utilities/signing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
25
25
  polymarket_apis/utilities/signing/eip712.py,sha256=fGLurznnfY5M0VCP1Txyq_FYQRfggyHoTwz-PGIK2Eg,895
26
26
  polymarket_apis/utilities/signing/hmac.py,sha256=1VPfO2yT8nyStk6U4AQeyTzQTt5-69PTw24Gdt5YOMo,718
27
27
  polymarket_apis/utilities/signing/model.py,sha256=kVduuJGth7WSCUDCVVydCgPd4yEVI85gEmMxohXsvp0,191
28
- polymarket_apis/utilities/signing/signer.py,sha256=7-nFALFrz0qg4F4lZRyHI41S7IWxr7t0WMLOHqHzJcg,732
28
+ polymarket_apis/utilities/signing/signer.py,sha256=cxjcYRn1a0ApVcir1AvamsFcLZ0rYNlEEX4sPUz_Nrw,776
29
29
  polymarket_apis/utilities/web3/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
30
30
  polymarket_apis/utilities/web3/helpers.py,sha256=qzQA-n9rHbuFk57olvBa8P_T6IzrUsaQ2Vv0887EX4o,4694
31
31
  polymarket_apis/utilities/web3/abis/CTFExchange.json,sha256=zt8fZnUaOrD8Vh5njM0EEUpeITWhuu0SZrIZigWxgV8,38499
@@ -38,6 +38,6 @@ polymarket_apis/utilities/web3/abis/SafeProxyFactory.json,sha256=bdr2WdYCRClXLTT
38
38
  polymarket_apis/utilities/web3/abis/UChildERC20Proxy.json,sha256=ZyQC38U0uxInlmnW2VXDVD3TJfTIRmSNMkTxQsaG7oA,27396
39
39
  polymarket_apis/utilities/web3/abis/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
40
40
  polymarket_apis/utilities/web3/abis/custom_contract_errors.py,sha256=GjCVn2b6iRheT7s-kc8Po9uwH9LfaHA1yRpJyjXRcxs,1172
41
- polymarket_apis-0.3.6.dist-info/METADATA,sha256=AToVPj7UlKers6vyyeIFBjgNvdridWvI7Y82_T8fHzg,12479
42
- polymarket_apis-0.3.6.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
43
- polymarket_apis-0.3.6.dist-info/RECORD,,
41
+ polymarket_apis-0.3.7.dist-info/METADATA,sha256=Z3jOxzSY7thon83qiZQbeZ_8sCebx2BKCVRKR-acFjA,12802
42
+ polymarket_apis-0.3.7.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
43
+ polymarket_apis-0.3.7.dist-info/RECORD,,