aotrust-protocol 2.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.
- aotrust_protocol-2.3.0/LICENSE +21 -0
- aotrust_protocol-2.3.0/PKG-INFO +116 -0
- aotrust_protocol-2.3.0/README.md +89 -0
- aotrust_protocol-2.3.0/agent_notary/__init__.py +26 -0
- aotrust_protocol-2.3.0/agent_notary/client.py +153 -0
- aotrust_protocol-2.3.0/agent_notary/exceptions.py +25 -0
- aotrust_protocol-2.3.0/agent_notary/models.py +148 -0
- aotrust_protocol-2.3.0/agent_notary/tools.py +68 -0
- aotrust_protocol-2.3.0/aotrust_protocol.egg-info/PKG-INFO +116 -0
- aotrust_protocol-2.3.0/aotrust_protocol.egg-info/SOURCES.txt +14 -0
- aotrust_protocol-2.3.0/aotrust_protocol.egg-info/dependency_links.txt +1 -0
- aotrust_protocol-2.3.0/aotrust_protocol.egg-info/requires.txt +9 -0
- aotrust_protocol-2.3.0/aotrust_protocol.egg-info/top_level.txt +1 -0
- aotrust_protocol-2.3.0/pyproject.toml +43 -0
- aotrust_protocol-2.3.0/setup.cfg +4 -0
- aotrust_protocol-2.3.0/tests/test_client.py +139 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 A&O Trust Layer
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aotrust-protocol
|
|
3
|
+
Version: 2.3.0
|
|
4
|
+
Summary: Asyncio-native SDK for A&O Trust Layer — Agent Notary Service
|
|
5
|
+
Author: A&O Trust Layer
|
|
6
|
+
License: MIT
|
|
7
|
+
Classifier: Development Status :: 4 - Beta
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Framework :: AsyncIO
|
|
14
|
+
Classifier: Topic :: Security :: Cryptography
|
|
15
|
+
Requires-Python: >=3.10
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Requires-Dist: aiohttp>=3.9
|
|
19
|
+
Requires-Dist: pydantic>=2.0
|
|
20
|
+
Requires-Dist: pynacl>=1.5
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
23
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
24
|
+
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
25
|
+
Requires-Dist: aioresponses>=0.7; extra == "dev"
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
|
|
28
|
+
# aotrust-protocol SDK
|
|
29
|
+
|
|
30
|
+
Asyncio-native Python SDK for the AOTrust Notary API — cryptographic proof-of-existence for AI agent outputs.
|
|
31
|
+
|
|
32
|
+
## Install
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install aotrust-protocol
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Quickstart
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
import asyncio
|
|
42
|
+
from agent_notary import NotaryClient, NotarizeRequest
|
|
43
|
+
|
|
44
|
+
async def main():
|
|
45
|
+
client = NotaryClient(
|
|
46
|
+
api_key="your-api-key",
|
|
47
|
+
base_url="https://api.aotrust.link/v1"
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# Submit notarization
|
|
51
|
+
req = NotarizeRequest(
|
|
52
|
+
tx_hash="EzrfDW5b...",
|
|
53
|
+
work_hash="599d6999...",
|
|
54
|
+
agent_sig="base64_sig_A...",
|
|
55
|
+
agent_pubkey="aff91a18...",
|
|
56
|
+
)
|
|
57
|
+
result = await client.notarize(req)
|
|
58
|
+
print(f"Job: {result.job_id}")
|
|
59
|
+
|
|
60
|
+
# Poll for PDR
|
|
61
|
+
status = await client.wait_for_pdr(result.job_id, timeout=60)
|
|
62
|
+
if status.is_anchored:
|
|
63
|
+
pdr = await client.get_pdr(result.job_id)
|
|
64
|
+
assert pdr.verify(client.notary_pubkey)
|
|
65
|
+
print("PDR Valid: YES")
|
|
66
|
+
|
|
67
|
+
asyncio.run(main())
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Configuration
|
|
71
|
+
|
|
72
|
+
| Env Var | Constructor Arg | Description |
|
|
73
|
+
|---------|-----------------|-------------|
|
|
74
|
+
| `NOTARY_API_KEY` | `api_key` | API key for auth |
|
|
75
|
+
| `NOTARY_API_URL` | `base_url` | API base URL |
|
|
76
|
+
| `NOTARY_PUBKEY` | `notary_pubkey` | Notary Ed25519 public key (hex) |
|
|
77
|
+
|
|
78
|
+
## Endpoints
|
|
79
|
+
|
|
80
|
+
| Method | Path | Description |
|
|
81
|
+
|--------|------|-------------|
|
|
82
|
+
| POST | `/v1/notarize` | Submit notarization |
|
|
83
|
+
| GET | `/v1/status/{job_id}` | Poll job status |
|
|
84
|
+
| GET | `/v1/pdr/{job_id}` | Get PDR |
|
|
85
|
+
| POST | `/v1/notarize/quote` | Get price quote |
|
|
86
|
+
| GET | `/.well-known/agent.json` | MCP Server Card |
|
|
87
|
+
| GET | `/openapi.json` | OpenAPI 3.0 spec |
|
|
88
|
+
|
|
89
|
+
## PDR Verification
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
pdr = await client.get_pdr(job_id)
|
|
93
|
+
is_valid = pdr.verify(notary_pubkey_hex="9c7d64bb...")
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Uses NEP-413 raw-buffer verification. No SHA256 pre-hash.
|
|
97
|
+
|
|
98
|
+
## Error Handling
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
from agent_notary.exceptions import NotaryAuthError, NotaryPaymentError, NotaryNotFoundError
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
result = await client.notarize(req)
|
|
105
|
+
except NotaryAuthError:
|
|
106
|
+
print("Invalid API key")
|
|
107
|
+
except NotaryPaymentError as e:
|
|
108
|
+
print(f"Payment/sig failed: {e}")
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Development
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
pip install -e ".[dev]"
|
|
115
|
+
pytest tests/ -v
|
|
116
|
+
```
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# aotrust-protocol SDK
|
|
2
|
+
|
|
3
|
+
Asyncio-native Python SDK for the AOTrust Notary API — cryptographic proof-of-existence for AI agent outputs.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install aotrust-protocol
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quickstart
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
import asyncio
|
|
15
|
+
from agent_notary import NotaryClient, NotarizeRequest
|
|
16
|
+
|
|
17
|
+
async def main():
|
|
18
|
+
client = NotaryClient(
|
|
19
|
+
api_key="your-api-key",
|
|
20
|
+
base_url="https://api.aotrust.link/v1"
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
# Submit notarization
|
|
24
|
+
req = NotarizeRequest(
|
|
25
|
+
tx_hash="EzrfDW5b...",
|
|
26
|
+
work_hash="599d6999...",
|
|
27
|
+
agent_sig="base64_sig_A...",
|
|
28
|
+
agent_pubkey="aff91a18...",
|
|
29
|
+
)
|
|
30
|
+
result = await client.notarize(req)
|
|
31
|
+
print(f"Job: {result.job_id}")
|
|
32
|
+
|
|
33
|
+
# Poll for PDR
|
|
34
|
+
status = await client.wait_for_pdr(result.job_id, timeout=60)
|
|
35
|
+
if status.is_anchored:
|
|
36
|
+
pdr = await client.get_pdr(result.job_id)
|
|
37
|
+
assert pdr.verify(client.notary_pubkey)
|
|
38
|
+
print("PDR Valid: YES")
|
|
39
|
+
|
|
40
|
+
asyncio.run(main())
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Configuration
|
|
44
|
+
|
|
45
|
+
| Env Var | Constructor Arg | Description |
|
|
46
|
+
|---------|-----------------|-------------|
|
|
47
|
+
| `NOTARY_API_KEY` | `api_key` | API key for auth |
|
|
48
|
+
| `NOTARY_API_URL` | `base_url` | API base URL |
|
|
49
|
+
| `NOTARY_PUBKEY` | `notary_pubkey` | Notary Ed25519 public key (hex) |
|
|
50
|
+
|
|
51
|
+
## Endpoints
|
|
52
|
+
|
|
53
|
+
| Method | Path | Description |
|
|
54
|
+
|--------|------|-------------|
|
|
55
|
+
| POST | `/v1/notarize` | Submit notarization |
|
|
56
|
+
| GET | `/v1/status/{job_id}` | Poll job status |
|
|
57
|
+
| GET | `/v1/pdr/{job_id}` | Get PDR |
|
|
58
|
+
| POST | `/v1/notarize/quote` | Get price quote |
|
|
59
|
+
| GET | `/.well-known/agent.json` | MCP Server Card |
|
|
60
|
+
| GET | `/openapi.json` | OpenAPI 3.0 spec |
|
|
61
|
+
|
|
62
|
+
## PDR Verification
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
pdr = await client.get_pdr(job_id)
|
|
66
|
+
is_valid = pdr.verify(notary_pubkey_hex="9c7d64bb...")
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Uses NEP-413 raw-buffer verification. No SHA256 pre-hash.
|
|
70
|
+
|
|
71
|
+
## Error Handling
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from agent_notary.exceptions import NotaryAuthError, NotaryPaymentError, NotaryNotFoundError
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
result = await client.notarize(req)
|
|
78
|
+
except NotaryAuthError:
|
|
79
|
+
print("Invalid API key")
|
|
80
|
+
except NotaryPaymentError as e:
|
|
81
|
+
print(f"Payment/sig failed: {e}")
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Development
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
pip install -e ".[dev]"
|
|
88
|
+
pytest tests/ -v
|
|
89
|
+
```
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""A&O Trust Layer — Agent Notary SDK (asyncio-native)."""
|
|
2
|
+
|
|
3
|
+
from .client import NotaryClient
|
|
4
|
+
from .models import NotarizeRequest, NotarizeResult, PdrResult, StatusResult
|
|
5
|
+
from .exceptions import (
|
|
6
|
+
NotaryError,
|
|
7
|
+
NotaryAuthError,
|
|
8
|
+
NotaryNotFoundError,
|
|
9
|
+
NotaryPaymentError,
|
|
10
|
+
NotaryServerError,
|
|
11
|
+
)
|
|
12
|
+
from .tools import AOTrustTool
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"NotaryClient",
|
|
16
|
+
"NotarizeRequest",
|
|
17
|
+
"NotarizeResult",
|
|
18
|
+
"PdrResult",
|
|
19
|
+
"StatusResult",
|
|
20
|
+
"NotaryError",
|
|
21
|
+
"NotaryAuthError",
|
|
22
|
+
"NotaryNotFoundError",
|
|
23
|
+
"NotaryPaymentError",
|
|
24
|
+
"NotaryServerError",
|
|
25
|
+
"AOTrustTool",
|
|
26
|
+
]
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""Asyncio-native NotaryClient for A&O Trust Layer."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import os
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
import aiohttp
|
|
10
|
+
|
|
11
|
+
from .models import NotarizeRequest, NotarizeResult, StatusResult, PdrResult
|
|
12
|
+
from .exceptions import (
|
|
13
|
+
NotaryError,
|
|
14
|
+
NotaryAuthError,
|
|
15
|
+
NotaryNotFoundError,
|
|
16
|
+
NotaryPaymentError,
|
|
17
|
+
NotaryServerError,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
DEFAULT_TIMEOUT = 30
|
|
21
|
+
DEFAULT_POLL_INTERVAL = 2.0
|
|
22
|
+
DEFAULT_POLL_TIMEOUT = 120.0
|
|
23
|
+
MAX_RETRIES = 3
|
|
24
|
+
RETRY_DELAYS = (1.0, 2.0, 4.0)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class NotaryClient:
|
|
28
|
+
"""Async client for the A&O Trust Layer Notary API."""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
api_key: Optional[str] = None,
|
|
33
|
+
base_url: Optional[str] = None,
|
|
34
|
+
notary_pubkey: Optional[str] = None,
|
|
35
|
+
):
|
|
36
|
+
self.api_key = api_key or os.environ.get("NOTARY_API_KEY", "")
|
|
37
|
+
self.base_url = (base_url or os.environ.get("NOTARY_API_URL", "https://api.aotrust.link/v1")).rstrip("/")
|
|
38
|
+
self.notary_pubkey = notary_pubkey or os.environ.get("NOTARY_PUBKEY", "9c7d64bbabe90ead8f18982497a47acbd95677cef0d9285caec95377e4ccf6b0")
|
|
39
|
+
|
|
40
|
+
def _headers(self) -> dict:
|
|
41
|
+
h = {"Content-Type": "application/json"}
|
|
42
|
+
if self.api_key:
|
|
43
|
+
h["X-API-Key"] = self.api_key
|
|
44
|
+
return h
|
|
45
|
+
|
|
46
|
+
async def _request(
|
|
47
|
+
self,
|
|
48
|
+
method: str,
|
|
49
|
+
path: str,
|
|
50
|
+
json_data: Optional[dict] = None,
|
|
51
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
52
|
+
) -> tuple[int, dict]:
|
|
53
|
+
url = f"{self.base_url}{path}"
|
|
54
|
+
last_exc = None
|
|
55
|
+
for attempt in range(MAX_RETRIES):
|
|
56
|
+
try:
|
|
57
|
+
async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=timeout)) as session:
|
|
58
|
+
async with session.request(method, url, headers=self._headers(), json=json_data) as resp:
|
|
59
|
+
body = await resp.json(content_type=None) if resp.content_type and "json" in resp.content_type else {}
|
|
60
|
+
if resp.status in (502, 503) and attempt < MAX_RETRIES - 1:
|
|
61
|
+
await asyncio.sleep(RETRY_DELAYS[attempt])
|
|
62
|
+
continue
|
|
63
|
+
return resp.status, body
|
|
64
|
+
except aiohttp.ClientError as exc:
|
|
65
|
+
last_exc = exc
|
|
66
|
+
if attempt < MAX_RETRIES - 1:
|
|
67
|
+
await asyncio.sleep(RETRY_DELAYS[attempt])
|
|
68
|
+
raise NotaryServerError(f"Request failed after {MAX_RETRIES} retries: {last_exc}")
|
|
69
|
+
|
|
70
|
+
def _check_status(self, status: int, body: dict) -> None:
|
|
71
|
+
if status == 401:
|
|
72
|
+
raise NotaryAuthError(body.get("error", "Unauthorized"))
|
|
73
|
+
if status == 403:
|
|
74
|
+
detail = body.get("detail", body.get("error", "Forbidden"))
|
|
75
|
+
raise NotaryPaymentError(detail)
|
|
76
|
+
if status == 404:
|
|
77
|
+
raise NotaryNotFoundError(body.get("error", "Not found"))
|
|
78
|
+
if status == 429:
|
|
79
|
+
raise NotaryError(body.get("error", body.get("message", "Rate limit reached")))
|
|
80
|
+
if status >= 500:
|
|
81
|
+
raise NotaryServerError(body.get("error", f"Server error {status}"), status_code=status)
|
|
82
|
+
|
|
83
|
+
async def notarize(self, request: NotarizeRequest) -> NotarizeResult:
|
|
84
|
+
status, body = await self._request("POST", "/notarize", json_data=request.to_api_payload())
|
|
85
|
+
self._check_status(status, body)
|
|
86
|
+
if status not in (200, 202):
|
|
87
|
+
raise NotaryError(f"Unexpected status {status}: {body}")
|
|
88
|
+
return NotarizeResult(job_id=body.get("job_id", ""))
|
|
89
|
+
|
|
90
|
+
async def get_status(self, job_id: str) -> StatusResult:
|
|
91
|
+
status, body = await self._request("GET", f"/status/{job_id}")
|
|
92
|
+
self._check_status(status, body)
|
|
93
|
+
result = body.get("result", {})
|
|
94
|
+
return StatusResult(
|
|
95
|
+
status=body.get("status", "unknown"),
|
|
96
|
+
pdr_b64=result.get("pdr_b64"),
|
|
97
|
+
payment_hash=result.get("near_tx_hash") or result.get("payment_hash"),
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
async def get_pdr(self, job_id: str) -> PdrResult:
|
|
101
|
+
status, body = await self._request("GET", f"/pdr/{job_id}")
|
|
102
|
+
self._check_status(status, body)
|
|
103
|
+
verify_url = f"{self.base_url}/verify/{job_id}" if "verify_url" not in body else body.get("verify_url")
|
|
104
|
+
return PdrResult(
|
|
105
|
+
pdr_b64=body.get("pdr", body.get("pdr_b64", "")),
|
|
106
|
+
payment_hash=body.get("near_tx_hash") or body.get("payment_hash"),
|
|
107
|
+
verify_url=verify_url,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
async def wait_for_pdr(
|
|
111
|
+
self,
|
|
112
|
+
job_id: str,
|
|
113
|
+
poll_interval: float = DEFAULT_POLL_INTERVAL,
|
|
114
|
+
timeout: float = DEFAULT_POLL_TIMEOUT,
|
|
115
|
+
) -> StatusResult:
|
|
116
|
+
elapsed = 0.0
|
|
117
|
+
while elapsed < timeout:
|
|
118
|
+
result = await self.get_status(job_id)
|
|
119
|
+
if result.is_anchored:
|
|
120
|
+
return result
|
|
121
|
+
if result.status.lower() in ("rejected", "error", "failed"):
|
|
122
|
+
raise NotaryPaymentError(f"Notarization {result.status}")
|
|
123
|
+
await asyncio.sleep(poll_interval)
|
|
124
|
+
elapsed += poll_interval
|
|
125
|
+
raise NotaryServerError(f"Timed out waiting for PDR ({timeout}s)")
|
|
126
|
+
|
|
127
|
+
async def quote(self) -> dict:
|
|
128
|
+
status, body = await self._request("POST", "/notarize/quote")
|
|
129
|
+
self._check_status(status, body)
|
|
130
|
+
return body
|
|
131
|
+
|
|
132
|
+
async def shield_free(self, work_hash: str, agent_sig: str = None, agent_pubkey: str = None) -> dict:
|
|
133
|
+
"""Create a free UNPAID PDR (no wallet required).
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
work_hash: SHA-256 hex string (64 chars)
|
|
137
|
+
agent_sig: Optional base64 Ed25519 signature (for bilateral v0x04)
|
|
138
|
+
agent_pubkey: Optional hex Ed25519 public key
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
dict with: status, job_id, pdr_b64, shield_id, payment_anchor_type, tier
|
|
142
|
+
|
|
143
|
+
Raises:
|
|
144
|
+
NotaryError: on API error or rate limit (429)
|
|
145
|
+
"""
|
|
146
|
+
payload: dict = {"work_hash": work_hash}
|
|
147
|
+
if agent_sig:
|
|
148
|
+
payload["agent_sig"] = agent_sig
|
|
149
|
+
if agent_pubkey:
|
|
150
|
+
payload["agent_pubkey"] = agent_pubkey
|
|
151
|
+
status, body = await self._request("POST", "/shield/free", json_data=payload)
|
|
152
|
+
self._check_status(status, body)
|
|
153
|
+
return body
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Custom exceptions for NotaryClient."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class NotaryError(Exception):
|
|
5
|
+
"""Base exception for all NotaryClient errors."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class NotaryAuthError(NotaryError):
|
|
9
|
+
"""401 Unauthorized — invalid or missing API key."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class NotaryPaymentError(NotaryError):
|
|
13
|
+
"""403 Forbidden — payment verification or agent signature failed."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class NotaryNotFoundError(NotaryError):
|
|
17
|
+
"""404 Not Found — job_id or PDR not found."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class NotaryServerError(NotaryError):
|
|
21
|
+
"""5xx server error with retry context."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, message: str, status_code: int = 0):
|
|
24
|
+
super().__init__(message)
|
|
25
|
+
self.status_code = status_code
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""Pydantic models for NotaryClient request/response schemas."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
import struct
|
|
7
|
+
from enum import IntEnum
|
|
8
|
+
from typing import Optional
|
|
9
|
+
from pydantic import BaseModel, Field, validator
|
|
10
|
+
|
|
11
|
+
NEP413_TAG = 2147484061
|
|
12
|
+
EXTERNAL_PDR_SIZE = 239
|
|
13
|
+
EXTERNAL_PAYLOAD_SIZE = 175
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class PaymentAnchorType(IntEnum):
|
|
17
|
+
UNPAID = 0x00
|
|
18
|
+
NEAR_DIRECT = 0x01
|
|
19
|
+
AGENT_MARKET_SUBMIT = 0x02
|
|
20
|
+
AGENT_MARKET_RELEASE= 0x03
|
|
21
|
+
NEAR_ESCROW_LOCK = 0x04
|
|
22
|
+
X402_BASE = 0x05
|
|
23
|
+
X402_POLYGON = 0x06
|
|
24
|
+
X402_NEAR = 0x07
|
|
25
|
+
CHAIN_SIG_PAYMENT = 0x08
|
|
26
|
+
NEAR_ESCROW_SETTLED = 0x09
|
|
27
|
+
UNKNOWN = 0xFF
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class NotarizeRequest(BaseModel):
|
|
31
|
+
tx_hash: str = Field(..., min_length=32, max_length=128, description="tx hash (NEAR Base58 or EVM hex)")
|
|
32
|
+
work_hash: Optional[str] = Field(None, description="SHA-256 hex digest (64 chars)")
|
|
33
|
+
agent_sig: Optional[str] = Field(None, description="Base64 Ed25519 signature (sig_A)")
|
|
34
|
+
agent_pubkey: Optional[str] = Field(None, description="Hex Ed25519 public key")
|
|
35
|
+
|
|
36
|
+
def to_api_payload(self) -> dict:
|
|
37
|
+
payload: dict = {"tx_hash": self.tx_hash}
|
|
38
|
+
if self.work_hash:
|
|
39
|
+
payload["payload"] = {"work_hash": self.work_hash}
|
|
40
|
+
if self.agent_sig:
|
|
41
|
+
payload["agent_sig"] = self.agent_sig
|
|
42
|
+
if self.agent_pubkey:
|
|
43
|
+
payload["agent_pubkey"] = self.agent_pubkey
|
|
44
|
+
return payload
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class NotarizeResult(BaseModel):
|
|
48
|
+
job_id: str
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class StatusResult(BaseModel):
|
|
52
|
+
status: str
|
|
53
|
+
pdr_b64: Optional[str] = None
|
|
54
|
+
payment_hash: Optional[str] = None
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def is_anchored(self) -> bool:
|
|
58
|
+
return self.status.lower() == "anchored"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class PdrResult(BaseModel):
|
|
62
|
+
pdr_b64: str
|
|
63
|
+
payment_hash: Optional[str] = None
|
|
64
|
+
payment_anchor_type: Optional[int] = None
|
|
65
|
+
format: str = "nep413-dualsig-v1"
|
|
66
|
+
verify_url: Optional[str] = None
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def pdr_bytes(self) -> bytes:
|
|
70
|
+
return base64.b64decode(self.pdr_b64)
|
|
71
|
+
|
|
72
|
+
def _resolve_payment_anchor_type(self) -> None:
|
|
73
|
+
if self.payment_anchor_type is not None:
|
|
74
|
+
return
|
|
75
|
+
raw = self.pdr_bytes
|
|
76
|
+
version = raw[0]
|
|
77
|
+
if version == 0x02:
|
|
78
|
+
self.payment_anchor_type = PaymentAnchorType.NEAR_DIRECT
|
|
79
|
+
elif version in (0x03, 0x04):
|
|
80
|
+
pat_byte = raw[2]
|
|
81
|
+
try:
|
|
82
|
+
self.payment_anchor_type = PaymentAnchorType(pat_byte)
|
|
83
|
+
except ValueError:
|
|
84
|
+
self.payment_anchor_type = PaymentAnchorType.UNKNOWN
|
|
85
|
+
else:
|
|
86
|
+
self.payment_anchor_type = PaymentAnchorType.UNKNOWN
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def payload_bytes(self) -> bytes:
|
|
90
|
+
raw = self.pdr_bytes
|
|
91
|
+
version = raw[0]
|
|
92
|
+
if version == 0x02:
|
|
93
|
+
expected = EXTERNAL_PDR_SIZE + 1 # v2.2 was 238 (one byte less than v2.3's 239)
|
|
94
|
+
payload_size = EXTERNAL_PAYLOAD_SIZE + 1 # v2.2 was 174 (one byte less than v2.3's 175)
|
|
95
|
+
elif version in (0x03, 0x04):
|
|
96
|
+
expected = EXTERNAL_PDR_SIZE
|
|
97
|
+
payload_size = EXTERNAL_PAYLOAD_SIZE
|
|
98
|
+
else:
|
|
99
|
+
raise ValueError(f"Unknown PDR version: {version:#04x}")
|
|
100
|
+
if len(raw) != expected:
|
|
101
|
+
raise ValueError(f"Invalid PDR size: {len(raw)}, expected {expected}")
|
|
102
|
+
return raw[:payload_size]
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def signature_bytes(self) -> bytes:
|
|
106
|
+
raw = self.pdr_bytes
|
|
107
|
+
version = raw[0]
|
|
108
|
+
if version == 0x02:
|
|
109
|
+
payload_size = EXTERNAL_PAYLOAD_SIZE + 1 # v2.2 was 174
|
|
110
|
+
elif version in (0x03, 0x04):
|
|
111
|
+
payload_size = EXTERNAL_PAYLOAD_SIZE
|
|
112
|
+
else:
|
|
113
|
+
payload_size = EXTERNAL_PAYLOAD_SIZE
|
|
114
|
+
return raw[payload_size:]
|
|
115
|
+
|
|
116
|
+
def verify(self, notary_pubkey_hex: str) -> bool:
|
|
117
|
+
from nacl.signing import VerifyKey
|
|
118
|
+
from nacl.exceptions import BadSignatureError
|
|
119
|
+
|
|
120
|
+
raw = self.pdr_bytes
|
|
121
|
+
version = raw[0]
|
|
122
|
+
if version == 0x02:
|
|
123
|
+
expected = EXTERNAL_PDR_SIZE + 1 # v2.2 was 238
|
|
124
|
+
payload_size = EXTERNAL_PAYLOAD_SIZE + 1 # v2.2 was 174
|
|
125
|
+
elif version in (0x03, 0x04):
|
|
126
|
+
expected = EXTERNAL_PDR_SIZE
|
|
127
|
+
payload_size = EXTERNAL_PAYLOAD_SIZE
|
|
128
|
+
else:
|
|
129
|
+
return False
|
|
130
|
+
if len(raw) != expected:
|
|
131
|
+
return False
|
|
132
|
+
|
|
133
|
+
self._resolve_payment_anchor_type()
|
|
134
|
+
|
|
135
|
+
payload = raw[:payload_size]
|
|
136
|
+
signature = raw[payload_size:]
|
|
137
|
+
verify_key = VerifyKey(bytes.fromhex(notary_pubkey_hex))
|
|
138
|
+
nep413_buffer = struct.pack("<II", NEP413_TAG, len(payload)) + payload
|
|
139
|
+
try:
|
|
140
|
+
verify_key.verify(nep413_buffer, signature)
|
|
141
|
+
except BadSignatureError:
|
|
142
|
+
return False
|
|
143
|
+
|
|
144
|
+
if self.payment_anchor_type not in (None, PaymentAnchorType.UNPAID):
|
|
145
|
+
if self.payment_hash is not None and len(self.payment_hash) < 4:
|
|
146
|
+
return False
|
|
147
|
+
|
|
148
|
+
return True
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""AOTrust Tool for LangChain / CrewAI integration.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
from agent_notary.tools import AOTrustTool
|
|
5
|
+
tool = AOTrustTool()
|
|
6
|
+
result = tool.run("my report content")
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import hashlib
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AOTrustTool:
|
|
14
|
+
"""Tool for AI agents to create cryptographic proof of existence for outputs."""
|
|
15
|
+
|
|
16
|
+
name: str = "aotrust_notarize"
|
|
17
|
+
description: str = (
|
|
18
|
+
"Notarize a digital artifact, report, or code to prove its existence "
|
|
19
|
+
"at the current time. Returns a verification URL and Shield ID. "
|
|
20
|
+
"Free tier: 5 requests per day per IP."
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
def __init__(self, api_url: str = "https://api.aotrust.link"):
|
|
24
|
+
self.api_url = api_url
|
|
25
|
+
|
|
26
|
+
def run(self, work_text_or_hash: str) -> str:
|
|
27
|
+
"""Synchronous run — compute hash if needed, notarize, return result string."""
|
|
28
|
+
work_hash = self._resolve_hash(work_text_or_hash)
|
|
29
|
+
try:
|
|
30
|
+
result = asyncio.run(self._notarize(work_hash))
|
|
31
|
+
shield_id = result.get("shield_id", "—")
|
|
32
|
+
verify_url = self._verify_url(result.get("pdr_b64", ""))
|
|
33
|
+
return f"✅ Notarized. Shield ID: {shield_id}\nVerify: {verify_url}"
|
|
34
|
+
except Exception as e:
|
|
35
|
+
return f"❌ Notarization failed: {e}"
|
|
36
|
+
|
|
37
|
+
async def _notarize(self, work_hash: str) -> dict:
|
|
38
|
+
from agent_notary.client import NotaryClient
|
|
39
|
+
client = NotaryClient(base_url=f"{self.api_url}/v1")
|
|
40
|
+
return await client.shield_free(work_hash)
|
|
41
|
+
|
|
42
|
+
async def arun(self, work_text_or_hash: str) -> str:
|
|
43
|
+
"""Async run for LangChain async chains."""
|
|
44
|
+
work_hash = self._resolve_hash(work_text_or_hash)
|
|
45
|
+
try:
|
|
46
|
+
result = await self._notarize(work_hash)
|
|
47
|
+
shield_id = result.get("shield_id", "—")
|
|
48
|
+
verify_url = self._verify_url(result.get("pdr_b64", ""))
|
|
49
|
+
return f"✅ Notarized. Shield ID: {shield_id}\nVerify: {verify_url}"
|
|
50
|
+
except Exception as e:
|
|
51
|
+
return f"❌ Notarization failed: {e}"
|
|
52
|
+
|
|
53
|
+
def _arun(self, work_text_or_hash: str) -> str:
|
|
54
|
+
"""LangChain BaseTool compatibility."""
|
|
55
|
+
return self.arun(work_text_or_hash)
|
|
56
|
+
|
|
57
|
+
@staticmethod
|
|
58
|
+
def _resolve_hash(work_text_or_hash: str) -> str:
|
|
59
|
+
"""If input is 64 hex chars, use as-is. Otherwise compute SHA-256."""
|
|
60
|
+
if len(work_text_or_hash) == 64 and all(c in "0123456789abcdef" for c in work_text_or_hash.lower()):
|
|
61
|
+
return work_text_or_hash
|
|
62
|
+
return hashlib.sha256(work_text_or_hash.encode()).hexdigest()
|
|
63
|
+
|
|
64
|
+
@staticmethod
|
|
65
|
+
def _verify_url(pdr_b64: str) -> str:
|
|
66
|
+
"""Build a verify.aotrust.link URL with base64url-encoded PDR."""
|
|
67
|
+
b64url = pdr_b64.replace("+", "-").replace("/", "_").rstrip("=")
|
|
68
|
+
return f"https://verify.aotrust.link/?pdr={b64url}"
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aotrust-protocol
|
|
3
|
+
Version: 2.3.0
|
|
4
|
+
Summary: Asyncio-native SDK for A&O Trust Layer — Agent Notary Service
|
|
5
|
+
Author: A&O Trust Layer
|
|
6
|
+
License: MIT
|
|
7
|
+
Classifier: Development Status :: 4 - Beta
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Framework :: AsyncIO
|
|
14
|
+
Classifier: Topic :: Security :: Cryptography
|
|
15
|
+
Requires-Python: >=3.10
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Requires-Dist: aiohttp>=3.9
|
|
19
|
+
Requires-Dist: pydantic>=2.0
|
|
20
|
+
Requires-Dist: pynacl>=1.5
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
23
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
24
|
+
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
25
|
+
Requires-Dist: aioresponses>=0.7; extra == "dev"
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
|
|
28
|
+
# aotrust-protocol SDK
|
|
29
|
+
|
|
30
|
+
Asyncio-native Python SDK for the AOTrust Notary API — cryptographic proof-of-existence for AI agent outputs.
|
|
31
|
+
|
|
32
|
+
## Install
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install aotrust-protocol
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Quickstart
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
import asyncio
|
|
42
|
+
from agent_notary import NotaryClient, NotarizeRequest
|
|
43
|
+
|
|
44
|
+
async def main():
|
|
45
|
+
client = NotaryClient(
|
|
46
|
+
api_key="your-api-key",
|
|
47
|
+
base_url="https://api.aotrust.link/v1"
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# Submit notarization
|
|
51
|
+
req = NotarizeRequest(
|
|
52
|
+
tx_hash="EzrfDW5b...",
|
|
53
|
+
work_hash="599d6999...",
|
|
54
|
+
agent_sig="base64_sig_A...",
|
|
55
|
+
agent_pubkey="aff91a18...",
|
|
56
|
+
)
|
|
57
|
+
result = await client.notarize(req)
|
|
58
|
+
print(f"Job: {result.job_id}")
|
|
59
|
+
|
|
60
|
+
# Poll for PDR
|
|
61
|
+
status = await client.wait_for_pdr(result.job_id, timeout=60)
|
|
62
|
+
if status.is_anchored:
|
|
63
|
+
pdr = await client.get_pdr(result.job_id)
|
|
64
|
+
assert pdr.verify(client.notary_pubkey)
|
|
65
|
+
print("PDR Valid: YES")
|
|
66
|
+
|
|
67
|
+
asyncio.run(main())
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Configuration
|
|
71
|
+
|
|
72
|
+
| Env Var | Constructor Arg | Description |
|
|
73
|
+
|---------|-----------------|-------------|
|
|
74
|
+
| `NOTARY_API_KEY` | `api_key` | API key for auth |
|
|
75
|
+
| `NOTARY_API_URL` | `base_url` | API base URL |
|
|
76
|
+
| `NOTARY_PUBKEY` | `notary_pubkey` | Notary Ed25519 public key (hex) |
|
|
77
|
+
|
|
78
|
+
## Endpoints
|
|
79
|
+
|
|
80
|
+
| Method | Path | Description |
|
|
81
|
+
|--------|------|-------------|
|
|
82
|
+
| POST | `/v1/notarize` | Submit notarization |
|
|
83
|
+
| GET | `/v1/status/{job_id}` | Poll job status |
|
|
84
|
+
| GET | `/v1/pdr/{job_id}` | Get PDR |
|
|
85
|
+
| POST | `/v1/notarize/quote` | Get price quote |
|
|
86
|
+
| GET | `/.well-known/agent.json` | MCP Server Card |
|
|
87
|
+
| GET | `/openapi.json` | OpenAPI 3.0 spec |
|
|
88
|
+
|
|
89
|
+
## PDR Verification
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
pdr = await client.get_pdr(job_id)
|
|
93
|
+
is_valid = pdr.verify(notary_pubkey_hex="9c7d64bb...")
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Uses NEP-413 raw-buffer verification. No SHA256 pre-hash.
|
|
97
|
+
|
|
98
|
+
## Error Handling
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
from agent_notary.exceptions import NotaryAuthError, NotaryPaymentError, NotaryNotFoundError
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
result = await client.notarize(req)
|
|
105
|
+
except NotaryAuthError:
|
|
106
|
+
print("Invalid API key")
|
|
107
|
+
except NotaryPaymentError as e:
|
|
108
|
+
print(f"Payment/sig failed: {e}")
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Development
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
pip install -e ".[dev]"
|
|
115
|
+
pytest tests/ -v
|
|
116
|
+
```
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
agent_notary/__init__.py
|
|
5
|
+
agent_notary/client.py
|
|
6
|
+
agent_notary/exceptions.py
|
|
7
|
+
agent_notary/models.py
|
|
8
|
+
agent_notary/tools.py
|
|
9
|
+
aotrust_protocol.egg-info/PKG-INFO
|
|
10
|
+
aotrust_protocol.egg-info/SOURCES.txt
|
|
11
|
+
aotrust_protocol.egg-info/dependency_links.txt
|
|
12
|
+
aotrust_protocol.egg-info/requires.txt
|
|
13
|
+
aotrust_protocol.egg-info/top_level.txt
|
|
14
|
+
tests/test_client.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
agent_notary
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "aotrust-protocol"
|
|
7
|
+
version = "2.3.0"
|
|
8
|
+
description = "Asyncio-native SDK for A&O Trust Layer — Agent Notary Service"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = {text = "MIT"}
|
|
12
|
+
authors = [{name = "A&O Trust Layer"}]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 4 - Beta",
|
|
15
|
+
"Intended Audience :: Developers",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3.10",
|
|
18
|
+
"Programming Language :: Python :: 3.11",
|
|
19
|
+
"Programming Language :: Python :: 3.12",
|
|
20
|
+
"Framework :: AsyncIO",
|
|
21
|
+
"Topic :: Security :: Cryptography",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
dependencies = [
|
|
25
|
+
"aiohttp>=3.9",
|
|
26
|
+
"pydantic>=2.0",
|
|
27
|
+
"pynacl>=1.5",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.optional-dependencies]
|
|
31
|
+
dev = [
|
|
32
|
+
"pytest>=7.0",
|
|
33
|
+
"pytest-asyncio>=0.23",
|
|
34
|
+
"pytest-cov>=4.0",
|
|
35
|
+
"aioresponses>=0.7",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
[tool.setuptools.packages.find]
|
|
39
|
+
include = ["agent_notary*"]
|
|
40
|
+
|
|
41
|
+
[tool.pytest.ini_options]
|
|
42
|
+
asyncio_mode = "auto"
|
|
43
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""Tests for NotaryClient SDK."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
import base64
|
|
6
|
+
import struct
|
|
7
|
+
import pytest
|
|
8
|
+
from unittest.mock import AsyncMock, patch, MagicMock
|
|
9
|
+
|
|
10
|
+
from agent_notary import NotaryClient, NotarizeRequest, PdrResult
|
|
11
|
+
from agent_notary.exceptions import (
|
|
12
|
+
NotaryAuthError,
|
|
13
|
+
NotaryNotFoundError,
|
|
14
|
+
NotaryPaymentError,
|
|
15
|
+
NotaryServerError,
|
|
16
|
+
)
|
|
17
|
+
from agent_notary.models import NEP413_TAG, EXTERNAL_PDR_SIZE, EXTERNAL_PAYLOAD_SIZE
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# ── Fixtures ──────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
MOCK_API_KEY = "test-api-key-12345"
|
|
23
|
+
MOCK_JOB_ID = "fe207267-6568-402d-8ee9-b332dff8b2f9"
|
|
24
|
+
MOCK_NOTARY_PUBKEY = "9c7d64bbabe90ead8f18982497a47acbd95677cef0d9285caec95377e4ccf6b0"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _make_client(**kwargs):
|
|
28
|
+
return NotaryClient(api_key=MOCK_API_KEY, base_url="http://localhost:8741/v1", **kwargs)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _make_real_pdr_b64():
|
|
32
|
+
from nacl.signing import SigningKey
|
|
33
|
+
seed = bytes(range(32))
|
|
34
|
+
sk = SigningKey(seed)
|
|
35
|
+
payload = b'\x03\x01\x00' + b'\x00' * (EXTERNAL_PAYLOAD_SIZE - 3)
|
|
36
|
+
nep413_buffer = struct.pack("<II", NEP413_TAG, len(payload)) + payload
|
|
37
|
+
sig = sk.sign(nep413_buffer).signature
|
|
38
|
+
return base64.b64encode(payload + sig).decode()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# ── NotarizeRequest ──────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
class TestNotarizeRequest:
|
|
44
|
+
def test_minimal_payload(self):
|
|
45
|
+
req = NotarizeRequest(tx_hash="EzrfDW5bA3KqkHeMq4J5hXKBq8MD7XDjhGg8RzP5HFvY")
|
|
46
|
+
payload = req.to_api_payload()
|
|
47
|
+
assert payload["tx_hash"] == "EzrfDW5bA3KqkqHeMq4J5hXKBq8MD7XDjhGg8RzP5HFvY"[:44] or "tx_hash" in payload
|
|
48
|
+
assert "agent_sig" not in payload
|
|
49
|
+
|
|
50
|
+
def test_full_chain_payload(self):
|
|
51
|
+
req = NotarizeRequest(
|
|
52
|
+
tx_hash="EzrfDW5bA3KqkHeMq4J5hXKBq8MD7XDjhGg8RzP5HFvY",
|
|
53
|
+
work_hash="599d6999abc123" + "0" * 50,
|
|
54
|
+
agent_sig="dGVzdHNpZw==",
|
|
55
|
+
agent_pubkey="aff91a18" + "0" * 56,
|
|
56
|
+
)
|
|
57
|
+
payload = req.to_api_payload()
|
|
58
|
+
assert "payload" in payload
|
|
59
|
+
assert payload["payload"]["work_hash"] == req.work_hash
|
|
60
|
+
assert payload["agent_sig"] == req.agent_sig
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# ── PdrResult verification ───────────────────────────────────
|
|
64
|
+
|
|
65
|
+
class TestPdrResult:
|
|
66
|
+
def test_verify_valid_pdr(self):
|
|
67
|
+
from nacl.signing import SigningKey
|
|
68
|
+
seed = bytes(range(32))
|
|
69
|
+
sk = SigningKey(seed)
|
|
70
|
+
pubkey_hex = bytes(sk.verify_key).hex()
|
|
71
|
+
payload = b'\x03\x01\x00' + b'\x01' * (EXTERNAL_PAYLOAD_SIZE - 3)
|
|
72
|
+
nep413_buffer = struct.pack("<II", NEP413_TAG, len(payload)) + payload
|
|
73
|
+
sig = sk.sign(nep413_buffer).signature
|
|
74
|
+
pdr_bytes = payload + sig
|
|
75
|
+
pdr_b64 = base64.b64encode(pdr_bytes).decode()
|
|
76
|
+
result = PdrResult(pdr_b64=pdr_b64)
|
|
77
|
+
assert result.verify(pubkey_hex) is True
|
|
78
|
+
|
|
79
|
+
def test_verify_invalid_pdr(self):
|
|
80
|
+
pdr_b64 = base64.b64encode(b'\x03\x01\x00' + b'\x00' * (EXTERNAL_PDR_SIZE - 3)).decode()
|
|
81
|
+
result = PdrResult(pdr_b64=pdr_b64)
|
|
82
|
+
assert result.verify(MOCK_NOTARY_PUBKEY) is False
|
|
83
|
+
|
|
84
|
+
def test_verify_wrong_size(self):
|
|
85
|
+
pdr_b64 = base64.b64encode(b'\x00' * 100).decode()
|
|
86
|
+
result = PdrResult(pdr_b64=pdr_b64)
|
|
87
|
+
assert result.verify(MOCK_NOTARY_PUBKEY) is False
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# ── NotaryClient error handling ──────────────────────────────
|
|
91
|
+
|
|
92
|
+
class TestNotaryClientErrors:
|
|
93
|
+
def test_auth_error(self):
|
|
94
|
+
client = _make_client()
|
|
95
|
+
with pytest.raises(NotaryAuthError):
|
|
96
|
+
client._check_status(401, {"error": "Unauthorized"})
|
|
97
|
+
|
|
98
|
+
def test_payment_error(self):
|
|
99
|
+
client = _make_client()
|
|
100
|
+
with pytest.raises(NotaryPaymentError):
|
|
101
|
+
client._check_status(403, {"error": "Payment verification failed"})
|
|
102
|
+
|
|
103
|
+
def test_not_found_error(self):
|
|
104
|
+
client = _make_client()
|
|
105
|
+
with pytest.raises(NotaryNotFoundError):
|
|
106
|
+
client._check_status(404, {"error": "Not found"})
|
|
107
|
+
|
|
108
|
+
def test_server_error(self):
|
|
109
|
+
client = _make_client()
|
|
110
|
+
with pytest.raises(NotaryServerError):
|
|
111
|
+
client._check_status(502, {"error": "Bad gateway"})
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
# ── NotaryClient construction ────────────────────────────────
|
|
115
|
+
|
|
116
|
+
class TestNotaryClientConstruction:
|
|
117
|
+
def test_default_base_url(self):
|
|
118
|
+
client = NotaryClient(api_key="k")
|
|
119
|
+
assert "aotrust.link/v1" in client.base_url
|
|
120
|
+
|
|
121
|
+
def test_custom_base_url(self):
|
|
122
|
+
client = NotaryClient(api_key="k", base_url="http://localhost:9999/v1")
|
|
123
|
+
assert client.base_url == "http://localhost:9999/v1"
|
|
124
|
+
|
|
125
|
+
def test_env_var_fallback(self):
|
|
126
|
+
with patch.dict(os.environ, {"NOTARY_API_KEY": "env-key"}, clear=False):
|
|
127
|
+
client = NotaryClient()
|
|
128
|
+
assert client.api_key == "env-key"
|
|
129
|
+
|
|
130
|
+
def test_headers_include_api_key(self):
|
|
131
|
+
client = NotaryClient(api_key="my-secret")
|
|
132
|
+
assert client._headers()["X-API-Key"] == "my-secret"
|
|
133
|
+
|
|
134
|
+
def test_headers_without_api_key(self):
|
|
135
|
+
client = NotaryClient(api_key="")
|
|
136
|
+
assert "X-API-Key" not in client._headers()
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
import os
|