polynode 0.7.4__py3-none-any.whl → 0.9.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/_version.py +1 -1
- polynode/trading/V2_ORDER_FLOW.md +264 -0
- polynode/trading/clob_api.py +59 -1
- polynode/trading/constants.py +15 -1
- polynode/trading/eip712.py +2 -0
- polynode/trading/onboarding.py +13 -5
- polynode/trading/trader.py +78 -3
- polynode/trading/types.py +3 -0
- {polynode-0.7.4.dist-info → polynode-0.9.0.dist-info}/METADATA +11 -3
- {polynode-0.7.4.dist-info → polynode-0.9.0.dist-info}/RECORD +11 -10
- {polynode-0.7.4.dist-info → polynode-0.9.0.dist-info}/WHEEL +0 -0
polynode/_version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.
|
|
1
|
+
__version__ = "0.9.0"
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
# V2 Order Flow — Reference
|
|
2
|
+
|
|
3
|
+
End-to-end recipe for placing a Polymarket V2 CLOB order with builder attribution.
|
|
4
|
+
Verified 2026-04-21 with matched order `0xaecd1060f7c978d0ab947eacc12a17106b7a5d087aefe04c275f3d83d2aa6281` (tx `0xb86168562057efd161a1f9ac77ecf3728653cf9668a21567c4923a298cb24d77`).
|
|
5
|
+
|
|
6
|
+
## TL;DR
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
1. Approve pUSD to: CTF_EXCHANGE_V2, NEG_RISK_EXCHANGE_V2_A, NEG_RISK_ADAPTER (V1!)
|
|
10
|
+
2. setApprovalForAll CTF → same three addresses
|
|
11
|
+
3. GET /balance-allowance/update — force CLOB cache refresh after approvals
|
|
12
|
+
4. Sign EIP-712 Order (domain name "Polymarket CTF Exchange", version "2")
|
|
13
|
+
5. POST /order with L2 HMAC headers
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Required Approvals (V2 CLOB balance-allowance check)
|
|
17
|
+
|
|
18
|
+
The V2 CLOB checks **three** pUSD allowances for orders:
|
|
19
|
+
|
|
20
|
+
| Address | Name | Why |
|
|
21
|
+
|---|---|---|
|
|
22
|
+
| `0xE111180000d2663C0091e4f400237545B87B996B` | `CTF_EXCHANGE_V2` | Standard market orders |
|
|
23
|
+
| `0xe2222d279d744050d28e00520010520000310F59` | `NEG_RISK_EXCHANGE_V2_A` | Neg-risk market orders |
|
|
24
|
+
| `0xd91E80cF2E7be2e162c6513ceD06f1dD0dA35296` | `NegRiskAdapter` (V1) | Settlement delegation |
|
|
25
|
+
|
|
26
|
+
**The V1 `NegRiskAdapter` MUST be approved for pUSD even though it's a V1 contract.** The V2 neg-risk exchange delegates settlement to it. Without this allowance set, the CLOB rejects every order with:
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
{"error":"not enough balance / allowance"}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Order Flow Step-by-Step
|
|
33
|
+
|
|
34
|
+
### 1. On-chain approvals (gasless via relayer for Safe wallets)
|
|
35
|
+
|
|
36
|
+
For **every** spender above:
|
|
37
|
+
- `pUSD.approve(spender, MAX_UINT256)`
|
|
38
|
+
- `CTF.setApprovalForAll(spender, true)` — only needed for SELL orders, but safe to set
|
|
39
|
+
|
|
40
|
+
Batch all 6 TXs into one Safe multisend via `RelayClient.execute(...)`.
|
|
41
|
+
|
|
42
|
+
### 2. Refresh CLOB balance/allowance cache
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
GET https://clob-v2.polymarket.com/balance-allowance/update?asset_type=COLLATERAL&signature_type=2
|
|
46
|
+
Headers: POLY_ADDRESS, POLY_SIGNATURE, POLY_TIMESTAMP, POLY_API_KEY, POLY_PASSPHRASE
|
|
47
|
+
HMAC path: /balance-allowance/update (NO query string in HMAC)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The CLOB caches balance/allowance per-API-key. Approvals on-chain are invisible until this endpoint is pinged. **Call this after every approval change.** Wait ~1s before placing orders.
|
|
51
|
+
|
|
52
|
+
Verify:
|
|
53
|
+
```
|
|
54
|
+
GET /balance-allowance?asset_type=COLLATERAL&signature_type=2
|
|
55
|
+
```
|
|
56
|
+
Response:
|
|
57
|
+
```json
|
|
58
|
+
{
|
|
59
|
+
"balance": "2179236",
|
|
60
|
+
"allowances": {
|
|
61
|
+
"0xE111180000d2663C0091e4f400237545B87B996B": "115792089...",
|
|
62
|
+
"0xd91E80cF2E7be2e162c6513ceD06f1dD0dA35296": "115792089...",
|
|
63
|
+
"0xe2222d279d744050d28e00520010520000310F59": "115792089..."
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
All three allowances must be non-zero.
|
|
68
|
+
|
|
69
|
+
### 3. Determine `neg_risk` and pick exchange address
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
GET /neg-risk?token_id={tokenId}
|
|
73
|
+
```
|
|
74
|
+
Response is `{"neg_risk": true|false}`. Do NOT use `!!response` — the **object itself** is always truthy, you need to read the `.neg_risk` field.
|
|
75
|
+
|
|
76
|
+
- `neg_risk: false` → sign against `CTF_EXCHANGE_V2` (`0xe111180000d2663c0091e4f400237545b87b996b`)
|
|
77
|
+
- `neg_risk: true` → sign against `NEG_RISK_EXCHANGE_V2_A` (`0xe2222d279d744050d28e00520010520000310f59`)
|
|
78
|
+
|
|
79
|
+
### 4. Compute raw amounts (string-based, no floats)
|
|
80
|
+
|
|
81
|
+
V2 CLOB requires clean decimal places:
|
|
82
|
+
|
|
83
|
+
| Order class | maker amount | taker amount |
|
|
84
|
+
|---|---|---|
|
|
85
|
+
| LIMIT BUY (tick 0.01) | ≤ 4 decimals | ≤ 2 decimals |
|
|
86
|
+
| LIMIT SELL (tick 0.01) | ≤ 2 decimals | ≤ 4 decimals |
|
|
87
|
+
| MARKET BUY (FAK/FOK) | ≤ 2 decimals | ≤ 4 decimals |
|
|
88
|
+
|
|
89
|
+
Use string-based conversion to avoid float precision bugs:
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
// BUY 4 tokens at $0.43 (cost $1.72)
|
|
93
|
+
const usdcAmount = (0.43 * 4).toFixed(4); // "1.7200"
|
|
94
|
+
const tokenAmount = (4).toFixed(2); // "4.00"
|
|
95
|
+
// → parseUnits(usdcAmount, 6) = 1720000n (raw makerAmount)
|
|
96
|
+
// → parseUnits(tokenAmount, 6) = 4000000n (raw takerAmount)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
`Math.trunc(0.18 * 10 * 1e6)` returns `1799999` (not `1800000`) due to IEEE 754 — DO NOT use float math.
|
|
100
|
+
|
|
101
|
+
### 5. EIP-712 sign
|
|
102
|
+
|
|
103
|
+
Domain:
|
|
104
|
+
```json
|
|
105
|
+
{
|
|
106
|
+
"name": "Polymarket CTF Exchange",
|
|
107
|
+
"version": "2",
|
|
108
|
+
"chainId": 137,
|
|
109
|
+
"verifyingContract": "<exchange address from step 3>"
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Order struct (uint256 for amounts, uint8 for side/signatureType, bytes32 for metadata/builder):
|
|
114
|
+
```ts
|
|
115
|
+
Order: [
|
|
116
|
+
{ name: 'salt', type: 'uint256' },
|
|
117
|
+
{ name: 'maker', type: 'address' },
|
|
118
|
+
{ name: 'signer', type: 'address' },
|
|
119
|
+
{ name: 'tokenId', type: 'uint256' },
|
|
120
|
+
{ name: 'makerAmount', type: 'uint256' },
|
|
121
|
+
{ name: 'takerAmount', type: 'uint256' },
|
|
122
|
+
{ name: 'side', type: 'uint8' }, // 0 = BUY, 1 = SELL
|
|
123
|
+
{ name: 'signatureType', type: 'uint8' }, // 0 = EOA, 1 = PROXY, 2 = SAFE
|
|
124
|
+
{ name: 'timestamp', type: 'uint256' },
|
|
125
|
+
{ name: 'metadata', type: 'bytes32' },
|
|
126
|
+
{ name: 'builder', type: 'bytes32' },
|
|
127
|
+
]
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
`taker` and `expiration` are **NOT** in the signed struct — they are post-signing payload fields.
|
|
131
|
+
|
|
132
|
+
### 6. POST /order
|
|
133
|
+
|
|
134
|
+
Endpoint: `POST https://clob-v2.polymarket.com/order`
|
|
135
|
+
|
|
136
|
+
Exact payload shape (matches `@polymarket/clob-client-v2`):
|
|
137
|
+
```json
|
|
138
|
+
{
|
|
139
|
+
"order": {
|
|
140
|
+
"salt": 1776743397975,
|
|
141
|
+
"maker": "0xbdc1453718AEc6836F03964c254A5F08bFB3c038",
|
|
142
|
+
"signer": "0x3d5BB69B72E731Ac7510b6ae451D523dAd55555e",
|
|
143
|
+
"tokenId": "102941331082550455210851718765952983636922827683407411724225360467552735563141",
|
|
144
|
+
"makerAmount": "1720000",
|
|
145
|
+
"takerAmount": "4000000",
|
|
146
|
+
"side": "BUY",
|
|
147
|
+
"signatureType": 2,
|
|
148
|
+
"timestamp": "1776743397975",
|
|
149
|
+
"metadata": "0x0000000000000000000000000000000000000000000000000000000000000000",
|
|
150
|
+
"builder": "0x5472fdd700a9b2b6613d103095048c92304e97215a2607f73a9d5aa3701f3f09",
|
|
151
|
+
"signature": "0x9f52a79de5cf438330f8c894a8a111e70de944d165b5e774f91a14906302c71a2b8cb5a1194aef4291827985885d54e201a7574b6c0aa1f002436ffda185344b1b",
|
|
152
|
+
"taker": "0x0000000000000000000000000000000000000000",
|
|
153
|
+
"expiration": "0"
|
|
154
|
+
},
|
|
155
|
+
"owner": "<YOUR_CLOB_API_KEY_UUID>",
|
|
156
|
+
"orderType": "FAK",
|
|
157
|
+
"postOnly": false,
|
|
158
|
+
"deferExec": false
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Field notes:
|
|
163
|
+
- `owner` = the API key UUID (not the wallet address, not the apiKey UUID of the BUILDER)
|
|
164
|
+
- `orderType` = `GTC` | `GTD` | `FAK` | `FOK`
|
|
165
|
+
- `taker` = zero address for public orders (private orders set this to the intended counterparty)
|
|
166
|
+
- `expiration` = unix seconds, `"0"` = no expiration
|
|
167
|
+
- `postOnly: true` is incompatible with `FOK`/`FAK`
|
|
168
|
+
- `deferExec: false` is standard — true defers on-chain execution to a later bucket
|
|
169
|
+
|
|
170
|
+
### 7. L2 HMAC headers
|
|
171
|
+
|
|
172
|
+
```
|
|
173
|
+
POLY_ADDRESS = EOA that signed the API key (NOT the Safe/funder)
|
|
174
|
+
POLY_API_KEY = UUID from creds.apiKey
|
|
175
|
+
POLY_PASSPHRASE= creds.apiPassphrase
|
|
176
|
+
POLY_TIMESTAMP = unix seconds (string)
|
|
177
|
+
POLY_SIGNATURE = base64url(HMAC-SHA256(base64_decode(secret), timestamp + method + path + body))
|
|
178
|
+
where base64url = base64 with "+" → "-", "/" → "_"
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Path in HMAC is `/order` only (no query string).
|
|
182
|
+
|
|
183
|
+
## Fees and Budget Math
|
|
184
|
+
|
|
185
|
+
Every market has a `base_fee` (in basis points of a 1.0 coefficient):
|
|
186
|
+
```
|
|
187
|
+
GET /fee-rate?token_id={tokenId} → {"base_fee": 1000} // 10%
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Effective fee per trade (from `@polymarket/clob-client-v2`):
|
|
191
|
+
```
|
|
192
|
+
platformFeeRate = feeRate * (price * (1 - price)) ** feeExponent
|
|
193
|
+
platformFee = (amount / price) * platformFeeRate
|
|
194
|
+
totalCost = amount + platformFee + amount * builderTakerFeeRate
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
For a BUY of `size` tokens at `price`:
|
|
198
|
+
- `amount = price * size` (raw USDC in 6 decimals)
|
|
199
|
+
- `tokens_out = size`
|
|
200
|
+
- Fee on trade ≈ `(price * size) * feeRate * (price * (1-price))^exponent`
|
|
201
|
+
|
|
202
|
+
**You need `balance >= amount + platformFee`.** The CLOB rejects orders with "not enough balance / allowance" if balance can't cover cost + fee.
|
|
203
|
+
|
|
204
|
+
Example: our 5-tokens-at-$0.42 attempts failed because `5 * 0.42 + fee = $2.15-2.20` and balance was `$2.179`. Dropping to 4 tokens put notional at `$1.72` with $0.07 fee — room to spare.
|
|
205
|
+
|
|
206
|
+
Minimum notional for a MARKETABLE BUY is `$1` (not checked for non-crossing limits). Minimum size `min_order_size` in the book response is a per-market floor that the matching engine enforces at fill time, but the CLOB accepts smaller notional orders (we tested size 4 against a market with `min_order_size: 5`).
|
|
207
|
+
|
|
208
|
+
## Verifying Builder Attribution
|
|
209
|
+
|
|
210
|
+
After a match, the builder code flows to `/builder/trades`:
|
|
211
|
+
```
|
|
212
|
+
GET /builder/trades?builder_code=0x5472fdd700a9b2b6613d103095048c92304e97215a2607f73a9d5aa3701f3f09
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Response entry confirms the on-chain settlement:
|
|
216
|
+
```json
|
|
217
|
+
{
|
|
218
|
+
"id": "019dae25-6845-7df6-8811-8e1bae76cf6a",
|
|
219
|
+
"tradeType": "TAKER",
|
|
220
|
+
"takerOrderHash": "0xaecd1060f7c978d0ab947eacc12a17106b7a5d087aefe04c275f3d83d2aa6281",
|
|
221
|
+
"market": "0xd76e7cbb6d6442801ca88e8433d1721c33a34a2f6d85d334e1d8331c76bf6c55",
|
|
222
|
+
"side": "BUY",
|
|
223
|
+
"size": "4.095237",
|
|
224
|
+
"sizeUsdc": "1.719999",
|
|
225
|
+
"price": "0.42",
|
|
226
|
+
"status": "TRADE_STATUS_MATCHED",
|
|
227
|
+
"transactionHash": "0xb86168562057efd161a1f9ac77ecf3728653cf9668a21567c4923a298cb24d77",
|
|
228
|
+
"feeUsdc": "0.07182",
|
|
229
|
+
"builderFee": "0",
|
|
230
|
+
"builderCode": "0x5472fdd700a9b2b6613d103095048c92304e97215a2607f73a9d5aa3701f3f09"
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
The `builderCode` field ties the on-chain trade to the attribution record — the exchange contract reads it from the signed order struct, so it's cryptographically bound and can't be stripped by any intermediate.
|
|
235
|
+
|
|
236
|
+
## Common Failure Modes
|
|
237
|
+
|
|
238
|
+
| Error | Cause |
|
|
239
|
+
|---|---|
|
|
240
|
+
| `"not enough balance / allowance"` | (a) V1 NegRiskAdapter not approved for pUSD; (b) balance-allowance cache stale — run `/balance-allowance/update`; (c) balance < notional + fee |
|
|
241
|
+
| `"invalid signature"` | Wrong exchange address in EIP-712 domain. Almost always caused by `neg_risk` misdetection — check the `.neg_risk` field, don't `!!` the response object |
|
|
242
|
+
| `"invalid amounts, ... max accuracy of N decimals"` | Float precision in amount calc. Use `parseUnits(decimalString, 6)`, not `Math.trunc(x * 1e6)` |
|
|
243
|
+
| `"no orders found to match with FAK order"` | The matching engine ran and found no crossing asks. Either (a) book API showing stale asks, or (b) price didn't actually cross — check you're using the correct tick size |
|
|
244
|
+
| `"invalid amount for a marketable BUY order ($0.5), min size: $1"` | Marketable BUY notional is below $1 |
|
|
245
|
+
| `"Unauthorized/Invalid api key"` on GET | HMAC computed over the **base path only** — query string must NOT be in the signing message |
|
|
246
|
+
|
|
247
|
+
## Working Example
|
|
248
|
+
|
|
249
|
+
See `/home/polygon/trading-lab/test-v2-real-match.mjs` for a full end-to-end example that placed and matched the order above.
|
|
250
|
+
|
|
251
|
+
## Exchange Constants (Polygon mainnet, chain 137)
|
|
252
|
+
|
|
253
|
+
```
|
|
254
|
+
pUSD : 0xC011a7E12a19f7B1f670d46F03B03f3342E82DFB
|
|
255
|
+
CTF (ERC-1155) : 0x4D97DCd97eC945f40cF65F87097ACe5EA0476045
|
|
256
|
+
CTF_EXCHANGE_V2 : 0xe111180000d2663c0091e4f400237545b87b996b
|
|
257
|
+
NEG_RISK_EXCHANGE_V2_A : 0xe2222d279d744050d28e00520010520000310f59
|
|
258
|
+
NegRiskAdapter (V1) : 0xd91E80cF2E7be2e162c6513ceD06f1dD0dA35296
|
|
259
|
+
CtfCollateralAdapter : 0xADa100874d00e3331D00F2007a9c336a65009718
|
|
260
|
+
NegRiskCtfCollateralAdapter : 0xAdA200001000ef00D07553cEE7006808F895c6F1
|
|
261
|
+
|
|
262
|
+
CLOB_V2_HOST : https://clob-v2.polymarket.com
|
|
263
|
+
RELAYER : https://relayer-v2.polymarket.com
|
|
264
|
+
```
|
polynode/trading/clob_api.py
CHANGED
|
@@ -187,4 +187,62 @@ async def fetch_neg_risk(
|
|
|
187
187
|
async with httpx.AsyncClient(timeout=5.0) as http:
|
|
188
188
|
resp = await http.get(f"{host}/neg-risk?token_id={token_id}")
|
|
189
189
|
data = resp.json()
|
|
190
|
-
|
|
190
|
+
if isinstance(data, dict):
|
|
191
|
+
# Read the .neg_risk field directly — `bool(dict)` is always True for non-empty dicts.
|
|
192
|
+
return bool(data.get("neg_risk", False))
|
|
193
|
+
return bool(data)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
async def refresh_balance_allowance(
|
|
197
|
+
credentials: dict[str, str],
|
|
198
|
+
wallet_address: str,
|
|
199
|
+
signature_type: int,
|
|
200
|
+
asset_type: str = "COLLATERAL",
|
|
201
|
+
) -> tuple[bool, int]:
|
|
202
|
+
"""Ask the V2 CLOB to refresh its cached balance/allowance view.
|
|
203
|
+
|
|
204
|
+
The CLOB caches on-chain balance + allowance per-API-key. Changes on-chain
|
|
205
|
+
are invisible until this endpoint is pinged. Call after setting approvals.
|
|
206
|
+
|
|
207
|
+
Returns (ok, status_code).
|
|
208
|
+
"""
|
|
209
|
+
path = "/balance-allowance/update"
|
|
210
|
+
headers = build_l2_headers(
|
|
211
|
+
credentials["apiKey"],
|
|
212
|
+
credentials["apiSecret"],
|
|
213
|
+
credentials["apiPassphrase"],
|
|
214
|
+
wallet_address,
|
|
215
|
+
"GET",
|
|
216
|
+
path, # HMAC uses base path only (NO query string)
|
|
217
|
+
)
|
|
218
|
+
url = f"{CLOB_HOST_V2}{path}?asset_type={asset_type}&signature_type={signature_type}"
|
|
219
|
+
async with httpx.AsyncClient(timeout=10.0) as http:
|
|
220
|
+
resp = await http.get(url, headers=headers)
|
|
221
|
+
return resp.is_success, resp.status_code
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
async def get_balance_allowance(
|
|
225
|
+
credentials: dict[str, str],
|
|
226
|
+
wallet_address: str,
|
|
227
|
+
signature_type: int,
|
|
228
|
+
asset_type: str = "COLLATERAL",
|
|
229
|
+
) -> dict[str, Any] | None:
|
|
230
|
+
"""Read the V2 CLOB's current cached view of balance + allowance.
|
|
231
|
+
|
|
232
|
+
Returns dict with {balance, allowances} or None on failure.
|
|
233
|
+
"""
|
|
234
|
+
path = "/balance-allowance"
|
|
235
|
+
headers = build_l2_headers(
|
|
236
|
+
credentials["apiKey"],
|
|
237
|
+
credentials["apiSecret"],
|
|
238
|
+
credentials["apiPassphrase"],
|
|
239
|
+
wallet_address,
|
|
240
|
+
"GET",
|
|
241
|
+
path, # HMAC uses base path only
|
|
242
|
+
)
|
|
243
|
+
url = f"{CLOB_HOST_V2}{path}?asset_type={asset_type}&signature_type={signature_type}"
|
|
244
|
+
async with httpx.AsyncClient(timeout=10.0) as http:
|
|
245
|
+
resp = await http.get(url, headers=headers)
|
|
246
|
+
if not resp.is_success:
|
|
247
|
+
return None
|
|
248
|
+
return resp.json()
|
polynode/trading/constants.py
CHANGED
|
@@ -32,7 +32,21 @@ PROXY_INIT_CODE_HASH = "0xd21df8dc65880a8606f09fe0ce3df9b8869287ab0b058be05aa9e8
|
|
|
32
32
|
|
|
33
33
|
# All spender contracts that need approval
|
|
34
34
|
SPENDERS = [CTF_EXCHANGE, NEG_RISK_CTF_EXCHANGE, NEG_RISK_ADAPTER]
|
|
35
|
-
|
|
35
|
+
|
|
36
|
+
# V2 spenders that need pUSD approval.
|
|
37
|
+
# Order matters: [0..2] are the three spenders the V2 CLOB validates during order placement.
|
|
38
|
+
# [0] CTF_EXCHANGE_V2 — standard market orders
|
|
39
|
+
# [1] NEG_RISK_CTF_EXCHANGE_V2_A — neg-risk market orders
|
|
40
|
+
# [2] NEG_RISK_ADAPTER (V1!) — V2 neg-risk delegates settlement to V1 NRA, so the CLOB
|
|
41
|
+
# requires pUSD allowance here even though it's a V1 contract.
|
|
42
|
+
# Without this, every order fails with "not enough balance / allowance".
|
|
43
|
+
# [3] NEG_RISK_CTF_EXCHANGE_V2_B — reserved variant
|
|
44
|
+
V2_SPENDERS = [
|
|
45
|
+
CTF_EXCHANGE_V2,
|
|
46
|
+
NEG_RISK_CTF_EXCHANGE_V2_A,
|
|
47
|
+
NEG_RISK_ADAPTER,
|
|
48
|
+
NEG_RISK_CTF_EXCHANGE_V2_B,
|
|
49
|
+
]
|
|
36
50
|
|
|
37
51
|
# PolyUSD wrapping contracts
|
|
38
52
|
POLY_USD = "0xc011a7e12a19f7b1f670d46f03b03f3342e82dfb"
|
polynode/trading/eip712.py
CHANGED
|
@@ -318,12 +318,14 @@ async def create_signed_order_v2(
|
|
|
318
318
|
"salt": payload.message["salt"], # number
|
|
319
319
|
"maker": funder_address,
|
|
320
320
|
"signer": signer_address,
|
|
321
|
+
"taker": "0x0000000000000000000000000000000000000000", # zero = public order (post-sign field)
|
|
321
322
|
"tokenId": token_id,
|
|
322
323
|
"makerAmount": str(maker_amount),
|
|
323
324
|
"takerAmount": str(taker_amount),
|
|
324
325
|
"side": side, # string "BUY"/"SELL"
|
|
325
326
|
"signatureType": int(signature_type), # number
|
|
326
327
|
"timestamp": str(payload.message["timestamp"]), # string
|
|
328
|
+
"expiration": "0", # "0" = no expiration (matches @polymarket/clob-client-v2)
|
|
327
329
|
"metadata": payload.message["metadata"],
|
|
328
330
|
"builder": payload.message["builder"],
|
|
329
331
|
"signature": signature,
|
polynode/trading/onboarding.py
CHANGED
|
@@ -156,11 +156,14 @@ async def check_approvals(
|
|
|
156
156
|
erc1155_abi = [{"inputs": [{"name": "account", "type": "address"}, {"name": "operator", "type": "address"}], "name": "isApprovedForAll", "outputs": [{"name": "", "type": "bool"}], "stateMutability": "view", "type": "function"}]
|
|
157
157
|
|
|
158
158
|
if version == ExchangeVersion.V2:
|
|
159
|
+
# V2 CLOB validates three pUSD allowances during order placement:
|
|
160
|
+
# CTF_EXCHANGE_V2 (standard), NEG_RISK_CTF_EXCHANGE_V2_A (neg-risk),
|
|
161
|
+
# and V1 NEG_RISK_ADAPTER (V2 neg-risk delegates settlement to it).
|
|
159
162
|
collateral_token = POLY_USD
|
|
160
163
|
spender_list = [
|
|
161
164
|
(CTF_EXCHANGE_V2, "ctf_exchange_v2"),
|
|
162
165
|
(NEG_RISK_CTF_EXCHANGE_V2_A, "neg_risk_ctf_exchange_v2_a"),
|
|
163
|
-
(
|
|
166
|
+
(NEG_RISK_ADAPTER, "neg_risk_adapter"),
|
|
164
167
|
]
|
|
165
168
|
else:
|
|
166
169
|
collateral_token = USDC
|
|
@@ -284,8 +287,12 @@ async def set_approvals(
|
|
|
284
287
|
return tx_hashes
|
|
285
288
|
|
|
286
289
|
|
|
287
|
-
async def check_balance(
|
|
288
|
-
|
|
290
|
+
async def check_balance(
|
|
291
|
+
funder_address: str,
|
|
292
|
+
rpc_url: str,
|
|
293
|
+
version: ExchangeVersion = ExchangeVersion.V1,
|
|
294
|
+
) -> BalanceInfo:
|
|
295
|
+
"""Check collateral and MATIC balance. V1 reads USDC.e, V2 reads pUSD."""
|
|
289
296
|
try:
|
|
290
297
|
from web3 import Web3
|
|
291
298
|
except ImportError:
|
|
@@ -294,10 +301,11 @@ async def check_balance(funder_address: str, rpc_url: str) -> BalanceInfo:
|
|
|
294
301
|
w3 = Web3(Web3.HTTPProvider(rpc_url))
|
|
295
302
|
erc20_abi = [{"inputs": [{"name": "account", "type": "address"}], "name": "balanceOf", "outputs": [{"name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"}]
|
|
296
303
|
|
|
297
|
-
|
|
304
|
+
collateral_token = POLY_USD if version == ExchangeVersion.V2 else USDC
|
|
305
|
+
token_contract = w3.eth.contract(address=Web3.to_checksum_address(collateral_token), abi=erc20_abi)
|
|
298
306
|
funder = Web3.to_checksum_address(funder_address)
|
|
299
307
|
|
|
300
|
-
usdc_raw =
|
|
308
|
+
usdc_raw = token_contract.functions.balanceOf(funder).call()
|
|
301
309
|
matic_raw = w3.eth.get_balance(funder)
|
|
302
310
|
|
|
303
311
|
return BalanceInfo(
|
polynode/trading/trader.py
CHANGED
|
@@ -12,8 +12,10 @@ from .clob_api import (
|
|
|
12
12
|
cancel_order as clob_cancel_order,
|
|
13
13
|
fetch_neg_risk,
|
|
14
14
|
fetch_tick_size,
|
|
15
|
+
get_balance_allowance as clob_get_balance_allowance,
|
|
15
16
|
get_open_orders as clob_get_open_orders,
|
|
16
17
|
post_order,
|
|
18
|
+
refresh_balance_allowance as clob_refresh_balance_allowance,
|
|
17
19
|
)
|
|
18
20
|
from .constants import (
|
|
19
21
|
COLLATERAL_OFFRAMP,
|
|
@@ -172,6 +174,26 @@ class PolyNodeTrader:
|
|
|
172
174
|
self._active_signer = normalized
|
|
173
175
|
self._active_wallet = normalized.address
|
|
174
176
|
|
|
177
|
+
# V2: force the CLOB to refresh its cached balance/allowance view. Without this,
|
|
178
|
+
# orders get rejected with "not enough balance / allowance" until the CLOB
|
|
179
|
+
# eventually notices on-chain approvals on its own.
|
|
180
|
+
if self._exchange_version == ExchangeVersion.V2 and any(
|
|
181
|
+
a.startswith("approvals") and "already" not in a for a in actions
|
|
182
|
+
):
|
|
183
|
+
try:
|
|
184
|
+
ok, _ = await clob_refresh_balance_allowance(
|
|
185
|
+
{
|
|
186
|
+
"apiKey": stored.api_key,
|
|
187
|
+
"apiSecret": stored.api_secret,
|
|
188
|
+
"apiPassphrase": stored.api_passphrase,
|
|
189
|
+
},
|
|
190
|
+
normalized.address,
|
|
191
|
+
int(sig_type),
|
|
192
|
+
)
|
|
193
|
+
actions.append("clob_cache_refreshed" if ok else "clob_cache_refresh_failed")
|
|
194
|
+
except Exception:
|
|
195
|
+
actions.append("clob_cache_refresh_failed")
|
|
196
|
+
|
|
175
197
|
return ReadyStatus(
|
|
176
198
|
wallet=normalized.address,
|
|
177
199
|
funder_address=funder,
|
|
@@ -343,7 +365,55 @@ class PolyNodeTrader:
|
|
|
343
365
|
|
|
344
366
|
async def check_balance(self, wallet: str | None = None) -> BalanceInfo:
|
|
345
367
|
creds = self._get_stored_creds(wallet)
|
|
346
|
-
return await check_balance_onchain(
|
|
368
|
+
return await check_balance_onchain(
|
|
369
|
+
creds.funder_address or creds.wallet_address,
|
|
370
|
+
self._rpc_url,
|
|
371
|
+
self._exchange_version,
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
async def refresh_balance_allowance(
|
|
375
|
+
self,
|
|
376
|
+
asset_type: str = "COLLATERAL",
|
|
377
|
+
wallet: str | None = None,
|
|
378
|
+
) -> tuple[bool, int]:
|
|
379
|
+
"""Ask the V2 CLOB to refresh its cached balance/allowance view.
|
|
380
|
+
|
|
381
|
+
No-op for V1. Call after changing on-chain approvals — without this,
|
|
382
|
+
the CLOB rejects orders with "not enough balance / allowance".
|
|
383
|
+
"""
|
|
384
|
+
if self._exchange_version != ExchangeVersion.V2:
|
|
385
|
+
return True, 0
|
|
386
|
+
creds = self._get_stored_creds(wallet)
|
|
387
|
+
return await clob_refresh_balance_allowance(
|
|
388
|
+
{
|
|
389
|
+
"apiKey": creds.api_key,
|
|
390
|
+
"apiSecret": creds.api_secret,
|
|
391
|
+
"apiPassphrase": creds.api_passphrase,
|
|
392
|
+
},
|
|
393
|
+
creds.wallet_address,
|
|
394
|
+
int(creds.signature_type),
|
|
395
|
+
asset_type,
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
async def get_balance_allowance(
|
|
399
|
+
self,
|
|
400
|
+
asset_type: str = "COLLATERAL",
|
|
401
|
+
wallet: str | None = None,
|
|
402
|
+
) -> dict[str, Any] | None:
|
|
403
|
+
"""Read the V2 CLOB's current cached balance + allowance view. None for V1."""
|
|
404
|
+
if self._exchange_version != ExchangeVersion.V2:
|
|
405
|
+
return None
|
|
406
|
+
creds = self._get_stored_creds(wallet)
|
|
407
|
+
return await clob_get_balance_allowance(
|
|
408
|
+
{
|
|
409
|
+
"apiKey": creds.api_key,
|
|
410
|
+
"apiSecret": creds.api_secret,
|
|
411
|
+
"apiPassphrase": creds.api_passphrase,
|
|
412
|
+
},
|
|
413
|
+
creds.wallet_address,
|
|
414
|
+
int(creds.signature_type),
|
|
415
|
+
asset_type,
|
|
416
|
+
)
|
|
347
417
|
|
|
348
418
|
# ── PolyUSD Wrap / Unwrap ──
|
|
349
419
|
|
|
@@ -540,6 +610,7 @@ class PolyNodeTrader:
|
|
|
540
610
|
signature_type=creds.signature_type,
|
|
541
611
|
tick_size=meta.tick_size,
|
|
542
612
|
neg_risk=meta.neg_risk,
|
|
613
|
+
builder=params.builder or "0x" + "00" * 32,
|
|
543
614
|
)
|
|
544
615
|
else:
|
|
545
616
|
signed_order = await create_signed_order(
|
|
@@ -557,12 +628,16 @@ class PolyNodeTrader:
|
|
|
557
628
|
expiration=params.expiration or 0,
|
|
558
629
|
)
|
|
559
630
|
|
|
560
|
-
payload = {
|
|
631
|
+
payload: dict[str, Any] = {
|
|
561
632
|
"order": signed_order,
|
|
562
633
|
"owner": creds.api_key,
|
|
563
634
|
"orderType": params.type,
|
|
564
635
|
}
|
|
565
|
-
if
|
|
636
|
+
if self._exchange_version == ExchangeVersion.V2:
|
|
637
|
+
# V2 CLOB wire format requires these top-level fields (matches @polymarket/clob-client-v2).
|
|
638
|
+
payload["postOnly"] = bool(params.post_only)
|
|
639
|
+
payload["deferExec"] = False
|
|
640
|
+
elif params.post_only:
|
|
566
641
|
payload["postOnly"] = True
|
|
567
642
|
|
|
568
643
|
body_str = json.dumps(payload, separators=(",", ":"))
|
polynode/trading/types.py
CHANGED
|
@@ -137,6 +137,9 @@ class OrderParams:
|
|
|
137
137
|
expiration: int | None = None
|
|
138
138
|
post_only: bool = False
|
|
139
139
|
fee_config: FeeConfig | None = None
|
|
140
|
+
# V2 builder attribution bytes32. Mint at polymarket.com/settings?tab=builder.
|
|
141
|
+
# Defaults to zero (no attribution). Ignored on V1.
|
|
142
|
+
builder: str | None = None
|
|
140
143
|
|
|
141
144
|
|
|
142
145
|
@dataclass
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: polynode
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.0
|
|
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
|
|
@@ -109,10 +109,15 @@ asyncio.run(main())
|
|
|
109
109
|
|
|
110
110
|
```python
|
|
111
111
|
import asyncio
|
|
112
|
-
from polynode.trading import PolyNodeTrader, TraderConfig, OrderParams
|
|
112
|
+
from polynode.trading import PolyNodeTrader, TraderConfig, OrderParams, ExchangeVersion
|
|
113
113
|
|
|
114
114
|
async def main():
|
|
115
|
-
|
|
115
|
+
# Default is V1 (USDC.e). Pass exchange_version=ExchangeVersion.V2 for the V2 CLOB
|
|
116
|
+
# (pUSD collateral, clob-v2.polymarket.com, builder attribution).
|
|
117
|
+
trader = PolyNodeTrader(TraderConfig(
|
|
118
|
+
polynode_key="pn_live_...",
|
|
119
|
+
exchange_version=ExchangeVersion.V2,
|
|
120
|
+
))
|
|
116
121
|
status = await trader.ensure_ready("0xYourPrivateKey...")
|
|
117
122
|
|
|
118
123
|
result = await trader.order(OrderParams(
|
|
@@ -120,6 +125,7 @@ async def main():
|
|
|
120
125
|
side="BUY",
|
|
121
126
|
price=0.55,
|
|
122
127
|
size=100,
|
|
128
|
+
builder="0x<your_builder_code_bytes32>", # V2 only; omit for V1
|
|
123
129
|
))
|
|
124
130
|
print(result)
|
|
125
131
|
|
|
@@ -128,6 +134,8 @@ async def main():
|
|
|
128
134
|
asyncio.run(main())
|
|
129
135
|
```
|
|
130
136
|
|
|
137
|
+
For the V2 order flow — required approvals, EIP-712 struct, fee math, and common failure modes — see `polynode/trading/V2_ORDER_FLOW.md` in the installed package.
|
|
138
|
+
|
|
131
139
|
## Documentation
|
|
132
140
|
|
|
133
141
|
Full docs at [docs.polynode.dev](https://docs.polynode.dev)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
polynode/__init__.py,sha256=ucqXzFPMdEcfGEAdbgOeJczREEiSCNBX1G3Il7y8-UI,1127
|
|
2
|
-
polynode/_version.py,sha256=
|
|
2
|
+
polynode/_version.py,sha256=H9NWRZb7NbeRRPLP_V1fARmLNXranorVM-OOY-8_2ug,22
|
|
3
3
|
polynode/client.py,sha256=eSCKpuZFg2ssQC5tmsfRYF3cTqCk1Gdw4KPjbd5MFHQ,21831
|
|
4
4
|
polynode/engine.py,sha256=DSXBmx3DXPh-PN0ihkXn7LSiyxeOoo0iY3z4YinQ3XI,6488
|
|
5
5
|
polynode/errors.py,sha256=5qUCI7K76VKbF5R5G5UYhPzcnOPlGLCA8-sJnKbrK-4,925
|
|
@@ -11,19 +11,20 @@ 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/V2_ORDER_FLOW.md,sha256=Ppd7iwZjtmVwok-fP4Ofo4UBQ9pIAg0t57RJ-OzqRHs,10926
|
|
14
15
|
polynode/trading/__init__.py,sha256=sPv30j9eyvqjnUBxGAYaKpAPQZ2jTw3UwA4DZXYZ21s,923
|
|
15
|
-
polynode/trading/clob_api.py,sha256=
|
|
16
|
-
polynode/trading/constants.py,sha256=
|
|
16
|
+
polynode/trading/clob_api.py,sha256=Sfb8XLN9MMzVE6jARNqEQxiD608OC_6Xjc7uiJbDQEc,7274
|
|
17
|
+
polynode/trading/constants.py,sha256=aW8F9Q2W0TRNVX0JNh39k0qxbb03ijUAGbWjrJFjf_o,2513
|
|
17
18
|
polynode/trading/cosigner.py,sha256=Fww9OgaKdHQAHd8Kom1AJBtmMGC8FbNZxdFaFALNBY8,3058
|
|
18
|
-
polynode/trading/eip712.py,sha256=
|
|
19
|
+
polynode/trading/eip712.py,sha256=PKLfMt58Diy419jiQXysnX1kSlU_QcWDRofGEb3u_JI,9744
|
|
19
20
|
polynode/trading/escrow.py,sha256=bU_ohyOlx2r6KjBYxUNdjROvrmunWV1Ey99AuSOt9Z8,4590
|
|
20
|
-
polynode/trading/onboarding.py,sha256
|
|
21
|
+
polynode/trading/onboarding.py,sha256=oXtFz1HF1vGvMnb5o44SwuADd2mdU5ws1v7n30rk6fg,15066
|
|
21
22
|
polynode/trading/position_management.py,sha256=z2pYls4ErhyRE6thQdOs45BF4llKvKNNruQgcMEFT_U,3827
|
|
22
23
|
polynode/trading/privy.py,sha256=iiUQZsBqjj29C7oUiFFktoT2muZY06bcMzABKstFKqE,4523
|
|
23
24
|
polynode/trading/signer.py,sha256=piifRquwnwFwrKLL24jsRQYMh_cdkz8wNz99Vx8_KTY,3413
|
|
24
25
|
polynode/trading/sqlite_backend.py,sha256=zfo4WevuunWV-7i6Z3gQi_e7Tonw-7m1TsEd88O5I4w,9486
|
|
25
|
-
polynode/trading/trader.py,sha256=
|
|
26
|
-
polynode/trading/types.py,sha256=
|
|
26
|
+
polynode/trading/trader.py,sha256=VpOwZ0TEwkOSRSLSAZ1LxjEJfxPkH3RO-XFrH6RG5Ys,34845
|
|
27
|
+
polynode/trading/types.py,sha256=mo5zJRlfQLWuXwrkS9AaGqKWrt7FMf6bPGRE-0T62J0,5698
|
|
27
28
|
polynode/types/__init__.py,sha256=0v_CByqG-zT1xucbT2So_77YDmsPj9u956at94nyrLM,280
|
|
28
29
|
polynode/types/enums.py,sha256=2cx9l10A-LUeDNBJZn7l0XBTka2YfxRQcXW0s7s3_4k,1082
|
|
29
30
|
polynode/types/events.py,sha256=MJGZqxBFKfT1y44dZ96bHbwvoPpGvtutBwYE6OTT6nU,6880
|
|
@@ -31,6 +32,6 @@ polynode/types/orderbook.py,sha256=MwD6P3poo3F70bONUZ5i3ajk1sgw39hHTOw6D0fP5_8,1
|
|
|
31
32
|
polynode/types/rest.py,sha256=ftJKQZH0wBrR_l199rDMqnVS8cyt8YnCvo9cKIsTcCw,7012
|
|
32
33
|
polynode/types/short_form.py,sha256=xfGklDi587u6K2aXaRWiwbxqkipW8BiEPeNIDT45v2Y,799
|
|
33
34
|
polynode/types/ws.py,sha256=EzE9Vzrb6cILemfssmnqM8fbHt4USn3latpUHKatwk4,994
|
|
34
|
-
polynode-0.
|
|
35
|
-
polynode-0.
|
|
36
|
-
polynode-0.
|
|
35
|
+
polynode-0.9.0.dist-info/METADATA,sha256=mzGKeGUGX-EvRNytrdz2FS7uq-N4-m944fpaW2GujoU,3777
|
|
36
|
+
polynode-0.9.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
37
|
+
polynode-0.9.0.dist-info/RECORD,,
|
|
File without changes
|