polynode 0.6.3__tar.gz → 0.7.0__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.
- {polynode-0.6.3 → polynode-0.7.0}/.gitignore +2 -0
- {polynode-0.6.3 → polynode-0.7.0}/PKG-INFO +1 -1
- {polynode-0.6.3 → polynode-0.7.0}/polynode/trading/__init__.py +1 -0
- {polynode-0.6.3 → polynode-0.7.0}/polynode/trading/clob_api.py +27 -5
- {polynode-0.6.3 → polynode-0.7.0}/polynode/trading/constants.py +14 -2
- {polynode-0.6.3 → polynode-0.7.0}/polynode/trading/cosigner.py +7 -5
- polynode-0.7.0/polynode/trading/eip712.py +330 -0
- {polynode-0.6.3 → polynode-0.7.0}/polynode/trading/onboarding.py +122 -9
- {polynode-0.6.3 → polynode-0.7.0}/polynode/trading/signer.py +2 -0
- {polynode-0.6.3 → polynode-0.7.0}/polynode/trading/trader.py +232 -23
- {polynode-0.6.3 → polynode-0.7.0}/polynode/trading/types.py +6 -0
- {polynode-0.6.3 → polynode-0.7.0}/pyproject.toml +1 -1
- polynode-0.7.0/tests/test_trading.py +334 -0
- polynode-0.6.3/polynode/trading/eip712.py +0 -169
- polynode-0.6.3/tests/test_trading.py +0 -50
- {polynode-0.6.3 → polynode-0.7.0}/README.md +0 -0
- {polynode-0.6.3 → polynode-0.7.0}/polynode/__init__.py +0 -0
- {polynode-0.6.3 → polynode-0.7.0}/polynode/_version.py +0 -0
- {polynode-0.6.3 → polynode-0.7.0}/polynode/cache/__init__.py +0 -0
- {polynode-0.6.3 → polynode-0.7.0}/polynode/client.py +0 -0
- {polynode-0.6.3 → polynode-0.7.0}/polynode/engine.py +0 -0
- {polynode-0.6.3 → polynode-0.7.0}/polynode/errors.py +0 -0
- {polynode-0.6.3 → polynode-0.7.0}/polynode/orderbook.py +0 -0
- {polynode-0.6.3 → polynode-0.7.0}/polynode/orderbook_state.py +0 -0
- {polynode-0.6.3 → polynode-0.7.0}/polynode/redemption_watcher.py +0 -0
- {polynode-0.6.3 → polynode-0.7.0}/polynode/short_form.py +0 -0
- {polynode-0.6.3 → polynode-0.7.0}/polynode/subscription.py +0 -0
- {polynode-0.6.3 → polynode-0.7.0}/polynode/testing.py +0 -0
- {polynode-0.6.3 → polynode-0.7.0}/polynode/trading/position_management.py +0 -0
- {polynode-0.6.3 → polynode-0.7.0}/polynode/trading/privy.py +0 -0
- {polynode-0.6.3 → polynode-0.7.0}/polynode/trading/sqlite_backend.py +0 -0
- {polynode-0.6.3 → polynode-0.7.0}/polynode/types/__init__.py +0 -0
- {polynode-0.6.3 → polynode-0.7.0}/polynode/types/enums.py +0 -0
- {polynode-0.6.3 → polynode-0.7.0}/polynode/types/events.py +0 -0
- {polynode-0.6.3 → polynode-0.7.0}/polynode/types/orderbook.py +0 -0
- {polynode-0.6.3 → polynode-0.7.0}/polynode/types/rest.py +0 -0
- {polynode-0.6.3 → polynode-0.7.0}/polynode/types/short_form.py +0 -0
- {polynode-0.6.3 → polynode-0.7.0}/polynode/types/ws.py +0 -0
- {polynode-0.6.3 → polynode-0.7.0}/polynode/ws.py +0 -0
- {polynode-0.6.3 → polynode-0.7.0}/tests/__init__.py +0 -0
- {polynode-0.6.3 → polynode-0.7.0}/tests/conftest.py +0 -0
- {polynode-0.6.3 → polynode-0.7.0}/tests/test_client.py +0 -0
- {polynode-0.6.3 → polynode-0.7.0}/tests/test_orderbook.py +0 -0
- {polynode-0.6.3 → polynode-0.7.0}/tests/test_types.py +0 -0
|
@@ -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
|
-
|
|
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"{
|
|
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(
|
|
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"{
|
|
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
|
-
#
|
|
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,12 @@ 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"
|
|
29
41
|
|
|
30
42
|
# Metadata cache TTL (5 minutes)
|
|
31
43
|
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
|
-
|
|
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
|
+
}
|