polynode 0.6.3__tar.gz → 0.7.1__tar.gz

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.
Files changed (45) hide show
  1. {polynode-0.6.3 → polynode-0.7.1}/.gitignore +2 -0
  2. {polynode-0.6.3 → polynode-0.7.1}/PKG-INFO +1 -1
  3. {polynode-0.6.3 → polynode-0.7.1}/polynode/trading/__init__.py +1 -0
  4. {polynode-0.6.3 → polynode-0.7.1}/polynode/trading/clob_api.py +27 -5
  5. {polynode-0.6.3 → polynode-0.7.1}/polynode/trading/constants.py +17 -2
  6. {polynode-0.6.3 → polynode-0.7.1}/polynode/trading/cosigner.py +7 -5
  7. polynode-0.7.1/polynode/trading/eip712.py +330 -0
  8. polynode-0.7.1/polynode/trading/escrow.py +166 -0
  9. {polynode-0.6.3 → polynode-0.7.1}/polynode/trading/onboarding.py +139 -9
  10. {polynode-0.6.3 → polynode-0.7.1}/polynode/trading/signer.py +2 -0
  11. {polynode-0.6.3 → polynode-0.7.1}/polynode/trading/sqlite_backend.py +32 -5
  12. {polynode-0.6.3 → polynode-0.7.1}/polynode/trading/trader.py +276 -24
  13. {polynode-0.6.3 → polynode-0.7.1}/polynode/trading/types.py +21 -0
  14. {polynode-0.6.3 → polynode-0.7.1}/pyproject.toml +1 -1
  15. polynode-0.7.1/tests/test_trading.py +334 -0
  16. polynode-0.6.3/polynode/trading/eip712.py +0 -169
  17. polynode-0.6.3/tests/test_trading.py +0 -50
  18. {polynode-0.6.3 → polynode-0.7.1}/README.md +0 -0
  19. {polynode-0.6.3 → polynode-0.7.1}/polynode/__init__.py +0 -0
  20. {polynode-0.6.3 → polynode-0.7.1}/polynode/_version.py +0 -0
  21. {polynode-0.6.3 → polynode-0.7.1}/polynode/cache/__init__.py +0 -0
  22. {polynode-0.6.3 → polynode-0.7.1}/polynode/client.py +0 -0
  23. {polynode-0.6.3 → polynode-0.7.1}/polynode/engine.py +0 -0
  24. {polynode-0.6.3 → polynode-0.7.1}/polynode/errors.py +0 -0
  25. {polynode-0.6.3 → polynode-0.7.1}/polynode/orderbook.py +0 -0
  26. {polynode-0.6.3 → polynode-0.7.1}/polynode/orderbook_state.py +0 -0
  27. {polynode-0.6.3 → polynode-0.7.1}/polynode/redemption_watcher.py +0 -0
  28. {polynode-0.6.3 → polynode-0.7.1}/polynode/short_form.py +0 -0
  29. {polynode-0.6.3 → polynode-0.7.1}/polynode/subscription.py +0 -0
  30. {polynode-0.6.3 → polynode-0.7.1}/polynode/testing.py +0 -0
  31. {polynode-0.6.3 → polynode-0.7.1}/polynode/trading/position_management.py +0 -0
  32. {polynode-0.6.3 → polynode-0.7.1}/polynode/trading/privy.py +0 -0
  33. {polynode-0.6.3 → polynode-0.7.1}/polynode/types/__init__.py +0 -0
  34. {polynode-0.6.3 → polynode-0.7.1}/polynode/types/enums.py +0 -0
  35. {polynode-0.6.3 → polynode-0.7.1}/polynode/types/events.py +0 -0
  36. {polynode-0.6.3 → polynode-0.7.1}/polynode/types/orderbook.py +0 -0
  37. {polynode-0.6.3 → polynode-0.7.1}/polynode/types/rest.py +0 -0
  38. {polynode-0.6.3 → polynode-0.7.1}/polynode/types/short_form.py +0 -0
  39. {polynode-0.6.3 → polynode-0.7.1}/polynode/types/ws.py +0 -0
  40. {polynode-0.6.3 → polynode-0.7.1}/polynode/ws.py +0 -0
  41. {polynode-0.6.3 → polynode-0.7.1}/tests/__init__.py +0 -0
  42. {polynode-0.6.3 → polynode-0.7.1}/tests/conftest.py +0 -0
  43. {polynode-0.6.3 → polynode-0.7.1}/tests/test_client.py +0 -0
  44. {polynode-0.6.3 → polynode-0.7.1}/tests/test_orderbook.py +0 -0
  45. {polynode-0.6.3 → polynode-0.7.1}/tests/test_types.py +0 -0
@@ -36,3 +36,5 @@ docs/
36
36
  # Compiled binaries
37
37
  ob/ob
38
38
  ob/ob.bak
39
+ wallet-resolver/wallets.db
40
+ __pycache__/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: polynode
3
- Version: 0.6.3
3
+ Version: 0.7.1
4
4
  Summary: Python SDK for the PolyNode real-time prediction market data platform
5
5
  Project-URL: Homepage, https://polynode.dev
6
6
  Project-URL: Documentation, https://docs.polynode.dev
@@ -12,6 +12,7 @@ from .privy import PrivySigner, PrivyConfig
12
12
  __all__ = [
13
13
  "PolyNodeTrader",
14
14
  "BuilderCredentials",
15
+ "ExchangeVersion",
15
16
  "PrivySigner",
16
17
  "PrivyConfig",
17
18
  "NormalizedSigner",
@@ -6,8 +6,9 @@ from typing import Any
6
6
 
7
7
  import httpx
8
8
 
9
- from .constants import CLOB_HOST
9
+ from .constants import CLOB_HOST, CLOB_HOST_V2
10
10
  from .cosigner import build_l2_headers
11
+ from .types import ExchangeVersion
11
12
 
12
13
 
13
14
  async def post_order(
@@ -18,6 +19,7 @@ async def post_order(
18
19
  wallet_address: str,
19
20
  order_body: str,
20
21
  builder_credentials: Any | None = None,
22
+ clob_host: str | None = None,
21
23
  ) -> dict[str, Any]:
22
24
  """Submit a signed order to the CLOB."""
23
25
  from .cosigner import send_via_cosigner
@@ -38,6 +40,7 @@ async def post_order(
38
40
  fallback_direct,
39
41
  {"method": "POST", "path": "/order", "body": order_body, "headers": headers},
40
42
  builder_credentials=builder_credentials,
43
+ clob_host=clob_host,
41
44
  )
42
45
 
43
46
 
@@ -49,6 +52,7 @@ async def cancel_order(
49
52
  wallet_address: str,
50
53
  order_id: str,
51
54
  builder_credentials: Any | None = None,
55
+ clob_host: str | None = None,
52
56
  ) -> dict[str, Any]:
53
57
  """Cancel a specific order."""
54
58
  from .cosigner import send_via_cosigner
@@ -71,6 +75,7 @@ async def cancel_order(
71
75
  fallback_direct,
72
76
  {"method": "DELETE", "path": "/order", "body": body, "headers": headers},
73
77
  builder_credentials=builder_credentials,
78
+ clob_host=clob_host,
74
79
  )
75
80
 
76
81
 
@@ -82,6 +87,7 @@ async def cancel_all_orders(
82
87
  wallet_address: str,
83
88
  market: str | None = None,
84
89
  builder_credentials: Any | None = None,
90
+ clob_host: str | None = None,
85
91
  ) -> dict[str, Any]:
86
92
  """Cancel all orders, optionally for a specific market."""
87
93
  from .cosigner import send_via_cosigner
@@ -105,6 +111,7 @@ async def cancel_all_orders(
105
111
  fallback_direct,
106
112
  {"method": "DELETE", "path": path, "body": body, "headers": headers},
107
113
  builder_credentials=builder_credentials,
114
+ clob_host=clob_host,
108
115
  )
109
116
 
110
117
 
@@ -117,6 +124,7 @@ async def get_open_orders(
117
124
  market: str | None = None,
118
125
  asset_id: str | None = None,
119
126
  builder_credentials: Any | None = None,
127
+ clob_host: str | None = None,
120
128
  ) -> list[dict[str, Any]]:
121
129
  """Get open orders from the CLOB."""
122
130
  from .cosigner import send_via_cosigner
@@ -145,6 +153,7 @@ async def get_open_orders(
145
153
  fallback_direct,
146
154
  {"method": "GET", "path": path, "headers": headers},
147
155
  builder_credentials=builder_credentials,
156
+ clob_host=clob_host,
148
157
  )
149
158
 
150
159
  if isinstance(result, list):
@@ -152,17 +161,30 @@ async def get_open_orders(
152
161
  return result.get("data") or result.get("orders") or []
153
162
 
154
163
 
155
- async def fetch_tick_size(token_id: str) -> str:
164
+ def _clob_host(version: ExchangeVersion = ExchangeVersion.V1) -> str:
165
+ """Return the CLOB host URL for the given exchange version."""
166
+ return CLOB_HOST_V2 if version == ExchangeVersion.V2 else CLOB_HOST
167
+
168
+
169
+ async def fetch_tick_size(
170
+ token_id: str,
171
+ version: ExchangeVersion = ExchangeVersion.V1,
172
+ ) -> str:
156
173
  """Fetch tick size for a token from the CLOB."""
174
+ host = _clob_host(version)
157
175
  async with httpx.AsyncClient(timeout=5.0) as http:
158
- resp = await http.get(f"{CLOB_HOST}/tick-size?token_id={token_id}")
176
+ resp = await http.get(f"{host}/tick-size?token_id={token_id}")
159
177
  data = resp.json()
160
178
  return str(data.get("minimum_tick_size", data) if isinstance(data, dict) else data)
161
179
 
162
180
 
163
- async def fetch_neg_risk(token_id: str) -> bool:
181
+ async def fetch_neg_risk(
182
+ token_id: str,
183
+ version: ExchangeVersion = ExchangeVersion.V1,
184
+ ) -> bool:
164
185
  """Fetch neg-risk flag for a token from the CLOB."""
186
+ host = _clob_host(version)
165
187
  async with httpx.AsyncClient(timeout=5.0) as http:
166
- resp = await http.get(f"{CLOB_HOST}/neg-risk?token_id={token_id}")
188
+ resp = await http.get(f"{host}/neg-risk?token_id={token_id}")
167
189
  data = resp.json()
168
190
  return bool(data.get("neg_risk", data) if isinstance(data, dict) else data)
@@ -1,16 +1,22 @@
1
1
  """Contract addresses and constants for Polymarket on Polygon (chain 137)."""
2
2
 
3
3
  CHAIN_ID = 137
4
- CLOB_HOST = "https://clob.polymarket.com"
5
4
  RELAYER_HOST = "https://relayer-v2.polymarket.com"
6
5
  DEFAULT_RPC = "https://polygon-bor-rpc.publicnode.com"
7
6
  DEFAULT_COSIGNER = "https://trade.polynode.dev"
8
7
 
9
- # Exchange contracts
8
+ # ── V1 ──
9
+ CLOB_HOST = "https://clob.polymarket.com"
10
10
  CTF_EXCHANGE = "0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E"
11
11
  NEG_RISK_CTF_EXCHANGE = "0xC5d563A36AE78145C45a50134d48A1215220f80a"
12
12
  NEG_RISK_ADAPTER = "0xd91E80cF2E7be2e162c6513ceD06f1dD0dA35296"
13
13
 
14
+ # ── V2 ──
15
+ CLOB_HOST_V2 = "https://clob-v2.polymarket.com"
16
+ CTF_EXCHANGE_V2 = "0xe111180000d2663c0091e4f400237545b87b996b"
17
+ NEG_RISK_CTF_EXCHANGE_V2_A = "0xe2222d279d744050d28e00520010520000310f59"
18
+ NEG_RISK_CTF_EXCHANGE_V2_B = "0xe2222d002000ba0053cef3375333610f64600036"
19
+
14
20
  # Token contracts
15
21
  USDC = "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174"
16
22
  CTF = "0x4D97DCd97eC945f40cF65F87097ACe5EA0476045"
@@ -26,6 +32,15 @@ PROXY_INIT_CODE_HASH = "0xd21df8dc65880a8606f09fe0ce3df9b8869287ab0b058be05aa9e8
26
32
 
27
33
  # All spender contracts that need approval
28
34
  SPENDERS = [CTF_EXCHANGE, NEG_RISK_CTF_EXCHANGE, NEG_RISK_ADAPTER]
35
+ V2_SPENDERS = [CTF_EXCHANGE_V2, NEG_RISK_CTF_EXCHANGE_V2_A, NEG_RISK_CTF_EXCHANGE_V2_B]
36
+
37
+ # PolyUSD wrapping contracts
38
+ POLY_USD = "0xc011a7e12a19f7b1f670d46f03b03f3342e82dfb"
39
+ COLLATERAL_ONRAMP = "0x93070a847efef7f70739046a929d47a521f5b8ee"
40
+ COLLATERAL_OFFRAMP = "0x2957922eb93258b93368531d39facca3b4dc5854"
41
+
42
+ # Fee Escrow
43
+ FEE_ESCROW_ADDRESS = "0xa11D28433B79D0A88F3119b16A090075752258EA"
29
44
 
30
45
  # Metadata cache TTL (5 minutes)
31
46
  META_TTL_SECONDS = 300
@@ -49,6 +49,7 @@ async def send_via_cosigner(
49
49
  fallback_direct: bool,
50
50
  request: dict[str, Any],
51
51
  builder_credentials: Any | None = None,
52
+ clob_host: str | None = None,
52
53
  ) -> Any:
53
54
  """Send a request through the co-signer with optional fallback to direct CLOB."""
54
55
  if cosigner_url:
@@ -70,19 +71,20 @@ async def send_via_cosigner(
70
71
  )
71
72
  data = resp.json()
72
73
  if resp.status_code >= 500 and fallback_direct:
73
- return await _send_direct(request)
74
+ return await _send_direct(request, clob_host=clob_host)
74
75
  return data
75
76
  except Exception as e:
76
77
  if fallback_direct:
77
- return await _send_direct(request)
78
+ return await _send_direct(request, clob_host=clob_host)
78
79
  raise RuntimeError(f"Co-signer unreachable: {e}")
79
80
 
80
- return await _send_direct(request)
81
+ return await _send_direct(request, clob_host=clob_host)
81
82
 
82
83
 
83
- async def _send_direct(request: dict[str, Any]) -> Any:
84
+ async def _send_direct(request: dict[str, Any], clob_host: str | None = None) -> Any:
84
85
  """Send directly to Polymarket CLOB (no builder attribution)."""
85
- url = f"{CLOB_HOST}{request['path']}"
86
+ host = clob_host or CLOB_HOST
87
+ url = f"{host}{request['path']}"
86
88
  async with httpx.AsyncClient(timeout=10.0) as http:
87
89
  resp = await http.request(
88
90
  request["method"],
@@ -0,0 +1,330 @@
1
+ """Pure Python EIP-712 order signing for Polymarket exchange contracts."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ from typing import Any
7
+
8
+ from .constants import (
9
+ CTF_EXCHANGE,
10
+ CTF_EXCHANGE_V2,
11
+ NEG_RISK_CTF_EXCHANGE,
12
+ NEG_RISK_CTF_EXCHANGE_V2_A,
13
+ )
14
+ from .types import Eip712Payload, ExchangeVersion, SignatureType
15
+
16
+
17
+ # ── V1 EIP-712 domains ──
18
+
19
+ EXCHANGE_DOMAIN = {
20
+ "name": "Polymarket CTF Exchange",
21
+ "version": "1",
22
+ "chainId": 137,
23
+ "verifyingContract": CTF_EXCHANGE,
24
+ }
25
+
26
+ NEG_RISK_DOMAIN = {
27
+ "name": "Polymarket CTF Exchange",
28
+ "version": "1",
29
+ "chainId": 137,
30
+ "verifyingContract": NEG_RISK_CTF_EXCHANGE,
31
+ }
32
+
33
+ ORDER_TYPES = {
34
+ "Order": [
35
+ {"name": "salt", "type": "uint256"},
36
+ {"name": "maker", "type": "address"},
37
+ {"name": "signer", "type": "address"},
38
+ {"name": "taker", "type": "address"},
39
+ {"name": "tokenId", "type": "uint256"},
40
+ {"name": "makerAmount", "type": "uint256"},
41
+ {"name": "takerAmount", "type": "uint256"},
42
+ {"name": "expiration", "type": "uint256"},
43
+ {"name": "nonce", "type": "uint256"},
44
+ {"name": "feeRateBps", "type": "uint256"},
45
+ {"name": "side", "type": "uint8"},
46
+ {"name": "signatureType", "type": "uint8"},
47
+ ],
48
+ }
49
+
50
+ # ── V2 EIP-712 domains ──
51
+
52
+ EXCHANGE_DOMAIN_V2 = {
53
+ "name": "Polymarket CTF Exchange",
54
+ "version": "2",
55
+ "chainId": 137,
56
+ "verifyingContract": CTF_EXCHANGE_V2,
57
+ }
58
+
59
+ NEG_RISK_DOMAIN_V2 = {
60
+ "name": "Polymarket CTF Exchange",
61
+ "version": "2",
62
+ "chainId": 137,
63
+ "verifyingContract": NEG_RISK_CTF_EXCHANGE_V2_A,
64
+ }
65
+
66
+ ORDER_TYPES_V2 = {
67
+ "Order": [
68
+ {"name": "salt", "type": "uint256"},
69
+ {"name": "maker", "type": "address"},
70
+ {"name": "signer", "type": "address"},
71
+ {"name": "tokenId", "type": "uint256"},
72
+ {"name": "makerAmount", "type": "uint256"},
73
+ {"name": "takerAmount", "type": "uint256"},
74
+ {"name": "side", "type": "uint8"},
75
+ {"name": "signatureType", "type": "uint8"},
76
+ {"name": "timestamp", "type": "uint256"},
77
+ {"name": "metadata", "type": "bytes32"},
78
+ {"name": "builder", "type": "bytes32"},
79
+ ],
80
+ }
81
+
82
+
83
+ def compute_amounts(
84
+ price: float,
85
+ size: float,
86
+ side: str,
87
+ tick_size: str,
88
+ ) -> tuple[int, int]:
89
+ """Compute maker_amount and taker_amount from price/size/tick_size."""
90
+ decimals = len(tick_size.rstrip("0").split(".")[-1]) if "." in tick_size else 0
91
+ raw_price = round(price * (10 ** decimals))
92
+ raw_size = round(size * 1_000_000) # USDC has 6 decimals
93
+ price_denom = 10 ** decimals
94
+
95
+ if side == "BUY":
96
+ maker_amount = raw_size * raw_price // price_denom
97
+ taker_amount = raw_size
98
+ else:
99
+ maker_amount = raw_size
100
+ taker_amount = raw_size * raw_price // price_denom
101
+
102
+ return maker_amount, taker_amount
103
+
104
+
105
+ def build_order_payload(
106
+ *,
107
+ maker: str,
108
+ signer: str,
109
+ token_id: str,
110
+ maker_amount: int,
111
+ taker_amount: int,
112
+ side: str,
113
+ signature_type: SignatureType,
114
+ fee_rate_bps: int = 0,
115
+ nonce: int = 0,
116
+ expiration: int = 0,
117
+ neg_risk: bool = False,
118
+ ) -> Eip712Payload:
119
+ """Build an EIP-712 typed data payload for a Polymarket order."""
120
+ import secrets
121
+
122
+ salt = secrets.randbelow(2**32)
123
+ side_int = 0 if side == "BUY" else 1
124
+
125
+ domain = NEG_RISK_DOMAIN if neg_risk else EXCHANGE_DOMAIN
126
+
127
+ message = {
128
+ "salt": salt,
129
+ "maker": maker,
130
+ "signer": signer,
131
+ "taker": "0x0000000000000000000000000000000000000000",
132
+ "tokenId": int(token_id),
133
+ "makerAmount": maker_amount,
134
+ "takerAmount": taker_amount,
135
+ "expiration": expiration,
136
+ "nonce": nonce,
137
+ "feeRateBps": fee_rate_bps,
138
+ "side": side_int,
139
+ "signatureType": int(signature_type),
140
+ }
141
+
142
+ return Eip712Payload(
143
+ domain=domain,
144
+ types=ORDER_TYPES,
145
+ primary_type="Order",
146
+ message=message,
147
+ )
148
+
149
+
150
+ async def create_signed_order(
151
+ sign_typed_data: Any,
152
+ *,
153
+ signer_address: str,
154
+ funder_address: str,
155
+ token_id: str,
156
+ price: float,
157
+ size: float,
158
+ side: str,
159
+ signature_type: SignatureType,
160
+ tick_size: str = "0.01",
161
+ neg_risk: bool = False,
162
+ fee_rate_bps: int = 0,
163
+ expiration: int = 0,
164
+ nonce: int = 0,
165
+ ) -> dict[str, Any]:
166
+ """Create and sign a V1 Polymarket order. Returns the order dict ready for CLOB submission."""
167
+ maker_amount, taker_amount = compute_amounts(price, size, side, tick_size)
168
+
169
+ payload = build_order_payload(
170
+ maker=funder_address,
171
+ signer=signer_address,
172
+ token_id=token_id,
173
+ maker_amount=maker_amount,
174
+ taker_amount=taker_amount,
175
+ side=side,
176
+ signature_type=signature_type,
177
+ fee_rate_bps=fee_rate_bps,
178
+ nonce=nonce,
179
+ expiration=expiration,
180
+ neg_risk=neg_risk,
181
+ )
182
+
183
+ signature = await sign_typed_data(payload)
184
+ if not signature.startswith("0x"):
185
+ signature = f"0x{signature}"
186
+
187
+ # Normalize v to raw recovery id (0/1): CLOB expects v=0/1 for order signatures
188
+ sig_bytes = bytearray(bytes.fromhex(signature[2:]))
189
+ if len(sig_bytes) == 65 and sig_bytes[-1] >= 27:
190
+ sig_bytes[-1] -= 27
191
+ signature = "0x" + sig_bytes.hex()
192
+
193
+ return {
194
+ "salt": payload.message["salt"],
195
+ "maker": funder_address,
196
+ "signer": signer_address,
197
+ "taker": "0x0000000000000000000000000000000000000000",
198
+ "tokenId": token_id,
199
+ "makerAmount": str(maker_amount),
200
+ "takerAmount": str(taker_amount),
201
+ "expiration": str(expiration),
202
+ "nonce": str(nonce),
203
+ "feeRateBps": str(fee_rate_bps),
204
+ "side": side,
205
+ "signatureType": int(signature_type),
206
+ "signature": signature,
207
+ }
208
+
209
+
210
+ # ── V2 order building ──
211
+
212
+
213
+ def build_order_payload_v2(
214
+ *,
215
+ maker: str,
216
+ signer: str,
217
+ token_id: str,
218
+ maker_amount: int,
219
+ taker_amount: int,
220
+ side: str,
221
+ signature_type: SignatureType,
222
+ neg_risk: bool = False,
223
+ timestamp_ms: int | None = None,
224
+ metadata: str = "0x" + "00" * 32,
225
+ builder: str = "0x" + "00" * 32,
226
+ ) -> Eip712Payload:
227
+ """Build a V2 EIP-712 typed data payload for a Polymarket order."""
228
+ import time
229
+
230
+ if timestamp_ms is None:
231
+ timestamp_ms = int(time.time() * 1000)
232
+
233
+ salt = timestamp_ms
234
+ side_int = 0 if side == "BUY" else 1
235
+ domain = NEG_RISK_DOMAIN_V2 if neg_risk else EXCHANGE_DOMAIN_V2
236
+
237
+ # Pad metadata/builder to bytes32
238
+ meta_hex = metadata if metadata.startswith("0x") else f"0x{metadata}"
239
+ meta_hex = meta_hex[:2] + meta_hex[2:].ljust(64, "0")
240
+ builder_hex = builder if builder.startswith("0x") else f"0x{builder}"
241
+ builder_hex = builder_hex[:2] + builder_hex[2:].ljust(64, "0")
242
+
243
+ message = {
244
+ "salt": salt,
245
+ "maker": maker,
246
+ "signer": signer,
247
+ "tokenId": int(token_id),
248
+ "makerAmount": maker_amount,
249
+ "takerAmount": taker_amount,
250
+ "side": side_int,
251
+ "signatureType": int(signature_type),
252
+ "timestamp": timestamp_ms,
253
+ "metadata": meta_hex,
254
+ "builder": builder_hex,
255
+ }
256
+
257
+ return Eip712Payload(
258
+ domain=domain,
259
+ types=ORDER_TYPES_V2,
260
+ primary_type="Order",
261
+ message=message,
262
+ )
263
+
264
+
265
+ async def create_signed_order_v2(
266
+ sign_typed_data: Any,
267
+ *,
268
+ signer_address: str,
269
+ funder_address: str,
270
+ token_id: str,
271
+ price: float,
272
+ size: float,
273
+ side: str,
274
+ signature_type: SignatureType,
275
+ tick_size: str = "0.01",
276
+ neg_risk: bool = False,
277
+ timestamp_ms: int | None = None,
278
+ metadata: str = "0x" + "00" * 32,
279
+ builder: str = "0x" + "00" * 32,
280
+ ) -> dict[str, Any]:
281
+ """Create and sign a V2 Polymarket order. Returns the order dict ready for V2 CLOB submission.
282
+
283
+ V2 differences from V1:
284
+ - No taker, expiration, nonce, or feeRateBps fields
285
+ - Adds timestamp (ms), metadata (bytes32), builder (bytes32)
286
+ - Domain version "2" with V2 exchange addresses
287
+ - salt is a number (not string) in the payload
288
+ - side is a string ("BUY"/"SELL") in the payload
289
+ - timestamp is a string in the payload
290
+ """
291
+ maker_amount, taker_amount = compute_amounts(price, size, side, tick_size)
292
+
293
+ payload = build_order_payload_v2(
294
+ maker=funder_address,
295
+ signer=signer_address,
296
+ token_id=token_id,
297
+ maker_amount=maker_amount,
298
+ taker_amount=taker_amount,
299
+ side=side,
300
+ signature_type=signature_type,
301
+ neg_risk=neg_risk,
302
+ timestamp_ms=timestamp_ms,
303
+ metadata=metadata,
304
+ builder=builder,
305
+ )
306
+
307
+ signature = await sign_typed_data(payload)
308
+ if not signature.startswith("0x"):
309
+ signature = f"0x{signature}"
310
+
311
+ # Normalize v to raw recovery id (0/1): CLOB expects v=0/1 for order signatures
312
+ sig_bytes = bytearray(bytes.fromhex(signature[2:]))
313
+ if len(sig_bytes) == 65 and sig_bytes[-1] >= 27:
314
+ sig_bytes[-1] -= 27
315
+ signature = "0x" + sig_bytes.hex()
316
+
317
+ return {
318
+ "salt": payload.message["salt"], # number
319
+ "maker": funder_address,
320
+ "signer": signer_address,
321
+ "tokenId": token_id,
322
+ "makerAmount": str(maker_amount),
323
+ "takerAmount": str(taker_amount),
324
+ "side": side, # string "BUY"/"SELL"
325
+ "signatureType": int(signature_type), # number
326
+ "timestamp": str(payload.message["timestamp"]), # string
327
+ "metadata": payload.message["metadata"],
328
+ "builder": payload.message["builder"],
329
+ "signature": signature,
330
+ }