a2a-protocol-core 0.2.1__tar.gz → 0.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.
- {a2a_protocol_core-0.2.1 → a2a_protocol_core-0.3.0}/CHANGELOG.md +38 -0
- {a2a_protocol_core-0.2.1 → a2a_protocol_core-0.3.0}/PKG-INFO +72 -1
- {a2a_protocol_core-0.2.1 → a2a_protocol_core-0.3.0}/README.md +66 -0
- a2a_protocol_core-0.3.0/examples/attest_byo_tx.py +56 -0
- a2a_protocol_core-0.3.0/examples/screen_and_verify.py +52 -0
- {a2a_protocol_core-0.2.1 → a2a_protocol_core-0.3.0}/pyproject.toml +14 -1
- {a2a_protocol_core-0.2.1 → a2a_protocol_core-0.3.0}/src/a2a_protocol_core/__init__.py +36 -3
- a2a_protocol_core-0.3.0/src/a2a_protocol_core/_retry.py +55 -0
- a2a_protocol_core-0.3.0/src/a2a_protocol_core/attestation_verify.py +264 -0
- {a2a_protocol_core-0.2.1 → a2a_protocol_core-0.3.0}/src/a2a_protocol_core/client.py +38 -5
- a2a_protocol_core-0.3.0/src/a2a_protocol_core/screen.py +198 -0
- {a2a_protocol_core-0.2.1 → a2a_protocol_core-0.3.0}/src/a2a_protocol_core/x402_pay.py +241 -2
- a2a_protocol_core-0.3.0/tests/conftest.py +7 -0
- a2a_protocol_core-0.3.0/tests/test_attestation_verify.py +245 -0
- a2a_protocol_core-0.3.0/tests/test_client.py +141 -0
- a2a_protocol_core-0.3.0/tests/test_pay_dispatch.py +173 -0
- a2a_protocol_core-0.3.0/tests/test_retry.py +74 -0
- a2a_protocol_core-0.3.0/tests/test_screen.py +255 -0
- {a2a_protocol_core-0.2.1 → a2a_protocol_core-0.3.0}/.gitignore +0 -0
- {a2a_protocol_core-0.2.1 → a2a_protocol_core-0.3.0}/LICENSE +0 -0
- {a2a_protocol_core-0.2.1 → a2a_protocol_core-0.3.0}/examples/agent_pay_alias.py +0 -0
- {a2a_protocol_core-0.2.1 → a2a_protocol_core-0.3.0}/examples/trigger_payment_hook.py +0 -0
- {a2a_protocol_core-0.2.1 → a2a_protocol_core-0.3.0}/src/a2a_protocol_core/addressing.py +0 -0
- {a2a_protocol_core-0.2.1 → a2a_protocol_core-0.3.0}/src/a2a_protocol_core/canonical_hash.py +0 -0
- {a2a_protocol_core-0.2.1 → a2a_protocol_core-0.3.0}/src/a2a_protocol_core/py.typed +0 -0
- {a2a_protocol_core-0.2.1 → a2a_protocol_core-0.3.0}/src/a2a_protocol_core/schemas.py +0 -0
- {a2a_protocol_core-0.2.1 → a2a_protocol_core-0.3.0}/src/a2a_protocol_core/semantic_normalizer.py +0 -0
- {a2a_protocol_core-0.2.1 → a2a_protocol_core-0.3.0}/tests/test_addressing.py +0 -0
- {a2a_protocol_core-0.2.1 → a2a_protocol_core-0.3.0}/tests/test_canonical_hash.py +0 -0
- {a2a_protocol_core-0.2.1 → a2a_protocol_core-0.3.0}/tests/test_cross_language_vectors.py +0 -0
- {a2a_protocol_core-0.2.1 → a2a_protocol_core-0.3.0}/tests/test_schemas.py +0 -0
- {a2a_protocol_core-0.2.1 → a2a_protocol_core-0.3.0}/tests/test_semantic_normalizer.py +0 -0
- {a2a_protocol_core-0.2.1 → a2a_protocol_core-0.3.0}/tests/test_x402_pay.py +0 -0
- {a2a_protocol_core-0.2.1 → a2a_protocol_core-0.3.0}/tests/vectors/canonical_vectors.json +0 -0
- {a2a_protocol_core-0.2.1 → a2a_protocol_core-0.3.0}/tests/vectors/generate_vectors.py +0 -0
|
@@ -3,6 +3,44 @@
|
|
|
3
3
|
All notable changes to `a2a-protocol-core` are documented here. This project
|
|
4
4
|
adheres to [Semantic Versioning](https://semver.org/).
|
|
5
5
|
|
|
6
|
+
## [0.3.0] — 2026-07-20
|
|
7
|
+
|
|
8
|
+
The version where the SDK can buy and verify the product end to end: pay DNS of
|
|
9
|
+
Money's own USDC-priced endpoints, screen a counterparty before paying it, and
|
|
10
|
+
cryptographically verify the attestation you paid for.
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `pay_alias_usdc_algorand(...)` — pay a `pay:` alias in USDC on Algorand from
|
|
14
|
+
the caller's own wallet. Signs the USDC transfer leg locally via the OFFICIAL
|
|
15
|
+
x402 client mechanisms (new `[algorand]` extra → `x402-avm[avm]`); the
|
|
16
|
+
facilitator co-signs the fee leg and submits. Omit the amount to pay the
|
|
17
|
+
alias's declared (enforced) price.
|
|
18
|
+
- `pay_alias(...)` — rail-dispatching front door: `currency="XRP"` → XRPL,
|
|
19
|
+
`"USDC"` → Algorand. Deterministic table lookup, no scoring.
|
|
20
|
+
- `screen(...)` / `screen_with_payment_header(...)` / `ScreenResult` — client
|
|
21
|
+
for the paid counterparty screen (`GET /x402/screen/{target}`): pay the
|
|
22
|
+
server-priced fee, receive the OFAC + resolution attestation about a
|
|
23
|
+
caller-named `pay:` alias or raw address. "Screen before you pay," one call.
|
|
24
|
+
- `verify_attestation(...)` — client-side verification of the signed
|
|
25
|
+
attestation's `eddsa-jcs-2022` Data Integrity proof: resolves the issuer's
|
|
26
|
+
`did:web` document, enforces `assertionMethod` authorization, verifies the
|
|
27
|
+
Ed25519 signature (new `[verify]` extra → `cryptography`). JCS
|
|
28
|
+
canonicalization, base58btc, and did:web resolution are pure Python; an
|
|
29
|
+
unsigned attestation FAILS verification by design.
|
|
30
|
+
- `fetch_requirement_header(...)` — raw `PAYMENT-REQUIRED` header fetch (any
|
|
31
|
+
currency, amount optional) for callers driving official x402 clients.
|
|
32
|
+
- Bounded retry with exponential backoff on every idempotent HTTP leg
|
|
33
|
+
(challenge fetch, settle, capabilities, DID-document fetch): connection
|
|
34
|
+
errors and 502/503/504 only, fixed attempt count, no jitter. Settle legs are
|
|
35
|
+
retry-safe because the server checks idempotency before verification. The
|
|
36
|
+
payment-hook POST is never auto-retried — re-send with the same
|
|
37
|
+
`job_id` + `semantic_hash` (the server's idempotency key) instead.
|
|
38
|
+
|
|
39
|
+
### Changed
|
|
40
|
+
- `A2APaymentHookClient` now raises `A2AClientError` (with `status_code` and a
|
|
41
|
+
`body` snippet) instead of a bare `requests.HTTPError`, so agents can branch
|
|
42
|
+
on the failure. `capabilities()` retries transient failures.
|
|
43
|
+
|
|
6
44
|
## [0.2.1] — 2026-07-20
|
|
7
45
|
|
|
8
46
|
Conformance fix for the XRPL exact scheme's field placement. Required before
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: a2a-protocol-core
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Open, deterministic protocol core for the DNS of Money A2A payment surface (FAS-1 pay: addressing, canonical hashing, payment-hook client).
|
|
5
5
|
Project-URL: Homepage, https://dnsofmoney.com
|
|
6
6
|
Project-URL: Specification, https://github.com/dnsofmoney/dns-of-money
|
|
@@ -17,9 +17,14 @@ Classifier: Topic :: Office/Business :: Financial
|
|
|
17
17
|
Requires-Python: >=3.10
|
|
18
18
|
Requires-Dist: pydantic>=2.10.0
|
|
19
19
|
Requires-Dist: requests>=2.31.0
|
|
20
|
+
Provides-Extra: algorand
|
|
21
|
+
Requires-Dist: x402-avm[avm]>=2.0.2; extra == 'algorand'
|
|
20
22
|
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: cryptography>=42.0.0; extra == 'dev'
|
|
21
24
|
Requires-Dist: pytest>=8.3.0; extra == 'dev'
|
|
22
25
|
Requires-Dist: ruff>=0.8.0; extra == 'dev'
|
|
26
|
+
Provides-Extra: verify
|
|
27
|
+
Requires-Dist: cryptography>=42.0.0; extra == 'verify'
|
|
23
28
|
Provides-Extra: xrpl
|
|
24
29
|
Requires-Dist: xrpl-py>=2.5.0; extra == 'xrpl'
|
|
25
30
|
Description-Content-Type: text/markdown
|
|
@@ -53,6 +58,9 @@ Runtime deps are intentionally minimal: `pydantic` and `requests`.
|
|
|
53
58
|
| `canonical_hash` | A2A-008 | metadata- & vocabulary-stable payment-intent hash |
|
|
54
59
|
| `schemas` | A2A-041 | payment-hook request/response wire models |
|
|
55
60
|
| `client` | A2A-041 | `A2APaymentHookClient` over `/v1/a2a/*` |
|
|
61
|
+
| `x402_pay` | x402 | one-call pay: XRP on XRPL (`[xrpl]`) / USDC on Algorand (`[algorand]`) |
|
|
62
|
+
| `screen` | x402 | paid counterparty screen — "screen before you pay" |
|
|
63
|
+
| `attestation_verify` | did:web | verify the attestation's Ed25519 proof (`[verify]`) |
|
|
56
64
|
|
|
57
65
|
## Quick start
|
|
58
66
|
|
|
@@ -124,6 +132,69 @@ result = attest_settled_payment(
|
|
|
124
132
|
DNS of Money **verifies an already-settled transaction and returns metadata** — it
|
|
125
133
|
never holds your keys or your funds.
|
|
126
134
|
|
|
135
|
+
## Pay in USDC on Algorand (the priced endpoints)
|
|
136
|
+
|
|
137
|
+
DNS of Money's own paid endpoints declare their price in USDC on Algorand.
|
|
138
|
+
`pay_alias_usdc_algorand` (or the rail-dispatching `pay_alias`) signs the USDC
|
|
139
|
+
transfer leg locally via the **official x402 client mechanisms** — omit the
|
|
140
|
+
amount and the server quotes its declared, enforced price:
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
pip install "a2a-protocol-core[algorand]"
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
from a2a_protocol_core import pay_alias_usdc_algorand
|
|
148
|
+
|
|
149
|
+
result = pay_alias_usdc_algorand(
|
|
150
|
+
base_url="https://api.dnsofmoney.com",
|
|
151
|
+
alias="pay:dnsofmoney",
|
|
152
|
+
api_key="fas_live_...",
|
|
153
|
+
mnemonic="...your 25-word Algorand mnemonic...", # signs locally, never sent
|
|
154
|
+
)
|
|
155
|
+
print(result.tx_hash, result.summary.verdict)
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Screen before you pay
|
|
159
|
+
|
|
160
|
+
Pay the screening fee, get an OFAC + resolution attestation about a
|
|
161
|
+
**caller-named** target — a `pay:` alias or a raw any-chain address — before you
|
|
162
|
+
send it money:
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
from a2a_protocol_core import screen
|
|
166
|
+
|
|
167
|
+
result = screen(
|
|
168
|
+
base_url="https://api.dnsofmoney.com",
|
|
169
|
+
target="pay:vendor.alpha",
|
|
170
|
+
api_key="fas_live_...",
|
|
171
|
+
algorand_mnemonic="...", # fee is USDC on Algorand (server-priced)
|
|
172
|
+
verify=True, # also check the attestation signature
|
|
173
|
+
)
|
|
174
|
+
if result.verdict == "CLEAR":
|
|
175
|
+
... # proceed to pay the vendor
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Verify the attestation yourself
|
|
179
|
+
|
|
180
|
+
Attestations are W3C Verifiable Credentials signed `eddsa-jcs-2022` by a
|
|
181
|
+
`did:web` issuer. `verify_attestation` resolves the issuer's DID document,
|
|
182
|
+
enforces `assertionMethod` authorization, and checks the Ed25519 signature —
|
|
183
|
+
so the proof chain ends at *your* process, not at TLS:
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
pip install "a2a-protocol-core[verify]"
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
```python
|
|
190
|
+
from a2a_protocol_core import verify_attestation
|
|
191
|
+
|
|
192
|
+
v = verify_attestation(result.attestation, expected_issuer="did:web:dnsofmoney.com")
|
|
193
|
+
assert v.verified
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
An unsigned attestation **fails** verification by design.
|
|
197
|
+
|
|
127
198
|
## Why canonical hashing?
|
|
128
199
|
|
|
129
200
|
Two agents describing the same payment with different words ("send" vs
|
|
@@ -27,6 +27,9 @@ Runtime deps are intentionally minimal: `pydantic` and `requests`.
|
|
|
27
27
|
| `canonical_hash` | A2A-008 | metadata- & vocabulary-stable payment-intent hash |
|
|
28
28
|
| `schemas` | A2A-041 | payment-hook request/response wire models |
|
|
29
29
|
| `client` | A2A-041 | `A2APaymentHookClient` over `/v1/a2a/*` |
|
|
30
|
+
| `x402_pay` | x402 | one-call pay: XRP on XRPL (`[xrpl]`) / USDC on Algorand (`[algorand]`) |
|
|
31
|
+
| `screen` | x402 | paid counterparty screen — "screen before you pay" |
|
|
32
|
+
| `attestation_verify` | did:web | verify the attestation's Ed25519 proof (`[verify]`) |
|
|
30
33
|
|
|
31
34
|
## Quick start
|
|
32
35
|
|
|
@@ -98,6 +101,69 @@ result = attest_settled_payment(
|
|
|
98
101
|
DNS of Money **verifies an already-settled transaction and returns metadata** — it
|
|
99
102
|
never holds your keys or your funds.
|
|
100
103
|
|
|
104
|
+
## Pay in USDC on Algorand (the priced endpoints)
|
|
105
|
+
|
|
106
|
+
DNS of Money's own paid endpoints declare their price in USDC on Algorand.
|
|
107
|
+
`pay_alias_usdc_algorand` (or the rail-dispatching `pay_alias`) signs the USDC
|
|
108
|
+
transfer leg locally via the **official x402 client mechanisms** — omit the
|
|
109
|
+
amount and the server quotes its declared, enforced price:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
pip install "a2a-protocol-core[algorand]"
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
from a2a_protocol_core import pay_alias_usdc_algorand
|
|
117
|
+
|
|
118
|
+
result = pay_alias_usdc_algorand(
|
|
119
|
+
base_url="https://api.dnsofmoney.com",
|
|
120
|
+
alias="pay:dnsofmoney",
|
|
121
|
+
api_key="fas_live_...",
|
|
122
|
+
mnemonic="...your 25-word Algorand mnemonic...", # signs locally, never sent
|
|
123
|
+
)
|
|
124
|
+
print(result.tx_hash, result.summary.verdict)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Screen before you pay
|
|
128
|
+
|
|
129
|
+
Pay the screening fee, get an OFAC + resolution attestation about a
|
|
130
|
+
**caller-named** target — a `pay:` alias or a raw any-chain address — before you
|
|
131
|
+
send it money:
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
from a2a_protocol_core import screen
|
|
135
|
+
|
|
136
|
+
result = screen(
|
|
137
|
+
base_url="https://api.dnsofmoney.com",
|
|
138
|
+
target="pay:vendor.alpha",
|
|
139
|
+
api_key="fas_live_...",
|
|
140
|
+
algorand_mnemonic="...", # fee is USDC on Algorand (server-priced)
|
|
141
|
+
verify=True, # also check the attestation signature
|
|
142
|
+
)
|
|
143
|
+
if result.verdict == "CLEAR":
|
|
144
|
+
... # proceed to pay the vendor
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Verify the attestation yourself
|
|
148
|
+
|
|
149
|
+
Attestations are W3C Verifiable Credentials signed `eddsa-jcs-2022` by a
|
|
150
|
+
`did:web` issuer. `verify_attestation` resolves the issuer's DID document,
|
|
151
|
+
enforces `assertionMethod` authorization, and checks the Ed25519 signature —
|
|
152
|
+
so the proof chain ends at *your* process, not at TLS:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
pip install "a2a-protocol-core[verify]"
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
from a2a_protocol_core import verify_attestation
|
|
160
|
+
|
|
161
|
+
v = verify_attestation(result.attestation, expected_issuer="did:web:dnsofmoney.com")
|
|
162
|
+
assert v.verified
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
An unsigned attestation **fails** verification by design.
|
|
166
|
+
|
|
101
167
|
## Why canonical hashing?
|
|
102
168
|
|
|
103
169
|
Two agents describing the same payment with different words ("send" vs
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Bring-your-own-wallet attestation — no signing extras needed.
|
|
3
|
+
|
|
4
|
+
For agents that already settled the payment through their own wallet stack
|
|
5
|
+
(their own xrpl-py flow, a Coinbase Agentic Wallet, anything that produced a
|
|
6
|
+
validated XRPL tx hash): skip the SDK's signing path entirely and just buy the
|
|
7
|
+
read-only verify + signed attestation for the tx you already made. Base
|
|
8
|
+
install only — no [xrpl], no [algorand].
|
|
9
|
+
|
|
10
|
+
pip install a2a-protocol-core # + [verify] to check the signature
|
|
11
|
+
|
|
12
|
+
Env:
|
|
13
|
+
FAS_API_KEY tenant API key
|
|
14
|
+
SETTLED_TX the already-validated XRPL payment hash
|
|
15
|
+
PAY_ALIAS alias that was paid (default pay:vendor.alpha)
|
|
16
|
+
AMOUNT_XRP the amount that tx paid (default 0.10)
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import os
|
|
22
|
+
import sys
|
|
23
|
+
|
|
24
|
+
from a2a_protocol_core import attest_settled_payment, verify_attestation
|
|
25
|
+
from a2a_protocol_core.attestation_verify import AttestationVerificationError
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def main() -> int:
|
|
29
|
+
api_key = os.getenv("FAS_API_KEY", "").strip()
|
|
30
|
+
tx_hash = os.getenv("SETTLED_TX", "").strip()
|
|
31
|
+
if not api_key or not tx_hash:
|
|
32
|
+
print("set FAS_API_KEY and SETTLED_TX")
|
|
33
|
+
return 2
|
|
34
|
+
|
|
35
|
+
result = attest_settled_payment(
|
|
36
|
+
base_url=os.getenv("FAS_BASE_URL", "https://api.dnsofmoney.com"),
|
|
37
|
+
alias=os.getenv("PAY_ALIAS", "pay:vendor.alpha"),
|
|
38
|
+
amount_xrp=os.getenv("AMOUNT_XRP", "0.10"),
|
|
39
|
+
tx_hash=tx_hash,
|
|
40
|
+
api_key=api_key,
|
|
41
|
+
)
|
|
42
|
+
print(f"settled : {result.settled} (idempotent={result.idempotent})")
|
|
43
|
+
print(f"verdict : {result.summary.verdict}")
|
|
44
|
+
print(f"signed : {result.summary.signed}")
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
v = verify_attestation(result.attestation, expected_issuer="did:web:dnsofmoney.com")
|
|
48
|
+
print(f"verified : {v.verified} by {v.issuer}")
|
|
49
|
+
except AttestationVerificationError as exc:
|
|
50
|
+
print(f"verify : FAILED — {exc}")
|
|
51
|
+
return 1
|
|
52
|
+
return 0
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
if __name__ == "__main__":
|
|
56
|
+
sys.exit(main())
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Screen a counterparty before paying it — and verify the attestation yourself.
|
|
3
|
+
|
|
4
|
+
The end-to-end 0.3.0 flow: pay the screening fee in USDC on Algorand from your
|
|
5
|
+
own wallet (server-priced; you never name the amount), receive the OFAC +
|
|
6
|
+
resolution attestation about the TARGET you named, and check the issuer's
|
|
7
|
+
Ed25519 signature against its did:web document locally.
|
|
8
|
+
|
|
9
|
+
pip install "a2a-protocol-core[algorand,verify]"
|
|
10
|
+
|
|
11
|
+
Env:
|
|
12
|
+
FAS_API_KEY tenant API key (the settle leg requires X-API-Key)
|
|
13
|
+
ALGORAND_MNEMONIC your 25-word payer mnemonic — signs locally, never sent
|
|
14
|
+
SCREEN_TARGET pay: alias or raw address to screen (default below)
|
|
15
|
+
FAS_BASE_URL default https://api.dnsofmoney.com
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import os
|
|
21
|
+
import sys
|
|
22
|
+
|
|
23
|
+
from a2a_protocol_core import screen
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def main() -> int:
|
|
27
|
+
api_key = os.getenv("FAS_API_KEY", "").strip()
|
|
28
|
+
mnemonic = os.getenv("ALGORAND_MNEMONIC", "").strip()
|
|
29
|
+
if not api_key or not mnemonic:
|
|
30
|
+
print("set FAS_API_KEY and ALGORAND_MNEMONIC")
|
|
31
|
+
return 2
|
|
32
|
+
|
|
33
|
+
result = screen(
|
|
34
|
+
base_url=os.getenv("FAS_BASE_URL", "https://api.dnsofmoney.com"),
|
|
35
|
+
target=os.getenv("SCREEN_TARGET", "pay:vendor.alpha"),
|
|
36
|
+
api_key=api_key,
|
|
37
|
+
algorand_mnemonic=mnemonic,
|
|
38
|
+
verify=True, # don't just trust TLS — check the issuer's signature
|
|
39
|
+
expected_issuer="did:web:dnsofmoney.com",
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
print(f"target : {result.target}")
|
|
43
|
+
print(f"verdict : {result.verdict}")
|
|
44
|
+
print(f"fee txid : {result.proof.get('transaction')}")
|
|
45
|
+
print(f"signed : {result.summary.signed}")
|
|
46
|
+
if result.verification:
|
|
47
|
+
print(f"verified : {result.verification.verified} by {result.verification.issuer}")
|
|
48
|
+
return 0 if result.verdict == "CLEAR" else 1
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
if __name__ == "__main__":
|
|
52
|
+
sys.exit(main())
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "a2a-protocol-core"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
8
8
|
description = "Open, deterministic protocol core for the DNS of Money A2A payment surface (FAS-1 pay: addressing, canonical hashing, payment-hook client)."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -36,9 +36,22 @@ Source = "https://github.com/dnsofmoney/a2a-protocol-core"
|
|
|
36
36
|
xrpl = [
|
|
37
37
|
"xrpl-py>=2.5.0",
|
|
38
38
|
]
|
|
39
|
+
# USDC-on-Algorand pay path (pay_alias_usdc_algorand / screen fee): drives the
|
|
40
|
+
# OFFICIAL x402 client mechanisms to build the atomic group — never hand-rolled.
|
|
41
|
+
# x402-avm is GoPlausible's distribution of the x402 client WITH the AVM
|
|
42
|
+
# mechanisms registered (it provides the `x402` import namespace).
|
|
43
|
+
algorand = [
|
|
44
|
+
"x402-avm[avm]>=2.0.2",
|
|
45
|
+
]
|
|
46
|
+
# Attestation signature verification (verify_attestation): Ed25519 only — JCS,
|
|
47
|
+
# base58btc, and did:web resolution are pure Python.
|
|
48
|
+
verify = [
|
|
49
|
+
"cryptography>=42.0.0",
|
|
50
|
+
]
|
|
39
51
|
dev = [
|
|
40
52
|
"pytest>=8.3.0",
|
|
41
53
|
"ruff>=0.8.0",
|
|
54
|
+
"cryptography>=42.0.0",
|
|
42
55
|
]
|
|
43
56
|
|
|
44
57
|
[tool.hatch.build.targets.wheel]
|
|
@@ -19,7 +19,7 @@ from a2a_protocol_core.addressing import (
|
|
|
19
19
|
is_valid_pay_uri,
|
|
20
20
|
)
|
|
21
21
|
from a2a_protocol_core.canonical_hash import compute_canonical_hash
|
|
22
|
-
from a2a_protocol_core.client import A2APaymentHookClient
|
|
22
|
+
from a2a_protocol_core.client import A2AClientError, A2APaymentHookClient
|
|
23
23
|
from a2a_protocol_core.schemas import (
|
|
24
24
|
A2ACapabilities,
|
|
25
25
|
A2APaymentHookRequest,
|
|
@@ -34,19 +34,36 @@ from a2a_protocol_core.semantic_normalizer import (
|
|
|
34
34
|
normalize_action,
|
|
35
35
|
normalize_message,
|
|
36
36
|
)
|
|
37
|
+
from a2a_protocol_core.attestation_verify import (
|
|
38
|
+
AttestationVerification,
|
|
39
|
+
AttestationVerificationError,
|
|
40
|
+
did_web_document_url,
|
|
41
|
+
fetch_did_document,
|
|
42
|
+
verify_attestation,
|
|
43
|
+
)
|
|
44
|
+
from a2a_protocol_core.screen import (
|
|
45
|
+
ScreenResult,
|
|
46
|
+
fetch_screen_requirement_header,
|
|
47
|
+
screen,
|
|
48
|
+
screen_with_payment_header,
|
|
49
|
+
)
|
|
37
50
|
from a2a_protocol_core.x402_pay import (
|
|
38
51
|
AttestationSummary,
|
|
39
52
|
X402PaymentResult,
|
|
40
53
|
X402PayError,
|
|
41
54
|
attest_settled_payment,
|
|
55
|
+
build_avm_payment_header,
|
|
42
56
|
build_x_payment_header,
|
|
43
57
|
decode_payment_required,
|
|
44
58
|
fetch_requirement,
|
|
59
|
+
fetch_requirement_header,
|
|
60
|
+
pay_alias,
|
|
61
|
+
pay_alias_usdc_algorand,
|
|
45
62
|
pay_alias_xrp,
|
|
46
63
|
summarize_attestation,
|
|
47
64
|
)
|
|
48
65
|
|
|
49
|
-
__version__ = "0.
|
|
66
|
+
__version__ = "0.3.0"
|
|
50
67
|
|
|
51
68
|
__all__ = [
|
|
52
69
|
"__version__",
|
|
@@ -70,14 +87,30 @@ __all__ = [
|
|
|
70
87
|
"A2ACapabilities",
|
|
71
88
|
# client
|
|
72
89
|
"A2APaymentHookClient",
|
|
73
|
-
|
|
90
|
+
"A2AClientError",
|
|
91
|
+
# x402 pay-path (one-call; XRP signing needs [xrpl], USDC needs [algorand])
|
|
92
|
+
"pay_alias",
|
|
74
93
|
"pay_alias_xrp",
|
|
94
|
+
"pay_alias_usdc_algorand",
|
|
75
95
|
"attest_settled_payment",
|
|
76
96
|
"fetch_requirement",
|
|
97
|
+
"fetch_requirement_header",
|
|
77
98
|
"decode_payment_required",
|
|
78
99
|
"build_x_payment_header",
|
|
100
|
+
"build_avm_payment_header",
|
|
79
101
|
"summarize_attestation",
|
|
80
102
|
"AttestationSummary",
|
|
81
103
|
"X402PaymentResult",
|
|
82
104
|
"X402PayError",
|
|
105
|
+
# paid counterparty screen ("screen before you pay")
|
|
106
|
+
"screen",
|
|
107
|
+
"screen_with_payment_header",
|
|
108
|
+
"fetch_screen_requirement_header",
|
|
109
|
+
"ScreenResult",
|
|
110
|
+
# attestation verification (signature checking needs the [verify] extra)
|
|
111
|
+
"verify_attestation",
|
|
112
|
+
"fetch_did_document",
|
|
113
|
+
"did_web_document_url",
|
|
114
|
+
"AttestationVerification",
|
|
115
|
+
"AttestationVerificationError",
|
|
83
116
|
]
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Bounded, deterministic retry for idempotent HTTP legs.
|
|
3
|
+
|
|
4
|
+
Agent runtimes call these endpoints over flaky networks; a transient connection
|
|
5
|
+
reset or a 502 from a proxy should not fail a whole pay/screen flow. Retries
|
|
6
|
+
are DELIBERATELY narrow:
|
|
7
|
+
|
|
8
|
+
- GET only, and only legs that are idempotent by contract. The x402 settle leg
|
|
9
|
+
qualifies: the server checks idempotency BEFORE verification, so re-sending
|
|
10
|
+
the same settled tx proof returns the recorded outcome rather than
|
|
11
|
+
double-settling.
|
|
12
|
+
- Connection errors / timeouts and 502/503/504 only. A 4xx or a 500 is a real
|
|
13
|
+
answer and is returned/raised immediately — retrying it would just repeat it.
|
|
14
|
+
- Fixed attempt count, exponential backoff, no jitter — deterministic, like
|
|
15
|
+
everything else in this package.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import time
|
|
21
|
+
from typing import Optional
|
|
22
|
+
|
|
23
|
+
import requests
|
|
24
|
+
|
|
25
|
+
RETRYABLE_STATUS = frozenset({502, 503, 504})
|
|
26
|
+
DEFAULT_RETRIES = 2 # total attempts = retries + 1
|
|
27
|
+
DEFAULT_BACKOFF = 0.5 # seconds; doubles per attempt
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def get_with_retries(
|
|
31
|
+
session: requests.Session,
|
|
32
|
+
url: str,
|
|
33
|
+
*,
|
|
34
|
+
params: Optional[dict] = None,
|
|
35
|
+
headers: Optional[dict] = None,
|
|
36
|
+
timeout: int,
|
|
37
|
+
retries: int = DEFAULT_RETRIES,
|
|
38
|
+
backoff: float = DEFAULT_BACKOFF,
|
|
39
|
+
) -> requests.Response:
|
|
40
|
+
"""GET with bounded retries on transient failures (see module docstring)."""
|
|
41
|
+
attempt = 0
|
|
42
|
+
while True:
|
|
43
|
+
try:
|
|
44
|
+
resp = session.get(url, params=params, headers=headers, timeout=timeout)
|
|
45
|
+
except (requests.ConnectionError, requests.Timeout):
|
|
46
|
+
if attempt >= retries:
|
|
47
|
+
raise
|
|
48
|
+
time.sleep(backoff * (2**attempt))
|
|
49
|
+
attempt += 1
|
|
50
|
+
continue
|
|
51
|
+
if resp.status_code in RETRYABLE_STATUS and attempt < retries:
|
|
52
|
+
time.sleep(backoff * (2**attempt))
|
|
53
|
+
attempt += 1
|
|
54
|
+
continue
|
|
55
|
+
return resp
|