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.
Files changed (25) hide show
  1. a2a_protocol_core-0.2.0/CHANGELOG.md +43 -0
  2. {a2a_protocol_core-0.1.0 → a2a_protocol_core-0.2.0}/PKG-INFO +44 -1
  3. {a2a_protocol_core-0.1.0 → a2a_protocol_core-0.2.0}/README.md +41 -0
  4. a2a_protocol_core-0.2.0/examples/agent_pay_alias.py +87 -0
  5. {a2a_protocol_core-0.1.0 → a2a_protocol_core-0.2.0}/pyproject.toml +7 -1
  6. {a2a_protocol_core-0.1.0 → a2a_protocol_core-0.2.0}/src/a2a_protocol_core/__init__.py +22 -1
  7. a2a_protocol_core-0.2.0/src/a2a_protocol_core/x402_pay.py +292 -0
  8. a2a_protocol_core-0.2.0/tests/test_cross_language_vectors.py +59 -0
  9. a2a_protocol_core-0.2.0/tests/test_x402_pay.py +178 -0
  10. a2a_protocol_core-0.2.0/tests/vectors/canonical_vectors.json +281 -0
  11. a2a_protocol_core-0.2.0/tests/vectors/generate_vectors.py +156 -0
  12. a2a_protocol_core-0.1.0/CHANGELOG.md +0 -24
  13. {a2a_protocol_core-0.1.0 → a2a_protocol_core-0.2.0}/.gitignore +0 -0
  14. {a2a_protocol_core-0.1.0 → a2a_protocol_core-0.2.0}/LICENSE +0 -0
  15. {a2a_protocol_core-0.1.0 → a2a_protocol_core-0.2.0}/examples/trigger_payment_hook.py +0 -0
  16. {a2a_protocol_core-0.1.0 → a2a_protocol_core-0.2.0}/src/a2a_protocol_core/addressing.py +0 -0
  17. {a2a_protocol_core-0.1.0 → a2a_protocol_core-0.2.0}/src/a2a_protocol_core/canonical_hash.py +0 -0
  18. {a2a_protocol_core-0.1.0 → a2a_protocol_core-0.2.0}/src/a2a_protocol_core/client.py +0 -0
  19. {a2a_protocol_core-0.1.0 → a2a_protocol_core-0.2.0}/src/a2a_protocol_core/py.typed +0 -0
  20. {a2a_protocol_core-0.1.0 → a2a_protocol_core-0.2.0}/src/a2a_protocol_core/schemas.py +0 -0
  21. {a2a_protocol_core-0.1.0 → a2a_protocol_core-0.2.0}/src/a2a_protocol_core/semantic_normalizer.py +0 -0
  22. {a2a_protocol_core-0.1.0 → a2a_protocol_core-0.2.0}/tests/test_addressing.py +0 -0
  23. {a2a_protocol_core-0.1.0 → a2a_protocol_core-0.2.0}/tests/test_canonical_hash.py +0 -0
  24. {a2a_protocol_core-0.1.0 → a2a_protocol_core-0.2.0}/tests/test_schemas.py +0 -0
  25. {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.1.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.1.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.1.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.