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.
- polynode/__init__.py +41 -0
- polynode/_version.py +1 -0
- polynode/cache/__init__.py +11 -0
- polynode/client.py +635 -0
- polynode/engine.py +201 -0
- polynode/errors.py +35 -0
- polynode/orderbook.py +243 -0
- polynode/orderbook_state.py +77 -0
- polynode/redemption_watcher.py +339 -0
- polynode/short_form.py +321 -0
- polynode/subscription.py +137 -0
- polynode/testing.py +83 -0
- polynode/trading/__init__.py +19 -0
- polynode/trading/clob_api.py +158 -0
- polynode/trading/constants.py +31 -0
- polynode/trading/cosigner.py +86 -0
- polynode/trading/eip712.py +163 -0
- polynode/trading/onboarding.py +242 -0
- polynode/trading/signer.py +91 -0
- polynode/trading/sqlite_backend.py +208 -0
- polynode/trading/trader.py +506 -0
- polynode/trading/types.py +191 -0
- polynode/types/__init__.py +8 -0
- polynode/types/enums.py +51 -0
- polynode/types/events.py +270 -0
- polynode/types/orderbook.py +66 -0
- polynode/types/rest.py +376 -0
- polynode/types/short_form.py +35 -0
- polynode/types/ws.py +38 -0
- polynode/ws.py +278 -0
- polynode-0.5.5.dist-info/METADATA +133 -0
- polynode-0.5.5.dist-info/RECORD +33 -0
- polynode-0.5.5.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
"""PolyNodeTrader — place orders on Polymarket with local credential custody."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import time
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from .clob_api import (
|
|
10
|
+
cancel_all_orders,
|
|
11
|
+
cancel_order as clob_cancel_order,
|
|
12
|
+
fetch_neg_risk,
|
|
13
|
+
fetch_tick_size,
|
|
14
|
+
get_open_orders as clob_get_open_orders,
|
|
15
|
+
post_order,
|
|
16
|
+
)
|
|
17
|
+
from .constants import DEFAULT_COSIGNER
|
|
18
|
+
from .cosigner import build_l2_headers, send_via_cosigner
|
|
19
|
+
from .eip712 import create_signed_order
|
|
20
|
+
from .onboarding import (
|
|
21
|
+
check_approvals as check_approvals_onchain,
|
|
22
|
+
check_balance as check_balance_onchain,
|
|
23
|
+
create_clob_credentials,
|
|
24
|
+
derive_funder_address,
|
|
25
|
+
detect_wallet_type,
|
|
26
|
+
is_safe_deployed,
|
|
27
|
+
)
|
|
28
|
+
from .signer import NormalizedSigner, normalize_signer
|
|
29
|
+
from .sqlite_backend import TradingSqliteBackend
|
|
30
|
+
from .types import (
|
|
31
|
+
ApprovalStatus,
|
|
32
|
+
BalanceInfo,
|
|
33
|
+
CancelResult,
|
|
34
|
+
GeneratedWallet,
|
|
35
|
+
HistoryParams,
|
|
36
|
+
LinkResult,
|
|
37
|
+
MarketMeta,
|
|
38
|
+
OpenOrder,
|
|
39
|
+
OrderHistoryRow,
|
|
40
|
+
OrderParams,
|
|
41
|
+
OrderResult,
|
|
42
|
+
ReadyStatus,
|
|
43
|
+
RouterSigner,
|
|
44
|
+
SignatureType,
|
|
45
|
+
StoredCredentials,
|
|
46
|
+
TraderConfig,
|
|
47
|
+
WalletExport,
|
|
48
|
+
WalletInfo,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class PolyNodeTrader:
|
|
53
|
+
"""Place orders on Polymarket with local credential custody and builder attribution."""
|
|
54
|
+
|
|
55
|
+
def __init__(self, config: TraderConfig | None = None) -> None:
|
|
56
|
+
c = config or TraderConfig()
|
|
57
|
+
self._polynode_key = c.polynode_key
|
|
58
|
+
self._db_path = c.db_path
|
|
59
|
+
self._cosigner_url = c.cosigner_url
|
|
60
|
+
self._fallback_direct = c.fallback_direct
|
|
61
|
+
self._default_sig_type = c.default_signature_type
|
|
62
|
+
self._rpc_url = c.rpc_url
|
|
63
|
+
|
|
64
|
+
self._db: TradingSqliteBackend | None = None
|
|
65
|
+
self._active_signer: NormalizedSigner | None = None
|
|
66
|
+
self._active_wallet: str | None = None
|
|
67
|
+
|
|
68
|
+
@staticmethod
|
|
69
|
+
async def generate_wallet() -> GeneratedWallet:
|
|
70
|
+
"""Generate a fresh EOA wallet. User MUST back up the private key."""
|
|
71
|
+
try:
|
|
72
|
+
from eth_account import Account
|
|
73
|
+
except ImportError:
|
|
74
|
+
raise ImportError("eth-account required. Install with: pip install polynode[trading]")
|
|
75
|
+
account = Account.create()
|
|
76
|
+
return GeneratedWallet(private_key=account.key.hex(), address=account.address)
|
|
77
|
+
|
|
78
|
+
def _get_db(self) -> TradingSqliteBackend:
|
|
79
|
+
if not self._db:
|
|
80
|
+
self._db = TradingSqliteBackend(self._db_path)
|
|
81
|
+
return self._db
|
|
82
|
+
|
|
83
|
+
# ── Onboarding ──
|
|
84
|
+
|
|
85
|
+
async def ensure_ready(
|
|
86
|
+
self,
|
|
87
|
+
signer: str | RouterSigner,
|
|
88
|
+
*,
|
|
89
|
+
type: SignatureType | None = None,
|
|
90
|
+
) -> ReadyStatus:
|
|
91
|
+
"""One-call onboarding: detect wallet type, deploy Safe, set approvals, create CLOB creds."""
|
|
92
|
+
temp = await normalize_signer(signer, type or SignatureType.POLY_GNOSIS_SAFE)
|
|
93
|
+
actions: list[str] = []
|
|
94
|
+
|
|
95
|
+
if type is not None:
|
|
96
|
+
sig_type = type
|
|
97
|
+
funder = temp.funder_address or await derive_funder_address(temp.address, sig_type)
|
|
98
|
+
else:
|
|
99
|
+
detected = await detect_wallet_type(temp.address)
|
|
100
|
+
sig_type = detected["signature_type"]
|
|
101
|
+
funder = detected["funder_address"]
|
|
102
|
+
actions.append(f"auto_detected_type_{int(sig_type)}")
|
|
103
|
+
|
|
104
|
+
normalized = await normalize_signer(signer, sig_type)
|
|
105
|
+
normalized.funder_address = funder
|
|
106
|
+
normalized.signature_type = sig_type
|
|
107
|
+
|
|
108
|
+
db = self._get_db()
|
|
109
|
+
stored = db.get_credentials(normalized.address)
|
|
110
|
+
safe_deployed = stored.safe_deployed if stored else False
|
|
111
|
+
approvals_set = stored.approvals_set if stored else False
|
|
112
|
+
|
|
113
|
+
# Deploy Safe if needed
|
|
114
|
+
if sig_type == SignatureType.POLY_GNOSIS_SAFE and not safe_deployed:
|
|
115
|
+
deployed = await is_safe_deployed(funder)
|
|
116
|
+
if deployed:
|
|
117
|
+
safe_deployed = True
|
|
118
|
+
actions.append("safe_already_deployed")
|
|
119
|
+
else:
|
|
120
|
+
actions.append("safe_deploy_needed")
|
|
121
|
+
|
|
122
|
+
# Check approvals
|
|
123
|
+
if sig_type in (SignatureType.POLY_GNOSIS_SAFE, SignatureType.EOA) and not approvals_set:
|
|
124
|
+
try:
|
|
125
|
+
approval_status = await check_approvals_onchain(funder, self._rpc_url)
|
|
126
|
+
if approval_status.all_approved:
|
|
127
|
+
approvals_set = True
|
|
128
|
+
actions.append("approvals_already_set")
|
|
129
|
+
else:
|
|
130
|
+
actions.append("approvals_needed")
|
|
131
|
+
except Exception:
|
|
132
|
+
actions.append("approval_check_failed")
|
|
133
|
+
|
|
134
|
+
# Create CLOB credentials
|
|
135
|
+
if not stored:
|
|
136
|
+
creds = await create_clob_credentials(normalized, funder)
|
|
137
|
+
stored = StoredCredentials(
|
|
138
|
+
wallet_address=normalized.address,
|
|
139
|
+
funder_address=funder,
|
|
140
|
+
api_key=creds["apiKey"],
|
|
141
|
+
api_secret=creds["apiSecret"],
|
|
142
|
+
api_passphrase=creds["apiPassphrase"],
|
|
143
|
+
signature_type=sig_type,
|
|
144
|
+
safe_deployed=safe_deployed,
|
|
145
|
+
approvals_set=approvals_set,
|
|
146
|
+
created_at=time.time(),
|
|
147
|
+
updated_at=time.time(),
|
|
148
|
+
)
|
|
149
|
+
db.upsert_credentials(stored)
|
|
150
|
+
actions.append("credentials_created")
|
|
151
|
+
else:
|
|
152
|
+
stored.safe_deployed = safe_deployed
|
|
153
|
+
stored.approvals_set = approvals_set
|
|
154
|
+
stored.updated_at = time.time()
|
|
155
|
+
db.upsert_credentials(stored)
|
|
156
|
+
actions.append("credentials_loaded")
|
|
157
|
+
|
|
158
|
+
self._active_signer = normalized
|
|
159
|
+
self._active_wallet = normalized.address
|
|
160
|
+
|
|
161
|
+
return ReadyStatus(
|
|
162
|
+
wallet=normalized.address,
|
|
163
|
+
funder_address=funder,
|
|
164
|
+
signature_type=sig_type,
|
|
165
|
+
safe_deployed=safe_deployed,
|
|
166
|
+
approvals_set=approvals_set,
|
|
167
|
+
credentials_stored=True,
|
|
168
|
+
credentials={
|
|
169
|
+
"apiKey": stored.api_key,
|
|
170
|
+
"apiSecret": stored.api_secret,
|
|
171
|
+
"apiPassphrase": stored.api_passphrase,
|
|
172
|
+
},
|
|
173
|
+
actions=actions,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
async def link_wallet(
|
|
177
|
+
self,
|
|
178
|
+
signer: str | RouterSigner,
|
|
179
|
+
*,
|
|
180
|
+
type: SignatureType | None = None,
|
|
181
|
+
) -> LinkResult:
|
|
182
|
+
"""Link a wallet manually (derive credentials, store locally)."""
|
|
183
|
+
sig_type = type or self._default_sig_type
|
|
184
|
+
normalized = await normalize_signer(signer, sig_type)
|
|
185
|
+
funder = normalized.funder_address or await derive_funder_address(normalized.address, sig_type)
|
|
186
|
+
|
|
187
|
+
db = self._get_db()
|
|
188
|
+
existing = db.get_credentials(normalized.address)
|
|
189
|
+
|
|
190
|
+
if existing:
|
|
191
|
+
creds_dict = {
|
|
192
|
+
"apiKey": existing.api_key,
|
|
193
|
+
"apiSecret": existing.api_secret,
|
|
194
|
+
"apiPassphrase": existing.api_passphrase,
|
|
195
|
+
}
|
|
196
|
+
else:
|
|
197
|
+
creds_dict = await create_clob_credentials(normalized, funder)
|
|
198
|
+
|
|
199
|
+
db.upsert_credentials(StoredCredentials(
|
|
200
|
+
wallet_address=normalized.address,
|
|
201
|
+
funder_address=funder,
|
|
202
|
+
api_key=creds_dict["apiKey"],
|
|
203
|
+
api_secret=creds_dict["apiSecret"],
|
|
204
|
+
api_passphrase=creds_dict["apiPassphrase"],
|
|
205
|
+
signature_type=sig_type,
|
|
206
|
+
safe_deployed=existing.safe_deployed if existing else False,
|
|
207
|
+
approvals_set=existing.approvals_set if existing else False,
|
|
208
|
+
created_at=existing.created_at if existing else time.time(),
|
|
209
|
+
updated_at=time.time(),
|
|
210
|
+
))
|
|
211
|
+
|
|
212
|
+
self._active_signer = normalized
|
|
213
|
+
self._active_wallet = normalized.address
|
|
214
|
+
|
|
215
|
+
return LinkResult(
|
|
216
|
+
wallet=normalized.address,
|
|
217
|
+
funder_address=funder,
|
|
218
|
+
signature_type=sig_type,
|
|
219
|
+
credentials=creds_dict,
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
def link_credentials(
|
|
223
|
+
self,
|
|
224
|
+
*,
|
|
225
|
+
wallet: str,
|
|
226
|
+
api_key: str,
|
|
227
|
+
api_secret: str,
|
|
228
|
+
api_passphrase: str,
|
|
229
|
+
signature_type: SignatureType = SignatureType.EOA,
|
|
230
|
+
funder_address: str | None = None,
|
|
231
|
+
) -> None:
|
|
232
|
+
"""Import existing CLOB credentials directly."""
|
|
233
|
+
db = self._get_db()
|
|
234
|
+
db.upsert_credentials(StoredCredentials(
|
|
235
|
+
wallet_address=wallet,
|
|
236
|
+
funder_address=funder_address,
|
|
237
|
+
api_key=api_key,
|
|
238
|
+
api_secret=api_secret,
|
|
239
|
+
api_passphrase=api_passphrase,
|
|
240
|
+
signature_type=signature_type,
|
|
241
|
+
safe_deployed=True,
|
|
242
|
+
approvals_set=True,
|
|
243
|
+
created_at=time.time(),
|
|
244
|
+
updated_at=time.time(),
|
|
245
|
+
))
|
|
246
|
+
self._active_wallet = wallet
|
|
247
|
+
|
|
248
|
+
def unlink_wallet(self, address: str | None = None) -> None:
|
|
249
|
+
wallet = address or self._active_wallet
|
|
250
|
+
if not wallet:
|
|
251
|
+
return
|
|
252
|
+
self._get_db().delete_credentials(wallet)
|
|
253
|
+
if self._active_wallet == wallet:
|
|
254
|
+
self._active_wallet = None
|
|
255
|
+
self._active_signer = None
|
|
256
|
+
|
|
257
|
+
def get_linked_wallets(self) -> list[WalletInfo]:
|
|
258
|
+
return [
|
|
259
|
+
WalletInfo(
|
|
260
|
+
wallet=c.wallet_address,
|
|
261
|
+
funder_address=c.funder_address or c.wallet_address,
|
|
262
|
+
signature_type=c.signature_type,
|
|
263
|
+
credentials={
|
|
264
|
+
"apiKey": c.api_key,
|
|
265
|
+
"apiSecret": c.api_secret,
|
|
266
|
+
"apiPassphrase": c.api_passphrase,
|
|
267
|
+
},
|
|
268
|
+
created_at=c.created_at,
|
|
269
|
+
)
|
|
270
|
+
for c in self._get_db().get_all_credentials()
|
|
271
|
+
]
|
|
272
|
+
|
|
273
|
+
# ── Export / Backup ──
|
|
274
|
+
|
|
275
|
+
def export_wallet(self, wallet: str | None = None) -> WalletExport | None:
|
|
276
|
+
addr = wallet or self._active_wallet
|
|
277
|
+
if not addr:
|
|
278
|
+
return None
|
|
279
|
+
creds = self._get_db().get_credentials(addr)
|
|
280
|
+
if not creds:
|
|
281
|
+
return None
|
|
282
|
+
return WalletExport(
|
|
283
|
+
wallet=creds.wallet_address,
|
|
284
|
+
funder_address=creds.funder_address or creds.wallet_address,
|
|
285
|
+
signature_type=creds.signature_type,
|
|
286
|
+
credentials={
|
|
287
|
+
"apiKey": creds.api_key,
|
|
288
|
+
"apiSecret": creds.api_secret,
|
|
289
|
+
"apiPassphrase": creds.api_passphrase,
|
|
290
|
+
},
|
|
291
|
+
safe_deployed=creds.safe_deployed,
|
|
292
|
+
approvals_set=creds.approvals_set,
|
|
293
|
+
created_at=creds.created_at,
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
def export_all(self) -> list[WalletExport]:
|
|
297
|
+
return [
|
|
298
|
+
WalletExport(
|
|
299
|
+
wallet=c.wallet_address,
|
|
300
|
+
funder_address=c.funder_address or c.wallet_address,
|
|
301
|
+
signature_type=c.signature_type,
|
|
302
|
+
credentials={"apiKey": c.api_key, "apiSecret": c.api_secret, "apiPassphrase": c.api_passphrase},
|
|
303
|
+
safe_deployed=c.safe_deployed,
|
|
304
|
+
approvals_set=c.approvals_set,
|
|
305
|
+
created_at=c.created_at,
|
|
306
|
+
)
|
|
307
|
+
for c in self._get_db().get_all_credentials()
|
|
308
|
+
]
|
|
309
|
+
|
|
310
|
+
def import_wallet(self, exported: WalletExport) -> None:
|
|
311
|
+
self._get_db().upsert_credentials(StoredCredentials(
|
|
312
|
+
wallet_address=exported.wallet,
|
|
313
|
+
funder_address=exported.funder_address,
|
|
314
|
+
api_key=exported.credentials["apiKey"],
|
|
315
|
+
api_secret=exported.credentials["apiSecret"],
|
|
316
|
+
api_passphrase=exported.credentials["apiPassphrase"],
|
|
317
|
+
signature_type=exported.signature_type,
|
|
318
|
+
safe_deployed=exported.safe_deployed,
|
|
319
|
+
approvals_set=exported.approvals_set,
|
|
320
|
+
created_at=exported.created_at,
|
|
321
|
+
updated_at=time.time(),
|
|
322
|
+
))
|
|
323
|
+
|
|
324
|
+
# ── Pre-Trade Checks ──
|
|
325
|
+
|
|
326
|
+
async def check_approvals(self, wallet: str | None = None) -> ApprovalStatus:
|
|
327
|
+
creds = self._get_stored_creds(wallet)
|
|
328
|
+
return await check_approvals_onchain(creds.funder_address or creds.wallet_address, self._rpc_url)
|
|
329
|
+
|
|
330
|
+
async def check_balance(self, wallet: str | None = None) -> BalanceInfo:
|
|
331
|
+
creds = self._get_stored_creds(wallet)
|
|
332
|
+
return await check_balance_onchain(creds.funder_address or creds.wallet_address, self._rpc_url)
|
|
333
|
+
|
|
334
|
+
# ── Trading ──
|
|
335
|
+
|
|
336
|
+
async def order(self, params: OrderParams) -> OrderResult:
|
|
337
|
+
"""Place an order on Polymarket."""
|
|
338
|
+
creds = self._get_stored_creds()
|
|
339
|
+
signer = self._require_signer()
|
|
340
|
+
meta = await self._fetch_meta(params.token_id)
|
|
341
|
+
|
|
342
|
+
signed_order = await create_signed_order(
|
|
343
|
+
signer.sign_typed_data,
|
|
344
|
+
signer_address=signer.address,
|
|
345
|
+
funder_address=creds.funder_address or creds.wallet_address,
|
|
346
|
+
token_id=params.token_id,
|
|
347
|
+
price=params.price,
|
|
348
|
+
size=params.size,
|
|
349
|
+
side=params.side,
|
|
350
|
+
signature_type=creds.signature_type,
|
|
351
|
+
tick_size=meta.tick_size,
|
|
352
|
+
neg_risk=meta.neg_risk,
|
|
353
|
+
fee_rate_bps=meta.fee_rate_bps,
|
|
354
|
+
expiration=params.expiration or 0,
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
payload = {
|
|
358
|
+
"order": signed_order,
|
|
359
|
+
"owner": creds.funder_address or creds.wallet_address,
|
|
360
|
+
"orderType": params.type,
|
|
361
|
+
}
|
|
362
|
+
if params.post_only:
|
|
363
|
+
payload["postOnly"] = True
|
|
364
|
+
|
|
365
|
+
body_str = json.dumps(payload)
|
|
366
|
+
headers = build_l2_headers(
|
|
367
|
+
creds.api_key, creds.api_secret, creds.api_passphrase,
|
|
368
|
+
creds.wallet_address, "POST", "/order", body_str,
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
db = self._get_db()
|
|
372
|
+
local_id = db.insert_order({
|
|
373
|
+
"wallet_address": creds.wallet_address,
|
|
374
|
+
"token_id": params.token_id,
|
|
375
|
+
"side": params.side,
|
|
376
|
+
"price": params.price,
|
|
377
|
+
"size": params.size,
|
|
378
|
+
"order_type": params.type,
|
|
379
|
+
"status": "submitting",
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
result = await send_via_cosigner(
|
|
383
|
+
self._cosigner_url, self._polynode_key, self._fallback_direct,
|
|
384
|
+
{"method": "POST", "path": "/order", "body": body_str, "headers": headers},
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
order_id = result.get("orderID") or result.get("orderId")
|
|
388
|
+
success = bool(result.get("success") or order_id)
|
|
389
|
+
error = result.get("error") or result.get("errorMsg")
|
|
390
|
+
|
|
391
|
+
db.update_order_status(
|
|
392
|
+
local_id,
|
|
393
|
+
"submitted" if success else "failed",
|
|
394
|
+
order_id, error, json.dumps(result),
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
return OrderResult(
|
|
398
|
+
success=success,
|
|
399
|
+
order_id=order_id,
|
|
400
|
+
status=result.get("status"),
|
|
401
|
+
error=error,
|
|
402
|
+
making_amount=result.get("makingAmount"),
|
|
403
|
+
taking_amount=result.get("takingAmount"),
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
async def cancel_order(self, order_id: str) -> CancelResult:
|
|
407
|
+
creds = self._get_stored_creds()
|
|
408
|
+
result = await clob_cancel_order(
|
|
409
|
+
self._cosigner_url, self._polynode_key, self._fallback_direct,
|
|
410
|
+
{"apiKey": creds.api_key, "apiSecret": creds.api_secret, "apiPassphrase": creds.api_passphrase},
|
|
411
|
+
creds.wallet_address, order_id,
|
|
412
|
+
)
|
|
413
|
+
return CancelResult(
|
|
414
|
+
canceled=result.get("canceled", []),
|
|
415
|
+
not_canceled=result.get("not_canceled", {}),
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
async def cancel_all(self, market: str | None = None) -> CancelResult:
|
|
419
|
+
creds = self._get_stored_creds()
|
|
420
|
+
result = await cancel_all_orders(
|
|
421
|
+
self._cosigner_url, self._polynode_key, self._fallback_direct,
|
|
422
|
+
{"apiKey": creds.api_key, "apiSecret": creds.api_secret, "apiPassphrase": creds.api_passphrase},
|
|
423
|
+
creds.wallet_address, market,
|
|
424
|
+
)
|
|
425
|
+
return CancelResult(
|
|
426
|
+
canceled=result.get("canceled", []),
|
|
427
|
+
not_canceled=result.get("not_canceled", {}),
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
async def get_open_orders(
|
|
431
|
+
self, *, market: str | None = None, asset_id: str | None = None
|
|
432
|
+
) -> list[OpenOrder]:
|
|
433
|
+
creds = self._get_stored_creds()
|
|
434
|
+
result = await clob_get_open_orders(
|
|
435
|
+
self._cosigner_url, self._polynode_key, self._fallback_direct,
|
|
436
|
+
{"apiKey": creds.api_key, "apiSecret": creds.api_secret, "apiPassphrase": creds.api_passphrase},
|
|
437
|
+
creds.wallet_address, market, asset_id,
|
|
438
|
+
)
|
|
439
|
+
return [
|
|
440
|
+
OpenOrder(
|
|
441
|
+
id=o.get("id", ""), market=o.get("market", ""),
|
|
442
|
+
asset_id=o.get("assetId") or o.get("asset_id", ""),
|
|
443
|
+
side=o.get("side", ""), price=o.get("price", ""),
|
|
444
|
+
original_size=o.get("originalSize") or o.get("original_size", ""),
|
|
445
|
+
size_matched=o.get("sizeMatched") or o.get("size_matched", ""),
|
|
446
|
+
status=o.get("status", ""), created_at=o.get("createdAt") or o.get("created_at", ""),
|
|
447
|
+
order_type=o.get("orderType") or o.get("order_type", ""),
|
|
448
|
+
)
|
|
449
|
+
for o in result
|
|
450
|
+
]
|
|
451
|
+
|
|
452
|
+
# ── Metadata ──
|
|
453
|
+
|
|
454
|
+
async def prefetch_meta(self, token_ids: list[str]) -> None:
|
|
455
|
+
import asyncio
|
|
456
|
+
await asyncio.gather(*(self._fetch_meta(tid) for tid in token_ids))
|
|
457
|
+
|
|
458
|
+
async def _fetch_meta(self, token_id: str) -> MarketMeta:
|
|
459
|
+
db = self._get_db()
|
|
460
|
+
cached = db.get_market_meta(token_id)
|
|
461
|
+
if cached:
|
|
462
|
+
return cached
|
|
463
|
+
|
|
464
|
+
import asyncio
|
|
465
|
+
tick, neg = await asyncio.gather(fetch_tick_size(token_id), fetch_neg_risk(token_id))
|
|
466
|
+
meta = MarketMeta(
|
|
467
|
+
token_id=token_id,
|
|
468
|
+
tick_size=tick,
|
|
469
|
+
fee_rate_bps=0,
|
|
470
|
+
neg_risk=neg,
|
|
471
|
+
fetched_at=time.time(),
|
|
472
|
+
)
|
|
473
|
+
db.upsert_market_meta(meta)
|
|
474
|
+
return meta
|
|
475
|
+
|
|
476
|
+
# ── History ──
|
|
477
|
+
|
|
478
|
+
def get_order_history(self, params: HistoryParams | None = None) -> list[OrderHistoryRow]:
|
|
479
|
+
if not self._active_wallet:
|
|
480
|
+
return []
|
|
481
|
+
return self._get_db().get_order_history(self._active_wallet, params)
|
|
482
|
+
|
|
483
|
+
# ── Lifecycle ──
|
|
484
|
+
|
|
485
|
+
def close(self) -> None:
|
|
486
|
+
if self._db:
|
|
487
|
+
self._db.close()
|
|
488
|
+
self._db = None
|
|
489
|
+
self._active_signer = None
|
|
490
|
+
self._active_wallet = None
|
|
491
|
+
|
|
492
|
+
# ── Internal ──
|
|
493
|
+
|
|
494
|
+
def _get_stored_creds(self, wallet: str | None = None) -> StoredCredentials:
|
|
495
|
+
addr = wallet or self._active_wallet
|
|
496
|
+
if not addr:
|
|
497
|
+
raise RuntimeError("No active wallet. Call ensure_ready() or link_wallet() first.")
|
|
498
|
+
creds = self._get_db().get_credentials(addr)
|
|
499
|
+
if not creds:
|
|
500
|
+
raise RuntimeError(f"No credentials found for {addr}. Call ensure_ready() first.")
|
|
501
|
+
return creds
|
|
502
|
+
|
|
503
|
+
def _require_signer(self) -> NormalizedSigner:
|
|
504
|
+
if not self._active_signer:
|
|
505
|
+
raise RuntimeError("No active signer. Call ensure_ready() or link_wallet() first.")
|
|
506
|
+
return self._active_signer
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""Trading module type definitions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from enum import IntEnum
|
|
7
|
+
from typing import Any, Literal, Protocol, runtime_checkable
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SignatureType(IntEnum):
|
|
11
|
+
EOA = 0
|
|
12
|
+
POLY_PROXY = 1
|
|
13
|
+
POLY_GNOSIS_SAFE = 2
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@runtime_checkable
|
|
17
|
+
class RouterSigner(Protocol):
|
|
18
|
+
async def get_address(self) -> str: ...
|
|
19
|
+
async def sign_typed_data(self, payload: Eip712Payload) -> str: ...
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class Eip712Payload:
|
|
24
|
+
domain: dict[str, Any]
|
|
25
|
+
types: dict[str, Any]
|
|
26
|
+
primary_type: str
|
|
27
|
+
message: dict[str, Any]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class GeneratedWallet:
|
|
32
|
+
private_key: str
|
|
33
|
+
address: str
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class TraderConfig:
|
|
38
|
+
polynode_key: str = ""
|
|
39
|
+
db_path: str = "./polynode-trading.db"
|
|
40
|
+
cosigner_url: str = "https://trade.polynode.dev"
|
|
41
|
+
fallback_direct: bool = True
|
|
42
|
+
default_signature_type: SignatureType = SignatureType.POLY_GNOSIS_SAFE
|
|
43
|
+
rpc_url: str = "https://polygon-bor-rpc.publicnode.com"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class ReadyStatus:
|
|
48
|
+
wallet: str
|
|
49
|
+
funder_address: str
|
|
50
|
+
signature_type: SignatureType
|
|
51
|
+
safe_deployed: bool
|
|
52
|
+
approvals_set: bool
|
|
53
|
+
credentials_stored: bool
|
|
54
|
+
credentials: dict[str, str]
|
|
55
|
+
actions: list[str]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass
|
|
59
|
+
class LinkResult:
|
|
60
|
+
wallet: str
|
|
61
|
+
funder_address: str
|
|
62
|
+
signature_type: SignatureType
|
|
63
|
+
credentials: dict[str, str]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass
|
|
67
|
+
class WalletInfo:
|
|
68
|
+
wallet: str
|
|
69
|
+
funder_address: str
|
|
70
|
+
signature_type: SignatureType
|
|
71
|
+
credentials: dict[str, str]
|
|
72
|
+
created_at: float
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass
|
|
76
|
+
class WalletExport:
|
|
77
|
+
wallet: str
|
|
78
|
+
funder_address: str
|
|
79
|
+
signature_type: SignatureType
|
|
80
|
+
credentials: dict[str, str]
|
|
81
|
+
safe_deployed: bool
|
|
82
|
+
approvals_set: bool
|
|
83
|
+
created_at: float
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass
|
|
87
|
+
class ApprovalStatus:
|
|
88
|
+
funder_address: str
|
|
89
|
+
usdc: dict[str, bool]
|
|
90
|
+
ctf: dict[str, bool]
|
|
91
|
+
all_approved: bool
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@dataclass
|
|
95
|
+
class BalanceInfo:
|
|
96
|
+
funder_address: str
|
|
97
|
+
usdc: str
|
|
98
|
+
usdc_raw: str
|
|
99
|
+
matic: str
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
OrderSide = Literal["BUY", "SELL"]
|
|
103
|
+
OrderType = Literal["GTC", "GTD", "FOK", "FAK"]
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@dataclass
|
|
107
|
+
class OrderParams:
|
|
108
|
+
token_id: str
|
|
109
|
+
side: OrderSide
|
|
110
|
+
price: float
|
|
111
|
+
size: float
|
|
112
|
+
type: OrderType = "GTC"
|
|
113
|
+
expiration: int | None = None
|
|
114
|
+
post_only: bool = False
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@dataclass
|
|
118
|
+
class OrderResult:
|
|
119
|
+
success: bool
|
|
120
|
+
order_id: str | None = None
|
|
121
|
+
status: str | None = None
|
|
122
|
+
error: str | None = None
|
|
123
|
+
making_amount: str | None = None
|
|
124
|
+
taking_amount: str | None = None
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@dataclass
|
|
128
|
+
class CancelResult:
|
|
129
|
+
canceled: list[str] = field(default_factory=list)
|
|
130
|
+
not_canceled: dict[str, str] = field(default_factory=dict)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@dataclass
|
|
134
|
+
class OpenOrder:
|
|
135
|
+
id: str
|
|
136
|
+
market: str
|
|
137
|
+
asset_id: str
|
|
138
|
+
side: str
|
|
139
|
+
price: str
|
|
140
|
+
original_size: str
|
|
141
|
+
size_matched: str
|
|
142
|
+
status: str
|
|
143
|
+
created_at: str
|
|
144
|
+
order_type: str
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@dataclass
|
|
148
|
+
class MarketMeta:
|
|
149
|
+
token_id: str
|
|
150
|
+
tick_size: str
|
|
151
|
+
fee_rate_bps: int
|
|
152
|
+
neg_risk: bool
|
|
153
|
+
fetched_at: float
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@dataclass
|
|
157
|
+
class OrderHistoryRow:
|
|
158
|
+
id: int
|
|
159
|
+
wallet_address: str
|
|
160
|
+
order_id: str | None
|
|
161
|
+
token_id: str
|
|
162
|
+
side: str
|
|
163
|
+
price: float
|
|
164
|
+
size: float
|
|
165
|
+
order_type: str
|
|
166
|
+
status: str
|
|
167
|
+
error_msg: str | None
|
|
168
|
+
response_json: str | None
|
|
169
|
+
created_at: float
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@dataclass
|
|
173
|
+
class HistoryParams:
|
|
174
|
+
limit: int | None = None
|
|
175
|
+
offset: int | None = None
|
|
176
|
+
token_id: str | None = None
|
|
177
|
+
side: OrderSide | None = None
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@dataclass
|
|
181
|
+
class StoredCredentials:
|
|
182
|
+
wallet_address: str
|
|
183
|
+
funder_address: str | None
|
|
184
|
+
api_key: str
|
|
185
|
+
api_secret: str
|
|
186
|
+
api_passphrase: str
|
|
187
|
+
signature_type: SignatureType
|
|
188
|
+
safe_deployed: bool
|
|
189
|
+
approvals_set: bool
|
|
190
|
+
created_at: float
|
|
191
|
+
updated_at: float
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""Re-export all types."""
|
|
2
|
+
|
|
3
|
+
from .enums import * # noqa: F401, F403
|
|
4
|
+
from .events import * # noqa: F401, F403
|
|
5
|
+
from .orderbook import * # noqa: F401, F403
|
|
6
|
+
from .rest import * # noqa: F401, F403
|
|
7
|
+
from .short_form import * # noqa: F401, F403
|
|
8
|
+
from .ws import * # noqa: F401, F403
|