polynode 0.10.6__tar.gz → 0.10.8__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 (48) hide show
  1. {polynode-0.10.6 → polynode-0.10.8}/.gitignore +2 -0
  2. {polynode-0.10.6 → polynode-0.10.8}/PKG-INFO +5 -1
  3. {polynode-0.10.6 → polynode-0.10.8}/README.md +4 -0
  4. polynode-0.10.8/polynode/_version.py +1 -0
  5. {polynode-0.10.6 → polynode-0.10.8}/polynode/trading/eip712.py +18 -7
  6. {polynode-0.10.6 → polynode-0.10.8}/polynode/trading/onboarding.py +17 -4
  7. {polynode-0.10.6 → polynode-0.10.8}/polynode/trading/trader.py +22 -2
  8. {polynode-0.10.6 → polynode-0.10.8}/pyproject.toml +1 -1
  9. {polynode-0.10.6 → polynode-0.10.8}/tests/test_trading.py +143 -0
  10. polynode-0.10.6/polynode/_version.py +0 -1
  11. {polynode-0.10.6 → polynode-0.10.8}/polynode/__init__.py +0 -0
  12. {polynode-0.10.6 → polynode-0.10.8}/polynode/cache/__init__.py +0 -0
  13. {polynode-0.10.6 → polynode-0.10.8}/polynode/client.py +0 -0
  14. {polynode-0.10.6 → polynode-0.10.8}/polynode/engine.py +0 -0
  15. {polynode-0.10.6 → polynode-0.10.8}/polynode/errors.py +0 -0
  16. {polynode-0.10.6 → polynode-0.10.8}/polynode/orderbook.py +0 -0
  17. {polynode-0.10.6 → polynode-0.10.8}/polynode/orderbook_state.py +0 -0
  18. {polynode-0.10.6 → polynode-0.10.8}/polynode/redemption_watcher.py +0 -0
  19. {polynode-0.10.6 → polynode-0.10.8}/polynode/short_form.py +0 -0
  20. {polynode-0.10.6 → polynode-0.10.8}/polynode/subscription.py +0 -0
  21. {polynode-0.10.6 → polynode-0.10.8}/polynode/testing.py +0 -0
  22. {polynode-0.10.6 → polynode-0.10.8}/polynode/trading/V2_ORDER_FLOW.md +0 -0
  23. {polynode-0.10.6 → polynode-0.10.8}/polynode/trading/__init__.py +0 -0
  24. {polynode-0.10.6 → polynode-0.10.8}/polynode/trading/clob_api.py +0 -0
  25. {polynode-0.10.6 → polynode-0.10.8}/polynode/trading/constants.py +0 -0
  26. {polynode-0.10.6 → polynode-0.10.8}/polynode/trading/cosigner.py +0 -0
  27. {polynode-0.10.6 → polynode-0.10.8}/polynode/trading/escrow.py +0 -0
  28. {polynode-0.10.6 → polynode-0.10.8}/polynode/trading/position_management.py +0 -0
  29. {polynode-0.10.6 → polynode-0.10.8}/polynode/trading/privy.py +0 -0
  30. {polynode-0.10.6 → polynode-0.10.8}/polynode/trading/relayer.py +0 -0
  31. {polynode-0.10.6 → polynode-0.10.8}/polynode/trading/signer.py +0 -0
  32. {polynode-0.10.6 → polynode-0.10.8}/polynode/trading/sqlite_backend.py +0 -0
  33. {polynode-0.10.6 → polynode-0.10.8}/polynode/trading/types.py +0 -0
  34. {polynode-0.10.6 → polynode-0.10.8}/polynode/types/__init__.py +0 -0
  35. {polynode-0.10.6 → polynode-0.10.8}/polynode/types/enums.py +0 -0
  36. {polynode-0.10.6 → polynode-0.10.8}/polynode/types/events.py +0 -0
  37. {polynode-0.10.6 → polynode-0.10.8}/polynode/types/orderbook.py +0 -0
  38. {polynode-0.10.6 → polynode-0.10.8}/polynode/types/rest.py +0 -0
  39. {polynode-0.10.6 → polynode-0.10.8}/polynode/types/short_form.py +0 -0
  40. {polynode-0.10.6 → polynode-0.10.8}/polynode/types/ws.py +0 -0
  41. {polynode-0.10.6 → polynode-0.10.8}/polynode/ws.py +0 -0
  42. {polynode-0.10.6 → polynode-0.10.8}/tests/__init__.py +0 -0
  43. {polynode-0.10.6 → polynode-0.10.8}/tests/conftest.py +0 -0
  44. {polynode-0.10.6 → polynode-0.10.8}/tests/test_client.py +0 -0
  45. {polynode-0.10.6 → polynode-0.10.8}/tests/test_orderbook.py +0 -0
  46. {polynode-0.10.6 → polynode-0.10.8}/tests/test_orderbook_protocol.py +0 -0
  47. {polynode-0.10.6 → polynode-0.10.8}/tests/test_types.py +0 -0
  48. {polynode-0.10.6 → polynode-0.10.8}/tests/test_ws_protocol.py +0 -0
@@ -37,6 +37,8 @@ docs/
37
37
  ob/ob
38
38
  ob/ob.bak
39
39
  wallet-resolver/wallets.db
40
+ wallet-resolver/wallets.db-shm
41
+ wallet-resolver/wallets.db-wal
40
42
  __pycache__/
41
43
 
42
44
  # Manual file backups (e.g. server.ts.bak.x-search-1777174461)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: polynode
3
- Version: 0.10.6
3
+ Version: 0.10.8
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
@@ -35,6 +35,10 @@ Description-Content-Type: text/markdown
35
35
 
36
36
  Python SDK for the [PolyNode](https://polynode.dev) real-time prediction market data platform.
37
37
 
38
+ **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.
39
+
40
+ **In v0.10.7:** Polymarket V2 deposit-wallet trading fixes. `ensure_ready()` detects deployed `POLY_1271` wallets correctly, V2 type-3 orders use the deposit wallet as both maker and signer, and existing local credentials can be repaired by rerunning `ensure_ready()`.
41
+
38
42
  ## Install
39
43
 
40
44
  ```bash
@@ -2,6 +2,10 @@
2
2
 
3
3
  Python SDK for the [PolyNode](https://polynode.dev) real-time prediction market data platform.
4
4
 
5
+ **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.
6
+
7
+ **In v0.10.7:** Polymarket V2 deposit-wallet trading fixes. `ensure_ready()` detects deployed `POLY_1271` wallets correctly, V2 type-3 orders use the deposit wallet as both maker and signer, and existing local credentials can be repaired by rerunning `ensure_ready()`.
8
+
5
9
  ## Install
6
10
 
7
11
  ```bash
@@ -0,0 +1 @@
1
+ __version__ = "0.10.8"
@@ -96,6 +96,20 @@ TYPED_DATA_SIGN_TYPES = {
96
96
  }
97
97
 
98
98
 
99
+ def _poly1271_inner_signature(signature: str) -> bytes:
100
+ """Return a 65-byte POLY_1271 inner signature with Ethereum v=27/28."""
101
+ if not signature.startswith("0x"):
102
+ signature = f"0x{signature}"
103
+ sig_bytes = bytearray(bytes.fromhex(signature[2:]))
104
+ if len(sig_bytes) != 65:
105
+ raise ValueError(f"POLY_1271 inner signature must be 65 bytes, got {len(sig_bytes)}")
106
+ if sig_bytes[-1] in (0, 1):
107
+ sig_bytes[-1] += 27
108
+ elif sig_bytes[-1] not in (27, 28):
109
+ raise ValueError(f"invalid POLY_1271 inner signature v: {sig_bytes[-1]}")
110
+ return bytes(sig_bytes)
111
+
112
+
99
113
  def compute_amounts(
100
114
  price: float,
101
115
  size: float,
@@ -305,10 +319,11 @@ async def create_signed_order_v2(
305
319
  - timestamp is a string in the payload
306
320
  """
307
321
  maker_amount, taker_amount = compute_amounts(price, size, side, tick_size)
322
+ order_signer = funder_address if signature_type == SignatureType.POLY_1271 else signer_address
308
323
 
309
324
  payload = build_order_payload_v2(
310
325
  maker=funder_address,
311
- signer=signer_address,
326
+ signer=order_signer,
312
327
  token_id=token_id,
313
328
  maker_amount=maker_amount,
314
329
  taker_amount=taker_amount,
@@ -337,11 +352,7 @@ async def create_signed_order_v2(
337
352
  },
338
353
  )
339
354
  inner_sig = await sign_typed_data(tds_payload)
340
- if not inner_sig.startswith("0x"):
341
- inner_sig = f"0x{inner_sig}"
342
- inner_bytes = bytearray(bytes.fromhex(inner_sig[2:]))
343
- if len(inner_bytes) == 65 and inner_bytes[-1] >= 27:
344
- inner_bytes[-1] -= 27
355
+ inner_bytes = _poly1271_inner_signature(inner_sig)
345
356
 
346
357
  # Compute app domain separator and contents hash for wrapping
347
358
  from eth_utils import keccak
@@ -388,7 +399,7 @@ async def create_signed_order_v2(
388
399
  return {
389
400
  "salt": payload.message["salt"], # number
390
401
  "maker": funder_address,
391
- "signer": signer_address,
402
+ "signer": order_signer,
392
403
  "taker": "0x0000000000000000000000000000000000000000",
393
404
  "tokenId": token_id,
394
405
  "makerAmount": str(maker_amount),
@@ -124,6 +124,7 @@ async def detect_wallet_type(eoa: str) -> dict:
124
124
  """Auto-detect if a Safe, Proxy, or deposit wallet is deployed for this EOA."""
125
125
  safe_addr = derive_safe_address(eoa)
126
126
  proxy_addr = derive_proxy_address(eoa)
127
+ deposit_wallet_addr = derive_deposit_wallet_address(eoa)
127
128
 
128
129
  async with httpx.AsyncClient(timeout=10.0) as http:
129
130
  # Check Safe first (most common for existing users)
@@ -152,13 +153,13 @@ async def detect_wallet_type(eoa: str) -> dict:
152
153
  except Exception:
153
154
  pass
154
155
 
155
- # Check deposit wallet (newer accounts)
156
+ # Check deposit wallet (newer accounts). Polymarket's relayer defaults
157
+ # /deployed to SAFE, so deposit wallets must be checked with type=WALLET.
156
158
  try:
157
- dw_addr = derive_deposit_wallet_address(eoa)
158
- if await is_safe_deployed(dw_addr):
159
+ if await is_deposit_wallet_deployed(deposit_wallet_addr):
159
160
  return {
160
161
  "signature_type": SignatureType.POLY_1271,
161
- "funder_address": dw_addr,
162
+ "funder_address": deposit_wallet_addr,
162
163
  }
163
164
  except Exception:
164
165
  pass
@@ -182,6 +183,18 @@ async def is_safe_deployed(funder_address: str) -> bool:
182
183
  return False
183
184
 
184
185
 
186
+ async def is_deposit_wallet_deployed(funder_address: str) -> bool:
187
+ """Check if a deposit wallet is deployed at the given address."""
188
+ async with httpx.AsyncClient(timeout=10.0) as http:
189
+ try:
190
+ resp = await http.get(f"{RELAYER_HOST}/deployed?address={funder_address}&type=WALLET")
191
+ if resp.is_success:
192
+ return resp.json().get("deployed", False)
193
+ except Exception:
194
+ pass
195
+ return False
196
+
197
+
185
198
  async def check_approvals(
186
199
  funder_address: str,
187
200
  rpc_url: str,
@@ -33,6 +33,7 @@ from .onboarding import (
33
33
  create_clob_credentials,
34
34
  derive_funder_address,
35
35
  detect_wallet_type,
36
+ is_deposit_wallet_deployed,
36
37
  is_safe_deployed,
37
38
  set_approvals as set_approvals_onchain,
38
39
  )
@@ -103,14 +104,19 @@ class PolyNodeTrader:
103
104
  signer: str | RouterSigner,
104
105
  *,
105
106
  type: SignatureType | None = None,
107
+ funder_address: str | None = None,
106
108
  ) -> ReadyStatus:
107
109
  """One-call onboarding: detect wallet type, deploy Safe, set approvals, create CLOB creds."""
110
+ if funder_address is not None and type is None:
111
+ raise ValueError("funder_address requires type so the wallet identity is unambiguous")
112
+
108
113
  temp = await normalize_signer(signer, type or SignatureType.POLY_GNOSIS_SAFE)
109
114
  actions: list[str] = []
110
115
 
111
116
  if type is not None:
112
117
  sig_type = type
113
- funder = temp.funder_address or await derive_funder_address(temp.address, sig_type)
118
+ funder = funder_address or temp.funder_address or await derive_funder_address(temp.address, sig_type)
119
+ actions.append(f"explicit_type_{int(sig_type)}")
114
120
  else:
115
121
  detected = await detect_wallet_type(temp.address)
116
122
  sig_type = detected["signature_type"]
@@ -135,6 +141,14 @@ class PolyNodeTrader:
135
141
  else:
136
142
  actions.append("safe_deploy_needed")
137
143
 
144
+ if sig_type == SignatureType.POLY_1271 and not safe_deployed:
145
+ deployed = await is_deposit_wallet_deployed(funder)
146
+ if deployed:
147
+ safe_deployed = True
148
+ actions.append("deposit_wallet_already_deployed")
149
+ else:
150
+ actions.append("deposit_wallet_deploy_needed")
151
+
138
152
  # Check approvals
139
153
  if sig_type in (SignatureType.POLY_GNOSIS_SAFE, SignatureType.EOA) and not approvals_set:
140
154
  try:
@@ -165,6 +179,8 @@ class PolyNodeTrader:
165
179
  db.upsert_credentials(stored)
166
180
  actions.append("credentials_created")
167
181
  else:
182
+ stored.funder_address = funder
183
+ stored.signature_type = sig_type
168
184
  stored.safe_deployed = safe_deployed
169
185
  stored.approvals_set = approvals_set
170
186
  stored.updated_at = time.time()
@@ -214,11 +230,15 @@ class PolyNodeTrader:
214
230
  signer: str | RouterSigner,
215
231
  *,
216
232
  type: SignatureType | None = None,
233
+ funder_address: str | None = None,
217
234
  ) -> LinkResult:
218
235
  """Link a wallet manually (derive credentials, store locally)."""
236
+ if funder_address is not None and type is None:
237
+ raise ValueError("funder_address requires type so the wallet identity is unambiguous")
238
+
219
239
  sig_type = type or self._default_sig_type
220
240
  normalized = await normalize_signer(signer, sig_type)
221
- funder = normalized.funder_address or await derive_funder_address(normalized.address, sig_type)
241
+ funder = funder_address or normalized.funder_address or await derive_funder_address(normalized.address, sig_type)
222
242
 
223
243
  db = self._get_db()
224
244
  existing = db.get_credentials(normalized.address)
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "polynode"
7
- version = "0.10.6"
7
+ version = "0.10.8"
8
8
  description = "Python SDK for the PolyNode real-time prediction market data platform"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -1,11 +1,13 @@
1
1
  """Tests for trading module — EIP-712 signing, HMAC headers, V1 and V2."""
2
2
 
3
3
  import base64
4
+ import asyncio
4
5
 
5
6
  from polynode.trading.cosigner import build_l2_headers
6
7
  from polynode.trading.eip712 import (
7
8
  build_order_payload,
8
9
  build_order_payload_v2,
10
+ create_signed_order_v2,
9
11
  compute_amounts,
10
12
  EXCHANGE_DOMAIN,
11
13
  EXCHANGE_DOMAIN_V2,
@@ -76,6 +78,147 @@ def test_safe_address_derivation():
76
78
  pass # Skip if trading deps not installed
77
79
 
78
80
 
81
+ def test_detect_wallet_type_uses_deposit_wallet_check(monkeypatch):
82
+ """Regression: deposit wallets must be checked as WALLET, not SAFE."""
83
+ from polynode.trading import onboarding
84
+
85
+ eoa = "0x7b3099fe73372D900a9F0F62CdcA261755f7EA60"
86
+ deposit_wallet = "0x58b8048C8f09C2FCc4914258587a8E7E0cc21D76"
87
+
88
+ async def safe_not_deployed(_address: str) -> bool:
89
+ return False
90
+
91
+ async def deposit_wallet_deployed(address: str) -> bool:
92
+ return address == deposit_wallet
93
+
94
+ monkeypatch.setattr(onboarding, "is_safe_deployed", safe_not_deployed)
95
+ monkeypatch.setattr(onboarding, "is_deposit_wallet_deployed", deposit_wallet_deployed)
96
+
97
+ detected = asyncio.run(onboarding.detect_wallet_type(eoa))
98
+
99
+ assert detected["signature_type"] == SignatureType.POLY_1271
100
+ assert detected["funder_address"] == deposit_wallet
101
+
102
+
103
+ def test_v2_poly1271_order_uses_deposit_wallet_as_signer():
104
+ """POLY_1271 V2 orders need maker and signer set to the deposit wallet."""
105
+ signer_address = "0x7b3099fe73372D900a9F0F62CdcA261755f7EA60"
106
+ deposit_wallet = "0x58b8048C8f09C2FCc4914258587a8E7E0cc21D76"
107
+ signed_payload = {}
108
+
109
+ async def fake_sign(payload):
110
+ signed_payload.update(payload.message["contents"])
111
+ return "0x" + "11" * 64 + "01"
112
+
113
+ order = asyncio.run(create_signed_order_v2(
114
+ fake_sign,
115
+ signer_address=signer_address,
116
+ funder_address=deposit_wallet,
117
+ token_id="12345",
118
+ price=0.5,
119
+ size=1,
120
+ side="BUY",
121
+ signature_type=SignatureType.POLY_1271,
122
+ timestamp_ms=1712345678000,
123
+ ))
124
+
125
+ assert signed_payload["maker"] == deposit_wallet
126
+ assert signed_payload["signer"] == deposit_wallet
127
+ assert order["maker"] == deposit_wallet
128
+ assert order["signer"] == deposit_wallet
129
+ assert order["signatureType"] == 3
130
+
131
+
132
+ def test_v2_poly1271_inner_signature_normalizes_recovery_id():
133
+ """POLY_1271 TypedDataSign inner signatures need Ethereum v=27/28."""
134
+ async def fake_sign(_payload):
135
+ return "0x" + "11" * 64 + "01"
136
+
137
+ order = asyncio.run(create_signed_order_v2(
138
+ fake_sign,
139
+ signer_address="0x7b3099fe73372D900a9F0F62CdcA261755f7EA60",
140
+ funder_address="0x58b8048C8f09C2FCc4914258587a8E7E0cc21D76",
141
+ token_id="12345",
142
+ price=0.5,
143
+ size=1,
144
+ side="BUY",
145
+ signature_type=SignatureType.POLY_1271,
146
+ timestamp_ms=1712345678000,
147
+ ))
148
+
149
+ wrapped = bytes.fromhex(order["signature"][2:])
150
+ assert len(wrapped) > 65
151
+ assert wrapped[64] == 28
152
+
153
+
154
+ def test_ensure_ready_allows_explicit_poly1271_funder(monkeypatch, tmp_path):
155
+ """Existing deposit-wallet users can force type 3 plus a known funder."""
156
+ from polynode.trading import trader as trader_mod
157
+ from polynode.trading.trader import PolyNodeTrader
158
+ from polynode.trading.types import ExchangeVersion, TraderConfig
159
+
160
+ private_key = "0x" + "01" * 32
161
+ deposit_wallet = "0x58b8048C8f09C2FCc4914258587a8E7E0cc21D76"
162
+
163
+ async def deposit_wallet_deployed(address: str) -> bool:
164
+ return address == deposit_wallet
165
+
166
+ async def fake_create_clob_credentials(*_args, **_kwargs):
167
+ return {
168
+ "apiKey": "key",
169
+ "apiSecret": base64.b64encode(b"secret").decode(),
170
+ "apiPassphrase": "pass",
171
+ }
172
+
173
+ monkeypatch.setattr(trader_mod, "is_deposit_wallet_deployed", deposit_wallet_deployed)
174
+ monkeypatch.setattr(trader_mod, "create_clob_credentials", fake_create_clob_credentials)
175
+
176
+ trader = PolyNodeTrader(TraderConfig(
177
+ db_path=str(tmp_path / "trading.db"),
178
+ exchange_version=ExchangeVersion.V2,
179
+ ))
180
+
181
+ status = asyncio.run(trader.ensure_ready(
182
+ private_key,
183
+ type=SignatureType.POLY_1271,
184
+ funder_address=deposit_wallet,
185
+ ))
186
+
187
+ assert status.signature_type == SignatureType.POLY_1271
188
+ assert status.funder_address == deposit_wallet
189
+ assert status.safe_deployed is True
190
+ assert "explicit_type_3" in status.actions
191
+ assert "deposit_wallet_already_deployed" in status.actions
192
+
193
+ try:
194
+ from eth_account import Account
195
+ except ImportError:
196
+ return
197
+
198
+ wallet = Account.from_key(private_key).address
199
+ wrong_safe = "0xb8f9248a67962c9FE37E70e7a6E38e100b7613f1"
200
+ trader.link_credentials(
201
+ wallet=wallet,
202
+ api_key="key",
203
+ api_secret=base64.b64encode(b"secret").decode(),
204
+ api_passphrase="pass",
205
+ signature_type=SignatureType.POLY_GNOSIS_SAFE,
206
+ funder_address=wrong_safe,
207
+ )
208
+
209
+ status = asyncio.run(trader.ensure_ready(
210
+ private_key,
211
+ type=SignatureType.POLY_1271,
212
+ funder_address=deposit_wallet,
213
+ ))
214
+ exported = trader.export_wallet(wallet)
215
+
216
+ assert status.signature_type == SignatureType.POLY_1271
217
+ assert exported is not None
218
+ assert exported.signature_type == SignatureType.POLY_1271
219
+ assert exported.funder_address == deposit_wallet
220
+
221
+
79
222
  # ── V2 constants ──
80
223
 
81
224
 
@@ -1 +0,0 @@
1
- __version__ = "0.10.6"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes