polynode 0.6.3__py3-none-any.whl → 0.7.0__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.
- polynode/trading/__init__.py +1 -0
- polynode/trading/clob_api.py +27 -5
- polynode/trading/constants.py +14 -2
- polynode/trading/cosigner.py +7 -5
- polynode/trading/eip712.py +165 -4
- polynode/trading/onboarding.py +122 -9
- polynode/trading/signer.py +2 -0
- polynode/trading/trader.py +232 -23
- polynode/trading/types.py +6 -0
- {polynode-0.6.3.dist-info → polynode-0.7.0.dist-info}/METADATA +1 -1
- {polynode-0.6.3.dist-info → polynode-0.7.0.dist-info}/RECORD +12 -12
- {polynode-0.6.3.dist-info → polynode-0.7.0.dist-info}/WHEEL +0 -0
polynode/trading/__init__.py
CHANGED
polynode/trading/clob_api.py
CHANGED
|
@@ -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)
|
polynode/trading/constants.py
CHANGED
|
@@ -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
|
polynode/trading/cosigner.py
CHANGED
|
@@ -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"],
|
polynode/trading/eip712.py
CHANGED
|
@@ -5,11 +5,17 @@ from __future__ import annotations
|
|
|
5
5
|
import math
|
|
6
6
|
from typing import Any
|
|
7
7
|
|
|
8
|
-
from .constants import
|
|
9
|
-
|
|
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
|
|
10
15
|
|
|
11
16
|
|
|
12
|
-
# EIP-712
|
|
17
|
+
# ── V1 EIP-712 domains ──
|
|
18
|
+
|
|
13
19
|
EXCHANGE_DOMAIN = {
|
|
14
20
|
"name": "Polymarket CTF Exchange",
|
|
15
21
|
"version": "1",
|
|
@@ -41,6 +47,38 @@ ORDER_TYPES = {
|
|
|
41
47
|
],
|
|
42
48
|
}
|
|
43
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
|
+
|
|
44
82
|
|
|
45
83
|
def compute_amounts(
|
|
46
84
|
price: float,
|
|
@@ -125,7 +163,7 @@ async def create_signed_order(
|
|
|
125
163
|
expiration: int = 0,
|
|
126
164
|
nonce: int = 0,
|
|
127
165
|
) -> dict[str, Any]:
|
|
128
|
-
"""Create and sign a Polymarket order. Returns the order dict ready for CLOB submission."""
|
|
166
|
+
"""Create and sign a V1 Polymarket order. Returns the order dict ready for CLOB submission."""
|
|
129
167
|
maker_amount, taker_amount = compute_amounts(price, size, side, tick_size)
|
|
130
168
|
|
|
131
169
|
payload = build_order_payload(
|
|
@@ -167,3 +205,126 @@ async def create_signed_order(
|
|
|
167
205
|
"signatureType": int(signature_type),
|
|
168
206
|
"signature": signature,
|
|
169
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
|
+
}
|
polynode/trading/onboarding.py
CHANGED
|
@@ -9,10 +9,17 @@ import httpx
|
|
|
9
9
|
from .constants import (
|
|
10
10
|
CHAIN_ID,
|
|
11
11
|
CLOB_HOST,
|
|
12
|
+
CLOB_HOST_V2,
|
|
13
|
+
COLLATERAL_OFFRAMP,
|
|
14
|
+
COLLATERAL_ONRAMP,
|
|
12
15
|
CTF,
|
|
13
16
|
CTF_EXCHANGE,
|
|
17
|
+
CTF_EXCHANGE_V2,
|
|
14
18
|
NEG_RISK_ADAPTER,
|
|
15
19
|
NEG_RISK_CTF_EXCHANGE,
|
|
20
|
+
NEG_RISK_CTF_EXCHANGE_V2_A,
|
|
21
|
+
NEG_RISK_CTF_EXCHANGE_V2_B,
|
|
22
|
+
POLY_USD,
|
|
16
23
|
PROXY_FACTORY,
|
|
17
24
|
PROXY_INIT_CODE_HASH,
|
|
18
25
|
RELAYER_HOST,
|
|
@@ -20,8 +27,9 @@ from .constants import (
|
|
|
20
27
|
SAFE_INIT_CODE_HASH,
|
|
21
28
|
SPENDERS,
|
|
22
29
|
USDC,
|
|
30
|
+
V2_SPENDERS,
|
|
23
31
|
)
|
|
24
|
-
from .types import ApprovalStatus, BalanceInfo, SignatureType
|
|
32
|
+
from .types import ApprovalStatus, BalanceInfo, ExchangeVersion, SignatureType
|
|
25
33
|
|
|
26
34
|
|
|
27
35
|
def derive_safe_address(eoa: str) -> str:
|
|
@@ -126,8 +134,17 @@ async def is_safe_deployed(funder_address: str) -> bool:
|
|
|
126
134
|
return False
|
|
127
135
|
|
|
128
136
|
|
|
129
|
-
async def check_approvals(
|
|
130
|
-
|
|
137
|
+
async def check_approvals(
|
|
138
|
+
funder_address: str,
|
|
139
|
+
rpc_url: str,
|
|
140
|
+
version: ExchangeVersion = ExchangeVersion.V1,
|
|
141
|
+
) -> ApprovalStatus:
|
|
142
|
+
"""Check token approvals for all spender contracts.
|
|
143
|
+
|
|
144
|
+
V1: checks USDC allowance to SPENDERS (CTF Exchange, NegRisk, Adapter)
|
|
145
|
+
V2: checks PolyUSD allowance to V2_SPENDERS (V2 Exchange, NegRisk V2 A/B)
|
|
146
|
+
Both: checks CTF isApprovedForAll to the respective spenders
|
|
147
|
+
"""
|
|
131
148
|
try:
|
|
132
149
|
from web3 import Web3
|
|
133
150
|
except ImportError:
|
|
@@ -137,7 +154,22 @@ async def check_approvals(funder_address: str, rpc_url: str) -> ApprovalStatus:
|
|
|
137
154
|
erc20_abi = [{"inputs": [{"name": "owner", "type": "address"}, {"name": "spender", "type": "address"}], "name": "allowance", "outputs": [{"name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"}]
|
|
138
155
|
erc1155_abi = [{"inputs": [{"name": "account", "type": "address"}, {"name": "operator", "type": "address"}], "name": "isApprovedForAll", "outputs": [{"name": "", "type": "bool"}], "stateMutability": "view", "type": "function"}]
|
|
139
156
|
|
|
140
|
-
|
|
157
|
+
if version == ExchangeVersion.V2:
|
|
158
|
+
collateral_token = POLY_USD
|
|
159
|
+
spender_list = [
|
|
160
|
+
(CTF_EXCHANGE_V2, "ctf_exchange_v2"),
|
|
161
|
+
(NEG_RISK_CTF_EXCHANGE_V2_A, "neg_risk_ctf_exchange_v2_a"),
|
|
162
|
+
(NEG_RISK_CTF_EXCHANGE_V2_B, "neg_risk_ctf_exchange_v2_b"),
|
|
163
|
+
]
|
|
164
|
+
else:
|
|
165
|
+
collateral_token = USDC
|
|
166
|
+
spender_list = [
|
|
167
|
+
(CTF_EXCHANGE, "ctf_exchange"),
|
|
168
|
+
(NEG_RISK_CTF_EXCHANGE, "neg_risk_ctf_exchange"),
|
|
169
|
+
(NEG_RISK_ADAPTER, "neg_risk_adapter"),
|
|
170
|
+
]
|
|
171
|
+
|
|
172
|
+
token_contract = w3.eth.contract(address=Web3.to_checksum_address(collateral_token), abi=erc20_abi)
|
|
141
173
|
ctf_contract = w3.eth.contract(address=Web3.to_checksum_address(CTF), abi=erc1155_abi)
|
|
142
174
|
|
|
143
175
|
funder = Web3.to_checksum_address(funder_address)
|
|
@@ -146,9 +178,9 @@ async def check_approvals(funder_address: str, rpc_url: str) -> ApprovalStatus:
|
|
|
146
178
|
|
|
147
179
|
usdc_status = {}
|
|
148
180
|
ctf_status = {}
|
|
149
|
-
for spender, name in
|
|
181
|
+
for spender, name in spender_list:
|
|
150
182
|
spender_addr = Web3.to_checksum_address(spender)
|
|
151
|
-
allowance =
|
|
183
|
+
allowance = token_contract.functions.allowance(funder, spender_addr).call()
|
|
152
184
|
usdc_status[name] = allowance >= threshold
|
|
153
185
|
approved = ctf_contract.functions.isApprovedForAll(funder, spender_addr).call()
|
|
154
186
|
ctf_status[name] = approved
|
|
@@ -163,6 +195,78 @@ async def check_approvals(funder_address: str, rpc_url: str) -> ApprovalStatus:
|
|
|
163
195
|
)
|
|
164
196
|
|
|
165
197
|
|
|
198
|
+
async def set_approvals(
|
|
199
|
+
signer_key: str,
|
|
200
|
+
funder_address: str,
|
|
201
|
+
rpc_url: str,
|
|
202
|
+
version: ExchangeVersion = ExchangeVersion.V1,
|
|
203
|
+
) -> list[str]:
|
|
204
|
+
"""Set max approvals for collateral and CTF tokens to exchange spenders.
|
|
205
|
+
|
|
206
|
+
V1: approve USDC to SPENDERS
|
|
207
|
+
V2: approve PolyUSD to V2_SPENDERS
|
|
208
|
+
Both: setApprovalForAll on CTF to the respective spenders
|
|
209
|
+
|
|
210
|
+
Returns a list of submitted transaction hashes.
|
|
211
|
+
"""
|
|
212
|
+
try:
|
|
213
|
+
from web3 import Web3
|
|
214
|
+
from eth_account import Account
|
|
215
|
+
except ImportError:
|
|
216
|
+
raise ImportError("web3 required. Install with: pip install polynode[trading]")
|
|
217
|
+
|
|
218
|
+
w3 = Web3(Web3.HTTPProvider(rpc_url))
|
|
219
|
+
account = Account.from_key(signer_key if signer_key.startswith("0x") else f"0x{signer_key}")
|
|
220
|
+
max_uint = 2**256 - 1
|
|
221
|
+
|
|
222
|
+
erc20_abi = [
|
|
223
|
+
{"inputs": [{"name": "spender", "type": "address"}, {"name": "amount", "type": "uint256"}], "name": "approve", "outputs": [{"name": "", "type": "bool"}], "stateMutability": "nonpayable", "type": "function"},
|
|
224
|
+
]
|
|
225
|
+
erc1155_abi = [
|
|
226
|
+
{"inputs": [{"name": "operator", "type": "address"}, {"name": "approved", "type": "bool"}], "name": "setApprovalForAll", "outputs": [], "stateMutability": "nonpayable", "type": "function"},
|
|
227
|
+
]
|
|
228
|
+
|
|
229
|
+
if version == ExchangeVersion.V2:
|
|
230
|
+
collateral_token = POLY_USD
|
|
231
|
+
spender_list = V2_SPENDERS
|
|
232
|
+
else:
|
|
233
|
+
collateral_token = USDC
|
|
234
|
+
spender_list = SPENDERS
|
|
235
|
+
|
|
236
|
+
token_contract = w3.eth.contract(address=Web3.to_checksum_address(collateral_token), abi=erc20_abi)
|
|
237
|
+
ctf_contract = w3.eth.contract(address=Web3.to_checksum_address(CTF), abi=erc1155_abi)
|
|
238
|
+
|
|
239
|
+
tx_hashes: list[str] = []
|
|
240
|
+
nonce = w3.eth.get_transaction_count(account.address)
|
|
241
|
+
|
|
242
|
+
for spender in spender_list:
|
|
243
|
+
spender_addr = Web3.to_checksum_address(spender)
|
|
244
|
+
|
|
245
|
+
# Approve collateral token
|
|
246
|
+
tx = token_contract.functions.approve(spender_addr, max_uint).build_transaction({
|
|
247
|
+
"from": account.address,
|
|
248
|
+
"nonce": nonce,
|
|
249
|
+
"chainId": CHAIN_ID,
|
|
250
|
+
})
|
|
251
|
+
signed = account.sign_transaction(tx)
|
|
252
|
+
tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction)
|
|
253
|
+
tx_hashes.append(tx_hash.hex())
|
|
254
|
+
nonce += 1
|
|
255
|
+
|
|
256
|
+
# Approve CTF (ERC-1155)
|
|
257
|
+
tx = ctf_contract.functions.setApprovalForAll(spender_addr, True).build_transaction({
|
|
258
|
+
"from": account.address,
|
|
259
|
+
"nonce": nonce,
|
|
260
|
+
"chainId": CHAIN_ID,
|
|
261
|
+
})
|
|
262
|
+
signed = account.sign_transaction(tx)
|
|
263
|
+
tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction)
|
|
264
|
+
tx_hashes.append(tx_hash.hex())
|
|
265
|
+
nonce += 1
|
|
266
|
+
|
|
267
|
+
return tx_hashes
|
|
268
|
+
|
|
269
|
+
|
|
166
270
|
async def check_balance(funder_address: str, rpc_url: str) -> BalanceInfo:
|
|
167
271
|
"""Check USDC and MATIC balance."""
|
|
168
272
|
try:
|
|
@@ -243,17 +347,26 @@ def _parse_clob_credentials(data: dict) -> dict[str, str]:
|
|
|
243
347
|
}
|
|
244
348
|
|
|
245
349
|
|
|
246
|
-
async def create_clob_credentials(
|
|
350
|
+
async def create_clob_credentials(
|
|
351
|
+
signer: Any,
|
|
352
|
+
funder_address: str,
|
|
353
|
+
version: ExchangeVersion = ExchangeVersion.V1,
|
|
354
|
+
) -> dict[str, str]:
|
|
247
355
|
"""Create or derive CLOB API credentials via the Polymarket CLOB API.
|
|
248
356
|
|
|
249
357
|
Tries POST /auth/api-key first (create new), falls back to
|
|
250
358
|
GET /auth/derive-api-key (derive existing). Matches Rust SDK flow.
|
|
359
|
+
|
|
360
|
+
Same API key works on both V1 and V2 CLOBs (shared database),
|
|
361
|
+
but we route to the correct CLOB host for the version.
|
|
251
362
|
"""
|
|
363
|
+
host = CLOB_HOST_V2 if version == ExchangeVersion.V2 else CLOB_HOST
|
|
364
|
+
|
|
252
365
|
async with httpx.AsyncClient(timeout=10.0) as http:
|
|
253
366
|
# Step 1: Try creating a new API key
|
|
254
367
|
headers = await _build_l1_headers(signer, funder_address)
|
|
255
368
|
resp = await http.post(
|
|
256
|
-
f"{
|
|
369
|
+
f"{host}/auth/api-key",
|
|
257
370
|
headers={**headers, "Accept": "*/*", "Content-Type": "application/json"},
|
|
258
371
|
)
|
|
259
372
|
if resp.is_success:
|
|
@@ -265,7 +378,7 @@ async def create_clob_credentials(signer: Any, funder_address: str) -> dict[str,
|
|
|
265
378
|
# Step 2: Fall back to deriving existing key
|
|
266
379
|
headers = await _build_l1_headers(signer, funder_address)
|
|
267
380
|
resp = await http.get(
|
|
268
|
-
f"{
|
|
381
|
+
f"{host}/auth/derive-api-key",
|
|
269
382
|
headers={**headers, "Accept": "*/*", "Content-Type": "application/json"},
|
|
270
383
|
)
|
|
271
384
|
if not resp.is_success:
|
polynode/trading/signer.py
CHANGED
|
@@ -15,6 +15,7 @@ class NormalizedSigner:
|
|
|
15
15
|
sign_message: Any # callable or None
|
|
16
16
|
signature_type: SignatureType
|
|
17
17
|
funder_address: str | None = None
|
|
18
|
+
_private_key: str | None = None # stored for on-chain txn signing (wrap/unwrap)
|
|
18
19
|
|
|
19
20
|
|
|
20
21
|
async def normalize_signer(
|
|
@@ -66,6 +67,7 @@ async def normalize_signer(
|
|
|
66
67
|
sign_typed_data=_sign_typed_data,
|
|
67
68
|
sign_message=_sign_message,
|
|
68
69
|
signature_type=default_signature_type,
|
|
70
|
+
_private_key=key,
|
|
69
71
|
)
|
|
70
72
|
|
|
71
73
|
if isinstance(signer, RouterSigner):
|
polynode/trading/trader.py
CHANGED
|
@@ -7,6 +7,7 @@ import time
|
|
|
7
7
|
from typing import Any
|
|
8
8
|
|
|
9
9
|
from .clob_api import (
|
|
10
|
+
_clob_host,
|
|
10
11
|
cancel_all_orders,
|
|
11
12
|
cancel_order as clob_cancel_order,
|
|
12
13
|
fetch_neg_risk,
|
|
@@ -14,9 +15,15 @@ from .clob_api import (
|
|
|
14
15
|
get_open_orders as clob_get_open_orders,
|
|
15
16
|
post_order,
|
|
16
17
|
)
|
|
17
|
-
from .constants import
|
|
18
|
+
from .constants import (
|
|
19
|
+
COLLATERAL_OFFRAMP,
|
|
20
|
+
COLLATERAL_ONRAMP,
|
|
21
|
+
DEFAULT_COSIGNER,
|
|
22
|
+
POLY_USD,
|
|
23
|
+
USDC,
|
|
24
|
+
)
|
|
18
25
|
from .cosigner import build_l2_headers, send_via_cosigner
|
|
19
|
-
from .eip712 import create_signed_order
|
|
26
|
+
from .eip712 import create_signed_order, create_signed_order_v2
|
|
20
27
|
from .onboarding import (
|
|
21
28
|
check_approvals as check_approvals_onchain,
|
|
22
29
|
check_balance as check_balance_onchain,
|
|
@@ -24,6 +31,7 @@ from .onboarding import (
|
|
|
24
31
|
derive_funder_address,
|
|
25
32
|
detect_wallet_type,
|
|
26
33
|
is_safe_deployed,
|
|
34
|
+
set_approvals as set_approvals_onchain,
|
|
27
35
|
)
|
|
28
36
|
from .signer import NormalizedSigner, normalize_signer
|
|
29
37
|
from .sqlite_backend import TradingSqliteBackend
|
|
@@ -31,6 +39,7 @@ from .types import (
|
|
|
31
39
|
ApprovalStatus,
|
|
32
40
|
BalanceInfo,
|
|
33
41
|
CancelResult,
|
|
42
|
+
ExchangeVersion,
|
|
34
43
|
GeneratedWallet,
|
|
35
44
|
HistoryParams,
|
|
36
45
|
LinkResult,
|
|
@@ -61,6 +70,7 @@ class PolyNodeTrader:
|
|
|
61
70
|
self._default_sig_type = c.default_signature_type
|
|
62
71
|
self._rpc_url = c.rpc_url
|
|
63
72
|
self._builder_credentials = c.builder_credentials
|
|
73
|
+
self._exchange_version = c.exchange_version
|
|
64
74
|
|
|
65
75
|
self._db: TradingSqliteBackend | None = None
|
|
66
76
|
self._active_signer: NormalizedSigner | None = None
|
|
@@ -123,7 +133,7 @@ class PolyNodeTrader:
|
|
|
123
133
|
# Check approvals
|
|
124
134
|
if sig_type in (SignatureType.POLY_GNOSIS_SAFE, SignatureType.EOA) and not approvals_set:
|
|
125
135
|
try:
|
|
126
|
-
approval_status = await check_approvals_onchain(funder, self._rpc_url)
|
|
136
|
+
approval_status = await check_approvals_onchain(funder, self._rpc_url, self._exchange_version)
|
|
127
137
|
if approval_status.all_approved:
|
|
128
138
|
approvals_set = True
|
|
129
139
|
actions.append("approvals_already_set")
|
|
@@ -134,7 +144,7 @@ class PolyNodeTrader:
|
|
|
134
144
|
|
|
135
145
|
# Create CLOB credentials
|
|
136
146
|
if not stored:
|
|
137
|
-
creds = await create_clob_credentials(normalized, funder)
|
|
147
|
+
creds = await create_clob_credentials(normalized, funder, self._exchange_version)
|
|
138
148
|
stored = StoredCredentials(
|
|
139
149
|
wallet_address=normalized.address,
|
|
140
150
|
funder_address=funder,
|
|
@@ -195,7 +205,7 @@ class PolyNodeTrader:
|
|
|
195
205
|
"apiPassphrase": existing.api_passphrase,
|
|
196
206
|
}
|
|
197
207
|
else:
|
|
198
|
-
creds_dict = await create_clob_credentials(normalized, funder)
|
|
208
|
+
creds_dict = await create_clob_credentials(normalized, funder, self._exchange_version)
|
|
199
209
|
|
|
200
210
|
db.upsert_credentials(StoredCredentials(
|
|
201
211
|
wallet_address=normalized.address,
|
|
@@ -326,34 +336,223 @@ class PolyNodeTrader:
|
|
|
326
336
|
|
|
327
337
|
async def check_approvals(self, wallet: str | None = None) -> ApprovalStatus:
|
|
328
338
|
creds = self._get_stored_creds(wallet)
|
|
329
|
-
return await check_approvals_onchain(creds.funder_address or creds.wallet_address, self._rpc_url)
|
|
339
|
+
return await check_approvals_onchain(creds.funder_address or creds.wallet_address, self._rpc_url, self._exchange_version)
|
|
330
340
|
|
|
331
341
|
async def check_balance(self, wallet: str | None = None) -> BalanceInfo:
|
|
332
342
|
creds = self._get_stored_creds(wallet)
|
|
333
343
|
return await check_balance_onchain(creds.funder_address or creds.wallet_address, self._rpc_url)
|
|
334
344
|
|
|
345
|
+
# ── PolyUSD Wrap / Unwrap ──
|
|
346
|
+
|
|
347
|
+
async def wrap_to_polyusd(self, amount: int) -> str:
|
|
348
|
+
"""Wrap USDC.e into PolyUSD via the Collateral Onramp.
|
|
349
|
+
|
|
350
|
+
Args:
|
|
351
|
+
amount: Amount in raw units (6 decimals, e.g. 1_000_000 = $1).
|
|
352
|
+
|
|
353
|
+
Returns:
|
|
354
|
+
Transaction hash as a hex string.
|
|
355
|
+
"""
|
|
356
|
+
try:
|
|
357
|
+
from web3 import Web3
|
|
358
|
+
from eth_account import Account
|
|
359
|
+
except ImportError:
|
|
360
|
+
raise ImportError("web3 required. Install with: pip install polynode[trading]")
|
|
361
|
+
|
|
362
|
+
signer = self._require_signer()
|
|
363
|
+
creds = self._get_stored_creds()
|
|
364
|
+
funder = creds.funder_address or creds.wallet_address
|
|
365
|
+
|
|
366
|
+
w3 = Web3(Web3.HTTPProvider(self._rpc_url))
|
|
367
|
+
|
|
368
|
+
erc20_abi = [
|
|
369
|
+
{"inputs": [{"name": "spender", "type": "address"}, {"name": "amount", "type": "uint256"}], "name": "approve", "outputs": [{"name": "", "type": "bool"}], "stateMutability": "nonpayable", "type": "function"},
|
|
370
|
+
{"inputs": [{"name": "owner", "type": "address"}, {"name": "spender", "type": "address"}], "name": "allowance", "outputs": [{"name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"},
|
|
371
|
+
]
|
|
372
|
+
|
|
373
|
+
usdc_contract = w3.eth.contract(address=Web3.to_checksum_address(USDC), abi=erc20_abi)
|
|
374
|
+
onramp_addr = Web3.to_checksum_address(COLLATERAL_ONRAMP)
|
|
375
|
+
signer_addr = Web3.to_checksum_address(signer.address)
|
|
376
|
+
|
|
377
|
+
# Build account from the signer's private key (requires sign_message support)
|
|
378
|
+
# For private-key signers, we can reconstruct the account
|
|
379
|
+
if not signer._private_key:
|
|
380
|
+
raise RuntimeError("wrap_to_polyusd requires a private key signer (not RouterSigner)")
|
|
381
|
+
|
|
382
|
+
nonce = w3.eth.get_transaction_count(signer_addr)
|
|
383
|
+
|
|
384
|
+
# Step 1: Check and set USDC.e allowance for Onramp
|
|
385
|
+
allowance = usdc_contract.functions.allowance(signer_addr, onramp_addr).call()
|
|
386
|
+
if allowance < amount:
|
|
387
|
+
max_uint = 2**256 - 1
|
|
388
|
+
tx = usdc_contract.functions.approve(onramp_addr, max_uint).build_transaction({
|
|
389
|
+
"from": signer_addr,
|
|
390
|
+
"nonce": nonce,
|
|
391
|
+
"chainId": 137,
|
|
392
|
+
})
|
|
393
|
+
signed = w3.eth.account.sign_transaction(tx, signer._private_key)
|
|
394
|
+
w3.eth.send_raw_transaction(signed.raw_transaction)
|
|
395
|
+
nonce += 1
|
|
396
|
+
|
|
397
|
+
# Step 2: Call wrap(address token, address funder, uint256 amount) on Onramp
|
|
398
|
+
# wrap selector: 0x62355638
|
|
399
|
+
wrap_abi = [
|
|
400
|
+
{"inputs": [{"name": "token", "type": "address"}, {"name": "funder", "type": "address"}, {"name": "amount", "type": "uint256"}], "name": "wrap", "outputs": [], "stateMutability": "nonpayable", "type": "function"},
|
|
401
|
+
]
|
|
402
|
+
onramp_contract = w3.eth.contract(address=onramp_addr, abi=wrap_abi)
|
|
403
|
+
tx = onramp_contract.functions.wrap(
|
|
404
|
+
Web3.to_checksum_address(USDC),
|
|
405
|
+
Web3.to_checksum_address(funder),
|
|
406
|
+
amount,
|
|
407
|
+
).build_transaction({
|
|
408
|
+
"from": signer_addr,
|
|
409
|
+
"nonce": nonce,
|
|
410
|
+
"chainId": 137,
|
|
411
|
+
})
|
|
412
|
+
signed = w3.eth.account.sign_transaction(tx, signer._private_key)
|
|
413
|
+
tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction)
|
|
414
|
+
return tx_hash.hex()
|
|
415
|
+
|
|
416
|
+
async def unwrap_from_polyusd(self, amount: int) -> str:
|
|
417
|
+
"""Unwrap PolyUSD back into USDC.e via the Collateral Offramp.
|
|
418
|
+
|
|
419
|
+
Args:
|
|
420
|
+
amount: Amount in raw units (6 decimals, e.g. 1_000_000 = $1).
|
|
421
|
+
|
|
422
|
+
Returns:
|
|
423
|
+
Transaction hash as a hex string.
|
|
424
|
+
"""
|
|
425
|
+
try:
|
|
426
|
+
from web3 import Web3
|
|
427
|
+
except ImportError:
|
|
428
|
+
raise ImportError("web3 required. Install with: pip install polynode[trading]")
|
|
429
|
+
|
|
430
|
+
signer = self._require_signer()
|
|
431
|
+
creds = self._get_stored_creds()
|
|
432
|
+
funder = creds.funder_address or creds.wallet_address
|
|
433
|
+
|
|
434
|
+
w3 = Web3(Web3.HTTPProvider(self._rpc_url))
|
|
435
|
+
|
|
436
|
+
if not signer._private_key:
|
|
437
|
+
raise RuntimeError("unwrap_from_polyusd requires a private key signer (not RouterSigner)")
|
|
438
|
+
|
|
439
|
+
signer_addr = Web3.to_checksum_address(signer.address)
|
|
440
|
+
offramp_addr = Web3.to_checksum_address(COLLATERAL_OFFRAMP)
|
|
441
|
+
|
|
442
|
+
# Check and set PolyUSD allowance for Offramp
|
|
443
|
+
erc20_abi = [
|
|
444
|
+
{"inputs": [{"name": "spender", "type": "address"}, {"name": "amount", "type": "uint256"}], "name": "approve", "outputs": [{"name": "", "type": "bool"}], "stateMutability": "nonpayable", "type": "function"},
|
|
445
|
+
{"inputs": [{"name": "owner", "type": "address"}, {"name": "spender", "type": "address"}], "name": "allowance", "outputs": [{"name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"},
|
|
446
|
+
]
|
|
447
|
+
polyusd_contract = w3.eth.contract(address=Web3.to_checksum_address(POLY_USD), abi=erc20_abi)
|
|
448
|
+
|
|
449
|
+
nonce = w3.eth.get_transaction_count(signer_addr)
|
|
450
|
+
|
|
451
|
+
allowance = polyusd_contract.functions.allowance(signer_addr, offramp_addr).call()
|
|
452
|
+
if allowance < amount:
|
|
453
|
+
max_uint = 2**256 - 1
|
|
454
|
+
tx = polyusd_contract.functions.approve(offramp_addr, max_uint).build_transaction({
|
|
455
|
+
"from": signer_addr,
|
|
456
|
+
"nonce": nonce,
|
|
457
|
+
"chainId": 137,
|
|
458
|
+
})
|
|
459
|
+
signed = w3.eth.account.sign_transaction(tx, signer._private_key)
|
|
460
|
+
w3.eth.send_raw_transaction(signed.raw_transaction)
|
|
461
|
+
nonce += 1
|
|
462
|
+
|
|
463
|
+
# Call unwrap(address token, address receiver, uint256 amount) on Offramp
|
|
464
|
+
unwrap_abi = [
|
|
465
|
+
{"inputs": [{"name": "token", "type": "address"}, {"name": "receiver", "type": "address"}, {"name": "amount", "type": "uint256"}], "name": "unwrap", "outputs": [], "stateMutability": "nonpayable", "type": "function"},
|
|
466
|
+
]
|
|
467
|
+
offramp_contract = w3.eth.contract(address=offramp_addr, abi=unwrap_abi)
|
|
468
|
+
tx = offramp_contract.functions.unwrap(
|
|
469
|
+
Web3.to_checksum_address(USDC),
|
|
470
|
+
Web3.to_checksum_address(funder),
|
|
471
|
+
amount,
|
|
472
|
+
).build_transaction({
|
|
473
|
+
"from": signer_addr,
|
|
474
|
+
"nonce": nonce,
|
|
475
|
+
"chainId": 137,
|
|
476
|
+
})
|
|
477
|
+
signed = w3.eth.account.sign_transaction(tx, signer._private_key)
|
|
478
|
+
tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction)
|
|
479
|
+
return tx_hash.hex()
|
|
480
|
+
|
|
481
|
+
def get_polyusd_balance(self) -> int:
|
|
482
|
+
"""Get PolyUSD balance for the active funder address.
|
|
483
|
+
|
|
484
|
+
Returns:
|
|
485
|
+
Balance in raw units (6 decimals).
|
|
486
|
+
"""
|
|
487
|
+
try:
|
|
488
|
+
from web3 import Web3
|
|
489
|
+
except ImportError:
|
|
490
|
+
raise ImportError("web3 required. Install with: pip install polynode[trading]")
|
|
491
|
+
|
|
492
|
+
creds = self._get_stored_creds()
|
|
493
|
+
funder = creds.funder_address or creds.wallet_address
|
|
494
|
+
|
|
495
|
+
w3 = Web3(Web3.HTTPProvider(self._rpc_url))
|
|
496
|
+
erc20_abi = [{"inputs": [{"name": "account", "type": "address"}], "name": "balanceOf", "outputs": [{"name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"}]
|
|
497
|
+
contract = w3.eth.contract(address=Web3.to_checksum_address(POLY_USD), abi=erc20_abi)
|
|
498
|
+
return contract.functions.balanceOf(Web3.to_checksum_address(funder)).call()
|
|
499
|
+
|
|
500
|
+
def get_usdce_balance(self) -> int:
|
|
501
|
+
"""Get USDC.e balance for the active funder address.
|
|
502
|
+
|
|
503
|
+
Returns:
|
|
504
|
+
Balance in raw units (6 decimals).
|
|
505
|
+
"""
|
|
506
|
+
try:
|
|
507
|
+
from web3 import Web3
|
|
508
|
+
except ImportError:
|
|
509
|
+
raise ImportError("web3 required. Install with: pip install polynode[trading]")
|
|
510
|
+
|
|
511
|
+
creds = self._get_stored_creds()
|
|
512
|
+
funder = creds.funder_address or creds.wallet_address
|
|
513
|
+
|
|
514
|
+
w3 = Web3(Web3.HTTPProvider(self._rpc_url))
|
|
515
|
+
erc20_abi = [{"inputs": [{"name": "account", "type": "address"}], "name": "balanceOf", "outputs": [{"name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"}]
|
|
516
|
+
contract = w3.eth.contract(address=Web3.to_checksum_address(USDC), abi=erc20_abi)
|
|
517
|
+
return contract.functions.balanceOf(Web3.to_checksum_address(funder)).call()
|
|
518
|
+
|
|
335
519
|
# ── Trading ──
|
|
336
520
|
|
|
337
521
|
async def order(self, params: OrderParams) -> OrderResult:
|
|
338
|
-
"""Place an order on Polymarket."""
|
|
522
|
+
"""Place an order on Polymarket. Routes to V1 or V2 CLOB based on exchange_version."""
|
|
339
523
|
creds = self._get_stored_creds()
|
|
340
524
|
signer = self._require_signer()
|
|
341
525
|
meta = await self._fetch_meta(params.token_id)
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
526
|
+
clob_host = _clob_host(self._exchange_version)
|
|
527
|
+
|
|
528
|
+
if self._exchange_version == ExchangeVersion.V2:
|
|
529
|
+
signed_order = await create_signed_order_v2(
|
|
530
|
+
signer.sign_typed_data,
|
|
531
|
+
signer_address=signer.address,
|
|
532
|
+
funder_address=creds.funder_address or creds.wallet_address,
|
|
533
|
+
token_id=params.token_id,
|
|
534
|
+
price=params.price,
|
|
535
|
+
size=params.size,
|
|
536
|
+
side=params.side,
|
|
537
|
+
signature_type=creds.signature_type,
|
|
538
|
+
tick_size=meta.tick_size,
|
|
539
|
+
neg_risk=meta.neg_risk,
|
|
540
|
+
)
|
|
541
|
+
else:
|
|
542
|
+
signed_order = await create_signed_order(
|
|
543
|
+
signer.sign_typed_data,
|
|
544
|
+
signer_address=signer.address,
|
|
545
|
+
funder_address=creds.funder_address or creds.wallet_address,
|
|
546
|
+
token_id=params.token_id,
|
|
547
|
+
price=params.price,
|
|
548
|
+
size=params.size,
|
|
549
|
+
side=params.side,
|
|
550
|
+
signature_type=creds.signature_type,
|
|
551
|
+
tick_size=meta.tick_size,
|
|
552
|
+
neg_risk=meta.neg_risk,
|
|
553
|
+
fee_rate_bps=meta.fee_rate_bps,
|
|
554
|
+
expiration=params.expiration or 0,
|
|
555
|
+
)
|
|
357
556
|
|
|
358
557
|
payload = {
|
|
359
558
|
"order": signed_order,
|
|
@@ -384,6 +583,7 @@ class PolyNodeTrader:
|
|
|
384
583
|
self._cosigner_url, self._polynode_key, self._fallback_direct,
|
|
385
584
|
{"method": "POST", "path": "/order", "body": body_str, "headers": headers},
|
|
386
585
|
builder_credentials=self._builder_credentials,
|
|
586
|
+
clob_host=clob_host,
|
|
387
587
|
)
|
|
388
588
|
|
|
389
589
|
order_id = result.get("orderID") or result.get("orderId")
|
|
@@ -407,11 +607,13 @@ class PolyNodeTrader:
|
|
|
407
607
|
|
|
408
608
|
async def cancel_order(self, order_id: str) -> CancelResult:
|
|
409
609
|
creds = self._get_stored_creds()
|
|
610
|
+
clob_host = _clob_host(self._exchange_version)
|
|
410
611
|
result = await clob_cancel_order(
|
|
411
612
|
self._cosigner_url, self._polynode_key, self._fallback_direct,
|
|
412
613
|
{"apiKey": creds.api_key, "apiSecret": creds.api_secret, "apiPassphrase": creds.api_passphrase},
|
|
413
614
|
creds.wallet_address, order_id,
|
|
414
615
|
builder_credentials=self._builder_credentials,
|
|
616
|
+
clob_host=clob_host,
|
|
415
617
|
)
|
|
416
618
|
return CancelResult(
|
|
417
619
|
canceled=result.get("canceled", []),
|
|
@@ -420,11 +622,13 @@ class PolyNodeTrader:
|
|
|
420
622
|
|
|
421
623
|
async def cancel_all(self, market: str | None = None) -> CancelResult:
|
|
422
624
|
creds = self._get_stored_creds()
|
|
625
|
+
clob_host = _clob_host(self._exchange_version)
|
|
423
626
|
result = await cancel_all_orders(
|
|
424
627
|
self._cosigner_url, self._polynode_key, self._fallback_direct,
|
|
425
628
|
{"apiKey": creds.api_key, "apiSecret": creds.api_secret, "apiPassphrase": creds.api_passphrase},
|
|
426
629
|
creds.wallet_address, market,
|
|
427
630
|
builder_credentials=self._builder_credentials,
|
|
631
|
+
clob_host=clob_host,
|
|
428
632
|
)
|
|
429
633
|
return CancelResult(
|
|
430
634
|
canceled=result.get("canceled", []),
|
|
@@ -435,11 +639,13 @@ class PolyNodeTrader:
|
|
|
435
639
|
self, *, market: str | None = None, asset_id: str | None = None
|
|
436
640
|
) -> list[OpenOrder]:
|
|
437
641
|
creds = self._get_stored_creds()
|
|
642
|
+
clob_host = _clob_host(self._exchange_version)
|
|
438
643
|
result = await clob_get_open_orders(
|
|
439
644
|
self._cosigner_url, self._polynode_key, self._fallback_direct,
|
|
440
645
|
{"apiKey": creds.api_key, "apiSecret": creds.api_secret, "apiPassphrase": creds.api_passphrase},
|
|
441
646
|
creds.wallet_address, market, asset_id,
|
|
442
647
|
builder_credentials=self._builder_credentials,
|
|
648
|
+
clob_host=clob_host,
|
|
443
649
|
)
|
|
444
650
|
return [
|
|
445
651
|
OpenOrder(
|
|
@@ -467,7 +673,10 @@ class PolyNodeTrader:
|
|
|
467
673
|
return cached
|
|
468
674
|
|
|
469
675
|
import asyncio
|
|
470
|
-
tick, neg = await asyncio.gather(
|
|
676
|
+
tick, neg = await asyncio.gather(
|
|
677
|
+
fetch_tick_size(token_id, self._exchange_version),
|
|
678
|
+
fetch_neg_risk(token_id, self._exchange_version),
|
|
679
|
+
)
|
|
471
680
|
meta = MarketMeta(
|
|
472
681
|
token_id=token_id,
|
|
473
682
|
tick_size=tick,
|
polynode/trading/types.py
CHANGED
|
@@ -13,6 +13,11 @@ class SignatureType(IntEnum):
|
|
|
13
13
|
POLY_GNOSIS_SAFE = 2
|
|
14
14
|
|
|
15
15
|
|
|
16
|
+
class ExchangeVersion(IntEnum):
|
|
17
|
+
V1 = 1
|
|
18
|
+
V2 = 2
|
|
19
|
+
|
|
20
|
+
|
|
16
21
|
@runtime_checkable
|
|
17
22
|
class RouterSigner(Protocol):
|
|
18
23
|
async def get_address(self) -> str: ...
|
|
@@ -50,6 +55,7 @@ class TraderConfig:
|
|
|
50
55
|
default_signature_type: SignatureType = SignatureType.POLY_GNOSIS_SAFE
|
|
51
56
|
rpc_url: str = "https://polygon-bor-rpc.publicnode.com"
|
|
52
57
|
builder_credentials: BuilderCredentials | None = None
|
|
58
|
+
exchange_version: ExchangeVersion = ExchangeVersion.V1
|
|
53
59
|
|
|
54
60
|
|
|
55
61
|
@dataclass
|
|
@@ -11,18 +11,18 @@ polynode/subscription.py,sha256=O2yKfdllI0px-1qrVK-0Qq9S4KUmIiXjjYCNC7QW0jM,4121
|
|
|
11
11
|
polynode/testing.py,sha256=bSSgV18ZSaRoLZPlNRCCLmRNwNIQDS35qi7NhuTg8Bg,2802
|
|
12
12
|
polynode/ws.py,sha256=VZdn2dkUJH2-CYBu_L45YnvCU0kQejxHcy2Ji2pRu60,9653
|
|
13
13
|
polynode/cache/__init__.py,sha256=A9lXcIE3ISKKo1r0o3o-CkmQpBhLdKwdQfCR0eW5i9o,519
|
|
14
|
-
polynode/trading/__init__.py,sha256=
|
|
15
|
-
polynode/trading/clob_api.py,sha256=
|
|
16
|
-
polynode/trading/constants.py,sha256=
|
|
17
|
-
polynode/trading/cosigner.py,sha256=
|
|
18
|
-
polynode/trading/eip712.py,sha256=
|
|
19
|
-
polynode/trading/onboarding.py,sha256=
|
|
14
|
+
polynode/trading/__init__.py,sha256=0mtvY4w2k1x000YOX7_ua9JZj4CY27Cz7p0u6L3Hfrw,906
|
|
15
|
+
polynode/trading/clob_api.py,sha256=ygaOoMIg4PGtMzNBT70wk4XrSbVX0EyCCU6PmXK3xrs,5308
|
|
16
|
+
polynode/trading/constants.py,sha256=CjHcfaOTzPFvlorY4XeyGtMS8XNKH4jjoxqKtpN6NmY,1782
|
|
17
|
+
polynode/trading/cosigner.py,sha256=Fww9OgaKdHQAHd8Kom1AJBtmMGC8FbNZxdFaFALNBY8,3058
|
|
18
|
+
polynode/trading/eip712.py,sha256=pZf0Pf4DDIPpK4qbz4DWfcdOeU8PlCQhJPpKSQ97pC4,9553
|
|
19
|
+
polynode/trading/onboarding.py,sha256=ouP9bzioFHCt4fnluAXLWL8O8U6d_1ePeu5JmBXeDyc,13768
|
|
20
20
|
polynode/trading/position_management.py,sha256=z2pYls4ErhyRE6thQdOs45BF4llKvKNNruQgcMEFT_U,3827
|
|
21
21
|
polynode/trading/privy.py,sha256=iiUQZsBqjj29C7oUiFFktoT2muZY06bcMzABKstFKqE,4523
|
|
22
|
-
polynode/trading/signer.py,sha256=
|
|
22
|
+
polynode/trading/signer.py,sha256=piifRquwnwFwrKLL24jsRQYMh_cdkz8wNz99Vx8_KTY,3413
|
|
23
23
|
polynode/trading/sqlite_backend.py,sha256=9ho7DnQVE_nJqmPH5yPSAYcVjVmTS1XmD3NebR-UAwA,7986
|
|
24
|
-
polynode/trading/trader.py,sha256=
|
|
25
|
-
polynode/trading/types.py,sha256=
|
|
24
|
+
polynode/trading/trader.py,sha256=VVNQ_DEkum5u3IC2M5IfLml7s06A7fCqjGXvSX3ayCg,29497
|
|
25
|
+
polynode/trading/types.py,sha256=WGSH25c1LIZax4KNPavHz0G6HEPLUK9ak9VnEYd8fxM,4944
|
|
26
26
|
polynode/types/__init__.py,sha256=0v_CByqG-zT1xucbT2So_77YDmsPj9u956at94nyrLM,280
|
|
27
27
|
polynode/types/enums.py,sha256=2cx9l10A-LUeDNBJZn7l0XBTka2YfxRQcXW0s7s3_4k,1082
|
|
28
28
|
polynode/types/events.py,sha256=MJGZqxBFKfT1y44dZ96bHbwvoPpGvtutBwYE6OTT6nU,6880
|
|
@@ -30,6 +30,6 @@ polynode/types/orderbook.py,sha256=MwD6P3poo3F70bONUZ5i3ajk1sgw39hHTOw6D0fP5_8,1
|
|
|
30
30
|
polynode/types/rest.py,sha256=ftJKQZH0wBrR_l199rDMqnVS8cyt8YnCvo9cKIsTcCw,7012
|
|
31
31
|
polynode/types/short_form.py,sha256=xfGklDi587u6K2aXaRWiwbxqkipW8BiEPeNIDT45v2Y,799
|
|
32
32
|
polynode/types/ws.py,sha256=EzE9Vzrb6cILemfssmnqM8fbHt4USn3latpUHKatwk4,994
|
|
33
|
-
polynode-0.
|
|
34
|
-
polynode-0.
|
|
35
|
-
polynode-0.
|
|
33
|
+
polynode-0.7.0.dist-info/METADATA,sha256=qIF1aMlx-nQAUySzKpoqj3y9Mx66vRNnG4Cuuwvbt0Q,3302
|
|
34
|
+
polynode-0.7.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
35
|
+
polynode-0.7.0.dist-info/RECORD,,
|
|
File without changes
|