polymarket-apis 0.3.0__py3-none-any.whl → 0.3.9__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.
- polymarket_apis/__init__.py +42 -0
- polymarket_apis/clients/__init__.py +23 -0
- polymarket_apis/clients/clob_client.py +224 -117
- polymarket_apis/clients/data_client.py +220 -67
- polymarket_apis/clients/gamma_client.py +589 -101
- polymarket_apis/clients/graphql_client.py +28 -11
- polymarket_apis/clients/web3_client.py +538 -131
- polymarket_apis/clients/websockets_client.py +24 -7
- polymarket_apis/types/__init__.py +167 -0
- polymarket_apis/types/clob_types.py +35 -14
- polymarket_apis/types/common.py +105 -35
- polymarket_apis/types/data_types.py +48 -3
- polymarket_apis/types/gamma_types.py +529 -257
- polymarket_apis/types/web3_types.py +45 -0
- polymarket_apis/types/websockets_types.py +92 -41
- polymarket_apis/utilities/config.py +1 -0
- polymarket_apis/utilities/constants.py +5 -4
- polymarket_apis/utilities/exceptions.py +9 -0
- polymarket_apis/utilities/order_builder/builder.py +38 -22
- polymarket_apis/utilities/order_builder/helpers.py +0 -1
- polymarket_apis/utilities/signing/hmac.py +5 -1
- polymarket_apis/utilities/signing/signer.py +2 -2
- polymarket_apis/utilities/web3/abis/Safe.json +1138 -0
- polymarket_apis/utilities/web3/abis/SafeProxyFactory.json +224 -0
- polymarket_apis/utilities/web3/abis/custom_contract_errors.py +1 -1
- polymarket_apis/utilities/web3/helpers.py +235 -0
- {polymarket_apis-0.3.0.dist-info → polymarket_apis-0.3.9.dist-info}/METADATA +48 -8
- polymarket_apis-0.3.9.dist-info/RECORD +44 -0
- polymarket_apis/utilities/schemas/activity-subgraph.graphql +0 -86
- polymarket_apis/utilities/schemas/open-interest.graphql +0 -30
- polymarket_apis-0.3.0.dist-info/RECORD +0 -43
- {polymarket_apis-0.3.0.dist-info → polymarket_apis-0.3.9.dist-info}/WHEEL +0 -0
|
@@ -61,6 +61,7 @@ def _process_market_event(event):
|
|
|
61
61
|
print(e.errors())
|
|
62
62
|
print(event.json)
|
|
63
63
|
|
|
64
|
+
|
|
64
65
|
def _process_user_event(event):
|
|
65
66
|
try:
|
|
66
67
|
message = event.json
|
|
@@ -75,6 +76,7 @@ def _process_user_event(event):
|
|
|
75
76
|
print(event.text)
|
|
76
77
|
print(e.errors(), "\n")
|
|
77
78
|
|
|
79
|
+
|
|
78
80
|
def _process_live_data_event(event):
|
|
79
81
|
try:
|
|
80
82
|
message = event.json
|
|
@@ -87,7 +89,12 @@ def _process_live_data_event(event):
|
|
|
87
89
|
print(CommentEvent(**message), "\n")
|
|
88
90
|
case "reaction_created" | "reaction_removed":
|
|
89
91
|
print(ReactionEvent(**message), "\n")
|
|
90
|
-
case
|
|
92
|
+
case (
|
|
93
|
+
"request_created"
|
|
94
|
+
| "request_edited"
|
|
95
|
+
| "request_canceled"
|
|
96
|
+
| "request_expired"
|
|
97
|
+
):
|
|
91
98
|
print(RequestEvent(**message), "\n")
|
|
92
99
|
case "quote_created" | "quote_edited" | "quote_canceled" | "quote_expired":
|
|
93
100
|
print(QuoteEvent(**message), "\n")
|
|
@@ -117,13 +124,16 @@ def _process_live_data_event(event):
|
|
|
117
124
|
print(e.errors(), "\n")
|
|
118
125
|
print(event.text)
|
|
119
126
|
|
|
127
|
+
|
|
120
128
|
class PolymarketWebsocketsClient:
|
|
121
129
|
def __init__(self):
|
|
122
130
|
self.url_market = "wss://ws-subscriptions-clob.polymarket.com/ws/market"
|
|
123
131
|
self.url_user = "wss://ws-subscriptions-clob.polymarket.com/ws/user"
|
|
124
132
|
self.url_live_data = "wss://ws-live-data.polymarket.com"
|
|
125
133
|
|
|
126
|
-
def market_socket(
|
|
134
|
+
def market_socket(
|
|
135
|
+
self, token_ids: list[str], process_event: Callable = _process_market_event
|
|
136
|
+
):
|
|
127
137
|
"""
|
|
128
138
|
Connect to the market websocket and subscribe to market events for specific token IDs.
|
|
129
139
|
|
|
@@ -142,7 +152,9 @@ class PolymarketWebsocketsClient:
|
|
|
142
152
|
elif event.name == "text":
|
|
143
153
|
process_event(event)
|
|
144
154
|
|
|
145
|
-
def user_socket(
|
|
155
|
+
def user_socket(
|
|
156
|
+
self, creds: ApiCreds, process_event: Callable = _process_user_event
|
|
157
|
+
):
|
|
146
158
|
"""
|
|
147
159
|
Connect to the user websocket and subscribe to user events.
|
|
148
160
|
|
|
@@ -161,7 +173,12 @@ class PolymarketWebsocketsClient:
|
|
|
161
173
|
elif event.name == "text":
|
|
162
174
|
process_event(event)
|
|
163
175
|
|
|
164
|
-
def live_data_socket(
|
|
176
|
+
def live_data_socket(
|
|
177
|
+
self,
|
|
178
|
+
subscriptions: list[dict[str, Any]],
|
|
179
|
+
process_event: Callable = _process_live_data_event,
|
|
180
|
+
creds: Optional[ApiCreds] = None,
|
|
181
|
+
):
|
|
165
182
|
# info on how to subscribe found at https://github.com/Polymarket/real-time-data-client?tab=readme-ov-file#subscribe
|
|
166
183
|
"""
|
|
167
184
|
Connect to the live data websocket and subscribe to specified events.
|
|
@@ -175,13 +192,13 @@ class PolymarketWebsocketsClient:
|
|
|
175
192
|
websocket = WebSocket(self.url_live_data)
|
|
176
193
|
|
|
177
194
|
needs_auth = any(sub.get("topic") == "clob_user" for sub in subscriptions)
|
|
178
|
-
if needs_auth and creds is None:
|
|
179
|
-
msg = "ApiCreds credentials are required for the clob_user topic subscriptions"
|
|
180
|
-
raise AuthenticationRequiredError(msg)
|
|
181
195
|
|
|
182
196
|
for event in persist(websocket):
|
|
183
197
|
if event.name == "ready":
|
|
184
198
|
if needs_auth:
|
|
199
|
+
if creds is None:
|
|
200
|
+
msg = "ApiCreds credentials are required for the clob_user topic subscriptions"
|
|
201
|
+
raise AuthenticationRequiredError(msg)
|
|
185
202
|
subscriptions_with_creds = []
|
|
186
203
|
for sub in subscriptions:
|
|
187
204
|
if sub.get("topic") == "clob_user":
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# python
|
|
2
|
+
"""
|
|
3
|
+
Type definitions for Polymarket APIs.
|
|
4
|
+
|
|
5
|
+
This module contains all the Pydantic models and type definitions used across
|
|
6
|
+
the Polymarket APIs.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from .clob_types import (
|
|
10
|
+
ApiCreds,
|
|
11
|
+
AssetType,
|
|
12
|
+
BidAsk,
|
|
13
|
+
BookParams,
|
|
14
|
+
ClobMarket,
|
|
15
|
+
ContractConfig,
|
|
16
|
+
CreateOrderOptions,
|
|
17
|
+
DailyEarnedReward,
|
|
18
|
+
MarketOrderArgs,
|
|
19
|
+
MarketRewards,
|
|
20
|
+
Midpoint,
|
|
21
|
+
OpenOrder,
|
|
22
|
+
OrderArgs,
|
|
23
|
+
OrderBookSummary,
|
|
24
|
+
OrderCancelResponse,
|
|
25
|
+
OrderPostResponse,
|
|
26
|
+
OrderType,
|
|
27
|
+
PaginatedResponse,
|
|
28
|
+
PartialCreateOrderOptions,
|
|
29
|
+
PolygonTrade,
|
|
30
|
+
PostOrdersArgs,
|
|
31
|
+
Price,
|
|
32
|
+
PriceHistory,
|
|
33
|
+
RewardMarket,
|
|
34
|
+
Spread,
|
|
35
|
+
TickSize,
|
|
36
|
+
Token,
|
|
37
|
+
TokenBidAsk,
|
|
38
|
+
TokenBidAskDict,
|
|
39
|
+
TokenValue,
|
|
40
|
+
TokenValueDict,
|
|
41
|
+
)
|
|
42
|
+
from .common import (
|
|
43
|
+
EmptyString,
|
|
44
|
+
EthAddress,
|
|
45
|
+
FlexibleDatetime,
|
|
46
|
+
Keccak256,
|
|
47
|
+
TimeseriesPoint,
|
|
48
|
+
)
|
|
49
|
+
from .data_types import (
|
|
50
|
+
Activity,
|
|
51
|
+
Holder,
|
|
52
|
+
HolderResponse,
|
|
53
|
+
Position,
|
|
54
|
+
Trade,
|
|
55
|
+
User,
|
|
56
|
+
UserMetric,
|
|
57
|
+
UserRank,
|
|
58
|
+
ValueResponse,
|
|
59
|
+
)
|
|
60
|
+
from .gamma_types import (
|
|
61
|
+
ClobReward,
|
|
62
|
+
Event,
|
|
63
|
+
GammaMarket,
|
|
64
|
+
Pagination,
|
|
65
|
+
Series,
|
|
66
|
+
Tag,
|
|
67
|
+
)
|
|
68
|
+
from .websockets_types import (
|
|
69
|
+
ActivityOrderMatchEvent,
|
|
70
|
+
ActivityTradeEvent,
|
|
71
|
+
CommentEvent,
|
|
72
|
+
CryptoPriceSubscribeEvent,
|
|
73
|
+
CryptoPriceUpdateEvent,
|
|
74
|
+
ErrorEvent,
|
|
75
|
+
LastTradePriceEvent,
|
|
76
|
+
LiveDataLastTradePriceEvent,
|
|
77
|
+
LiveDataOrderBookSummaryEvent,
|
|
78
|
+
LiveDataOrderEvent,
|
|
79
|
+
LiveDataPriceChangeEvent,
|
|
80
|
+
LiveDataTickSizeChangeEvent,
|
|
81
|
+
LiveDataTradeEvent,
|
|
82
|
+
MarketStatusChangeEvent,
|
|
83
|
+
OrderBookSummaryEvent,
|
|
84
|
+
OrderEvent,
|
|
85
|
+
PriceChangeEvent,
|
|
86
|
+
QuoteEvent,
|
|
87
|
+
ReactionEvent,
|
|
88
|
+
RequestEvent,
|
|
89
|
+
TickSizeChangeEvent,
|
|
90
|
+
TradeEvent,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
__all__ = [
|
|
94
|
+
"Activity",
|
|
95
|
+
"ActivityOrderMatchEvent",
|
|
96
|
+
"ActivityTradeEvent",
|
|
97
|
+
"ApiCreds",
|
|
98
|
+
"AssetType",
|
|
99
|
+
"BidAsk",
|
|
100
|
+
"BookParams",
|
|
101
|
+
"ClobMarket",
|
|
102
|
+
"ClobReward",
|
|
103
|
+
"CommentEvent",
|
|
104
|
+
"ContractConfig",
|
|
105
|
+
"CreateOrderOptions",
|
|
106
|
+
"CryptoPriceSubscribeEvent",
|
|
107
|
+
"CryptoPriceUpdateEvent",
|
|
108
|
+
"DailyEarnedReward",
|
|
109
|
+
"EmptyString",
|
|
110
|
+
"ErrorEvent",
|
|
111
|
+
"EthAddress",
|
|
112
|
+
"Event",
|
|
113
|
+
"FlexibleDatetime",
|
|
114
|
+
"GammaMarket",
|
|
115
|
+
"Holder",
|
|
116
|
+
"HolderResponse",
|
|
117
|
+
"Keccak256",
|
|
118
|
+
"LastTradePriceEvent",
|
|
119
|
+
"LiveDataLastTradePriceEvent",
|
|
120
|
+
"LiveDataOrderBookSummaryEvent",
|
|
121
|
+
"LiveDataOrderEvent",
|
|
122
|
+
"LiveDataPriceChangeEvent",
|
|
123
|
+
"LiveDataTickSizeChangeEvent",
|
|
124
|
+
"LiveDataTradeEvent",
|
|
125
|
+
"MarketOrderArgs",
|
|
126
|
+
"MarketRewards",
|
|
127
|
+
"MarketStatusChangeEvent",
|
|
128
|
+
"Midpoint",
|
|
129
|
+
"OpenOrder",
|
|
130
|
+
"OrderArgs",
|
|
131
|
+
"OrderBookSummary",
|
|
132
|
+
"OrderBookSummaryEvent",
|
|
133
|
+
"OrderCancelResponse",
|
|
134
|
+
"OrderEvent",
|
|
135
|
+
"OrderPostResponse",
|
|
136
|
+
"OrderType",
|
|
137
|
+
"PaginatedResponse",
|
|
138
|
+
"Pagination",
|
|
139
|
+
"PartialCreateOrderOptions",
|
|
140
|
+
"PolygonTrade",
|
|
141
|
+
"Position",
|
|
142
|
+
"PostOrdersArgs",
|
|
143
|
+
"Price",
|
|
144
|
+
"PriceChangeEvent",
|
|
145
|
+
"PriceHistory",
|
|
146
|
+
"QuoteEvent",
|
|
147
|
+
"ReactionEvent",
|
|
148
|
+
"RequestEvent",
|
|
149
|
+
"RewardMarket",
|
|
150
|
+
"Series",
|
|
151
|
+
"Spread",
|
|
152
|
+
"Tag",
|
|
153
|
+
"TickSize",
|
|
154
|
+
"TickSizeChangeEvent",
|
|
155
|
+
"TimeseriesPoint",
|
|
156
|
+
"Token",
|
|
157
|
+
"TokenBidAsk",
|
|
158
|
+
"TokenBidAskDict",
|
|
159
|
+
"TokenValue",
|
|
160
|
+
"TokenValueDict",
|
|
161
|
+
"Trade",
|
|
162
|
+
"TradeEvent",
|
|
163
|
+
"User",
|
|
164
|
+
"UserMetric",
|
|
165
|
+
"UserRank",
|
|
166
|
+
"ValueResponse",
|
|
167
|
+
]
|
|
@@ -17,10 +17,11 @@ from pydantic import (
|
|
|
17
17
|
)
|
|
18
18
|
|
|
19
19
|
from ..types.common import EthAddress, Keccak256, TimeseriesPoint
|
|
20
|
-
from ..utilities.constants import
|
|
20
|
+
from ..utilities.constants import ADDRESS_ZERO
|
|
21
21
|
|
|
22
22
|
logger = logging.getLogger(__name__)
|
|
23
23
|
|
|
24
|
+
|
|
24
25
|
class ApiCreds(BaseModel):
|
|
25
26
|
key: str = Field(alias="apiKey")
|
|
26
27
|
secret: str
|
|
@@ -122,11 +123,13 @@ class Rewards(BaseModel):
|
|
|
122
123
|
rewards_min_size: int = Field(alias="min_size")
|
|
123
124
|
rewards_max_spread: float = Field(alias="max_spread")
|
|
124
125
|
|
|
126
|
+
|
|
125
127
|
class EarnedReward(BaseModel):
|
|
126
128
|
asset_address: EthAddress
|
|
127
129
|
earnings: float
|
|
128
130
|
asset_rate: float
|
|
129
131
|
|
|
132
|
+
|
|
130
133
|
class DailyEarnedReward(BaseModel):
|
|
131
134
|
date: datetime
|
|
132
135
|
asset_address: EthAddress
|
|
@@ -134,12 +137,12 @@ class DailyEarnedReward(BaseModel):
|
|
|
134
137
|
earnings: float
|
|
135
138
|
asset_rate: float
|
|
136
139
|
|
|
137
|
-
|
|
140
|
+
|
|
141
|
+
class RewardMarket(BaseModel):
|
|
138
142
|
market_id: str
|
|
139
143
|
condition_id: Keccak256
|
|
140
144
|
question: str
|
|
141
145
|
market_slug: str
|
|
142
|
-
market_description: str
|
|
143
146
|
event_slug: str
|
|
144
147
|
image: str
|
|
145
148
|
maker_address: EthAddress
|
|
@@ -165,6 +168,7 @@ class MarketRewards(BaseModel):
|
|
|
165
168
|
rewards_min_size: int
|
|
166
169
|
market_competitiveness: float
|
|
167
170
|
|
|
171
|
+
|
|
168
172
|
class ClobMarket(BaseModel):
|
|
169
173
|
# Core market information
|
|
170
174
|
token_ids: list[Token] = Field(alias="tokens")
|
|
@@ -215,7 +219,9 @@ class ClobMarket(BaseModel):
|
|
|
215
219
|
|
|
216
220
|
@field_validator("neg_risk_market_id", "neg_risk_request_id", mode="wrap")
|
|
217
221
|
@classmethod
|
|
218
|
-
def validate_neg_risk_fields(
|
|
222
|
+
def validate_neg_risk_fields(
|
|
223
|
+
cls, value: str, handler: ValidatorFunctionWrapHandler, info: ValidationInfo
|
|
224
|
+
) -> str | None:
|
|
219
225
|
try:
|
|
220
226
|
return handler(value)
|
|
221
227
|
except ValidationError as e:
|
|
@@ -227,12 +233,18 @@ class ClobMarket(BaseModel):
|
|
|
227
233
|
return value
|
|
228
234
|
if neg_risk and value == "":
|
|
229
235
|
for _ in e.errors():
|
|
230
|
-
msg = (
|
|
231
|
-
|
|
236
|
+
msg = (
|
|
237
|
+
"Poorly setup market: negative risk is True, but either neg_risk_market_id or neg_risk_request_id is missing. "
|
|
238
|
+
f" Question: {info.data.get('question')}; Market slug: {info.data.get('market_slug')} \n"
|
|
239
|
+
)
|
|
232
240
|
logger.warning(msg)
|
|
241
|
+
return None
|
|
242
|
+
|
|
233
243
|
@field_validator("condition_id", "question_id", mode="wrap")
|
|
234
244
|
@classmethod
|
|
235
|
-
def validate_condition_fields(
|
|
245
|
+
def validate_condition_fields(
|
|
246
|
+
cls, value: str, handler: ValidatorFunctionWrapHandler, info: ValidationInfo
|
|
247
|
+
) -> str:
|
|
236
248
|
try:
|
|
237
249
|
return handler(value)
|
|
238
250
|
except ValueError:
|
|
@@ -241,6 +253,7 @@ class ClobMarket(BaseModel):
|
|
|
241
253
|
return value
|
|
242
254
|
raise
|
|
243
255
|
|
|
256
|
+
|
|
244
257
|
class OpenOrder(BaseModel):
|
|
245
258
|
order_id: Keccak256 = Field(alias="id")
|
|
246
259
|
status: str
|
|
@@ -258,6 +271,7 @@ class OpenOrder(BaseModel):
|
|
|
258
271
|
associate_trades: list[str]
|
|
259
272
|
created_at: datetime
|
|
260
273
|
|
|
274
|
+
|
|
261
275
|
class MakerOrder(BaseModel):
|
|
262
276
|
token_id: str = Field(alias="asset_id")
|
|
263
277
|
order_id: Keccak256
|
|
@@ -268,6 +282,7 @@ class MakerOrder(BaseModel):
|
|
|
268
282
|
outcome: str
|
|
269
283
|
fee_rate_bps: float
|
|
270
284
|
|
|
285
|
+
|
|
271
286
|
class PolygonTrade(BaseModel):
|
|
272
287
|
trade_id: str = Field(alias="id")
|
|
273
288
|
taker_order_id: Keccak256
|
|
@@ -277,7 +292,7 @@ class PolygonTrade(BaseModel):
|
|
|
277
292
|
size: float
|
|
278
293
|
fee_rate_bps: float
|
|
279
294
|
price: float
|
|
280
|
-
status: str
|
|
295
|
+
status: str # change to literals MINED, CONFIRMED
|
|
281
296
|
match_time: datetime
|
|
282
297
|
last_update: datetime
|
|
283
298
|
outcome: str
|
|
@@ -288,6 +303,7 @@ class PolygonTrade(BaseModel):
|
|
|
288
303
|
maker_orders: list[MakerOrder]
|
|
289
304
|
trader_side: Literal["TAKER", "MAKER"]
|
|
290
305
|
|
|
306
|
+
|
|
291
307
|
class TradeParams(BaseModel):
|
|
292
308
|
id: Optional[str] = None
|
|
293
309
|
maker_address: Optional[str] = None
|
|
@@ -304,12 +320,13 @@ class OpenOrderParams(BaseModel):
|
|
|
304
320
|
|
|
305
321
|
|
|
306
322
|
class DropNotificationParams(BaseModel):
|
|
307
|
-
ids: Optional[list[str]]= None
|
|
323
|
+
ids: Optional[list[str]] = None
|
|
308
324
|
|
|
309
325
|
|
|
310
326
|
class OrderSummary(BaseModel):
|
|
311
|
-
price:
|
|
312
|
-
size:
|
|
327
|
+
price: float
|
|
328
|
+
size: float
|
|
329
|
+
|
|
313
330
|
|
|
314
331
|
class PriceLevel(OrderSummary):
|
|
315
332
|
side: Literal["BUY", "SELL"]
|
|
@@ -375,6 +392,7 @@ class RoundConfig(BaseModel):
|
|
|
375
392
|
size: int
|
|
376
393
|
amount: int
|
|
377
394
|
|
|
395
|
+
|
|
378
396
|
class OrderArgs(BaseModel):
|
|
379
397
|
token_id: str
|
|
380
398
|
"""
|
|
@@ -411,7 +429,7 @@ class OrderArgs(BaseModel):
|
|
|
411
429
|
Timestamp after which the order is expired.
|
|
412
430
|
"""
|
|
413
431
|
|
|
414
|
-
taker: str =
|
|
432
|
+
taker: str = ADDRESS_ZERO
|
|
415
433
|
"""
|
|
416
434
|
Address of the order taker. The zero address is used to indicate a public order.
|
|
417
435
|
"""
|
|
@@ -449,19 +467,21 @@ class MarketOrderArgs(BaseModel):
|
|
|
449
467
|
Nonce used for onchain cancellations.
|
|
450
468
|
"""
|
|
451
469
|
|
|
452
|
-
taker: str =
|
|
470
|
+
taker: str = ADDRESS_ZERO
|
|
453
471
|
"""
|
|
454
472
|
Address of the order taker. The zero address is used to indicate a public order.
|
|
455
473
|
"""
|
|
456
474
|
|
|
457
475
|
order_type: OrderType = OrderType.FOK
|
|
458
476
|
|
|
477
|
+
|
|
459
478
|
class PostOrdersArgs(BaseModel):
|
|
460
479
|
order: SignedOrder
|
|
461
480
|
order_type: OrderType = OrderType.GTC
|
|
462
481
|
|
|
463
482
|
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
464
483
|
|
|
484
|
+
|
|
465
485
|
class ContractConfig(BaseModel):
|
|
466
486
|
"""Contract Configuration."""
|
|
467
487
|
|
|
@@ -486,9 +506,10 @@ class OrderPostResponse(BaseModel):
|
|
|
486
506
|
order_id: Union[Keccak256, Literal[""]] = Field(alias="orderID")
|
|
487
507
|
taking_amount: str = Field(alias="takingAmount")
|
|
488
508
|
making_amount: str = Field(alias="makingAmount")
|
|
489
|
-
status:
|
|
509
|
+
status: Literal["live", "matched", "delayed"]
|
|
490
510
|
success: bool
|
|
491
511
|
|
|
512
|
+
|
|
492
513
|
class OrderCancelResponse(BaseModel):
|
|
493
514
|
not_canceled: Optional[dict[Keccak256, str]]
|
|
494
515
|
canceled: Optional[list[Keccak256]]
|
polymarket_apis/types/common.py
CHANGED
|
@@ -1,51 +1,121 @@
|
|
|
1
1
|
import re
|
|
2
|
-
from datetime import datetime
|
|
3
|
-
from typing import Annotated
|
|
2
|
+
from datetime import UTC, datetime
|
|
3
|
+
from typing import Annotated, Any
|
|
4
4
|
|
|
5
|
-
from
|
|
5
|
+
from dateutil import parser
|
|
6
|
+
from hexbytes import HexBytes
|
|
7
|
+
from pydantic import AfterValidator, BaseModel, BeforeValidator, ConfigDict, Field
|
|
6
8
|
|
|
7
9
|
|
|
8
|
-
def
|
|
10
|
+
def parse_flexible_datetime(v: str | datetime) -> datetime:
|
|
11
|
+
"""Parse datetime from multiple formats using dateutil."""
|
|
12
|
+
if v in {"NOW*()", "NOW()"}:
|
|
13
|
+
return datetime.fromtimestamp(0, tz=UTC)
|
|
14
|
+
|
|
15
|
+
if isinstance(v, str):
|
|
16
|
+
parsed = parser.parse(v)
|
|
17
|
+
if not isinstance(parsed, datetime):
|
|
18
|
+
msg = f"Failed to parse '{v}' as datetime, got {type(parsed)}"
|
|
19
|
+
raise TypeError(msg)
|
|
20
|
+
return parsed
|
|
21
|
+
return v
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def validate_keccak256(v: str | HexBytes | bytes) -> str:
|
|
25
|
+
"""Validate and normalize Keccak256 hash format."""
|
|
26
|
+
# Convert HexBytes/bytes to string
|
|
27
|
+
if isinstance(v, HexBytes | bytes):
|
|
28
|
+
v = v.hex()
|
|
29
|
+
|
|
30
|
+
# Ensure string and add 0x prefix if missing
|
|
31
|
+
if not isinstance(v, str):
|
|
32
|
+
msg = f"Expected string or bytes, got {type(v)}"
|
|
33
|
+
raise TypeError(msg)
|
|
34
|
+
|
|
35
|
+
if not v.startswith("0x"):
|
|
36
|
+
v = "0x" + v
|
|
37
|
+
|
|
38
|
+
# Validate format: 0x followed by 64 hex characters
|
|
9
39
|
if not re.match(r"^0x[a-fA-F0-9]{64}$", v):
|
|
10
|
-
msg = "Invalid Keccak256 hash format"
|
|
40
|
+
msg = f"Invalid Keccak256 hash format: {v}"
|
|
11
41
|
raise ValueError(msg)
|
|
42
|
+
|
|
12
43
|
return v
|
|
13
44
|
|
|
14
|
-
|
|
15
|
-
|
|
45
|
+
|
|
46
|
+
def validate_eth_address(v: str | HexBytes | bytes) -> str:
|
|
47
|
+
"""Validate and normalize Ethereum address format."""
|
|
48
|
+
# Convert HexBytes/bytes to string
|
|
49
|
+
if isinstance(v, HexBytes | bytes):
|
|
50
|
+
v = v.hex()
|
|
51
|
+
|
|
52
|
+
# Ensure string and add 0x prefix if missing
|
|
53
|
+
if not isinstance(v, str):
|
|
54
|
+
msg = f"Expected string or bytes, got {type(v)}"
|
|
55
|
+
raise TypeError(msg)
|
|
56
|
+
|
|
57
|
+
if not v.startswith("0x"):
|
|
58
|
+
v = "0x" + v
|
|
59
|
+
|
|
60
|
+
# Validate format: 0x followed by 40 hex characters
|
|
61
|
+
if not re.match(r"^0x[a-fA-F0-9]{40}$", v, re.IGNORECASE):
|
|
62
|
+
msg = f"Invalid Ethereum address format: {v}"
|
|
63
|
+
raise ValueError(msg)
|
|
64
|
+
|
|
65
|
+
return v
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def hexbytes_to_str(v: Any) -> str:
|
|
69
|
+
"""Convert HexBytes to hex string with 0x prefix."""
|
|
70
|
+
if isinstance(v, HexBytes):
|
|
71
|
+
hex_str = v.hex()
|
|
72
|
+
return hex_str if hex_str.startswith("0x") else f"0x{hex_str}"
|
|
73
|
+
if isinstance(v, bytes):
|
|
74
|
+
return "0x" + v.hex()
|
|
75
|
+
if isinstance(v, str) and not v.startswith("0x"):
|
|
76
|
+
return f"0x{v}"
|
|
77
|
+
return v
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def validate_keccak_or_padded(v: Any) -> str:
|
|
81
|
+
"""
|
|
82
|
+
Validate Keccak256 or accept padded addresses (32 bytes with leading zeros).
|
|
83
|
+
|
|
84
|
+
Some log topics are padded addresses, not proper Keccak256 hashes.
|
|
85
|
+
"""
|
|
86
|
+
# First convert HexBytes/bytes to string with 0x prefix
|
|
87
|
+
if isinstance(v, HexBytes | bytes):
|
|
88
|
+
v = v.hex()
|
|
89
|
+
|
|
90
|
+
# Ensure it's a string
|
|
91
|
+
if not isinstance(v, str):
|
|
92
|
+
msg = f"Expected string or bytes, got {type(v)}"
|
|
93
|
+
raise TypeError(msg)
|
|
94
|
+
|
|
95
|
+
# Add 0x prefix if missing
|
|
96
|
+
if not v.startswith("0x"):
|
|
97
|
+
v = "0x" + v
|
|
98
|
+
|
|
99
|
+
# Accept 66 character hex strings (0x + 64 hex chars)
|
|
100
|
+
if len(v) == 66 and all(c in "0123456789abcdefABCDEF" for c in v[2:]):
|
|
16
101
|
return v
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
# Pad fractional seconds to 6 digits if present
|
|
22
|
-
if "." in v:
|
|
23
|
-
dot_pos = v.find(".")
|
|
24
|
-
tz_pos = v.find("+", dot_pos) # Find timezone start after '.'
|
|
25
|
-
if tz_pos == -1:
|
|
26
|
-
tz_pos = v.find("-", dot_pos)
|
|
27
|
-
|
|
28
|
-
if tz_pos != -1:
|
|
29
|
-
frac = v[dot_pos+1:tz_pos]
|
|
30
|
-
if len(frac) < 6:
|
|
31
|
-
frac = frac.ljust(6, "0")
|
|
32
|
-
v = f"{v[:dot_pos+1]}{frac}{v[tz_pos:]}"
|
|
33
|
-
|
|
34
|
-
# Try parsing with and without microseconds
|
|
35
|
-
for fmt in ("%Y-%m-%d %H:%M:%S.%f%z", "%Y-%m-%d %H:%M:%S%z"):
|
|
36
|
-
try:
|
|
37
|
-
return datetime.strptime(v, fmt) # noqa: DTZ007
|
|
38
|
-
except ValueError:
|
|
39
|
-
continue
|
|
40
|
-
msg = f"Time data '{v}' does not match expected formats."
|
|
102
|
+
|
|
103
|
+
msg = (
|
|
104
|
+
f"Invalid hash format: expected 66 characters (0x + 64 hex), got {len(v)}: {v}"
|
|
105
|
+
)
|
|
41
106
|
raise ValueError(msg)
|
|
42
107
|
|
|
43
108
|
|
|
44
|
-
|
|
45
|
-
EthAddress = Annotated[str,
|
|
109
|
+
FlexibleDatetime = Annotated[datetime, BeforeValidator(parse_flexible_datetime)]
|
|
110
|
+
EthAddress = Annotated[str, AfterValidator(validate_eth_address)]
|
|
46
111
|
Keccak256 = Annotated[str, AfterValidator(validate_keccak256)]
|
|
112
|
+
HexString = Annotated[str, BeforeValidator(hexbytes_to_str)]
|
|
113
|
+
Keccak256OrPadded = Annotated[str, BeforeValidator(validate_keccak_or_padded)]
|
|
47
114
|
EmptyString = Annotated[str, Field(pattern=r"^$", description="An empty string")]
|
|
48
115
|
|
|
116
|
+
|
|
49
117
|
class TimeseriesPoint(BaseModel):
|
|
50
|
-
|
|
51
|
-
|
|
118
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
119
|
+
|
|
120
|
+
value: float = Field(alias="p")
|
|
121
|
+
timestamp: datetime = Field(alias="t")
|
|
@@ -1,11 +1,43 @@
|
|
|
1
1
|
from datetime import UTC, datetime
|
|
2
|
-
from typing import Literal
|
|
2
|
+
from typing import Literal, Optional
|
|
3
3
|
|
|
4
|
-
from pydantic import BaseModel, Field, field_validator
|
|
4
|
+
from pydantic import BaseModel, Field, field_validator, model_validator
|
|
5
5
|
|
|
6
6
|
from .common import EmptyString, EthAddress, Keccak256
|
|
7
7
|
|
|
8
8
|
|
|
9
|
+
class GQLPosition(BaseModel):
|
|
10
|
+
user: EthAddress
|
|
11
|
+
token_id: str
|
|
12
|
+
complementary_token_id: str
|
|
13
|
+
condition_id: Keccak256
|
|
14
|
+
outcome_index: int
|
|
15
|
+
balance: float
|
|
16
|
+
|
|
17
|
+
@model_validator(mode="before")
|
|
18
|
+
def _flatten(cls, values):
|
|
19
|
+
asset = values.get("asset")
|
|
20
|
+
if isinstance(asset, dict):
|
|
21
|
+
if "id" in asset:
|
|
22
|
+
values.setdefault("token_id", asset["id"])
|
|
23
|
+
if "complement" in asset:
|
|
24
|
+
values.setdefault("complementary_token_id", asset["complement"])
|
|
25
|
+
condition = asset.get("condition")
|
|
26
|
+
if isinstance(condition, dict) and "id" in condition:
|
|
27
|
+
values.setdefault("condition_id", condition["id"])
|
|
28
|
+
if "outcomeIndex" in asset:
|
|
29
|
+
values.setdefault("outcome_index", asset["outcomeIndex"])
|
|
30
|
+
values.pop("asset", None)
|
|
31
|
+
return values
|
|
32
|
+
|
|
33
|
+
@field_validator("balance", mode="before")
|
|
34
|
+
@classmethod
|
|
35
|
+
def _parse_balance(cls, value):
|
|
36
|
+
if isinstance(value, str):
|
|
37
|
+
value = int(value)
|
|
38
|
+
return value / 10**6
|
|
39
|
+
|
|
40
|
+
|
|
9
41
|
class Position(BaseModel):
|
|
10
42
|
# User identification
|
|
11
43
|
proxy_wallet: EthAddress = Field(alias="proxyWallet")
|
|
@@ -44,7 +76,7 @@ class Position(BaseModel):
|
|
|
44
76
|
@field_validator("end_date", mode="before")
|
|
45
77
|
def handle_empty_end_date(cls, v):
|
|
46
78
|
if v == "":
|
|
47
|
-
return datetime(2099,12,31, tzinfo=UTC)
|
|
79
|
+
return datetime(2099, 12, 31, tzinfo=UTC)
|
|
48
80
|
return v
|
|
49
81
|
|
|
50
82
|
|
|
@@ -145,6 +177,7 @@ class ValueResponse(BaseModel):
|
|
|
145
177
|
# Value information
|
|
146
178
|
value: float
|
|
147
179
|
|
|
180
|
+
|
|
148
181
|
class User(BaseModel):
|
|
149
182
|
proxy_wallet: EthAddress = Field(alias="proxyWallet")
|
|
150
183
|
name: str
|
|
@@ -152,10 +185,22 @@ class User(BaseModel):
|
|
|
152
185
|
profile_image: str = Field(alias="profileImage")
|
|
153
186
|
profile_image_optimized: str = Field(alias="profileImageOptimized")
|
|
154
187
|
|
|
188
|
+
|
|
155
189
|
class UserMetric(User):
|
|
156
190
|
amount: float
|
|
157
191
|
pseudonym: str
|
|
158
192
|
|
|
193
|
+
|
|
159
194
|
class UserRank(User):
|
|
160
195
|
amount: float
|
|
161
196
|
rank: int
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class MarketValue(BaseModel):
|
|
200
|
+
condition_id: Keccak256 = Field(alias="market")
|
|
201
|
+
value: float
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class EventLiveVolume(BaseModel):
|
|
205
|
+
total: Optional[float]
|
|
206
|
+
markets: Optional[list[MarketValue]]
|