oris-kya-verify 0.1.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.
- oris_kya_verify-0.1.0/.gitignore +36 -0
- oris_kya_verify-0.1.0/PKG-INFO +130 -0
- oris_kya_verify-0.1.0/README.md +99 -0
- oris_kya_verify-0.1.0/oris_kya/__init__.py +38 -0
- oris_kya_verify-0.1.0/oris_kya/bundle.py +89 -0
- oris_kya_verify-0.1.0/oris_kya/eas_reader.py +250 -0
- oris_kya_verify-0.1.0/oris_kya/eip712.py +162 -0
- oris_kya_verify-0.1.0/oris_kya/errors.py +39 -0
- oris_kya_verify-0.1.0/oris_kya/verifier.py +235 -0
- oris_kya_verify-0.1.0/pyproject.toml +53 -0
- oris_kya_verify-0.1.0/tests/test_eas_reader.py +75 -0
- oris_kya_verify-0.1.0/tests/test_eip712.py +60 -0
- oris_kya_verify-0.1.0/tests/test_verifier.py +119 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
node_modules/
|
|
2
|
+
.next/
|
|
3
|
+
dist/
|
|
4
|
+
.turbo/
|
|
5
|
+
*.log
|
|
6
|
+
.env
|
|
7
|
+
.env.local
|
|
8
|
+
.env.production
|
|
9
|
+
__pycache__/
|
|
10
|
+
*.pyc
|
|
11
|
+
.venv/
|
|
12
|
+
*.egg-info/
|
|
13
|
+
.mypy_cache/
|
|
14
|
+
.pytest_cache/
|
|
15
|
+
.coverage
|
|
16
|
+
htmlcov/
|
|
17
|
+
celerybeat-schedule
|
|
18
|
+
|
|
19
|
+
# Docker
|
|
20
|
+
docker/certs/
|
|
21
|
+
docker/data/
|
|
22
|
+
docker/secrets/*.txt
|
|
23
|
+
|
|
24
|
+
# Vault
|
|
25
|
+
.vault-backup/
|
|
26
|
+
|
|
27
|
+
# IDE
|
|
28
|
+
.idea/
|
|
29
|
+
.vscode/
|
|
30
|
+
*.swp
|
|
31
|
+
*.swo
|
|
32
|
+
.DS_Store
|
|
33
|
+
|
|
34
|
+
# Ruff
|
|
35
|
+
.ruff_cache/
|
|
36
|
+
tsconfig.tsbuildinfo
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: oris-kya-verify
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Standalone Know-Your-Agent verifier. Reads on-chain EAS attestations + verifies EIP-712 signatures with zero Oris API dependency.
|
|
5
|
+
Project-URL: Homepage, https://useoris.xyz
|
|
6
|
+
Project-URL: Documentation, https://docs.useoris.xyz/kya-verify
|
|
7
|
+
Project-URL: Repository, https://github.com/fluxaventures/oris
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/fluxaventures/oris/issues
|
|
9
|
+
Author-email: Fluxa Ventures <engineering@fluxa.ventures>
|
|
10
|
+
License: Apache-2.0
|
|
11
|
+
Keywords: agent-payments,attestation,compliance,eas,ethereum,kya
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Security
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Requires-Dist: coincurve>=19
|
|
24
|
+
Requires-Dist: eth-abi>=5
|
|
25
|
+
Requires-Dist: httpx>=0.27
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
29
|
+
Requires-Dist: respx>=0.21; extra == 'dev'
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# oris-kya-verify
|
|
33
|
+
|
|
34
|
+
> Standalone Know-Your-Agent verifier. Reads on-chain EAS
|
|
35
|
+
> attestations and verifies EIP-712 signatures with **zero Oris API
|
|
36
|
+
> dependency**. Drop into any payment gateway, wallet provider, or
|
|
37
|
+
> compliance pipeline to verify the agent on the other side of a
|
|
38
|
+
> transaction.
|
|
39
|
+
|
|
40
|
+
The package implements the public `KYABundle` schema published at
|
|
41
|
+
[`docs.useoris.xyz/spec/kya-bundle-v1.json`](https://docs.useoris.xyz/spec/kya-bundle-v1.json).
|
|
42
|
+
Any KYA attestation issued by Oris on Base, Ethereum, Arbitrum, or
|
|
43
|
+
Optimism mainnet can be verified by this library without ever
|
|
44
|
+
hitting an Oris service.
|
|
45
|
+
|
|
46
|
+
## Why this exists
|
|
47
|
+
|
|
48
|
+
Oris issues two parallel attestations for every Know-Your-Agent
|
|
49
|
+
state transition (promote, demote, suspend):
|
|
50
|
+
|
|
51
|
+
1. An off-chain **EIP-712** signed message recoverable to the
|
|
52
|
+
Oris MPC key.
|
|
53
|
+
2. An on-chain **EAS** attestation on Base + Ethereum (with optional
|
|
54
|
+
replication to Arbitrum / Optimism).
|
|
55
|
+
|
|
56
|
+
A third party can verify either independently. This library wraps
|
|
57
|
+
both checks behind a single `KYAVerifier.verify()` call.
|
|
58
|
+
|
|
59
|
+
## Install
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pip install oris-kya-verify
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Dependencies: `httpx`, `coincurve`, `eth-abi`. No web3.py, no
|
|
66
|
+
Postgres, no Oris SDK.
|
|
67
|
+
|
|
68
|
+
## Usage
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
import asyncio
|
|
72
|
+
from oris_kya import KYAVerifier
|
|
73
|
+
|
|
74
|
+
async def main():
|
|
75
|
+
verifier = KYAVerifier(chain="base")
|
|
76
|
+
bundle = await verifier.verify("did:ethr:8453:0x7f3a9c2d1b4e5f6a7b8c9d0e1f2a3b4c5d6e7c2d")
|
|
77
|
+
|
|
78
|
+
print(f"KYA level: {bundle.kya_level}")
|
|
79
|
+
print(f"Risk score: {bundle.risk_score}/100")
|
|
80
|
+
print(f"Status: {bundle.kya_status}")
|
|
81
|
+
print(f"Attestation UID: {bundle.attestation_uid}")
|
|
82
|
+
print(f"Signed by: {bundle.signer_address}") # Should match the Oris MPC key
|
|
83
|
+
print(f"Issued at: {bundle.issued_at}")
|
|
84
|
+
print(f"Expires at: {bundle.expires_at}")
|
|
85
|
+
print(f"Expired? {bundle.is_expired}")
|
|
86
|
+
|
|
87
|
+
if bundle.kya_level >= 3 and not bundle.is_expired:
|
|
88
|
+
# Authorize the payment.
|
|
89
|
+
pass
|
|
90
|
+
|
|
91
|
+
asyncio.run(main())
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Supported chains
|
|
95
|
+
|
|
96
|
+
| Chain | EAS contract | Default RPC |
|
|
97
|
+
|---|---|---|
|
|
98
|
+
| `base` | `0x4200000000000000000000000000000000000021` | `https://mainnet.base.org` |
|
|
99
|
+
| `ethereum` | `0xA1207F3BBa224E2c9c3c6D5aF63D0eb1582Ce587` | `https://eth.llamarpc.com` |
|
|
100
|
+
| `arbitrum` | `0xbD75f629A22Dc1ceD33dDA0b68c546A1c035c458` | `https://arb1.arbitrum.io/rpc` |
|
|
101
|
+
| `optimism` | `0x4200000000000000000000000000000000000021` | `https://mainnet.optimism.io` |
|
|
102
|
+
|
|
103
|
+
Override `rpc_url` in the `KYAVerifier` constructor to point at
|
|
104
|
+
your own RPC (recommended for production: Alchemy, Infura, Helius,
|
|
105
|
+
etc.).
|
|
106
|
+
|
|
107
|
+
## Error handling
|
|
108
|
+
|
|
109
|
+
The library raises typed exceptions you can catch by category:
|
|
110
|
+
|
|
111
|
+
- `KYANotAttestedError` — no on-chain attestation exists for this agent.
|
|
112
|
+
- `KYAExpiredError` — the latest attestation has passed its `expires_at`.
|
|
113
|
+
- `KYAInvalidSignatureError` — the EIP-712 signature does not recover
|
|
114
|
+
to the expected Oris MPC signer.
|
|
115
|
+
- `KYAVerificationError` — base class for any of the above.
|
|
116
|
+
|
|
117
|
+
Network failures bubble up as `httpx.HTTPError`. The library never
|
|
118
|
+
silently fails: every "yes" answer is cryptographically grounded.
|
|
119
|
+
|
|
120
|
+
## Schema
|
|
121
|
+
|
|
122
|
+
The `KYABundle` returned by `verify()` is a frozen dataclass that
|
|
123
|
+
mirrors the public JSON schema at
|
|
124
|
+
[`docs.useoris.xyz/spec/kya-bundle-v1.json`](https://docs.useoris.xyz/spec/kya-bundle-v1.json).
|
|
125
|
+
Use the schema directly if you are implementing your own verifier
|
|
126
|
+
in a different language.
|
|
127
|
+
|
|
128
|
+
## License
|
|
129
|
+
|
|
130
|
+
Apache-2.0. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# oris-kya-verify
|
|
2
|
+
|
|
3
|
+
> Standalone Know-Your-Agent verifier. Reads on-chain EAS
|
|
4
|
+
> attestations and verifies EIP-712 signatures with **zero Oris API
|
|
5
|
+
> dependency**. Drop into any payment gateway, wallet provider, or
|
|
6
|
+
> compliance pipeline to verify the agent on the other side of a
|
|
7
|
+
> transaction.
|
|
8
|
+
|
|
9
|
+
The package implements the public `KYABundle` schema published at
|
|
10
|
+
[`docs.useoris.xyz/spec/kya-bundle-v1.json`](https://docs.useoris.xyz/spec/kya-bundle-v1.json).
|
|
11
|
+
Any KYA attestation issued by Oris on Base, Ethereum, Arbitrum, or
|
|
12
|
+
Optimism mainnet can be verified by this library without ever
|
|
13
|
+
hitting an Oris service.
|
|
14
|
+
|
|
15
|
+
## Why this exists
|
|
16
|
+
|
|
17
|
+
Oris issues two parallel attestations for every Know-Your-Agent
|
|
18
|
+
state transition (promote, demote, suspend):
|
|
19
|
+
|
|
20
|
+
1. An off-chain **EIP-712** signed message recoverable to the
|
|
21
|
+
Oris MPC key.
|
|
22
|
+
2. An on-chain **EAS** attestation on Base + Ethereum (with optional
|
|
23
|
+
replication to Arbitrum / Optimism).
|
|
24
|
+
|
|
25
|
+
A third party can verify either independently. This library wraps
|
|
26
|
+
both checks behind a single `KYAVerifier.verify()` call.
|
|
27
|
+
|
|
28
|
+
## Install
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install oris-kya-verify
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Dependencies: `httpx`, `coincurve`, `eth-abi`. No web3.py, no
|
|
35
|
+
Postgres, no Oris SDK.
|
|
36
|
+
|
|
37
|
+
## Usage
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
import asyncio
|
|
41
|
+
from oris_kya import KYAVerifier
|
|
42
|
+
|
|
43
|
+
async def main():
|
|
44
|
+
verifier = KYAVerifier(chain="base")
|
|
45
|
+
bundle = await verifier.verify("did:ethr:8453:0x7f3a9c2d1b4e5f6a7b8c9d0e1f2a3b4c5d6e7c2d")
|
|
46
|
+
|
|
47
|
+
print(f"KYA level: {bundle.kya_level}")
|
|
48
|
+
print(f"Risk score: {bundle.risk_score}/100")
|
|
49
|
+
print(f"Status: {bundle.kya_status}")
|
|
50
|
+
print(f"Attestation UID: {bundle.attestation_uid}")
|
|
51
|
+
print(f"Signed by: {bundle.signer_address}") # Should match the Oris MPC key
|
|
52
|
+
print(f"Issued at: {bundle.issued_at}")
|
|
53
|
+
print(f"Expires at: {bundle.expires_at}")
|
|
54
|
+
print(f"Expired? {bundle.is_expired}")
|
|
55
|
+
|
|
56
|
+
if bundle.kya_level >= 3 and not bundle.is_expired:
|
|
57
|
+
# Authorize the payment.
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
asyncio.run(main())
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Supported chains
|
|
64
|
+
|
|
65
|
+
| Chain | EAS contract | Default RPC |
|
|
66
|
+
|---|---|---|
|
|
67
|
+
| `base` | `0x4200000000000000000000000000000000000021` | `https://mainnet.base.org` |
|
|
68
|
+
| `ethereum` | `0xA1207F3BBa224E2c9c3c6D5aF63D0eb1582Ce587` | `https://eth.llamarpc.com` |
|
|
69
|
+
| `arbitrum` | `0xbD75f629A22Dc1ceD33dDA0b68c546A1c035c458` | `https://arb1.arbitrum.io/rpc` |
|
|
70
|
+
| `optimism` | `0x4200000000000000000000000000000000000021` | `https://mainnet.optimism.io` |
|
|
71
|
+
|
|
72
|
+
Override `rpc_url` in the `KYAVerifier` constructor to point at
|
|
73
|
+
your own RPC (recommended for production: Alchemy, Infura, Helius,
|
|
74
|
+
etc.).
|
|
75
|
+
|
|
76
|
+
## Error handling
|
|
77
|
+
|
|
78
|
+
The library raises typed exceptions you can catch by category:
|
|
79
|
+
|
|
80
|
+
- `KYANotAttestedError` — no on-chain attestation exists for this agent.
|
|
81
|
+
- `KYAExpiredError` — the latest attestation has passed its `expires_at`.
|
|
82
|
+
- `KYAInvalidSignatureError` — the EIP-712 signature does not recover
|
|
83
|
+
to the expected Oris MPC signer.
|
|
84
|
+
- `KYAVerificationError` — base class for any of the above.
|
|
85
|
+
|
|
86
|
+
Network failures bubble up as `httpx.HTTPError`. The library never
|
|
87
|
+
silently fails: every "yes" answer is cryptographically grounded.
|
|
88
|
+
|
|
89
|
+
## Schema
|
|
90
|
+
|
|
91
|
+
The `KYABundle` returned by `verify()` is a frozen dataclass that
|
|
92
|
+
mirrors the public JSON schema at
|
|
93
|
+
[`docs.useoris.xyz/spec/kya-bundle-v1.json`](https://docs.useoris.xyz/spec/kya-bundle-v1.json).
|
|
94
|
+
Use the schema directly if you are implementing your own verifier
|
|
95
|
+
in a different language.
|
|
96
|
+
|
|
97
|
+
## License
|
|
98
|
+
|
|
99
|
+
Apache-2.0. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Public API of the oris-kya-verify package.
|
|
2
|
+
|
|
3
|
+
Import surface:
|
|
4
|
+
|
|
5
|
+
from oris_kya import (
|
|
6
|
+
KYAVerifier,
|
|
7
|
+
KYABundle,
|
|
8
|
+
KYAVerificationError,
|
|
9
|
+
KYAExpiredError,
|
|
10
|
+
KYAInvalidSignatureError,
|
|
11
|
+
KYANotAttestedError,
|
|
12
|
+
SCHEMA_VERSION,
|
|
13
|
+
)
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from oris_kya.bundle import KYABundle
|
|
17
|
+
from oris_kya.errors import (
|
|
18
|
+
KYAExpiredError,
|
|
19
|
+
KYAInvalidSignatureError,
|
|
20
|
+
KYANotAttestedError,
|
|
21
|
+
KYAVerificationError,
|
|
22
|
+
)
|
|
23
|
+
from oris_kya.verifier import KYAVerifier
|
|
24
|
+
|
|
25
|
+
SCHEMA_VERSION = "kya-bundle-v1"
|
|
26
|
+
|
|
27
|
+
__version__ = "0.1.0"
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"KYAVerifier",
|
|
31
|
+
"KYABundle",
|
|
32
|
+
"KYAVerificationError",
|
|
33
|
+
"KYAExpiredError",
|
|
34
|
+
"KYAInvalidSignatureError",
|
|
35
|
+
"KYANotAttestedError",
|
|
36
|
+
"SCHEMA_VERSION",
|
|
37
|
+
"__version__",
|
|
38
|
+
]
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""KYABundle dataclass mirroring the public JSON schema.
|
|
2
|
+
|
|
3
|
+
Schema URL: https://docs.useoris.xyz/spec/kya-bundle-v1.json
|
|
4
|
+
Schema version: kya-bundle-v1
|
|
5
|
+
|
|
6
|
+
A bundle is the result of a successful verification. It contains
|
|
7
|
+
everything a downstream caller needs to decide whether to allow a
|
|
8
|
+
payment: the agent's KYA level (0-4), the latest composite risk
|
|
9
|
+
score (0-100), the validity window, the on-chain attestation UID
|
|
10
|
+
that proves provenance, and the recovered signer address that
|
|
11
|
+
proves authenticity.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from datetime import datetime, timezone
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True, slots=True)
|
|
22
|
+
class KYABundle:
|
|
23
|
+
"""Verified Know-Your-Agent state for one agent.
|
|
24
|
+
|
|
25
|
+
Frozen + slotted: cheap to pass around, deterministic equality,
|
|
26
|
+
safe to use as a dict key. Construct via KYAVerifier.verify().
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
# ── Identity ────────────────────────────────────────────────────
|
|
30
|
+
did: str # did:ethr:{chain_id}:{address}
|
|
31
|
+
agent_id_hex: str # 0x-prefixed 64-char hex of the agent UID
|
|
32
|
+
chain: str # base | ethereum | arbitrum | optimism
|
|
33
|
+
|
|
34
|
+
# ── KYA verdict ─────────────────────────────────────────────────
|
|
35
|
+
kya_level: int # 0..4
|
|
36
|
+
kya_status: int # 0=pending, 1=verified, 2=suspended, 3=revoked
|
|
37
|
+
risk_score: int # 0..100
|
|
38
|
+
|
|
39
|
+
# ── Provenance ──────────────────────────────────────────────────
|
|
40
|
+
attestation_uid: str # on-chain EAS UID (0x-prefixed 32-byte hex)
|
|
41
|
+
signer_address: str # recovered from the EIP-712 signature
|
|
42
|
+
issued_at: datetime
|
|
43
|
+
expires_at: datetime
|
|
44
|
+
|
|
45
|
+
# ── Schema versioning ───────────────────────────────────────────
|
|
46
|
+
schema_version: str = "kya-bundle-v1"
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def is_expired(self) -> bool:
|
|
50
|
+
"""True if the attestation's expires_at is in the past.
|
|
51
|
+
|
|
52
|
+
Comparison is in UTC. Callers should still gate by both
|
|
53
|
+
is_expired and their own business window.
|
|
54
|
+
"""
|
|
55
|
+
return datetime.now(timezone.utc) >= self.expires_at
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def kya_status_label(self) -> str:
|
|
59
|
+
"""Human-readable status name (verified / pending / etc.)."""
|
|
60
|
+
return _STATUS_LABELS.get(self.kya_status, "unknown")
|
|
61
|
+
|
|
62
|
+
def to_dict(self) -> dict[str, Any]:
|
|
63
|
+
"""JSON-serialisable form.
|
|
64
|
+
|
|
65
|
+
Conforms to kya-bundle-v1.json. Datetimes are ISO-8601 UTC.
|
|
66
|
+
"""
|
|
67
|
+
return {
|
|
68
|
+
"schema_version": self.schema_version,
|
|
69
|
+
"did": self.did,
|
|
70
|
+
"agent_id_hex": self.agent_id_hex,
|
|
71
|
+
"chain": self.chain,
|
|
72
|
+
"kya_level": self.kya_level,
|
|
73
|
+
"kya_status": self.kya_status,
|
|
74
|
+
"kya_status_label": self.kya_status_label,
|
|
75
|
+
"risk_score": self.risk_score,
|
|
76
|
+
"attestation_uid": self.attestation_uid,
|
|
77
|
+
"signer_address": self.signer_address,
|
|
78
|
+
"issued_at": self.issued_at.astimezone(timezone.utc).isoformat(),
|
|
79
|
+
"expires_at": self.expires_at.astimezone(timezone.utc).isoformat(),
|
|
80
|
+
"is_expired": self.is_expired,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
_STATUS_LABELS = {
|
|
85
|
+
0: "pending",
|
|
86
|
+
1: "verified",
|
|
87
|
+
2: "suspended",
|
|
88
|
+
3: "revoked",
|
|
89
|
+
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"""Read EAS attestations over JSON-RPC.
|
|
2
|
+
|
|
3
|
+
Pure-httpx, pure-eth-abi. No web3.py dependency. One `eth_call` to
|
|
4
|
+
the EAS contract's `getAttestation(bytes32 uid)` method, plus the
|
|
5
|
+
attestation lookup by recipient + schema if uid is not known.
|
|
6
|
+
|
|
7
|
+
The encoded data field on the attestation carries the Oris KYA
|
|
8
|
+
schema: `bytes32 agentId, uint8 kyaLevel, uint8 kyaStatus,
|
|
9
|
+
uint32 riskScore, uint64 issuedAt, uint64 expiresAt`.
|
|
10
|
+
|
|
11
|
+
Per-chain EAS contract addresses are pinned to mainnet values
|
|
12
|
+
verified against https://docs.attest.org/docs/quick--start/contracts.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
|
|
19
|
+
import httpx
|
|
20
|
+
from eth_abi import decode as abi_decode
|
|
21
|
+
from eth_abi import encode as abi_encode
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# Canonical mainnet EAS contracts. These are the official deployments.
|
|
25
|
+
EAS_CONTRACTS: dict[str, str] = {
|
|
26
|
+
"base": "0x4200000000000000000000000000000000000021",
|
|
27
|
+
"ethereum": "0xA1207F3BBa224E2c9c3c6D5aF63D0eb1582Ce587",
|
|
28
|
+
"arbitrum": "0xbD75f629A22Dc1ceD33dDA0b68c546A1c035c458",
|
|
29
|
+
"optimism": "0x4200000000000000000000000000000000000021",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
# Default public RPCs. Production users should override via the
|
|
33
|
+
# KYAVerifier(rpc_url=...) constructor for rate-limit headroom.
|
|
34
|
+
DEFAULT_RPC: dict[str, str] = {
|
|
35
|
+
"base": "https://mainnet.base.org",
|
|
36
|
+
"ethereum": "https://eth.llamarpc.com",
|
|
37
|
+
"arbitrum": "https://arb1.arbitrum.io/rpc",
|
|
38
|
+
"optimism": "https://mainnet.optimism.io",
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# EAS chain IDs, used to validate did:ethr:{chain_id}:{address} inputs.
|
|
42
|
+
EAS_CHAIN_IDS: dict[str, int] = {
|
|
43
|
+
"base": 8453,
|
|
44
|
+
"ethereum": 1,
|
|
45
|
+
"arbitrum": 42161,
|
|
46
|
+
"optimism": 10,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# Function selectors. Keccak256 of the canonical signature, first 4 bytes.
|
|
51
|
+
# getAttestation(bytes32) -> 0xa3112a64
|
|
52
|
+
# getAttestationUIDs(address,address,bytes32,bool,uint256,uint256) -> see Oris docs
|
|
53
|
+
#
|
|
54
|
+
# We use the simple getAttestation entry point and require the
|
|
55
|
+
# caller to know the UID. UID lookup by (recipient, schema) is
|
|
56
|
+
# left for a future revision; today the Oris API itself surfaces
|
|
57
|
+
# the latest UID per agent, so external verifiers receive it as
|
|
58
|
+
# part of the agent's published bundle metadata.
|
|
59
|
+
_GET_ATTESTATION_SELECTOR = "0xa3112a64"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass(frozen=True, slots=True)
|
|
63
|
+
class RawAttestation:
|
|
64
|
+
"""Decoded EAS Attestation struct + parsed Oris schema data."""
|
|
65
|
+
|
|
66
|
+
uid: str # 0x-prefixed 32-byte hex
|
|
67
|
+
schema_uid: str
|
|
68
|
+
time: int # unix seconds when attested
|
|
69
|
+
expiration_time: int
|
|
70
|
+
revocation_time: int # 0 if not revoked
|
|
71
|
+
recipient: str
|
|
72
|
+
attester: str
|
|
73
|
+
|
|
74
|
+
# ── Decoded schema data ──
|
|
75
|
+
agent_id_hex: str
|
|
76
|
+
kya_level: int
|
|
77
|
+
kya_status: int
|
|
78
|
+
risk_score: int
|
|
79
|
+
issued_at: int
|
|
80
|
+
expires_at: int
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def is_revoked(self) -> bool:
|
|
84
|
+
return self.revocation_time != 0
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
async def fetch_attestation(
|
|
88
|
+
*,
|
|
89
|
+
uid: str,
|
|
90
|
+
chain: str,
|
|
91
|
+
rpc_url: str | None = None,
|
|
92
|
+
client: httpx.AsyncClient | None = None,
|
|
93
|
+
timeout: float = 8.0,
|
|
94
|
+
) -> RawAttestation | None:
|
|
95
|
+
"""Return the EAS attestation for `uid`, or None if not found.
|
|
96
|
+
|
|
97
|
+
`uid` must be a 0x-prefixed 64-char hex (the 32-byte EAS UID).
|
|
98
|
+
`chain` must be one of `EAS_CONTRACTS.keys()`. `rpc_url` defaults
|
|
99
|
+
to a public RPC; production callers should override.
|
|
100
|
+
|
|
101
|
+
Returns None if the call returns the empty struct (UID never
|
|
102
|
+
attested). Raises httpx.HTTPError on network failure -- the
|
|
103
|
+
caller decides whether to retry.
|
|
104
|
+
"""
|
|
105
|
+
if chain not in EAS_CONTRACTS:
|
|
106
|
+
raise ValueError(f"unsupported chain: {chain}")
|
|
107
|
+
rpc = rpc_url or DEFAULT_RPC[chain]
|
|
108
|
+
contract = EAS_CONTRACTS[chain]
|
|
109
|
+
|
|
110
|
+
# Strip 0x and pad to 32 bytes if needed.
|
|
111
|
+
uid_clean = uid.removeprefix("0x").rjust(64, "0").lower()
|
|
112
|
+
call_data = _GET_ATTESTATION_SELECTOR + uid_clean
|
|
113
|
+
|
|
114
|
+
payload = {
|
|
115
|
+
"jsonrpc": "2.0",
|
|
116
|
+
"id": 1,
|
|
117
|
+
"method": "eth_call",
|
|
118
|
+
"params": [
|
|
119
|
+
{"to": contract, "data": call_data},
|
|
120
|
+
"latest",
|
|
121
|
+
],
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
own_client = client is None
|
|
125
|
+
if own_client:
|
|
126
|
+
client = httpx.AsyncClient(timeout=timeout)
|
|
127
|
+
|
|
128
|
+
try:
|
|
129
|
+
response = await client.post(rpc, json=payload)
|
|
130
|
+
response.raise_for_status()
|
|
131
|
+
body = response.json()
|
|
132
|
+
finally:
|
|
133
|
+
if own_client:
|
|
134
|
+
await client.aclose()
|
|
135
|
+
|
|
136
|
+
if "error" in body:
|
|
137
|
+
raise RuntimeError(f"RPC error: {body['error']}")
|
|
138
|
+
|
|
139
|
+
hex_result = body.get("result", "0x")
|
|
140
|
+
if hex_result == "0x" or not hex_result:
|
|
141
|
+
return None
|
|
142
|
+
|
|
143
|
+
return _decode_attestation(uid_clean, hex_result)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# ABI types for `Attestation` struct returned by getAttestation.
|
|
147
|
+
# Per https://github.com/ethereum-attestation-service/eas-contracts:
|
|
148
|
+
# struct Attestation {
|
|
149
|
+
# bytes32 uid;
|
|
150
|
+
# bytes32 schema;
|
|
151
|
+
# uint64 time;
|
|
152
|
+
# uint64 expirationTime;
|
|
153
|
+
# uint64 revocationTime;
|
|
154
|
+
# bytes32 refUID;
|
|
155
|
+
# address recipient;
|
|
156
|
+
# address attester;
|
|
157
|
+
# bool revocable;
|
|
158
|
+
# bytes data;
|
|
159
|
+
# }
|
|
160
|
+
_ATTESTATION_TUPLE_TYPES = (
|
|
161
|
+
"bytes32", # uid
|
|
162
|
+
"bytes32", # schema
|
|
163
|
+
"uint64", # time
|
|
164
|
+
"uint64", # expirationTime
|
|
165
|
+
"uint64", # revocationTime
|
|
166
|
+
"bytes32", # refUID
|
|
167
|
+
"address", # recipient
|
|
168
|
+
"address", # attester
|
|
169
|
+
"bool", # revocable
|
|
170
|
+
"bytes", # data
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
# ABI types of the Oris KYA schema (encoded in `data`).
|
|
174
|
+
_ORIS_KYA_SCHEMA_TYPES = (
|
|
175
|
+
"bytes32", # agentId
|
|
176
|
+
"uint8", # kyaLevel
|
|
177
|
+
"uint8", # kyaStatus
|
|
178
|
+
"uint32", # riskScore
|
|
179
|
+
"uint64", # issuedAt
|
|
180
|
+
"uint64", # expiresAt
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _decode_attestation(uid_clean: str, hex_result: str) -> RawAttestation | None:
|
|
185
|
+
"""Decode the eth_call return blob into a RawAttestation.
|
|
186
|
+
|
|
187
|
+
Returns None when the EAS contract returns a zero-uid struct
|
|
188
|
+
(UID not found).
|
|
189
|
+
"""
|
|
190
|
+
raw_bytes = bytes.fromhex(hex_result.removeprefix("0x"))
|
|
191
|
+
|
|
192
|
+
# The returned struct is a single tuple wrapped in ABI encoding.
|
|
193
|
+
# eth_abi.decode with a tuple type extracts the fields in order.
|
|
194
|
+
try:
|
|
195
|
+
decoded = abi_decode(
|
|
196
|
+
["(" + ",".join(_ATTESTATION_TUPLE_TYPES) + ")"],
|
|
197
|
+
raw_bytes,
|
|
198
|
+
)
|
|
199
|
+
except Exception:
|
|
200
|
+
return None
|
|
201
|
+
|
|
202
|
+
if not decoded:
|
|
203
|
+
return None
|
|
204
|
+
tup = decoded[0]
|
|
205
|
+
(uid_b, schema_b, time, expiration_time, revocation_time,
|
|
206
|
+
ref_uid_b, recipient, attester, revocable, data_bytes) = tup
|
|
207
|
+
|
|
208
|
+
# Empty attestation: uid is all zeros.
|
|
209
|
+
if int.from_bytes(uid_b, "big") == 0:
|
|
210
|
+
return None
|
|
211
|
+
|
|
212
|
+
# Decode the schema-specific data payload.
|
|
213
|
+
try:
|
|
214
|
+
(agent_id_b, kya_level, kya_status, risk_score,
|
|
215
|
+
issued_at, expires_at) = abi_decode(_ORIS_KYA_SCHEMA_TYPES, data_bytes)
|
|
216
|
+
except Exception:
|
|
217
|
+
return None
|
|
218
|
+
|
|
219
|
+
return RawAttestation(
|
|
220
|
+
uid="0x" + uid_b.hex(),
|
|
221
|
+
schema_uid="0x" + schema_b.hex(),
|
|
222
|
+
time=int(time),
|
|
223
|
+
expiration_time=int(expiration_time),
|
|
224
|
+
revocation_time=int(revocation_time),
|
|
225
|
+
recipient=_address_lower(recipient),
|
|
226
|
+
attester=_address_lower(attester),
|
|
227
|
+
agent_id_hex="0x" + agent_id_b.hex(),
|
|
228
|
+
kya_level=int(kya_level),
|
|
229
|
+
kya_status=int(kya_status),
|
|
230
|
+
risk_score=int(risk_score),
|
|
231
|
+
issued_at=int(issued_at),
|
|
232
|
+
expires_at=int(expires_at),
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _address_lower(addr: str | bytes) -> str:
|
|
237
|
+
"""Normalise an EVM address to 0x-prefixed lowercase hex."""
|
|
238
|
+
if isinstance(addr, bytes):
|
|
239
|
+
return "0x" + addr.hex()
|
|
240
|
+
return addr.lower()
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
# Exported for caller-side schema validation if needed.
|
|
244
|
+
__all__ = [
|
|
245
|
+
"EAS_CONTRACTS",
|
|
246
|
+
"DEFAULT_RPC",
|
|
247
|
+
"EAS_CHAIN_IDS",
|
|
248
|
+
"RawAttestation",
|
|
249
|
+
"fetch_attestation",
|
|
250
|
+
]
|