polymarket-apis 0.2.2__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.

Potentially problematic release.


This version of polymarket-apis might be problematic. Click here for more details.

Files changed (40) hide show
  1. polymarket_apis/__init__.py +2 -0
  2. polymarket_apis/clients/__init__.py +0 -0
  3. polymarket_apis/clients/clob_client.py +730 -0
  4. polymarket_apis/clients/data_client.py +234 -0
  5. polymarket_apis/clients/gamma_client.py +311 -0
  6. polymarket_apis/clients/web3_client.py +261 -0
  7. polymarket_apis/clients/websockets_client.py +131 -0
  8. polymarket_apis/types/__init__.py +0 -0
  9. polymarket_apis/types/clob_types.py +494 -0
  10. polymarket_apis/types/common.py +49 -0
  11. polymarket_apis/types/data_types.py +161 -0
  12. polymarket_apis/types/gamma_types.py +313 -0
  13. polymarket_apis/types/websockets_types.py +191 -0
  14. polymarket_apis/utilities/__init__.py +0 -0
  15. polymarket_apis/utilities/config.py +36 -0
  16. polymarket_apis/utilities/constants.py +26 -0
  17. polymarket_apis/utilities/endpoints.py +37 -0
  18. polymarket_apis/utilities/exceptions.py +11 -0
  19. polymarket_apis/utilities/headers.py +54 -0
  20. polymarket_apis/utilities/order_builder/__init__.py +0 -0
  21. polymarket_apis/utilities/order_builder/builder.py +240 -0
  22. polymarket_apis/utilities/order_builder/helpers.py +61 -0
  23. polymarket_apis/utilities/signing/__init__.py +0 -0
  24. polymarket_apis/utilities/signing/eip712.py +28 -0
  25. polymarket_apis/utilities/signing/hmac.py +20 -0
  26. polymarket_apis/utilities/signing/model.py +8 -0
  27. polymarket_apis/utilities/signing/signer.py +25 -0
  28. polymarket_apis/utilities/web3/__init__.py +0 -0
  29. polymarket_apis/utilities/web3/abis/CTFExchange.json +1851 -0
  30. polymarket_apis/utilities/web3/abis/ConditionalTokens.json +705 -0
  31. polymarket_apis/utilities/web3/abis/NegRiskAdapter.json +999 -0
  32. polymarket_apis/utilities/web3/abis/NegRiskCtfExchange.json +1856 -0
  33. polymarket_apis/utilities/web3/abis/ProxyWalletFactory.json +319 -0
  34. polymarket_apis/utilities/web3/abis/UChildERC20Proxy.json +1438 -0
  35. polymarket_apis/utilities/web3/abis/__init__.py +0 -0
  36. polymarket_apis/utilities/web3/abis/custom_contract_errors.py +31 -0
  37. polymarket_apis/utilities/web3/helpers.py +8 -0
  38. polymarket_apis-0.2.2.dist-info/METADATA +18 -0
  39. polymarket_apis-0.2.2.dist-info/RECORD +40 -0
  40. polymarket_apis-0.2.2.dist-info/WHEEL +4 -0
@@ -0,0 +1,261 @@
1
+ from json import load
2
+ from pathlib import Path
3
+ from typing import Literal, Optional
4
+
5
+ from web3 import Web3
6
+ from web3.exceptions import ContractCustomError
7
+ from web3.middleware import ExtraDataToPOAMiddleware, SignAndSendRawMiddlewareBuilder
8
+
9
+ from ..types.common import EthAddress, Keccak256
10
+ from ..utilities.config import get_contract_config
11
+ from ..utilities.constants import HASH_ZERO, POLYGON
12
+ from ..utilities.web3.abis.custom_contract_errors import CUSTOM_ERROR_DICT
13
+ from ..utilities.web3.helpers import get_index_set
14
+
15
+
16
+ def _load_abi(contract_name: str) -> list:
17
+ abi_path = Path(__file__).parent.parent/"utilities"/"web3"/"abis"/f"{contract_name}.json"
18
+ with Path.open(abi_path) as f:
19
+ return load(f)
20
+
21
+ class PolymarketWeb3Client:
22
+ def __init__(self, private_key: str , chain_id: Literal[137, 80002] = POLYGON):
23
+
24
+ self.w3 = Web3(Web3.HTTPProvider("https://polygon-rpc.com"))
25
+ self.w3.middleware_onion.inject(ExtraDataToPOAMiddleware, layer=0)
26
+ self.w3.middleware_onion.inject(SignAndSendRawMiddlewareBuilder.build(private_key), layer=0)
27
+
28
+ self.account = self.w3.eth.account.from_key(private_key)
29
+
30
+ self.config = get_contract_config(chain_id, neg_risk=False)
31
+ self.neg_risk_config = get_contract_config(chain_id, neg_risk=True)
32
+
33
+ self.usdc_address = Web3.to_checksum_address(self.config.collateral)
34
+ self.usdc_abi = _load_abi("UChildERC20Proxy")
35
+ self.usdc = self.contract(self.usdc_address, self.usdc_abi)
36
+
37
+ self.conditional_tokens_address = Web3.to_checksum_address(self.config.conditional_tokens)
38
+ self.conditional_tokens_abi = _load_abi("ConditionalTokens")
39
+ self.conditional_tokens = self.contract(self.conditional_tokens_address, self.conditional_tokens_abi)
40
+
41
+ self.exchange_address = Web3.to_checksum_address(self.config.exchange)
42
+ self.exchange_abi = _load_abi("CTFExchange")
43
+ self.exchange = self.contract(self.exchange_address, self.exchange_abi)
44
+
45
+ self.neg_risk_exchange_address = Web3.to_checksum_address(self.neg_risk_config.exchange)
46
+ self.neg_risk_exchange_abi = _load_abi("NegRiskCtfExchange")
47
+ self.neg_risk_exchange = self.contract(self.neg_risk_exchange_address, self.neg_risk_exchange_abi)
48
+
49
+ self.neg_risk_adapter_address = "0xd91E80cF2E7be2e162c6513ceD06f1dD0dA35296"
50
+ self.neg_risk_adapter_abi = _load_abi("NegRiskAdapter")
51
+ self.neg_risk_adapter = self.contract(self.neg_risk_adapter_address, self.neg_risk_adapter_abi)
52
+
53
+ self.proxy_factory_address = "0xaB45c5A4B0c941a2F231C04C3f49182e1A254052"
54
+ self.proxy_factory_abi = _load_abi("ProxyWalletFactory")
55
+ self.proxy_factory = self.contract(self.proxy_factory_address, self.proxy_factory_abi)
56
+
57
+ def _encode_split(self, condition_id: Keccak256, amount: int) -> str:
58
+ return self.conditional_tokens.encode_abi(
59
+ abi_element_identifier="splitPosition",
60
+ args=[self.usdc_address, HASH_ZERO, condition_id, [1, 2], amount],
61
+ )
62
+
63
+ def _encode_merge(self, condition_id: Keccak256, amount: int) -> str:
64
+ return self.conditional_tokens.encode_abi(
65
+ abi_element_identifier="mergePositions",
66
+ args=[self.usdc_address, HASH_ZERO, condition_id, [1, 2], amount],
67
+ )
68
+
69
+ def _encode_redeem(self, condition_id: Keccak256) -> str:
70
+ return self.conditional_tokens.encode_abi(
71
+ abi_element_identifier="redeemPositions",
72
+ args=[self.usdc_address, HASH_ZERO, condition_id, [1, 2]],
73
+ )
74
+
75
+ def _encode_redeem_neg_risk(self, condition_id: Keccak256, amounts: list[int]) -> str:
76
+ return self.neg_risk_adapter.encode_abi(
77
+ abi_element_identifier="redeemPositions",
78
+ args=[condition_id, amounts],
79
+ )
80
+ def _encode_convert(self, neg_risk_market_id: Keccak256, index_set: int, amount: int):
81
+ return self.neg_risk_adapter.encode_abi(
82
+ abi_element_identifier="convertPositions",
83
+ args=[neg_risk_market_id, index_set, amount],
84
+ )
85
+
86
+ def contract(self, address, abi):
87
+ return self.w3.eth.contract(
88
+ address=Web3.to_checksum_address(address),
89
+ abi=abi,
90
+ )
91
+
92
+ def get_usdc_balance(self, address: EthAddress | None = None) -> float:
93
+ """
94
+ Get the usdc balance of the given address.
95
+
96
+ If no address is given, the balance of the proxy account corresponding to
97
+ the private key is returned (i.e. Polymarket balance).
98
+ Explicitly passing the proxy address is faster due to only one contract function call.
99
+ """
100
+ if address is None:
101
+ address = self.exchange.functions.getPolyProxyWalletAddress(self.account.address).call()
102
+ balance_res = self.usdc.functions.balanceOf(address).call()
103
+ return float(balance_res / 1e6)
104
+
105
+ def get_token_balance(self, token_id: str, address: EthAddress | None = None) -> float:
106
+ """Get the token balance of the given address."""
107
+ if address is None:
108
+ address = self.exchange.functions.getPolyProxyWalletAddress(self.account.address).call()
109
+ balance_res = self.conditional_tokens.functions.balanceOf(address, int(token_id)).call()
110
+ return float(balance_res / 1e6)
111
+
112
+ def get_token_complement(self, token_id: str) -> Optional[str]:
113
+ """Get the complement of the given token."""
114
+ try:
115
+ return str(self.neg_risk_exchange.functions.getComplement(int(token_id)).call())
116
+ except ContractCustomError as e:
117
+ if e.args[0] in CUSTOM_ERROR_DICT:
118
+ try:
119
+ return str(self.exchange.functions.getComplement(int(token_id)).call())
120
+ except ContractCustomError as e2:
121
+ if e2.args[0] in CUSTOM_ERROR_DICT:
122
+ msg = f"{CUSTOM_ERROR_DICT[e2.args[0]]}"
123
+ raise ContractCustomError(
124
+ msg,
125
+ ) from e2
126
+
127
+ def get_condition_id_neg_risk(self, question_id: Keccak256) -> Keccak256:
128
+ """
129
+ Get the condition id for a given question id.
130
+
131
+ Warning: this works for neg risk markets (where the
132
+ outcomeSlotCount is represented by the last two digits of question id). Returns a keccak256 hash of
133
+ the oracle and question id.
134
+ """
135
+ return "0x" + self.neg_risk_adapter.functions.getConditionId(question_id).call().hex()
136
+
137
+ def split_position(self, condition_id: Keccak256, amount: int, neg_risk: bool = True):
138
+ """Splits usdc into two complementary positions of equal size."""
139
+ nonce = self.w3.eth.get_transaction_count(self.account.address)
140
+ amount = int(amount * 1e6)
141
+
142
+ proxy_txn = {
143
+ "typeCode": 1,
144
+ "to": self.neg_risk_adapter_address if neg_risk else self.conditional_tokens_address,
145
+ "value": 0,
146
+ "data": self._encode_split(condition_id, amount),
147
+ }
148
+
149
+ # Send transaction through proxy factory
150
+ txn_data = self.proxy_factory.functions.proxy([proxy_txn]).build_transaction({
151
+ "nonce": nonce,
152
+ "gasPrice": int(1.05 * self.w3.eth.gas_price),
153
+ "gas": 1000000,
154
+ "from": self.account.address,
155
+ })
156
+
157
+ # Sign and send transaction
158
+ signed_txn = self.account.sign_transaction(txn_data)
159
+ tx_hash = self.w3.eth.send_raw_transaction(signed_txn.raw_transaction).hex()
160
+
161
+ print(f"Txn hash: {tx_hash}")
162
+
163
+ # Wait for transaction to be mined
164
+ self.w3.eth.wait_for_transaction_receipt(tx_hash)
165
+
166
+ print("Done!")
167
+
168
+ def merge_position(self, condition_id: Keccak256, amount: int, neg_risk: bool = True):
169
+ """Merges two complementary positions into usdc."""
170
+ nonce = self.w3.eth.get_transaction_count(self.account.address)
171
+ amount = int(amount * 1e6)
172
+
173
+ proxy_txn = {
174
+ "typeCode": 1,
175
+ "to": self.neg_risk_adapter_address if neg_risk else self.conditional_tokens_address,
176
+ "value": 0,
177
+ "data": self._encode_merge(condition_id, amount),
178
+ }
179
+
180
+ # Send transaction through proxy factory
181
+ txn_data = self.proxy_factory.functions.proxy([proxy_txn]).build_transaction({
182
+ "nonce": nonce,
183
+ "gasPrice": int(1.05 * self.w3.eth.gas_price),
184
+ "gas": 1000000,
185
+ "from": self.account.address,
186
+ })
187
+
188
+ # Sign and send transaction
189
+ signed_txn = self.account.sign_transaction(txn_data)
190
+ tx_hash = self.w3.eth.send_raw_transaction(signed_txn.raw_transaction).hex()
191
+
192
+ print(f"Txn hash: {tx_hash}")
193
+
194
+ # Wait for transaction to be mined
195
+ self.w3.eth.wait_for_transaction_receipt(tx_hash)
196
+
197
+ print("Done!")
198
+ def redeem_position(self, condition_id: Keccak256, amounts: list[float], neg_risk: bool = True):
199
+ """
200
+ Redeem a position into usdc.
201
+
202
+ Takes a condition id and a list of sizes in shares [x, y]
203
+ where x is the number of shares of the first outcome
204
+ y is the number of shares of the second outcome.
205
+ """
206
+ nonce = self.w3.eth.get_transaction_count(self.account.address)
207
+ amounts = [int(amount * 1e6) for amount in amounts]
208
+
209
+ proxy_txn = {
210
+ "typeCode": 1,
211
+ "to": self.neg_risk_adapter_address if neg_risk else self.conditional_tokens_address,
212
+ "value": 0,
213
+ "data": self._encode_redeem_neg_risk(condition_id, amounts) if neg_risk else self._encode_redeem(condition_id),
214
+ }
215
+
216
+ # Send transaction through proxy factory
217
+ txn_data = self.proxy_factory.functions.proxy([proxy_txn]).build_transaction({
218
+ "nonce": nonce,
219
+ "gasPrice": int(1.05 * self.w3.eth.gas_price),
220
+ "gas": 1000000,
221
+ "from": self.account.address,
222
+ })
223
+
224
+ # Sign and send transaction
225
+ signed_txn = self.account.sign_transaction(txn_data)
226
+ tx_hash = self.w3.eth.send_raw_transaction(signed_txn.raw_transaction).hex()
227
+
228
+ print(f"Txn hash: {tx_hash}")
229
+
230
+ # Wait for transaction to be mined
231
+ self.w3.eth.wait_for_transaction_receipt(tx_hash)
232
+
233
+ print("Done!")
234
+
235
+ def convert_positions(self, question_ids: list[Keccak256], neg_risk_market_id: Keccak256, amount: int):
236
+ nonce = self.w3.eth.get_transaction_count(self.account.address)
237
+ amount = int(amount * 1e6)
238
+
239
+ proxy_txn = {
240
+ "typeCode": 1,
241
+ "to": self.neg_risk_adapter_address,
242
+ "value": 0,
243
+ "data": self._encode_convert(neg_risk_market_id, get_index_set(question_ids), amount),
244
+ }
245
+
246
+ txn_data = self.proxy_factory.functions.proxy([proxy_txn]).build_transaction({
247
+ "nonce": nonce,
248
+ "gasPrice": int(1.05 * self.w3.eth.gas_price),
249
+ "gas": 1000000,
250
+ "from": self.account.address,
251
+ })
252
+
253
+ signed_txn = self.account.sign_transaction(txn_data)
254
+ tx_hash = self.w3.eth.send_raw_transaction(signed_txn.raw_transaction).hex()
255
+
256
+ print(f"Txn hash: {tx_hash}")
257
+
258
+ # Wait for transaction to be mined
259
+ self.w3.eth.wait_for_transaction_receipt(tx_hash)
260
+
261
+ print("Done!")
@@ -0,0 +1,131 @@
1
+ from collections.abc import Callable
2
+ from typing import Any
3
+
4
+ from lomond import WebSocket
5
+ from lomond.persist import persist
6
+ from pydantic import ValidationError
7
+
8
+ from ..types.clob_types import ApiCreds
9
+ from ..types.websockets_types import (
10
+ CommentEvent,
11
+ LiveDataOrderMatchEvent,
12
+ LiveDataTradeEvent,
13
+ OrderBookSummaryEvent,
14
+ OrderEvent,
15
+ PriceChangeEvent,
16
+ QuoteEvent,
17
+ ReactionEvent,
18
+ RequestEvent,
19
+ TickSizeChangeEvent,
20
+ TradeEvent,
21
+ )
22
+
23
+
24
+ def _process_market_event(event):
25
+ try:
26
+ event = event.json
27
+ for message in event:
28
+ match message["event_type"]:
29
+ case "book":
30
+ print(OrderBookSummaryEvent(**message), "\n")
31
+ case "price_change":
32
+ print(PriceChangeEvent(**message), "\n")
33
+ case "tick_size_change":
34
+ print(TickSizeChangeEvent(**message), "\n")
35
+ except ValidationError as e:
36
+ print(event.text)
37
+ print(e.errors(), "\n")
38
+
39
+ def _process_user_event(event):
40
+ try:
41
+ event = event.json
42
+ for message in event:
43
+ match message["event_type"]:
44
+ case "order":
45
+ print(OrderEvent(**message), "\n")
46
+ case "trade":
47
+ print(TradeEvent(**message), "\n")
48
+ except ValidationError as e:
49
+ print(event.text)
50
+ print(e.errors(), "\n")
51
+
52
+ def _process_live_data_event(event):
53
+ try:
54
+ message = event.json
55
+ match message["type"]:
56
+ case "trades":
57
+ print(LiveDataTradeEvent(**message), "\n")
58
+ case "orders_matched":
59
+ print(LiveDataOrderMatchEvent(**message), "\n")
60
+ case "comment_created" | "comment_removed":
61
+ print(CommentEvent(**message), "\n")
62
+ case "reaction_created" | "reaction_removed":
63
+ print(ReactionEvent(**message), "\n")
64
+ case "request_created" | "request_edited" | "request_canceled" | "request_expired":
65
+ print(RequestEvent(**message), "\n")
66
+ case "quote_created" | "quote_edited" | "quote_canceled" | "quote_expired":
67
+ print(QuoteEvent(**message), "\n")
68
+ except ValidationError as e:
69
+ print(event.text)
70
+ print(e.errors(), "\n")
71
+
72
+ class PolymarketWebsocketsClient:
73
+ def __init__(self):
74
+ self.url_market = "wss://ws-subscriptions-clob.polymarket.com/ws/market"
75
+ self.url_user = "wss://ws-subscriptions-clob.polymarket.com/ws/user"
76
+ self.url_live_data = "wss://ws-live-data.polymarket.com"
77
+
78
+ def market_socket(self, token_ids: list[str], process_event: Callable = _process_market_event):
79
+ """
80
+ Connect to the market websocket and subscribe to market events for specific token IDs.
81
+
82
+ Args:
83
+ token_ids: List of token IDs to subscribe to
84
+ process_event: Callback function to process received events
85
+
86
+ """
87
+ websocket = WebSocket(self.url_market)
88
+ for event in persist(websocket): # persist automatically reconnects
89
+ if event.name == "ready":
90
+ websocket.send_json(
91
+ assets_ids=token_ids,
92
+ )
93
+ elif event.name == "text":
94
+ process_event(event)
95
+
96
+ def user_socket(self, creds: ApiCreds, process_event: Callable = _process_user_event):
97
+ """
98
+ Connect to the user websocket and subscribe to user events.
99
+
100
+ Args:
101
+ creds: API credentials for authentication
102
+ process_event: Callback function to process received events
103
+
104
+ """
105
+ websocket = WebSocket(self.url_user)
106
+ for event in persist(websocket):
107
+ if event.name == "ready":
108
+ websocket.send_json(
109
+ auth = creds.model_dump(by_alias=True),
110
+ )
111
+ elif event.name == "text":
112
+ process_event(event)
113
+
114
+ def live_data_socket(self, subscriptions: list[dict[str, Any]], process_event: Callable = _process_live_data_event):
115
+ """
116
+ Connect to the live data websocket and subscribe to specified events.
117
+
118
+ Args:
119
+ subscriptions: List of subscription configurations
120
+ process_event: Callback function to process received events
121
+
122
+ """
123
+ websocket = WebSocket(self.url_live_data)
124
+ for event in persist(websocket):
125
+ if event.name == "ready":
126
+ websocket.send_json(
127
+ action="subscribe",
128
+ subscriptions=subscriptions,
129
+ )
130
+ elif event.name == "text":
131
+ process_event(event)
File without changes