polynode 0.13.0__tar.gz → 0.14.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 (52) hide show
  1. {polynode-0.13.0 → polynode-0.14.0}/PKG-INFO +266 -7
  2. polynode-0.14.0/README.md +572 -0
  3. polynode-0.14.0/polynode/_version.py +1 -0
  4. polynode-0.14.0/polynode/testing.py +29 -0
  5. {polynode-0.13.0 → polynode-0.14.0}/polynode/trading/__init__.py +10 -0
  6. polynode-0.14.0/polynode/trading/eip712.py +740 -0
  7. {polynode-0.13.0 → polynode-0.14.0}/polynode/trading/signer.py +48 -25
  8. {polynode-0.13.0 → polynode-0.14.0}/polynode/trading/trader.py +680 -9
  9. polynode-0.14.0/polynode/trading/types.py +991 -0
  10. {polynode-0.13.0 → polynode-0.14.0}/pyproject.toml +5 -1
  11. polynode-0.13.0/README.md +0 -313
  12. polynode-0.13.0/polynode/_version.py +0 -1
  13. polynode-0.13.0/polynode/testing.py +0 -83
  14. polynode-0.13.0/polynode/trading/V2_ORDER_FLOW.md +0 -236
  15. polynode-0.13.0/polynode/trading/eip712.py +0 -415
  16. polynode-0.13.0/polynode/trading/types.py +0 -321
  17. {polynode-0.13.0 → polynode-0.14.0}/.gitignore +0 -0
  18. {polynode-0.13.0 → polynode-0.14.0}/core-contract-v1.json +0 -0
  19. {polynode-0.13.0 → polynode-0.14.0}/core-fixtures-v1.json +0 -0
  20. {polynode-0.13.0 → polynode-0.14.0}/polynode/__init__.py +0 -0
  21. {polynode-0.13.0 → polynode-0.14.0}/polynode/cache/__init__.py +0 -0
  22. {polynode-0.13.0 → polynode-0.14.0}/polynode/client.py +0 -0
  23. {polynode-0.13.0 → polynode-0.14.0}/polynode/engine.py +0 -0
  24. {polynode-0.13.0 → polynode-0.14.0}/polynode/errors.py +0 -0
  25. {polynode-0.13.0 → polynode-0.14.0}/polynode/orderbook.py +0 -0
  26. {polynode-0.13.0 → polynode-0.14.0}/polynode/orderbook_integrity.py +0 -0
  27. {polynode-0.13.0 → polynode-0.14.0}/polynode/orderbook_state.py +0 -0
  28. {polynode-0.13.0 → polynode-0.14.0}/polynode/perps.py +0 -0
  29. {polynode-0.13.0 → polynode-0.14.0}/polynode/redemption_watcher.py +0 -0
  30. {polynode-0.13.0 → polynode-0.14.0}/polynode/short_form.py +0 -0
  31. {polynode-0.13.0 → polynode-0.14.0}/polynode/subscription.py +0 -0
  32. {polynode-0.13.0 → polynode-0.14.0}/polynode/trading/clob_api.py +0 -0
  33. {polynode-0.13.0 → polynode-0.14.0}/polynode/trading/constants.py +0 -0
  34. {polynode-0.13.0 → polynode-0.14.0}/polynode/trading/cosigner.py +0 -0
  35. {polynode-0.13.0 → polynode-0.14.0}/polynode/trading/escrow.py +0 -0
  36. {polynode-0.13.0 → polynode-0.14.0}/polynode/trading/onboarding.py +0 -0
  37. {polynode-0.13.0 → polynode-0.14.0}/polynode/trading/position_management.py +0 -0
  38. {polynode-0.13.0 → polynode-0.14.0}/polynode/trading/privy.py +0 -0
  39. {polynode-0.13.0 → polynode-0.14.0}/polynode/trading/relayer.py +0 -0
  40. {polynode-0.13.0 → polynode-0.14.0}/polynode/trading/sqlite_backend.py +0 -0
  41. {polynode-0.13.0 → polynode-0.14.0}/polynode/trading/user_relayer.py +0 -0
  42. {polynode-0.13.0 → polynode-0.14.0}/polynode/types/__init__.py +0 -0
  43. {polynode-0.13.0 → polynode-0.14.0}/polynode/types/enums.py +0 -0
  44. {polynode-0.13.0 → polynode-0.14.0}/polynode/types/events.py +0 -0
  45. {polynode-0.13.0 → polynode-0.14.0}/polynode/types/orderbook.py +0 -0
  46. {polynode-0.13.0 → polynode-0.14.0}/polynode/types/perps.py +0 -0
  47. {polynode-0.13.0 → polynode-0.14.0}/polynode/types/rest.py +0 -0
  48. {polynode-0.13.0 → polynode-0.14.0}/polynode/types/short_form.py +0 -0
  49. {polynode-0.13.0 → polynode-0.14.0}/polynode/types/ws.py +0 -0
  50. {polynode-0.13.0 → polynode-0.14.0}/polynode/v3.py +0 -0
  51. {polynode-0.13.0 → polynode-0.14.0}/polynode/v3_operations.py +0 -0
  52. {polynode-0.13.0 → polynode-0.14.0}/polynode/ws.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: polynode
3
- Version: 0.13.0
3
+ Version: 0.14.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
@@ -36,7 +36,9 @@ Description-Content-Type: text/markdown
36
36
 
37
37
  Python SDK for the [Polynode](https://polynode.dev) real-time prediction market data platform.
38
38
 
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.
39
+ **New in v0.14.0:** Web platforms can take a user from wallet authorization through an exact browser-signed user-owned order without exposing backend credentials. The SDK imports the shared versioned browser bundle into memory, produces a credential-free signing request with a complete order preview, supports one-time multi-worker state, validates canonical signatures and wallet identity, checks BUY collateral before prompting, and submits with exact zero builder attribution.
40
+
41
+ **In v0.13.0:** Trading added 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
42
 
41
43
  **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
44
 
@@ -293,7 +295,7 @@ async def main():
293
295
  asyncio.run(main())
294
296
  ```
295
297
 
296
- 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.
298
+ For the V2 order flow, required approvals, and common failure modes, see [docs.polynode.dev](https://docs.polynode.dev).
297
299
 
298
300
  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.
299
301
 
@@ -313,7 +315,7 @@ from polynode.trading import (
313
315
  trader = PolyNodeTrader(TraderConfig(
314
316
  polynode_key=os.environ["POLYNODE_API_KEY"],
315
317
  execution_mode=ExecutionMode.USER_OWNED,
316
- # Optional, explicit regional egress; direct is the default.
318
+ # Optional transport selection; direct is the default.
317
319
  # user_owned_clob_transport=UserOwnedClobTransport.PROXY,
318
320
  ))
319
321
  ready = await trader.ensure_ready(user_wallet_signer)
@@ -322,11 +324,268 @@ print(ready.execution_mode, ready.user_relayer_authorized)
322
324
 
323
325
  `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
326
 
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.
327
+ `UserOwnedClobTransport.DIRECT` is the default. Integrations configured for `UserOwnedClobTransport.PROXY` must select it explicitly; the SDK never changes transport or falls back between paths automatically. 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.
328
+
329
+ For browser-wallet integrations, begin authorization on the trusted backend with the trader's configured Polynode key. Keep the complete challenge in one-time backend state and return only its public fields:
330
+
331
+ ```python
332
+ async def begin_wallet_authorization(trader, session, pending_authorizations):
333
+ challenge = await trader.begin_user_relayer_authorization(
334
+ session.wallet_address
335
+ )
336
+ await pending_authorizations.put_once(
337
+ challenge.challenge_id,
338
+ challenge,
339
+ expires_at=challenge.expires_at,
340
+ )
341
+ return {
342
+ "challengeId": challenge.challenge_id,
343
+ "address": challenge.address,
344
+ "expiresAt": challenge.expires_at,
345
+ "signatureType": challenge.signature_type,
346
+ "message": challenge.message,
347
+ }
348
+ ```
349
+
350
+ The connected wallet signs the exact message without reconstructing it:
351
+
352
+ ```javascript
353
+ const [address] = await window.ethereum.request({
354
+ method: "eth_requestAccounts",
355
+ });
356
+ if (address.toLowerCase() !== challenge.address.toLowerCase()) {
357
+ throw new Error("Connected wallet changed");
358
+ }
359
+ const signature = await window.ethereum.request({
360
+ method: "personal_sign",
361
+ params: [challenge.message, address],
362
+ });
363
+ ```
364
+
365
+ Atomically take the original challenge and complete it against the wallet bound to the authenticated application session:
366
+
367
+ ```python
368
+ async def complete_wallet_authorization(
369
+ trader, session, browser_result, pending_authorizations
370
+ ):
371
+ challenge = await pending_authorizations.take_once(
372
+ browser_result.challenge_id
373
+ )
374
+ if challenge is None:
375
+ raise ValueError("Unknown or already-used authorization challenge")
376
+ credentials = await trader.complete_user_relayer_authorization(
377
+ challenge,
378
+ browser_result.signature,
379
+ session.wallet_address,
380
+ )
381
+ return credentials
382
+ ```
383
+
384
+ Completion validates the challenge, signature shape, expected wallet, and returned credential owner, then keeps the credential only in that trader instance's memory. It does not write the signature or credential to SQLite or another store. If the account must survive the process, encrypt the returned credential in your own wallet-bound secret manager. The global `begin_user_relayer_authorization()` and `complete_user_relayer_authorization()` helpers remain available when explicit service configuration is preferable.
385
+
386
+ For long-running services, `await trader.authorize_user_owned_execution(signer)` provides the combined convenience flow. Backend secret-manager storage is recommended, and the wallet-owned credential can be supplied later as `TraderConfig.user_relayer_credentials`. Never log it, commit it, or persist it in cookies, local storage, IndexedDB, or other browser storage. Current-tab memory is the explicitly supported, higher-risk session-only option described below.
387
+
388
+ ##### Browser wallet to submitted order
389
+
390
+ Use the split-phase order API when a browser wallet signs but your backend owns submission. The backend retains the Polynode key, wallet-owned relayer credential, and CLOB credentials. The browser receives only one short-lived EIP-712 request and returns its signature.
391
+
392
+ Create a separate user-owned trader for each active wallet context. This example uses an in-memory SDK database so decrypted credentials exist only for the process lifetime; load them from your own encrypted secret store:
393
+
394
+ ```python
395
+ from polynode.trading import (
396
+ ExecutionMode,
397
+ PolyNodeTrader,
398
+ SignatureType,
399
+ TraderConfig,
400
+ UserRelayerCredentials,
401
+ )
402
+
403
+ trader = PolyNodeTrader(TraderConfig(
404
+ polynode_key=server_secrets.polynode_key,
405
+ db_path=":memory:",
406
+ execution_mode=ExecutionMode.USER_OWNED,
407
+ user_relayer_credentials=UserRelayerCredentials(
408
+ key=wallet_secrets.relayer_key,
409
+ address=wallet_address,
410
+ ),
411
+ ))
412
+ trader.link_credentials(
413
+ wallet=wallet_address,
414
+ funder_address=funder_address,
415
+ signature_type=SignatureType.EOA, # This minimal example uses an EOA account.
416
+ api_key=wallet_secrets.clob_key,
417
+ api_secret=wallet_secrets.clob_secret,
418
+ api_passphrase=wallet_secrets.clob_passphrase,
419
+ )
420
+ ```
421
+
422
+ There are two safe credential patterns:
423
+
424
+ - **Backend vault (recommended for durable accounts):** store each wallet's relayer and CLOB credentials encrypted under that wallet identity, decrypt them only into the user-owned trader, and close the trader after use.
425
+ - **Session-only browser handoff (higher risk):** if your frontend SDK holds a versioned user-owned bundle only in current-tab memory, send that complete bundle once to an authenticated backend endpoint over HTTPS and import it into an in-memory trader:
426
+
427
+ ```json
428
+ {
429
+ "version": "1",
430
+ "executionMode": "user_owned",
431
+ "wallet": {
432
+ "address": "0x...",
433
+ "funderAddress": "0x...",
434
+ "signatureType": 0
435
+ },
436
+ "clobCredentials": {
437
+ "apiKey": "...",
438
+ "apiSecret": "...",
439
+ "apiPassphrase": "..."
440
+ },
441
+ "userRelayerCredentials": {
442
+ "key": "...",
443
+ "address": "0x..."
444
+ }
445
+ }
446
+ ```
447
+
448
+ `signatureType` is `0` for an EOA, `2` for an EOA-controlled Safe, or `3` for an EOA-controlled deposit wallet.
449
+
450
+ ```python
451
+ async def trader_from_browser_bundle(request_json):
452
+ trader = PolyNodeTrader(TraderConfig(
453
+ polynode_key=server_secrets.polynode_key,
454
+ db_path=":memory:",
455
+ execution_mode=ExecutionMode.USER_OWNED,
456
+ ))
457
+ await trader.import_user_owned_browser_bundle(request_json)
458
+ return trader
459
+ ```
460
+
461
+ `import_user_owned_browser_bundle()` accepts only the exact version-1 `user_owned` schema, validates the relayer owner plus wallet/funder/signature-type binding, redacts the model representation, and refuses any database other than `:memory:`. The CLOB API secret may use canonical standard Base64 or Base64url, padded or unpadded; malformed padding, whitespace, empty values, and noncanonical encodings are rejected before the trader is mutated. Treat the request body as a secret: exclude it from access logs, traces, error reports, analytics, and replay queues. Do not retain it after the session.
462
+
463
+ When your application receives a validated order intent, prepare it without submitting:
464
+
465
+ ```python
466
+ from polynode.trading import OrderParams
467
+
468
+ async def prepare_order(trader, order_intent, prepared_orders):
469
+ prepared = await trader.prepare_user_owned_order(OrderParams(
470
+ token_id=order_intent.token_id,
471
+ side="BUY",
472
+ price=0.52,
473
+ size=10,
474
+ type="GTC",
475
+ ))
476
+
477
+ # Save the complete object in an application-owned, backend-only one-time store.
478
+ await prepared_orders.put_once(
479
+ prepared.signing_request.request_id,
480
+ prepared,
481
+ expires_at=prepared.signing_request.expires_at,
482
+ )
483
+
484
+ # This is the only value returned to the browser.
485
+ return prepared.signing_request.to_dict()
486
+ ```
487
+
488
+ The browser request has one language-neutral shape: `version`, `requestId`, `address`, Unix-seconds `expiresAt`, `typedData`, and `order`. The credential-free `order` preview includes the canonical positive token ID, side, tick-rounded price, two-decimal round-down size, order type, post-only and expiration controls, maker, signer, `makerAmount`, and `takerAmount`. Those are the exact values used for signing; show them to the user, but never accept a browser-edited copy as submission state.
489
+
490
+ For a BUY, preparation reads the returned preview's `maker` (the active funder) and requires its PolyUSD balance to cover the exact canonical `makerAmount` before returning anything for wallet signature. This check never moves or wraps funds. `ensure_ready()` deploys/configures the wallet but does not create collateral, so your platform must fund or wrap into that funder first and prepare a new order after any balance change.
491
+
492
+ Require an authenticated application session for both endpoints, bind that session to the expected wallet on the backend, validate that the user may place the requested order, and apply normal CSRF protection when using cookies. A wallet address supplied by the browser is not application authentication.
493
+
494
+ Verify the connected address and sign the exact `typedData` object in the browser. Do not rebuild or edit it:
495
+
496
+ ```javascript
497
+ const signingRequest = await fetch("/api/orders/prepare", {
498
+ method: "POST",
499
+ headers: { "content-type": "application/json" },
500
+ body: JSON.stringify(orderIntent),
501
+ }).then((response) => response.json());
502
+
503
+ const [connectedAddress] = await window.ethereum.request({
504
+ method: "eth_requestAccounts",
505
+ });
506
+ const chainId = await window.ethereum.request({ method: "eth_chainId" });
507
+ if (
508
+ signingRequest.version !== "1"
509
+ || !Number.isSafeInteger(signingRequest.expiresAt)
510
+ || Math.floor(Date.now() / 1000) >= signingRequest.expiresAt
511
+ ) {
512
+ throw new Error("Order signing request is invalid or expired");
513
+ }
514
+ if (chainId.toLowerCase() !== "0x89") {
515
+ throw new Error("Switch the wallet to Polygon");
516
+ }
517
+ if (connectedAddress.toLowerCase() !== signingRequest.address.toLowerCase()) {
518
+ throw new Error("Connect the wallet that owns this trading account");
519
+ }
520
+
521
+ // Display signingRequest.order for confirmation before requesting the signature.
522
+
523
+ const signature = await window.ethereum.request({
524
+ method: "eth_signTypedData_v4",
525
+ params: [connectedAddress, JSON.stringify(signingRequest.typedData)],
526
+ });
527
+
528
+ const result = await fetch("/api/orders/submit", {
529
+ method: "POST",
530
+ headers: { "content-type": "application/json" },
531
+ body: JSON.stringify({
532
+ requestId: signingRequest.requestId,
533
+ address: connectedAddress,
534
+ signature,
535
+ }),
536
+ }).then((response) => response.json());
537
+ ```
538
+
539
+ On the backend, atomically take the prepared object from session state, then validate and submit it:
540
+
541
+ ```python
542
+ async def submit_order(trader, browser_result, prepared_orders):
543
+ # `take_once` must delete atomically so two workers cannot submit the same request.
544
+ prepared = await prepared_orders.take_once(browser_result.request_id)
545
+ if prepared is None:
546
+ raise ValueError("Unknown or already-used signing request")
547
+
548
+ result = await trader.submit_prepared_user_owned_order(
549
+ prepared,
550
+ request_id=browser_result.request_id,
551
+ address=browser_result.address,
552
+ signature=browser_result.signature,
553
+ )
554
+ return {
555
+ "success": result.success,
556
+ "orderId": result.order_id,
557
+ "error": result.error,
558
+ }
559
+ ```
560
+
561
+ `prepare_user_owned_order()` supports V2 user-owned execution only, expires after five minutes by default, forces zero builder attribution, rejects positive fee authentication, and performs no submission. GTC, FOK, and FAK orders must omit expiration (zero is accepted and canonicalized to no expiration); GTD requires a fresh Unix-seconds expiration with the 60-second safety buffer, and the signing request is clamped to that window. `submit_prepared_user_owned_order()` verifies the request ID, expiry, connected wallet, stored wallet/funder identity, exact typed data, and recovered signer before consuming the request. A consumed request cannot be replayed. If submission has an ambiguous network result, reconcile its status instead of preparing an automatic duplicate.
562
+
563
+ For a single-process application, a backend-only in-memory dictionary is sufficient. For multiple workers, serialize only with the SDK's trusted server-state methods and use a server-side store that can atomically take/delete by `requestId`:
564
+
565
+ ```python
566
+ import json
567
+ import time
568
+
569
+ # Prepare worker: expire server state with the signing request.
570
+ ttl_seconds = prepared.signing_request.expires_at - int(time.time())
571
+ if ttl_seconds <= 0:
572
+ raise ValueError("Signing request already expired")
573
+ await server_store.put(
574
+ prepared.signing_request.request_id,
575
+ json.dumps(trader.export_prepared_user_owned_order(prepared)),
576
+ ttl_seconds=ttl_seconds,
577
+ )
578
+
579
+ # Submit worker: `take_once` must atomically return and delete the value.
580
+ raw_state = await server_store.take_once(browser_result.request_id)
581
+ if raw_state is None:
582
+ raise ValueError("Unknown or already-used signing request")
583
+ prepared = trader.import_prepared_user_owned_order(json.loads(raw_state))
584
+ ```
326
585
 
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.
586
+ The trader authenticates exported state with a domain-separated HMAC derived from its configured backend Polynode key, verifies it on import and again before submission, then strictly reconstructs the exact V2 order from the retained market inputs. Every worker that prepares, restores, or submits these requests must use the same key. The serialized tag never contains that key, and rotating the key intentionally invalidates outstanding prepared requests. Accept exported state only from your own authenticated server-side store; never accept it from a browser. The object contains no credentials, but it contains submission controls and must remain one-time.
328
587
 
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.
588
+ Never serialize the complete prepared object to the browser; only serialize `prepared.signing_request.to_dict()`. Never place the Polynode key, wallet-owned relayer credential, CLOB credentials, or authenticated submission headers in frontend code, cookies, browser storage, analytics, or logs.
330
589
 
331
590
  `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
591