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,242 @@
|
|
|
1
|
+
"""Onboarding logic for Safe deployment, wallet type detection, approvals, and CLOB credentials."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from .constants import (
|
|
10
|
+
CHAIN_ID,
|
|
11
|
+
CLOB_HOST,
|
|
12
|
+
CTF,
|
|
13
|
+
CTF_EXCHANGE,
|
|
14
|
+
NEG_RISK_ADAPTER,
|
|
15
|
+
NEG_RISK_CTF_EXCHANGE,
|
|
16
|
+
PROXY_FACTORY,
|
|
17
|
+
PROXY_INIT_CODE_HASH,
|
|
18
|
+
RELAYER_HOST,
|
|
19
|
+
SAFE_FACTORY,
|
|
20
|
+
SAFE_INIT_CODE_HASH,
|
|
21
|
+
SPENDERS,
|
|
22
|
+
USDC,
|
|
23
|
+
)
|
|
24
|
+
from .types import ApprovalStatus, BalanceInfo, SignatureType
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def derive_safe_address(eoa: str) -> str:
|
|
28
|
+
"""Derive the CREATE2 Safe address for an EOA."""
|
|
29
|
+
try:
|
|
30
|
+
from eth_abi import encode
|
|
31
|
+
from eth_utils import keccak, to_checksum_address
|
|
32
|
+
except ImportError:
|
|
33
|
+
raise ImportError("eth-account/web3 required. Install with: pip install polynode[trading]")
|
|
34
|
+
|
|
35
|
+
eoa_lower = eoa.lower()
|
|
36
|
+
if eoa_lower.startswith("0x"):
|
|
37
|
+
eoa_lower = eoa_lower[2:]
|
|
38
|
+
|
|
39
|
+
# Salt = keccak256(abi.encode(["address"], [eoa]))
|
|
40
|
+
encoded = encode(["address"], [eoa])
|
|
41
|
+
salt = keccak(encoded)
|
|
42
|
+
|
|
43
|
+
factory_bytes = bytes.fromhex(SAFE_FACTORY[2:])
|
|
44
|
+
init_code_hash = bytes.fromhex(SAFE_INIT_CODE_HASH[2:])
|
|
45
|
+
|
|
46
|
+
addr_bytes = keccak(b"\xff" + factory_bytes + salt + init_code_hash)
|
|
47
|
+
return to_checksum_address("0x" + addr_bytes[-20:].hex())
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def derive_proxy_address(eoa: str) -> str:
|
|
51
|
+
"""Derive the CREATE2 Proxy address for an EOA."""
|
|
52
|
+
try:
|
|
53
|
+
from eth_abi.packed import encode_packed
|
|
54
|
+
from eth_utils import keccak, to_checksum_address
|
|
55
|
+
except ImportError:
|
|
56
|
+
raise ImportError("eth-account/web3 required. Install with: pip install polynode[trading]")
|
|
57
|
+
|
|
58
|
+
# Salt = keccak256(encodePacked(["address"], [eoa]))
|
|
59
|
+
packed = encode_packed(["address"], [eoa])
|
|
60
|
+
salt = keccak(packed)
|
|
61
|
+
|
|
62
|
+
factory_bytes = bytes.fromhex(PROXY_FACTORY[2:])
|
|
63
|
+
init_code_hash = bytes.fromhex(PROXY_INIT_CODE_HASH[2:])
|
|
64
|
+
|
|
65
|
+
addr_bytes = keccak(b"\xff" + factory_bytes + salt + init_code_hash)
|
|
66
|
+
return to_checksum_address("0x" + addr_bytes[-20:].hex())
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
async def derive_funder_address(eoa: str, sig_type: SignatureType) -> str:
|
|
70
|
+
"""Derive the funder address based on signature type."""
|
|
71
|
+
if sig_type == SignatureType.POLY_GNOSIS_SAFE:
|
|
72
|
+
return derive_safe_address(eoa)
|
|
73
|
+
elif sig_type == SignatureType.POLY_PROXY:
|
|
74
|
+
return derive_proxy_address(eoa)
|
|
75
|
+
return eoa
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
async def detect_wallet_type(eoa: str) -> dict:
|
|
79
|
+
"""Auto-detect if a Safe or Proxy is deployed for this EOA."""
|
|
80
|
+
safe_addr = derive_safe_address(eoa)
|
|
81
|
+
proxy_addr = derive_proxy_address(eoa)
|
|
82
|
+
|
|
83
|
+
async with httpx.AsyncClient(timeout=10.0) as http:
|
|
84
|
+
# Check Safe first (most common)
|
|
85
|
+
try:
|
|
86
|
+
resp = await http.get(f"{RELAYER_HOST}/deployed?owner={eoa}&salt={eoa}")
|
|
87
|
+
if resp.is_success:
|
|
88
|
+
data = resp.json()
|
|
89
|
+
if data.get("deployed"):
|
|
90
|
+
return {
|
|
91
|
+
"signature_type": SignatureType.POLY_GNOSIS_SAFE,
|
|
92
|
+
"funder_address": safe_addr,
|
|
93
|
+
}
|
|
94
|
+
except Exception:
|
|
95
|
+
pass
|
|
96
|
+
|
|
97
|
+
# Check Proxy
|
|
98
|
+
try:
|
|
99
|
+
resp = await http.get(f"{RELAYER_HOST}/deployed?owner={eoa}&salt={eoa}&type=proxy")
|
|
100
|
+
if resp.is_success:
|
|
101
|
+
data = resp.json()
|
|
102
|
+
if data.get("deployed"):
|
|
103
|
+
return {
|
|
104
|
+
"signature_type": SignatureType.POLY_PROXY,
|
|
105
|
+
"funder_address": proxy_addr,
|
|
106
|
+
}
|
|
107
|
+
except Exception:
|
|
108
|
+
pass
|
|
109
|
+
|
|
110
|
+
# Neither deployed — default to Safe
|
|
111
|
+
return {
|
|
112
|
+
"signature_type": SignatureType.POLY_GNOSIS_SAFE,
|
|
113
|
+
"funder_address": safe_addr,
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
async def is_safe_deployed(funder_address: str) -> bool:
|
|
118
|
+
"""Check if a Safe/Proxy is deployed at the given address."""
|
|
119
|
+
async with httpx.AsyncClient(timeout=10.0) as http:
|
|
120
|
+
try:
|
|
121
|
+
resp = await http.get(f"{RELAYER_HOST}/deployed?address={funder_address}")
|
|
122
|
+
if resp.is_success:
|
|
123
|
+
return resp.json().get("deployed", False)
|
|
124
|
+
except Exception:
|
|
125
|
+
pass
|
|
126
|
+
return False
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
async def check_approvals(funder_address: str, rpc_url: str) -> ApprovalStatus:
|
|
130
|
+
"""Check token approvals for all spender contracts."""
|
|
131
|
+
try:
|
|
132
|
+
from web3 import Web3
|
|
133
|
+
except ImportError:
|
|
134
|
+
raise ImportError("web3 required. Install with: pip install polynode[trading]")
|
|
135
|
+
|
|
136
|
+
w3 = Web3(Web3.HTTPProvider(rpc_url))
|
|
137
|
+
erc20_abi = [{"inputs": [{"name": "owner", "type": "address"}, {"name": "spender", "type": "address"}], "name": "allowance", "outputs": [{"name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"}]
|
|
138
|
+
erc1155_abi = [{"inputs": [{"name": "account", "type": "address"}, {"name": "operator", "type": "address"}], "name": "isApprovedForAll", "outputs": [{"name": "", "type": "bool"}], "stateMutability": "view", "type": "function"}]
|
|
139
|
+
|
|
140
|
+
usdc_contract = w3.eth.contract(address=Web3.to_checksum_address(USDC), abi=erc20_abi)
|
|
141
|
+
ctf_contract = w3.eth.contract(address=Web3.to_checksum_address(CTF), abi=erc1155_abi)
|
|
142
|
+
|
|
143
|
+
funder = Web3.to_checksum_address(funder_address)
|
|
144
|
+
max_uint = 2**256 - 1
|
|
145
|
+
threshold = max_uint // 2
|
|
146
|
+
|
|
147
|
+
usdc_status = {}
|
|
148
|
+
ctf_status = {}
|
|
149
|
+
for spender, name in [(CTF_EXCHANGE, "ctf_exchange"), (NEG_RISK_CTF_EXCHANGE, "neg_risk_ctf_exchange"), (NEG_RISK_ADAPTER, "neg_risk_adapter")]:
|
|
150
|
+
spender_addr = Web3.to_checksum_address(spender)
|
|
151
|
+
allowance = usdc_contract.functions.allowance(funder, spender_addr).call()
|
|
152
|
+
usdc_status[name] = allowance >= threshold
|
|
153
|
+
approved = ctf_contract.functions.isApprovedForAll(funder, spender_addr).call()
|
|
154
|
+
ctf_status[name] = approved
|
|
155
|
+
|
|
156
|
+
all_approved = all(usdc_status.values()) and all(ctf_status.values())
|
|
157
|
+
|
|
158
|
+
return ApprovalStatus(
|
|
159
|
+
funder_address=funder_address,
|
|
160
|
+
usdc=usdc_status,
|
|
161
|
+
ctf=ctf_status,
|
|
162
|
+
all_approved=all_approved,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
async def check_balance(funder_address: str, rpc_url: str) -> BalanceInfo:
|
|
167
|
+
"""Check USDC and MATIC balance."""
|
|
168
|
+
try:
|
|
169
|
+
from web3 import Web3
|
|
170
|
+
except ImportError:
|
|
171
|
+
raise ImportError("web3 required. Install with: pip install polynode[trading]")
|
|
172
|
+
|
|
173
|
+
w3 = Web3(Web3.HTTPProvider(rpc_url))
|
|
174
|
+
erc20_abi = [{"inputs": [{"name": "account", "type": "address"}], "name": "balanceOf", "outputs": [{"name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"}]
|
|
175
|
+
|
|
176
|
+
usdc_contract = w3.eth.contract(address=Web3.to_checksum_address(USDC), abi=erc20_abi)
|
|
177
|
+
funder = Web3.to_checksum_address(funder_address)
|
|
178
|
+
|
|
179
|
+
usdc_raw = usdc_contract.functions.balanceOf(funder).call()
|
|
180
|
+
matic_raw = w3.eth.get_balance(funder)
|
|
181
|
+
|
|
182
|
+
return BalanceInfo(
|
|
183
|
+
funder_address=funder_address,
|
|
184
|
+
usdc=f"{usdc_raw / 1_000_000:.2f}",
|
|
185
|
+
usdc_raw=str(usdc_raw),
|
|
186
|
+
matic=f"{matic_raw / 10**18:.4f}",
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
async def create_clob_credentials(signer: Any, funder_address: str) -> dict[str, str]:
|
|
191
|
+
"""Derive or create CLOB API credentials via the Polymarket CLOB API."""
|
|
192
|
+
from .types import Eip712Payload
|
|
193
|
+
|
|
194
|
+
# Build the derive-api-key signing payload
|
|
195
|
+
timestamp = str(int(__import__("time").time()))
|
|
196
|
+
nonce = 0
|
|
197
|
+
|
|
198
|
+
payload = Eip712Payload(
|
|
199
|
+
domain={
|
|
200
|
+
"name": "ClobAuthDomain",
|
|
201
|
+
"version": "1",
|
|
202
|
+
"chainId": CHAIN_ID,
|
|
203
|
+
},
|
|
204
|
+
types={
|
|
205
|
+
"ClobAuth": [
|
|
206
|
+
{"name": "address", "type": "address"},
|
|
207
|
+
{"name": "timestamp", "type": "string"},
|
|
208
|
+
{"name": "nonce", "type": "uint256"},
|
|
209
|
+
],
|
|
210
|
+
},
|
|
211
|
+
primary_type="ClobAuth",
|
|
212
|
+
message={
|
|
213
|
+
"address": funder_address,
|
|
214
|
+
"timestamp": timestamp,
|
|
215
|
+
"nonce": nonce,
|
|
216
|
+
},
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
signature = await signer.sign_typed_data(payload)
|
|
220
|
+
if not signature.startswith("0x"):
|
|
221
|
+
signature = f"0x{signature}"
|
|
222
|
+
|
|
223
|
+
async with httpx.AsyncClient(timeout=10.0) as http:
|
|
224
|
+
resp = await http.post(
|
|
225
|
+
f"{CLOB_HOST}/derive-api-key",
|
|
226
|
+
json={
|
|
227
|
+
"message": payload.message,
|
|
228
|
+
"owner": funder_address,
|
|
229
|
+
"signature": signature,
|
|
230
|
+
"nonce": nonce,
|
|
231
|
+
"timestamp": timestamp,
|
|
232
|
+
},
|
|
233
|
+
)
|
|
234
|
+
if not resp.is_success:
|
|
235
|
+
raise RuntimeError(f"Failed to derive CLOB credentials: {resp.status_code} {resp.text}")
|
|
236
|
+
data = resp.json()
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
"apiKey": data["apiKey"],
|
|
240
|
+
"apiSecret": data["secret"],
|
|
241
|
+
"apiPassphrase": data["passphrase"],
|
|
242
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Wallet signer abstraction — normalizes private keys and RouterSigners."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from .types import RouterSigner, SignatureType
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class NormalizedSigner:
|
|
13
|
+
address: str
|
|
14
|
+
sign_typed_data: Any # callable
|
|
15
|
+
sign_message: Any # callable or None
|
|
16
|
+
signature_type: SignatureType
|
|
17
|
+
funder_address: str | None = None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
async def normalize_signer(
|
|
21
|
+
signer: str | RouterSigner,
|
|
22
|
+
default_signature_type: SignatureType = SignatureType.POLY_GNOSIS_SAFE,
|
|
23
|
+
) -> NormalizedSigner:
|
|
24
|
+
"""Normalize a private key string or RouterSigner into a consistent interface."""
|
|
25
|
+
|
|
26
|
+
if isinstance(signer, str):
|
|
27
|
+
# Hex private key
|
|
28
|
+
try:
|
|
29
|
+
from eth_account import Account
|
|
30
|
+
except ImportError:
|
|
31
|
+
raise ImportError(
|
|
32
|
+
"eth-account is required for trading. Install with: pip install polynode[trading]"
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
key = signer if signer.startswith("0x") else f"0x{signer}"
|
|
36
|
+
account = Account.from_key(key)
|
|
37
|
+
|
|
38
|
+
async def _sign_typed_data(payload: Any) -> str:
|
|
39
|
+
from eth_account.messages import encode_typed_data
|
|
40
|
+
# Build full EIP-712 message dict for encode_typed_data
|
|
41
|
+
eip712_domain_type = [
|
|
42
|
+
{"name": k, "type": "string" if isinstance(v, str) else ("uint256" if isinstance(v, int) else "address")}
|
|
43
|
+
for k, v in payload.domain.items()
|
|
44
|
+
]
|
|
45
|
+
full_message = {
|
|
46
|
+
"primaryType": payload.primary_type,
|
|
47
|
+
"domain": payload.domain,
|
|
48
|
+
"types": {**payload.types, "EIP712Domain": eip712_domain_type},
|
|
49
|
+
"message": payload.message,
|
|
50
|
+
}
|
|
51
|
+
msg = encode_typed_data(full_message=full_message)
|
|
52
|
+
signed = account.sign_message(msg)
|
|
53
|
+
return signed.signature.hex()
|
|
54
|
+
|
|
55
|
+
async def _sign_message(message: str | bytes) -> str:
|
|
56
|
+
from eth_account.messages import encode_defunct
|
|
57
|
+
if isinstance(message, bytes):
|
|
58
|
+
msg = encode_defunct(primitive=message)
|
|
59
|
+
else:
|
|
60
|
+
msg = encode_defunct(text=message)
|
|
61
|
+
signed = account.sign_message(msg)
|
|
62
|
+
return signed.signature.hex()
|
|
63
|
+
|
|
64
|
+
return NormalizedSigner(
|
|
65
|
+
address=account.address,
|
|
66
|
+
sign_typed_data=_sign_typed_data,
|
|
67
|
+
sign_message=_sign_message,
|
|
68
|
+
signature_type=default_signature_type,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
if isinstance(signer, RouterSigner):
|
|
72
|
+
address = await signer.get_address()
|
|
73
|
+
|
|
74
|
+
async def _rs_sign_typed_data(payload: Any) -> str:
|
|
75
|
+
return await signer.sign_typed_data(payload)
|
|
76
|
+
|
|
77
|
+
_rs_sign_message = None
|
|
78
|
+
if hasattr(signer, "sign_message") and callable(getattr(signer, "sign_message", None)):
|
|
79
|
+
async def _rs_sign_message(message: str | bytes) -> str:
|
|
80
|
+
return await signer.sign_message(message) # type: ignore
|
|
81
|
+
|
|
82
|
+
return NormalizedSigner(
|
|
83
|
+
address=address,
|
|
84
|
+
sign_typed_data=_rs_sign_typed_data,
|
|
85
|
+
sign_message=_rs_sign_message,
|
|
86
|
+
signature_type=default_signature_type,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
raise TypeError(
|
|
90
|
+
"Unsupported signer type. Provide a hex private key string or RouterSigner."
|
|
91
|
+
)
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"""SQLite storage backend for trading credentials, market metadata, and order history."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sqlite3
|
|
6
|
+
import time
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from .constants import META_TTL_SECONDS
|
|
10
|
+
from .types import HistoryParams, MarketMeta, OrderHistoryRow, SignatureType, StoredCredentials
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TradingSqliteBackend:
|
|
14
|
+
"""Local SQLite storage for trading module."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, db_path: str) -> None:
|
|
17
|
+
self._db = sqlite3.connect(db_path)
|
|
18
|
+
self._db.execute("PRAGMA journal_mode=WAL")
|
|
19
|
+
self._db.execute("PRAGMA synchronous=NORMAL")
|
|
20
|
+
self._init_schema()
|
|
21
|
+
|
|
22
|
+
def _init_schema(self) -> None:
|
|
23
|
+
self._db.executescript("""
|
|
24
|
+
CREATE TABLE IF NOT EXISTS clob_credentials (
|
|
25
|
+
wallet_address TEXT PRIMARY KEY,
|
|
26
|
+
funder_address TEXT,
|
|
27
|
+
api_key TEXT NOT NULL,
|
|
28
|
+
api_secret TEXT NOT NULL,
|
|
29
|
+
api_passphrase TEXT NOT NULL,
|
|
30
|
+
signature_type INTEGER NOT NULL DEFAULT 2,
|
|
31
|
+
safe_deployed INTEGER NOT NULL DEFAULT 0,
|
|
32
|
+
approvals_set INTEGER NOT NULL DEFAULT 0,
|
|
33
|
+
created_at REAL NOT NULL,
|
|
34
|
+
updated_at REAL NOT NULL
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
CREATE TABLE IF NOT EXISTS market_meta (
|
|
38
|
+
token_id TEXT PRIMARY KEY,
|
|
39
|
+
tick_size TEXT NOT NULL,
|
|
40
|
+
fee_rate_bps INTEGER NOT NULL DEFAULT 0,
|
|
41
|
+
neg_risk INTEGER NOT NULL DEFAULT 0,
|
|
42
|
+
fetched_at REAL NOT NULL
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
CREATE TABLE IF NOT EXISTS order_history (
|
|
46
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
47
|
+
wallet_address TEXT NOT NULL,
|
|
48
|
+
order_id TEXT,
|
|
49
|
+
token_id TEXT NOT NULL,
|
|
50
|
+
side TEXT NOT NULL,
|
|
51
|
+
price REAL NOT NULL,
|
|
52
|
+
size REAL NOT NULL,
|
|
53
|
+
order_type TEXT NOT NULL DEFAULT 'GTC',
|
|
54
|
+
status TEXT NOT NULL DEFAULT 'submitting',
|
|
55
|
+
error_msg TEXT,
|
|
56
|
+
response_json TEXT,
|
|
57
|
+
created_at REAL NOT NULL
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
CREATE INDEX IF NOT EXISTS idx_order_history_wallet ON order_history(wallet_address);
|
|
61
|
+
CREATE INDEX IF NOT EXISTS idx_order_history_token ON order_history(token_id);
|
|
62
|
+
""")
|
|
63
|
+
|
|
64
|
+
def get_credentials(self, wallet_address: str) -> StoredCredentials | None:
|
|
65
|
+
row = self._db.execute(
|
|
66
|
+
"SELECT * FROM clob_credentials WHERE wallet_address = ? COLLATE NOCASE",
|
|
67
|
+
(wallet_address,),
|
|
68
|
+
).fetchone()
|
|
69
|
+
if not row:
|
|
70
|
+
return None
|
|
71
|
+
return StoredCredentials(
|
|
72
|
+
wallet_address=row[0],
|
|
73
|
+
funder_address=row[1],
|
|
74
|
+
api_key=row[2],
|
|
75
|
+
api_secret=row[3],
|
|
76
|
+
api_passphrase=row[4],
|
|
77
|
+
signature_type=SignatureType(row[5]),
|
|
78
|
+
safe_deployed=bool(row[6]),
|
|
79
|
+
approvals_set=bool(row[7]),
|
|
80
|
+
created_at=row[8],
|
|
81
|
+
updated_at=row[9],
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
def upsert_credentials(self, creds: StoredCredentials) -> None:
|
|
85
|
+
self._db.execute(
|
|
86
|
+
"""INSERT OR REPLACE INTO clob_credentials
|
|
87
|
+
(wallet_address, funder_address, api_key, api_secret, api_passphrase,
|
|
88
|
+
signature_type, safe_deployed, approvals_set, created_at, updated_at)
|
|
89
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
|
90
|
+
(
|
|
91
|
+
creds.wallet_address,
|
|
92
|
+
creds.funder_address,
|
|
93
|
+
creds.api_key,
|
|
94
|
+
creds.api_secret,
|
|
95
|
+
creds.api_passphrase,
|
|
96
|
+
int(creds.signature_type),
|
|
97
|
+
int(creds.safe_deployed),
|
|
98
|
+
int(creds.approvals_set),
|
|
99
|
+
creds.created_at,
|
|
100
|
+
creds.updated_at,
|
|
101
|
+
),
|
|
102
|
+
)
|
|
103
|
+
self._db.commit()
|
|
104
|
+
|
|
105
|
+
def delete_credentials(self, wallet_address: str) -> None:
|
|
106
|
+
self._db.execute(
|
|
107
|
+
"DELETE FROM clob_credentials WHERE wallet_address = ? COLLATE NOCASE",
|
|
108
|
+
(wallet_address,),
|
|
109
|
+
)
|
|
110
|
+
self._db.commit()
|
|
111
|
+
|
|
112
|
+
def get_all_credentials(self) -> list[StoredCredentials]:
|
|
113
|
+
rows = self._db.execute("SELECT * FROM clob_credentials").fetchall()
|
|
114
|
+
return [
|
|
115
|
+
StoredCredentials(
|
|
116
|
+
wallet_address=r[0], funder_address=r[1], api_key=r[2],
|
|
117
|
+
api_secret=r[3], api_passphrase=r[4], signature_type=SignatureType(r[5]),
|
|
118
|
+
safe_deployed=bool(r[6]), approvals_set=bool(r[7]),
|
|
119
|
+
created_at=r[8], updated_at=r[9],
|
|
120
|
+
)
|
|
121
|
+
for r in rows
|
|
122
|
+
]
|
|
123
|
+
|
|
124
|
+
def get_market_meta(self, token_id: str) -> MarketMeta | None:
|
|
125
|
+
row = self._db.execute(
|
|
126
|
+
"SELECT * FROM market_meta WHERE token_id = ?", (token_id,)
|
|
127
|
+
).fetchone()
|
|
128
|
+
if not row:
|
|
129
|
+
return None
|
|
130
|
+
meta = MarketMeta(
|
|
131
|
+
token_id=row[0], tick_size=row[1], fee_rate_bps=row[2],
|
|
132
|
+
neg_risk=bool(row[3]), fetched_at=row[4],
|
|
133
|
+
)
|
|
134
|
+
if time.time() - meta.fetched_at > META_TTL_SECONDS:
|
|
135
|
+
return None
|
|
136
|
+
return meta
|
|
137
|
+
|
|
138
|
+
def upsert_market_meta(self, meta: MarketMeta) -> None:
|
|
139
|
+
self._db.execute(
|
|
140
|
+
"""INSERT OR REPLACE INTO market_meta
|
|
141
|
+
(token_id, tick_size, fee_rate_bps, neg_risk, fetched_at)
|
|
142
|
+
VALUES (?, ?, ?, ?, ?)""",
|
|
143
|
+
(meta.token_id, meta.tick_size, meta.fee_rate_bps, int(meta.neg_risk), meta.fetched_at),
|
|
144
|
+
)
|
|
145
|
+
self._db.commit()
|
|
146
|
+
|
|
147
|
+
def insert_order(self, order: dict[str, Any]) -> int:
|
|
148
|
+
cur = self._db.execute(
|
|
149
|
+
"""INSERT INTO order_history
|
|
150
|
+
(wallet_address, order_id, token_id, side, price, size, order_type,
|
|
151
|
+
status, error_msg, response_json, created_at)
|
|
152
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
|
153
|
+
(
|
|
154
|
+
order["wallet_address"], order.get("order_id"), order["token_id"],
|
|
155
|
+
order["side"], order["price"], order["size"], order.get("order_type", "GTC"),
|
|
156
|
+
order.get("status", "submitting"), order.get("error_msg"),
|
|
157
|
+
order.get("response_json"), order.get("created_at", time.time()),
|
|
158
|
+
),
|
|
159
|
+
)
|
|
160
|
+
self._db.commit()
|
|
161
|
+
return cur.lastrowid # type: ignore
|
|
162
|
+
|
|
163
|
+
def update_order_status(
|
|
164
|
+
self, row_id: int, status: str, order_id: str | None = None,
|
|
165
|
+
error_msg: str | None = None, response_json: str | None = None,
|
|
166
|
+
) -> None:
|
|
167
|
+
self._db.execute(
|
|
168
|
+
"""UPDATE order_history
|
|
169
|
+
SET status = ?, order_id = COALESCE(?, order_id),
|
|
170
|
+
error_msg = COALESCE(?, error_msg), response_json = COALESCE(?, response_json)
|
|
171
|
+
WHERE id = ?""",
|
|
172
|
+
(status, order_id, error_msg, response_json, row_id),
|
|
173
|
+
)
|
|
174
|
+
self._db.commit()
|
|
175
|
+
|
|
176
|
+
def get_order_history(self, wallet_address: str, params: HistoryParams | None = None) -> list[OrderHistoryRow]:
|
|
177
|
+
sql = "SELECT * FROM order_history WHERE wallet_address = ? COLLATE NOCASE"
|
|
178
|
+
args: list[Any] = [wallet_address]
|
|
179
|
+
|
|
180
|
+
if params:
|
|
181
|
+
if params.token_id:
|
|
182
|
+
sql += " AND token_id = ?"
|
|
183
|
+
args.append(params.token_id)
|
|
184
|
+
if params.side:
|
|
185
|
+
sql += " AND side = ?"
|
|
186
|
+
args.append(params.side)
|
|
187
|
+
|
|
188
|
+
sql += " ORDER BY created_at DESC"
|
|
189
|
+
|
|
190
|
+
if params and params.limit:
|
|
191
|
+
sql += " LIMIT ?"
|
|
192
|
+
args.append(params.limit)
|
|
193
|
+
if params and params.offset:
|
|
194
|
+
sql += " OFFSET ?"
|
|
195
|
+
args.append(params.offset)
|
|
196
|
+
|
|
197
|
+
rows = self._db.execute(sql, args).fetchall()
|
|
198
|
+
return [
|
|
199
|
+
OrderHistoryRow(
|
|
200
|
+
id=r[0], wallet_address=r[1], order_id=r[2], token_id=r[3],
|
|
201
|
+
side=r[4], price=r[5], size=r[6], order_type=r[7],
|
|
202
|
+
status=r[8], error_msg=r[9], response_json=r[10], created_at=r[11],
|
|
203
|
+
)
|
|
204
|
+
for r in rows
|
|
205
|
+
]
|
|
206
|
+
|
|
207
|
+
def close(self) -> None:
|
|
208
|
+
self._db.close()
|