phion-client 1.52.9__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.
- phion_client-1.52.9/PKG-INFO +49 -0
- phion_client-1.52.9/README.md +24 -0
- phion_client-1.52.9/phion_client/__init__.py +26 -0
- phion_client-1.52.9/phion_client/client.py +178 -0
- phion_client-1.52.9/phion_client.egg-info/PKG-INFO +49 -0
- phion_client-1.52.9/phion_client.egg-info/SOURCES.txt +10 -0
- phion_client-1.52.9/phion_client.egg-info/dependency_links.txt +1 -0
- phion_client-1.52.9/phion_client.egg-info/requires.txt +6 -0
- phion_client-1.52.9/phion_client.egg-info/top_level.txt +1 -0
- phion_client-1.52.9/pyproject.toml +39 -0
- phion_client-1.52.9/setup.cfg +4 -0
- phion_client-1.52.9/tests/test_policy.py +79 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: phion-client
|
|
3
|
+
Version: 1.52.9
|
|
4
|
+
Summary: Budget-safe x402 client for PHION agent services
|
|
5
|
+
Author: PHION Systems
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://phion.systems
|
|
8
|
+
Project-URL: Documentation, https://phion.systems/docs
|
|
9
|
+
Project-URL: Source, https://github.com/NeoNine0/phion-agent-connector
|
|
10
|
+
Project-URL: Issues, https://github.com/NeoNine0/phion-agent-connector/issues
|
|
11
|
+
Keywords: agents,mcp,x402,usdc,phion
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
18
|
+
Requires-Python: >=3.11
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
Requires-Dist: x402<3,>=2.22
|
|
21
|
+
Requires-Dist: eth-account<1,>=0.13
|
|
22
|
+
Requires-Dist: requests<3,>=2.32
|
|
23
|
+
Provides-Extra: test
|
|
24
|
+
Requires-Dist: pytest<9,>=8; extra == "test"
|
|
25
|
+
|
|
26
|
+
# PHION Python client
|
|
27
|
+
|
|
28
|
+
Budget-safe synchronous x402 v2 client. It gets a non-paying quote, validates the
|
|
29
|
+
origin, Base network, official USDC contract, PHION recipient and maximum amount,
|
|
30
|
+
then delegates local signing and retry to the official `x402` SDK.
|
|
31
|
+
|
|
32
|
+
Installing this package never authorizes a payment. A paid call requires an
|
|
33
|
+
explicit `PhionBuyer.call(...)`, a locally supplied wallet key and a
|
|
34
|
+
`SpendingPolicy`. Use a dedicated low-balance wallet and set cumulative, call,
|
|
35
|
+
expiry and service-path limits through `SpendingMandate`.
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
python -m venv .venv
|
|
39
|
+
. .venv/bin/activate
|
|
40
|
+
pip install -e .
|
|
41
|
+
export EVM_PRIVATE_KEY=0x... # dedicated agent wallet; never commit this value
|
|
42
|
+
python example.py
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The example has a hard limit of `0.001 USDC` and verifies the signed result.
|
|
46
|
+
|
|
47
|
+
The public MCP decision surface remains available without this package at
|
|
48
|
+
`https://phion.systems/mcp/decision`; start with `phion_resolve` in `PREFLIGHT`
|
|
49
|
+
mode when payment is not yet authorized.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# PHION Python client
|
|
2
|
+
|
|
3
|
+
Budget-safe synchronous x402 v2 client. It gets a non-paying quote, validates the
|
|
4
|
+
origin, Base network, official USDC contract, PHION recipient and maximum amount,
|
|
5
|
+
then delegates local signing and retry to the official `x402` SDK.
|
|
6
|
+
|
|
7
|
+
Installing this package never authorizes a payment. A paid call requires an
|
|
8
|
+
explicit `PhionBuyer.call(...)`, a locally supplied wallet key and a
|
|
9
|
+
`SpendingPolicy`. Use a dedicated low-balance wallet and set cumulative, call,
|
|
10
|
+
expiry and service-path limits through `SpendingMandate`.
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
python -m venv .venv
|
|
14
|
+
. .venv/bin/activate
|
|
15
|
+
pip install -e .
|
|
16
|
+
export EVM_PRIVATE_KEY=0x... # dedicated agent wallet; never commit this value
|
|
17
|
+
python example.py
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
The example has a hard limit of `0.001 USDC` and verifies the signed result.
|
|
21
|
+
|
|
22
|
+
The public MCP decision surface remains available without this package at
|
|
23
|
+
`https://phion.systems/mcp/decision`; start with `phion_resolve` in `PREFLIGHT`
|
|
24
|
+
mode when payment is not yet authorized.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from .client import (
|
|
2
|
+
BASE_MAINNET,
|
|
3
|
+
BASE_USDC,
|
|
4
|
+
PHION_ORIGIN,
|
|
5
|
+
PHION_RECEIVER,
|
|
6
|
+
PhionBuyer,
|
|
7
|
+
SpendingPolicy,
|
|
8
|
+
SpendingMandate,
|
|
9
|
+
authorize_requirement,
|
|
10
|
+
decode_base64_json,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__version__ = "1.52.9"
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"BASE_MAINNET",
|
|
17
|
+
"BASE_USDC",
|
|
18
|
+
"PHION_ORIGIN",
|
|
19
|
+
"PHION_RECEIVER",
|
|
20
|
+
"PhionBuyer",
|
|
21
|
+
"SpendingPolicy",
|
|
22
|
+
"SpendingMandate",
|
|
23
|
+
"authorize_requirement",
|
|
24
|
+
"decode_base64_json",
|
|
25
|
+
"__version__",
|
|
26
|
+
]
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import json
|
|
5
|
+
import threading
|
|
6
|
+
import time
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from typing import Any
|
|
9
|
+
from urllib.parse import urljoin, urlparse
|
|
10
|
+
|
|
11
|
+
import requests
|
|
12
|
+
|
|
13
|
+
PHION_ORIGIN = "https://phion.systems"
|
|
14
|
+
BASE_MAINNET = "eip155:8453"
|
|
15
|
+
BASE_USDC = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
|
|
16
|
+
PHION_RECEIVER = "0x7BEcA68DD9aC733D4de6b16d16B85Dc391c071e2"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class SpendingPolicy:
|
|
21
|
+
"""All amounts use atomic USDC units: 1,000 = 0.001 USDC."""
|
|
22
|
+
|
|
23
|
+
max_atomic: int
|
|
24
|
+
allowed_origins: tuple[str, ...] = (PHION_ORIGIN,)
|
|
25
|
+
allowed_networks: tuple[str, ...] = (BASE_MAINNET,)
|
|
26
|
+
allowed_assets: tuple[str, ...] = (BASE_USDC,)
|
|
27
|
+
allowed_recipients: tuple[str, ...] = (PHION_RECEIVER,)
|
|
28
|
+
allowed_paths: tuple[str, ...] | None = None
|
|
29
|
+
max_total_atomic: int | None = None
|
|
30
|
+
max_calls: int | None = None
|
|
31
|
+
expires_at: int | None = None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class SpendingMandate:
|
|
35
|
+
"""Thread-safe, conservative in-process authorization budget."""
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
policy: SpendingPolicy,
|
|
40
|
+
*,
|
|
41
|
+
authorized_atomic: int = 0,
|
|
42
|
+
calls: int = 0,
|
|
43
|
+
):
|
|
44
|
+
if authorized_atomic < 0 or calls < 0:
|
|
45
|
+
raise ValueError("Invalid mandate snapshot")
|
|
46
|
+
if policy.max_total_atomic is not None and authorized_atomic > policy.max_total_atomic:
|
|
47
|
+
raise ValueError("Mandate snapshot exceeds cumulative limit")
|
|
48
|
+
self.policy = policy
|
|
49
|
+
self._authorized_atomic = authorized_atomic
|
|
50
|
+
self._calls = calls
|
|
51
|
+
self._lock = threading.Lock()
|
|
52
|
+
|
|
53
|
+
def authorize(self, requirement: dict[str, Any], url: str) -> None:
|
|
54
|
+
path = urlparse(url).path
|
|
55
|
+
with self._lock:
|
|
56
|
+
if self.policy.allowed_paths is not None and path not in self.policy.allowed_paths:
|
|
57
|
+
raise ValueError(f"Service path is not allowed: {path}")
|
|
58
|
+
if self.policy.expires_at is not None and int(time.time()) >= self.policy.expires_at:
|
|
59
|
+
raise ValueError("Spending mandate has expired")
|
|
60
|
+
if self.policy.max_calls is not None and self._calls >= self.policy.max_calls:
|
|
61
|
+
raise ValueError("Spending mandate call limit reached")
|
|
62
|
+
amount = int(requirement["amount"])
|
|
63
|
+
if (
|
|
64
|
+
self.policy.max_total_atomic is not None
|
|
65
|
+
and self._authorized_atomic + amount > self.policy.max_total_atomic
|
|
66
|
+
):
|
|
67
|
+
raise ValueError("Spending mandate cumulative limit exceeded")
|
|
68
|
+
# Reserve before signing. Unknown settlements remain counted to avoid overspend.
|
|
69
|
+
self._authorized_atomic += amount
|
|
70
|
+
self._calls += 1
|
|
71
|
+
|
|
72
|
+
def snapshot(self) -> dict[str, int | None]:
|
|
73
|
+
with self._lock:
|
|
74
|
+
total = self.policy.max_total_atomic
|
|
75
|
+
return {
|
|
76
|
+
"authorized_atomic": self._authorized_atomic,
|
|
77
|
+
"calls": self._calls,
|
|
78
|
+
"remaining_atomic": None if total is None else total - self._authorized_atomic,
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def decode_base64_json(value: str) -> dict[str, Any]:
|
|
83
|
+
padding = "=" * (-len(value) % 4)
|
|
84
|
+
return json.loads(base64.urlsafe_b64decode(value + padding))
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _contains(values: tuple[str, ...], candidate: str) -> bool:
|
|
88
|
+
return candidate.casefold() in {value.casefold() for value in values}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def authorize_requirement(
|
|
92
|
+
challenge: dict[str, Any], url: str, policy: SpendingPolicy
|
|
93
|
+
) -> dict[str, Any]:
|
|
94
|
+
if challenge.get("x402Version") != 2:
|
|
95
|
+
raise ValueError("PHION returned an invalid x402 v2 challenge")
|
|
96
|
+
requirement = next(
|
|
97
|
+
(item for item in challenge.get("accepts", []) if item.get("scheme") == "exact"),
|
|
98
|
+
None,
|
|
99
|
+
)
|
|
100
|
+
if requirement is None:
|
|
101
|
+
raise ValueError("No supported exact payment requirement")
|
|
102
|
+
|
|
103
|
+
parsed = urlparse(url)
|
|
104
|
+
origin = f"{parsed.scheme}://{parsed.netloc}"
|
|
105
|
+
checks = (
|
|
106
|
+
(_contains(policy.allowed_origins, origin), f"Origin is not allowed: {origin}"),
|
|
107
|
+
(_contains(policy.allowed_networks, requirement.get("network", "")), "Network is not allowed"),
|
|
108
|
+
(_contains(policy.allowed_assets, requirement.get("asset", "")), "Asset is not allowed"),
|
|
109
|
+
(_contains(policy.allowed_recipients, requirement.get("payTo", "")), "Recipient is not allowed"),
|
|
110
|
+
)
|
|
111
|
+
for accepted, message in checks:
|
|
112
|
+
if not accepted:
|
|
113
|
+
raise ValueError(message)
|
|
114
|
+
amount_text = requirement.get("amount", "")
|
|
115
|
+
if not isinstance(amount_text, str) or not amount_text.isdigit():
|
|
116
|
+
raise ValueError("Payment amount must be an atomic-unit integer")
|
|
117
|
+
if int(amount_text) > policy.max_atomic:
|
|
118
|
+
raise ValueError(f"Price {amount_text} exceeds limit {policy.max_atomic}")
|
|
119
|
+
return requirement
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class PhionBuyer:
|
|
123
|
+
def __init__(self, private_key: str):
|
|
124
|
+
# Imports stay here so policy validation can be tested without wallet packages.
|
|
125
|
+
from eth_account import Account
|
|
126
|
+
from x402 import x402ClientSync
|
|
127
|
+
from x402.mechanisms.evm import EthAccountSigner
|
|
128
|
+
from x402.mechanisms.evm.exact.register import register_exact_evm_client
|
|
129
|
+
|
|
130
|
+
self._client = x402ClientSync()
|
|
131
|
+
register_exact_evm_client(
|
|
132
|
+
self._client, EthAccountSigner(Account.from_key(private_key))
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
def call(
|
|
136
|
+
self, path: str, body: dict[str, Any], policy: SpendingPolicy | SpendingMandate
|
|
137
|
+
) -> dict[str, Any]:
|
|
138
|
+
from x402.http.clients import x402_requests
|
|
139
|
+
|
|
140
|
+
url = urljoin(f"{PHION_ORIGIN}/", path.lstrip("/"))
|
|
141
|
+
# Quote first without a signer. This request cannot charge.
|
|
142
|
+
quote = requests.post(url, json=body, timeout=30)
|
|
143
|
+
if quote.status_code != 402:
|
|
144
|
+
raise RuntimeError(f"Expected an x402 quote, received HTTP {quote.status_code}")
|
|
145
|
+
encoded = quote.headers.get("PAYMENT-REQUIRED")
|
|
146
|
+
if not encoded:
|
|
147
|
+
raise RuntimeError("Missing PAYMENT-REQUIRED header")
|
|
148
|
+
effective_policy = policy.policy if isinstance(policy, SpendingMandate) else policy
|
|
149
|
+
requirement = authorize_requirement(decode_base64_json(encoded), url, effective_policy)
|
|
150
|
+
if isinstance(policy, SpendingMandate):
|
|
151
|
+
policy.authorize(requirement, url)
|
|
152
|
+
|
|
153
|
+
with x402_requests(self._client) as session:
|
|
154
|
+
response = session.post(url, json=body, timeout=60)
|
|
155
|
+
if not response.ok:
|
|
156
|
+
raise RuntimeError(
|
|
157
|
+
f"Paid PHION call failed with HTTP {response.status_code}: {response.text}"
|
|
158
|
+
)
|
|
159
|
+
settlement_header = response.headers.get("PAYMENT-RESPONSE")
|
|
160
|
+
return {
|
|
161
|
+
"data": response.json(),
|
|
162
|
+
"requirement": requirement,
|
|
163
|
+
"settlement": decode_base64_json(settlement_header)
|
|
164
|
+
if settlement_header
|
|
165
|
+
else None,
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
@staticmethod
|
|
169
|
+
def verify_receipt(
|
|
170
|
+
receipt: dict[str, Any], content: Any, sources: list[dict[str, Any]] | None = None
|
|
171
|
+
) -> bool:
|
|
172
|
+
response = requests.post(
|
|
173
|
+
f"{PHION_ORIGIN}/v1/verify",
|
|
174
|
+
json={"receipt": receipt, "content": content, "sources": sources or []},
|
|
175
|
+
timeout=30,
|
|
176
|
+
)
|
|
177
|
+
response.raise_for_status()
|
|
178
|
+
return response.json().get("valid") is True
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: phion-client
|
|
3
|
+
Version: 1.52.9
|
|
4
|
+
Summary: Budget-safe x402 client for PHION agent services
|
|
5
|
+
Author: PHION Systems
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://phion.systems
|
|
8
|
+
Project-URL: Documentation, https://phion.systems/docs
|
|
9
|
+
Project-URL: Source, https://github.com/NeoNine0/phion-agent-connector
|
|
10
|
+
Project-URL: Issues, https://github.com/NeoNine0/phion-agent-connector/issues
|
|
11
|
+
Keywords: agents,mcp,x402,usdc,phion
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
18
|
+
Requires-Python: >=3.11
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
Requires-Dist: x402<3,>=2.22
|
|
21
|
+
Requires-Dist: eth-account<1,>=0.13
|
|
22
|
+
Requires-Dist: requests<3,>=2.32
|
|
23
|
+
Provides-Extra: test
|
|
24
|
+
Requires-Dist: pytest<9,>=8; extra == "test"
|
|
25
|
+
|
|
26
|
+
# PHION Python client
|
|
27
|
+
|
|
28
|
+
Budget-safe synchronous x402 v2 client. It gets a non-paying quote, validates the
|
|
29
|
+
origin, Base network, official USDC contract, PHION recipient and maximum amount,
|
|
30
|
+
then delegates local signing and retry to the official `x402` SDK.
|
|
31
|
+
|
|
32
|
+
Installing this package never authorizes a payment. A paid call requires an
|
|
33
|
+
explicit `PhionBuyer.call(...)`, a locally supplied wallet key and a
|
|
34
|
+
`SpendingPolicy`. Use a dedicated low-balance wallet and set cumulative, call,
|
|
35
|
+
expiry and service-path limits through `SpendingMandate`.
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
python -m venv .venv
|
|
39
|
+
. .venv/bin/activate
|
|
40
|
+
pip install -e .
|
|
41
|
+
export EVM_PRIVATE_KEY=0x... # dedicated agent wallet; never commit this value
|
|
42
|
+
python example.py
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The example has a hard limit of `0.001 USDC` and verifies the signed result.
|
|
46
|
+
|
|
47
|
+
The public MCP decision surface remains available without this package at
|
|
48
|
+
`https://phion.systems/mcp/decision`; start with `phion_resolve` in `PREFLIGHT`
|
|
49
|
+
mode when payment is not yet authorized.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
phion_client/__init__.py
|
|
4
|
+
phion_client/client.py
|
|
5
|
+
phion_client.egg-info/PKG-INFO
|
|
6
|
+
phion_client.egg-info/SOURCES.txt
|
|
7
|
+
phion_client.egg-info/dependency_links.txt
|
|
8
|
+
phion_client.egg-info/requires.txt
|
|
9
|
+
phion_client.egg-info/top_level.txt
|
|
10
|
+
tests/test_policy.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
phion_client
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "phion-client"
|
|
7
|
+
version = "1.52.9"
|
|
8
|
+
description = "Budget-safe x402 client for PHION agent services"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "Apache-2.0"
|
|
11
|
+
authors = [{ name = "PHION Systems" }]
|
|
12
|
+
requires-python = ">=3.11"
|
|
13
|
+
dependencies = [
|
|
14
|
+
"x402>=2.22,<3",
|
|
15
|
+
"eth-account>=0.13,<1",
|
|
16
|
+
"requests>=2.32,<3"
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
keywords = ["agents", "mcp", "x402", "usdc", "phion"]
|
|
20
|
+
classifiers = [
|
|
21
|
+
"Development Status :: 4 - Beta",
|
|
22
|
+
"Intended Audience :: Developers",
|
|
23
|
+
"Programming Language :: Python :: 3",
|
|
24
|
+
"Programming Language :: Python :: 3.11",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
Homepage = "https://phion.systems"
|
|
31
|
+
Documentation = "https://phion.systems/docs"
|
|
32
|
+
Source = "https://github.com/NeoNine0/phion-agent-connector"
|
|
33
|
+
Issues = "https://github.com/NeoNine0/phion-agent-connector/issues"
|
|
34
|
+
|
|
35
|
+
[project.optional-dependencies]
|
|
36
|
+
test = ["pytest>=8,<9"]
|
|
37
|
+
|
|
38
|
+
[tool.pytest.ini_options]
|
|
39
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from phion_client import (
|
|
4
|
+
BASE_MAINNET,
|
|
5
|
+
BASE_USDC,
|
|
6
|
+
PHION_RECEIVER,
|
|
7
|
+
SpendingPolicy,
|
|
8
|
+
SpendingMandate,
|
|
9
|
+
authorize_requirement,
|
|
10
|
+
__version__,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_package_version_matches_phion_release():
|
|
15
|
+
assert __version__ == "1.52.9"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def challenge():
|
|
19
|
+
return {
|
|
20
|
+
"x402Version": 2,
|
|
21
|
+
"accepts": [{
|
|
22
|
+
"scheme": "exact",
|
|
23
|
+
"network": BASE_MAINNET,
|
|
24
|
+
"asset": BASE_USDC,
|
|
25
|
+
"amount": "1000",
|
|
26
|
+
"payTo": PHION_RECEIVER,
|
|
27
|
+
}],
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_accepts_quote_within_budget():
|
|
32
|
+
result = authorize_requirement(
|
|
33
|
+
challenge(), "https://phion.systems/v1/paid/attest", SpendingPolicy(1000)
|
|
34
|
+
)
|
|
35
|
+
assert result["amount"] == "1000"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_rejects_quote_above_budget():
|
|
39
|
+
with pytest.raises(ValueError, match="exceeds limit"):
|
|
40
|
+
authorize_requirement(
|
|
41
|
+
challenge(), "https://phion.systems/v1/paid/attest", SpendingPolicy(999)
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_rejects_recipient_substitution():
|
|
46
|
+
value = challenge()
|
|
47
|
+
value["accepts"][0]["payTo"] = "0x0000000000000000000000000000000000000001"
|
|
48
|
+
with pytest.raises(ValueError, match="Recipient"):
|
|
49
|
+
authorize_requirement(
|
|
50
|
+
value, "https://phion.systems/v1/paid/attest", SpendingPolicy(1000)
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_rejects_untrusted_origin():
|
|
55
|
+
with pytest.raises(ValueError, match="Origin"):
|
|
56
|
+
authorize_requirement(challenge(), "https://evil.example/attest", SpendingPolicy(1000))
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_mandate_enforces_cumulative_and_service_limits():
|
|
60
|
+
mandate = SpendingMandate(
|
|
61
|
+
SpendingPolicy(
|
|
62
|
+
max_atomic=1_000,
|
|
63
|
+
max_total_atomic=2_000,
|
|
64
|
+
max_calls=2,
|
|
65
|
+
allowed_paths=("/v1/paid/attest",),
|
|
66
|
+
)
|
|
67
|
+
)
|
|
68
|
+
requirement = authorize_requirement(
|
|
69
|
+
challenge(), "https://phion.systems/v1/paid/attest", mandate.policy
|
|
70
|
+
)
|
|
71
|
+
mandate.authorize(requirement, "https://phion.systems/v1/paid/attest")
|
|
72
|
+
mandate.authorize(requirement, "https://phion.systems/v1/paid/attest")
|
|
73
|
+
assert mandate.snapshot() == {
|
|
74
|
+
"authorized_atomic": 2_000,
|
|
75
|
+
"calls": 2,
|
|
76
|
+
"remaining_atomic": 0,
|
|
77
|
+
}
|
|
78
|
+
with pytest.raises(ValueError, match="call limit"):
|
|
79
|
+
mandate.authorize(requirement, "https://phion.systems/v1/paid/attest")
|