polynode 0.5.5__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.
@@ -0,0 +1,137 @@
1
+ """Subscription builder and async iterator for PolyNode WebSocket events."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from typing import TYPE_CHECKING, Any, Callable
7
+
8
+ from .types.enums import EventType, SubscriptionType
9
+ from .types.events import PolyNodeEvent
10
+ from .types.ws import SubscriptionFilters
11
+
12
+ if TYPE_CHECKING:
13
+ from .ws import PolyNodeWS
14
+
15
+
16
+ class SubscriptionBuilder:
17
+ """Fluent builder for WebSocket subscriptions."""
18
+
19
+ def __init__(self, ws: PolyNodeWS, type: SubscriptionType) -> None:
20
+ self._ws = ws
21
+ self._type = type
22
+ self._filters = SubscriptionFilters()
23
+
24
+ def wallets(self, addresses: list[str]) -> SubscriptionBuilder:
25
+ self._filters.wallets = addresses
26
+ return self
27
+
28
+ def tokens(self, token_ids: list[str]) -> SubscriptionBuilder:
29
+ self._filters.tokens = token_ids
30
+ return self
31
+
32
+ def slugs(self, slugs: list[str]) -> SubscriptionBuilder:
33
+ self._filters.slugs = slugs
34
+ return self
35
+
36
+ def condition_ids(self, ids: list[str]) -> SubscriptionBuilder:
37
+ self._filters.condition_ids = ids
38
+ return self
39
+
40
+ def side(self, side: str) -> SubscriptionBuilder:
41
+ self._filters.side = side
42
+ return self
43
+
44
+ def status(self, status: str) -> SubscriptionBuilder:
45
+ self._filters.status = status
46
+ return self
47
+
48
+ def min_size(self, usd: float) -> SubscriptionBuilder:
49
+ self._filters.min_size = usd
50
+ return self
51
+
52
+ def max_size(self, usd: float) -> SubscriptionBuilder:
53
+ self._filters.max_size = usd
54
+ return self
55
+
56
+ def event_types(self, types: list[EventType]) -> SubscriptionBuilder:
57
+ self._filters.event_types = types
58
+ return self
59
+
60
+ def snapshot_count(self, count: int) -> SubscriptionBuilder:
61
+ self._filters.snapshot_count = count
62
+ return self
63
+
64
+ def feeds(self, feed_names: list[str]) -> SubscriptionBuilder:
65
+ self._filters.feeds = feed_names
66
+ return self
67
+
68
+ async def send(self) -> Subscription:
69
+ return await self._ws._subscribe(self._type, self._filters)
70
+
71
+
72
+ class Subscription:
73
+ """An active WebSocket subscription with event handling and async iteration."""
74
+
75
+ def __init__(self, ws: PolyNodeWS, type: SubscriptionType, filters: SubscriptionFilters) -> None:
76
+ self._ws = ws
77
+ self.type = type
78
+ self.filters = filters
79
+ self._id: str | None = None
80
+ self._handlers: dict[str, set[Callable]] = {}
81
+ self._queue: asyncio.Queue[PolyNodeEvent | None] = asyncio.Queue()
82
+ self._closed = False
83
+
84
+ @property
85
+ def id(self) -> str | None:
86
+ return self._id
87
+
88
+ def _set_id(self, id: str) -> None:
89
+ self._id = id
90
+
91
+ def on(self, type: str, handler: Callable) -> Subscription:
92
+ if type not in self._handlers:
93
+ self._handlers[type] = set()
94
+ self._handlers[type].add(handler)
95
+ return self
96
+
97
+ def off(self, type: str, handler: Callable) -> Subscription:
98
+ if type in self._handlers:
99
+ self._handlers[type].discard(handler)
100
+ return self
101
+
102
+ def _emit(self, event: PolyNodeEvent) -> None:
103
+ # Typed handlers
104
+ event_type = event.event_type # type: ignore[union-attr]
105
+ handlers = self._handlers.get(event_type)
106
+ if handlers:
107
+ for h in handlers:
108
+ h(event)
109
+ # Catch-all
110
+ star = self._handlers.get("*")
111
+ if star:
112
+ for h in star:
113
+ h(event)
114
+ # Async iterator queue
115
+ self._queue.put_nowait(event)
116
+
117
+ def unsubscribe(self) -> None:
118
+ self._closed = True
119
+ self._queue.put_nowait(None)
120
+ self._ws._unsubscribe(self._id)
121
+
122
+ def __aiter__(self):
123
+ return self
124
+
125
+ async def __anext__(self) -> PolyNodeEvent:
126
+ if self._closed:
127
+ raise StopAsyncIteration
128
+ event = await self._queue.get()
129
+ if event is None:
130
+ raise StopAsyncIteration
131
+ return event
132
+
133
+ async def __aenter__(self) -> Subscription:
134
+ return self
135
+
136
+ async def __aexit__(self, *args: Any) -> None:
137
+ self.unsubscribe()
polynode/testing.py ADDED
@@ -0,0 +1,83 @@
1
+ """Testing utilities for SDK examples and tests."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+
7
+ import httpx
8
+
9
+ FALLBACK_WALLETS = [
10
+ "0xc2e7800b5af46e6093872b177b7a5e7f0563be51",
11
+ "0x02227b8f5a9636e895607edd3185ed6ee5598ff7",
12
+ "0xefbc5fec8d7b0acdc8911bdd9a98d6964308f9a2",
13
+ "0x2a2c53bd278c04da9962fcf96490e17f3dfb9bc1",
14
+ "0x37c1874a60d348903594a96703e0507c518fc53a",
15
+ ]
16
+
17
+
18
+ async def get_active_test_wallet(*, fresh: bool = False) -> str:
19
+ if not fresh:
20
+ return FALLBACK_WALLETS[0]
21
+ try:
22
+ traders = await _fetch_top_traders(1)
23
+ return traders[0]["wallet"] if traders else FALLBACK_WALLETS[0]
24
+ except Exception:
25
+ return FALLBACK_WALLETS[0]
26
+
27
+
28
+ async def get_active_test_wallets(count: int = 5, *, fresh: bool = False) -> list[str]:
29
+ if not fresh:
30
+ return FALLBACK_WALLETS[:count]
31
+ try:
32
+ traders = await _fetch_top_traders(count)
33
+ if traders:
34
+ return [t["wallet"] for t in traders]
35
+ return FALLBACK_WALLETS[:count]
36
+ except Exception:
37
+ return FALLBACK_WALLETS[:count]
38
+
39
+
40
+ async def _fetch_top_traders(count: int) -> list[dict]:
41
+ build_id = await _discover_build_id()
42
+ if not build_id:
43
+ return []
44
+
45
+ url = f"https://polymarket.com/_next/data/{build_id}/en/leaderboard/overall/monthly/profit.json?segment=overall&period=monthly&sort=profit"
46
+
47
+ async with httpx.AsyncClient(timeout=10.0) as http:
48
+ resp = await http.get(url, headers={"User-Agent": "polynode-sdk/testing"})
49
+ if not resp.is_success:
50
+ return []
51
+ data = resp.json()
52
+
53
+ queries = data.get("pageProps", {}).get("dehydratedState", {}).get("queries", [])
54
+ for q in queries:
55
+ key = q.get("queryKey", [])
56
+ if isinstance(key, list) and "profit" in key and "/leaderboard" in str(key):
57
+ records = q.get("state", {}).get("data", [])
58
+ if isinstance(records, list):
59
+ return [
60
+ {
61
+ "wallet": r.get("proxyWallet", ""),
62
+ "name": r.get("pseudonym") or r.get("name", ""),
63
+ "pnl": r.get("pnl") or r.get("amount", 0),
64
+ "volume": r.get("volume", 0),
65
+ }
66
+ for r in records[:count]
67
+ ]
68
+ return []
69
+
70
+
71
+ async def _discover_build_id() -> str | None:
72
+ try:
73
+ async with httpx.AsyncClient(timeout=10.0) as http:
74
+ resp = await http.get(
75
+ "https://polymarket.com/",
76
+ headers={"User-Agent": "polynode-sdk/testing"},
77
+ )
78
+ if not resp.is_success:
79
+ return None
80
+ match = re.search(r"/_next/data/([^/]+)/", resp.text)
81
+ return match.group(1) if match else None
82
+ except Exception:
83
+ return None
@@ -0,0 +1,19 @@
1
+ """Trading module — place orders on Polymarket with local credential custody."""
2
+
3
+ from .types import * # noqa: F401, F403
4
+ from .constants import * # noqa: F401, F403
5
+ from .signer import NormalizedSigner, normalize_signer
6
+ from .cosigner import build_l2_headers, send_via_cosigner
7
+ from .onboarding import derive_safe_address, derive_proxy_address, detect_wallet_type
8
+ from .trader import PolyNodeTrader
9
+
10
+ __all__ = [
11
+ "PolyNodeTrader",
12
+ "NormalizedSigner",
13
+ "normalize_signer",
14
+ "build_l2_headers",
15
+ "send_via_cosigner",
16
+ "derive_safe_address",
17
+ "derive_proxy_address",
18
+ "detect_wallet_type",
19
+ ]
@@ -0,0 +1,158 @@
1
+ """Direct CLOB HTTP calls for order management."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ import httpx
8
+
9
+ from .constants import CLOB_HOST
10
+ from .cosigner import build_l2_headers
11
+
12
+
13
+ async def post_order(
14
+ cosigner_url: str,
15
+ polynode_key: str,
16
+ fallback_direct: bool,
17
+ credentials: dict[str, str],
18
+ wallet_address: str,
19
+ order_body: str,
20
+ ) -> dict[str, Any]:
21
+ """Submit a signed order to the CLOB."""
22
+ from .cosigner import send_via_cosigner
23
+
24
+ headers = build_l2_headers(
25
+ credentials["apiKey"],
26
+ credentials["apiSecret"],
27
+ credentials["apiPassphrase"],
28
+ wallet_address,
29
+ "POST",
30
+ "/order",
31
+ order_body,
32
+ )
33
+
34
+ return await send_via_cosigner(
35
+ cosigner_url,
36
+ polynode_key,
37
+ fallback_direct,
38
+ {"method": "POST", "path": "/order", "body": order_body, "headers": headers},
39
+ )
40
+
41
+
42
+ async def cancel_order(
43
+ cosigner_url: str,
44
+ polynode_key: str,
45
+ fallback_direct: bool,
46
+ credentials: dict[str, str],
47
+ wallet_address: str,
48
+ order_id: str,
49
+ ) -> dict[str, Any]:
50
+ """Cancel a specific order."""
51
+ from .cosigner import send_via_cosigner
52
+ import json
53
+
54
+ body = json.dumps({"orderID": order_id})
55
+ headers = build_l2_headers(
56
+ credentials["apiKey"],
57
+ credentials["apiSecret"],
58
+ credentials["apiPassphrase"],
59
+ wallet_address,
60
+ "DELETE",
61
+ "/order",
62
+ body,
63
+ )
64
+
65
+ return await send_via_cosigner(
66
+ cosigner_url,
67
+ polynode_key,
68
+ fallback_direct,
69
+ {"method": "DELETE", "path": "/order", "body": body, "headers": headers},
70
+ )
71
+
72
+
73
+ async def cancel_all_orders(
74
+ cosigner_url: str,
75
+ polynode_key: str,
76
+ fallback_direct: bool,
77
+ credentials: dict[str, str],
78
+ wallet_address: str,
79
+ market: str | None = None,
80
+ ) -> dict[str, Any]:
81
+ """Cancel all orders, optionally for a specific market."""
82
+ from .cosigner import send_via_cosigner
83
+ import json
84
+
85
+ path = "/cancel-market-orders" if market else "/cancel-all"
86
+ body = json.dumps({"market": market}) if market else None
87
+ headers = build_l2_headers(
88
+ credentials["apiKey"],
89
+ credentials["apiSecret"],
90
+ credentials["apiPassphrase"],
91
+ wallet_address,
92
+ "DELETE",
93
+ path,
94
+ body,
95
+ )
96
+
97
+ return await send_via_cosigner(
98
+ cosigner_url,
99
+ polynode_key,
100
+ fallback_direct,
101
+ {"method": "DELETE", "path": path, "body": body, "headers": headers},
102
+ )
103
+
104
+
105
+ async def get_open_orders(
106
+ cosigner_url: str,
107
+ polynode_key: str,
108
+ fallback_direct: bool,
109
+ credentials: dict[str, str],
110
+ wallet_address: str,
111
+ market: str | None = None,
112
+ asset_id: str | None = None,
113
+ ) -> list[dict[str, Any]]:
114
+ """Get open orders from the CLOB."""
115
+ from .cosigner import send_via_cosigner
116
+
117
+ path = "/data/orders"
118
+ params = []
119
+ if market:
120
+ params.append(f"market={market}")
121
+ if asset_id:
122
+ params.append(f"asset_id={asset_id}")
123
+ if params:
124
+ path += "?" + "&".join(params)
125
+
126
+ headers = build_l2_headers(
127
+ credentials["apiKey"],
128
+ credentials["apiSecret"],
129
+ credentials["apiPassphrase"],
130
+ wallet_address,
131
+ "GET",
132
+ path,
133
+ )
134
+
135
+ result = await send_via_cosigner(
136
+ cosigner_url,
137
+ polynode_key,
138
+ fallback_direct,
139
+ {"method": "GET", "path": path, "headers": headers},
140
+ )
141
+
142
+ if isinstance(result, list):
143
+ return result
144
+ return result.get("data") or result.get("orders") or []
145
+
146
+
147
+ async def fetch_tick_size(token_id: str) -> str:
148
+ """Fetch tick size for a token from the CLOB."""
149
+ async with httpx.AsyncClient(timeout=5.0) as http:
150
+ resp = await http.get(f"{CLOB_HOST}/tick-size?token_id={token_id}")
151
+ return str(resp.json())
152
+
153
+
154
+ async def fetch_neg_risk(token_id: str) -> bool:
155
+ """Fetch neg-risk flag for a token from the CLOB."""
156
+ async with httpx.AsyncClient(timeout=5.0) as http:
157
+ resp = await http.get(f"{CLOB_HOST}/neg-risk?token_id={token_id}")
158
+ return bool(resp.json())
@@ -0,0 +1,31 @@
1
+ """Contract addresses and constants for Polymarket on Polygon (chain 137)."""
2
+
3
+ CHAIN_ID = 137
4
+ CLOB_HOST = "https://clob.polymarket.com"
5
+ RELAYER_HOST = "https://relayer-v2.polymarket.com"
6
+ DEFAULT_RPC = "https://polygon-bor-rpc.publicnode.com"
7
+ DEFAULT_COSIGNER = "https://trade.polynode.dev"
8
+
9
+ # Exchange contracts
10
+ CTF_EXCHANGE = "0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E"
11
+ NEG_RISK_CTF_EXCHANGE = "0xC5d563A36AE78145C45a50134d48A1215220f80a"
12
+ NEG_RISK_ADAPTER = "0xd91E80cF2E7be2e162c6513ceD06f1dD0dA35296"
13
+
14
+ # Token contracts
15
+ USDC = "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174"
16
+ CTF = "0x4D97DCd97eC945f40cF65F87097ACe5EA0476045"
17
+
18
+ # Safe derivation
19
+ SAFE_FACTORY = "0xaacFeEa03eb1561C4e67d661e40682Bd20E3541b"
20
+ SAFE_MULTISEND = "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761"
21
+ SAFE_INIT_CODE_HASH = "0x2bce2127ff07fb632d16c8347c4ebf501f4841168bed00d9e6ef715ddb6fcecf"
22
+
23
+ # Proxy derivation
24
+ PROXY_FACTORY = "0xaB45c5A4B0c941a2F231C04C3f49182e1A254052"
25
+ PROXY_INIT_CODE_HASH = "0xd21df8dc65880a8606f09fe0ce3df9b8869287ab0b058be05aa9e8af6330a00b"
26
+
27
+ # All spender contracts that need approval
28
+ SPENDERS = [CTF_EXCHANGE, NEG_RISK_CTF_EXCHANGE, NEG_RISK_ADAPTER]
29
+
30
+ # Metadata cache TTL (5 minutes)
31
+ META_TTL_SECONDS = 300
@@ -0,0 +1,86 @@
1
+ """Co-signer client — routes orders through the PolyNode builder attribution proxy."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import base64
6
+ import hashlib
7
+ import hmac
8
+ import time
9
+ from typing import Any
10
+
11
+ import httpx
12
+
13
+ from .constants import CLOB_HOST
14
+
15
+
16
+ def build_l2_headers(
17
+ api_key: str,
18
+ api_secret: str,
19
+ api_passphrase: str,
20
+ wallet_address: str,
21
+ method: str,
22
+ path: str,
23
+ body: str | None = None,
24
+ ) -> dict[str, str]:
25
+ """Build L2 HMAC headers for Polymarket CLOB authentication."""
26
+ timestamp = str(int(time.time()))
27
+ message = f"{timestamp}{method}{path}"
28
+ if body:
29
+ message += body
30
+
31
+ secret_bytes = base64.b64decode(api_secret)
32
+ sig = base64.b64encode(
33
+ hmac.new(secret_bytes, message.encode(), hashlib.sha256).digest()
34
+ ).decode()
35
+ sig = sig.replace("+", "-").replace("/", "_")
36
+
37
+ return {
38
+ "POLY_ADDRESS": wallet_address,
39
+ "POLY_SIGNATURE": sig,
40
+ "POLY_TIMESTAMP": timestamp,
41
+ "POLY_API_KEY": api_key,
42
+ "POLY_PASSPHRASE": api_passphrase,
43
+ }
44
+
45
+
46
+ async def send_via_cosigner(
47
+ cosigner_url: str,
48
+ polynode_key: str,
49
+ fallback_direct: bool,
50
+ request: dict[str, Any],
51
+ ) -> Any:
52
+ """Send a request through the co-signer with optional fallback to direct CLOB."""
53
+ if cosigner_url:
54
+ try:
55
+ async with httpx.AsyncClient(timeout=10.0) as http:
56
+ resp = await http.post(
57
+ f"{cosigner_url}/submit",
58
+ headers={
59
+ "Content-Type": "application/json",
60
+ "X-PolyNode-Key": polynode_key,
61
+ },
62
+ json=request,
63
+ )
64
+ data = resp.json()
65
+ if resp.status_code >= 500 and fallback_direct:
66
+ return await _send_direct(request)
67
+ return data
68
+ except Exception as e:
69
+ if fallback_direct:
70
+ return await _send_direct(request)
71
+ raise RuntimeError(f"Co-signer unreachable: {e}")
72
+
73
+ return await _send_direct(request)
74
+
75
+
76
+ async def _send_direct(request: dict[str, Any]) -> Any:
77
+ """Send directly to Polymarket CLOB (no builder attribution)."""
78
+ url = f"{CLOB_HOST}{request['path']}"
79
+ async with httpx.AsyncClient(timeout=10.0) as http:
80
+ resp = await http.request(
81
+ request["method"],
82
+ url,
83
+ headers={**request.get("headers", {}), "Content-Type": "application/json"},
84
+ content=request.get("body"),
85
+ )
86
+ return resp.json()
@@ -0,0 +1,163 @@
1
+ """Pure Python EIP-712 order signing for Polymarket exchange contracts."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ from typing import Any
7
+
8
+ from .constants import CTF_EXCHANGE, NEG_RISK_CTF_EXCHANGE
9
+ from .types import Eip712Payload, SignatureType
10
+
11
+
12
+ # EIP-712 domain for Polymarket Exchange
13
+ EXCHANGE_DOMAIN = {
14
+ "name": "PolymarketExchange",
15
+ "version": "1",
16
+ "chainId": 137,
17
+ "verifyingContract": CTF_EXCHANGE,
18
+ }
19
+
20
+ NEG_RISK_DOMAIN = {
21
+ "name": "PolymarketExchange",
22
+ "version": "1",
23
+ "chainId": 137,
24
+ "verifyingContract": NEG_RISK_CTF_EXCHANGE,
25
+ }
26
+
27
+ ORDER_TYPES = {
28
+ "Order": [
29
+ {"name": "salt", "type": "uint256"},
30
+ {"name": "maker", "type": "address"},
31
+ {"name": "signer", "type": "address"},
32
+ {"name": "taker", "type": "address"},
33
+ {"name": "tokenId", "type": "uint256"},
34
+ {"name": "makerAmount", "type": "uint256"},
35
+ {"name": "takerAmount", "type": "uint256"},
36
+ {"name": "expiration", "type": "uint256"},
37
+ {"name": "nonce", "type": "uint256"},
38
+ {"name": "feeRateBps", "type": "uint256"},
39
+ {"name": "side", "type": "uint8"},
40
+ {"name": "signatureType", "type": "uint8"},
41
+ ],
42
+ }
43
+
44
+
45
+ def compute_amounts(
46
+ price: float,
47
+ size: float,
48
+ side: str,
49
+ tick_size: str,
50
+ ) -> tuple[int, int]:
51
+ """Compute maker_amount and taker_amount from price/size/tick_size."""
52
+ decimals = len(tick_size.rstrip("0").split(".")[-1]) if "." in tick_size else 0
53
+ raw_price = round(price * (10 ** decimals))
54
+ raw_size = round(size * 1_000_000) # USDC has 6 decimals
55
+ price_denom = 10 ** decimals
56
+
57
+ if side == "BUY":
58
+ maker_amount = raw_size * raw_price // price_denom
59
+ taker_amount = raw_size
60
+ else:
61
+ maker_amount = raw_size
62
+ taker_amount = raw_size * raw_price // price_denom
63
+
64
+ return maker_amount, taker_amount
65
+
66
+
67
+ def build_order_payload(
68
+ *,
69
+ maker: str,
70
+ signer: str,
71
+ token_id: str,
72
+ maker_amount: int,
73
+ taker_amount: int,
74
+ side: str,
75
+ signature_type: SignatureType,
76
+ fee_rate_bps: int = 0,
77
+ nonce: int = 0,
78
+ expiration: int = 0,
79
+ neg_risk: bool = False,
80
+ ) -> Eip712Payload:
81
+ """Build an EIP-712 typed data payload for a Polymarket order."""
82
+ import secrets
83
+
84
+ salt = int(secrets.token_hex(32), 16)
85
+ side_int = 0 if side == "BUY" else 1
86
+
87
+ domain = NEG_RISK_DOMAIN if neg_risk else EXCHANGE_DOMAIN
88
+
89
+ message = {
90
+ "salt": salt,
91
+ "maker": maker,
92
+ "signer": signer,
93
+ "taker": "0x0000000000000000000000000000000000000000",
94
+ "tokenId": int(token_id),
95
+ "makerAmount": maker_amount,
96
+ "takerAmount": taker_amount,
97
+ "expiration": expiration,
98
+ "nonce": nonce,
99
+ "feeRateBps": fee_rate_bps,
100
+ "side": side_int,
101
+ "signatureType": int(signature_type),
102
+ }
103
+
104
+ return Eip712Payload(
105
+ domain=domain,
106
+ types=ORDER_TYPES,
107
+ primary_type="Order",
108
+ message=message,
109
+ )
110
+
111
+
112
+ async def create_signed_order(
113
+ sign_typed_data: Any,
114
+ *,
115
+ signer_address: str,
116
+ funder_address: str,
117
+ token_id: str,
118
+ price: float,
119
+ size: float,
120
+ side: str,
121
+ signature_type: SignatureType,
122
+ tick_size: str = "0.01",
123
+ neg_risk: bool = False,
124
+ fee_rate_bps: int = 0,
125
+ expiration: int = 0,
126
+ nonce: int = 0,
127
+ ) -> dict[str, Any]:
128
+ """Create and sign a Polymarket order. Returns the order dict ready for CLOB submission."""
129
+ maker_amount, taker_amount = compute_amounts(price, size, side, tick_size)
130
+
131
+ payload = build_order_payload(
132
+ maker=funder_address,
133
+ signer=signer_address,
134
+ token_id=token_id,
135
+ maker_amount=maker_amount,
136
+ taker_amount=taker_amount,
137
+ side=side,
138
+ signature_type=signature_type,
139
+ fee_rate_bps=fee_rate_bps,
140
+ nonce=nonce,
141
+ expiration=expiration,
142
+ neg_risk=neg_risk,
143
+ )
144
+
145
+ signature = await sign_typed_data(payload)
146
+ if not signature.startswith("0x"):
147
+ signature = f"0x{signature}"
148
+
149
+ return {
150
+ "salt": str(payload.message["salt"]),
151
+ "maker": funder_address,
152
+ "signer": signer_address,
153
+ "taker": "0x0000000000000000000000000000000000000000",
154
+ "tokenId": token_id,
155
+ "makerAmount": str(maker_amount),
156
+ "takerAmount": str(taker_amount),
157
+ "expiration": str(expiration),
158
+ "nonce": str(nonce),
159
+ "feeRateBps": str(fee_rate_bps),
160
+ "side": side,
161
+ "signatureType": int(signature_type),
162
+ "signature": signature,
163
+ }