a2a-protocol-core 0.1.0__tar.gz → 0.2.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.0/CHANGELOG.md +43 -0
- {a2a_protocol_core-0.1.0 → a2a_protocol_core-0.2.0}/PKG-INFO +44 -1
- {a2a_protocol_core-0.1.0 → a2a_protocol_core-0.2.0}/README.md +41 -0
- a2a_protocol_core-0.2.0/examples/agent_pay_alias.py +87 -0
- {a2a_protocol_core-0.1.0 → a2a_protocol_core-0.2.0}/pyproject.toml +7 -1
- {a2a_protocol_core-0.1.0 → a2a_protocol_core-0.2.0}/src/a2a_protocol_core/__init__.py +22 -1
- a2a_protocol_core-0.2.0/src/a2a_protocol_core/x402_pay.py +292 -0
- a2a_protocol_core-0.2.0/tests/test_cross_language_vectors.py +59 -0
- a2a_protocol_core-0.2.0/tests/test_x402_pay.py +178 -0
- a2a_protocol_core-0.2.0/tests/vectors/canonical_vectors.json +281 -0
- a2a_protocol_core-0.2.0/tests/vectors/generate_vectors.py +156 -0
- a2a_protocol_core-0.1.0/CHANGELOG.md +0 -24
- {a2a_protocol_core-0.1.0 → a2a_protocol_core-0.2.0}/.gitignore +0 -0
- {a2a_protocol_core-0.1.0 → a2a_protocol_core-0.2.0}/LICENSE +0 -0
- {a2a_protocol_core-0.1.0 → a2a_protocol_core-0.2.0}/examples/trigger_payment_hook.py +0 -0
- {a2a_protocol_core-0.1.0 → a2a_protocol_core-0.2.0}/src/a2a_protocol_core/addressing.py +0 -0
- {a2a_protocol_core-0.1.0 → a2a_protocol_core-0.2.0}/src/a2a_protocol_core/canonical_hash.py +0 -0
- {a2a_protocol_core-0.1.0 → a2a_protocol_core-0.2.0}/src/a2a_protocol_core/client.py +0 -0
- {a2a_protocol_core-0.1.0 → a2a_protocol_core-0.2.0}/src/a2a_protocol_core/py.typed +0 -0
- {a2a_protocol_core-0.1.0 → a2a_protocol_core-0.2.0}/src/a2a_protocol_core/schemas.py +0 -0
- {a2a_protocol_core-0.1.0 → a2a_protocol_core-0.2.0}/src/a2a_protocol_core/semantic_normalizer.py +0 -0
- {a2a_protocol_core-0.1.0 → a2a_protocol_core-0.2.0}/tests/test_addressing.py +0 -0
- {a2a_protocol_core-0.1.0 → a2a_protocol_core-0.2.0}/tests/test_canonical_hash.py +0 -0
- {a2a_protocol_core-0.1.0 → a2a_protocol_core-0.2.0}/tests/test_schemas.py +0 -0
- {a2a_protocol_core-0.1.0 → a2a_protocol_core-0.2.0}/tests/test_semantic_normalizer.py +0 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `a2a-protocol-core` are documented here. This project
|
|
4
|
+
adheres to [Semantic Versioning](https://semver.org/).
|
|
5
|
+
|
|
6
|
+
## [0.2.0] — Unreleased
|
|
7
|
+
|
|
8
|
+
Adds the one-call x402 pay-path so an agent can pay a `pay:` alias from its own
|
|
9
|
+
wallet — the "make it actually easy" client, still fully non-custodial.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- `pay_alias_xrp(...)` — resolve the x402 requirement for a `pay:` alias, sign +
|
|
13
|
+
submit the XRP payment **locally with the caller's own seed** (the seed never
|
|
14
|
+
leaves the process), then verify + fetch the signed resolve/verify/OFAC-screen
|
|
15
|
+
attestation. One call, end to end.
|
|
16
|
+
- `attest_settled_payment(...)` — bring-your-own already-settled tx hash (e.g. an
|
|
17
|
+
agent paying through its own wallet stack / a Coinbase Agentic Wallet) and get
|
|
18
|
+
the read-only verify + attestation. No `[xrpl]` extra needed.
|
|
19
|
+
- Pure, network-free helpers: `fetch_requirement`, `decode_payment_required`,
|
|
20
|
+
`build_x_payment_header`, `invoice_id_hash`, `summarize_attestation`, plus
|
|
21
|
+
`AttestationSummary` / `X402PaymentResult` / `X402PayError`.
|
|
22
|
+
- New optional extra `[xrpl]` (pulls `xrpl-py`) — only needed for the local
|
|
23
|
+
signing in `pay_alias_xrp`; the base install stays `pydantic` + `requests`.
|
|
24
|
+
|
|
25
|
+
## [0.1.0] — 2026-06-28
|
|
26
|
+
|
|
27
|
+
Initial public protocol-core extraction from the DNS of Money Financial
|
|
28
|
+
Autonomy Stack. Published to PyPI.
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
- `pay:` URI addressing (FAS-1 grammar): `is_valid_pay_uri`, `assert_valid_pay_uri`,
|
|
32
|
+
`PAY_URI_PATTERN`.
|
|
33
|
+
- A2A-009 semantic normalizer: `normalize_action`, `normalize_message`,
|
|
34
|
+
`compute_semantic_hash`, `SYNONYM_MAP`, `CANONICAL_ACTIONS`.
|
|
35
|
+
- A2A-008 canonical hashing: `compute_canonical_hash` — metadata-stable,
|
|
36
|
+
vocabulary-stable payment-intent hashing.
|
|
37
|
+
- A2A-041 wire schemas: `A2APaymentHookRequest`, `A2APaymentHookResponse`,
|
|
38
|
+
`ResolutionDetail`, `SettlementDetail`, `A2ACapabilities`.
|
|
39
|
+
- `A2APaymentHookClient` — synchronous payment-hook + capabilities client with
|
|
40
|
+
client-side validation.
|
|
41
|
+
- Test suite covering addressing, normalization, canonical hashing, and schema
|
|
42
|
+
validation.
|
|
43
|
+
- Apache-2.0 license, `py.typed` marker, runnable example.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: a2a-protocol-core
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.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
|
|
@@ -20,6 +20,8 @@ Requires-Dist: requests>=2.31.0
|
|
|
20
20
|
Provides-Extra: dev
|
|
21
21
|
Requires-Dist: pytest>=8.3.0; extra == 'dev'
|
|
22
22
|
Requires-Dist: ruff>=0.8.0; extra == 'dev'
|
|
23
|
+
Provides-Extra: xrpl
|
|
24
|
+
Requires-Dist: xrpl-py>=2.5.0; extra == 'xrpl'
|
|
23
25
|
Description-Content-Type: text/markdown
|
|
24
26
|
|
|
25
27
|
# a2a-protocol-core
|
|
@@ -81,6 +83,47 @@ print(result.settlement_result.status, result.iso_message_ref)
|
|
|
81
83
|
|
|
82
84
|
See [`examples/trigger_payment_hook.py`](examples/trigger_payment_hook.py).
|
|
83
85
|
|
|
86
|
+
## Pay a `pay:` alias in one call (non-custodial)
|
|
87
|
+
|
|
88
|
+
Resolve the price, pay from **your own wallet**, and get back a signed
|
|
89
|
+
resolve/verify/OFAC-screen attestation — one function, and your seed never leaves
|
|
90
|
+
your process:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
pip install "a2a-protocol-core[xrpl]"
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
from a2a_protocol_core import pay_alias_xrp
|
|
98
|
+
|
|
99
|
+
result = pay_alias_xrp(
|
|
100
|
+
base_url="https://api.dnsofmoney.com",
|
|
101
|
+
alias="pay:vendor.alpha",
|
|
102
|
+
amount_xrp="0.10",
|
|
103
|
+
seed="s...", # YOUR XRPL wallet seed — signs locally, never transmitted
|
|
104
|
+
api_key="fas_live_...", # attributes the settle leg
|
|
105
|
+
)
|
|
106
|
+
print(result.tx_hash, result.summary.verdict) # e.g. "A1B2…", "CLEAR"
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Already settled the payment through your own wallet stack (or a Coinbase Agentic
|
|
110
|
+
Wallet)? Skip the signing and just fetch the attestation — no `[xrpl]` extra:
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
from a2a_protocol_core import attest_settled_payment
|
|
114
|
+
|
|
115
|
+
result = attest_settled_payment(
|
|
116
|
+
base_url="https://api.dnsofmoney.com",
|
|
117
|
+
alias="pay:vendor.alpha",
|
|
118
|
+
amount_xrp="0.10",
|
|
119
|
+
tx_hash="A1B2C3...", # your already-validated XRPL tx
|
|
120
|
+
api_key="fas_live_...",
|
|
121
|
+
)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
DNS of Money **verifies an already-settled transaction and returns metadata** — it
|
|
125
|
+
never holds your keys or your funds.
|
|
126
|
+
|
|
84
127
|
## Why canonical hashing?
|
|
85
128
|
|
|
86
129
|
Two agents describing the same payment with different words ("send" vs
|
|
@@ -57,6 +57,47 @@ print(result.settlement_result.status, result.iso_message_ref)
|
|
|
57
57
|
|
|
58
58
|
See [`examples/trigger_payment_hook.py`](examples/trigger_payment_hook.py).
|
|
59
59
|
|
|
60
|
+
## Pay a `pay:` alias in one call (non-custodial)
|
|
61
|
+
|
|
62
|
+
Resolve the price, pay from **your own wallet**, and get back a signed
|
|
63
|
+
resolve/verify/OFAC-screen attestation — one function, and your seed never leaves
|
|
64
|
+
your process:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
pip install "a2a-protocol-core[xrpl]"
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
from a2a_protocol_core import pay_alias_xrp
|
|
72
|
+
|
|
73
|
+
result = pay_alias_xrp(
|
|
74
|
+
base_url="https://api.dnsofmoney.com",
|
|
75
|
+
alias="pay:vendor.alpha",
|
|
76
|
+
amount_xrp="0.10",
|
|
77
|
+
seed="s...", # YOUR XRPL wallet seed — signs locally, never transmitted
|
|
78
|
+
api_key="fas_live_...", # attributes the settle leg
|
|
79
|
+
)
|
|
80
|
+
print(result.tx_hash, result.summary.verdict) # e.g. "A1B2…", "CLEAR"
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Already settled the payment through your own wallet stack (or a Coinbase Agentic
|
|
84
|
+
Wallet)? Skip the signing and just fetch the attestation — no `[xrpl]` extra:
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
from a2a_protocol_core import attest_settled_payment
|
|
88
|
+
|
|
89
|
+
result = attest_settled_payment(
|
|
90
|
+
base_url="https://api.dnsofmoney.com",
|
|
91
|
+
alias="pay:vendor.alpha",
|
|
92
|
+
amount_xrp="0.10",
|
|
93
|
+
tx_hash="A1B2C3...", # your already-validated XRPL tx
|
|
94
|
+
api_key="fas_live_...",
|
|
95
|
+
)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
DNS of Money **verifies an already-settled transaction and returns metadata** — it
|
|
99
|
+
never holds your keys or your funds.
|
|
100
|
+
|
|
60
101
|
## Why canonical hashing?
|
|
61
102
|
|
|
62
103
|
Two agents describing the same payment with different words ("send" vs
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Example: an AI agent pays a DNS of Money ``pay:`` alias and screens the counterparty.
|
|
3
|
+
|
|
4
|
+
The scenario the agent-payment world actually needs solved: your agent knows *who*
|
|
5
|
+
it wants to pay (a human-readable ``pay:`` name, not a raw r-address), needs to pay
|
|
6
|
+
in one step, and wants proof it screened the counterparty before releasing funds.
|
|
7
|
+
|
|
8
|
+
This is the whole loop in ~10 lines using ``a2a-protocol-core``:
|
|
9
|
+
|
|
10
|
+
quote → resolve the x402 price for the alias (no wallet needed)
|
|
11
|
+
pay → sign + submit XRP from the agent's OWN wallet (self-custody; seed never sent)
|
|
12
|
+
verify → get back a signed resolve/verify/OFAC-screen attestation
|
|
13
|
+
act → the agent decides based on the screen verdict
|
|
14
|
+
|
|
15
|
+
Works today for any XRPL-holding agent — e.g. one built on Ripple's XRPL AI Starter
|
|
16
|
+
Kit, Crossmint agent wallets, or a Xaman/xApp wallet. DNS of Money is non-custodial:
|
|
17
|
+
it verifies an already-settled tx and returns metadata; it never holds your keys or
|
|
18
|
+
funds.
|
|
19
|
+
|
|
20
|
+
Run (quote only — safe, no spend):
|
|
21
|
+
X402_BASE=https://api.dnsofmoney.com X402_ALIAS=pay:dnsofmoney \
|
|
22
|
+
python examples/agent_pay_alias.py
|
|
23
|
+
|
|
24
|
+
Run (real payment — needs the [xrpl] extra + your funded XRPL wallet + an API key):
|
|
25
|
+
pip install "a2a-protocol-core[xrpl]"
|
|
26
|
+
X402_BASE=https://api.dnsofmoney.com X402_ALIAS=pay:vendor.alpha X402_AMOUNT=0.10 \
|
|
27
|
+
FAS_API_KEY=fas_live_... XRPL_AGENT_SEED=s... \
|
|
28
|
+
python examples/agent_pay_alias.py
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import os
|
|
34
|
+
import sys
|
|
35
|
+
|
|
36
|
+
from a2a_protocol_core import (
|
|
37
|
+
X402PayError,
|
|
38
|
+
fetch_requirement,
|
|
39
|
+
pay_alias_xrp,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def main() -> int:
|
|
44
|
+
base_url = os.getenv("X402_BASE", "https://api.dnsofmoney.com")
|
|
45
|
+
alias = os.getenv("X402_ALIAS", "pay:dnsofmoney")
|
|
46
|
+
amount = os.getenv("X402_AMOUNT", "0.01")
|
|
47
|
+
seed = os.getenv("XRPL_AGENT_SEED", "").strip()
|
|
48
|
+
api_key = os.getenv("FAS_API_KEY", "").strip()
|
|
49
|
+
rpc = os.getenv("XRPL_RPC_URL", "https://xrplcluster.com")
|
|
50
|
+
|
|
51
|
+
# 1) QUOTE — resolve what this alias costs, on which rail. No wallet, no spend.
|
|
52
|
+
try:
|
|
53
|
+
req = fetch_requirement(base_url=base_url, alias=alias, amount_xrp=amount)
|
|
54
|
+
except X402PayError as exc:
|
|
55
|
+
print(f"[quote] could not get a price for {alias}: {exc}")
|
|
56
|
+
return 1
|
|
57
|
+
print(f"[quote] {alias} -> pay {req['maxAmountRequired']} drops to {req['payTo']} on {req['network']}")
|
|
58
|
+
|
|
59
|
+
# Quote-only mode: stop here unless the agent is configured to actually pay.
|
|
60
|
+
if not (seed and api_key):
|
|
61
|
+
print("[dry-run] set XRPL_AGENT_SEED (your wallet) + FAS_API_KEY to pay for real.")
|
|
62
|
+
return 0
|
|
63
|
+
|
|
64
|
+
# 2-3) PAY + VERIFY — one call: the agent signs from its own wallet, DNS of Money
|
|
65
|
+
# verifies read-only and returns the signed attestation.
|
|
66
|
+
try:
|
|
67
|
+
result = pay_alias_xrp(
|
|
68
|
+
base_url=base_url, alias=alias, amount_xrp=amount, seed=seed, api_key=api_key, xrpl_rpc_url=rpc
|
|
69
|
+
)
|
|
70
|
+
except X402PayError as exc:
|
|
71
|
+
print(f"[pay] failed: {exc}")
|
|
72
|
+
return 1
|
|
73
|
+
|
|
74
|
+
s = result.summary
|
|
75
|
+
print(f"[paid] tx={result.tx_hash} settled={result.settled} ({'signed' if s.signed else 'unsigned'} attestation)")
|
|
76
|
+
print(f"[screen] overall={s.verdict} payee={s.payee_verdict} payer={s.payer_verdict}")
|
|
77
|
+
|
|
78
|
+
# 4) ACT — the agent does its own thing based on the compliance verdict.
|
|
79
|
+
if s.verdict == "BLOCKED":
|
|
80
|
+
print("[decide] counterparty screened BLOCKED — the agent would halt / escalate here.")
|
|
81
|
+
return 2
|
|
82
|
+
print("[decide] counterparty clear — the agent proceeds with the delivered service.")
|
|
83
|
+
return 0
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
if __name__ == "__main__":
|
|
87
|
+
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.2.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"
|
|
@@ -30,6 +30,12 @@ Specification = "https://github.com/dnsofmoney/dns-of-money"
|
|
|
30
30
|
Source = "https://github.com/dnsofmoney/a2a-protocol-core"
|
|
31
31
|
|
|
32
32
|
[project.optional-dependencies]
|
|
33
|
+
# The one-call pay-path (pay_alias_xrp) signs an XRP payment with the caller's own
|
|
34
|
+
# wallet — that needs xrpl-py. Kept optional so the base package stays dep-light
|
|
35
|
+
# (resolve/hash/attest work without it).
|
|
36
|
+
xrpl = [
|
|
37
|
+
"xrpl-py>=2.5.0",
|
|
38
|
+
]
|
|
33
39
|
dev = [
|
|
34
40
|
"pytest>=8.3.0",
|
|
35
41
|
"ruff>=0.8.0",
|
|
@@ -34,8 +34,19 @@ from a2a_protocol_core.semantic_normalizer import (
|
|
|
34
34
|
normalize_action,
|
|
35
35
|
normalize_message,
|
|
36
36
|
)
|
|
37
|
+
from a2a_protocol_core.x402_pay import (
|
|
38
|
+
AttestationSummary,
|
|
39
|
+
X402PaymentResult,
|
|
40
|
+
X402PayError,
|
|
41
|
+
attest_settled_payment,
|
|
42
|
+
build_x_payment_header,
|
|
43
|
+
decode_payment_required,
|
|
44
|
+
fetch_requirement,
|
|
45
|
+
pay_alias_xrp,
|
|
46
|
+
summarize_attestation,
|
|
47
|
+
)
|
|
37
48
|
|
|
38
|
-
__version__ = "0.
|
|
49
|
+
__version__ = "0.2.0"
|
|
39
50
|
|
|
40
51
|
__all__ = [
|
|
41
52
|
"__version__",
|
|
@@ -59,4 +70,14 @@ __all__ = [
|
|
|
59
70
|
"A2ACapabilities",
|
|
60
71
|
# client
|
|
61
72
|
"A2APaymentHookClient",
|
|
73
|
+
# x402 pay-path (one-call; signing needs the [xrpl] extra)
|
|
74
|
+
"pay_alias_xrp",
|
|
75
|
+
"attest_settled_payment",
|
|
76
|
+
"fetch_requirement",
|
|
77
|
+
"decode_payment_required",
|
|
78
|
+
"build_x_payment_header",
|
|
79
|
+
"summarize_attestation",
|
|
80
|
+
"AttestationSummary",
|
|
81
|
+
"X402PaymentResult",
|
|
82
|
+
"X402PayError",
|
|
62
83
|
]
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
"""
|
|
2
|
+
One-call x402 pay-path — pay a ``pay:`` alias from YOUR own wallet.
|
|
3
|
+
|
|
4
|
+
This is the "make it actually easy" helper: an agent developer calls one function
|
|
5
|
+
to resolve the x402 requirement for a ``pay:`` alias, sign + submit an XRP payment
|
|
6
|
+
**with their own wallet seed**, and hand the proof back to DNS of Money for a
|
|
7
|
+
read-only verify + the signed resolve/verify/OFAC-screen attestation.
|
|
8
|
+
|
|
9
|
+
Non-custodial by construction: the seed is used to sign locally, in *your* process,
|
|
10
|
+
and is never sent over the wire. DNS of Money verifies an already-settled tx and
|
|
11
|
+
returns metadata — it never holds your keys or your funds.
|
|
12
|
+
|
|
13
|
+
pip install "a2a-protocol-core[xrpl]"
|
|
14
|
+
|
|
15
|
+
from a2a_protocol_core import pay_alias_xrp
|
|
16
|
+
|
|
17
|
+
result = pay_alias_xrp(
|
|
18
|
+
base_url="https://api.dnsofmoney.com",
|
|
19
|
+
alias="pay:vendor.alpha",
|
|
20
|
+
amount_xrp="0.10",
|
|
21
|
+
seed="s...", # YOUR XRPL wallet seed — signs locally, never sent
|
|
22
|
+
api_key="fas_live_...", # attributes the settle leg
|
|
23
|
+
)
|
|
24
|
+
print(result.tx_hash, result.summary.verdict)
|
|
25
|
+
|
|
26
|
+
The pure helpers (``decode_payment_required``, ``build_x_payment_header``,
|
|
27
|
+
``summarize_attestation``) have no network/xrpl dependency and are unit-tested; the
|
|
28
|
+
XRPL signing lazy-imports ``xrpl-py`` so the base package stays dependency-light.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import base64
|
|
34
|
+
import hashlib
|
|
35
|
+
import json
|
|
36
|
+
from dataclasses import dataclass
|
|
37
|
+
from decimal import Decimal
|
|
38
|
+
from typing import Any, Optional, Union
|
|
39
|
+
|
|
40
|
+
import requests
|
|
41
|
+
|
|
42
|
+
# Public mainnet XRPL JSON-RPC (multi-host cluster). Override for testnet/devnet.
|
|
43
|
+
DEFAULT_XRPL_RPC = "https://xrplcluster.com"
|
|
44
|
+
DEFAULT_TIMEOUT = 60
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class X402PayError(RuntimeError):
|
|
48
|
+
"""Raised when the x402 pay-path fails (bad challenge, failed settle, or signing error)."""
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# ── Pure helpers (no network, no xrpl — unit-tested) ─────────────────────────────
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def decode_payment_required(header_value: str) -> dict:
|
|
55
|
+
"""Decode the base64(JSON) PAYMENT-REQUIRED header into the XRP requirement dict."""
|
|
56
|
+
return json.loads(base64.b64decode(header_value))
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def build_x_payment_header(tx_hash: str, payer: Optional[str] = None) -> str:
|
|
60
|
+
"""Build the X-PAYMENT header: base64(JSON) proof of the settled tx hash."""
|
|
61
|
+
payload: dict = {"txHash": tx_hash}
|
|
62
|
+
if payer:
|
|
63
|
+
payload["payer"] = payer
|
|
64
|
+
envelope = {"x402Version": 2, "payload": payload}
|
|
65
|
+
return base64.b64encode(json.dumps(envelope).encode()).decode()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def invoice_id_hash(invoice_id: str) -> str:
|
|
69
|
+
"""The on-chain InvoiceID that binds a payment to a 402 challenge.
|
|
70
|
+
|
|
71
|
+
Matches the server verifier: SHA-256 of the requirement's ``invoiceId``, hex,
|
|
72
|
+
uppercased. Setting it on the Payment replay-binds the tx to this challenge.
|
|
73
|
+
"""
|
|
74
|
+
return hashlib.sha256(invoice_id.encode("utf-8")).hexdigest().upper()
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass
|
|
78
|
+
class AttestationSummary:
|
|
79
|
+
"""The deliverable at a glance — the resolve/verify/OFAC-screen verdicts."""
|
|
80
|
+
|
|
81
|
+
verdict: Optional[str] # overall: CLEAR / REVIEW / BLOCKED / UNKNOWN
|
|
82
|
+
payee_verdict: Optional[str]
|
|
83
|
+
payer_verdict: Optional[str]
|
|
84
|
+
tx_id: Optional[str]
|
|
85
|
+
signed: bool
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def summarize_attestation(attestation: dict) -> AttestationSummary:
|
|
89
|
+
"""One-glance summary of the attestation (signed VC or unsigned flat form)."""
|
|
90
|
+
subject = attestation.get("credentialSubject", attestation)
|
|
91
|
+
screen = subject.get("screen") or {}
|
|
92
|
+
settlement = subject.get("settlement") or {}
|
|
93
|
+
return AttestationSummary(
|
|
94
|
+
verdict=screen.get("verdict"),
|
|
95
|
+
payee_verdict=(screen.get("payee") or {}).get("verdict"),
|
|
96
|
+
payer_verdict=(screen.get("payer") or {}).get("verdict"),
|
|
97
|
+
tx_id=settlement.get("txid"),
|
|
98
|
+
signed=bool(attestation.get("proof")),
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@dataclass
|
|
103
|
+
class X402PaymentResult:
|
|
104
|
+
"""The outcome of a completed x402 pay-path."""
|
|
105
|
+
|
|
106
|
+
tx_hash: str
|
|
107
|
+
payer: Optional[str]
|
|
108
|
+
settled: bool
|
|
109
|
+
idempotent: bool
|
|
110
|
+
attestation: dict
|
|
111
|
+
proof: dict
|
|
112
|
+
summary: AttestationSummary
|
|
113
|
+
raw: dict
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
# ── HTTP legs (challenge + settle) ───────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _pay_url(base_url: str, alias: str) -> str:
|
|
120
|
+
return f"{base_url.rstrip('/')}/api/v1/x402/pay/{alias}"
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def fetch_requirement(
|
|
124
|
+
*,
|
|
125
|
+
base_url: str,
|
|
126
|
+
alias: str,
|
|
127
|
+
amount_xrp: Union[str, Decimal],
|
|
128
|
+
session: Optional[requests.Session] = None,
|
|
129
|
+
timeout: int = DEFAULT_TIMEOUT,
|
|
130
|
+
) -> dict:
|
|
131
|
+
"""GET the 402 challenge and return the decoded XRP payment requirement.
|
|
132
|
+
|
|
133
|
+
Raises ``X402PayError`` if the server does not answer 402 (e.g. x402 disabled,
|
|
134
|
+
alias not payable, or the alias has no XRPL address).
|
|
135
|
+
"""
|
|
136
|
+
http = session or requests.Session()
|
|
137
|
+
resp = http.get(_pay_url(base_url, alias), params={"amount": str(amount_xrp), "currency": "XRP"}, timeout=timeout)
|
|
138
|
+
if resp.status_code != 402:
|
|
139
|
+
raise X402PayError(f"expected a 402 challenge, got {resp.status_code}: {resp.text[:200]}")
|
|
140
|
+
header = resp.headers.get("PAYMENT-REQUIRED")
|
|
141
|
+
if not header:
|
|
142
|
+
raise X402PayError("402 response missing the PAYMENT-REQUIRED header")
|
|
143
|
+
return decode_payment_required(header)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _settle(
|
|
147
|
+
*,
|
|
148
|
+
base_url: str,
|
|
149
|
+
alias: str,
|
|
150
|
+
amount_xrp: Union[str, Decimal],
|
|
151
|
+
tx_hash: str,
|
|
152
|
+
payer: Optional[str],
|
|
153
|
+
api_key: str,
|
|
154
|
+
session: requests.Session,
|
|
155
|
+
timeout: int,
|
|
156
|
+
) -> dict:
|
|
157
|
+
headers = {"X-PAYMENT": build_x_payment_header(tx_hash, payer), "X-API-Key": api_key}
|
|
158
|
+
resp = session.get(
|
|
159
|
+
_pay_url(base_url, alias),
|
|
160
|
+
params={"amount": str(amount_xrp), "currency": "XRP"},
|
|
161
|
+
headers=headers,
|
|
162
|
+
timeout=timeout,
|
|
163
|
+
)
|
|
164
|
+
if resp.status_code != 200:
|
|
165
|
+
raise X402PayError(f"settle leg failed {resp.status_code}: {resp.text[:300]}")
|
|
166
|
+
return resp.json()
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _result_from_body(body: dict) -> X402PaymentResult:
|
|
170
|
+
attestation = body.get("attestation") or {}
|
|
171
|
+
proof = body.get("proof") or {}
|
|
172
|
+
return X402PaymentResult(
|
|
173
|
+
tx_hash=proof.get("transaction") or "",
|
|
174
|
+
payer=proof.get("payer"),
|
|
175
|
+
settled=bool(body.get("settled")),
|
|
176
|
+
idempotent=bool(body.get("idempotent")),
|
|
177
|
+
attestation=attestation,
|
|
178
|
+
proof=proof,
|
|
179
|
+
summary=summarize_attestation(attestation),
|
|
180
|
+
raw=body,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
# ── XRPL signing (lazy xrpl-py import — the [xrpl] extra) ─────────────────────────
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _sign_and_submit_xrp(
|
|
188
|
+
*, pay_to: str, drops: str, seed: str, rpc_url: str, invoice_id: Optional[str], source_tag: Optional[int]
|
|
189
|
+
) -> tuple[str, str]:
|
|
190
|
+
"""Sign + submit an XRP Payment to ``pay_to`` for ``drops`` with the caller's seed.
|
|
191
|
+
|
|
192
|
+
Returns ``(tx_hash, payer_address)``. The seed never leaves this process.
|
|
193
|
+
"""
|
|
194
|
+
try:
|
|
195
|
+
from xrpl.clients import JsonRpcClient
|
|
196
|
+
from xrpl.models.transactions import Payment
|
|
197
|
+
from xrpl.transaction import submit_and_wait
|
|
198
|
+
from xrpl.wallet import Wallet
|
|
199
|
+
except ImportError as exc: # pragma: no cover - import guard
|
|
200
|
+
raise X402PayError(
|
|
201
|
+
"signing requires the 'xrpl' extra — install with: pip install 'a2a-protocol-core[xrpl]'"
|
|
202
|
+
) from exc
|
|
203
|
+
|
|
204
|
+
wallet = Wallet.from_seed(seed)
|
|
205
|
+
client = JsonRpcClient(rpc_url)
|
|
206
|
+
fields: dict[str, Any] = {"account": wallet.address, "destination": pay_to, "amount": str(drops)}
|
|
207
|
+
if invoice_id:
|
|
208
|
+
fields["invoice_id"] = invoice_id
|
|
209
|
+
if source_tag is not None:
|
|
210
|
+
fields["source_tag"] = source_tag
|
|
211
|
+
resp = submit_and_wait(Payment(**fields), client, wallet)
|
|
212
|
+
result = resp.result
|
|
213
|
+
tx_result = (result.get("meta") or {}).get("TransactionResult")
|
|
214
|
+
if result.get("validated") and tx_result == "tesSUCCESS":
|
|
215
|
+
return result["hash"], wallet.address
|
|
216
|
+
raise X402PayError(f"XRPL payment did not succeed (result={tx_result})")
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
# ── One-call entry points ────────────────────────────────────────────────────────
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def pay_alias_xrp(
|
|
223
|
+
*,
|
|
224
|
+
base_url: str,
|
|
225
|
+
alias: str,
|
|
226
|
+
amount_xrp: Union[str, Decimal],
|
|
227
|
+
seed: str,
|
|
228
|
+
api_key: str,
|
|
229
|
+
xrpl_rpc_url: str = DEFAULT_XRPL_RPC,
|
|
230
|
+
session: Optional[requests.Session] = None,
|
|
231
|
+
timeout: int = DEFAULT_TIMEOUT,
|
|
232
|
+
) -> X402PaymentResult:
|
|
233
|
+
"""Pay a ``pay:`` alias in XRP from your own wallet, end to end.
|
|
234
|
+
|
|
235
|
+
Resolve the x402 requirement (402 challenge) → sign + submit the XRP payment
|
|
236
|
+
locally with ``seed`` → hand the proof to DNS of Money for read-only verify +
|
|
237
|
+
the signed attestation. Non-custodial: the seed signs in-process and is never
|
|
238
|
+
transmitted. Requires the ``[xrpl]`` extra. Raises ``X402PayError`` on failure.
|
|
239
|
+
"""
|
|
240
|
+
http = session or requests.Session()
|
|
241
|
+
req = fetch_requirement(base_url=base_url, alias=alias, amount_xrp=amount_xrp, session=http, timeout=timeout)
|
|
242
|
+
invoice = req.get("invoiceId")
|
|
243
|
+
tx_hash, payer = _sign_and_submit_xrp(
|
|
244
|
+
pay_to=req["payTo"],
|
|
245
|
+
drops=req["maxAmountRequired"],
|
|
246
|
+
seed=seed,
|
|
247
|
+
rpc_url=xrpl_rpc_url,
|
|
248
|
+
invoice_id=invoice_id_hash(invoice) if invoice else None,
|
|
249
|
+
source_tag=req.get("sourceTag"),
|
|
250
|
+
)
|
|
251
|
+
body = _settle(
|
|
252
|
+
base_url=base_url,
|
|
253
|
+
alias=alias,
|
|
254
|
+
amount_xrp=amount_xrp,
|
|
255
|
+
tx_hash=tx_hash,
|
|
256
|
+
payer=payer,
|
|
257
|
+
api_key=api_key,
|
|
258
|
+
session=http,
|
|
259
|
+
timeout=timeout,
|
|
260
|
+
)
|
|
261
|
+
return _result_from_body(body)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def attest_settled_payment(
|
|
265
|
+
*,
|
|
266
|
+
base_url: str,
|
|
267
|
+
alias: str,
|
|
268
|
+
amount_xrp: Union[str, Decimal],
|
|
269
|
+
tx_hash: str,
|
|
270
|
+
api_key: str,
|
|
271
|
+
payer: Optional[str] = None,
|
|
272
|
+
session: Optional[requests.Session] = None,
|
|
273
|
+
timeout: int = DEFAULT_TIMEOUT,
|
|
274
|
+
) -> X402PaymentResult:
|
|
275
|
+
"""Get the attestation for a payment you ALREADY settled on XRPL (bring-your-own tx).
|
|
276
|
+
|
|
277
|
+
Same settle leg as :func:`pay_alias_xrp`, minus the signing — for agents that pay
|
|
278
|
+
through their own wallet stack (or a Coinbase Agentic Wallet) and just want the
|
|
279
|
+
read-only verify + attestation. No ``[xrpl]`` extra needed.
|
|
280
|
+
"""
|
|
281
|
+
http = session or requests.Session()
|
|
282
|
+
body = _settle(
|
|
283
|
+
base_url=base_url,
|
|
284
|
+
alias=alias,
|
|
285
|
+
amount_xrp=amount_xrp,
|
|
286
|
+
tx_hash=tx_hash,
|
|
287
|
+
payer=payer,
|
|
288
|
+
api_key=api_key,
|
|
289
|
+
session=http,
|
|
290
|
+
timeout=timeout,
|
|
291
|
+
)
|
|
292
|
+
return _result_from_body(body)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Cross-language conformance — the Python side of the shared contract.
|
|
3
|
+
|
|
4
|
+
Both this suite and the TS `@dnsofmoney/a2a-protocol-core` suite assert against
|
|
5
|
+
the SAME `vectors/canonical_vectors.json` (generated from this Python reference
|
|
6
|
+
by `generate_vectors.py`). The canonical/semantic hashes are byte-for-byte
|
|
7
|
+
identical across languages; this test pins Python to the vectors so neither side
|
|
8
|
+
can silently drift.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
import pytest
|
|
17
|
+
|
|
18
|
+
from a2a_protocol_core import (
|
|
19
|
+
compute_canonical_hash,
|
|
20
|
+
compute_semantic_hash,
|
|
21
|
+
is_valid_pay_uri,
|
|
22
|
+
normalize_action,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
VECTORS = json.loads((Path(__file__).parent / "vectors" / "canonical_vectors.json").read_text(encoding="utf-8"))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_pay_uri_vectors():
|
|
29
|
+
for v in VECTORS["pay_uri"]:
|
|
30
|
+
assert is_valid_pay_uri(v["input"]) is v["valid"], v["input"]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_normalize_action_ok_vectors():
|
|
34
|
+
for v in VECTORS["normalize_action"]["ok"]:
|
|
35
|
+
assert normalize_action(v["input"]) == v["output"], v["input"]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_normalize_action_error_vectors():
|
|
39
|
+
for bad in VECTORS["normalize_action"]["error"]:
|
|
40
|
+
with pytest.raises(ValueError):
|
|
41
|
+
normalize_action(bad)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_canonical_hash_matches_vectors():
|
|
45
|
+
for c in VECTORS["canonical_hash"]:
|
|
46
|
+
assert compute_canonical_hash(c["request"]) == c["expected_hash"], c["name"]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_canonical_hash_equivalence_groups():
|
|
50
|
+
by_name = {c["name"]: c["expected_hash"] for c in VECTORS["canonical_hash"]}
|
|
51
|
+
for group in VECTORS["canonical_hash_equivalence_groups"]:
|
|
52
|
+
first = by_name[group[0]]
|
|
53
|
+
for name in group:
|
|
54
|
+
assert by_name[name] == first, name
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_semantic_hash_matches_vectors():
|
|
58
|
+
for s in VECTORS["semantic_hash"]:
|
|
59
|
+
assert compute_semantic_hash(s["normalized"]) == s["expected_hash"], s["name"]
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""Tests for the one-call x402 pay-path (no real network, no xrpl-py needed)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
import hashlib
|
|
7
|
+
import json
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from a2a_protocol_core import x402_pay
|
|
12
|
+
from a2a_protocol_core.x402_pay import (
|
|
13
|
+
X402PayError,
|
|
14
|
+
attest_settled_payment,
|
|
15
|
+
build_x_payment_header,
|
|
16
|
+
decode_payment_required,
|
|
17
|
+
fetch_requirement,
|
|
18
|
+
pay_alias_xrp,
|
|
19
|
+
summarize_attestation,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# ── Fakes ────────────────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class _Resp:
|
|
27
|
+
def __init__(self, status_code, *, headers=None, body=None, text=""):
|
|
28
|
+
self.status_code = status_code
|
|
29
|
+
self.headers = headers or {}
|
|
30
|
+
self._body = body
|
|
31
|
+
self.text = text
|
|
32
|
+
|
|
33
|
+
def json(self):
|
|
34
|
+
return self._body
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class _Session:
|
|
38
|
+
"""Queue of responses; records each .get(...) call."""
|
|
39
|
+
|
|
40
|
+
def __init__(self, responses):
|
|
41
|
+
self._responses = list(responses)
|
|
42
|
+
self.calls = []
|
|
43
|
+
|
|
44
|
+
def get(self, url, params=None, headers=None, timeout=None):
|
|
45
|
+
self.calls.append({"url": url, "params": params, "headers": headers})
|
|
46
|
+
return self._responses.pop(0)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _pr_header(req: dict) -> str:
|
|
50
|
+
return base64.b64encode(json.dumps(req).encode()).decode()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
REQ = {"payTo": "rPayeeDest", "maxAmountRequired": "100000", "network": "xrpl:0", "invoiceId": "inv-1"}
|
|
54
|
+
|
|
55
|
+
SETTLED_BODY = {
|
|
56
|
+
"success": True,
|
|
57
|
+
"settled": True,
|
|
58
|
+
"idempotent": False,
|
|
59
|
+
"transaction_id": "txn-uuid",
|
|
60
|
+
"attestation": {
|
|
61
|
+
"type": "fas1.resolve-verify-screen",
|
|
62
|
+
"settlement": {"txid": "TXHASH", "amount": "0.10"},
|
|
63
|
+
"screen": {"verdict": "CLEAR", "payee": {"verdict": "CLEAR"}, "payer": {"verdict": "CLEAR"}},
|
|
64
|
+
},
|
|
65
|
+
"proof": {"transaction": "TXHASH", "payer": "rPayer", "network": "xrpl:0"},
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# ── Pure helpers ─────────────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_decode_payment_required_roundtrip():
|
|
73
|
+
assert decode_payment_required(_pr_header(REQ)) == REQ
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_build_x_payment_header_shape():
|
|
77
|
+
decoded = json.loads(base64.b64decode(build_x_payment_header("TX", "rPayer")))
|
|
78
|
+
assert decoded == {"x402Version": 2, "payload": {"txHash": "TX", "payer": "rPayer"}}
|
|
79
|
+
# payer omitted when absent
|
|
80
|
+
decoded2 = json.loads(base64.b64decode(build_x_payment_header("TX")))
|
|
81
|
+
assert decoded2["payload"] == {"txHash": "TX"}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_invoice_id_hash_matches_server_rule():
|
|
85
|
+
assert x402_pay.invoice_id_hash("inv-1") == hashlib.sha256(b"inv-1").hexdigest().upper()
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_summarize_attestation_flat_and_signed():
|
|
89
|
+
flat = summarize_attestation(SETTLED_BODY["attestation"])
|
|
90
|
+
assert (flat.verdict, flat.payee_verdict, flat.tx_id, flat.signed) == ("CLEAR", "CLEAR", "TXHASH", False)
|
|
91
|
+
signed = summarize_attestation({"proof": {"x": 1}, "credentialSubject": SETTLED_BODY["attestation"]})
|
|
92
|
+
assert signed.signed is True and signed.verdict == "CLEAR"
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# ── fetch_requirement ────────────────────────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_fetch_requirement_ok():
|
|
99
|
+
sess = _Session([_Resp(402, headers={"PAYMENT-REQUIRED": _pr_header(REQ)})])
|
|
100
|
+
req = fetch_requirement(base_url="https://x.test", alias="pay:v.alpha", amount_xrp="0.10", session=sess)
|
|
101
|
+
assert req == REQ
|
|
102
|
+
assert sess.calls[0]["params"] == {"amount": "0.10", "currency": "XRP"}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def test_fetch_requirement_non_402_raises():
|
|
106
|
+
sess = _Session([_Resp(400, text="not payable")])
|
|
107
|
+
with pytest.raises(X402PayError, match="expected a 402"):
|
|
108
|
+
fetch_requirement(base_url="https://x.test", alias="pay:v.alpha", amount_xrp="1", session=sess)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def test_fetch_requirement_missing_header_raises():
|
|
112
|
+
sess = _Session([_Resp(402, headers={})])
|
|
113
|
+
with pytest.raises(X402PayError, match="missing the PAYMENT-REQUIRED"):
|
|
114
|
+
fetch_requirement(base_url="https://x.test", alias="pay:v.alpha", amount_xrp="1", session=sess)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
# ── pay_alias_xrp (signing monkeypatched — no xrpl) ──────────────────────────
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def test_pay_alias_xrp_full_flow(monkeypatch):
|
|
121
|
+
captured = {}
|
|
122
|
+
|
|
123
|
+
def fake_sign(*, pay_to, drops, seed, rpc_url, invoice_id, source_tag):
|
|
124
|
+
captured.update(pay_to=pay_to, drops=drops, seed=seed, invoice_id=invoice_id, source_tag=source_tag)
|
|
125
|
+
return "TXHASH", "rPayer"
|
|
126
|
+
|
|
127
|
+
monkeypatch.setattr(x402_pay, "_sign_and_submit_xrp", fake_sign)
|
|
128
|
+
sess = _Session([_Resp(402, headers={"PAYMENT-REQUIRED": _pr_header(REQ)}), _Resp(200, body=SETTLED_BODY)])
|
|
129
|
+
|
|
130
|
+
result = pay_alias_xrp(
|
|
131
|
+
base_url="https://x.test", alias="pay:v.alpha", amount_xrp="0.10", seed="sSEED", api_key="k", session=sess
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
# signed the right thing: payTo, drops, and the invoice-bound InvoiceID
|
|
135
|
+
assert captured["pay_to"] == "rPayeeDest"
|
|
136
|
+
assert captured["drops"] == "100000"
|
|
137
|
+
assert captured["invoice_id"] == hashlib.sha256(b"inv-1").hexdigest().upper()
|
|
138
|
+
# settle leg carried the proof header + api key
|
|
139
|
+
settle_headers = sess.calls[1]["headers"]
|
|
140
|
+
assert settle_headers["X-API-Key"] == "k"
|
|
141
|
+
assert "X-PAYMENT" in settle_headers
|
|
142
|
+
# parsed result
|
|
143
|
+
assert result.tx_hash == "TXHASH"
|
|
144
|
+
assert result.payer == "rPayer"
|
|
145
|
+
assert result.settled is True and result.idempotent is False
|
|
146
|
+
assert result.summary.verdict == "CLEAR"
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def test_pay_alias_xrp_settle_failure_raises(monkeypatch):
|
|
150
|
+
monkeypatch.setattr(x402_pay, "_sign_and_submit_xrp", lambda **_: ("TX", "rPayer"))
|
|
151
|
+
sess = _Session([_Resp(402, headers={"PAYMENT-REQUIRED": _pr_header(REQ)}), _Resp(402, text="bad proof")])
|
|
152
|
+
with pytest.raises(X402PayError, match="settle leg failed 402"):
|
|
153
|
+
pay_alias_xrp(
|
|
154
|
+
base_url="https://x.test", alias="pay:v.alpha", amount_xrp="0.10", seed="s", api_key="k", session=sess
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def test_pay_alias_xrp_no_invoice_no_binding(monkeypatch):
|
|
159
|
+
captured = {}
|
|
160
|
+
monkeypatch.setattr(x402_pay, "_sign_and_submit_xrp", lambda **kw: captured.update(kw) or ("TX", "rP"))
|
|
161
|
+
req_no_inv = {"payTo": "rD", "maxAmountRequired": "5", "network": "xrpl:0"}
|
|
162
|
+
sess = _Session([_Resp(402, headers={"PAYMENT-REQUIRED": _pr_header(req_no_inv)}), _Resp(200, body=SETTLED_BODY)])
|
|
163
|
+
pay_alias_xrp(base_url="https://x.test", alias="pay:v", amount_xrp="1", seed="s", api_key="k", session=sess)
|
|
164
|
+
assert captured["invoice_id"] is None and captured["source_tag"] is None
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
# ── attest_settled_payment (bring-your-own tx) ───────────────────────────────
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def test_attest_settled_payment_ok():
|
|
171
|
+
sess = _Session([_Resp(200, body=SETTLED_BODY)])
|
|
172
|
+
result = attest_settled_payment(
|
|
173
|
+
base_url="https://x.test", alias="pay:v.alpha", amount_xrp="0.10", tx_hash="TXHASH", api_key="k", session=sess
|
|
174
|
+
)
|
|
175
|
+
assert result.tx_hash == "TXHASH" and result.summary.verdict == "CLEAR"
|
|
176
|
+
# single call = settle only (no challenge)
|
|
177
|
+
assert len(sess.calls) == 1
|
|
178
|
+
assert "X-PAYMENT" in sess.calls[0]["headers"]
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "SHARED cross-language test vectors for a2a-protocol-core. Generated from the Python reference impl (source of truth) by generate_vectors.py. Both the Python and the @dnsofmoney/a2a-protocol-core (TS) test suites assert against THIS file byte-for-byte. Do not hand-edit hashes \u2014 regenerate.",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"pay_uri": [
|
|
5
|
+
{
|
|
6
|
+
"input": "pay:vendor.alpha",
|
|
7
|
+
"valid": true
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"input": "pay:agent.compute",
|
|
11
|
+
"valid": true
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"input": "pay:a",
|
|
15
|
+
"valid": true
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"input": "pay:a.b.c.d",
|
|
19
|
+
"valid": true
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"input": "pay:with-hyphen.ok",
|
|
23
|
+
"valid": true
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"input": "PAY:VENDOR.ALPHA",
|
|
27
|
+
"valid": false
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"input": "not-a-pay-uri",
|
|
31
|
+
"valid": false
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"input": "pay:-leadinghyphen",
|
|
35
|
+
"valid": false
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"input": "pay:",
|
|
39
|
+
"valid": false
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"input": "pay:UPPER",
|
|
43
|
+
"valid": false
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"input": "vendor.alpha",
|
|
47
|
+
"valid": false
|
|
48
|
+
}
|
|
49
|
+
],
|
|
50
|
+
"normalize_action": {
|
|
51
|
+
"ok": [
|
|
52
|
+
{
|
|
53
|
+
"input": "send",
|
|
54
|
+
"output": "EXECUTE_PAYMENT"
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"input": "transfer",
|
|
58
|
+
"output": "EXECUTE_PAYMENT"
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"input": "pay",
|
|
62
|
+
"output": "EXECUTE_PAYMENT"
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"input": "payment",
|
|
66
|
+
"output": "EXECUTE_PAYMENT"
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"input": " SEND ",
|
|
70
|
+
"output": "EXECUTE_PAYMENT"
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"input": "resolve",
|
|
74
|
+
"output": "QUERY"
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
"input": "lookup",
|
|
78
|
+
"output": "QUERY"
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"input": "verify",
|
|
82
|
+
"output": "VERIFY"
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"input": "check",
|
|
86
|
+
"output": "VERIFY"
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"input": "confirm",
|
|
90
|
+
"output": "VERIFY"
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"input": "report",
|
|
94
|
+
"output": "REPORT"
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
"input": "status",
|
|
98
|
+
"output": "REPORT"
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"input": "QUERY",
|
|
102
|
+
"output": "QUERY"
|
|
103
|
+
}
|
|
104
|
+
],
|
|
105
|
+
"error": [
|
|
106
|
+
"frobnicate",
|
|
107
|
+
"",
|
|
108
|
+
"xyz",
|
|
109
|
+
"sendd"
|
|
110
|
+
]
|
|
111
|
+
},
|
|
112
|
+
"canonical_hash": [
|
|
113
|
+
{
|
|
114
|
+
"name": "base",
|
|
115
|
+
"request": {
|
|
116
|
+
"action": "send",
|
|
117
|
+
"amount": "2.50",
|
|
118
|
+
"currency": "USD",
|
|
119
|
+
"alias": "pay:agent.compute"
|
|
120
|
+
},
|
|
121
|
+
"expected_hash": "9f719d1efcf1acbc7d18e5347d6ee29b6967bcea095b0c50dac0ecc962593e20"
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
"name": "synonym_transfer",
|
|
125
|
+
"request": {
|
|
126
|
+
"action": "transfer",
|
|
127
|
+
"amount": "2.50",
|
|
128
|
+
"currency": "USD",
|
|
129
|
+
"alias": "pay:agent.compute"
|
|
130
|
+
},
|
|
131
|
+
"expected_hash": "9f719d1efcf1acbc7d18e5347d6ee29b6967bcea095b0c50dac0ecc962593e20"
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
"name": "synonym_pay",
|
|
135
|
+
"request": {
|
|
136
|
+
"action": "pay",
|
|
137
|
+
"amount": "2.50",
|
|
138
|
+
"currency": "USD",
|
|
139
|
+
"alias": "pay:agent.compute"
|
|
140
|
+
},
|
|
141
|
+
"expected_hash": "9f719d1efcf1acbc7d18e5347d6ee29b6967bcea095b0c50dac0ecc962593e20"
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
"name": "with_noise",
|
|
145
|
+
"request": {
|
|
146
|
+
"action": "payment",
|
|
147
|
+
"amount": "2.50",
|
|
148
|
+
"currency": "USD",
|
|
149
|
+
"alias": "pay:agent.compute",
|
|
150
|
+
"session_id": "s1",
|
|
151
|
+
"request_id": "r1",
|
|
152
|
+
"trace_id": "t1",
|
|
153
|
+
"timestamp": "2026-06-28T00:00:00Z",
|
|
154
|
+
"idempotency_key": "idem-1",
|
|
155
|
+
"memo": "lunch",
|
|
156
|
+
"payload_hash": "ph",
|
|
157
|
+
"canonical_hash": "old",
|
|
158
|
+
"created_at": "x",
|
|
159
|
+
"updated_at": "y"
|
|
160
|
+
},
|
|
161
|
+
"expected_hash": "9f719d1efcf1acbc7d18e5347d6ee29b6967bcea095b0c50dac0ecc962593e20"
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
"name": "unknown_action",
|
|
165
|
+
"request": {
|
|
166
|
+
"action": "frobnicate",
|
|
167
|
+
"amount": "1",
|
|
168
|
+
"currency": "XRP"
|
|
169
|
+
},
|
|
170
|
+
"expected_hash": "36bfcdde32ce25f9c0cfc3d1817a32acb5b882225308dc6d29aaabde8c706380"
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
"name": "rail_included",
|
|
174
|
+
"request": {
|
|
175
|
+
"action": "send",
|
|
176
|
+
"amount": "10",
|
|
177
|
+
"currency": "USD",
|
|
178
|
+
"rail": "xrpl",
|
|
179
|
+
"alias": "pay:x"
|
|
180
|
+
},
|
|
181
|
+
"expected_hash": "b138c5962df892d1d241350aaa4d39a61f1bcbd0a2d9896649fbaf624badda83"
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
"name": "preferred_rail",
|
|
185
|
+
"request": {
|
|
186
|
+
"action": "send",
|
|
187
|
+
"amount": "10",
|
|
188
|
+
"currency": "USD",
|
|
189
|
+
"preferred_rail": "xrpl",
|
|
190
|
+
"alias": "pay:x"
|
|
191
|
+
},
|
|
192
|
+
"expected_hash": "15eba96dd9746623b7c0438709ade2e7dc45abf389f5418882959fbdc220c51b"
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
"name": "category",
|
|
196
|
+
"request": {
|
|
197
|
+
"action": "verify",
|
|
198
|
+
"amount": "5",
|
|
199
|
+
"currency": "USD",
|
|
200
|
+
"payment_category": "compute",
|
|
201
|
+
"alias": "pay:agent.compute"
|
|
202
|
+
},
|
|
203
|
+
"expected_hash": "d6a87eca79c9f81ecd0ef40f36f3bba9240bcf6ce5ce70665c2f3c77dea604f7"
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
"name": "alias_uri_variant",
|
|
207
|
+
"request": {
|
|
208
|
+
"action": "send",
|
|
209
|
+
"amount": "3",
|
|
210
|
+
"currency": "USD",
|
|
211
|
+
"alias_uri": "pay:agent.compute"
|
|
212
|
+
},
|
|
213
|
+
"expected_hash": "02422a90ba079ef599280e02385255c09c1d9efff448835b29e3a85af23d1ada"
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
"name": "numeric_int_amount",
|
|
217
|
+
"request": {
|
|
218
|
+
"action": "send",
|
|
219
|
+
"amount": 10,
|
|
220
|
+
"currency": "USD",
|
|
221
|
+
"alias": "pay:x"
|
|
222
|
+
},
|
|
223
|
+
"expected_hash": "29ea6c0d24255296ac5047f05cd761a971d58623043bc01a395c373870b31cd2"
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
"name": "numeric_float_amount",
|
|
227
|
+
"request": {
|
|
228
|
+
"action": "send",
|
|
229
|
+
"amount": 2.5,
|
|
230
|
+
"currency": "USD",
|
|
231
|
+
"alias": "pay:x"
|
|
232
|
+
},
|
|
233
|
+
"expected_hash": "a6466b7002f12f132f5ff6aada2c1fe1bfffb77502250ee6a4df8ed62def62b8"
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
"name": "payment_type_field",
|
|
237
|
+
"request": {
|
|
238
|
+
"action": "report",
|
|
239
|
+
"amount": "0.01",
|
|
240
|
+
"currency": "XRP",
|
|
241
|
+
"payment_type": "micropayment",
|
|
242
|
+
"alias_name": "pay:agent.analysis"
|
|
243
|
+
},
|
|
244
|
+
"expected_hash": "230fa9b1056f7907e8ae93bb4c5f6f2d4d26d3ff23487782a21c87f14707f7ed"
|
|
245
|
+
}
|
|
246
|
+
],
|
|
247
|
+
"canonical_hash_equivalence_groups": [
|
|
248
|
+
[
|
|
249
|
+
"base",
|
|
250
|
+
"synonym_transfer",
|
|
251
|
+
"synonym_pay",
|
|
252
|
+
"with_noise"
|
|
253
|
+
]
|
|
254
|
+
],
|
|
255
|
+
"semantic_hash": [
|
|
256
|
+
{
|
|
257
|
+
"name": "send_msg",
|
|
258
|
+
"message": {
|
|
259
|
+
"action": "send",
|
|
260
|
+
"intent": "pay vendor"
|
|
261
|
+
},
|
|
262
|
+
"normalized": {
|
|
263
|
+
"action": "EXECUTE_PAYMENT",
|
|
264
|
+
"intent": "pay vendor"
|
|
265
|
+
},
|
|
266
|
+
"expected_hash": "c78571192616d3675014dc1eb405fc965f2acd6354a73daf823ccb639b141e16"
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
"name": "verify_msg",
|
|
270
|
+
"message": {
|
|
271
|
+
"action": "check",
|
|
272
|
+
"ref": "abc"
|
|
273
|
+
},
|
|
274
|
+
"normalized": {
|
|
275
|
+
"action": "VERIFY",
|
|
276
|
+
"ref": "abc"
|
|
277
|
+
},
|
|
278
|
+
"expected_hash": "9ce7da89f5f6ca3864ecf9eb841a15497ad666bef0c4100d4a6c5db83f13eb17"
|
|
279
|
+
}
|
|
280
|
+
]
|
|
281
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Regenerate the shared cross-language test vectors from the Python reference
|
|
3
|
+
implementation (the source of truth).
|
|
4
|
+
|
|
5
|
+
python tests/vectors/generate_vectors.py
|
|
6
|
+
|
|
7
|
+
Writes ``canonical_vectors.json`` next to this file. After regenerating, copy the
|
|
8
|
+
identical file into the TS package
|
|
9
|
+
(``dns-of-money/sdk/a2a-protocol-core/test/vectors/``) so both suites assert
|
|
10
|
+
against the same bytes. Never hand-edit the hashes — regenerate.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
from a2a_protocol_core import (
|
|
19
|
+
compute_canonical_hash,
|
|
20
|
+
compute_semantic_hash,
|
|
21
|
+
is_valid_pay_uri,
|
|
22
|
+
normalize_action,
|
|
23
|
+
normalize_message,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
PAY_URI = [
|
|
27
|
+
{"input": "pay:vendor.alpha", "valid": True},
|
|
28
|
+
{"input": "pay:agent.compute", "valid": True},
|
|
29
|
+
{"input": "pay:a", "valid": True},
|
|
30
|
+
{"input": "pay:a.b.c.d", "valid": True},
|
|
31
|
+
{"input": "pay:with-hyphen.ok", "valid": True},
|
|
32
|
+
{"input": "PAY:VENDOR.ALPHA", "valid": False},
|
|
33
|
+
{"input": "not-a-pay-uri", "valid": False},
|
|
34
|
+
{"input": "pay:-leadinghyphen", "valid": False},
|
|
35
|
+
{"input": "pay:", "valid": False},
|
|
36
|
+
{"input": "pay:UPPER", "valid": False},
|
|
37
|
+
{"input": "vendor.alpha", "valid": False},
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
NORM_OK = [
|
|
41
|
+
{"input": "send", "output": "EXECUTE_PAYMENT"},
|
|
42
|
+
{"input": "transfer", "output": "EXECUTE_PAYMENT"},
|
|
43
|
+
{"input": "pay", "output": "EXECUTE_PAYMENT"},
|
|
44
|
+
{"input": "payment", "output": "EXECUTE_PAYMENT"},
|
|
45
|
+
{"input": " SEND ", "output": "EXECUTE_PAYMENT"},
|
|
46
|
+
{"input": "resolve", "output": "QUERY"},
|
|
47
|
+
{"input": "lookup", "output": "QUERY"},
|
|
48
|
+
{"input": "verify", "output": "VERIFY"},
|
|
49
|
+
{"input": "check", "output": "VERIFY"},
|
|
50
|
+
{"input": "confirm", "output": "VERIFY"},
|
|
51
|
+
{"input": "report", "output": "REPORT"},
|
|
52
|
+
{"input": "status", "output": "REPORT"},
|
|
53
|
+
{"input": "QUERY", "output": "QUERY"},
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
NORM_ERR = ["frobnicate", "", "xyz", "sendd"]
|
|
57
|
+
|
|
58
|
+
CH_CASES = {
|
|
59
|
+
"base": {"action": "send", "amount": "2.50", "currency": "USD", "alias": "pay:agent.compute"},
|
|
60
|
+
"synonym_transfer": {"action": "transfer", "amount": "2.50", "currency": "USD", "alias": "pay:agent.compute"},
|
|
61
|
+
"synonym_pay": {"action": "pay", "amount": "2.50", "currency": "USD", "alias": "pay:agent.compute"},
|
|
62
|
+
"with_noise": {
|
|
63
|
+
"action": "payment",
|
|
64
|
+
"amount": "2.50",
|
|
65
|
+
"currency": "USD",
|
|
66
|
+
"alias": "pay:agent.compute",
|
|
67
|
+
"session_id": "s1",
|
|
68
|
+
"request_id": "r1",
|
|
69
|
+
"trace_id": "t1",
|
|
70
|
+
"timestamp": "2026-06-28T00:00:00Z",
|
|
71
|
+
"idempotency_key": "idem-1",
|
|
72
|
+
"memo": "lunch",
|
|
73
|
+
"payload_hash": "ph",
|
|
74
|
+
"canonical_hash": "old",
|
|
75
|
+
"created_at": "x",
|
|
76
|
+
"updated_at": "y",
|
|
77
|
+
},
|
|
78
|
+
"unknown_action": {"action": "frobnicate", "amount": "1", "currency": "XRP"},
|
|
79
|
+
"rail_included": {"action": "send", "amount": "10", "currency": "USD", "rail": "xrpl", "alias": "pay:x"},
|
|
80
|
+
"preferred_rail": {"action": "send", "amount": "10", "currency": "USD", "preferred_rail": "xrpl", "alias": "pay:x"},
|
|
81
|
+
"category": {
|
|
82
|
+
"action": "verify",
|
|
83
|
+
"amount": "5",
|
|
84
|
+
"currency": "USD",
|
|
85
|
+
"payment_category": "compute",
|
|
86
|
+
"alias": "pay:agent.compute",
|
|
87
|
+
},
|
|
88
|
+
"alias_uri_variant": {"action": "send", "amount": "3", "currency": "USD", "alias_uri": "pay:agent.compute"},
|
|
89
|
+
"numeric_int_amount": {"action": "send", "amount": 10, "currency": "USD", "alias": "pay:x"},
|
|
90
|
+
"numeric_float_amount": {"action": "send", "amount": 2.5, "currency": "USD", "alias": "pay:x"},
|
|
91
|
+
"payment_type_field": {
|
|
92
|
+
"action": "report",
|
|
93
|
+
"amount": "0.01",
|
|
94
|
+
"currency": "XRP",
|
|
95
|
+
"payment_type": "micropayment",
|
|
96
|
+
"alias_name": "pay:agent.analysis",
|
|
97
|
+
},
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
SEM_CASES = {
|
|
101
|
+
"send_msg": {"action": "send", "intent": "pay vendor"},
|
|
102
|
+
"verify_msg": {"action": "check", "ref": "abc"},
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def build() -> dict:
|
|
107
|
+
# Self-verify the static expectations before emitting.
|
|
108
|
+
for v in PAY_URI:
|
|
109
|
+
assert is_valid_pay_uri(v["input"]) is v["valid"], v
|
|
110
|
+
for v in NORM_OK:
|
|
111
|
+
assert normalize_action(v["input"]) == v["output"], v
|
|
112
|
+
for bad in NORM_ERR:
|
|
113
|
+
try:
|
|
114
|
+
normalize_action(bad)
|
|
115
|
+
except ValueError:
|
|
116
|
+
pass
|
|
117
|
+
else: # pragma: no cover
|
|
118
|
+
raise AssertionError(f"expected ValueError for {bad!r}")
|
|
119
|
+
|
|
120
|
+
canonical_hash = [
|
|
121
|
+
{"name": k, "request": r, "expected_hash": compute_canonical_hash(r)} for k, r in CH_CASES.items()
|
|
122
|
+
]
|
|
123
|
+
by_name = {c["name"]: c["expected_hash"] for c in canonical_hash}
|
|
124
|
+
for n in ("synonym_transfer", "synonym_pay", "with_noise"):
|
|
125
|
+
assert by_name[n] == by_name["base"], n
|
|
126
|
+
|
|
127
|
+
semantic_hash = [
|
|
128
|
+
{
|
|
129
|
+
"name": k,
|
|
130
|
+
"message": m,
|
|
131
|
+
"normalized": normalize_message(m),
|
|
132
|
+
"expected_hash": compute_semantic_hash(normalize_message(m)),
|
|
133
|
+
}
|
|
134
|
+
for k, m in SEM_CASES.items()
|
|
135
|
+
]
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
"_comment": (
|
|
139
|
+
"SHARED cross-language test vectors for a2a-protocol-core. Generated from the "
|
|
140
|
+
"Python reference impl (source of truth) by generate_vectors.py. Both the Python "
|
|
141
|
+
"and the @dnsofmoney/a2a-protocol-core (TS) test suites assert against THIS file "
|
|
142
|
+
"byte-for-byte. Do not hand-edit hashes — regenerate."
|
|
143
|
+
),
|
|
144
|
+
"version": "0.1.0",
|
|
145
|
+
"pay_uri": PAY_URI,
|
|
146
|
+
"normalize_action": {"ok": NORM_OK, "error": NORM_ERR},
|
|
147
|
+
"canonical_hash": canonical_hash,
|
|
148
|
+
"canonical_hash_equivalence_groups": [["base", "synonym_transfer", "synonym_pay", "with_noise"]],
|
|
149
|
+
"semantic_hash": semantic_hash,
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
if __name__ == "__main__":
|
|
154
|
+
out_path = Path(__file__).parent / "canonical_vectors.json"
|
|
155
|
+
out_path.write_text(json.dumps(build(), indent=2) + "\n", encoding="utf-8")
|
|
156
|
+
print(f"wrote {out_path}")
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
All notable changes to `a2a-protocol-core` are documented here. This project
|
|
4
|
-
adheres to [Semantic Versioning](https://semver.org/).
|
|
5
|
-
|
|
6
|
-
## [0.1.0] — Unreleased
|
|
7
|
-
|
|
8
|
-
Initial public protocol-core extraction from the DNS of Money Financial
|
|
9
|
-
Autonomy Stack.
|
|
10
|
-
|
|
11
|
-
### Added
|
|
12
|
-
- `pay:` URI addressing (FAS-1 grammar): `is_valid_pay_uri`, `assert_valid_pay_uri`,
|
|
13
|
-
`PAY_URI_PATTERN`.
|
|
14
|
-
- A2A-009 semantic normalizer: `normalize_action`, `normalize_message`,
|
|
15
|
-
`compute_semantic_hash`, `SYNONYM_MAP`, `CANONICAL_ACTIONS`.
|
|
16
|
-
- A2A-008 canonical hashing: `compute_canonical_hash` — metadata-stable,
|
|
17
|
-
vocabulary-stable payment-intent hashing.
|
|
18
|
-
- A2A-041 wire schemas: `A2APaymentHookRequest`, `A2APaymentHookResponse`,
|
|
19
|
-
`ResolutionDetail`, `SettlementDetail`, `A2ACapabilities`.
|
|
20
|
-
- `A2APaymentHookClient` — synchronous payment-hook + capabilities client with
|
|
21
|
-
client-side validation.
|
|
22
|
-
- Test suite covering addressing, normalization, canonical hashing, and schema
|
|
23
|
-
validation.
|
|
24
|
-
- Apache-2.0 license, `py.typed` marker, runnable example.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{a2a_protocol_core-0.1.0 → a2a_protocol_core-0.2.0}/src/a2a_protocol_core/semantic_normalizer.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|