polynode 0.12.1__tar.gz → 0.13.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. {polynode-0.12.1 → polynode-0.13.0}/PKG-INFO +55 -8
  2. {polynode-0.12.1 → polynode-0.13.0}/README.md +51 -4
  3. polynode-0.13.0/polynode/_version.py +1 -0
  4. {polynode-0.12.1 → polynode-0.13.0}/polynode/trading/V2_ORDER_FLOW.md +9 -14
  5. polynode-0.13.0/polynode/trading/__init__.py +70 -0
  6. {polynode-0.12.1 → polynode-0.13.0}/polynode/trading/clob_api.py +37 -13
  7. {polynode-0.12.1 → polynode-0.13.0}/polynode/trading/constants.py +20 -13
  8. polynode-0.13.0/polynode/trading/cosigner.py +190 -0
  9. {polynode-0.12.1 → polynode-0.13.0}/polynode/trading/onboarding.py +373 -75
  10. polynode-0.13.0/polynode/trading/position_management.py +226 -0
  11. polynode-0.13.0/polynode/trading/relayer.py +731 -0
  12. {polynode-0.12.1 → polynode-0.13.0}/polynode/trading/signer.py +27 -7
  13. polynode-0.13.0/polynode/trading/trader.py +1705 -0
  14. {polynode-0.12.1 → polynode-0.13.0}/polynode/trading/types.py +56 -5
  15. polynode-0.13.0/polynode/trading/user_relayer.py +321 -0
  16. {polynode-0.12.1 → polynode-0.13.0}/polynode/types/rest.py +1 -1
  17. {polynode-0.12.1 → polynode-0.13.0}/pyproject.toml +4 -3
  18. polynode-0.12.1/polynode/_version.py +0 -1
  19. polynode-0.12.1/polynode/trading/__init__.py +0 -30
  20. polynode-0.12.1/polynode/trading/cosigner.py +0 -95
  21. polynode-0.12.1/polynode/trading/position_management.py +0 -104
  22. polynode-0.12.1/polynode/trading/relayer.py +0 -422
  23. polynode-0.12.1/polynode/trading/trader.py +0 -936
  24. {polynode-0.12.1 → polynode-0.13.0}/.gitignore +0 -0
  25. {polynode-0.12.1 → polynode-0.13.0}/core-contract-v1.json +0 -0
  26. {polynode-0.12.1 → polynode-0.13.0}/core-fixtures-v1.json +0 -0
  27. {polynode-0.12.1 → polynode-0.13.0}/polynode/__init__.py +0 -0
  28. {polynode-0.12.1 → polynode-0.13.0}/polynode/cache/__init__.py +0 -0
  29. {polynode-0.12.1 → polynode-0.13.0}/polynode/client.py +0 -0
  30. {polynode-0.12.1 → polynode-0.13.0}/polynode/engine.py +0 -0
  31. {polynode-0.12.1 → polynode-0.13.0}/polynode/errors.py +0 -0
  32. {polynode-0.12.1 → polynode-0.13.0}/polynode/orderbook.py +0 -0
  33. {polynode-0.12.1 → polynode-0.13.0}/polynode/orderbook_integrity.py +0 -0
  34. {polynode-0.12.1 → polynode-0.13.0}/polynode/orderbook_state.py +0 -0
  35. {polynode-0.12.1 → polynode-0.13.0}/polynode/perps.py +0 -0
  36. {polynode-0.12.1 → polynode-0.13.0}/polynode/redemption_watcher.py +0 -0
  37. {polynode-0.12.1 → polynode-0.13.0}/polynode/short_form.py +0 -0
  38. {polynode-0.12.1 → polynode-0.13.0}/polynode/subscription.py +0 -0
  39. {polynode-0.12.1 → polynode-0.13.0}/polynode/testing.py +0 -0
  40. {polynode-0.12.1 → polynode-0.13.0}/polynode/trading/eip712.py +0 -0
  41. {polynode-0.12.1 → polynode-0.13.0}/polynode/trading/escrow.py +0 -0
  42. {polynode-0.12.1 → polynode-0.13.0}/polynode/trading/privy.py +0 -0
  43. {polynode-0.12.1 → polynode-0.13.0}/polynode/trading/sqlite_backend.py +0 -0
  44. {polynode-0.12.1 → polynode-0.13.0}/polynode/types/__init__.py +0 -0
  45. {polynode-0.12.1 → polynode-0.13.0}/polynode/types/enums.py +0 -0
  46. {polynode-0.12.1 → polynode-0.13.0}/polynode/types/events.py +0 -0
  47. {polynode-0.12.1 → polynode-0.13.0}/polynode/types/orderbook.py +0 -0
  48. {polynode-0.12.1 → polynode-0.13.0}/polynode/types/perps.py +0 -0
  49. {polynode-0.12.1 → polynode-0.13.0}/polynode/types/short_form.py +0 -0
  50. {polynode-0.12.1 → polynode-0.13.0}/polynode/types/ws.py +0 -0
  51. {polynode-0.12.1 → polynode-0.13.0}/polynode/v3.py +0 -0
  52. {polynode-0.12.1 → polynode-0.13.0}/polynode/v3_operations.py +0 -0
  53. {polynode-0.12.1 → polynode-0.13.0}/polynode/ws.py +0 -0
@@ -1,11 +1,11 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: polynode
3
- Version: 0.12.1
4
- Summary: Python SDK for the PolyNode real-time prediction market data platform
3
+ Version: 0.13.0
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
7
7
  Project-URL: Repository, https://github.com/Polynode-Dev/sdk-python
8
- Author-email: PolyNode <josh@quantish.live>
8
+ Author-email: Polynode <josh@quantish.live>
9
9
  License: MIT
10
10
  Keywords: polymarket,polynode,prediction-markets,trading,websocket
11
11
  Classifier: Development Status :: 4 - Beta
@@ -34,11 +34,13 @@ Description-Content-Type: text/markdown
34
34
 
35
35
  # polynode
36
36
 
37
- Python SDK for the [PolyNode](https://polynode.dev) real-time prediction market data platform.
37
+ Python SDK for the [Polynode](https://polynode.dev) real-time prediction market data platform.
38
38
 
39
- **Current in v0.12.1:** Python provides the same core capabilities as the TypeScript and Rust SDKs: complete V3 API access, the V3 perps WebSocket, reconnect-aware settlement delivery, and PN1 orderbook integrity. Unknown additive events remain available as raw payloads, decimal values remain precision-safe, and any local queue eviction is reported.
39
+ **New in v0.13.0:** Trading adds explicit `user_owned` execution. Existing builder mode remains the default; opted-in wallets use zero builder attribution, one wallet-ownership authorization, and strict wallet-bound gasless credentials. Builder credentials and nonzero builder codes fail closed in this mode. EOA-controlled Safe and deposit wallets are supported; legacy Magic/proxy wallets are intentionally excluded from the first release.
40
40
 
41
- **New in v0.11.0:** Current-production parity. Trading now defaults to CLOB V2 on `clob.polymarket.com`, uses PolyNode's public builder attribution unless overridden, omits removed V1 wire fields, and supports V2 GTD expiration. Managed 5-minute, 15-minute, and 4-hour streams select the required 30/60-second Chainlink TWAP lookbacks on a dedicated connection and reconnect/resubscribe at every market rotation. WebSocket models, presets, and filters now cover current redemption, position-conversion, dome/fill, and PM2 combo events. REST position queries now include redeemable/condition filters, multi-wallet batches, and market-holder views; connection and status observability match the current public API.
41
+ **In v0.12.2:** Python provides the same core capabilities as the TypeScript and Rust SDKs: complete V3 API access, the V3 perps WebSocket, reconnect-aware settlement delivery, and PN1 orderbook integrity. Unknown additive events remain available as raw payloads, decimal values remain precision-safe, and any local queue eviction is reported.
42
+
43
+ **New in v0.11.0:** Current-production parity. Trading now defaults to CLOB V2 on `clob.polymarket.com`, uses Polynode's public builder attribution unless overridden, omits removed V1 wire fields, and supports V2 GTD expiration. Managed 5-minute, 15-minute, and 4-hour streams select the required 30/60-second Chainlink TWAP lookbacks on a dedicated connection and reconnect/resubscribe at every market rotation. WebSocket models, presets, and filters now cover current redemption, position-conversion, dome/fill, and PM2 combo events. REST position queries now include redeemable/condition filters, multi-wallet batches, and market-holder views; connection and status observability match the current public API.
42
44
 
43
45
  **New in v0.10.8:** POLY_1271 V2 order signatures now normalize the ERC-7739 `TypedDataSign` recovery byte to Ethereum `v=27/28` for on-chain ERC-1271 validation.
44
46
 
@@ -273,7 +275,7 @@ async def main():
273
275
  trader = PolyNodeTrader(TraderConfig(
274
276
  polynode_key="pn_live_...",
275
277
  # exchange_version=ExchangeVersion.V2,
276
- # builder_code=None, # disables default public PolyNode attribution
278
+ # builder_code=None, # disables default public Polynode attribution
277
279
  ))
278
280
  status = await trader.ensure_ready("0xYourPrivateKey...")
279
281
 
@@ -295,6 +297,51 @@ For the V2 order flow — required approvals, EIP-712 struct, fee math, and comm
295
297
 
296
298
  V2 fees are determined at match time and are not signed into an order, so V2 payloads omit `feeRateBps`, `nonce`, and `taker`. Explicit legacy V1 mode still signs `feeRateBps`; for that path the SDK fetches `/fee-rate` and fails closed if fee, tick-size, or neg-risk metadata is unavailable or malformed.
297
299
 
300
+ #### Optional user-owned execution
301
+
302
+ Set `execution_mode=ExecutionMode.USER_OWNED` when the signing wallet should trade with zero builder attribution and use its own gasless authorization. Builder mode remains the default and existing integrations are unchanged.
303
+
304
+ ```python
305
+ import os
306
+ from polynode.trading import (
307
+ ExecutionMode,
308
+ PolyNodeTrader,
309
+ TraderConfig,
310
+ UserOwnedClobTransport,
311
+ )
312
+
313
+ trader = PolyNodeTrader(TraderConfig(
314
+ polynode_key=os.environ["POLYNODE_API_KEY"],
315
+ execution_mode=ExecutionMode.USER_OWNED,
316
+ # Optional, explicit regional egress; direct is the default.
317
+ # user_owned_clob_transport=UserOwnedClobTransport.PROXY,
318
+ ))
319
+ ready = await trader.ensure_ready(user_wallet_signer)
320
+ print(ready.execution_mode, ready.user_relayer_authorized)
321
+ ```
322
+
323
+ `user_wallet_signer` is a caller-controlled `RouterSigner`; the SDK asks it to sign scoped messages and never persists or transmits its private key. A private-key string is also accepted when the caller already manages it inside a trusted process. User-owned mode rejects builder credentials, nonzero builder codes, and credentials owned by another wallet. It supports EOA signers and EOA-controlled Safe or deposit wallets; legacy `POLY_PROXY` and Magic/DID signers are not supported in the first release. Normal CLOB authentication and Polymarket rate limits still apply.
324
+
325
+ Signed CLOB requests go directly to Polymarket by default. Platforms that need Polynode's regional egress can explicitly set `user_owned_clob_transport=UserOwnedClobTransport.PROXY`; this transport never activates automatically and never falls back between paths. Fee-authenticated orders are not available in user-owned mode, and any positive effective `fee_bps` is rejected before an order is signed or submitted.
326
+
327
+ For browser-wallet integrations, the typed `begin_user_relayer_authorization()` and `complete_user_relayer_authorization()` functions let a trusted backend request a validated message, send only that message to the user's browser for signing, and complete authorization for the same expected address. Keep the Polynode API key on the backend. Neither primitive writes the wallet signature or returned credential to local storage.
328
+
329
+ For long-running services, `await trader.authorize_user_owned_execution(signer)` provides the combined convenience flow. Its wallet-owned credential can be kept in a server-side secret manager and supplied later as `TraderConfig.user_relayer_credentials`. Never log it, commit it, or store it in a browser.
330
+
331
+ `ensure_ready()` is the one-call onboarding path for user-owned Safe and deposit-wallet accounts: it deploys the selected wallet when needed, applies the base trading approvals, verifies both results, and only then reports the account ready. New deposit-wallet integrations must resolve the current address asynchronously:
332
+
333
+ ```python
334
+ from polynode.trading import resolve_deposit_wallet_address
335
+
336
+ funder = await resolve_deposit_wallet_address("0xYourEOA...")
337
+ ```
338
+
339
+ The synchronous `derive_deposit_wallet_address()` helper is deprecated and derives only the legacy UUPS address; do not use it to bind a new or live deposit-wallet account. For the same reason, synchronous `link_credentials()` and `import_wallet()` reject deposit wallets in user-owned mode; use `await link_wallet()` or `await ensure_ready()` so the current address is resolved before anything is stored.
340
+
341
+ `trader.split(SplitParams(...))` and `trader.merge(MergeParams(...))` preserve the synchronous build-only API and return a `TransactionRequest` without signing or submitting anything. In user-owned mode, `await trader.execute_split(SplitParams(...))` and `await trader.execute_merge(MergeParams(...))` execute the corresponding gasless operation for EOA-controlled Safe and deposit-wallet accounts. Supply either an explicit `neg_risk` boolean or a `token_id` for fail-closed execution routing; V2 uses the appropriate collateral adapter automatically. Each executed operation batches only the additional permission it needs; split allowance is limited to the exact split amount.
342
+
343
+ `await trader.wrap_to_polyusd(...)` and `await trader.unwrap_from_polyusd(...)` also use the account's wallet-specific gasless path for Safe and deposit-wallet accounts. Caller-controlled browser or HSM signers remain supported; these smart-wallet operations do not require handing a private key to the SDK.
344
+
298
345
  ## Documentation
299
346
 
300
347
  Full docs at [docs.polynode.dev](https://docs.polynode.dev)
@@ -1,10 +1,12 @@
1
1
  # polynode
2
2
 
3
- Python SDK for the [PolyNode](https://polynode.dev) real-time prediction market data platform.
3
+ Python SDK for the [Polynode](https://polynode.dev) real-time prediction market data platform.
4
4
 
5
- **Current in v0.12.1:** Python provides the same core capabilities as the TypeScript and Rust SDKs: complete V3 API access, the V3 perps WebSocket, reconnect-aware settlement delivery, and PN1 orderbook integrity. Unknown additive events remain available as raw payloads, decimal values remain precision-safe, and any local queue eviction is reported.
5
+ **New in v0.13.0:** Trading adds explicit `user_owned` execution. Existing builder mode remains the default; opted-in wallets use zero builder attribution, one wallet-ownership authorization, and strict wallet-bound gasless credentials. Builder credentials and nonzero builder codes fail closed in this mode. EOA-controlled Safe and deposit wallets are supported; legacy Magic/proxy wallets are intentionally excluded from the first release.
6
6
 
7
- **New in v0.11.0:** Current-production parity. Trading now defaults to CLOB V2 on `clob.polymarket.com`, uses PolyNode's public builder attribution unless overridden, omits removed V1 wire fields, and supports V2 GTD expiration. Managed 5-minute, 15-minute, and 4-hour streams select the required 30/60-second Chainlink TWAP lookbacks on a dedicated connection and reconnect/resubscribe at every market rotation. WebSocket models, presets, and filters now cover current redemption, position-conversion, dome/fill, and PM2 combo events. REST position queries now include redeemable/condition filters, multi-wallet batches, and market-holder views; connection and status observability match the current public API.
7
+ **In v0.12.2:** Python provides the same core capabilities as the TypeScript and Rust SDKs: complete V3 API access, the V3 perps WebSocket, reconnect-aware settlement delivery, and PN1 orderbook integrity. Unknown additive events remain available as raw payloads, decimal values remain precision-safe, and any local queue eviction is reported.
8
+
9
+ **New in v0.11.0:** Current-production parity. Trading now defaults to CLOB V2 on `clob.polymarket.com`, uses Polynode's public builder attribution unless overridden, omits removed V1 wire fields, and supports V2 GTD expiration. Managed 5-minute, 15-minute, and 4-hour streams select the required 30/60-second Chainlink TWAP lookbacks on a dedicated connection and reconnect/resubscribe at every market rotation. WebSocket models, presets, and filters now cover current redemption, position-conversion, dome/fill, and PM2 combo events. REST position queries now include redeemable/condition filters, multi-wallet batches, and market-holder views; connection and status observability match the current public API.
8
10
 
9
11
  **New in v0.10.8:** POLY_1271 V2 order signatures now normalize the ERC-7739 `TypedDataSign` recovery byte to Ethereum `v=27/28` for on-chain ERC-1271 validation.
10
12
 
@@ -239,7 +241,7 @@ async def main():
239
241
  trader = PolyNodeTrader(TraderConfig(
240
242
  polynode_key="pn_live_...",
241
243
  # exchange_version=ExchangeVersion.V2,
242
- # builder_code=None, # disables default public PolyNode attribution
244
+ # builder_code=None, # disables default public Polynode attribution
243
245
  ))
244
246
  status = await trader.ensure_ready("0xYourPrivateKey...")
245
247
 
@@ -261,6 +263,51 @@ For the V2 order flow — required approvals, EIP-712 struct, fee math, and comm
261
263
 
262
264
  V2 fees are determined at match time and are not signed into an order, so V2 payloads omit `feeRateBps`, `nonce`, and `taker`. Explicit legacy V1 mode still signs `feeRateBps`; for that path the SDK fetches `/fee-rate` and fails closed if fee, tick-size, or neg-risk metadata is unavailable or malformed.
263
265
 
266
+ #### Optional user-owned execution
267
+
268
+ Set `execution_mode=ExecutionMode.USER_OWNED` when the signing wallet should trade with zero builder attribution and use its own gasless authorization. Builder mode remains the default and existing integrations are unchanged.
269
+
270
+ ```python
271
+ import os
272
+ from polynode.trading import (
273
+ ExecutionMode,
274
+ PolyNodeTrader,
275
+ TraderConfig,
276
+ UserOwnedClobTransport,
277
+ )
278
+
279
+ trader = PolyNodeTrader(TraderConfig(
280
+ polynode_key=os.environ["POLYNODE_API_KEY"],
281
+ execution_mode=ExecutionMode.USER_OWNED,
282
+ # Optional, explicit regional egress; direct is the default.
283
+ # user_owned_clob_transport=UserOwnedClobTransport.PROXY,
284
+ ))
285
+ ready = await trader.ensure_ready(user_wallet_signer)
286
+ print(ready.execution_mode, ready.user_relayer_authorized)
287
+ ```
288
+
289
+ `user_wallet_signer` is a caller-controlled `RouterSigner`; the SDK asks it to sign scoped messages and never persists or transmits its private key. A private-key string is also accepted when the caller already manages it inside a trusted process. User-owned mode rejects builder credentials, nonzero builder codes, and credentials owned by another wallet. It supports EOA signers and EOA-controlled Safe or deposit wallets; legacy `POLY_PROXY` and Magic/DID signers are not supported in the first release. Normal CLOB authentication and Polymarket rate limits still apply.
290
+
291
+ Signed CLOB requests go directly to Polymarket by default. Platforms that need Polynode's regional egress can explicitly set `user_owned_clob_transport=UserOwnedClobTransport.PROXY`; this transport never activates automatically and never falls back between paths. Fee-authenticated orders are not available in user-owned mode, and any positive effective `fee_bps` is rejected before an order is signed or submitted.
292
+
293
+ For browser-wallet integrations, the typed `begin_user_relayer_authorization()` and `complete_user_relayer_authorization()` functions let a trusted backend request a validated message, send only that message to the user's browser for signing, and complete authorization for the same expected address. Keep the Polynode API key on the backend. Neither primitive writes the wallet signature or returned credential to local storage.
294
+
295
+ For long-running services, `await trader.authorize_user_owned_execution(signer)` provides the combined convenience flow. Its wallet-owned credential can be kept in a server-side secret manager and supplied later as `TraderConfig.user_relayer_credentials`. Never log it, commit it, or store it in a browser.
296
+
297
+ `ensure_ready()` is the one-call onboarding path for user-owned Safe and deposit-wallet accounts: it deploys the selected wallet when needed, applies the base trading approvals, verifies both results, and only then reports the account ready. New deposit-wallet integrations must resolve the current address asynchronously:
298
+
299
+ ```python
300
+ from polynode.trading import resolve_deposit_wallet_address
301
+
302
+ funder = await resolve_deposit_wallet_address("0xYourEOA...")
303
+ ```
304
+
305
+ The synchronous `derive_deposit_wallet_address()` helper is deprecated and derives only the legacy UUPS address; do not use it to bind a new or live deposit-wallet account. For the same reason, synchronous `link_credentials()` and `import_wallet()` reject deposit wallets in user-owned mode; use `await link_wallet()` or `await ensure_ready()` so the current address is resolved before anything is stored.
306
+
307
+ `trader.split(SplitParams(...))` and `trader.merge(MergeParams(...))` preserve the synchronous build-only API and return a `TransactionRequest` without signing or submitting anything. In user-owned mode, `await trader.execute_split(SplitParams(...))` and `await trader.execute_merge(MergeParams(...))` execute the corresponding gasless operation for EOA-controlled Safe and deposit-wallet accounts. Supply either an explicit `neg_risk` boolean or a `token_id` for fail-closed execution routing; V2 uses the appropriate collateral adapter automatically. Each executed operation batches only the additional permission it needs; split allowance is limited to the exact split amount.
308
+
309
+ `await trader.wrap_to_polyusd(...)` and `await trader.unwrap_from_polyusd(...)` also use the account's wallet-specific gasless path for Safe and deposit-wallet accounts. Caller-controlled browser or HSM signers remain supported; these smart-wallet operations do not require handing a private key to the SDK.
310
+
264
311
  ## Documentation
265
312
 
266
313
  Full docs at [docs.polynode.dev](https://docs.polynode.dev)
@@ -0,0 +1 @@
1
+ __version__ = "0.13.0"
@@ -6,8 +6,8 @@ Verified 2026-04-21 with matched order `0xaecd1060f7c978d0ab947eacc12a17106b7a5d
6
6
  ## TL;DR
7
7
 
8
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
9
+ 1. Approve pUSD to: CTF_EXCHANGE_V2 and NEG_RISK_EXCHANGE_V2_A
10
+ 2. setApprovalForAll CTF → the same two addresses
11
11
  3. GET /balance-allowance/update — force CLOB cache refresh after approvals
12
12
  4. Sign EIP-712 Order (domain name "Polymarket CTF Exchange", version "2")
13
13
  5. POST /order with L2 HMAC headers
@@ -15,19 +15,15 @@ Verified 2026-04-21 with matched order `0xaecd1060f7c978d0ab947eacc12a17106b7a5d
15
15
 
16
16
  ## Required Approvals (V2 CLOB balance-allowance check)
17
17
 
18
- The V2 CLOB checks **three** pUSD allowances for orders:
18
+ The V2 CLOB checks **two** pUSD allowances for orders:
19
19
 
20
20
  | Address | Name | Why |
21
21
  |---|---|---|
22
22
  | `0xE111180000d2663C0091e4f400237545B87B996B` | `CTF_EXCHANGE_V2` | Standard market orders |
23
23
  | `0xe2222d279d744050d28e00520010520000310F59` | `NEG_RISK_EXCHANGE_V2_A` | Neg-risk market orders |
24
- | `0xd91E80cF2E7be2e162c6513ceD06f1dD0dA35296` | `NegRiskAdapter` (V1) | Settlement delegation |
25
24
 
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
- ```
25
+ The V1 `NegRiskAdapter` is not part of V2 base order readiness. Position adapters
26
+ are approved just in time in the same relayed batch as each split or merge.
31
27
 
32
28
  ## Order Flow Step-by-Step
33
29
 
@@ -37,7 +33,7 @@ For **every** spender above:
37
33
  - `pUSD.approve(spender, MAX_UINT256)`
38
34
  - `CTF.setApprovalForAll(spender, true)` — only needed for SELL orders, but safe to set
39
35
 
40
- Batch all 6 TXs into one Safe multisend via `RelayClient.execute(...)`.
36
+ Batch all 4 TXs into one Safe multisend via `RelayClient.execute(...)`.
41
37
 
42
38
  ### 2. Refresh CLOB balance/allowance cache
43
39
 
@@ -59,12 +55,11 @@ Response:
59
55
  "balance": "2179236",
60
56
  "allowances": {
61
57
  "0xE111180000d2663C0091e4f400237545B87B996B": "115792089...",
62
- "0xd91E80cF2E7be2e162c6513ceD06f1dD0dA35296": "115792089...",
63
58
  "0xe2222d279d744050d28e00520010520000310F59": "115792089..."
64
59
  }
65
60
  }
66
61
  ```
67
- All three allowances must be non-zero.
62
+ Both allowances must be non-zero.
68
63
 
69
64
  ### 3. Determine `neg_risk` and pick exchange address
70
65
 
@@ -218,7 +213,7 @@ The `builderCode` field ties the on-chain trade to the attribution record — th
218
213
 
219
214
  | Error | Cause |
220
215
  |---|---|
221
- | `"not enough balance / allowance"` | (a) V1 NegRiskAdapter not approved for pUSD; (b) balance-allowance cache stale — run `/balance-allowance/update`; (c) balance < notional + fee |
216
+ | `"not enough balance / allowance"` | (a) a current V2 exchange allowance is missing; (b) balance-allowance cache is stale — run `/balance-allowance/update`; (c) balance < notional + fee |
222
217
  | `"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 |
223
218
  | `"invalid amounts, ... max accuracy of N decimals"` | Float precision in amount calc. Use `parseUnits(decimalString, 6)`, not `Math.trunc(x * 1e6)` |
224
219
  | `"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 |
@@ -232,7 +227,7 @@ pUSD : 0xC011a7E12a19f7B1f670d46F03B03f3342E82DFB
232
227
  CTF (ERC-1155) : 0x4D97DCd97eC945f40cF65F87097ACe5EA0476045
233
228
  CTF_EXCHANGE_V2 : 0xe111180000d2663c0091e4f400237545b87b996b
234
229
  NEG_RISK_EXCHANGE_V2_A : 0xe2222d279d744050d28e00520010520000310f59
235
- NegRiskAdapter (V1) : 0xd91E80cF2E7be2e162c6513ceD06f1dD0dA35296
230
+ NegRiskAdapter (V1 only) : 0xd91E80cF2E7be2e162c6513ceD06f1dD0dA35296
236
231
  CtfCollateralAdapter : 0xAdA100Db00Ca00073811820692005400218FcE1f
237
232
  NegRiskCtfCollateralAdapter : 0xadA2005600Dec949baf300f4C6120000bDB6eAab
238
233
 
@@ -0,0 +1,70 @@
1
+ """Trading module — place orders on Polymarket with wallet-owned credentials."""
2
+
3
+ from .constants import * # noqa: F401, F403
4
+ from .cosigner import build_l2_headers, send_via_cosigner
5
+ from .onboarding import (
6
+ derive_beacon_deposit_wallet_address,
7
+ derive_deposit_wallet_address,
8
+ derive_proxy_address,
9
+ derive_safe_address,
10
+ derive_uups_deposit_wallet_address,
11
+ detect_wallet_type,
12
+ resolve_deposit_wallet_address,
13
+ )
14
+ from .position_management import build_convert_txn, build_merge_txn, build_split_txn
15
+ from .privy import PrivyConfig, PrivySigner
16
+ from .signer import NormalizedSigner, normalize_signer
17
+ from .trader import PolyNodeTrader
18
+ from .types import * # noqa: F401, F403
19
+ from .types import (
20
+ BuilderCredentials,
21
+ ExchangeVersion,
22
+ ExecutionMode,
23
+ FeeConfig,
24
+ MergeParams,
25
+ PositionResult,
26
+ SplitParams,
27
+ TransactionRequest,
28
+ UserOwnedClobTransport,
29
+ UserRelayerAuthorizationChallenge,
30
+ UserRelayerCredentials,
31
+ )
32
+ from .user_relayer import (
33
+ authorize_user_relayer,
34
+ begin_user_relayer_authorization,
35
+ complete_user_relayer_authorization,
36
+ )
37
+
38
+ __all__ = [
39
+ "PolyNodeTrader",
40
+ "BuilderCredentials",
41
+ "ExecutionMode",
42
+ "UserOwnedClobTransport",
43
+ "UserRelayerCredentials",
44
+ "UserRelayerAuthorizationChallenge",
45
+ "ExchangeVersion",
46
+ "FeeConfig",
47
+ "SplitParams",
48
+ "MergeParams",
49
+ "TransactionRequest",
50
+ "PositionResult",
51
+ "PrivySigner",
52
+ "PrivyConfig",
53
+ "NormalizedSigner",
54
+ "normalize_signer",
55
+ "build_l2_headers",
56
+ "send_via_cosigner",
57
+ "derive_safe_address",
58
+ "derive_proxy_address",
59
+ "derive_deposit_wallet_address",
60
+ "derive_uups_deposit_wallet_address",
61
+ "derive_beacon_deposit_wallet_address",
62
+ "resolve_deposit_wallet_address",
63
+ "detect_wallet_type",
64
+ "build_split_txn",
65
+ "build_merge_txn",
66
+ "build_convert_txn",
67
+ "authorize_user_relayer",
68
+ "begin_user_relayer_authorization",
69
+ "complete_user_relayer_authorization",
70
+ ]
@@ -4,12 +4,13 @@ from __future__ import annotations
4
4
 
5
5
  import math
6
6
  from typing import Any
7
+ from urllib.parse import urlencode
7
8
 
8
9
  import httpx
9
10
 
10
11
  from .constants import CLOB_HOST, CLOB_HOST_V2
11
12
  from .cosigner import build_l2_headers
12
- from .types import ExchangeVersion
13
+ from .types import ExchangeVersion, ExecutionMode, UserOwnedClobTransport
13
14
 
14
15
 
15
16
  async def post_order(
@@ -21,6 +22,8 @@ async def post_order(
21
22
  order_body: str,
22
23
  builder_credentials: Any | None = None,
23
24
  clob_host: str | None = None,
25
+ execution_mode: ExecutionMode = ExecutionMode.BUILDER,
26
+ user_owned_clob_transport: UserOwnedClobTransport = UserOwnedClobTransport.DIRECT,
24
27
  ) -> dict[str, Any]:
25
28
  """Submit a signed order to the CLOB."""
26
29
  from .cosigner import send_via_cosigner
@@ -42,6 +45,8 @@ async def post_order(
42
45
  {"method": "POST", "path": "/order", "body": order_body, "headers": headers},
43
46
  builder_credentials=builder_credentials,
44
47
  clob_host=clob_host,
48
+ execution_mode=execution_mode,
49
+ user_owned_clob_transport=user_owned_clob_transport,
45
50
  )
46
51
 
47
52
 
@@ -54,11 +59,14 @@ async def cancel_order(
54
59
  order_id: str,
55
60
  builder_credentials: Any | None = None,
56
61
  clob_host: str | None = None,
62
+ execution_mode: ExecutionMode = ExecutionMode.BUILDER,
63
+ user_owned_clob_transport: UserOwnedClobTransport = UserOwnedClobTransport.DIRECT,
57
64
  ) -> dict[str, Any]:
58
65
  """Cancel a specific order."""
59
- from .cosigner import send_via_cosigner
60
66
  import json
61
67
 
68
+ from .cosigner import send_via_cosigner
69
+
62
70
  body = json.dumps({"orderID": order_id})
63
71
  headers = build_l2_headers(
64
72
  credentials["apiKey"],
@@ -77,6 +85,8 @@ async def cancel_order(
77
85
  {"method": "DELETE", "path": "/order", "body": body, "headers": headers},
78
86
  builder_credentials=builder_credentials,
79
87
  clob_host=clob_host,
88
+ execution_mode=execution_mode,
89
+ user_owned_clob_transport=user_owned_clob_transport,
80
90
  )
81
91
 
82
92
 
@@ -89,11 +99,14 @@ async def cancel_all_orders(
89
99
  market: str | None = None,
90
100
  builder_credentials: Any | None = None,
91
101
  clob_host: str | None = None,
102
+ execution_mode: ExecutionMode = ExecutionMode.BUILDER,
103
+ user_owned_clob_transport: UserOwnedClobTransport = UserOwnedClobTransport.DIRECT,
92
104
  ) -> dict[str, Any]:
93
105
  """Cancel all orders, optionally for a specific market."""
94
- from .cosigner import send_via_cosigner
95
106
  import json
96
107
 
108
+ from .cosigner import send_via_cosigner
109
+
97
110
  path = "/cancel-market-orders" if market else "/cancel-all"
98
111
  body = json.dumps({"market": market}) if market else None
99
112
  headers = build_l2_headers(
@@ -113,6 +126,8 @@ async def cancel_all_orders(
113
126
  {"method": "DELETE", "path": path, "body": body, "headers": headers},
114
127
  builder_credentials=builder_credentials,
115
128
  clob_host=clob_host,
129
+ execution_mode=execution_mode,
130
+ user_owned_clob_transport=user_owned_clob_transport,
116
131
  )
117
132
 
118
133
 
@@ -126,18 +141,19 @@ async def get_open_orders(
126
141
  asset_id: str | None = None,
127
142
  builder_credentials: Any | None = None,
128
143
  clob_host: str | None = None,
144
+ execution_mode: ExecutionMode = ExecutionMode.BUILDER,
145
+ user_owned_clob_transport: UserOwnedClobTransport = UserOwnedClobTransport.DIRECT,
129
146
  ) -> list[dict[str, Any]]:
130
147
  """Get open orders from the CLOB."""
131
148
  from .cosigner import send_via_cosigner
132
149
 
133
- path = "/data/orders"
134
- params = []
150
+ base_path = "/data/orders"
151
+ params: list[tuple[str, str]] = []
135
152
  if market:
136
- params.append(f"market={market}")
153
+ params.append(("market", market))
137
154
  if asset_id:
138
- params.append(f"asset_id={asset_id}")
139
- if params:
140
- path += "?" + "&".join(params)
155
+ params.append(("asset_id", asset_id))
156
+ path = base_path + ("?" + urlencode(params) if params else "")
141
157
 
142
158
  headers = build_l2_headers(
143
159
  credentials["apiKey"],
@@ -145,7 +161,7 @@ async def get_open_orders(
145
161
  credentials["apiPassphrase"],
146
162
  wallet_address,
147
163
  "GET",
148
- path,
164
+ base_path,
149
165
  )
150
166
 
151
167
  result = await send_via_cosigner(
@@ -155,6 +171,8 @@ async def get_open_orders(
155
171
  {"method": "GET", "path": path, "headers": headers},
156
172
  builder_credentials=builder_credentials,
157
173
  clob_host=clob_host,
174
+ execution_mode=execution_mode,
175
+ user_owned_clob_transport=user_owned_clob_transport,
158
176
  )
159
177
 
160
178
  if isinstance(result, list):
@@ -183,7 +201,9 @@ def _parse_tick_size(data: Any) -> str:
183
201
  """Parse CLOB tick size without substituting an unsafe default."""
184
202
  raw = data.get("minimum_tick_size") if isinstance(data, dict) else data
185
203
  if isinstance(raw, bool) or not isinstance(raw, (int, float, str)):
186
- raise ValueError("CLOB tick-size response is missing a positive minimum_tick_size")
204
+ raise ValueError(
205
+ "CLOB tick-size response is missing a positive minimum_tick_size"
206
+ )
187
207
  try:
188
208
  tick = float(raw)
189
209
  except ValueError as exc:
@@ -274,7 +294,9 @@ async def refresh_balance_allowance(
274
294
  "GET",
275
295
  path, # HMAC uses base path only (NO query string)
276
296
  )
277
- url = f"{CLOB_HOST_V2}{path}?asset_type={asset_type}&signature_type={signature_type}"
297
+ url = (
298
+ f"{CLOB_HOST_V2}{path}?asset_type={asset_type}&signature_type={signature_type}"
299
+ )
278
300
  async with httpx.AsyncClient(timeout=10.0) as http:
279
301
  resp = await http.get(url, headers=headers)
280
302
  return resp.is_success, resp.status_code
@@ -299,7 +321,9 @@ async def get_balance_allowance(
299
321
  "GET",
300
322
  path, # HMAC uses base path only
301
323
  )
302
- url = f"{CLOB_HOST_V2}{path}?asset_type={asset_type}&signature_type={signature_type}"
324
+ url = (
325
+ f"{CLOB_HOST_V2}{path}?asset_type={asset_type}&signature_type={signature_type}"
326
+ )
303
327
  async with httpx.AsyncClient(timeout=10.0) as http:
304
328
  resp = await http.get(url, headers=headers)
305
329
  if not resp.is_success:
@@ -16,6 +16,8 @@ CLOB_HOST_V2 = "https://clob.polymarket.com"
16
16
  CTF_EXCHANGE_V2 = "0xe111180000d2663c0091e4f400237545b87b996b"
17
17
  NEG_RISK_CTF_EXCHANGE_V2_A = "0xe2222d279d744050d28e00520010520000310f59"
18
18
  NEG_RISK_CTF_EXCHANGE_V2_B = "0xe2222d002000ba0053cef3375333610f64600036"
19
+ CTF_COLLATERAL_ADAPTER = "0xAdA100Db00Ca00073811820692005400218FcE1f"
20
+ NEG_RISK_CTF_COLLATERAL_ADAPTER = "0xadA2005600Dec949baf300f4C6120000bDB6eAab"
19
21
 
20
22
  # Token contracts
21
23
  USDC = "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174"
@@ -24,11 +26,15 @@ CTF = "0x4D97DCd97eC945f40cF65F87097ACe5EA0476045"
24
26
  # Safe derivation
25
27
  SAFE_FACTORY = "0xaacFeEa03eb1561C4e67d661e40682Bd20E3541b"
26
28
  SAFE_MULTISEND = "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761"
27
- SAFE_INIT_CODE_HASH = "0x2bce2127ff07fb632d16c8347c4ebf501f4841168bed00d9e6ef715ddb6fcecf"
29
+ SAFE_INIT_CODE_HASH = (
30
+ "0x2bce2127ff07fb632d16c8347c4ebf501f4841168bed00d9e6ef715ddb6fcecf"
31
+ )
28
32
 
29
33
  # Proxy derivation
30
34
  PROXY_FACTORY = "0xaB45c5A4B0c941a2F231C04C3f49182e1A254052"
31
- PROXY_INIT_CODE_HASH = "0xd21df8dc65880a8606f09fe0ce3df9b8869287ab0b058be05aa9e8af6330a00b"
35
+ PROXY_INIT_CODE_HASH = (
36
+ "0xd21df8dc65880a8606f09fe0ce3df9b8869287ab0b058be05aa9e8af6330a00b"
37
+ )
32
38
 
33
39
  # Deposit wallet derivation (Solady ERC-1967 clones)
34
40
  DEPOSIT_WALLET_FACTORY = "0x00000000000Fb5C9ADea0298D729A0CB3823Cc07"
@@ -37,19 +43,18 @@ DEPOSIT_WALLET_IMPL = "0x58CA52ebe0DadfdF531Cde7062e76746de4Db1eB"
37
43
  # All spender contracts that need approval
38
44
  SPENDERS = [CTF_EXCHANGE, NEG_RISK_CTF_EXCHANGE, NEG_RISK_ADAPTER]
39
45
 
40
- # V2 spenders that need pUSD approval.
41
- # Order matters: [0..2] are the three spenders the V2 CLOB validates during order placement.
42
- # [0] CTF_EXCHANGE_V2 — standard market orders
43
- # [1] NEG_RISK_CTF_EXCHANGE_V2_A — neg-risk market orders
44
- # [2] NEG_RISK_ADAPTER (V1!) — V2 neg-risk delegates settlement to V1 NRA, so the CLOB
45
- # requires pUSD allowance here even though it's a V1 contract.
46
- # Without this, every order fails with "not enough balance / allowance".
47
- # [3] NEG_RISK_CTF_EXCHANGE_V2_B — reserved variant
46
+ # Current V2 order spenders: standard and neg-risk exchange contracts.
48
47
  V2_SPENDERS = [
49
48
  CTF_EXCHANGE_V2,
50
49
  NEG_RISK_CTF_EXCHANGE_V2_A,
51
- NEG_RISK_ADAPTER,
52
- NEG_RISK_CTF_EXCHANGE_V2_B,
50
+ ]
51
+
52
+ # Least-privilege persistent approvals for user-owned V2 order execution.
53
+ # Position adapters are intentionally excluded: split/merge adds only the
54
+ # required adapter approval in the same relayed batch as each operation.
55
+ USER_OWNED_V2_ORDER_SPENDERS = [
56
+ CTF_EXCHANGE_V2,
57
+ NEG_RISK_CTF_EXCHANGE_V2_A,
53
58
  ]
54
59
 
55
60
  # PolyUSD wrapping contracts
@@ -58,7 +63,9 @@ COLLATERAL_ONRAMP = "0x93070a847efef7f70739046a929d47a521f5b8ee"
58
63
  COLLATERAL_OFFRAMP = "0x2957922eb93258b93368531d39facca3b4dc5854"
59
64
 
60
65
  # Public attribution value signed into V2 orders; this is not a credential.
61
- POLYNODE_BUILDER_CODE = "0x5472fdd700a9b2b6613d103095048c92304e97215a2607f73a9d5aa3701f3f09"
66
+ POLYNODE_BUILDER_CODE = (
67
+ "0x5472fdd700a9b2b6613d103095048c92304e97215a2607f73a9d5aa3701f3f09"
68
+ )
62
69
 
63
70
  # Fee Escrow
64
71
  # V1 (USDC.e collateral, paired with the V1 CLOB)