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.
- {polynode-0.12.1 → polynode-0.13.0}/PKG-INFO +55 -8
- {polynode-0.12.1 → polynode-0.13.0}/README.md +51 -4
- polynode-0.13.0/polynode/_version.py +1 -0
- {polynode-0.12.1 → polynode-0.13.0}/polynode/trading/V2_ORDER_FLOW.md +9 -14
- polynode-0.13.0/polynode/trading/__init__.py +70 -0
- {polynode-0.12.1 → polynode-0.13.0}/polynode/trading/clob_api.py +37 -13
- {polynode-0.12.1 → polynode-0.13.0}/polynode/trading/constants.py +20 -13
- polynode-0.13.0/polynode/trading/cosigner.py +190 -0
- {polynode-0.12.1 → polynode-0.13.0}/polynode/trading/onboarding.py +373 -75
- polynode-0.13.0/polynode/trading/position_management.py +226 -0
- polynode-0.13.0/polynode/trading/relayer.py +731 -0
- {polynode-0.12.1 → polynode-0.13.0}/polynode/trading/signer.py +27 -7
- polynode-0.13.0/polynode/trading/trader.py +1705 -0
- {polynode-0.12.1 → polynode-0.13.0}/polynode/trading/types.py +56 -5
- polynode-0.13.0/polynode/trading/user_relayer.py +321 -0
- {polynode-0.12.1 → polynode-0.13.0}/polynode/types/rest.py +1 -1
- {polynode-0.12.1 → polynode-0.13.0}/pyproject.toml +4 -3
- polynode-0.12.1/polynode/_version.py +0 -1
- polynode-0.12.1/polynode/trading/__init__.py +0 -30
- polynode-0.12.1/polynode/trading/cosigner.py +0 -95
- polynode-0.12.1/polynode/trading/position_management.py +0 -104
- polynode-0.12.1/polynode/trading/relayer.py +0 -422
- polynode-0.12.1/polynode/trading/trader.py +0 -936
- {polynode-0.12.1 → polynode-0.13.0}/.gitignore +0 -0
- {polynode-0.12.1 → polynode-0.13.0}/core-contract-v1.json +0 -0
- {polynode-0.12.1 → polynode-0.13.0}/core-fixtures-v1.json +0 -0
- {polynode-0.12.1 → polynode-0.13.0}/polynode/__init__.py +0 -0
- {polynode-0.12.1 → polynode-0.13.0}/polynode/cache/__init__.py +0 -0
- {polynode-0.12.1 → polynode-0.13.0}/polynode/client.py +0 -0
- {polynode-0.12.1 → polynode-0.13.0}/polynode/engine.py +0 -0
- {polynode-0.12.1 → polynode-0.13.0}/polynode/errors.py +0 -0
- {polynode-0.12.1 → polynode-0.13.0}/polynode/orderbook.py +0 -0
- {polynode-0.12.1 → polynode-0.13.0}/polynode/orderbook_integrity.py +0 -0
- {polynode-0.12.1 → polynode-0.13.0}/polynode/orderbook_state.py +0 -0
- {polynode-0.12.1 → polynode-0.13.0}/polynode/perps.py +0 -0
- {polynode-0.12.1 → polynode-0.13.0}/polynode/redemption_watcher.py +0 -0
- {polynode-0.12.1 → polynode-0.13.0}/polynode/short_form.py +0 -0
- {polynode-0.12.1 → polynode-0.13.0}/polynode/subscription.py +0 -0
- {polynode-0.12.1 → polynode-0.13.0}/polynode/testing.py +0 -0
- {polynode-0.12.1 → polynode-0.13.0}/polynode/trading/eip712.py +0 -0
- {polynode-0.12.1 → polynode-0.13.0}/polynode/trading/escrow.py +0 -0
- {polynode-0.12.1 → polynode-0.13.0}/polynode/trading/privy.py +0 -0
- {polynode-0.12.1 → polynode-0.13.0}/polynode/trading/sqlite_backend.py +0 -0
- {polynode-0.12.1 → polynode-0.13.0}/polynode/types/__init__.py +0 -0
- {polynode-0.12.1 → polynode-0.13.0}/polynode/types/enums.py +0 -0
- {polynode-0.12.1 → polynode-0.13.0}/polynode/types/events.py +0 -0
- {polynode-0.12.1 → polynode-0.13.0}/polynode/types/orderbook.py +0 -0
- {polynode-0.12.1 → polynode-0.13.0}/polynode/types/perps.py +0 -0
- {polynode-0.12.1 → polynode-0.13.0}/polynode/types/short_form.py +0 -0
- {polynode-0.12.1 → polynode-0.13.0}/polynode/types/ws.py +0 -0
- {polynode-0.12.1 → polynode-0.13.0}/polynode/v3.py +0 -0
- {polynode-0.12.1 → polynode-0.13.0}/polynode/v3_operations.py +0 -0
- {polynode-0.12.1 → polynode-0.13.0}/polynode/ws.py +0 -0
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: polynode
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: Python SDK for the
|
|
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:
|
|
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 [
|
|
37
|
+
Python SDK for the [Polynode](https://polynode.dev) real-time prediction market data platform.
|
|
38
38
|
|
|
39
|
-
**
|
|
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
|
-
**
|
|
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
|
|
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 [
|
|
3
|
+
Python SDK for the [Polynode](https://polynode.dev) real-time prediction market data platform.
|
|
4
4
|
|
|
5
|
-
**
|
|
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
|
-
**
|
|
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
|
|
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
|
|
10
|
-
2. setApprovalForAll CTF → same
|
|
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 **
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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)
|
|
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)
|
|
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
|
-
|
|
134
|
-
params = []
|
|
150
|
+
base_path = "/data/orders"
|
|
151
|
+
params: list[tuple[str, str]] = []
|
|
135
152
|
if market:
|
|
136
|
-
params.append(
|
|
153
|
+
params.append(("market", market))
|
|
137
154
|
if asset_id:
|
|
138
|
-
params.append(
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
29
|
+
SAFE_INIT_CODE_HASH = (
|
|
30
|
+
"0x2bce2127ff07fb632d16c8347c4ebf501f4841168bed00d9e6ef715ddb6fcecf"
|
|
31
|
+
)
|
|
28
32
|
|
|
29
33
|
# Proxy derivation
|
|
30
34
|
PROXY_FACTORY = "0xaB45c5A4B0c941a2F231C04C3f49182e1A254052"
|
|
31
|
-
PROXY_INIT_CODE_HASH =
|
|
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
|
|
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
|
-
|
|
52
|
-
|
|
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 =
|
|
66
|
+
POLYNODE_BUILDER_CODE = (
|
|
67
|
+
"0x5472fdd700a9b2b6613d103095048c92304e97215a2607f73a9d5aa3701f3f09"
|
|
68
|
+
)
|
|
62
69
|
|
|
63
70
|
# Fee Escrow
|
|
64
71
|
# V1 (USDC.e collateral, paired with the V1 CLOB)
|