aotrust-protocol 2.2.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,24 @@
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
+
13
+ __all__ = [
14
+ "NotaryClient",
15
+ "NotarizeRequest",
16
+ "NotarizeResult",
17
+ "PdrResult",
18
+ "StatusResult",
19
+ "NotaryError",
20
+ "NotaryAuthError",
21
+ "NotaryNotFoundError",
22
+ "NotaryPaymentError",
23
+ "NotaryServerError",
24
+ ]
agent_notary/client.py ADDED
@@ -0,0 +1,128 @@
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 >= 500:
79
+ raise NotaryServerError(body.get("error", f"Server error {status}"), status_code=status)
80
+
81
+ async def notarize(self, request: NotarizeRequest) -> NotarizeResult:
82
+ status, body = await self._request("POST", "/notarize", json_data=request.to_api_payload())
83
+ self._check_status(status, body)
84
+ if status not in (200, 202):
85
+ raise NotaryError(f"Unexpected status {status}: {body}")
86
+ return NotarizeResult(job_id=body.get("job_id", ""))
87
+
88
+ async def get_status(self, job_id: str) -> StatusResult:
89
+ status, body = await self._request("GET", f"/status/{job_id}")
90
+ self._check_status(status, body)
91
+ result = body.get("result", {})
92
+ return StatusResult(
93
+ status=body.get("status", "unknown"),
94
+ pdr_b64=result.get("pdr_b64"),
95
+ payment_hash=result.get("near_tx_hash") or result.get("payment_hash"),
96
+ )
97
+
98
+ async def get_pdr(self, job_id: str) -> PdrResult:
99
+ status, body = await self._request("GET", f"/pdr/{job_id}")
100
+ self._check_status(status, body)
101
+ verify_url = f"{self.base_url}/verify/{job_id}" if "verify_url" not in body else body.get("verify_url")
102
+ return PdrResult(
103
+ pdr_b64=body.get("pdr", body.get("pdr_b64", "")),
104
+ payment_hash=body.get("near_tx_hash") or body.get("payment_hash"),
105
+ verify_url=verify_url,
106
+ )
107
+
108
+ async def wait_for_pdr(
109
+ self,
110
+ job_id: str,
111
+ poll_interval: float = DEFAULT_POLL_INTERVAL,
112
+ timeout: float = DEFAULT_POLL_TIMEOUT,
113
+ ) -> StatusResult:
114
+ elapsed = 0.0
115
+ while elapsed < timeout:
116
+ result = await self.get_status(job_id)
117
+ if result.is_anchored:
118
+ return result
119
+ if result.status.lower() in ("rejected", "error", "failed"):
120
+ raise NotaryPaymentError(f"Notarization {result.status}")
121
+ await asyncio.sleep(poll_interval)
122
+ elapsed += poll_interval
123
+ raise NotaryServerError(f"Timed out waiting for PDR ({timeout}s)")
124
+
125
+ async def quote(self) -> dict:
126
+ status, body = await self._request("POST", "/notarize/quote")
127
+ self._check_status(status, body)
128
+ 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
agent_notary/models.py ADDED
@@ -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,116 @@
1
+ Metadata-Version: 2.4
2
+ Name: aotrust-protocol
3
+ Version: 2.2.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
+ # agent-notary SDK
29
+
30
+ Asyncio-native Python SDK for the A&O Trust Layer Notary API.
31
+
32
+ ## Install
33
+
34
+ ```bash
35
+ pip install agent-notary
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,9 @@
1
+ agent_notary/__init__.py,sha256=XFlyetoH5WTncJ2SrDyJzVFt2z6jWlTTD1UxdAwFc1Q,546
2
+ agent_notary/client.py,sha256=0gEDKRMaqgl22bqAnk-7FhF3age_vx0ahXC9_zm-lCI,5077
3
+ agent_notary/exceptions.py,sha256=YE_sKmK7BHnxTWB4wCUeWIyBgu54DknDoIAQgbW6h5Y,657
4
+ agent_notary/models.py,sha256=w522Co863dflEpTlWQVE9Gs6SGLgNMFTiqVr2OStLu0,4908
5
+ aotrust_protocol-2.2.0.dist-info/licenses/LICENSE,sha256=rbAuFQIJDAkZOMMLJ02qyksduBpI2mg2Y4PD26zgsq4,1071
6
+ aotrust_protocol-2.2.0.dist-info/METADATA,sha256=ihm0fG3COkZD1DtXyHYB4vjOUvrB4Y-g5mrQ8VQt5uM,3036
7
+ aotrust_protocol-2.2.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
8
+ aotrust_protocol-2.2.0.dist-info/top_level.txt,sha256=bKhWjMrwnDELWzmK7yRXR5W-Wjtl55EdgH_ZimS6l7o,13
9
+ aotrust_protocol-2.2.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -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 @@
1
+ agent_notary