polynode 0.9.0__py3-none-any.whl → 0.9.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
polynode/_version.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.9.0"
1
+ __version__ = "0.9.1"
@@ -54,7 +54,10 @@ COLLATERAL_ONRAMP = "0x93070a847efef7f70739046a929d47a521f5b8ee"
54
54
  COLLATERAL_OFFRAMP = "0x2957922eb93258b93368531d39facca3b4dc5854"
55
55
 
56
56
  # Fee Escrow
57
+ # V1 (USDC.e collateral, paired with the V1 CLOB)
57
58
  FEE_ESCROW_ADDRESS = "0xa11D28433B79D0A88F3119b16A090075752258EA"
59
+ # V2 (pUSD collateral, paired with the V2 CLOB). Deployed 2026-04-24.
60
+ FEE_ESCROW_ADDRESS_V2 = "0x3A43D88ef8Aae4dF5a50B3abf67122CAAeEF7c9F"
58
61
 
59
62
  # Metadata cache TTL (5 minutes)
60
63
  META_TTL_SECONDS = 300
@@ -14,10 +14,15 @@ from typing import Any
14
14
 
15
15
  import httpx
16
16
 
17
- from .constants import CHAIN_ID, FEE_ESCROW_ADDRESS
18
- from .types import Eip712Payload
17
+ from .constants import CHAIN_ID, FEE_ESCROW_ADDRESS, FEE_ESCROW_ADDRESS_V2
18
+ from .types import Eip712Payload, ExchangeVersion
19
19
 
20
20
  # ── EIP-712 Types ──
21
+ #
22
+ # Both V1 and V2 FeeEscrow share the same FeeAuth struct typehash. They differ only in
23
+ # the EIP-712 domain: V1 is ("PolyNodeFeeEscrow", "1", V1 contract); V2 is
24
+ # ("PolyNodeFeeEscrowV2", "2", V2 contract). Signing against the wrong domain produces
25
+ # a digest the contract won't accept — `pullFee` reverts with `InvalidSignature`.
21
26
 
22
27
  FEE_AUTH_DOMAIN = {
23
28
  "name": "PolyNodeFeeEscrow",
@@ -26,6 +31,13 @@ FEE_AUTH_DOMAIN = {
26
31
  "verifyingContract": FEE_ESCROW_ADDRESS,
27
32
  }
28
33
 
34
+ FEE_AUTH_DOMAIN_V2 = {
35
+ "name": "PolyNodeFeeEscrowV2",
36
+ "version": "2",
37
+ "chainId": CHAIN_ID,
38
+ "verifyingContract": FEE_ESCROW_ADDRESS_V2,
39
+ }
40
+
29
41
  FEE_AUTH_TYPES = {
30
42
  "FeeAuth": [
31
43
  {"name": "orderId", "type": "bytes32"},
@@ -40,6 +52,16 @@ FEE_AUTH_TYPES = {
40
52
  ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"
41
53
 
42
54
 
55
+ def fee_auth_domain_for(version: ExchangeVersion) -> dict:
56
+ """Return the EIP-712 FeeAuth domain for a given exchange version."""
57
+ return FEE_AUTH_DOMAIN_V2 if version == ExchangeVersion.V2 else FEE_AUTH_DOMAIN
58
+
59
+
60
+ def fee_escrow_address_for(version: ExchangeVersion) -> str:
61
+ """Return the FeeEscrow contract address for a given exchange version."""
62
+ return FEE_ESCROW_ADDRESS_V2 if version == ExchangeVersion.V2 else FEE_ESCROW_ADDRESS
63
+
64
+
43
65
  # ── Fee Calculation ──
44
66
 
45
67
  def calculate_fee(price: float, size: float, fee_bps: int) -> int:
@@ -67,8 +89,17 @@ def generate_escrow_order_id() -> str:
67
89
 
68
90
  # ── Nonce Fetching ──
69
91
 
70
- async def fetch_escrow_nonce(rpc_url: str, signer_address: str) -> int:
71
- """Fetch the current nonce for a signer from the FeeEscrow contract via eth_call."""
92
+ async def fetch_escrow_nonce(
93
+ rpc_url: str,
94
+ signer_address: str,
95
+ escrow_address: str = FEE_ESCROW_ADDRESS,
96
+ ) -> int:
97
+ """Fetch the current nonce for a signer from a FeeEscrow contract via eth_call.
98
+
99
+ Pass `fee_escrow_address_for(exchange_version)` for the `escrow_address` argument
100
+ when the caller's exchange version is known (V2 customers must read nonces from
101
+ the V2 contract — nonces on V1 and V2 are tracked independently).
102
+ """
72
103
  # getNonce(address) selector = 0x2d0335ab
73
104
  addr = signer_address.lower().replace("0x", "").zfill(64)
74
105
  data = "0x2d0335ab" + addr
@@ -80,7 +111,7 @@ async def fetch_escrow_nonce(rpc_url: str, signer_address: str) -> int:
80
111
  "jsonrpc": "2.0",
81
112
  "id": 1,
82
113
  "method": "eth_call",
83
- "params": [{"to": FEE_ESCROW_ADDRESS, "data": data}, "latest"],
114
+ "params": [{"to": escrow_address, "data": data}, "latest"],
84
115
  },
85
116
  )
86
117
  result = resp.json()
@@ -103,6 +134,9 @@ class FeeAuthRequest:
103
134
  signature: str
104
135
  affiliate: str
105
136
  affiliate_share_bps: int
137
+ # V2-routing hint for the cosigner. Set when exchange_version == V2 so the
138
+ # cosigner picks the V2 operator; absent for V1 (preserves backwards compat).
139
+ escrow_contract: str | None = None
106
140
 
107
141
 
108
142
  async def sign_fee_auth(
@@ -116,19 +150,22 @@ async def sign_fee_auth(
116
150
  nonce: int,
117
151
  affiliate: str | None = None,
118
152
  affiliate_share_bps: int | None = None,
153
+ exchange_version: ExchangeVersion = ExchangeVersion.V1,
119
154
  ) -> FeeAuthRequest:
120
155
  """Sign a FeeAuth EIP-712 message.
121
156
 
122
157
  Args:
123
158
  sign_typed_data: Callable that signs an Eip712Payload and returns a hex signature.
124
159
  escrow_order_id: Random bytes32 hex string.
125
- payer: Safe wallet address (where USDC is pulled from).
160
+ payer: Safe wallet address (where collateral is pulled from — USDC.e on V1, pUSD on V2).
126
161
  signer_address: EOA address (signs the message).
127
- fee_amount: Fee in raw USDC (6 decimals).
162
+ fee_amount: Fee in raw collateral units (6 decimals).
128
163
  deadline: Unix timestamp — authorization expires after this.
129
164
  nonce: Signer's current nonce from the escrow contract.
130
165
  affiliate: Partner wallet address.
131
166
  affiliate_share_bps: Partner's share of fee in bps.
167
+ exchange_version: V1 (default, USDC.e) or V2 (pUSD). Selects the EIP-712 domain
168
+ and the escrow contract address the cosigner should route to.
132
169
 
133
170
  Returns:
134
171
  FeeAuthRequest ready for the cosigner.
@@ -143,7 +180,7 @@ async def sign_fee_auth(
143
180
  }
144
181
 
145
182
  payload = Eip712Payload(
146
- domain=FEE_AUTH_DOMAIN,
183
+ domain=fee_auth_domain_for(exchange_version),
147
184
  types=FEE_AUTH_TYPES,
148
185
  primary_type="FeeAuth",
149
186
  message=message,
@@ -153,6 +190,10 @@ async def sign_fee_auth(
153
190
  if not signature.startswith("0x"):
154
191
  signature = f"0x{signature}"
155
192
 
193
+ escrow_contract = (
194
+ FEE_ESCROW_ADDRESS_V2 if exchange_version == ExchangeVersion.V2 else None
195
+ )
196
+
156
197
  return FeeAuthRequest(
157
198
  escrow_order_id=escrow_order_id,
158
199
  payer=payer,
@@ -163,4 +204,5 @@ async def sign_fee_auth(
163
204
  signature=signature,
164
205
  affiliate=affiliate or ZERO_ADDRESS,
165
206
  affiliate_share_bps=affiliate_share_bps if affiliate_share_bps is not None else 10000,
207
+ escrow_contract=escrow_contract,
166
208
  )
@@ -29,6 +29,7 @@ from .constants import (
29
29
  USDC,
30
30
  V2_SPENDERS,
31
31
  FEE_ESCROW_ADDRESS,
32
+ FEE_ESCROW_ADDRESS_V2,
32
33
  )
33
34
  from .types import ApprovalStatus, BalanceInfo, ExchangeVersion, SignatureType
34
35
 
@@ -189,9 +190,16 @@ async def check_approvals(
189
190
  approved = ctf_contract.functions.isApprovedForAll(funder, spender_addr).call()
190
191
  ctf_status[name] = approved
191
192
 
192
- # Fee Escrow USDC approval (always USDC.e regardless of exchange version)
193
- usdc_contract = w3.eth.contract(address=Web3.to_checksum_address(USDC), abi=erc20_abi)
194
- escrow_allowance = usdc_contract.functions.allowance(funder, Web3.to_checksum_address(FEE_ESCROW_ADDRESS)).call()
193
+ # Fee Escrow approval. V1 approves USDC.e for the V1 contract; V2 approves pUSD
194
+ # for the V2 contract. pullFee fails with `TransferFailed` if the approval is
195
+ # missing on the version the caller's orders are routed to.
196
+ if version == ExchangeVersion.V2:
197
+ escrow_token_contract = w3.eth.contract(address=Web3.to_checksum_address(POLY_USD), abi=erc20_abi)
198
+ escrow_addr = Web3.to_checksum_address(FEE_ESCROW_ADDRESS_V2)
199
+ else:
200
+ escrow_token_contract = w3.eth.contract(address=Web3.to_checksum_address(USDC), abi=erc20_abi)
201
+ escrow_addr = Web3.to_checksum_address(FEE_ESCROW_ADDRESS)
202
+ escrow_allowance = escrow_token_contract.functions.allowance(funder, escrow_addr).call()
195
203
  usdc_status["fee_escrow"] = escrow_allowance >= threshold
196
204
 
197
205
  all_approved = all(usdc_status.values()) and all(ctf_status.values())
@@ -273,9 +281,16 @@ async def set_approvals(
273
281
  tx_hashes.append(tx_hash.hex())
274
282
  nonce += 1
275
283
 
276
- # Fee Escrow USDC approval (always USDC.e regardless of exchange version)
277
- escrow_contract = w3.eth.contract(address=Web3.to_checksum_address(USDC), abi=erc20_abi)
278
- tx = escrow_contract.functions.approve(Web3.to_checksum_address(FEE_ESCROW_ADDRESS), max_uint).build_transaction({
284
+ # Fee Escrow collateral approval V1 approves USDC.e for V1 FeeEscrow;
285
+ # V2 approves pUSD for V2 FeeEscrow.
286
+ if version == ExchangeVersion.V2:
287
+ escrow_token = POLY_USD
288
+ escrow_target = FEE_ESCROW_ADDRESS_V2
289
+ else:
290
+ escrow_token = USDC
291
+ escrow_target = FEE_ESCROW_ADDRESS
292
+ escrow_token_contract = w3.eth.contract(address=Web3.to_checksum_address(escrow_token), abi=erc20_abi)
293
+ tx = escrow_token_contract.functions.approve(Web3.to_checksum_address(escrow_target), max_uint).build_transaction({
279
294
  "from": account.address,
280
295
  "nonce": nonce,
281
296
  "chainId": CHAIN_ID,
@@ -0,0 +1,303 @@
1
+ """Polymarket builder-relayer client (Python port).
2
+
3
+ Ports the minimum needed from @polymarket/builder-relayer-client +
4
+ @polymarket/builder-signing-sdk to submit gasless Safe transactions.
5
+
6
+ Flow:
7
+ 1. GET {relayer}/nonce?address={eoa}&type=SAFE → next SafeTx nonce
8
+ 2. Build multisend (if >1 tx) or single tx
9
+ 3. EIP-712 hash SafeTx(domain={chainId, verifyingContract=safe}, types, msg)
10
+ 4. EIP-191 personal_sign the hash with EOA private key
11
+ 5. Pack r||s||v (v adjusted by +4 for Gnosis)
12
+ 6. POST {relayer}/submit with builder HMAC headers
13
+ 7. Poll {relayer}/transaction?id=... until STATE_MINED / STATE_CONFIRMED
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import asyncio
18
+ import base64
19
+ import hashlib
20
+ import hmac
21
+ import json
22
+ import time
23
+ from dataclasses import dataclass
24
+ from typing import Any
25
+
26
+ import httpx
27
+
28
+ from .constants import CHAIN_ID, RELAYER_HOST, SAFE_FACTORY, SAFE_MULTISEND
29
+ from .onboarding import derive_safe_address
30
+
31
+
32
+ @dataclass
33
+ class BuilderCreds:
34
+ key: str
35
+ secret: str
36
+ passphrase: str
37
+
38
+
39
+ ZERO_ADDR = "0x" + "0" * 40
40
+
41
+
42
+ # ── EIP-712 helpers ──
43
+
44
+ def _keccak(data: bytes) -> bytes:
45
+ from eth_utils import keccak as _k
46
+ return _k(data)
47
+
48
+
49
+ # Compute once on module load
50
+ def _safe_tx_type_hash() -> bytes:
51
+ return _keccak(
52
+ b"SafeTx(address to,uint256 value,bytes data,uint8 operation,"
53
+ b"uint256 safeTxGas,uint256 baseGas,uint256 gasPrice,address gasToken,"
54
+ b"address refundReceiver,uint256 nonce)"
55
+ )
56
+
57
+
58
+ def _domain_type_hash() -> bytes:
59
+ return _keccak(b"EIP712Domain(uint256 chainId,address verifyingContract)")
60
+
61
+
62
+ def _pad32(h: bytes) -> bytes:
63
+ return h if len(h) == 32 else (b"\x00" * (32 - len(h))) + h
64
+
65
+
66
+ def _encode_address(addr: str) -> bytes:
67
+ return _pad32(bytes.fromhex(addr.lower().removeprefix("0x")))
68
+
69
+
70
+ def _encode_uint256(n: int) -> bytes:
71
+ return int(n).to_bytes(32, "big", signed=False)
72
+
73
+
74
+ def _domain_separator(chain_id: int, verifying_contract: str) -> bytes:
75
+ return _keccak(
76
+ _domain_type_hash() + _encode_uint256(chain_id) + _encode_address(verifying_contract)
77
+ )
78
+
79
+
80
+ def _safe_tx_struct_hash(
81
+ to: str, value: int, data: bytes, operation: int,
82
+ safe_tx_gas: int, base_gas: int, gas_price: int,
83
+ gas_token: str, refund_receiver: str, nonce: int,
84
+ ) -> bytes:
85
+ data_hash = _keccak(data)
86
+ encoded = (
87
+ _safe_tx_type_hash()
88
+ + _encode_address(to)
89
+ + _encode_uint256(value)
90
+ + data_hash
91
+ + _encode_uint256(operation)
92
+ + _encode_uint256(safe_tx_gas)
93
+ + _encode_uint256(base_gas)
94
+ + _encode_uint256(gas_price)
95
+ + _encode_address(gas_token)
96
+ + _encode_address(refund_receiver)
97
+ + _encode_uint256(nonce)
98
+ )
99
+ return _keccak(encoded)
100
+
101
+
102
+ def _eip712_digest(chain_id: int, safe_addr: str, struct_hash: bytes) -> bytes:
103
+ domain_sep = _domain_separator(chain_id, safe_addr)
104
+ return _keccak(b"\x19\x01" + domain_sep + struct_hash)
105
+
106
+
107
+ # ── Safe multisend encoding ──
108
+
109
+ def _encode_multisend(txns: list[dict[str, Any]]) -> bytes:
110
+ """Encode txns into the multiSend bytes arg.
111
+ Each tx packed as: uint8 operation | address to | uint256 value | uint256 dataLen | bytes data
112
+ """
113
+ parts: list[bytes] = []
114
+ for tx in txns:
115
+ op = int(tx.get("operation", 0))
116
+ to = tx["to"]
117
+ value = int(tx.get("value", 0))
118
+ data = tx["data"]
119
+ if isinstance(data, str):
120
+ data_bytes = bytes.fromhex(data.removeprefix("0x"))
121
+ else:
122
+ data_bytes = data
123
+ parts.append(
124
+ op.to_bytes(1, "big")
125
+ + bytes.fromhex(to.lower().removeprefix("0x"))
126
+ + value.to_bytes(32, "big")
127
+ + len(data_bytes).to_bytes(32, "big")
128
+ + data_bytes
129
+ )
130
+ return b"".join(parts)
131
+
132
+
133
+ def _encode_multisend_calldata(txns: list[dict[str, Any]]) -> str:
134
+ """Wrap encoded txns in multiSend(bytes) ABI call."""
135
+ # multiSend(bytes) selector = keccak256("multiSend(bytes)")[:4] = 0x8d80ff0a
136
+ inner = _encode_multisend(txns)
137
+ # ABI-encode single `bytes` arg: offset(32) + length(32) + data (padded to 32)
138
+ padded_len = ((len(inner) + 31) // 32) * 32
139
+ padded = inner + b"\x00" * (padded_len - len(inner))
140
+ encoded = (
141
+ bytes.fromhex("8d80ff0a")
142
+ + _encode_uint256(32) # offset to bytes arg
143
+ + _encode_uint256(len(inner))
144
+ + padded
145
+ )
146
+ return "0x" + encoded.hex()
147
+
148
+
149
+ # ── Builder HMAC ──
150
+
151
+ def _build_hmac_headers(creds: BuilderCreds, method: str, path: str, body: str | None = None) -> dict[str, str]:
152
+ timestamp = str(int(time.time()))
153
+ message = timestamp + method + path + (body or "")
154
+ # Secret may be url-safe base64 (`-` / `_`); normalize before decoding.
155
+ secret_norm = creds.secret.replace("-", "+").replace("_", "/")
156
+ key_bytes = base64.b64decode(secret_norm)
157
+ mac = hmac.new(key_bytes, message.encode(), hashlib.sha256).digest()
158
+ sig = base64.b64encode(mac).decode().replace("+", "-").replace("/", "_")
159
+ return {
160
+ "POLY_BUILDER_API_KEY": creds.key,
161
+ "POLY_BUILDER_PASSPHRASE": creds.passphrase,
162
+ "POLY_BUILDER_SIGNATURE": sig,
163
+ "POLY_BUILDER_TIMESTAMP": timestamp,
164
+ }
165
+
166
+
167
+ # ── Signature packing ──
168
+
169
+ def _pack_sig(sig_hex: str) -> str:
170
+ """Split 65-byte sig into r||s||v with Gnosis v-adjustment (+4)."""
171
+ raw = bytes.fromhex(sig_hex.removeprefix("0x"))
172
+ assert len(raw) == 65, f"expected 65-byte sig, got {len(raw)}"
173
+ r, s, v = raw[:32], raw[32:64], raw[64]
174
+ # Adjust v: 0/1 → 31/32, 27/28 → 31/32 (eth_sign variant per Gnosis spec)
175
+ if v in (0, 1):
176
+ v += 31
177
+ elif v in (27, 28):
178
+ v += 4
179
+ else:
180
+ raise ValueError(f"invalid sig v byte: {v}")
181
+ return "0x" + (r + s + v.to_bytes(1, "big")).hex()
182
+
183
+
184
+ # ── RelayClient ──
185
+
186
+ class RelayClient:
187
+ """Minimal Polymarket relayer client for Safe transactions."""
188
+
189
+ def __init__(self, builder_creds: BuilderCreds, relayer_url: str = RELAYER_HOST, chain_id: int = CHAIN_ID):
190
+ self.relayer_url = relayer_url.rstrip("/")
191
+ self.chain_id = chain_id
192
+ self.builder_creds = builder_creds
193
+
194
+ async def execute_safe(
195
+ self,
196
+ signer_pk: str,
197
+ eoa_address: str,
198
+ txns: list[dict[str, Any]],
199
+ ) -> str:
200
+ """Build, sign, submit, and wait for a batch of Safe transactions.
201
+
202
+ Returns the on-chain transaction hash.
203
+ """
204
+ if not txns:
205
+ raise ValueError("no transactions to execute")
206
+
207
+ try:
208
+ from eth_account import Account
209
+ from eth_account.messages import encode_defunct
210
+ except ImportError:
211
+ raise ImportError("eth-account required for Safe relayer")
212
+
213
+ safe_addr = derive_safe_address(eoa_address)
214
+
215
+ # 1. Get nonce
216
+ async with httpx.AsyncClient(timeout=20.0) as client:
217
+ r = await client.get(
218
+ f"{self.relayer_url}/nonce",
219
+ params={"address": eoa_address, "type": "SAFE"},
220
+ )
221
+ r.raise_for_status()
222
+ nonce_payload = r.json()
223
+ nonce = int(nonce_payload["nonce"])
224
+
225
+ # 2. Aggregate (single tx → direct, multi → multisend DelegateCall)
226
+ if len(txns) == 1:
227
+ tx = txns[0]
228
+ to_addr = tx["to"]
229
+ data = tx["data"]
230
+ operation = int(tx.get("operation", 0))
231
+ else:
232
+ to_addr = SAFE_MULTISEND
233
+ data = _encode_multisend_calldata(
234
+ [{**t, "operation": int(t.get("operation", 0))} for t in txns]
235
+ )
236
+ operation = 1 # DelegateCall
237
+
238
+ # 3. Compute EIP-712 digest
239
+ data_bytes = bytes.fromhex(data.removeprefix("0x")) if isinstance(data, str) else data
240
+ struct_hash = _safe_tx_struct_hash(
241
+ to=to_addr, value=0, data=data_bytes, operation=operation,
242
+ safe_tx_gas=0, base_gas=0, gas_price=0,
243
+ gas_token=ZERO_ADDR, refund_receiver=ZERO_ADDR,
244
+ nonce=nonce,
245
+ )
246
+ digest = _eip712_digest(self.chain_id, safe_addr, struct_hash)
247
+
248
+ # 4. EIP-191 personal_sign on the digest
249
+ account = Account.from_key(signer_pk if signer_pk.startswith("0x") else f"0x{signer_pk}")
250
+ signed = account.sign_message(encode_defunct(primitive=digest))
251
+ packed_sig = _pack_sig(signed.signature.hex())
252
+
253
+ # 5. Build submit payload
254
+ req = {
255
+ "from": eoa_address,
256
+ "to": to_addr,
257
+ "proxyWallet": safe_addr,
258
+ "data": data if isinstance(data, str) else "0x" + data.hex(),
259
+ "nonce": str(nonce),
260
+ "signature": packed_sig,
261
+ "signatureParams": {
262
+ "gasPrice": "0",
263
+ "operation": str(operation),
264
+ "safeTxnGas": "0",
265
+ "baseGas": "0",
266
+ "gasToken": ZERO_ADDR,
267
+ "refundReceiver": ZERO_ADDR,
268
+ },
269
+ "type": "SAFE",
270
+ "metadata": "",
271
+ }
272
+ body = json.dumps(req, separators=(",", ":"))
273
+
274
+ # 6. POST /submit with builder HMAC headers
275
+ headers = _build_hmac_headers(self.builder_creds, "POST", "/submit", body)
276
+ headers["Content-Type"] = "application/json"
277
+ async with httpx.AsyncClient(timeout=30.0) as client:
278
+ r = await client.post(f"{self.relayer_url}/submit", content=body, headers=headers)
279
+ if not r.is_success:
280
+ raise RuntimeError(f"relayer /submit failed: {r.status_code} {r.text}")
281
+ resp = r.json()
282
+ tx_id = resp["transactionID"]
283
+
284
+ # 7. Poll /transaction until mined
285
+ for _ in range(30): # up to 60s
286
+ await asyncio.sleep(2)
287
+ async with httpx.AsyncClient(timeout=10.0) as client:
288
+ r = await client.get(
289
+ f"{self.relayer_url}/transaction",
290
+ params={"id": tx_id},
291
+ )
292
+ if not r.is_success:
293
+ continue
294
+ rows = r.json()
295
+ if not rows:
296
+ continue
297
+ row = rows[0] if isinstance(rows, list) else rows
298
+ state = row.get("state")
299
+ if state in ("STATE_MINED", "STATE_CONFIRMED"):
300
+ return row["transactionHash"]
301
+ if state in ("STATE_FAILED", "STATE_REVERTED"):
302
+ raise RuntimeError(f"tx {tx_id} failed on-chain: state={state} hash={row.get('transactionHash')}")
303
+ raise RuntimeError(f"tx {tx_id} timed out waiting for mined state")
@@ -26,7 +26,7 @@ from .constants import (
26
26
  )
27
27
  from .cosigner import build_l2_headers, send_via_cosigner
28
28
  from .eip712 import create_signed_order, create_signed_order_v2
29
- from .escrow import calculate_fee, generate_escrow_order_id, fetch_escrow_nonce, sign_fee_auth
29
+ from .escrow import calculate_fee, fee_escrow_address_for, fetch_escrow_nonce, generate_escrow_order_id, sign_fee_auth
30
30
  from .onboarding import (
31
31
  check_approvals as check_approvals_onchain,
32
32
  check_balance as check_balance_onchain,
@@ -420,6 +420,9 @@ class PolyNodeTrader:
420
420
  async def wrap_to_polyusd(self, amount: int) -> str:
421
421
  """Wrap USDC.e into PolyUSD via the Collateral Onramp.
422
422
 
423
+ Routes through the Polymarket relayer (gasless) for Safe/Proxy wallets,
424
+ and sends directly for EOA wallets.
425
+
423
426
  Args:
424
427
  amount: Amount in raw units (6 decimals, e.g. 1_000_000 = $1).
425
428
 
@@ -428,7 +431,6 @@ class PolyNodeTrader:
428
431
  """
429
432
  try:
430
433
  from web3 import Web3
431
- from eth_account import Account
432
434
  except ImportError:
433
435
  raise ImportError("web3 required. Install with: pip install polynode[trading]")
434
436
 
@@ -445,42 +447,55 @@ class PolyNodeTrader:
445
447
 
446
448
  usdc_contract = w3.eth.contract(address=Web3.to_checksum_address(USDC), abi=erc20_abi)
447
449
  onramp_addr = Web3.to_checksum_address(COLLATERAL_ONRAMP)
448
- signer_addr = Web3.to_checksum_address(signer.address)
450
+ funder_addr = Web3.to_checksum_address(funder)
449
451
 
450
- # Build account from the signer's private key (requires sign_message support)
451
- # For private-key signers, we can reconstruct the account
452
- if not signer._private_key:
453
- raise RuntimeError("wrap_to_polyusd requires a private key signer (not RouterSigner)")
452
+ allowance = usdc_contract.functions.allowance(funder_addr, onramp_addr).call()
453
+
454
+ # Build calldata for both ops
455
+ approve_data = usdc_contract.encode_abi("approve", args=[onramp_addr, 2**256 - 1])
454
456
 
455
- nonce = w3.eth.get_transaction_count(signer_addr)
457
+ wrap_abi = [
458
+ {"inputs": [{"name": "token", "type": "address"}, {"name": "funder", "type": "address"}, {"name": "amount", "type": "uint256"}], "name": "wrap", "outputs": [], "stateMutability": "nonpayable", "type": "function"},
459
+ ]
460
+ onramp_contract = w3.eth.contract(address=onramp_addr, abi=wrap_abi)
461
+ wrap_data = onramp_contract.encode_abi("wrap", args=[
462
+ Web3.to_checksum_address(USDC), funder_addr, int(amount)
463
+ ])
464
+
465
+ # Safe / Proxy wallets → route via relayer (gasless)
466
+ if creds.signature_type in (SignatureType.POLY_GNOSIS_SAFE, SignatureType.POLY_PROXY):
467
+ from .relayer import BuilderCreds, RelayClient
468
+ bc_raw = self._builder_credentials
469
+ if not bc_raw:
470
+ raise RuntimeError(
471
+ "wrap_to_polyusd on a Safe/Proxy wallet requires builder_credentials "
472
+ "in TraderConfig. Pass your Polymarket builder key/secret/passphrase."
473
+ )
474
+ bc = BuilderCreds(key=bc_raw.key, secret=bc_raw.secret, passphrase=bc_raw.passphrase)
475
+ rc = RelayClient(bc, rpc_url=self._rpc_url) if False else RelayClient(bc) # default relayer
476
+ txns = []
477
+ if allowance < amount:
478
+ txns.append({"to": USDC, "data": approve_data, "value": 0})
479
+ txns.append({"to": COLLATERAL_ONRAMP, "data": wrap_data, "value": 0})
480
+ return await rc.execute_safe(signer._private_key or "", signer.address, txns)
481
+
482
+ # EOA path: direct send
483
+ if not signer._private_key:
484
+ raise RuntimeError("wrap_to_polyusd requires a private key signer for EOA wallets")
456
485
 
457
- # Step 1: Check and set USDC.e allowance for Onramp
458
- allowance = usdc_contract.functions.allowance(signer_addr, onramp_addr).call()
486
+ nonce = w3.eth.get_transaction_count(funder_addr)
459
487
  if allowance < amount:
460
- max_uint = 2**256 - 1
461
- tx = usdc_contract.functions.approve(onramp_addr, max_uint).build_transaction({
462
- "from": signer_addr,
463
- "nonce": nonce,
464
- "chainId": 137,
488
+ tx = usdc_contract.functions.approve(onramp_addr, 2**256 - 1).build_transaction({
489
+ "from": funder_addr, "nonce": nonce, "chainId": 137,
465
490
  })
466
491
  signed = w3.eth.account.sign_transaction(tx, signer._private_key)
467
492
  w3.eth.send_raw_transaction(signed.raw_transaction)
468
493
  nonce += 1
469
494
 
470
- # Step 2: Call wrap(address token, address funder, uint256 amount) on Onramp
471
- # wrap selector: 0x62355638
472
- wrap_abi = [
473
- {"inputs": [{"name": "token", "type": "address"}, {"name": "funder", "type": "address"}, {"name": "amount", "type": "uint256"}], "name": "wrap", "outputs": [], "stateMutability": "nonpayable", "type": "function"},
474
- ]
475
- onramp_contract = w3.eth.contract(address=onramp_addr, abi=wrap_abi)
476
495
  tx = onramp_contract.functions.wrap(
477
- Web3.to_checksum_address(USDC),
478
- Web3.to_checksum_address(funder),
479
- amount,
496
+ Web3.to_checksum_address(USDC), funder_addr, int(amount),
480
497
  ).build_transaction({
481
- "from": signer_addr,
482
- "nonce": nonce,
483
- "chainId": 137,
498
+ "from": funder_addr, "nonce": nonce, "chainId": 137,
484
499
  })
485
500
  signed = w3.eth.account.sign_transaction(tx, signer._private_key)
486
501
  tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction)
@@ -489,6 +504,9 @@ class PolyNodeTrader:
489
504
  async def unwrap_from_polyusd(self, amount: int) -> str:
490
505
  """Unwrap PolyUSD back into USDC.e via the Collateral Offramp.
491
506
 
507
+ Routes through the Polymarket relayer (gasless) for Safe/Proxy wallets,
508
+ and sends directly for EOA wallets.
509
+
492
510
  Args:
493
511
  amount: Amount in raw units (6 decimals, e.g. 1_000_000 = $1).
494
512
 
@@ -506,46 +524,60 @@ class PolyNodeTrader:
506
524
 
507
525
  w3 = Web3(Web3.HTTPProvider(self._rpc_url))
508
526
 
509
- if not signer._private_key:
510
- raise RuntimeError("unwrap_from_polyusd requires a private key signer (not RouterSigner)")
511
-
512
- signer_addr = Web3.to_checksum_address(signer.address)
527
+ funder_addr = Web3.to_checksum_address(funder)
513
528
  offramp_addr = Web3.to_checksum_address(COLLATERAL_OFFRAMP)
514
529
 
515
- # Check and set PolyUSD allowance for Offramp
516
530
  erc20_abi = [
517
531
  {"inputs": [{"name": "spender", "type": "address"}, {"name": "amount", "type": "uint256"}], "name": "approve", "outputs": [{"name": "", "type": "bool"}], "stateMutability": "nonpayable", "type": "function"},
518
532
  {"inputs": [{"name": "owner", "type": "address"}, {"name": "spender", "type": "address"}], "name": "allowance", "outputs": [{"name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"},
519
533
  ]
520
534
  polyusd_contract = w3.eth.contract(address=Web3.to_checksum_address(POLY_USD), abi=erc20_abi)
535
+ allowance = polyusd_contract.functions.allowance(funder_addr, offramp_addr).call()
536
+
537
+ approve_data = polyusd_contract.encode_abi("approve", args=[offramp_addr, 2**256 - 1])
521
538
 
522
- nonce = w3.eth.get_transaction_count(signer_addr)
539
+ unwrap_abi = [
540
+ {"inputs": [{"name": "token", "type": "address"}, {"name": "receiver", "type": "address"}, {"name": "amount", "type": "uint256"}], "name": "unwrap", "outputs": [], "stateMutability": "nonpayable", "type": "function"},
541
+ ]
542
+ offramp_contract = w3.eth.contract(address=offramp_addr, abi=unwrap_abi)
543
+ unwrap_data = offramp_contract.encode_abi("unwrap", args=[
544
+ Web3.to_checksum_address(USDC), funder_addr, int(amount)
545
+ ])
546
+
547
+ # Safe / Proxy wallets → route via relayer (gasless)
548
+ if creds.signature_type in (SignatureType.POLY_GNOSIS_SAFE, SignatureType.POLY_PROXY):
549
+ from .relayer import BuilderCreds, RelayClient
550
+ bc_raw = self._builder_credentials
551
+ if not bc_raw:
552
+ raise RuntimeError(
553
+ "unwrap_from_polyusd on a Safe/Proxy wallet requires builder_credentials "
554
+ "in TraderConfig. Pass your Polymarket builder key/secret/passphrase."
555
+ )
556
+ bc = BuilderCreds(key=bc_raw.key, secret=bc_raw.secret, passphrase=bc_raw.passphrase)
557
+ rc = RelayClient(bc)
558
+ txns = []
559
+ if allowance < amount:
560
+ txns.append({"to": POLY_USD, "data": approve_data, "value": 0})
561
+ txns.append({"to": COLLATERAL_OFFRAMP, "data": unwrap_data, "value": 0})
562
+ return await rc.execute_safe(signer._private_key or "", signer.address, txns)
563
+
564
+ # EOA path
565
+ if not signer._private_key:
566
+ raise RuntimeError("unwrap_from_polyusd requires a private key signer for EOA wallets")
523
567
 
524
- allowance = polyusd_contract.functions.allowance(signer_addr, offramp_addr).call()
568
+ nonce = w3.eth.get_transaction_count(funder_addr)
525
569
  if allowance < amount:
526
- max_uint = 2**256 - 1
527
- tx = polyusd_contract.functions.approve(offramp_addr, max_uint).build_transaction({
528
- "from": signer_addr,
529
- "nonce": nonce,
530
- "chainId": 137,
570
+ tx = polyusd_contract.functions.approve(offramp_addr, 2**256 - 1).build_transaction({
571
+ "from": funder_addr, "nonce": nonce, "chainId": 137,
531
572
  })
532
573
  signed = w3.eth.account.sign_transaction(tx, signer._private_key)
533
574
  w3.eth.send_raw_transaction(signed.raw_transaction)
534
575
  nonce += 1
535
576
 
536
- # Call unwrap(address token, address receiver, uint256 amount) on Offramp
537
- unwrap_abi = [
538
- {"inputs": [{"name": "token", "type": "address"}, {"name": "receiver", "type": "address"}, {"name": "amount", "type": "uint256"}], "name": "unwrap", "outputs": [], "stateMutability": "nonpayable", "type": "function"},
539
- ]
540
- offramp_contract = w3.eth.contract(address=offramp_addr, abi=unwrap_abi)
541
577
  tx = offramp_contract.functions.unwrap(
542
- Web3.to_checksum_address(USDC),
543
- Web3.to_checksum_address(funder),
544
- amount,
578
+ Web3.to_checksum_address(USDC), funder_addr, int(amount),
545
579
  ).build_transaction({
546
- "from": signer_addr,
547
- "nonce": nonce,
548
- "chainId": 137,
580
+ "from": funder_addr, "nonce": nonce, "chainId": 137,
549
581
  })
550
582
  signed = w3.eth.account.sign_transaction(tx, signer._private_key)
551
583
  tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction)
@@ -667,7 +699,8 @@ class PolyNodeTrader:
667
699
  fee_amount_raw = calculate_fee(params.price, params.size, fee_config.fee_bps)
668
700
  if fee_amount_raw > 0:
669
701
  funder = creds.funder_address or creds.wallet_address
670
- nonce = await fetch_escrow_nonce(self._rpc_url, signer.address)
702
+ escrow_address = fee_escrow_address_for(self._exchange_version)
703
+ nonce = await fetch_escrow_nonce(self._rpc_url, signer.address, escrow_address)
671
704
  deadline = int(time.time()) + 300
672
705
  escrow_oid = generate_escrow_order_id()
673
706
  fee_auth = await sign_fee_auth(
@@ -680,6 +713,7 @@ class PolyNodeTrader:
680
713
  nonce=nonce,
681
714
  affiliate=fee_config.affiliate,
682
715
  affiliate_share_bps=fee_config.affiliate_share_bps,
716
+ exchange_version=self._exchange_version,
683
717
  )
684
718
  fee_auth_dict = {
685
719
  "escrow_order_id": fee_auth.escrow_order_id,
@@ -692,6 +726,8 @@ class PolyNodeTrader:
692
726
  "affiliate": fee_auth.affiliate,
693
727
  "affiliate_share_bps": fee_auth.affiliate_share_bps,
694
728
  }
729
+ if fee_auth.escrow_contract:
730
+ fee_auth_dict["escrow_contract"] = fee_auth.escrow_contract
695
731
 
696
732
  request_data: dict[str, Any] = {"method": "POST", "path": "/order", "body": body_str, "headers": headers}
697
733
  if fee_auth_dict:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: polynode
3
- Version: 0.9.0
3
+ Version: 0.9.2
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
@@ -1,5 +1,5 @@
1
1
  polynode/__init__.py,sha256=ucqXzFPMdEcfGEAdbgOeJczREEiSCNBX1G3Il7y8-UI,1127
2
- polynode/_version.py,sha256=H9NWRZb7NbeRRPLP_V1fARmLNXranorVM-OOY-8_2ug,22
2
+ polynode/_version.py,sha256=UwJXM8JY2T3tE2id0K2k_lEaVThbRTrGO1mNibyzIz8,22
3
3
  polynode/client.py,sha256=eSCKpuZFg2ssQC5tmsfRYF3cTqCk1Gdw4KPjbd5MFHQ,21831
4
4
  polynode/engine.py,sha256=DSXBmx3DXPh-PN0ihkXn7LSiyxeOoo0iY3z4YinQ3XI,6488
5
5
  polynode/errors.py,sha256=5qUCI7K76VKbF5R5G5UYhPzcnOPlGLCA8-sJnKbrK-4,925
@@ -14,16 +14,17 @@ polynode/cache/__init__.py,sha256=A9lXcIE3ISKKo1r0o3o-CkmQpBhLdKwdQfCR0eW5i9o,51
14
14
  polynode/trading/V2_ORDER_FLOW.md,sha256=Ppd7iwZjtmVwok-fP4Ofo4UBQ9pIAg0t57RJ-OzqRHs,10926
15
15
  polynode/trading/__init__.py,sha256=sPv30j9eyvqjnUBxGAYaKpAPQZ2jTw3UwA4DZXYZ21s,923
16
16
  polynode/trading/clob_api.py,sha256=Sfb8XLN9MMzVE6jARNqEQxiD608OC_6Xjc7uiJbDQEc,7274
17
- polynode/trading/constants.py,sha256=aW8F9Q2W0TRNVX0JNh39k0qxbb03ijUAGbWjrJFjf_o,2513
17
+ polynode/trading/constants.py,sha256=IEdrwC4bPjo4MoVlDKYqXphlmIKsoxO4f_s_pyZ3x08,2702
18
18
  polynode/trading/cosigner.py,sha256=Fww9OgaKdHQAHd8Kom1AJBtmMGC8FbNZxdFaFALNBY8,3058
19
19
  polynode/trading/eip712.py,sha256=PKLfMt58Diy419jiQXysnX1kSlU_QcWDRofGEb3u_JI,9744
20
- polynode/trading/escrow.py,sha256=bU_ohyOlx2r6KjBYxUNdjROvrmunWV1Ey99AuSOt9Z8,4590
21
- polynode/trading/onboarding.py,sha256=oXtFz1HF1vGvMnb5o44SwuADd2mdU5ws1v7n30rk6fg,15066
20
+ polynode/trading/escrow.py,sha256=T2v5bv3wIyQTcFoxcHXlDBlzyeWDYbxRIqQamqlhaog,6513
21
+ polynode/trading/onboarding.py,sha256=GdQh1aM9F0LuCL3uqNTguXX5Sxe1_0hSVdHR7kksKZ0,15777
22
22
  polynode/trading/position_management.py,sha256=z2pYls4ErhyRE6thQdOs45BF4llKvKNNruQgcMEFT_U,3827
23
23
  polynode/trading/privy.py,sha256=iiUQZsBqjj29C7oUiFFktoT2muZY06bcMzABKstFKqE,4523
24
+ polynode/trading/relayer.py,sha256=adTo-aclWnaqrFtfWEOQ2Ye859CZMc9q_fsd3DePDGA,10542
24
25
  polynode/trading/signer.py,sha256=piifRquwnwFwrKLL24jsRQYMh_cdkz8wNz99Vx8_KTY,3413
25
26
  polynode/trading/sqlite_backend.py,sha256=zfo4WevuunWV-7i6Z3gQi_e7Tonw-7m1TsEd88O5I4w,9486
26
- polynode/trading/trader.py,sha256=VpOwZ0TEwkOSRSLSAZ1LxjEJfxPkH3RO-XFrH6RG5Ys,34845
27
+ polynode/trading/trader.py,sha256=EEwF_U2DXVPRnENY5azk8FgzSx_zXns27apQQj9isus,37188
27
28
  polynode/trading/types.py,sha256=mo5zJRlfQLWuXwrkS9AaGqKWrt7FMf6bPGRE-0T62J0,5698
28
29
  polynode/types/__init__.py,sha256=0v_CByqG-zT1xucbT2So_77YDmsPj9u956at94nyrLM,280
29
30
  polynode/types/enums.py,sha256=2cx9l10A-LUeDNBJZn7l0XBTka2YfxRQcXW0s7s3_4k,1082
@@ -32,6 +33,6 @@ polynode/types/orderbook.py,sha256=MwD6P3poo3F70bONUZ5i3ajk1sgw39hHTOw6D0fP5_8,1
32
33
  polynode/types/rest.py,sha256=ftJKQZH0wBrR_l199rDMqnVS8cyt8YnCvo9cKIsTcCw,7012
33
34
  polynode/types/short_form.py,sha256=xfGklDi587u6K2aXaRWiwbxqkipW8BiEPeNIDT45v2Y,799
34
35
  polynode/types/ws.py,sha256=EzE9Vzrb6cILemfssmnqM8fbHt4USn3latpUHKatwk4,994
35
- polynode-0.9.0.dist-info/METADATA,sha256=mzGKeGUGX-EvRNytrdz2FS7uq-N4-m944fpaW2GujoU,3777
36
- polynode-0.9.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
37
- polynode-0.9.0.dist-info/RECORD,,
36
+ polynode-0.9.2.dist-info/METADATA,sha256=-FqTxtI_g04JQ7AxM9kzp9RxfnF6dGNADA3CmfXORK8,3777
37
+ polynode-0.9.2.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
38
+ polynode-0.9.2.dist-info/RECORD,,