Skip to content

feat(python/batch-settlement): add foundation (PR1)#2199

Open
shuhei0866 wants to merge 2 commits intox402-foundation:mainfrom
shuhei0866:feat/batch-settlement-python-sdk-pr1-foundation
Open

feat(python/batch-settlement): add foundation (PR1)#2199
shuhei0866 wants to merge 2 commits intox402-foundation:mainfrom
shuhei0866:feat/batch-settlement-python-sdk-pr1-foundation

Conversation

@shuhei0866
Copy link
Copy Markdown
Contributor

@shuhei0866 shuhei0866 commented May 5, 2026

Summary

This is part 1 of a planned 13-PR Python port of @x402/evm/batch-settlement (TS PR #2061). It delivers the wire-level foundation that subsequent PRs will build on: constants, ABI, and 18 Pydantic wire-format models with manual discriminator/XOR helpers.

Per our intent comment on #2061, this opens the series.

Stacking strategy & branch request

We would like to land this as a stacked PR series into a long-lived feat/batch-settlement-python-sdk branch on upstream, mirroring two existing precedents in this repo:

  1. feat/python-v2-sdk — Coinbase ran a 16-PR stacked series (2025-12-29 → 2026-01-21) into this branch to land Python SDK v2. External and internal PRs were both accepted there.
  2. feat/batch-settlement-go-sdk — sibling Go-language port currently in progress under the same naming convention.

If a feat/batch-settlement-python-sdk branch is created, we'll rebase this PR onto it and continue with PR2-PR13 stacked. If you'd prefer a different shape (sequential into main, or fewer larger PRs), happy to adapt — flagging this up front so we can align before review effort accumulates.

What's in PR1

python/x402/mechanisms/evm/batch_settlement/
├ __init__.py
├ constants.py   — addresses, EIP-712 type structs, withdraw delays, scheme IDs
├ abi.py         — contract ABI as a Python dict (private-marked claim components)
└ types.py       — 18 wire-format Pydantic models, parse_payload / parse_facilitator_payload helpers, _validate_deposit_xor

python/x402/tests/unit/mechanisms/evm/batch_settlement/
├ test_constants.py — 12 tests (D2/D3/D6 sanity + L2.x preconditions)
├ test_abi.py       — 8 tests (function/event names, private marker)
└ test_types.py     — 36 tests (D1/D4/D5/D7/D8 + extra="ignore" + round-trip)

python/x402/changelog.d/+batch-settlement-foundation.feature.md
scripts/check.sh — Layer 1 universal CI gate

Wire decisions affirmed

PR1 locks down 12 wire-level decisions to keep subsequent PR review focused on behavior. Statement of intent:

We mirror the pre-existing x402 Python SDK convention — wire-level validation is enforced through cross-language fixture tests (Layer 2 invariants L2.1-L2.11, landing in PR2) rather than Pydantic Annotated validators or discriminated unions. This keeps the codebase consistent with exact / upto mechanisms while ensuring TS↔Python interop through deterministic byte-level fixtures.

Notable D7/D8 implementation note: Pydantic v2's Field(discriminator=) does not allow two models in a Union to share the same Literal value. The batch-settlement wire has both BatchSettlementRefundPayload (client) and BatchSettlementEnrichedRefundPayload (facilitator) carrying type: "refund", so we route them via manual helpers (parse_payload / parse_facilitator_payload) — also keeping consistency with exact / upto which use no Field(discriminator=).

# Decision PR1 status
D1 Integer JSON = string (no IntStr alias)
D2 EIP-712 Domain byte equivalence — constants ✅ (fixtures in PR2)
D3 EIP-712 typed structs — constants ✅ (fixtures in PR2)
D4 camelCase ↔ snake_case via BaseX402Model
D5 Optional T | None = None + caller exclude_none=True
D6 Hex format — call-site normalize_address() ⏳ PR2 (utils.py)
D7 Discriminated union via manual parse_payload
D8 XOR deposit auth via _validate_deposit_xor
D9 Time unit = int seconds
D10 Salt = str (encoding to BigInt) ⏳ PR2 (encoding.py)
D11 Signature format ⏳ PR2 (cross-language fixtures)
D12 Storage abstraction ⏳ PR4/PR10

Test plan

$ bash scripts/check.sh
[1/6] ruff lint            ✅ All checks passed
[2/6] ruff format check    ✅ 8 files already formatted
[3/6] mypy                 ✅ Success: no issues found in 6 source files
[4/6] pytest               ✅ 56 passed
[5/6] commit signing       ✅ signed
[6/6] towncrier fragment   ✅ found

Notable test vectors:

  • test_large_integer_round_trips_as_str (D1): "123456789012345678901234567890" survives model_dump → model_validate as str — demonstrates no IntStr alias needed
  • test_payload_round_trip_is_stable (D4/D5): parametrized over 5 fixtures (deposit / voucher / refund / claim / settle); model_dump(by_alias=True, exclude_none=True) round-trips losslessly
  • test_validate_deposit_xor (D8): all 4 combinations (erc3009-only / permit2-only / both / neither)

Roadmap

Re-architected by reviewer-concern: each PR has one Yes/No claim the reviewer verifies. LOC is now a sanity check, not a scoping input.

PR Reviewer-facing claim Scope Est. LOC
PR1 (this) Wire types are normalized using pre-existing x402 Python SDK conventions constants + ABI + 18 types + parse_payload + 56 unit tests ~1,500
PR2 Python and TS produce byte-identical output for the same wire input encoding + utils + L2.1-L2.6 cross-language fixtures + TS-side fixture generator ~700
PR3 Abstractions follow pre-existing SDK conventions authorizer_signer Protocol + errors (51 codes) + ChannelStorage Protocol ~500
PR4-PR6 facilitator + server (verify / settle) work facilitator + server core ~3,300
PR7-PR8 client core + recovery (corrective-402) work client + recovery (Bortlesboat 6 vectors) ~1,800
PR9-PR10 refund + Permit2 deposit work client/facilitator refund + Permit2 ~1,500
PR11-PR12 Redis storage + channelManager work redis_storage + channel_manager (asyncio.Task loop) ~1,700
PR13 e2e + examples work FastAPI / Flask servers + Python examples ~1,500

Note on Layer 2 invariants split: L2.1-L2.6 are byte-level (EIP-712 digests × 4, ERC-3009 deposit nonce, collectorData ABI encoding) and land in PR2 with cross-language fixtures. L2.7-L2.11 are type-level (JSON roundtrip, payload discriminator, deposit XOR, optional field tri-state, large integer serialization) and are already covered by this PR's 36 unit tests in test_types.py.

Open questions

  • OQ-1: Best routing for the enrich-payment-required-response flow (TS handleEnrichPaymentRequiredResponse equivalent) — investigating whether mutating VerifyFailureContext.extra from on_verify_failure is sufficient, or whether a small on_enrich_response core hook (~30 LOC) is needed. Resolution targeted in PR2.
  • OQ-2: Normative loop-guard for corrective-402 retry — TS recovery.ts leaves it caller-responsibility. Will propose in PR7 description.

References

cc @phdargen @CarsonRoscoe

shuhei0866 and others added 2 commits May 5, 2026 19:11
…1 gate)

Python port of x402-foundation#2061 (batch-settlement TS SDK), introduced as PR1 of a
stacked series. This PR ships only the dependency foundation; the wire
interface (Pydantic models, EIP-712 digest cross-language fixtures) lands
in subsequent PRs.

- mechanisms/evm/batch_settlement/{__init__,constants,abi}.py:
  scheme identifier, contract addresses, EIP-712 type definitions, and the
  contract ABI, mirroring the TypeScript SDK verbatim.
- tests/unit/mechanisms/evm/batch_settlement/{test_constants,test_abi}.py:
  20 self-consistency tests (field ordering, EIP-55 checksum, TYPEHASH
  canonical signature, ABI function/event names).
- scripts/check.sh: Layer 1 universal gate (ruff / mypy --follow-imports=silent
  / pytest / commit signing warn / towncrier fragment).
- changelog.d/+batch-settlement-foundation.feature.md: towncrier issueless
  fragment, renamed once the PR number is assigned.

Refs: x402-foundation#2061

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the wire interface for the batch-settlement mechanism, mirroring
the TypeScript ``types.ts``. Per the pre-existing x402 SDK convention,
wire-level invariants (XOR deposit auth, hex format, integer-as-string,
EIP-712 byte equivalence) are enforced through cross-language fixture
tests (Layer 2 invariants) rather than Pydantic Annotated validators or
discriminated unions, keeping consistency with the ``exact`` / ``upto``
mechanisms.

- mechanisms/evm/batch_settlement/types.py:
  18 wire models inheriting ``BaseX402Model``, two Union type aliases
  (BatchSettlementPayload / BatchSettlementFacilitatorSettlePayload),
  and ``parse_payload`` / ``parse_facilitator_payload`` helpers
  performing manual ``type`` discrimination plus XOR auth validation.
  Module docstring documents wire spec conventions (uint256 → str,
  uint40 → int) and the ``extra="ignore"`` policy.
- tests/unit/mechanisms/evm/batch_settlement/test_types.py:
  36 tests covering camelCase ↔ snake_case round-trips, ``from``/``from_``
  alias handling, optional 三状態 (absent/null/present), manual
  discrimination, XOR auth, large-integer round-trip without IntStr,
  and the ``extra="ignore"`` wire contract.
- mechanisms/evm/batch_settlement/__init__.py:
  re-export the wire types and parse helpers.

The ``AuthorizerSigner`` Protocol, ``encoding.py``, ``utils.py``, and the
TypeScript-side fixture generator are deferred to subsequent stacked PRs.

Refs: x402-foundation#2061

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented May 5, 2026

@shuhei0866 is attempting to deploy a commit to the Coinbase Team on Vercel.

A member of the Team first needs to authorize it.

@github-actions github-actions Bot added sdk Changes to core v2 packages python labels May 5, 2026
@shuhei0866 shuhei0866 marked this pull request as ready for review May 6, 2026 02:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

python sdk Changes to core v2 packages

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant