dominusnode 2.0.0__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.
- dominusnode/__init__.py +45 -0
- dominusnode/_ops.py +122 -0
- dominusnode/client.py +192 -0
- dominusnode/constants.py +24 -0
- dominusnode/errors.py +153 -0
- dominusnode/http_client.py +354 -0
- dominusnode/messages.py +79 -0
- dominusnode/pow.py +33 -0
- dominusnode/proxy_fetch.py +341 -0
- dominusnode/py.typed +0 -0
- dominusnode/resources.py +791 -0
- dominusnode/secret.py +60 -0
- dominusnode/signer.py +56 -0
- dominusnode/ssrf.py +176 -0
- dominusnode/token_manager.py +150 -0
- dominusnode/types.py +99 -0
- dominusnode/validation.py +117 -0
- dominusnode-2.0.0.dist-info/METADATA +177 -0
- dominusnode-2.0.0.dist-info/RECORD +22 -0
- dominusnode-2.0.0.dist-info/WHEEL +5 -0
- dominusnode-2.0.0.dist-info/licenses/LICENSE +21 -0
- dominusnode-2.0.0.dist-info/top_level.txt +1 -0
dominusnode/resources.py
ADDED
|
@@ -0,0 +1,791 @@
|
|
|
1
|
+
"""The 15 resources (71 methods), each written once as sans-I/O generators.
|
|
2
|
+
|
|
3
|
+
``make_variants`` turns each core class into a sync and an async resource class.
|
|
4
|
+
Results are the server's JSON as dicts (camelCase keys, as on the wire); see types.py.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
from typing import Any, Dict, List, Mapping, Optional
|
|
11
|
+
|
|
12
|
+
from ._ops import Call, Gen, make_variants
|
|
13
|
+
from .constants import DEFAULT_HTTP_PROXY_PORT, DEFAULT_PROXY_HOST, DEFAULT_SOCKS5_PROXY_PORT, DEFAULT_TLS_PROXY_PORT
|
|
14
|
+
from .errors import AuthenticationError, ProtocolError, invalid_arg
|
|
15
|
+
from .http_client import Request
|
|
16
|
+
from .messages import (
|
|
17
|
+
assert_same_message,
|
|
18
|
+
build_attestation_message,
|
|
19
|
+
build_payout_wallet_message,
|
|
20
|
+
build_withdraw_message,
|
|
21
|
+
extract_user_id,
|
|
22
|
+
)
|
|
23
|
+
from .pow import solve_pow
|
|
24
|
+
from .proxy_fetch import GatewayTls, ProxyFetchResponse, build_proxy_username, execute_fetch, prepare_fetch
|
|
25
|
+
from .secret import Secret
|
|
26
|
+
from .signer import assert_signer, sign
|
|
27
|
+
from .ssrf import LookupAll
|
|
28
|
+
from .validation import (
|
|
29
|
+
HEX64_RE,
|
|
30
|
+
UUID_RE,
|
|
31
|
+
new_idempotency_key,
|
|
32
|
+
qs,
|
|
33
|
+
seg,
|
|
34
|
+
to_musd,
|
|
35
|
+
validate_address,
|
|
36
|
+
validate_api_key,
|
|
37
|
+
validate_cents,
|
|
38
|
+
validate_idempotency_key,
|
|
39
|
+
validate_int_range,
|
|
40
|
+
validate_non_empty,
|
|
41
|
+
validate_optional_int,
|
|
42
|
+
validate_signature,
|
|
43
|
+
validate_uuid,
|
|
44
|
+
)
|
|
45
|
+
from urllib.parse import quote
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class _Ctx:
|
|
49
|
+
"""Shared client state handed to resources (internal)."""
|
|
50
|
+
|
|
51
|
+
def __init__(self, tokens: Any) -> None:
|
|
52
|
+
self.tokens = tokens
|
|
53
|
+
self.api_key: Optional[str] = None
|
|
54
|
+
|
|
55
|
+
def __repr__(self) -> str:
|
|
56
|
+
return "_Ctx([REDACTED])"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class _Base:
|
|
60
|
+
def __init__(self, http: Any, ctx: _Ctx) -> None:
|
|
61
|
+
self._http = http
|
|
62
|
+
self._ctx = ctx
|
|
63
|
+
|
|
64
|
+
def __repr__(self) -> str:
|
|
65
|
+
return f"{type(self).__name__}()"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _get(path: str, auth: str = "jwt") -> Request:
|
|
69
|
+
return Request("GET", path, auth=auth)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _post(path: str, body: Any = None, auth: str = "jwt", headers: Optional[Dict[str, str]] = None) -> Request:
|
|
73
|
+
return Request("POST", path, {} if body is None else body, auth, dict(headers or {}))
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _put(path: str, body: Any, auth: str = "jwt") -> Request:
|
|
77
|
+
return Request("PUT", path, body, auth)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _patch(path: str, body: Any, auth: str = "jwt") -> Request:
|
|
81
|
+
return Request("PATCH", path, body, auth)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _delete(path: str, auth: str = "jwt") -> Request:
|
|
85
|
+
return Request("DELETE", path, auth=auth)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _page(limit: Any, offset: Any) -> str:
|
|
89
|
+
return qs({"limit": validate_optional_int(limit, "limit"), "offset": validate_optional_int(offset, "offset")})
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _id(v: Any, name: str) -> str:
|
|
93
|
+
return seg(validate_non_empty(v, name))
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# ---------------------------------------------------------------- 3.1 auth
|
|
97
|
+
class _AuthResource(_Base):
|
|
98
|
+
"""Token plumbing. Human email/password, registration and MFA flows are not in the SDK."""
|
|
99
|
+
|
|
100
|
+
def verify_key(self, api_key: str) -> Gen:
|
|
101
|
+
"""POST /api/auth/verify-key [none] -> {valid, token, refreshToken, userId, email}."""
|
|
102
|
+
return (yield _post("/api/auth/verify-key", {"apiKey": validate_api_key(api_key)}, "none"))
|
|
103
|
+
|
|
104
|
+
def refresh(self, refresh_token: str) -> Gen:
|
|
105
|
+
"""POST /api/auth/refresh [none] -> {token, refreshToken?}."""
|
|
106
|
+
if not isinstance(refresh_token, str) or not refresh_token:
|
|
107
|
+
raise invalid_arg("refresh_token is required")
|
|
108
|
+
return (yield _post("/api/auth/refresh", {"refreshToken": refresh_token}, "none"))
|
|
109
|
+
|
|
110
|
+
def logout(self) -> Gen:
|
|
111
|
+
"""POST /api/auth/logout [jwt]."""
|
|
112
|
+
yield _post("/api/auth/logout", {})
|
|
113
|
+
return None
|
|
114
|
+
|
|
115
|
+
def me(self) -> Gen:
|
|
116
|
+
"""GET /api/auth/me [jwt] -> {user}."""
|
|
117
|
+
return (yield _get("/api/auth/me"))
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# ---------------------------------------------------------------- 3.2 walletAuth
|
|
121
|
+
class _WalletAuthResource(_Base):
|
|
122
|
+
"""EIP-191 wallet sign-in. Wallet-identity accounts are agent principals for /v1 routes."""
|
|
123
|
+
|
|
124
|
+
def challenge(self, address: str) -> Gen:
|
|
125
|
+
return (yield _post("/api/auth/wallet/challenge", {"address": validate_address(address)}, "none"))
|
|
126
|
+
|
|
127
|
+
def verify(self, address: str, signature: str) -> Gen:
|
|
128
|
+
"""Stores the JWT pair unless the response says mfaRequired (then nothing is stored)."""
|
|
129
|
+
body = {"address": validate_address(address), "signature": validate_signature(signature)}
|
|
130
|
+
result = yield _post("/api/auth/wallet/verify", body, "none")
|
|
131
|
+
if isinstance(result, dict) and result.get("mfaRequired") is True:
|
|
132
|
+
return {"mfaRequired": True, "mfaChallengeToken": result.get("mfaChallengeToken")}
|
|
133
|
+
if isinstance(result, dict) and isinstance(result.get("token"), str) and isinstance(result.get("refreshToken"), str):
|
|
134
|
+
self._ctx.tokens.set_tokens(result["token"], result["refreshToken"])
|
|
135
|
+
return result
|
|
136
|
+
|
|
137
|
+
def sign_in(self, signer: Any) -> Gen:
|
|
138
|
+
"""challenge -> signer.sign_message -> verify. Refuses if the challenge does not name the signer."""
|
|
139
|
+
assert_signer(signer)
|
|
140
|
+
ch = yield from _WalletAuthResource.challenge(self, signer.address)
|
|
141
|
+
msg = ch.get("message") if isinstance(ch, dict) else None
|
|
142
|
+
if not isinstance(msg, str) or signer.address.lower() not in msg.lower():
|
|
143
|
+
raise ProtocolError("wallet challenge does not name the signer's address; refusing to sign")
|
|
144
|
+
signature = yield from sign(signer, msg)
|
|
145
|
+
return (yield from _WalletAuthResource.verify(self, signer.address, signature))
|
|
146
|
+
|
|
147
|
+
def link(self, address: str, signature: str) -> Gen:
|
|
148
|
+
body = {"address": validate_address(address), "signature": validate_signature(signature)}
|
|
149
|
+
return (yield _post("/api/auth/wallet/link", body))
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
# ---------------------------------------------------------------- 3.3 keys
|
|
153
|
+
class _KeysResource(_Base):
|
|
154
|
+
def create(self, label: str = "Default") -> Gen:
|
|
155
|
+
"""POST /api/keys -> {id, prefix, label, created_at, key: Secret}."""
|
|
156
|
+
validate_non_empty(label, "label", 100)
|
|
157
|
+
r = yield _post("/api/keys", {"label": label})
|
|
158
|
+
return {**r, "key": Secret(str(r.get("key", "")))}
|
|
159
|
+
|
|
160
|
+
def list(self) -> Gen:
|
|
161
|
+
return (yield _get("/api/keys"))
|
|
162
|
+
|
|
163
|
+
def revoke(self, id: str) -> Gen: # noqa: A002
|
|
164
|
+
yield _delete(f"/api/keys/{_id(id, 'id')}")
|
|
165
|
+
return None
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
# ---------------------------------------------------------------- 3.4 wallet
|
|
169
|
+
CRYPTO_CURRENCIES = ("btc", "eth", "ltc", "xmr", "zec", "usdc", "sol", "usdt", "dai", "bnb", "link")
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class _WalletResource(_Base):
|
|
173
|
+
"""Requester (prepaid) wallet in cents. Card/PayPal top-ups are not available to agents."""
|
|
174
|
+
|
|
175
|
+
def get_balance(self) -> Gen:
|
|
176
|
+
return (yield _get("/api/wallet"))
|
|
177
|
+
|
|
178
|
+
def get_transactions(self, limit: Optional[int] = None, offset: Optional[int] = None) -> Gen:
|
|
179
|
+
return (yield _get(f"/api/wallet/transactions{_page(limit, offset)}"))
|
|
180
|
+
|
|
181
|
+
def get_forecast(self) -> Gen:
|
|
182
|
+
return (yield _get("/api/wallet/forecast"))
|
|
183
|
+
|
|
184
|
+
def top_up_crypto(self, amount_cents: int, currency: str) -> Gen:
|
|
185
|
+
"""$10..$1,000 (1000..100000 cents)."""
|
|
186
|
+
validate_cents(amount_cents)
|
|
187
|
+
if amount_cents < 1000 or amount_cents > 100_000:
|
|
188
|
+
raise invalid_arg("crypto top-up must be between 1000 and 100000 cents ($10-$1,000)")
|
|
189
|
+
c = str(currency).lower()
|
|
190
|
+
if c not in CRYPTO_CURRENCIES:
|
|
191
|
+
raise invalid_arg(f"currency must be one of {', '.join(CRYPTO_CURRENCIES)}")
|
|
192
|
+
amount_usd: Any = amount_cents / 100
|
|
193
|
+
if amount_cents % 100 == 0:
|
|
194
|
+
amount_usd = amount_cents // 100 # serialise as 10, not 10.0 (matches JS)
|
|
195
|
+
return (yield _post("/api/wallet/topup/crypto", {"amountUsd": amount_usd, "currency": c}))
|
|
196
|
+
|
|
197
|
+
def get_crypto_status(self, invoice_id: str) -> Gen:
|
|
198
|
+
return (yield _get(f"/api/wallet/crypto/status/{seg(validate_uuid(invoice_id, 'invoice_id'))}"))
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
# ---------------------------------------------------------------- 3.5 agenticWallets
|
|
202
|
+
class _AgenticWalletResource(_Base):
|
|
203
|
+
def create(
|
|
204
|
+
self,
|
|
205
|
+
label: str,
|
|
206
|
+
spending_limit_cents: int = 10000,
|
|
207
|
+
daily_limit_cents: Optional[int] = None,
|
|
208
|
+
allowed_domains: Optional[List[str]] = None,
|
|
209
|
+
) -> Gen:
|
|
210
|
+
validate_cents(spending_limit_cents, "spending_limit_cents", True)
|
|
211
|
+
if daily_limit_cents is not None:
|
|
212
|
+
validate_cents(daily_limit_cents, "daily_limit_cents", True)
|
|
213
|
+
body: Dict[str, Any] = {"label": label, "spendingLimitCents": spending_limit_cents}
|
|
214
|
+
if daily_limit_cents is not None:
|
|
215
|
+
body["dailyLimitCents"] = daily_limit_cents
|
|
216
|
+
if allowed_domains is not None:
|
|
217
|
+
body["allowedDomains"] = list(allowed_domains)
|
|
218
|
+
return (yield _post("/api/agent-wallet", body))
|
|
219
|
+
|
|
220
|
+
def list(self) -> Gen:
|
|
221
|
+
return (yield _get("/api/agent-wallet"))
|
|
222
|
+
|
|
223
|
+
def get(self, wallet_id: str) -> Gen:
|
|
224
|
+
return (yield _get(f"/api/agent-wallet/{_id(wallet_id, 'wallet_id')}"))
|
|
225
|
+
|
|
226
|
+
def fund(self, wallet_id: str, amount_cents: int) -> Gen:
|
|
227
|
+
validate_cents(amount_cents)
|
|
228
|
+
return (yield _post(f"/api/agent-wallet/{_id(wallet_id, 'wallet_id')}/fund", {"amountCents": amount_cents}))
|
|
229
|
+
|
|
230
|
+
def update_wallet_policy(
|
|
231
|
+
self, wallet_id: str, daily_limit_cents: Any = ..., allowed_domains: Any = ...
|
|
232
|
+
) -> Gen:
|
|
233
|
+
"""PATCH /{id}/policy. Pass None explicitly to clear a limit/list; omit to leave it unchanged."""
|
|
234
|
+
body: Dict[str, Any] = {}
|
|
235
|
+
if daily_limit_cents is not ...:
|
|
236
|
+
if daily_limit_cents is not None:
|
|
237
|
+
validate_cents(daily_limit_cents, "daily_limit_cents", True)
|
|
238
|
+
body["dailyLimitCents"] = daily_limit_cents
|
|
239
|
+
if allowed_domains is not ...:
|
|
240
|
+
body["allowedDomains"] = None if allowed_domains is None else list(allowed_domains)
|
|
241
|
+
return (yield _patch(f"/api/agent-wallet/{_id(wallet_id, 'wallet_id')}/policy", body))
|
|
242
|
+
|
|
243
|
+
def transactions(self, wallet_id: str, limit: Optional[int] = None, offset: Optional[int] = None) -> Gen:
|
|
244
|
+
return (yield _get(f"/api/agent-wallet/{_id(wallet_id, 'wallet_id')}/transactions{_page(limit, offset)}"))
|
|
245
|
+
|
|
246
|
+
def freeze(self, wallet_id: str) -> Gen:
|
|
247
|
+
return (yield _post(f"/api/agent-wallet/{_id(wallet_id, 'wallet_id')}/freeze", {}))
|
|
248
|
+
|
|
249
|
+
def unfreeze(self, wallet_id: str) -> Gen:
|
|
250
|
+
return (yield _post(f"/api/agent-wallet/{_id(wallet_id, 'wallet_id')}/unfreeze", {}))
|
|
251
|
+
|
|
252
|
+
def delete(self, wallet_id: str) -> Gen:
|
|
253
|
+
return (yield _delete(f"/api/agent-wallet/{_id(wallet_id, 'wallet_id')}"))
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
# ---------------------------------------------------------------- 3.6 teams
|
|
257
|
+
class _TeamsResource(_Base):
|
|
258
|
+
def _t(self, team_id: str) -> str:
|
|
259
|
+
return f"/api/teams/{_id(team_id, 'team_id')}"
|
|
260
|
+
|
|
261
|
+
def create(self, name: str, max_members: Optional[int] = None) -> Gen:
|
|
262
|
+
body: Dict[str, Any] = {"name": name}
|
|
263
|
+
if max_members is not None:
|
|
264
|
+
body["maxMembers"] = validate_optional_int(max_members, "max_members")
|
|
265
|
+
return (yield _post("/api/teams", body))
|
|
266
|
+
|
|
267
|
+
def list(self) -> Gen:
|
|
268
|
+
return (yield _get("/api/teams"))
|
|
269
|
+
|
|
270
|
+
def get(self, team_id: str) -> Gen:
|
|
271
|
+
return (yield _get(self._t(team_id)))
|
|
272
|
+
|
|
273
|
+
def update(self, team_id: str, name: Optional[str] = None, max_members: Optional[int] = None) -> Gen:
|
|
274
|
+
body: Dict[str, Any] = {}
|
|
275
|
+
if name is not None:
|
|
276
|
+
body["name"] = name
|
|
277
|
+
if max_members is not None:
|
|
278
|
+
body["maxMembers"] = validate_optional_int(max_members, "max_members")
|
|
279
|
+
return (yield _patch(self._t(team_id), body))
|
|
280
|
+
|
|
281
|
+
def delete(self, team_id: str) -> Gen:
|
|
282
|
+
return (yield _delete(self._t(team_id)))
|
|
283
|
+
|
|
284
|
+
def fund_wallet(self, team_id: str, amount_cents: int) -> Gen:
|
|
285
|
+
validate_cents(amount_cents)
|
|
286
|
+
return (yield _post(f"{self._t(team_id)}/wallet/fund", {"amountCents": amount_cents}))
|
|
287
|
+
|
|
288
|
+
def get_transactions(self, team_id: str, limit: Optional[int] = None, offset: Optional[int] = None) -> Gen:
|
|
289
|
+
return (yield _get(f"{self._t(team_id)}/wallet/transactions{_page(limit, offset)}"))
|
|
290
|
+
|
|
291
|
+
def list_members(self, team_id: str) -> Gen:
|
|
292
|
+
return (yield _get(f"{self._t(team_id)}/members"))
|
|
293
|
+
|
|
294
|
+
def add_member(self, team_id: str, email: str, role: Optional[str] = None) -> Gen:
|
|
295
|
+
body: Dict[str, Any] = {"email": email}
|
|
296
|
+
if role is not None:
|
|
297
|
+
body["role"] = role
|
|
298
|
+
return (yield _post(f"{self._t(team_id)}/members", body))
|
|
299
|
+
|
|
300
|
+
def update_member_role(self, team_id: str, user_id: str, role: str) -> Gen:
|
|
301
|
+
return (yield _patch(f"{self._t(team_id)}/members/{_id(user_id, 'user_id')}", {"role": role}))
|
|
302
|
+
|
|
303
|
+
def remove_member(self, team_id: str, user_id: str) -> Gen:
|
|
304
|
+
yield _delete(f"{self._t(team_id)}/members/{_id(user_id, 'user_id')}")
|
|
305
|
+
return None
|
|
306
|
+
|
|
307
|
+
def create_invite(self, team_id: str, email: str, role: Optional[str] = None) -> Gen:
|
|
308
|
+
body: Dict[str, Any] = {"email": email}
|
|
309
|
+
if role is not None:
|
|
310
|
+
body["role"] = role
|
|
311
|
+
return (yield _post(f"{self._t(team_id)}/invites", body))
|
|
312
|
+
|
|
313
|
+
def list_invites(self, team_id: str) -> Gen:
|
|
314
|
+
return (yield _get(f"{self._t(team_id)}/invites"))
|
|
315
|
+
|
|
316
|
+
def cancel_invite(self, team_id: str, invite_id: str) -> Gen:
|
|
317
|
+
yield _delete(f"{self._t(team_id)}/invites/{_id(invite_id, 'invite_id')}")
|
|
318
|
+
return None
|
|
319
|
+
|
|
320
|
+
def accept_invite(self, token: str) -> Gen:
|
|
321
|
+
return (yield _post(f"/api/teams/invites/{_id(token, 'token')}/accept", {}))
|
|
322
|
+
|
|
323
|
+
def create_key(self, team_id: str, label: str) -> Gen:
|
|
324
|
+
"""POST /{teamId}/keys -> {id, keyPrefix, label, key: Secret}. Team keys cannot call /v1 routes."""
|
|
325
|
+
validate_non_empty(label, "label", 100)
|
|
326
|
+
r = yield _post(f"{self._t(team_id)}/keys", {"label": label})
|
|
327
|
+
return {**r, "key": Secret(str(r.get("key", "")))}
|
|
328
|
+
|
|
329
|
+
def list_keys(self, team_id: str) -> Gen:
|
|
330
|
+
return (yield _get(f"{self._t(team_id)}/keys"))
|
|
331
|
+
|
|
332
|
+
def revoke_key(self, team_id: str, key_id: str) -> Gen:
|
|
333
|
+
yield _delete(f"{self._t(team_id)}/keys/{_id(key_id, 'key_id')}")
|
|
334
|
+
return None
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
# ---------------------------------------------------------------- 3.7 usage
|
|
338
|
+
class _UsageResource(_Base):
|
|
339
|
+
"""Date range: ``from_`` -> query ``since``, ``to`` -> ``until`` (ISO 8601)."""
|
|
340
|
+
|
|
341
|
+
def get(
|
|
342
|
+
self, from_: Optional[str] = None, to: Optional[str] = None, limit: Optional[int] = None, offset: Optional[int] = None
|
|
343
|
+
) -> Gen:
|
|
344
|
+
q = qs({"since": from_, "until": to, "limit": validate_optional_int(limit, "limit"),
|
|
345
|
+
"offset": validate_optional_int(offset, "offset")})
|
|
346
|
+
return (yield _get(f"/api/usage{q}"))
|
|
347
|
+
|
|
348
|
+
def get_daily(self, from_: Optional[str] = None, to: Optional[str] = None) -> Gen:
|
|
349
|
+
return (yield _get(f"/api/usage/daily{qs({'since': from_, 'until': to})}"))
|
|
350
|
+
|
|
351
|
+
def get_top_hosts(self, from_: Optional[str] = None, to: Optional[str] = None, limit: Optional[int] = None) -> Gen:
|
|
352
|
+
q = qs({"since": from_, "until": to, "limit": validate_optional_int(limit, "limit")})
|
|
353
|
+
return (yield _get(f"/api/usage/top-hosts{q}"))
|
|
354
|
+
|
|
355
|
+
def export_csv(self, from_: Optional[str] = None, to: Optional[str] = None) -> Gen:
|
|
356
|
+
"""Raw CSV text."""
|
|
357
|
+
return (yield Request("GET", f"/api/usage/export{qs({'since': from_, 'until': to})}", raw=True))
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
# ---------------------------------------------------------------- 3.8 sessions
|
|
361
|
+
REPORT_REASONS = ("blocked", "captcha", "rate_limited", "slow", "bad_content", "other")
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
class _SessionsResource(_Base):
|
|
365
|
+
def get_active(self) -> Gen:
|
|
366
|
+
"""ActiveSession = {id, startedAt, status} ONLY; IPs and unknown fields are dropped."""
|
|
367
|
+
r = yield _get("/api/sessions/active")
|
|
368
|
+
raw = r.get("sessions") if isinstance(r, dict) else None
|
|
369
|
+
sessions = [
|
|
370
|
+
{"id": str(s.get("id")), "startedAt": str(s.get("startedAt")), "status": str(s.get("status"))}
|
|
371
|
+
for s in (raw if isinstance(raw, list) else [])
|
|
372
|
+
if isinstance(s, dict)
|
|
373
|
+
]
|
|
374
|
+
return {"sessions": sessions}
|
|
375
|
+
|
|
376
|
+
def report(self, session_id: str, reason: str) -> Gen:
|
|
377
|
+
"""POST /v1/sessions/{id}/report [agent] -> {reportId, reportsToday, limitPerDay}."""
|
|
378
|
+
validate_uuid(session_id, "session_id")
|
|
379
|
+
if reason not in REPORT_REASONS:
|
|
380
|
+
raise invalid_arg(f"reason must be one of {', '.join(REPORT_REASONS)}")
|
|
381
|
+
return (yield _post(f"/v1/sessions/{seg(session_id)}/report", {"reason": reason}, "agent"))
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
# ---------------------------------------------------------------- 3.9 x402
|
|
385
|
+
class _X402Resource(_Base):
|
|
386
|
+
def get_info(self) -> Gen:
|
|
387
|
+
"""GET /api/x402/info [none]. Prices are server-reported; the SDK hardcodes none."""
|
|
388
|
+
return (yield _get("/api/x402/info", "none"))
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
# ---------------------------------------------------------------- 3.10 network
|
|
392
|
+
class _NetworkResource(_Base):
|
|
393
|
+
def policy(self) -> Gen:
|
|
394
|
+
return (yield _get("/v1/policy", "none"))
|
|
395
|
+
|
|
396
|
+
def pricing(self) -> Gen:
|
|
397
|
+
"""Live per-GB price by IP class (µUSD strings)."""
|
|
398
|
+
return (yield _get("/v1/pricing", "none"))
|
|
399
|
+
|
|
400
|
+
def stats(self) -> Gen:
|
|
401
|
+
return (yield _get("/v1/network/stats", "none"))
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
# ---------------------------------------------------------------- 3.11 nodes
|
|
405
|
+
NODE_KEY_RE = re.compile(r"^dn_node_[0-9a-f]{64}\Z")
|
|
406
|
+
MAX_ATTESTATION_TEXT = 64 * 1024
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def _norm_pubkey(v: Any) -> Optional[str]:
|
|
410
|
+
if v is None:
|
|
411
|
+
return None
|
|
412
|
+
k = v.lower() if isinstance(v, str) else ""
|
|
413
|
+
if not HEX64_RE.match(k):
|
|
414
|
+
raise invalid_arg("node_pubkey must be 64 hex characters (32-byte ed25519 public key)")
|
|
415
|
+
return k
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def _is_int(v: Any) -> bool:
|
|
419
|
+
return isinstance(v, int) and not isinstance(v, bool)
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def _check_challenge(ch: Any) -> None:
|
|
423
|
+
if not isinstance(ch, dict):
|
|
424
|
+
raise ProtocolError("invalid node challenge")
|
|
425
|
+
if not isinstance(ch.get("nonce"), str) or not HEX64_RE.match(ch["nonce"]):
|
|
426
|
+
raise ProtocolError("invalid challenge nonce")
|
|
427
|
+
text = ch.get("attestationText")
|
|
428
|
+
if not isinstance(text, str) or not text or len(text) > MAX_ATTESTATION_TEXT:
|
|
429
|
+
raise ProtocolError("invalid attestation text")
|
|
430
|
+
ver = ch.get("attestationVersion")
|
|
431
|
+
if not _is_int(ver) or ver < 1 or ver > 100_000:
|
|
432
|
+
raise ProtocolError("invalid attestation version")
|
|
433
|
+
p = ch.get("powChallenge")
|
|
434
|
+
if (
|
|
435
|
+
not isinstance(p, dict)
|
|
436
|
+
or not isinstance(p.get("prefix"), str)
|
|
437
|
+
or not isinstance(p.get("difficulty"), (int, float))
|
|
438
|
+
or isinstance(p.get("difficulty"), bool)
|
|
439
|
+
or not isinstance(p.get("challengeId"), str)
|
|
440
|
+
or not UUID_RE.match(p["challengeId"])
|
|
441
|
+
):
|
|
442
|
+
raise ProtocolError("invalid PoW challenge")
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
class _NodesResource(_Base):
|
|
446
|
+
"""Relay nodes you own. All routes are /v1 agent routes."""
|
|
447
|
+
|
|
448
|
+
def register_challenge(self, node_pubkey: Optional[str] = None) -> Gen:
|
|
449
|
+
pk = _norm_pubkey(node_pubkey)
|
|
450
|
+
return (yield _post("/v1/nodes/challenge", {"nodePubkey": pk} if pk else {}, "agent"))
|
|
451
|
+
|
|
452
|
+
def register(
|
|
453
|
+
self,
|
|
454
|
+
signer: Any,
|
|
455
|
+
node_pubkey: Optional[str] = None,
|
|
456
|
+
challenge: Optional[Dict[str, Any]] = None,
|
|
457
|
+
max_streams: Optional[int] = None,
|
|
458
|
+
max_gb_day: Optional[int] = None,
|
|
459
|
+
max_pow_iterations: int = 200_000_000,
|
|
460
|
+
) -> Gen:
|
|
461
|
+
"""challenge -> rebuild attestation (refuse on mismatch) -> PoW -> sign -> POST /v1/nodes.
|
|
462
|
+
|
|
463
|
+
Returns {nodeId, status, attestationVersion, nodeKey: Secret}; the node key is shown once.
|
|
464
|
+
"""
|
|
465
|
+
assert_signer(signer)
|
|
466
|
+
pk = _norm_pubkey(node_pubkey)
|
|
467
|
+
if max_streams is not None:
|
|
468
|
+
validate_int_range(max_streams, "max_streams", 1, 1024)
|
|
469
|
+
if max_gb_day is not None:
|
|
470
|
+
validate_int_range(max_gb_day, "max_gb_day", 1, 10_000)
|
|
471
|
+
validate_int_range(max_pow_iterations, "max_pow_iterations", 1, 10**12)
|
|
472
|
+
ch = challenge if challenge is not None else (yield from _NodesResource.register_challenge(self, pk))
|
|
473
|
+
_check_challenge(ch)
|
|
474
|
+
message = build_attestation_message(signer.address, ch["nonce"], ch["attestationVersion"], ch["attestationText"], pk)
|
|
475
|
+
assert_same_message(ch.get("message"), message, "node attestation")
|
|
476
|
+
pow_ch = ch["powChallenge"]
|
|
477
|
+
diff = pow_ch["difficulty"]
|
|
478
|
+
if isinstance(diff, float):
|
|
479
|
+
if diff != diff or not diff.is_integer():
|
|
480
|
+
raise ProtocolError("invalid PoW difficulty")
|
|
481
|
+
diff = int(diff)
|
|
482
|
+
pow_nonce = yield Call(solve_pow, (pow_ch["prefix"], diff, max_pow_iterations))
|
|
483
|
+
wallet_signature = yield from sign(signer, message)
|
|
484
|
+
body: Dict[str, Any] = {
|
|
485
|
+
"nonce": ch["nonce"],
|
|
486
|
+
"walletSignature": wallet_signature,
|
|
487
|
+
"attestationVersion": ch["attestationVersion"],
|
|
488
|
+
"pow": {"challengeId": pow_ch["challengeId"], "nonce": pow_nonce},
|
|
489
|
+
}
|
|
490
|
+
if pk:
|
|
491
|
+
body["nodePubkey"] = pk
|
|
492
|
+
if max_streams is not None:
|
|
493
|
+
body["maxStreams"] = max_streams
|
|
494
|
+
if max_gb_day is not None:
|
|
495
|
+
body["maxGbDay"] = max_gb_day
|
|
496
|
+
r = yield _post("/v1/nodes", body, "agent")
|
|
497
|
+
if not isinstance(r, dict) or not isinstance(r.get("nodeId"), str) or not UUID_RE.match(r["nodeId"]):
|
|
498
|
+
raise ProtocolError("invalid nodeId in response")
|
|
499
|
+
if not isinstance(r.get("nodeKey"), str) or not NODE_KEY_RE.match(r["nodeKey"]):
|
|
500
|
+
raise ProtocolError("invalid node key in response")
|
|
501
|
+
return {
|
|
502
|
+
"nodeId": r["nodeId"],
|
|
503
|
+
"status": str(r.get("status")),
|
|
504
|
+
"attestationVersion": r.get("attestationVersion"),
|
|
505
|
+
"nodeKey": Secret(r["nodeKey"]),
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
def list(self, limit: Optional[int] = None, offset: Optional[int] = None) -> Gen:
|
|
509
|
+
if limit is not None:
|
|
510
|
+
validate_int_range(limit, "limit", 1, 500)
|
|
511
|
+
if offset is not None:
|
|
512
|
+
validate_int_range(offset, "offset", 0, 1_000_000)
|
|
513
|
+
return (yield _get(f"/v1/nodes{qs({'limit': limit, 'offset': offset})}", "agent"))
|
|
514
|
+
|
|
515
|
+
def revoke(self, node_id: str) -> Gen:
|
|
516
|
+
"""Irreversible."""
|
|
517
|
+
return (yield _delete(f"/v1/nodes/{seg(validate_uuid(node_id, 'node_id'))}", "agent"))
|
|
518
|
+
|
|
519
|
+
def update_caps(
|
|
520
|
+
self,
|
|
521
|
+
node_id: str,
|
|
522
|
+
max_streams: Optional[int] = None,
|
|
523
|
+
gb_per_day: Optional[int] = None,
|
|
524
|
+
req_per_sec: Optional[int] = None,
|
|
525
|
+
) -> Gen:
|
|
526
|
+
"""Lower caps only (server: CAP_ABOVE_DEFAULT with details['max'])."""
|
|
527
|
+
validate_uuid(node_id, "node_id")
|
|
528
|
+
body: Dict[str, int] = {}
|
|
529
|
+
for wire, name, v in (("maxStreams", "max_streams", max_streams), ("gbPerDay", "gb_per_day", gb_per_day),
|
|
530
|
+
("reqPerSec", "req_per_sec", req_per_sec)):
|
|
531
|
+
if v is not None:
|
|
532
|
+
body[wire] = validate_int_range(v, name, 1, 1_000_000)
|
|
533
|
+
if not body:
|
|
534
|
+
raise invalid_arg("update_caps needs at least one of max_streams, gb_per_day, req_per_sec")
|
|
535
|
+
return (yield _patch(f"/v1/nodes/{seg(node_id)}", body, "agent"))
|
|
536
|
+
|
|
537
|
+
def tier(self) -> Gen:
|
|
538
|
+
return (yield _get("/v1/node/tier", "agent"))
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
# ---------------------------------------------------------------- 3.12 credits
|
|
542
|
+
CREDIT_TX_TYPES = ("earn", "mature", "clawback", "convert", "withdraw", "withdraw_reversal")
|
|
543
|
+
MIN_CONVERT_MUSD = 10_000
|
|
544
|
+
MAX_CONVERT_MUSD = 10_000_000_000
|
|
545
|
+
MIN_WITHDRAW_MUSD = 1_000_000
|
|
546
|
+
MAX_WITHDRAW_MUSD = 100_000_000_000
|
|
547
|
+
_CURSOR_RE = re.compile(r"^[0-9]{1,20}\Z")
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
def _check_signed_challenge(ch: Any, signer: Any) -> Dict[str, Any]:
|
|
551
|
+
if not isinstance(ch, dict) or not isinstance(ch.get("nonce"), str) or not HEX64_RE.match(ch["nonce"]):
|
|
552
|
+
raise ProtocolError("invalid challenge nonce")
|
|
553
|
+
s = ch.get("signer")
|
|
554
|
+
if not isinstance(s, str) or s.lower() != signer.address.lower():
|
|
555
|
+
raise ProtocolError("the account's linked wallet is not the signer's address; refusing to sign")
|
|
556
|
+
return ch
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
class _CreditsResource(_Base):
|
|
560
|
+
"""Node-operator earnings in µUSD. /v1 agent routes."""
|
|
561
|
+
|
|
562
|
+
def balance(self) -> Gen:
|
|
563
|
+
return (yield _get("/v1/credits", "agent"))
|
|
564
|
+
|
|
565
|
+
def transactions(self, limit: Optional[int] = None, cursor: Optional[str] = None, type: Optional[str] = None) -> Gen: # noqa: A002
|
|
566
|
+
if limit is not None:
|
|
567
|
+
validate_int_range(limit, "limit", 1, 200)
|
|
568
|
+
if cursor is not None and (not isinstance(cursor, str) or not _CURSOR_RE.match(cursor)):
|
|
569
|
+
raise invalid_arg("cursor must be a nextCursor value")
|
|
570
|
+
if type is not None and type not in CREDIT_TX_TYPES:
|
|
571
|
+
raise invalid_arg(f"type must be one of {', '.join(CREDIT_TX_TYPES)}")
|
|
572
|
+
return (yield _get(f"/v1/credits/transactions{qs({'limit': limit, 'cursor': cursor, 'type': type})}", "agent"))
|
|
573
|
+
|
|
574
|
+
def convert(self, amount_musd: Any, idempotency_key: Optional[str] = None) -> Gen:
|
|
575
|
+
"""10,000 <= amount_musd <= 10,000,000,000. The key is echoed so callers can retry."""
|
|
576
|
+
amount = to_musd(amount_musd, MIN_CONVERT_MUSD, MAX_CONVERT_MUSD)
|
|
577
|
+
key = validate_idempotency_key(idempotency_key if idempotency_key is not None else new_idempotency_key())
|
|
578
|
+
r = yield _post("/v1/credits/convert", {"amountMusd": amount}, "agent", {"Idempotency-Key": key})
|
|
579
|
+
return {**r, "idempotencyKey": key}
|
|
580
|
+
|
|
581
|
+
def withdraw(self, amount_musd: Any, signer: Any, idempotency_key: Optional[str] = None) -> Gen:
|
|
582
|
+
"""challenge -> rebuild message (refuse on mismatch) -> linked wallet signs -> POST /v1/credits/withdraw."""
|
|
583
|
+
assert_signer(signer)
|
|
584
|
+
amount = to_musd(amount_musd, MIN_WITHDRAW_MUSD, MAX_WITHDRAW_MUSD)
|
|
585
|
+
key = validate_idempotency_key(idempotency_key if idempotency_key is not None else new_idempotency_key())
|
|
586
|
+
ch = yield _post("/v1/credits/withdraw/challenge", {"amountMusd": amount, "idempotencyKey": key}, "agent")
|
|
587
|
+
_check_signed_challenge(ch, signer)
|
|
588
|
+
server_msg = ch.get("message")
|
|
589
|
+
local = build_withdraw_message(extract_user_id(str(server_msg)), signer.address, amount, key, ch["nonce"])
|
|
590
|
+
assert_same_message(server_msg, local, "withdrawal")
|
|
591
|
+
signature = yield from sign(signer, local)
|
|
592
|
+
body = {"amountMusd": amount, "idempotencyKey": key, "nonce": ch["nonce"], "signature": signature}
|
|
593
|
+
r = yield _post("/v1/credits/withdraw", body, "agent")
|
|
594
|
+
return {**r, "idempotencyKey": key}
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
# ---------------------------------------------------------------- 3.13 payoutWallet
|
|
598
|
+
class _PayoutWalletResource(_Base):
|
|
599
|
+
def get(self) -> Gen:
|
|
600
|
+
return (yield _get("/v1/payout-wallet", "agent"))
|
|
601
|
+
|
|
602
|
+
def set(self, address: str, signer: Any, chain: str = "base", old_wallet_signer: Any = None) -> Gen:
|
|
603
|
+
"""Register or change the payout wallet (old wallet co-signs the SAME message when required)."""
|
|
604
|
+
assert_signer(signer)
|
|
605
|
+
addr = validate_address(address).lower()
|
|
606
|
+
if chain != "base":
|
|
607
|
+
raise invalid_arg("chain must be 'base'")
|
|
608
|
+
if old_wallet_signer is not None:
|
|
609
|
+
assert_signer(old_wallet_signer, "old_wallet_signer")
|
|
610
|
+
ch = yield _post("/v1/payout-wallet/challenge", {"address": addr, "chain": chain}, "agent")
|
|
611
|
+
_check_signed_challenge(ch, signer)
|
|
612
|
+
server_msg = ch.get("message")
|
|
613
|
+
local = build_payout_wallet_message(extract_user_id(str(server_msg)), signer.address, addr, chain, ch["nonce"])
|
|
614
|
+
assert_same_message(server_msg, local, "payout wallet")
|
|
615
|
+
required = ch.get("oldWalletSignatureRequired") is True
|
|
616
|
+
if required:
|
|
617
|
+
if old_wallet_signer is None:
|
|
618
|
+
raise invalid_arg(f"changing the payout wallet requires old_wallet_signer for {ch.get('oldWalletAddress')}")
|
|
619
|
+
old = ch.get("oldWalletAddress")
|
|
620
|
+
if not isinstance(old, str) or old.lower() != old_wallet_signer.address.lower():
|
|
621
|
+
raise invalid_arg("old_wallet_signer.address is not the current payout wallet")
|
|
622
|
+
signature = yield from sign(signer, local)
|
|
623
|
+
body = {"address": addr, "chain": chain, "nonce": ch["nonce"], "signature": signature}
|
|
624
|
+
if required:
|
|
625
|
+
body["oldWalletSignature"] = yield from sign(old_wallet_signer, local, "old_wallet_signer")
|
|
626
|
+
return (yield _put("/v1/payout-wallet", body, "agent"))
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
# ---------------------------------------------------------------- 3.14 payouts
|
|
630
|
+
class _PayoutsResource(_Base):
|
|
631
|
+
def list(self, limit: Optional[int] = None, offset: Optional[int] = None) -> Gen:
|
|
632
|
+
if limit is not None:
|
|
633
|
+
validate_int_range(limit, "limit", 1, 200)
|
|
634
|
+
if offset is not None:
|
|
635
|
+
validate_int_range(offset, "offset", 0, 1_000_000)
|
|
636
|
+
return (yield _get(f"/v1/payouts{qs({'limit': limit, 'offset': offset})}", "agent"))
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
# ---------------------------------------------------------------- 3.15 proxy
|
|
640
|
+
_PROXY_HOST_RE = re.compile(r"^[A-Za-z0-9.-]{1,253}\Z|^\[[0-9A-Fa-f:.]+\]\Z")
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
def _valid_port(p: Any, name: str) -> int:
|
|
644
|
+
if not _is_int(p) or p < 1 or p > 65535:
|
|
645
|
+
raise invalid_arg(f"{name} must be a TCP port")
|
|
646
|
+
return int(p)
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
def _gateway_tls(g: Any) -> Optional[GatewayTls]:
|
|
650
|
+
"""``None``/``False`` -> off; ``True`` -> port 8443; mapping ``{port, ca, servername}`` or GatewayTls."""
|
|
651
|
+
if g is None or g is False:
|
|
652
|
+
return None
|
|
653
|
+
if g is True:
|
|
654
|
+
return GatewayTls()
|
|
655
|
+
if isinstance(g, GatewayTls):
|
|
656
|
+
g = {"port": g.port, "ca": g.ca, "servername": g.servername}
|
|
657
|
+
if not isinstance(g, Mapping):
|
|
658
|
+
raise invalid_arg("gateway_tls must be a bool, a mapping or GatewayTls")
|
|
659
|
+
unknown = set(g) - {"port", "ca", "servername"}
|
|
660
|
+
if unknown:
|
|
661
|
+
raise invalid_arg("gateway_tls has unknown fields")
|
|
662
|
+
port = g.get("port")
|
|
663
|
+
port = DEFAULT_TLS_PROXY_PORT if port is None else _valid_port(port, "gateway_tls.port")
|
|
664
|
+
sn = g.get("servername")
|
|
665
|
+
if sn is not None and (not isinstance(sn, str) or not _SERVERNAME_RE.match(sn)):
|
|
666
|
+
raise invalid_arg("gateway_tls.servername is invalid")
|
|
667
|
+
ca = g.get("ca")
|
|
668
|
+
if ca is not None and not isinstance(ca, (str, bytes)):
|
|
669
|
+
raise invalid_arg("gateway_tls.ca must be a PEM string or bytes")
|
|
670
|
+
return GatewayTls(port, ca, sn)
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
_SERVERNAME_RE = re.compile(r"^[A-Za-z0-9.-]{1,253}\Z")
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
class _ProxyResource(_Base):
|
|
677
|
+
"""Gateway helpers. API key in the PASSWORD; routing flags in the USERNAME (hyphen-joined)."""
|
|
678
|
+
|
|
679
|
+
def __init__(
|
|
680
|
+
self,
|
|
681
|
+
http: Any,
|
|
682
|
+
ctx: _Ctx,
|
|
683
|
+
proxy_host: str = DEFAULT_PROXY_HOST,
|
|
684
|
+
http_proxy_port: int = DEFAULT_HTTP_PROXY_PORT,
|
|
685
|
+
socks5_proxy_port: int = DEFAULT_SOCKS5_PROXY_PORT,
|
|
686
|
+
gateway_tls: Any = None,
|
|
687
|
+
) -> None:
|
|
688
|
+
super().__init__(http, ctx)
|
|
689
|
+
if not isinstance(proxy_host, str) or not _PROXY_HOST_RE.match(proxy_host):
|
|
690
|
+
raise invalid_arg("proxy_host contains invalid characters")
|
|
691
|
+
self._host = proxy_host
|
|
692
|
+
self._http_port = _valid_port(http_proxy_port, "http_proxy_port")
|
|
693
|
+
self._socks_port = _valid_port(socks5_proxy_port, "socks5_proxy_port")
|
|
694
|
+
self._tls = _gateway_tls(gateway_tls)
|
|
695
|
+
self._lookup: Optional[LookupAll] = None
|
|
696
|
+
|
|
697
|
+
def set_lookup(self, lookup: Optional[LookupAll]) -> None:
|
|
698
|
+
"""Inject a DNS resolver (tests)."""
|
|
699
|
+
self._lookup = lookup
|
|
700
|
+
|
|
701
|
+
def _key(self, override: Optional[str]) -> str:
|
|
702
|
+
k = override if override is not None else self._ctx.api_key
|
|
703
|
+
if not k:
|
|
704
|
+
raise AuthenticationError("proxy access needs an API key: call connect_with_key() or pass api_key")
|
|
705
|
+
return validate_api_key(k)
|
|
706
|
+
|
|
707
|
+
def build_username(
|
|
708
|
+
self,
|
|
709
|
+
country: Optional[str] = None,
|
|
710
|
+
ip_class: Optional[str] = None,
|
|
711
|
+
session_id: Optional[str] = None,
|
|
712
|
+
agentic_wallet_id: Optional[str] = None,
|
|
713
|
+
allow_http: bool = False,
|
|
714
|
+
) -> str:
|
|
715
|
+
return build_proxy_username(country, ip_class, session_id, agentic_wallet_id, allow_http)
|
|
716
|
+
|
|
717
|
+
def build_url(
|
|
718
|
+
self,
|
|
719
|
+
country: Optional[str] = None,
|
|
720
|
+
ip_class: Optional[str] = None,
|
|
721
|
+
session_id: Optional[str] = None,
|
|
722
|
+
agentic_wallet_id: Optional[str] = None,
|
|
723
|
+
allow_http: bool = False,
|
|
724
|
+
protocol: Optional[str] = None,
|
|
725
|
+
api_key: Optional[str] = None,
|
|
726
|
+
) -> str:
|
|
727
|
+
"""e.g. http://country-US-class-dc:dn_live_...@proxy.dominusnode.daedalusdevelopmentgroup.com:8080. CONTAINS the key: do not log it.
|
|
728
|
+
|
|
729
|
+
``protocol``: http, https (TLS to the gateway; needs gateway_tls) or socks5.
|
|
730
|
+
Default: https when gateway_tls is configured, else http."""
|
|
731
|
+
if protocol is None:
|
|
732
|
+
protocol = "https" if self._tls is not None else "http"
|
|
733
|
+
if protocol not in ("http", "https", "socks5"):
|
|
734
|
+
raise invalid_arg("protocol must be http, https or socks5")
|
|
735
|
+
if protocol == "https" and self._tls is None:
|
|
736
|
+
raise invalid_arg("protocol https needs the gateway_tls config")
|
|
737
|
+
if protocol == "socks5":
|
|
738
|
+
port = self._socks_port
|
|
739
|
+
elif protocol == "https":
|
|
740
|
+
port = self._tls.port # type: ignore[union-attr]
|
|
741
|
+
else:
|
|
742
|
+
port = self._http_port
|
|
743
|
+
user = build_proxy_username(country, ip_class, session_id, agentic_wallet_id, allow_http)
|
|
744
|
+
key = self._key(api_key)
|
|
745
|
+
return f"{protocol}://{quote(user, safe='')}:{quote(key, safe='')}@{self._host}:{port}"
|
|
746
|
+
|
|
747
|
+
def fetch(
|
|
748
|
+
self,
|
|
749
|
+
url: str,
|
|
750
|
+
method: str = "GET",
|
|
751
|
+
headers: Optional[Dict[str, str]] = None,
|
|
752
|
+
timeout_ms: Optional[float] = None,
|
|
753
|
+
max_bytes: Optional[int] = None,
|
|
754
|
+
country: Optional[str] = None,
|
|
755
|
+
ip_class: Optional[str] = None,
|
|
756
|
+
session_id: Optional[str] = None,
|
|
757
|
+
agentic_wallet_id: Optional[str] = None,
|
|
758
|
+
allow_http: bool = False,
|
|
759
|
+
api_key: Optional[str] = None,
|
|
760
|
+
) -> Gen:
|
|
761
|
+
"""GET/HEAD through the relay. Redirects returned, not followed; body capped at max_bytes (10 MB)."""
|
|
762
|
+
key = self._key(api_key)
|
|
763
|
+
prepared = prepare_fetch(
|
|
764
|
+
key, url, method, headers, timeout_ms, max_bytes, country, ip_class, session_id, agentic_wallet_id, allow_http
|
|
765
|
+
)
|
|
766
|
+
host = self._host.strip("[]")
|
|
767
|
+
result: ProxyFetchResponse = yield Call(execute_fetch, (host, self._http_port, prepared, self._lookup, self._tls))
|
|
768
|
+
return result
|
|
769
|
+
|
|
770
|
+
def get_health(self) -> Gen:
|
|
771
|
+
return (yield _get("/api/proxy/health", "none"))
|
|
772
|
+
|
|
773
|
+
def get_config(self) -> Gen:
|
|
774
|
+
return (yield _get("/api/proxy/config"))
|
|
775
|
+
|
|
776
|
+
|
|
777
|
+
AuthResource, AsyncAuthResource = make_variants(_AuthResource)
|
|
778
|
+
WalletAuthResource, AsyncWalletAuthResource = make_variants(_WalletAuthResource)
|
|
779
|
+
KeysResource, AsyncKeysResource = make_variants(_KeysResource)
|
|
780
|
+
WalletResource, AsyncWalletResource = make_variants(_WalletResource)
|
|
781
|
+
AgenticWalletResource, AsyncAgenticWalletResource = make_variants(_AgenticWalletResource)
|
|
782
|
+
TeamsResource, AsyncTeamsResource = make_variants(_TeamsResource)
|
|
783
|
+
UsageResource, AsyncUsageResource = make_variants(_UsageResource)
|
|
784
|
+
SessionsResource, AsyncSessionsResource = make_variants(_SessionsResource)
|
|
785
|
+
X402Resource, AsyncX402Resource = make_variants(_X402Resource)
|
|
786
|
+
NetworkResource, AsyncNetworkResource = make_variants(_NetworkResource)
|
|
787
|
+
NodesResource, AsyncNodesResource = make_variants(_NodesResource)
|
|
788
|
+
CreditsResource, AsyncCreditsResource = make_variants(_CreditsResource)
|
|
789
|
+
PayoutWalletResource, AsyncPayoutWalletResource = make_variants(_PayoutWalletResource)
|
|
790
|
+
PayoutsResource, AsyncPayoutsResource = make_variants(_PayoutsResource)
|
|
791
|
+
ProxyResource, AsyncProxyResource = make_variants(_ProxyResource)
|