dominusnode 1.1.1__tar.gz → 1.3.0__tar.gz
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-1.1.1 → dominusnode-1.3.0}/PKG-INFO +1 -1
- {dominusnode-1.1.1 → dominusnode-1.3.0}/README.md +12 -12
- {dominusnode-1.1.1 → dominusnode-1.3.0}/dominusnode/__init__.py +25 -1
- {dominusnode-1.1.1 → dominusnode-1.3.0}/dominusnode/client.py +13 -0
- dominusnode-1.3.0/dominusnode/mpp.py +303 -0
- dominusnode-1.3.0/dominusnode/x402.py +138 -0
- {dominusnode-1.1.1 → dominusnode-1.3.0}/dominusnode.egg-info/PKG-INFO +1 -1
- {dominusnode-1.1.1 → dominusnode-1.3.0}/dominusnode.egg-info/SOURCES.txt +1 -0
- {dominusnode-1.1.1 → dominusnode-1.3.0}/pyproject.toml +1 -1
- dominusnode-1.1.1/dominusnode/x402.py +0 -88
- {dominusnode-1.1.1 → dominusnode-1.3.0}/LICENSE +0 -0
- {dominusnode-1.1.1 → dominusnode-1.3.0}/dominusnode/admin.py +0 -0
- {dominusnode-1.1.1 → dominusnode-1.3.0}/dominusnode/agent_wallet.py +0 -0
- {dominusnode-1.1.1 → dominusnode-1.3.0}/dominusnode/auth.py +0 -0
- {dominusnode-1.1.1 → dominusnode-1.3.0}/dominusnode/constants.py +0 -0
- {dominusnode-1.1.1 → dominusnode-1.3.0}/dominusnode/errors.py +0 -0
- {dominusnode-1.1.1 → dominusnode-1.3.0}/dominusnode/http_client.py +0 -0
- {dominusnode-1.1.1 → dominusnode-1.3.0}/dominusnode/keys.py +0 -0
- {dominusnode-1.1.1 → dominusnode-1.3.0}/dominusnode/plans.py +0 -0
- {dominusnode-1.1.1 → dominusnode-1.3.0}/dominusnode/proxy.py +0 -0
- {dominusnode-1.1.1 → dominusnode-1.3.0}/dominusnode/py.typed +0 -0
- {dominusnode-1.1.1 → dominusnode-1.3.0}/dominusnode/sessions.py +0 -0
- {dominusnode-1.1.1 → dominusnode-1.3.0}/dominusnode/slots.py +0 -0
- {dominusnode-1.1.1 → dominusnode-1.3.0}/dominusnode/teams.py +0 -0
- {dominusnode-1.1.1 → dominusnode-1.3.0}/dominusnode/token_manager.py +0 -0
- {dominusnode-1.1.1 → dominusnode-1.3.0}/dominusnode/types.py +0 -0
- {dominusnode-1.1.1 → dominusnode-1.3.0}/dominusnode/usage.py +0 -0
- {dominusnode-1.1.1 → dominusnode-1.3.0}/dominusnode/wallet.py +0 -0
- {dominusnode-1.1.1 → dominusnode-1.3.0}/dominusnode/wallet_auth.py +0 -0
- {dominusnode-1.1.1 → dominusnode-1.3.0}/dominusnode.egg-info/dependency_links.txt +0 -0
- {dominusnode-1.1.1 → dominusnode-1.3.0}/dominusnode.egg-info/requires.txt +0 -0
- {dominusnode-1.1.1 → dominusnode-1.3.0}/dominusnode.egg-info/top_level.txt +0 -0
- {dominusnode-1.1.1 → dominusnode-1.3.0}/setup.cfg +0 -0
- {dominusnode-1.1.1 → dominusnode-1.3.0}/tests/test_admin.py +0 -0
- {dominusnode-1.1.1 → dominusnode-1.3.0}/tests/test_auth.py +0 -0
- {dominusnode-1.1.1 → dominusnode-1.3.0}/tests/test_client.py +0 -0
- {dominusnode-1.1.1 → dominusnode-1.3.0}/tests/test_errors.py +0 -0
- {dominusnode-1.1.1 → dominusnode-1.3.0}/tests/test_keys.py +0 -0
- {dominusnode-1.1.1 → dominusnode-1.3.0}/tests/test_proxy.py +0 -0
- {dominusnode-1.1.1 → dominusnode-1.3.0}/tests/test_token_manager.py +0 -0
- {dominusnode-1.1.1 → dominusnode-1.3.0}/tests/test_usage.py +0 -0
- {dominusnode-1.1.1 → dominusnode-1.3.0}/tests/test_wallet.py +0 -0
- {dominusnode-1.1.1 → dominusnode-1.3.0}/tests/test_wallet_auth.py +0 -0
- {dominusnode-1.1.1 → dominusnode-1.3.0}/tests/test_x402.py +0 -0
|
@@ -394,18 +394,18 @@ with DominusNodeClient(base_url="https://api.dominusnode.com") as client:
|
|
|
394
394
|
print(f"API error: {e}")
|
|
395
395
|
```
|
|
396
396
|
|
|
397
|
-
| Error Class
|
|
398
|
-
|
|
399
|
-
| `AuthenticationError`
|
|
400
|
-
| `AuthorizationError`
|
|
401
|
-
| `InsufficientBalanceError` | 402
|
|
402
|
-
| `RateLimitError`
|
|
403
|
-
| `ValidationError`
|
|
404
|
-
| `NotFoundError`
|
|
405
|
-
| `ConflictError`
|
|
406
|
-
| `ServerError`
|
|
407
|
-
| `NetworkError`
|
|
408
|
-
| `ProxyError`
|
|
397
|
+
| Error Class | HTTP Status | When |
|
|
398
|
+
| -------------------------- | ----------- | ------------------------------------------ |
|
|
399
|
+
| `AuthenticationError` | 401 | Invalid credentials, expired token |
|
|
400
|
+
| `AuthorizationError` | 403 | Insufficient permissions (e.g., non-admin) |
|
|
401
|
+
| `InsufficientBalanceError` | 402 | Wallet balance too low |
|
|
402
|
+
| `RateLimitError` | 429 | Too many requests |
|
|
403
|
+
| `ValidationError` | 400 | Invalid input |
|
|
404
|
+
| `NotFoundError` | 404 | Resource not found |
|
|
405
|
+
| `ConflictError` | 409 | Duplicate resource |
|
|
406
|
+
| `ServerError` | 500+ | Server-side error |
|
|
407
|
+
| `NetworkError` | -- | Connection failure, timeout |
|
|
408
|
+
| `ProxyError` | -- | Proxy URL building error |
|
|
409
409
|
|
|
410
410
|
## Configuration
|
|
411
411
|
|
|
@@ -34,7 +34,19 @@ from .errors import (
|
|
|
34
34
|
ServerError,
|
|
35
35
|
ValidationError,
|
|
36
36
|
)
|
|
37
|
-
from .x402 import
|
|
37
|
+
from .x402 import (
|
|
38
|
+
X402Info,
|
|
39
|
+
X402Facilitator,
|
|
40
|
+
X402PaymentHeaders,
|
|
41
|
+
X402Pricing,
|
|
42
|
+
X402_PROTOCOL_VERSION,
|
|
43
|
+
X402_HEADER_PAYMENT_SIGNATURE,
|
|
44
|
+
X402_HEADER_PAYMENT_SIGNATURE_LEGACY,
|
|
45
|
+
X402_HEADER_PAYMENT_RESPONSE,
|
|
46
|
+
X402_HEADER_PAYMENT_RESPONSE_LEGACY,
|
|
47
|
+
X402_HEADER_PAYMENT_REQUIRED,
|
|
48
|
+
)
|
|
49
|
+
from .mpp import MppResource, AsyncMppResource, build_proxy_headers, build_session_headers
|
|
38
50
|
from .wallet_auth import WalletChallenge, WalletVerifyResult, WalletLinkResult
|
|
39
51
|
from .types import (
|
|
40
52
|
ActiveSession,
|
|
@@ -141,11 +153,23 @@ __all__ = [
|
|
|
141
153
|
# x402
|
|
142
154
|
"X402Info",
|
|
143
155
|
"X402Facilitator",
|
|
156
|
+
"X402PaymentHeaders",
|
|
144
157
|
"X402Pricing",
|
|
158
|
+
"X402_PROTOCOL_VERSION",
|
|
159
|
+
"X402_HEADER_PAYMENT_SIGNATURE",
|
|
160
|
+
"X402_HEADER_PAYMENT_SIGNATURE_LEGACY",
|
|
161
|
+
"X402_HEADER_PAYMENT_RESPONSE",
|
|
162
|
+
"X402_HEADER_PAYMENT_RESPONSE_LEGACY",
|
|
163
|
+
"X402_HEADER_PAYMENT_REQUIRED",
|
|
145
164
|
# Wallet Auth
|
|
146
165
|
"WalletChallenge",
|
|
147
166
|
"WalletVerifyResult",
|
|
148
167
|
"WalletLinkResult",
|
|
168
|
+
# MPP
|
|
169
|
+
"MppResource",
|
|
170
|
+
"AsyncMppResource",
|
|
171
|
+
"build_proxy_headers",
|
|
172
|
+
"build_session_headers",
|
|
149
173
|
# Constants
|
|
150
174
|
"SDK_VERSION",
|
|
151
175
|
]
|
|
@@ -40,6 +40,7 @@ from .constants import (
|
|
|
40
40
|
)
|
|
41
41
|
from .http_client import AsyncHttpClient, SyncHttpClient
|
|
42
42
|
from .keys import AsyncKeysResource, KeysResource
|
|
43
|
+
from .mpp import AsyncMppResource, MppResource
|
|
43
44
|
from .plans import AsyncPlansResource, PlansResource
|
|
44
45
|
from .proxy import AsyncProxyResource, ProxyResource
|
|
45
46
|
from .sessions import AsyncSessionsResource, SessionsResource
|
|
@@ -130,6 +131,7 @@ class DominusNodeClient:
|
|
|
130
131
|
self._teams = TeamsRes(self._http)
|
|
131
132
|
self._x402 = X402Resource(self._http)
|
|
132
133
|
self._wallet_auth = WalletAuthResource(self._http, self._token_manager)
|
|
134
|
+
self._mpp = MppResource(self._http)
|
|
133
135
|
|
|
134
136
|
# Auto-connect if credentials provided.
|
|
135
137
|
# On failure, clear credentials to prevent retention in a partial object.
|
|
@@ -210,6 +212,11 @@ class DominusNodeClient:
|
|
|
210
212
|
"""Wallet-based authentication (EIP-191 signed messages)."""
|
|
211
213
|
return self._wallet_auth
|
|
212
214
|
|
|
215
|
+
@property
|
|
216
|
+
def mpp(self) -> MppResource:
|
|
217
|
+
"""MPP (Micropayment Protocol) operations."""
|
|
218
|
+
return self._mpp
|
|
219
|
+
|
|
213
220
|
# ── Connection methods ───────────────────────────────────────────
|
|
214
221
|
|
|
215
222
|
def connect_with_key(self, api_key: str) -> LoginResult:
|
|
@@ -341,6 +348,7 @@ class AsyncDominusNodeClient:
|
|
|
341
348
|
self._teams = AsyncTeamsRes(self._http)
|
|
342
349
|
self._x402 = AsyncX402Resource(self._http)
|
|
343
350
|
self._wallet_auth = AsyncWalletAuthResource(self._http, self._token_manager)
|
|
351
|
+
self._mpp = AsyncMppResource(self._http)
|
|
344
352
|
|
|
345
353
|
# ── Resource properties ──────────────────────────────────────────
|
|
346
354
|
|
|
@@ -399,6 +407,11 @@ class AsyncDominusNodeClient:
|
|
|
399
407
|
"""Wallet-based authentication (EIP-191 signed messages)."""
|
|
400
408
|
return self._wallet_auth
|
|
401
409
|
|
|
410
|
+
@property
|
|
411
|
+
def mpp(self) -> AsyncMppResource:
|
|
412
|
+
"""MPP (Micropayment Protocol) operations."""
|
|
413
|
+
return self._mpp
|
|
414
|
+
|
|
402
415
|
# ── Connection methods ───────────────────────────────────────────
|
|
403
416
|
|
|
404
417
|
async def connect_with_key(self, api_key: str) -> LoginResult:
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
"""MPP (Micropayment Protocol) resource -- info, top-ups, payment sessions, and keyless proxy access.
|
|
2
|
+
|
|
3
|
+
MPP allows AI agents to use the proxy WITHOUT any API key -- just pay per request.
|
|
4
|
+
Call ``get_challenge()`` to obtain a credential, then use ``build_proxy_headers()``
|
|
5
|
+
to construct the headers needed for keyless proxy access.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import math
|
|
12
|
+
from typing import Any, Dict
|
|
13
|
+
|
|
14
|
+
# IETF MPP auth scheme names
|
|
15
|
+
MPP_AUTH_SCHEME = "Payment"
|
|
16
|
+
MPP_AUTH_SCHEME_LEGACY = "MPP"
|
|
17
|
+
from urllib.parse import quote
|
|
18
|
+
|
|
19
|
+
from .http_client import AsyncHttpClient, SyncHttpClient
|
|
20
|
+
from .wallet import _validate_amount_cents
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _validate_finite_int(value: int, name: str) -> None:
|
|
24
|
+
"""Validate that *value* is a finite integer (guards against NaN/Infinity via float coercion)."""
|
|
25
|
+
if not isinstance(value, int):
|
|
26
|
+
raise ValueError(f"{name} must be an integer")
|
|
27
|
+
# Python int can't be NaN/Inf, but guard against float passed as int subclass
|
|
28
|
+
as_float = float(value)
|
|
29
|
+
if math.isnan(as_float) or math.isinf(as_float):
|
|
30
|
+
raise ValueError(f"{name} must be a finite number")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def build_proxy_headers(credential: Dict[str, Any]) -> Dict[str, str]:
|
|
34
|
+
"""Build proxy Authorization header for keyless MPP access.
|
|
35
|
+
|
|
36
|
+
Returns headers that should be set on the HTTP CONNECT / proxy request
|
|
37
|
+
to route through the proxy without an API key.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
credential: The credential dict from ``get_challenge()`` (or a manually
|
|
41
|
+
constructed dict with at least ``challengeId``, ``nonce``, ``poolType``).
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
Headers dict with ``Authorization: MPP <json>``.
|
|
45
|
+
"""
|
|
46
|
+
return {
|
|
47
|
+
"Authorization": MPP_AUTH_SCHEME + " " + json.dumps(credential, separators=(",", ":")),
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def build_session_headers(session_token: str) -> Dict[str, str]:
|
|
52
|
+
"""Build proxy session token header for ongoing MPP sessions.
|
|
53
|
+
|
|
54
|
+
After opening a session, use the session token for subsequent requests
|
|
55
|
+
within the same session.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
session_token: The session token from an active MPP session.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
Headers dict with ``MPP-SESSION-TOKEN``.
|
|
62
|
+
"""
|
|
63
|
+
if not session_token or not isinstance(session_token, str):
|
|
64
|
+
raise ValueError("session_token must be a non-empty string")
|
|
65
|
+
return {
|
|
66
|
+
"MPP-SESSION-TOKEN": session_token,
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
71
|
+
# Sync
|
|
72
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class MppResource:
|
|
76
|
+
"""Synchronous MPP (Micropayment Protocol) operations.
|
|
77
|
+
|
|
78
|
+
MPP enables keyless proxy access for AI agents: call ``get_challenge()`` to
|
|
79
|
+
obtain a credential, then use ``build_proxy_headers()`` to construct the proxy
|
|
80
|
+
Authorization header -- no API key needed.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
def __init__(self, http: SyncHttpClient) -> None:
|
|
84
|
+
self._http = http
|
|
85
|
+
|
|
86
|
+
def get_info(self) -> dict:
|
|
87
|
+
"""Get MPP protocol information.
|
|
88
|
+
|
|
89
|
+
This endpoint does not require authentication.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
Server response dict with MPP capabilities and configuration.
|
|
93
|
+
"""
|
|
94
|
+
data = self._http.get("/api/mpp/info", requires_auth=False)
|
|
95
|
+
return data
|
|
96
|
+
|
|
97
|
+
def get_challenge(self, pool_type: str) -> dict:
|
|
98
|
+
"""Request an MPP challenge for keyless proxy access.
|
|
99
|
+
|
|
100
|
+
This endpoint does not require authentication. The returned credential
|
|
101
|
+
can be used with ``build_proxy_headers()`` to access the proxy without
|
|
102
|
+
an API key.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
pool_type: Proxy pool type -- ``"dc"`` (datacenter, $3/GB) or
|
|
106
|
+
``"residential"`` ($5/GB).
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
Server response dict with ``challengeId``, ``nonce``, ``expiresAt``,
|
|
110
|
+
``poolType``, and ``credential``.
|
|
111
|
+
"""
|
|
112
|
+
if pool_type not in ("dc", "residential"):
|
|
113
|
+
raise ValueError("pool_type must be 'dc' or 'residential'")
|
|
114
|
+
data = self._http.post(
|
|
115
|
+
"/api/mpp/challenge",
|
|
116
|
+
json={"poolType": pool_type},
|
|
117
|
+
requires_auth=False,
|
|
118
|
+
)
|
|
119
|
+
return data
|
|
120
|
+
|
|
121
|
+
def top_up(self, amount_cents: int, method: str) -> dict:
|
|
122
|
+
"""Top up the MPP balance.
|
|
123
|
+
|
|
124
|
+
Requires authentication (wallet credit needs a user account).
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
amount_cents: Amount in cents (must be positive, <= 2,147,483,647).
|
|
128
|
+
method: Payment method identifier (e.g. "stripe", "paypal", "crypto").
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
Server response dict with top-up confirmation details.
|
|
132
|
+
"""
|
|
133
|
+
_validate_finite_int(amount_cents, "amount_cents")
|
|
134
|
+
_validate_amount_cents(amount_cents, "amount_cents")
|
|
135
|
+
data = self._http.post("/api/mpp/topup", json={
|
|
136
|
+
"amountCents": amount_cents,
|
|
137
|
+
"method": method,
|
|
138
|
+
})
|
|
139
|
+
return data
|
|
140
|
+
|
|
141
|
+
def open_session(self, max_deposit_cents: int, method: str, pool_type: str) -> dict:
|
|
142
|
+
"""Open a new MPP payment session (payment channel).
|
|
143
|
+
|
|
144
|
+
This endpoint does not require authentication -- anonymous agents can
|
|
145
|
+
open sessions by providing a payment method and deposit.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
max_deposit_cents: Maximum deposit in cents (must be positive, <= 2,147,483,647).
|
|
149
|
+
method: Payment method identifier.
|
|
150
|
+
pool_type: Proxy pool type (e.g. "dc", "residential").
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
Server response dict with session/channel details.
|
|
154
|
+
"""
|
|
155
|
+
_validate_finite_int(max_deposit_cents, "max_deposit_cents")
|
|
156
|
+
_validate_amount_cents(max_deposit_cents, "max_deposit_cents")
|
|
157
|
+
data = self._http.post("/api/mpp/session/open", json={
|
|
158
|
+
"maxDepositCents": max_deposit_cents,
|
|
159
|
+
"method": method,
|
|
160
|
+
"poolType": pool_type,
|
|
161
|
+
}, requires_auth=False)
|
|
162
|
+
return data
|
|
163
|
+
|
|
164
|
+
def close_session(self, channel_id: str) -> dict:
|
|
165
|
+
"""Close an existing MPP payment session.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
channel_id: The payment channel identifier.
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
Server response dict with closure confirmation and final settlement.
|
|
172
|
+
"""
|
|
173
|
+
data = self._http.post("/api/mpp/session/close", json={
|
|
174
|
+
"channelId": channel_id,
|
|
175
|
+
})
|
|
176
|
+
return data
|
|
177
|
+
|
|
178
|
+
def get_session(self, channel_id: str) -> dict:
|
|
179
|
+
"""Get details of an MPP payment session.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
channel_id: The payment channel identifier (URL-encoded automatically).
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
Server response dict with session state and usage details.
|
|
186
|
+
"""
|
|
187
|
+
data = self._http.get(f"/api/mpp/session/{quote(channel_id, safe='')}")
|
|
188
|
+
return data
|
|
189
|
+
|
|
190
|
+
# Convenience re-exports as static methods for discoverability
|
|
191
|
+
build_proxy_headers = staticmethod(build_proxy_headers)
|
|
192
|
+
build_session_headers = staticmethod(build_session_headers)
|
|
193
|
+
|
|
194
|
+
def __repr__(self) -> str:
|
|
195
|
+
return "MppResource()"
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
199
|
+
# Async
|
|
200
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
class AsyncMppResource:
|
|
204
|
+
"""Asynchronous MPP (Micropayment Protocol) operations.
|
|
205
|
+
|
|
206
|
+
MPP enables keyless proxy access for AI agents: call ``get_challenge()`` to
|
|
207
|
+
obtain a credential, then use ``build_proxy_headers()`` to construct the proxy
|
|
208
|
+
Authorization header -- no API key needed.
|
|
209
|
+
"""
|
|
210
|
+
|
|
211
|
+
def __init__(self, http: AsyncHttpClient) -> None:
|
|
212
|
+
self._http = http
|
|
213
|
+
|
|
214
|
+
async def get_info(self) -> dict:
|
|
215
|
+
"""Get MPP protocol information.
|
|
216
|
+
|
|
217
|
+
This endpoint does not require authentication.
|
|
218
|
+
"""
|
|
219
|
+
data = await self._http.get("/api/mpp/info", requires_auth=False)
|
|
220
|
+
return data
|
|
221
|
+
|
|
222
|
+
async def get_challenge(self, pool_type: str) -> dict:
|
|
223
|
+
"""Request an MPP challenge for keyless proxy access.
|
|
224
|
+
|
|
225
|
+
This endpoint does not require authentication. The returned credential
|
|
226
|
+
can be used with ``build_proxy_headers()`` to access the proxy without
|
|
227
|
+
an API key.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
pool_type: Proxy pool type -- ``"dc"`` or ``"residential"``.
|
|
231
|
+
"""
|
|
232
|
+
if pool_type not in ("dc", "residential"):
|
|
233
|
+
raise ValueError("pool_type must be 'dc' or 'residential'")
|
|
234
|
+
data = await self._http.post(
|
|
235
|
+
"/api/mpp/challenge",
|
|
236
|
+
json={"poolType": pool_type},
|
|
237
|
+
requires_auth=False,
|
|
238
|
+
)
|
|
239
|
+
return data
|
|
240
|
+
|
|
241
|
+
async def top_up(self, amount_cents: int, method: str) -> dict:
|
|
242
|
+
"""Top up the MPP balance.
|
|
243
|
+
|
|
244
|
+
Requires authentication (wallet credit needs a user account).
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
amount_cents: Amount in cents (must be positive, <= 2,147,483,647).
|
|
248
|
+
method: Payment method identifier (e.g. "stripe", "paypal", "crypto").
|
|
249
|
+
"""
|
|
250
|
+
_validate_finite_int(amount_cents, "amount_cents")
|
|
251
|
+
_validate_amount_cents(amount_cents, "amount_cents")
|
|
252
|
+
data = await self._http.post("/api/mpp/topup", json={
|
|
253
|
+
"amountCents": amount_cents,
|
|
254
|
+
"method": method,
|
|
255
|
+
})
|
|
256
|
+
return data
|
|
257
|
+
|
|
258
|
+
async def open_session(self, max_deposit_cents: int, method: str, pool_type: str) -> dict:
|
|
259
|
+
"""Open a new MPP payment session (payment channel).
|
|
260
|
+
|
|
261
|
+
This endpoint does not require authentication -- anonymous agents can
|
|
262
|
+
open sessions by providing a payment method and deposit.
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
max_deposit_cents: Maximum deposit in cents (must be positive, <= 2,147,483,647).
|
|
266
|
+
method: Payment method identifier.
|
|
267
|
+
pool_type: Proxy pool type (e.g. "dc", "residential").
|
|
268
|
+
"""
|
|
269
|
+
_validate_finite_int(max_deposit_cents, "max_deposit_cents")
|
|
270
|
+
_validate_amount_cents(max_deposit_cents, "max_deposit_cents")
|
|
271
|
+
data = await self._http.post("/api/mpp/session/open", json={
|
|
272
|
+
"maxDepositCents": max_deposit_cents,
|
|
273
|
+
"method": method,
|
|
274
|
+
"poolType": pool_type,
|
|
275
|
+
}, requires_auth=False)
|
|
276
|
+
return data
|
|
277
|
+
|
|
278
|
+
async def close_session(self, channel_id: str) -> dict:
|
|
279
|
+
"""Close an existing MPP payment session.
|
|
280
|
+
|
|
281
|
+
Args:
|
|
282
|
+
channel_id: The payment channel identifier.
|
|
283
|
+
"""
|
|
284
|
+
data = await self._http.post("/api/mpp/session/close", json={
|
|
285
|
+
"channelId": channel_id,
|
|
286
|
+
})
|
|
287
|
+
return data
|
|
288
|
+
|
|
289
|
+
async def get_session(self, channel_id: str) -> dict:
|
|
290
|
+
"""Get details of an MPP payment session.
|
|
291
|
+
|
|
292
|
+
Args:
|
|
293
|
+
channel_id: The payment channel identifier (URL-encoded automatically).
|
|
294
|
+
"""
|
|
295
|
+
data = await self._http.get(f"/api/mpp/session/{quote(channel_id, safe='')}")
|
|
296
|
+
return data
|
|
297
|
+
|
|
298
|
+
# Convenience re-exports as static methods for discoverability
|
|
299
|
+
build_proxy_headers = staticmethod(build_proxy_headers)
|
|
300
|
+
build_session_headers = staticmethod(build_session_headers)
|
|
301
|
+
|
|
302
|
+
def __repr__(self) -> str:
|
|
303
|
+
return "AsyncMppResource()"
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""x402 V2 (HTTP 402 Payment Required) protocol information resource.
|
|
2
|
+
|
|
3
|
+
V2 changes from V1:
|
|
4
|
+
- Protocol version "2" (was "1")
|
|
5
|
+
- CAIP-2 network IDs (e.g. "eip155:8453" instead of "base")
|
|
6
|
+
- Payment header: PAYMENT-SIGNATURE (was X-PAYMENT)
|
|
7
|
+
- Response header: PAYMENT-RESPONSE (was X-PAYMENT-RESPONSE)
|
|
8
|
+
- Requirements header: PAYMENT-REQUIRED
|
|
9
|
+
- New token: EURC (in addition to USDC)
|
|
10
|
+
- New scheme: permit2 (in addition to exact)
|
|
11
|
+
- Additional facilitator chains: Ethereum (eip155:1), Optimism (eip155:10),
|
|
12
|
+
Arbitrum (eip155:42161), and Solana
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from typing import List
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
|
+
|
|
20
|
+
from .http_client import AsyncHttpClient, SyncHttpClient
|
|
21
|
+
|
|
22
|
+
# x402 V2 protocol version.
|
|
23
|
+
X402_PROTOCOL_VERSION = "2"
|
|
24
|
+
|
|
25
|
+
# x402 V2 header constants.
|
|
26
|
+
# V2 renamed the payment headers. Servers should accept both old and new
|
|
27
|
+
# for backward compatibility during the transition period.
|
|
28
|
+
X402_HEADER_PAYMENT_SIGNATURE = "PAYMENT-SIGNATURE"
|
|
29
|
+
X402_HEADER_PAYMENT_SIGNATURE_LEGACY = "X-PAYMENT"
|
|
30
|
+
X402_HEADER_PAYMENT_RESPONSE = "PAYMENT-RESPONSE"
|
|
31
|
+
X402_HEADER_PAYMENT_RESPONSE_LEGACY = "X-PAYMENT-RESPONSE"
|
|
32
|
+
X402_HEADER_PAYMENT_REQUIRED = "PAYMENT-REQUIRED"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class X402Facilitator:
|
|
37
|
+
address: str
|
|
38
|
+
chain: str
|
|
39
|
+
"""Human-readable network name (e.g. "base", "ethereum")."""
|
|
40
|
+
network: str
|
|
41
|
+
"""CAIP-2 network identifier (e.g. "eip155:8453"). Added in V2."""
|
|
42
|
+
network_id: str = ""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(frozen=True)
|
|
46
|
+
class X402Pricing:
|
|
47
|
+
per_request_cents: int
|
|
48
|
+
per_gb_cents: int
|
|
49
|
+
currency: str
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass(frozen=True)
|
|
53
|
+
class X402PaymentHeaders:
|
|
54
|
+
"""Header names used by the current protocol version."""
|
|
55
|
+
request: str = X402_HEADER_PAYMENT_SIGNATURE
|
|
56
|
+
response: str = X402_HEADER_PAYMENT_RESPONSE
|
|
57
|
+
required: str = X402_HEADER_PAYMENT_REQUIRED
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass(frozen=True)
|
|
61
|
+
class X402Info:
|
|
62
|
+
supported: bool
|
|
63
|
+
enabled: bool
|
|
64
|
+
protocol: str
|
|
65
|
+
"""Protocol version -- "2" for x402 V2."""
|
|
66
|
+
version: str
|
|
67
|
+
facilitators: List[X402Facilitator]
|
|
68
|
+
pricing: X402Pricing
|
|
69
|
+
"""Supported token symbols (e.g. ["USDC", "EURC"])."""
|
|
70
|
+
currencies: List[str]
|
|
71
|
+
"""Supported payment schemes (e.g. ["exact", "permit2"])."""
|
|
72
|
+
schemes: List[str] = field(default_factory=list)
|
|
73
|
+
wallet_type: str = ""
|
|
74
|
+
agentic_wallets: bool = False
|
|
75
|
+
"""Header names used by this protocol version."""
|
|
76
|
+
payment_headers: X402PaymentHeaders = field(default_factory=X402PaymentHeaders)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _parse_x402_info(data: dict) -> X402Info:
|
|
80
|
+
"""Parse x402 info response into typed dataclass."""
|
|
81
|
+
facilitators = [
|
|
82
|
+
X402Facilitator(
|
|
83
|
+
address=f["address"],
|
|
84
|
+
chain=f["chain"],
|
|
85
|
+
network=f["network"],
|
|
86
|
+
network_id=f.get("networkId", ""),
|
|
87
|
+
)
|
|
88
|
+
for f in data.get("facilitators", [])
|
|
89
|
+
]
|
|
90
|
+
pricing_data = data.get("pricing", {})
|
|
91
|
+
pricing = X402Pricing(
|
|
92
|
+
per_request_cents=pricing_data.get("perRequestCents", 0),
|
|
93
|
+
per_gb_cents=pricing_data.get("perGbCents", 0),
|
|
94
|
+
currency=pricing_data.get("currency", ""),
|
|
95
|
+
)
|
|
96
|
+
headers_data = data.get("paymentHeaders", {})
|
|
97
|
+
payment_headers = X402PaymentHeaders(
|
|
98
|
+
request=headers_data.get("request", X402_HEADER_PAYMENT_SIGNATURE),
|
|
99
|
+
response=headers_data.get("response", X402_HEADER_PAYMENT_RESPONSE),
|
|
100
|
+
required=headers_data.get("required", X402_HEADER_PAYMENT_REQUIRED),
|
|
101
|
+
)
|
|
102
|
+
return X402Info(
|
|
103
|
+
supported=data.get("supported", False),
|
|
104
|
+
enabled=data.get("enabled", False),
|
|
105
|
+
protocol=data.get("protocol", ""),
|
|
106
|
+
version=data.get("version", ""),
|
|
107
|
+
facilitators=facilitators,
|
|
108
|
+
pricing=pricing,
|
|
109
|
+
currencies=data.get("currencies", []),
|
|
110
|
+
schemes=data.get("schemes", []),
|
|
111
|
+
wallet_type=data.get("walletType", ""),
|
|
112
|
+
agentic_wallets=data.get("agenticWallets", False),
|
|
113
|
+
payment_headers=payment_headers,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class X402Resource:
|
|
118
|
+
"""Synchronous x402 V2 protocol information operations."""
|
|
119
|
+
|
|
120
|
+
def __init__(self, http: SyncHttpClient) -> None:
|
|
121
|
+
self._http = http
|
|
122
|
+
|
|
123
|
+
def get_info(self) -> X402Info:
|
|
124
|
+
"""Get x402 V2 protocol information (no auth required)."""
|
|
125
|
+
data = self._http.get("/api/x402/info", requires_auth=False)
|
|
126
|
+
return _parse_x402_info(data)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class AsyncX402Resource:
|
|
130
|
+
"""Asynchronous x402 V2 protocol information operations."""
|
|
131
|
+
|
|
132
|
+
def __init__(self, http: AsyncHttpClient) -> None:
|
|
133
|
+
self._http = http
|
|
134
|
+
|
|
135
|
+
async def get_info(self) -> X402Info:
|
|
136
|
+
"""Get x402 V2 protocol information (no auth required)."""
|
|
137
|
+
data = await self._http.get("/api/x402/info", requires_auth=False)
|
|
138
|
+
return _parse_x402_info(data)
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
"""x402 (HTTP 402 Payment Required) protocol information resource."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
from typing import List
|
|
6
|
-
from dataclasses import dataclass, field
|
|
7
|
-
|
|
8
|
-
from .http_client import AsyncHttpClient, SyncHttpClient
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
@dataclass(frozen=True)
|
|
12
|
-
class X402Facilitator:
|
|
13
|
-
address: str
|
|
14
|
-
chain: str
|
|
15
|
-
network: str
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
@dataclass(frozen=True)
|
|
19
|
-
class X402Pricing:
|
|
20
|
-
per_request_cents: int
|
|
21
|
-
per_gb_cents: int
|
|
22
|
-
currency: str
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
@dataclass(frozen=True)
|
|
26
|
-
class X402Info:
|
|
27
|
-
supported: bool
|
|
28
|
-
enabled: bool
|
|
29
|
-
protocol: str
|
|
30
|
-
version: str
|
|
31
|
-
facilitators: List[X402Facilitator]
|
|
32
|
-
pricing: X402Pricing
|
|
33
|
-
currencies: List[str]
|
|
34
|
-
wallet_type: str
|
|
35
|
-
agentic_wallets: bool
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def _parse_x402_info(data: dict) -> X402Info:
|
|
39
|
-
"""Parse x402 info response into typed dataclass."""
|
|
40
|
-
facilitators = [
|
|
41
|
-
X402Facilitator(
|
|
42
|
-
address=f["address"],
|
|
43
|
-
chain=f["chain"],
|
|
44
|
-
network=f["network"],
|
|
45
|
-
)
|
|
46
|
-
for f in data.get("facilitators", [])
|
|
47
|
-
]
|
|
48
|
-
pricing_data = data.get("pricing", {})
|
|
49
|
-
pricing = X402Pricing(
|
|
50
|
-
per_request_cents=pricing_data.get("perRequestCents", 0),
|
|
51
|
-
per_gb_cents=pricing_data.get("perGbCents", 0),
|
|
52
|
-
currency=pricing_data.get("currency", ""),
|
|
53
|
-
)
|
|
54
|
-
return X402Info(
|
|
55
|
-
supported=data.get("supported", False),
|
|
56
|
-
enabled=data.get("enabled", False),
|
|
57
|
-
protocol=data.get("protocol", ""),
|
|
58
|
-
version=data.get("version", ""),
|
|
59
|
-
facilitators=facilitators,
|
|
60
|
-
pricing=pricing,
|
|
61
|
-
currencies=data.get("currencies", []),
|
|
62
|
-
wallet_type=data.get("walletType", ""),
|
|
63
|
-
agentic_wallets=data.get("agenticWallets", False),
|
|
64
|
-
)
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
class X402Resource:
|
|
68
|
-
"""Synchronous x402 protocol information operations."""
|
|
69
|
-
|
|
70
|
-
def __init__(self, http: SyncHttpClient) -> None:
|
|
71
|
-
self._http = http
|
|
72
|
-
|
|
73
|
-
def get_info(self) -> X402Info:
|
|
74
|
-
"""Get x402 protocol information (no auth required)."""
|
|
75
|
-
data = self._http.get("/api/x402/info", requires_auth=False)
|
|
76
|
-
return _parse_x402_info(data)
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
class AsyncX402Resource:
|
|
80
|
-
"""Asynchronous x402 protocol information operations."""
|
|
81
|
-
|
|
82
|
-
def __init__(self, http: AsyncHttpClient) -> None:
|
|
83
|
-
self._http = http
|
|
84
|
-
|
|
85
|
-
async def get_info(self) -> X402Info:
|
|
86
|
-
"""Get x402 protocol information (no auth required)."""
|
|
87
|
-
data = await self._http.get("/api/x402/info", requires_auth=False)
|
|
88
|
-
return _parse_x402_info(data)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|