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.
@@ -0,0 +1,45 @@
1
+ """Official DominusNode SDK for Python: AI-agent relay network (proxy, nodes, credits, payouts)."""
2
+
3
+ from .client import AsyncDominusNodeClient, DominusNodeClient
4
+ from .constants import (
5
+ DEFAULT_BASE_URL,
6
+ DEFAULT_HTTP_PROXY_PORT,
7
+ DEFAULT_PROXY_HOST,
8
+ DEFAULT_SOCKS5_PROXY_PORT,
9
+ MAX_AMOUNT_CENTS,
10
+ MAX_RESPONSE_BYTES,
11
+ SDK_VERSION,
12
+ USER_AGENT,
13
+ )
14
+ from .errors import (
15
+ AuthenticationError,
16
+ AuthorizationError,
17
+ ConflictError,
18
+ DominusNodeError,
19
+ InsufficientBalanceError,
20
+ NetworkError,
21
+ NotFoundError,
22
+ ProtocolError,
23
+ ProxyError,
24
+ RateLimitError,
25
+ ServerError,
26
+ ValidationError,
27
+ scrub_credentials,
28
+ )
29
+ from .messages import build_attestation_message, build_payout_wallet_message, build_withdraw_message
30
+ from .proxy_fetch import GatewayTls, ProxyFetchResponse, build_proxy_username
31
+ from .secret import Secret, json_default
32
+ from .signer import Signer
33
+ from .ssrf import is_private_ip, validate_target_url
34
+
35
+ __version__ = SDK_VERSION
36
+
37
+ __all__ = [
38
+ "DominusNodeClient", "AsyncDominusNodeClient", "Signer", "Secret", "json_default",
39
+ "build_proxy_username", "ProxyFetchResponse", "GatewayTls", "is_private_ip", "validate_target_url",
40
+ "build_attestation_message", "build_payout_wallet_message", "build_withdraw_message",
41
+ "DominusNodeError", "AuthenticationError", "AuthorizationError", "RateLimitError", "InsufficientBalanceError",
42
+ "ValidationError", "NotFoundError", "ConflictError", "ServerError", "NetworkError", "ProtocolError", "ProxyError",
43
+ "scrub_credentials", "SDK_VERSION", "USER_AGENT", "DEFAULT_BASE_URL", "DEFAULT_PROXY_HOST",
44
+ "DEFAULT_HTTP_PROXY_PORT", "DEFAULT_SOCKS5_PROXY_PORT", "MAX_RESPONSE_BYTES", "MAX_AMOUNT_CENTS", "__version__",
45
+ ]
dominusnode/_ops.py ADDED
@@ -0,0 +1,122 @@
1
+ """Sans-I/O core shared by the sync and async clients (internal).
2
+
3
+ Each resource method is written once as a generator that yields operations
4
+ (an API ``Request``, a ``Sign`` call or a blocking ``Call``) and receives their
5
+ results. ``drive_sync`` / ``drive_async`` execute those operations, so the sync
6
+ and async clients expose identical behaviour from a single implementation.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import asyncio
12
+ import functools
13
+ import inspect
14
+ from dataclasses import dataclass
15
+ from typing import Any, Callable, Dict, Generator, Tuple, Type, TypeVar
16
+
17
+ from .errors import ProtocolError
18
+ from .http_client import AsyncHttpClient, Request, SyncHttpClient
19
+
20
+ Op = Any
21
+ Gen = Generator[Op, Any, Any]
22
+
23
+
24
+ @dataclass
25
+ class Sign:
26
+ signer: Any
27
+ message: str
28
+
29
+
30
+ @dataclass
31
+ class Call:
32
+ """Blocking work (PoW, socket I/O). Sync: called inline. Async: run in a worker thread."""
33
+
34
+ fn: Callable[..., Any]
35
+ args: Tuple[Any, ...] = ()
36
+
37
+
38
+ def _exec_sync(http: SyncHttpClient, op: Op) -> Any:
39
+ if isinstance(op, Request):
40
+ return http.request(op)
41
+ if isinstance(op, Sign):
42
+ result = op.signer.sign_message(op.message)
43
+ if inspect.isawaitable(result):
44
+ if hasattr(result, "close"):
45
+ result.close()
46
+ raise ProtocolError("sign_message returned an awaitable; use AsyncDominusNodeClient for async signers")
47
+ return result
48
+ if isinstance(op, Call):
49
+ return op.fn(*op.args)
50
+ raise TypeError(f"unknown op {op!r}")
51
+
52
+
53
+ async def _exec_async(http: AsyncHttpClient, op: Op) -> Any:
54
+ if isinstance(op, Request):
55
+ return await http.request(op)
56
+ if isinstance(op, Sign):
57
+ result = op.signer.sign_message(op.message)
58
+ if inspect.isawaitable(result):
59
+ result = await result
60
+ return result
61
+ if isinstance(op, Call):
62
+ return await asyncio.to_thread(op.fn, *op.args)
63
+ raise TypeError(f"unknown op {op!r}")
64
+
65
+
66
+ def drive_sync(http: SyncHttpClient, gen: Gen) -> Any:
67
+ value: Any = None
68
+ exc: Any = None
69
+ while True:
70
+ try:
71
+ op = gen.throw(exc) if exc is not None else gen.send(value)
72
+ except StopIteration as stop:
73
+ return stop.value
74
+ try:
75
+ value, exc = _exec_sync(http, op), None
76
+ except Exception as e: # noqa: BLE001 - delivered into the generator
77
+ value, exc = None, e
78
+
79
+
80
+ async def drive_async(http: AsyncHttpClient, gen: Gen) -> Any:
81
+ value: Any = None
82
+ exc: Any = None
83
+ while True:
84
+ try:
85
+ op = gen.throw(exc) if exc is not None else gen.send(value)
86
+ except StopIteration as stop:
87
+ return stop.value
88
+ try:
89
+ value, exc = await _exec_async(http, op), None
90
+ except Exception as e: # noqa: BLE001
91
+ value, exc = None, e
92
+
93
+
94
+ T = TypeVar("T")
95
+
96
+
97
+ def _sync_method(fn: Callable[..., Gen]) -> Callable[..., Any]:
98
+ @functools.wraps(fn)
99
+ def wrapper(self: Any, *args: Any, **kwargs: Any) -> Any:
100
+ return drive_sync(self._http, fn(self, *args, **kwargs))
101
+
102
+ return wrapper
103
+
104
+
105
+ def _async_method(fn: Callable[..., Gen]) -> Callable[..., Any]:
106
+ @functools.wraps(fn)
107
+ async def wrapper(self: Any, *args: Any, **kwargs: Any) -> Any:
108
+ return await drive_async(self._http, fn(self, *args, **kwargs))
109
+
110
+ return wrapper
111
+
112
+
113
+ def make_variants(core: Type[T]) -> Tuple[Type[T], Type[T]]:
114
+ """Build (SyncResource, AsyncResource) subclasses: every public generator method becomes a plain/async method."""
115
+ name = core.__name__.lstrip("_")
116
+ sync_ns: Dict[str, Any] = {"__doc__": core.__doc__, "__module__": core.__module__}
117
+ async_ns: Dict[str, Any] = {"__doc__": core.__doc__, "__module__": core.__module__}
118
+ for attr, value in vars(core).items():
119
+ if not attr.startswith("_") and inspect.isgeneratorfunction(value):
120
+ sync_ns[attr] = _sync_method(value)
121
+ async_ns[attr] = _async_method(value)
122
+ return type(name, (core,), sync_ns), type("Async" + name, (core,), async_ns) # type: ignore[return-value]
dominusnode/client.py ADDED
@@ -0,0 +1,192 @@
1
+ """DominusNode relay-network clients (sync and async, identical surface)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Dict, Generator, Optional
6
+
7
+ import httpx
8
+
9
+ from ._ops import drive_async, drive_sync
10
+ from .constants import DEFAULT_BASE_URL, DEFAULT_HTTP_PROXY_PORT, DEFAULT_PROXY_HOST, DEFAULT_SOCKS5_PROXY_PORT
11
+ from .errors import DominusNodeError
12
+ from .http_client import AsyncHttpClient, SyncHttpClient
13
+ from .resources import (
14
+ AgenticWalletResource, AsyncAgenticWalletResource, AsyncAuthResource, AsyncCreditsResource,
15
+ AsyncKeysResource, AsyncNetworkResource, AsyncNodesResource, AsyncPayoutsResource,
16
+ AsyncPayoutWalletResource, AsyncProxyResource, AsyncSessionsResource, AsyncTeamsResource,
17
+ AsyncUsageResource, AsyncWalletAuthResource, AsyncWalletResource, AsyncX402Resource, AuthResource,
18
+ CreditsResource, KeysResource, NetworkResource, NodesResource, PayoutsResource, PayoutWalletResource,
19
+ ProxyResource, SessionsResource, TeamsResource, UsageResource, WalletAuthResource, WalletResource,
20
+ X402Resource, _AuthResource, _Ctx,
21
+ )
22
+ from .token_manager import AsyncTokenManager, TokenManager
23
+ from .validation import validate_api_key
24
+
25
+
26
+ def _connect_gen(ctx: _Ctx, auth: Any, api_key: str) -> Generator[Any, Any, Dict[str, Any]]:
27
+ validate_api_key(api_key)
28
+ try:
29
+ r = yield from _AuthResource.verify_key(auth, api_key)
30
+ except DominusNodeError as err:
31
+ if err.code == "MFA_REQUIRED":
32
+ ctx.api_key = api_key
33
+ return {"mfaRequired": True}
34
+ raise
35
+ ctx.api_key = api_key
36
+ ctx.tokens.set_tokens(r.get("token"), r.get("refreshToken"))
37
+ return {"mfaRequired": False, "userId": r.get("userId")}
38
+
39
+
40
+ def _disconnect_gen(ctx: _Ctx, auth: Any) -> Generator[Any, Any, None]:
41
+ try:
42
+ if ctx.tokens.has_tokens():
43
+ try:
44
+ yield from _AuthResource.logout(auth)
45
+ except Exception: # noqa: BLE001 - best effort
46
+ pass
47
+ finally:
48
+ ctx.api_key = None
49
+ ctx.tokens.clear()
50
+
51
+
52
+ class _ClientBase:
53
+ _ctx: _Ctx
54
+
55
+ @property
56
+ def is_connected(self) -> bool:
57
+ """True when an API key or any JWT is held."""
58
+ return self._ctx.api_key is not None or self._ctx.tokens.has_tokens()
59
+
60
+ def __repr__(self) -> str:
61
+ return f"{type(self).__name__}(connected={'true' if self.is_connected else 'false'})"
62
+
63
+ __str__ = __repr__
64
+
65
+ def to_json(self) -> Dict[str, Any]:
66
+ return {"connected": self.is_connected}
67
+
68
+ def __reduce__(self) -> Any:
69
+ raise TypeError("client cannot be pickled")
70
+
71
+
72
+ class DominusNodeClient(_ClientBase):
73
+ """Synchronous client::
74
+
75
+ with DominusNodeClient() as dn:
76
+ dn.connect_with_key(os.environ["DOMINUSNODE_API_KEY"])
77
+ res = dn.proxy.fetch("https://example.com", country="US")
78
+ """
79
+
80
+ def __init__(
81
+ self,
82
+ base_url: str = DEFAULT_BASE_URL,
83
+ proxy_host: str = DEFAULT_PROXY_HOST,
84
+ http_proxy_port: int = DEFAULT_HTTP_PROXY_PORT,
85
+ socks5_proxy_port: int = DEFAULT_SOCKS5_PROXY_PORT,
86
+ access_token: Optional[str] = None,
87
+ refresh_token: Optional[str] = None,
88
+ transport: Optional[httpx.BaseTransport] = None,
89
+ gateway_tls: Any = None,
90
+ ) -> None:
91
+ tokens = TokenManager()
92
+ self._ctx = ctx = _Ctx(tokens)
93
+ self._http = http = SyncHttpClient(base_url, tokens, lambda: ctx.api_key, transport)
94
+ self.auth = AuthResource(http, ctx)
95
+ self.wallet_auth = WalletAuthResource(http, ctx)
96
+ self.keys = KeysResource(http, ctx)
97
+ self.wallet = WalletResource(http, ctx)
98
+ self.agentic_wallets = AgenticWalletResource(http, ctx)
99
+ self.teams = TeamsResource(http, ctx)
100
+ self.usage = UsageResource(http, ctx)
101
+ self.sessions = SessionsResource(http, ctx)
102
+ self.x402 = X402Resource(http, ctx)
103
+ self.network = NetworkResource(http, ctx)
104
+ self.nodes = NodesResource(http, ctx)
105
+ self.credits = CreditsResource(http, ctx)
106
+ self.payout_wallet = PayoutWalletResource(http, ctx)
107
+ self.payouts = PayoutsResource(http, ctx)
108
+ self.proxy = ProxyResource(http, ctx, proxy_host, http_proxy_port, socks5_proxy_port, gateway_tls)
109
+
110
+ def refresh(rt: str) -> Any:
111
+ r = self.auth.refresh(rt)
112
+ return r.get("token"), r.get("refreshToken")
113
+
114
+ tokens.set_refresh_function(refresh) # never cleared
115
+ if access_token:
116
+ tokens.set_tokens(access_token, refresh_token)
117
+
118
+ def connect_with_key(self, api_key: str) -> Dict[str, Any]:
119
+ """Returns {mfaRequired: False, userId} or {mfaRequired: True} (key-auth routes still work)."""
120
+ return drive_sync(self._http, _connect_gen(self._ctx, self.auth, api_key))
121
+
122
+ def disconnect(self) -> None:
123
+ """Best-effort logout, then clear the API key, access token and refresh token."""
124
+ drive_sync(self._http, _disconnect_gen(self._ctx, self.auth))
125
+
126
+ def close(self) -> None:
127
+ self.disconnect()
128
+ self._http.close()
129
+
130
+ def __enter__(self) -> "DominusNodeClient":
131
+ return self
132
+
133
+ def __exit__(self, *exc: object) -> None:
134
+ self.close()
135
+
136
+
137
+ class AsyncDominusNodeClient(_ClientBase):
138
+ """asyncio client with the same surface; every resource method is a coroutine (signers may be async)."""
139
+
140
+ def __init__(
141
+ self,
142
+ base_url: str = DEFAULT_BASE_URL,
143
+ proxy_host: str = DEFAULT_PROXY_HOST,
144
+ http_proxy_port: int = DEFAULT_HTTP_PROXY_PORT,
145
+ socks5_proxy_port: int = DEFAULT_SOCKS5_PROXY_PORT,
146
+ access_token: Optional[str] = None,
147
+ refresh_token: Optional[str] = None,
148
+ transport: Optional[httpx.AsyncBaseTransport] = None,
149
+ gateway_tls: Any = None,
150
+ ) -> None:
151
+ tokens = AsyncTokenManager()
152
+ self._ctx = ctx = _Ctx(tokens)
153
+ self._http = http = AsyncHttpClient(base_url, tokens, lambda: ctx.api_key, transport)
154
+ self.auth = AsyncAuthResource(http, ctx)
155
+ self.wallet_auth = AsyncWalletAuthResource(http, ctx)
156
+ self.keys = AsyncKeysResource(http, ctx)
157
+ self.wallet = AsyncWalletResource(http, ctx)
158
+ self.agentic_wallets = AsyncAgenticWalletResource(http, ctx)
159
+ self.teams = AsyncTeamsResource(http, ctx)
160
+ self.usage = AsyncUsageResource(http, ctx)
161
+ self.sessions = AsyncSessionsResource(http, ctx)
162
+ self.x402 = AsyncX402Resource(http, ctx)
163
+ self.network = AsyncNetworkResource(http, ctx)
164
+ self.nodes = AsyncNodesResource(http, ctx)
165
+ self.credits = AsyncCreditsResource(http, ctx)
166
+ self.payout_wallet = AsyncPayoutWalletResource(http, ctx)
167
+ self.payouts = AsyncPayoutsResource(http, ctx)
168
+ self.proxy = AsyncProxyResource(http, ctx, proxy_host, http_proxy_port, socks5_proxy_port, gateway_tls)
169
+
170
+ async def refresh(rt: str) -> Any:
171
+ r = await self.auth.refresh(rt)
172
+ return r.get("token"), r.get("refreshToken")
173
+
174
+ tokens.set_refresh_function(refresh)
175
+ if access_token:
176
+ tokens.set_tokens(access_token, refresh_token)
177
+
178
+ async def connect_with_key(self, api_key: str) -> Dict[str, Any]:
179
+ return await drive_async(self._http, _connect_gen(self._ctx, self.auth, api_key))
180
+
181
+ async def disconnect(self) -> None:
182
+ await drive_async(self._http, _disconnect_gen(self._ctx, self.auth))
183
+
184
+ async def close(self) -> None:
185
+ await self.disconnect()
186
+ await self._http.close()
187
+
188
+ async def __aenter__(self) -> "AsyncDominusNodeClient":
189
+ return self
190
+
191
+ async def __aexit__(self, *exc: object) -> None:
192
+ await self.close()
@@ -0,0 +1,24 @@
1
+ """SDK-wide constants (mirrors sdks/node/src/constants.ts)."""
2
+
3
+ SDK_VERSION = "2.0.0"
4
+ USER_AGENT = f"dominusnode-sdk-python/{SDK_VERSION}"
5
+ DEFAULT_BASE_URL = "https://api.dominusnode.daedalusdevelopmentgroup.com"
6
+ DEFAULT_PROXY_HOST = "proxy.dominusnode.daedalusdevelopmentgroup.com"
7
+ DEFAULT_HTTP_PROXY_PORT = 8080
8
+ DEFAULT_SOCKS5_PROXY_PORT = 1080
9
+ #: Gateway HTTPS proxy listener (PROXY_TLS_PORT), used with gateway_tls.
10
+ DEFAULT_TLS_PROXY_PORT = 8443
11
+ #: Refresh JWTs this many seconds before expiry.
12
+ TOKEN_REFRESH_BUFFER_SECONDS = 60
13
+ #: Hard cap on any response body the SDK reads (API or proxy fetch).
14
+ MAX_RESPONSE_BYTES = 10 * 1024 * 1024
15
+ #: API request timeout (seconds).
16
+ REQUEST_TIMEOUT_SECONDS = 30.0
17
+ #: 429 retry: one retry, wait = min(Retry-After, 10s) +/-10% jitter, floor 100ms.
18
+ MAX_RATE_LIMIT_RETRIES = 1
19
+ MAX_RETRY_WAIT_SECONDS = 10.0
20
+ MIN_RETRY_WAIT_SECONDS = 0.1
21
+ #: Largest value accepted for any *_cents argument (PostgreSQL INTEGER max).
22
+ MAX_AMOUNT_CENTS = 2_147_483_647
23
+ #: 1 USD = 1,000,000 µUSD.
24
+ MUSD_PER_USD = 1_000_000
dominusnode/errors.py ADDED
@@ -0,0 +1,153 @@
1
+ """Error hierarchy.
2
+
3
+ Every message passes through :func:`scrub_credentials` so API keys
4
+ (dn_live_/dn_test_/dn_node_/dn_proxy_) and JWTs never reach logs or callers
5
+ via error strings.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import re
11
+ from typing import Dict, Optional, Union
12
+
13
+ _CREDENTIAL_PATTERNS = [
14
+ re.compile(r"dn_(?:live|test|node|proxy)_[A-Za-z0-9_-]+"),
15
+ re.compile(r"eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]*"),
16
+ ]
17
+
18
+ Details = Dict[str, Union[int, float, str]]
19
+
20
+
21
+ def scrub_credentials(message: object) -> str:
22
+ out = str(message)
23
+ for pattern in _CREDENTIAL_PATTERNS:
24
+ out = pattern.sub("[REDACTED]", out)
25
+ return out
26
+
27
+
28
+ class DominusNodeError(Exception):
29
+ """Base class. ``code`` is the server's error code (e.g. ``INVALID_CHALLENGE``)."""
30
+
31
+ default_message = "DominusNode error"
32
+
33
+ def __init__(
34
+ self,
35
+ message: Optional[str] = None,
36
+ status_code: Optional[int] = None,
37
+ code: Optional[str] = None,
38
+ details: Optional[Details] = None,
39
+ ) -> None:
40
+ self.message = scrub_credentials(message if message is not None else self.default_message)
41
+ self.status_code = status_code
42
+ self.code = code
43
+ self.details = details
44
+ super().__init__(self.message)
45
+
46
+ def __str__(self) -> str:
47
+ return self.message
48
+
49
+ def __repr__(self) -> str:
50
+ return f"{type(self).__name__}({self.message!r}, status_code={self.status_code!r}, code={self.code!r})"
51
+
52
+
53
+ class AuthenticationError(DominusNodeError):
54
+ default_message = "Authentication failed"
55
+
56
+ def __init__(self, message: Optional[str] = None, code: Optional[str] = None, details: Optional[Details] = None) -> None:
57
+ super().__init__(message, 401, code, details)
58
+
59
+
60
+ class InsufficientBalanceError(DominusNodeError):
61
+ default_message = "Insufficient balance"
62
+
63
+ def __init__(self, message: Optional[str] = None, code: Optional[str] = None, details: Optional[Details] = None) -> None:
64
+ super().__init__(message, 402, code, details)
65
+
66
+
67
+ class AuthorizationError(DominusNodeError):
68
+ default_message = "Access denied"
69
+
70
+ def __init__(self, message: Optional[str] = None, code: Optional[str] = None, details: Optional[Details] = None) -> None:
71
+ super().__init__(message, 403, code, details)
72
+
73
+
74
+ class NotFoundError(DominusNodeError):
75
+ default_message = "Resource not found"
76
+
77
+ def __init__(self, message: Optional[str] = None, code: Optional[str] = None, details: Optional[Details] = None) -> None:
78
+ super().__init__(message, 404, code, details)
79
+
80
+
81
+ class ConflictError(DominusNodeError):
82
+ default_message = "Resource conflict"
83
+
84
+ def __init__(self, message: Optional[str] = None, code: Optional[str] = None, details: Optional[Details] = None) -> None:
85
+ super().__init__(message, 409, code, details)
86
+
87
+
88
+ class RateLimitError(DominusNodeError):
89
+ default_message = "Rate limit exceeded"
90
+
91
+ def __init__(
92
+ self,
93
+ message: Optional[str] = None,
94
+ retry_after_seconds: int = 60,
95
+ code: Optional[str] = None,
96
+ details: Optional[Details] = None,
97
+ ) -> None:
98
+ super().__init__(message, 429, code, details)
99
+ self.retry_after_seconds = retry_after_seconds
100
+
101
+
102
+ class ServerError(DominusNodeError):
103
+ default_message = "Server error"
104
+
105
+ def __init__(
106
+ self,
107
+ message: Optional[str] = None,
108
+ status_code: int = 500,
109
+ code: Optional[str] = None,
110
+ details: Optional[Details] = None,
111
+ ) -> None:
112
+ super().__init__(message, status_code, code, details)
113
+
114
+
115
+ class ValidationError(DominusNodeError):
116
+ """HTTP 400, or a client-side argument check (status_code None, code INVALID_ARGUMENT)."""
117
+
118
+ default_message = "Validation failed"
119
+
120
+ def __init__(
121
+ self,
122
+ message: Optional[str] = None,
123
+ status_code: Optional[int] = 400,
124
+ code: Optional[str] = None,
125
+ details: Optional[Details] = None,
126
+ ) -> None:
127
+ super().__init__(message, status_code, code, details)
128
+
129
+
130
+ class NetworkError(DominusNodeError):
131
+ default_message = "Network error"
132
+
133
+ def __init__(self, message: Optional[str] = None) -> None:
134
+ super().__init__(message)
135
+
136
+
137
+ class ProtocolError(DominusNodeError):
138
+ """The server sent something the SDK refuses to act on, or a signer failed."""
139
+
140
+ def __init__(self, message: str) -> None:
141
+ super().__init__(message)
142
+
143
+
144
+ class ProxyError(DominusNodeError):
145
+ """Failure inside proxy.fetch(): blocked target, refused tunnel, timeout."""
146
+
147
+ def __init__(self, message: str, status_code: Optional[int] = None) -> None:
148
+ super().__init__(message, status_code)
149
+
150
+
151
+ def invalid_arg(message: str) -> ValidationError:
152
+ """Client-side argument validation failure (no request sent)."""
153
+ return ValidationError(message, None, "INVALID_ARGUMENT")