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 CHANGED
@@ -1 +1 @@
1
- __version__ = "0.7.4"
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
+ ```
@@ -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
- return bool(data.get("neg_risk", data) if isinstance(data, dict) else data)
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()
@@ -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
- V2_SPENDERS = [CTF_EXCHANGE_V2, NEG_RISK_CTF_EXCHANGE_V2_A, NEG_RISK_CTF_EXCHANGE_V2_B]
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"
@@ -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,
@@ -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
- (NEG_RISK_CTF_EXCHANGE_V2_B, "neg_risk_ctf_exchange_v2_b"),
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(funder_address: str, rpc_url: str) -> BalanceInfo:
288
- """Check USDC and MATIC balance."""
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
- usdc_contract = w3.eth.contract(address=Web3.to_checksum_address(USDC), abi=erc20_abi)
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 = usdc_contract.functions.balanceOf(funder).call()
308
+ usdc_raw = token_contract.functions.balanceOf(funder).call()
301
309
  matic_raw = w3.eth.get_balance(funder)
302
310
 
303
311
  return BalanceInfo(
@@ -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(creds.funder_address or creds.wallet_address, self._rpc_url)
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 params.post_only:
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.7.4
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
- trader = PolyNodeTrader(TraderConfig(polynode_key="pn_live_..."))
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=A6fZ_oURo3l_Fa_K29LgV21A4Onqu3NquwGYzL05E1Y,22
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=ygaOoMIg4PGtMzNBT70wk4XrSbVX0EyCCU6PmXK3xrs,5308
16
- polynode/trading/constants.py,sha256=_DDwutJkOfeyPkClrTcyJCOYMoqDymZzDpjYWeHbfG0,1862
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=pZf0Pf4DDIPpK4qbz4DWfcdOeU8PlCQhJPpKSQ97pC4,9553
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=-Ub5tsJA4vqPJmkNLx2OE_szUY08UmXYi_9ATThajkA,14670
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=eAdFhZz0KLr_oXBb5LsW7JHUuNCSfZuWaCn4XIqVByM,31798
26
- polynode/trading/types.py,sha256=JxhjsRRhh8WA127VNNPLf7QJKn5e_PZucaG7WAfdOsM,5528
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.7.4.dist-info/METADATA,sha256=YMkUQnSt2Vpc8Sd7vIlttfD8oenHGDk_xKHMqpf2JEg,3302
35
- polynode-0.7.4.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
36
- polynode-0.7.4.dist-info/RECORD,,
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,,