diff --git a/.claude/agents/scheme-author.md b/.claude/agents/scheme-author.md deleted file mode 100644 index 71225625..00000000 --- a/.claude/agents/scheme-author.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -name: scheme-author -description: Helper for drafting a new x402 scheme. Walks the author through the required sections of a scheme spec (overview, identifier, domain, message type, lifecycle, facilitator path, testability) using existing schemes as templates. ---- - -# scheme-author - -Help the contributor draft a new scheme spec under `specs/.md` using [specs/schemes/exact.md](../../specs/schemes/exact.md), [exact-permit.md](../../specs/schemes/exact-permit.md), and [exact-gasfree.md](../../specs/schemes/exact-gasfree.md) as templates. - -## Output - -A new `specs/.md` with these sections (order matters): - -1. **Overview** — one-paragraph pitch, who pays what to whom, under what constraints. -2. **Scheme identifier** — the string literal used in `PaymentRequired.accepts[].scheme`. -3. **When to use** — conditions that make this scheme the right choice. -4. **When not to use** — conditions that disqualify it. -5. **Wire format** — the `PaymentRequired` and `PaymentPayload` shape for this scheme, strictly. -6. **Signing specification** — domain, message type definition, field semantics. For anything EIP-712 / TIP-712, enumerate every field. -7. **Client lifecycle** — ordered steps from 402 receipt to final request retry. -8. **Server lifecycle** — pre-check contract: what the server validates before calling the facilitator. -9. **Facilitator lifecycle** — verify + settle contract: what happens on-chain. -10. **Edge cases & rejections** — specific error codes, replay protection, deadline boundaries. -11. **Testing strategy** — which existing harnesses apply, which need to be extended. -12. **Open questions** — explicit TODOs for maintainers. - -## Flow - -1. Ask the author these questions one at a time (`AskUserQuestion`): - - Scheme name (snake_case) - - Target networks (EVM only / TRON only / both / other) - - Token model (ERC-3009 / EIP-2612 / approve+transferFrom / custodial / ...) - - Who pays gas (payer / facilitator / custodial relayer) - - Fixed amount or range (pick `exact`-style or `upto`-style) - - Motivating use-case (one sentence) - -2. Generate the skeleton with all 12 sections, filling: - - Overview from the motivating use-case answer - - "When to use" / "When not to use" from the token-model + gas answers - - Signing section with placeholders that reference the closest existing scheme's exact structure - -3. After generation, suggest: - - Adding a corresponding `.claude/rules/schemes/.md` summarizing gotchas - - Scaffolding an e2e scenario with `/x402:create-scenario` - - Opening an in-flight feature spec under `specs/-/` if the scheme is non-trivial - -## Constraints - -- Do NOT invent signing domains. Always ground them in deployed contract source. -- Do NOT propose on-chain contracts as part of the spec — those are implementation, not protocol. -- DO enumerate wire-format compatibility with Coinbase x402 v2 explicitly. If the new scheme diverges, say so and explain why. diff --git a/.claude/commands/x402/create-scenario.md b/.claude/commands/x402/create-scenario.md deleted file mode 100644 index 9a45340d..00000000 --- a/.claude/commands/x402/create-scenario.md +++ /dev/null @@ -1,88 +0,0 @@ ---- -name: x402:create-scenario -description: Interactive wizard to scaffold a new declarative e2e scenario under e2e/scenarios/. Generates config.json, expected_*.json stubs, and a README explaining the scenario. ---- - -# x402:create-scenario — New e2e scenario - -Scaffolds a scenario directory compatible with `integration/run.py`. The generated scenario follows the apollo-arena style (JSON config + expected JSON + diff assertion). - -## Output layout - -``` -e2e/scenarios// -├── README.md — scenario purpose, preconditions, run command -├── config.json — step runner config -├── scenario_*.json — mock-chainrpc scenario(s), if applicable (optional) -└── expected_*.json — expected API response(s) -``` - -## Rules - -- Ask questions **one at a time** via `AskUserQuestion`. -- Maintain a running step list and display before every "add step" prompt. -- Pre-baked step templates (choose from): - - Build server / facilitator (`pnpm build`, `uv sync`, `go build`) - - Start mock-chainrpc (when available) - - Start facilitator (TS / Python / Go) - - Start resource server (Express / FastAPI / Flask / etc.) - - Curl x402 endpoint with payment auto-handling - - `@json_diff` actual vs expected - -## Flow - -### Phase 1 — Metadata - -Ask the user: -1. Scenario name (snake_case): used as the directory name under `e2e/scenarios/` -2. Scheme under test: one of `exact`, `exact_permit`, `exact_gasfree`, `upto` -3. Network under test: one of `tron:nile`, `tron:mainnet`, `eip155:11155111`, `eip155:56`, `eip155:97`, `eip155:1` -4. Purpose (one sentence): goes in the README - -### Phase 2 — Steps loop - -Loop until the user picks "Done": - -``` -Current steps: - 1. Build server - 2. Start facilitator (ts) - -Add step: - - Build server/facilitator - - Start mock-chainrpc - - Start facilitator - - Start resource server - - Curl + capture - - @json_diff - - Done — generate -``` - -For each selected step, ask the minimal sub-questions needed (workdir, port, command args), then append and loop back. - -### Phase 3 — Expected payload - -For each `@json_diff` step the user added, ask whether to: -- Generate an empty stub (`{}`) — user will fill in later from real run -- Paste a known-good JSON now -- Copy from an existing scenario - -### Phase 4 — Generate files - -Write: -- `e2e/scenarios//config.json` -- `e2e/scenarios//expected_*.json` (one per json_diff step) -- `e2e/scenarios//README.md` with purpose, preconditions, run command - -Tell the user how to run it: - -```bash -python3 integration/run.py --config e2e/scenarios//config.json -``` - -### Phase 5 — Suggest follow-ups - -After generation, suggest: -- Adding an entry to `e2e/scenarios/README.md` (index) -- Wiring into `.github/workflows/` once stable -- Adding a `solutions.md` entry (`/x402:compound`) if the scenario was motivated by a specific bug diff --git a/.claude/rules/CLAUDE.md b/.claude/rules/CLAUDE.md index 8205bcde..6a799a72 100644 --- a/.claude/rules/CLAUDE.md +++ b/.claude/rules/CLAUDE.md @@ -2,19 +2,22 @@ This is the rules knowledge base. Every file here is a **self-contained rule sheet** Claude loads on demand when the touched code matches its scope. Rules override defaults; CLAUDE.md at the repo root sets the floor. +The SDK is a **TypeScript-only** pnpm/turbo monorepo under [typescript/](../../typescript/CLAUDE.md). `core` and `mechanisms/evm` are **upstream forks** (keep byte-identical, additions go in overlays); `mechanisms/tron` is in-house. There is no Python in the current SDK. (The previous-generation Python + old TypeScript code lives under `legacy/` and is slated for removal — these rules do **not** cover it.) + ## When Claude reads what | Scope of the change | Always read | Also read | |---|---|---| | Any file in the repo | [common/conventions.md](common/conventions.md) | — | -| Python code under `python/x402/` | common/ | [python/conventions.md](python/conventions.md) | | TypeScript under `typescript/packages/` | common/ | [typescript/conventions.md](typescript/conventions.md) | -| Scheme `exact` (ERC-3009) code or spec | common/ | [schemes/exact.md](schemes/exact.md) | -| Scheme `exact_permit` (EIP-2612) code or spec | common/ | [schemes/exact-permit.md](schemes/exact-permit.md) | -| Scheme `exact_gasfree` (TRON GasFree) code or spec | common/ | [schemes/exact-gasfree.md](schemes/exact-gasfree.md) + [networks/tron.md](networks/tron.md) | -| TRON mechanisms / config | common/ | [networks/tron.md](networks/tron.md) | -| EVM mechanisms / config | common/ | [networks/evm.md](networks/evm.md) | -| e2e scenarios (`e2e/scenarios/`, `integration/`) | common/ | [testing/scenarios.md](testing/scenarios.md) | +| Scheme `exact` (ERC-3009 / Permit2) | common/ | [schemes/exact.md](schemes/exact.md) | +| Scheme `upto` (usage-based) | common/ | [schemes/upto.md](schemes/upto.md) | +| Scheme `batch-settlement` (payment channels) | common/ | [schemes/batch-settlement.md](schemes/batch-settlement.md) | +| Scheme `auth-capture` (EVM authorize+capture) | common/ | [schemes/auth-capture.md](schemes/auth-capture.md) | +| Scheme `exact_gasfree` (TRON GasFree) | common/ | [schemes/exact-gasfree.md](schemes/exact-gasfree.md) + [networks/tron.md](networks/tron.md) | +| TRON mechanisms (`mechanisms/tron`) | common/ | [networks/tron.md](networks/tron.md) | +| EVM mechanisms (`mechanisms/evm`) | common/ | [networks/evm.md](networks/evm.md) | +| Tests (`packages/*/test/**`) | common/ | [testing/conventions.md](testing/conventions.md) | When multiple rule files apply, read all of them before editing. Rules are layered, not exclusive. @@ -25,18 +28,18 @@ When multiple rule files apply, read all of them before editing. Rules are layer ├── common/ │ └── conventions.md # repo-wide: addressing, amounts, headers, pipeline, commit style ├── schemes/ -│ ├── exact.md # ERC-3009 transferWithAuthorization -│ ├── exact-permit.md # EIP-2612 / TIP-2612 permit + transferFrom +│ ├── exact.md # ERC-3009 / Permit2 (extra.assetTransferMethod) +│ ├── upto.md # usage-based: sign up-to-max, settle actual +│ ├── batch-settlement.md # payment channels: deposit + off-chain vouchers + refund +│ ├── auth-capture.md # EVM authorize then capture │ └── exact-gasfree.md # TRON GasFree custodial + TIP-712 relayer ├── networks/ -│ ├── evm.md # eip155:, contracts, signing, smoke tests +│ ├── evm.md # eip155:, contracts, signing │ └── tron.md # tron:, TIP-712 hex-address rule, GasFree link -├── python/ -│ └── conventions.md # Python 3.11+, uv, pytest, pydantic v2, async-first ├── typescript/ -│ └── conventions.md # Node 20+, pnpm, ESM, viem, BigInt, named exports +│ └── conventions.md # pnpm/turbo, ESM, viem/tronweb, fork+overlay, signer factories └── testing/ - └── scenarios.md # e2e/scenarios/ JSON scenario authoring + ports + tiers + └── conventions.md # vitest unit + integration layout, self-skip, mocking ``` ## Authoring a new rule @@ -45,35 +48,31 @@ Add a rule **only when the knowledge is non-obvious and would otherwise be relea Structure every rule file: -1. **Header** — one line: what scope this covers + pointer to the canonical spec under `specs/`. +1. **Header** — one line: what scope this covers + pointer to the authoritative source (the package source, or [typescript/CLAUDE.md](../../typescript/CLAUDE.md)). 2. **When to use** — the decision the rule answers. 3. **Key invariants** — the short list that must hold. -4. **Common gotchas** — cite commits or `docs/solutions.md` entries. Every bullet should map to a real incident. +4. **Common gotchas** — cite commits. Every bullet should map to a real incident. 5. **Testing** — where the tests live and what template to reuse. ### New scheme -1. New spec at `specs/.md`. -2. New rule at `.claude/rules/schemes/.md` — link back to the spec. -3. Register the scheme in the mechanism and the `config.md` registry. -4. Scenario skeleton: `/x402:create-scenario` (see [../commands/x402/create-scenario.md](../commands/x402/create-scenario.md)). +1. New scheme classes per role under `mechanisms//src//{client,facilitator,server}` — `Scheme`, same class name across roles, role = import subpath (see [typescript/CLAUDE.md](../../typescript/CLAUDE.md)). +2. New rule at `.claude/rules/schemes/.md`. +3. Register the scheme on the client/facilitator with `new + register` (thin pass-through register helpers are not added). ### New network -1. Add to `specs/config.md` registry. -2. New rule at `.claude/rules/networks/.md` — chain ids, signing rules, RPC defaults, contract table. -3. If it introduces a new signing flavor (e.g. TIP-712 analogue), document the conversion helpers. +1. New rule at `.claude/rules/networks/.md` — chain ids, signing rules, RPC defaults, contract table. +2. If it introduces a new signing flavor (e.g. a TIP-712 analogue), document the conversion helpers. ## Relation to other surfaces -- **specs/** — authoritative protocol definitions. Rules cite specs; specs do not cite rules. -- **docs/solutions.md** — incident log. Rules point here when a gotcha came from a real bug; new solutions are added via `/x402:compound`. -- **.claude/agents/** — specialized reviewers (`code-reviewer`, `security-reviewer`, `scheme-author`) that load the rules relevant to their domain. Payment-path / signing changes route through `security-reviewer`. -- **.claude/commands/x402/** — slash-command wizards that scaffold code + scenarios conforming to these rules. +- **[typescript/CLAUDE.md](../../typescript/CLAUDE.md)** — the SDK's own build/test/layout guide and the fork/overlay + signer-factory + scheme-API rules. Rules here cite it; it is the authoritative source for structure. +- **.claude/agents/** — specialized reviewers (`code-reviewer`, `security-reviewer`) that load the rules relevant to their domain. Payment-path / signing changes route through `security-reviewer`. +- **.claude/commands/x402/** — slash-command wizards that scaffold code conforming to these rules. ## Don'ts -- **No code examples longer than 10 lines** in a rule. If it needs more, it belongs in `specs/` or an example. -- **No duplicating specs.** Rules are *behavioral guidance*; specs are *definitions*. Link, don't copy. +- **No code examples longer than 10 lines** in a rule. - **No rule without a scope.** If you can't say which file touches trigger it, it belongs in `CLAUDE.md` instead. - **No rules that rot.** Anything citing a specific line number or commit that isn't a historical anchor will drift — prefer filenames and invariants. diff --git a/.claude/rules/common/conventions.md b/.claude/rules/common/conventions.md index d726e223..6059b69e 100644 --- a/.claude/rules/common/conventions.md +++ b/.claude/rules/common/conventions.md @@ -1,17 +1,17 @@ # x402 Conventions -Repository-wide conventions that apply to every component, every language, every scheme. Break these silently and you will produce bugs that take days to diagnose. +Repository-wide conventions that apply to every component, every scheme. Break these silently and you will produce bugs that take days to diagnose. ## Addressing - **Lowercase everything**: addresses, selectors, event topics. -- **TRON + EIP/TIP-712**: convert TRON Base58 addresses (`T...`) to **0x-prefixed EVM hex** before passing to any EIP-712 / TIP-712 signer. This applies to `verifyingContract`, `token`, `serviceProvider`, `user`, `receiver`, and any other `address`-typed field, **both in the domain and in the message**. See [docs/solutions.md entry #1](../../../docs/solutions.md). -- **Network identifiers**: CAIP-2 `eip155:` for EVM, `tron:` for TRON. Detection rule: `tron:` prefix → TRON, `eip155:` prefix → EVM. See [specs/config.md](../../../specs/config.md). +- **TRON + EIP/TIP-712**: convert TRON Base58 addresses (`T...`) to **0x-prefixed EVM hex** before passing to any EIP-712 / TIP-712 signer. This applies to `verifyingContract`, `token`, `serviceProvider`, `user`, `receiver`, and any other `address`-typed field, **both in the domain and in the message**. Use `tronAddressToEvm` / `normalizeAddressForSigning` from `mechanisms/tron/src/utils.ts` — do not reimplement. See [docs/solutions.md entry #1](../../../docs/solutions.md). +- **Network identifiers**: CAIP-2 `eip155:` for EVM, `tron:` for TRON. Detection rule: `tron:` prefix → TRON, `eip155:` prefix → EVM. ## Amounts & fees - **Fees in millionths** (e.g. `3000` = 0.3%). -- **Token amounts in smallest unit** (wei / sun). Never use floating point. Use `BigInt` in TS, `int` in Python. +- **Token amounts in smallest unit** (wei / sun). Never use floating point. Use `BigInt` in TS; serialize with `.toString()`, never through `Number`. ## Payment identifiers @@ -20,7 +20,7 @@ Repository-wide conventions that apply to every component, every language, every ## HTTP wire format - **Header encoding**: `Base64(UTF-8(JSON.stringify(object)))`. -- **Headers** (see [specs/protocol.md](../../../specs/protocol.md)): +- **Headers**: - `PAYMENT-REQUIRED` — server → client (402 response) - `PAYMENT-SIGNATURE` — client → server (retry request) - `PAYMENT-RESPONSE` — server → client (success response, with settlement info) @@ -37,11 +37,11 @@ Repository-wide conventions that apply to every component, every language, every ## Mechanism registration - **Exact match** (e.g. `"tron:nile"`) outranks **wildcard** (e.g. `"tron:*"`). Register more specific first, or accept that exact-match mechanisms win the lookup regardless of insertion order. -- Each mechanism is associated with a scheme (`exact`, `exact_permit`, `exact_gasfree`, ...). Do not conflate schemes across mechanisms; register each scheme-network pair explicitly. +- Each scheme class is registered per `(network, scheme)` with `new + register` (no thin wrapper). Do not conflate schemes across mechanisms; register each scheme-network pair explicitly. See [typescript/CLAUDE.md](../../../typescript/CLAUDE.md) "Scheme API surface". ## Policy pipeline -Client payment selection is a five-step pipeline (see [specs/roles.md](../../../specs/roles.md)): +Client payment selection is a five-step pipeline: ``` [1] Filter by scheme → [2] Filter by network → [3] Filter by max amount @@ -56,4 +56,4 @@ Format: `(): `. Types: `feat`, `fix`, `refactor`, `docs`, `test`, `chore`, `perf`, `ci`. -Scopes (choose the narrowest one that fits): `python`, `typescript`, `tron`, `evm`, `gasfree`, `facilitator`, `client`, `server`, `specs`, `docs`, `ci`, `design`. +Scopes (choose the narrowest one that fits): `typescript`, `tron`, `evm`, `gasfree`, `facilitator`, `client`, `server`, `core`, `http`, `mcp`, `docs`, `ci`, `design`. diff --git a/.claude/rules/networks/evm.md b/.claude/rules/networks/evm.md index 02adb36f..68ce153c 100644 --- a/.claude/rules/networks/evm.md +++ b/.claude/rules/networks/evm.md @@ -1,6 +1,6 @@ # Network: EVM -**Config**: [specs/config.md](../../../specs/config.md) +Mechanisms: `typescript/packages/mechanisms/evm` (`@bankofai/x402-evm`). **Upstream fork** — see [typescript/conventions.md](../typescript/conventions.md). Identifier: CAIP-2 `eip155:`. Chain ID is a decimal integer after the colon. @@ -13,25 +13,20 @@ Identifier: CAIP-2 `eip155:`. Chain ID is a decimal integer after the c ## Signing rules -- EIP-712 typed data signing. -- All address fields lowercase-normalized (except within the signature payload where they must be checksummed only if the contract expects checksummed). -- Default tooling: `viem` + `viem/accounts` in TS, `eth_account` in Python. No new wrappers — use existing helpers in `typescript/packages/x402/src/mechanisms/` and `python/x402/src/bankofai/x402/mechanisms/evm/`. +- EIP-712 typed data signing via `viem`. +- All address fields lowercase-normalized (checksum only at the boundary of APIs that require it). +- Signer factories: `createClientEvmSigner` / `createFacilitatorEvmSigner` / `createAuthorizerEvmSigner` from `@bankofai/x402-evm/adapters/agent-wallet`. They build the viem client from the CAIP-2 `network`; pass `{ network, rpcUrl? }`. Do not construct a chain client by hand. -## Contracts - -| Role | Fallback if unconfigured | -|---|---| -| `PaymentPermit` | EVM zero address | -| Facilitator | required — no implicit fallback | +## Schemes & token support -BSC has a deployed `PaymentPermit` at `0x1825bB32db3443dEc2cc7508b2D818fc13EaD878` on both mainnet and testnet. See `specs/config.md` for the authoritative registry. +- `exact` — single fixed payment. Transfer method selected by `extra.assetTransferMethod`: **`eip3009`** (ERC-3009 `transferWithAuthorization`, signs offline) or **`permit2`** (Uniswap Permit2, reads the chain). See [schemes/exact.md](../schemes/exact.md). +- `upto` — usage-based ([schemes/upto.md](../schemes/upto.md)). `batch-settlement` — payment channels ([schemes/batch-settlement.md](../schemes/batch-settlement.md)). `auth-capture` — authorize+capture ([schemes/auth-capture.md](../schemes/auth-capture.md)). -## Token support +## RPC -- **ERC-3009** (`transferWithAuthorization`): prefer `exact` scheme. -- **EIP-2612** (`permit`): use `exact_permit`. -- Neither: not yet supported; a new scheme is needed (likely `exact_approve` with a separate approval step). +- viem's default public BSC-testnet node (`data-seed-prebsc-*.bnbchain.org:8545`) is frequently unreachable. Permit2 / channel-state reads need a working RPC; pass `rpcUrl` (or set the example's `EVM_RPC_URL`) to a reliable endpoint, e.g. `https://bsc-testnet-rpc.publicnode.com`. ERC-3009 (`eip3009`) signs offline and does not need RPC. -## Smoke tests +## Contracts -`examples/bsc-testnet-smoke/` validated bidirectional compatibility with Coinbase upstream on 2026-04-03. See [specs/001-exact-v2-compat/spec.md](../../../specs/001-exact-v2-compat/spec.md). +- Permit2 is the canonical singleton `0x000000000022D473030F116dDEE9F6B43aC78BA3` on every chain. +- Other contract addresses (PaymentPermit, batch channel manager, etc.) come from the in-tree config registry. Facilitator address is required — no implicit fallback. diff --git a/.claude/rules/networks/tron.md b/.claude/rules/networks/tron.md index 1da19d50..73d3b41f 100644 --- a/.claude/rules/networks/tron.md +++ b/.claude/rules/networks/tron.md @@ -1,6 +1,6 @@ # Network: TRON -**Config**: [specs/config.md](../../../specs/config.md) +Mechanisms: `typescript/packages/mechanisms/tron` (`@bankofai/x402-tron`). **In-house, no upstream** — edit directly. Identifier: `tron:` where `` ∈ {`mainnet`, `shasta`, `nile`}. @@ -10,26 +10,28 @@ Identifier: `tron:` where `` ∈ {`mainnet`, `shasta`, `nile`}. | `tron:shasta` | 2494104990 | 0x94a9059e | | `tron:nile` | 3448148188 | 0xcd8690dc | +Chain id helper: `getTronChainId(network)` in `mechanisms/tron/src/utils.ts`. + ## Signing rules -- TIP-712 is TRON's EIP-712 analogue. Structurally identical; same struct-hash / domain-separator algorithm. +- TIP-712 is TRON's EIP-712 analogue. Structurally identical; same struct-hash / domain-separator algorithm. Uses the in-tree TIP-712 signer — do not add a new lib. - **Every `address` field** — domain and message — must be **0x-prefixed EVM hex**. Passing Base58 produces a valid-looking but wrong signature. -- Convert Base58 ↔ hex via existing helpers (`tron-utils.addressToHex` in TS, `bankofai.x402.utils.tron.to_hex_address` in Python). Do not reimplement. +- Convert Base58 → hex via `tronAddressToEvm` / `normalizeAddressForSigning` (`mechanisms/tron/src/utils.ts`). Do not reimplement. +- Signer factories: `createClientTronSigner` / `createFacilitatorTronSigner` / `createAuthorizerTronSigner` from `@bankofai/x402-tron`. Pass `{ network, apiKey? }`; the factory builds TronWeb internally. ## Gas & fees - Native token: TRX. 1 TRX = 1,000,000 SUN. -- Energy/Bandwidth: TRON has a dual-resource fee model; consuming contracts charges energy, consuming bandwidth charges bandwidth. For x402 purposes, the facilitator pays these resources; clients only care about the token transfer. +- TRON has a dual-resource fee model (energy for contract calls, bandwidth for transfers). For x402 the facilitator (or, in GasFree, the relayer) pays these; clients only care about the token transfer. -## GasFree integration +## Schemes -See [.claude/rules/schemes/exact-gasfree.md](../schemes/exact-gasfree.md). +- `exact`, `upto`, `batch-settlement`, and `exact_gasfree` (TRON-only gasless). See [schemes/exact-gasfree.md](../schemes/exact-gasfree.md). ## RPC -- Default RPC: TronGrid when `TRON_GRID_API_KEY` is set. Otherwise the BankofAI-operated fallback `https://hptg.bankofai.io` (set in the client transparently). -- Node client: `tronweb` for JS, `tronpy` / in-house `bankofai.x402.mechanisms.tron.*` for Python. +- Default RPC: TronGrid when `TRON_GRID_API_KEY` is set; otherwise the BankofAI-operated fallback `https://hptg.bankofai.io` (wired transparently). Node client: `tronweb`. ## Contracts -See `specs/config.md` for the live registry. Mainnet `PaymentPermit` is `TT8rEWbCoNX7vpEUauxb7rWJsTgs8vDLAn`; Nile is `TFxDcGvS7zfQrS1YzcCMp673ta2NHHzsiH`. TronScan verify all deployed contracts. +Contract addresses come from the in-tree config registry; TronScan-verify all deployed contracts before referencing a new one. diff --git a/.claude/rules/python/conventions.md b/.claude/rules/python/conventions.md deleted file mode 100644 index d25d3162..00000000 --- a/.claude/rules/python/conventions.md +++ /dev/null @@ -1,39 +0,0 @@ -# Python conventions (x402) - -Target: **Python 3.11+**. Package: `bankofai-x402` (`src/bankofai/x402/`). - -## Tooling - -- **Deps**: `uv` (`uv sync --group dev`, `uv run pytest`). Do not commit `pip` artefacts. -- **Build**: `hatchling`. Do not switch build backends. -- **Tests**: `pytest` + `pytest-asyncio`. Test discovery and logging format are pinned in `pytest.ini` — do not relax. -- **Type check**: `mypy` (strict in CI). Keep the module green; don't `# type: ignore` without a comment explaining why. -- **Lint**: project does not yet enforce ruff/black via CI, but code should be formatted as if it did. Don't let whitespace churn dominate diffs. - -## Structure - -- Single namespace package under `src/bankofai/x402/`. One concept per subpackage (`mechanisms/`, `facilitator/`, `server/`, `clients/`). -- `types.py` + `schemas/` hold **protocol-shape** Pydantic models. Keep these 1:1 with the wire format. -- Never shadow stdlib names (`types`, `http`, `config`) outside of this package's own namespace. - -## Idioms - -- **Pydantic v2** — use `model_dump(mode="json")` for wire output; `model_validate` for input. Never `dict()`, never `parse_obj`. -- **Async-first client code**. Sync wrappers delegate via `asyncio.run` at the entry point; don't interleave blocking and async in library code. -- **HTTP**: `httpx.AsyncClient`. Configure with explicit timeouts; never rely on the default. -- **Logging**: module-level `logger = logging.getLogger(__name__)`. No `print` in library code. No `logging.basicConfig` — that's the application's call, not the library's. -- **Exceptions**: `X402Error` hierarchy in `exceptions.py`. Map facilitator error codes to specific subclasses; don't collapse them to a generic `FacilitatorError`. -- **BigInt equivalent**: Python `int` is arbitrary-precision. Never convert amounts through `float`, even briefly for formatting. - -## Tests - -- Mirror source tree: `tests//test_.py`. -- Mocks live in `tests/mocks/` — reuse before inventing. -- Async tests: `@pytest.mark.asyncio`, default loop `asyncio` (already set). -- When mocking HTTP, use `httpx.MockTransport`, not `respx` (the repo is `httpx`-native). - -## Don'ts - -- **Don't pin dependencies to exact versions** in `pyproject.toml` without a reason in the commit message. -- **Don't import across component boundaries** at module load time — i.e. `mechanisms/` must not import from `server/`. Shared helpers belong in `utils/`. -- **Don't catch bare `Exception` in the pay path.** Be specific; let unexpected errors surface. diff --git a/.claude/rules/schemes/auth-capture.md b/.claude/rules/schemes/auth-capture.md new file mode 100644 index 00000000..8371cb9d --- /dev/null +++ b/.claude/rules/schemes/auth-capture.md @@ -0,0 +1,24 @@ +# Scheme: `auth-capture` + +Code: `mechanisms/evm/src/auth-capture/**`. Class `AuthCaptureEvmScheme`. **EVM-only** (no TRON). No register helper — use `new + register`. + +`auth-capture` adds **refundable payments** to x402, built on Base's audited [Commerce Payments Protocol](https://github.com/base/commerce-payments). The client signs a single payload (ERC-3009 **or** Permit2) over a payer-agnostic `PaymentInfo` hash. A facilitator submits it to `AuthCaptureEscrow`, where funds are **held under a `captureAuthorizer` role** rather than transferred straight to the merchant — enabling **capture, void, and refund** before settlement is final. + +## When to use + +- Payment that must be authorized first and captured (or voided / refunded) later — pre-auth / hold semantics, not an immediate transfer. + +## Key invariants + +- **`extra.captureAuthorizer` is required.** It is committed on-chain as `PaymentInfo.operator`; `authorize` / `capture` / `void` / `refund` / `charge` on `AuthCaptureEscrow` are gated by `onlySender(operator)`, so it must be the `msg.sender` of the `Authorize` call. +- Transfer method (ERC-3009 vs Permit2) follows the same `extra.assetTransferMethod` selection as `exact`; default is the ERC-3009 `ReceiveWithAuthorization` collector. +- `minFeeBps` / `maxFeeBps` bound the `captureAuthorizer`'s fee (in bps; `0` minimum = no floor). +- `AuthCaptureEscrow` + token-collector deployments come from the in-tree canonical address constants — don't hardcode. + +## Testing + +`mechanisms/evm/test/unit/auth-capture/**`. + +## Safety + +Escrow authorize/capture/void/refund + signing — **security-reviewer territory**. diff --git a/.claude/rules/schemes/batch-settlement.md b/.claude/rules/schemes/batch-settlement.md new file mode 100644 index 00000000..95d949c3 --- /dev/null +++ b/.claude/rules/schemes/batch-settlement.md @@ -0,0 +1,27 @@ +# Scheme: `batch-settlement` + +Code: `mechanisms/{evm,tron}/src/batch-settlement/{client,facilitator,server}`. Classes `BatchSettlementEvmScheme` / `BatchSettlementTronScheme`. No register helper — use `new + register`. The aggregate entry (`@bankofai/x402-/batch-settlement`) re-exports the **client** scheme plus helpers (`signVoucher`, `InMemoryClientChannelStorage`, `computeChannelId`); server/facilitator classes come from their role subpaths only. + +`batch-settlement` uses **payment channels**: the payer deposits once on-chain, then pays many requests with off-chain **vouchers**; the server's channel manager claims a batch and settles to `payTo`. A **refund** path returns the remaining channel balance. + +## When to use + +- Many small payments to the same server (deposit once, amortize gas across N requests). +- The payer wants to reclaim the unspent deposit afterward (refund). + +## Key invariants + +- Deposit/channel state is read from chain (allowance, channel balance), so the EVM path **needs a working RPC** ([networks/evm.md](../networks/evm.md)). +- Vouchers are signed off-chain and accumulate; the facilitator claims the batch on-chain. + +## Common gotchas + +- **Refund must select its own VM family's accept** (commit `d667b9e`). A route can advertise `accepts[]` for multiple networks; the refund probe must filter by chain family (`a.network.startsWith("tron:")` / `"eip155:"`) before picking the channel — taking the first scheme match can hand an `eip155:*` accept to the TRON refund (or vice-versa) and crash in the chain-id helper. Both the TRON and EVM refund probes were fixed; the EVM file is upstream-derived and the same latent bug exists upstream. + +## Testing + +`mechanisms/evm/test/unit/batch-settlement/**` (incl. `refund-network.test.ts`), `mechanisms/tron/test/unit/batch-settlement/**` and `batch-refund-network.test.ts` — the network tests mock a multi-network 402 (foreign family first) + a `readContract` sentinel to prove the correct family is selected. + +## Safety + +Channel deposit / claim / refund + signing — **security-reviewer territory**. diff --git a/.claude/rules/schemes/exact-gasfree.md b/.claude/rules/schemes/exact-gasfree.md index 22bd3a76..594a428f 100644 --- a/.claude/rules/schemes/exact-gasfree.md +++ b/.claude/rules/schemes/exact-gasfree.md @@ -1,8 +1,8 @@ # Scheme: `exact_gasfree` -**Spec**: [specs/schemes/exact-gasfree.md](../../../specs/schemes/exact-gasfree.md) +Code: `mechanisms/tron/src/gasfree/{client,facilitator,server}`. Class `ExactGasFreeTronScheme`. **TRON-only.** Wire it with `registerExactGasFreeTronScheme` (it assembles the GasFree relayer API clients — `createGasFreeApiClients`, base-URL registry — that callers shouldn't build by hand). -`exact_gasfree` enables **gasless payments on TRON**. Users hold tokens in a GasFree custodial wallet and sign TIP-712 permits. A service provider (relayer) submits the transaction on-chain, paying TRX gas and collecting a `maxFee`. **TRON-only.** +`exact_gasfree` enables **gasless payments on TRON**. Users hold tokens in a GasFree custodial wallet and sign TIP-712 permits. A service provider (relayer) submits the transaction on-chain, paying TRX gas and collecting a `maxFee`. ## When to use @@ -13,22 +13,19 @@ ## Key invariants - Domain is `{"name": "GasFreeController", "version": "V1.0.0", "chainId": , "verifyingContract": }`. -- All address fields — `verifyingContract`, `token`, `serviceProvider`, `user`, `receiver` — are **EVM hex**, not Base58. See [docs/solutions.md entry #1](../../../docs/solutions.md). +- All address fields — `verifyingContract`, `token`, `serviceProvider`, `user`, `receiver` — are **EVM hex**, not Base58 ([networks/tron.md](../networks/tron.md)). - Funds are in `gasFreeAddress`, not the main wallet. +- **`feeTo` is the relayer's registered service provider.** Verify rejects any other `feeTo` with `gasfree_fee_to_mismatch`. The facilitator must not advertise its own wallet as `feeTo` — only emit `feeTo` when it is an actual registered provider (commit `c4472cc`). The client resolves the provider from `fee.feeTo` if present, else picks from the providers endpoint. ## Common gotchas — all caught in production already, do not repeat -- **Address format** ([solutions.md #1](../../../docs/solutions.md)): TIP-712 requires 0x-prefixed hex for every address field. Passing Base58 produces a signature that looks valid but verifies to the wrong address. `PR #53` fixed this. -- **Deadline bounds** ([solutions.md #2](../../../docs/solutions.md)): mainnet enforces `[50, 600]` seconds, testnets `[50, 3600]`. Clamp per-network with 5s padding on each side: mainnet `[55, 595]`, testnet `[55, 3595]`. Warn when clamping. `PR #56` fixed this. -- **Balance source** ([solutions.md #3](../../../docs/solutions.md)): `SufficientBalancePolicy` must call `mechanism.checkBalance()` (which queries `gasFreeAddress` via GasFree API), **not** `signer.checkBalance()` (which queries the main wallet). Main wallet may be empty while GasFree wallet has funds. `PR #59` fixed this. -- **`raw_data_hex` preservation** (commit `e6f4cb7`): when constructing TRON approval transactions, preserve `raw_data_hex` as-is on the payload; do not recompute. +- **Address format**: TIP-712 requires 0x-prefixed hex for every address field. Base58 → a signature that verifies to the wrong address. +- **Deadline bounds**: mainnet enforces `[50, 600]`s, testnets `[50, 3600]`s. Clamp per-network with 5s padding: mainnet `[55, 595]`, testnet `[55, 3595]`. Warn when clamping. +- **Balance source**: balance checks must query `gasFreeAddress` via the GasFree API, **not** the main wallet (which may be empty while the GasFree wallet has funds). ## Testing -`python/x402/tests/exact/test_tron_client.py`, `test_tron_facilitator.py`. GasFree-specific: - -- Mock the GasFree API for deadline clamping tests. -- Use TRON zero-address fallback in config. +`mechanisms/tron/test/unit/gasfree-*.test.ts` (`gasfree-api`, `gasfree-digest`, `gasfree-feeto`, `gasfree-flow`). Mock the GasFree API for deadline-clamping and feeTo tests. ## Safety diff --git a/.claude/rules/schemes/exact-permit.md b/.claude/rules/schemes/exact-permit.md deleted file mode 100644 index 890a0e98..00000000 --- a/.claude/rules/schemes/exact-permit.md +++ /dev/null @@ -1,27 +0,0 @@ -# Scheme: `exact_permit` - -**Spec**: [specs/schemes/exact-permit.md](../../../specs/schemes/exact-permit.md) - -`exact_permit` pays a fixed amount using **EIP-2612 / TIP-2612 `permit`** for tokens that do not support ERC-3009 `transferWithAuthorization`. The user signs a permit; the facilitator submits `permit` + `transferFrom` as a settlement transaction. - -## When to use - -- Fixed price, single request, like `exact`. -- Token supports `permit` (EIP-2612) but **not** `transferWithAuthorization`. -- Payer has the token balance and can sign. - -## Key invariants - -- Permit is scoped to the facilitator as `spender`, not the resource server. -- `deadline` must be validated both at signing time (client) and at settlement (facilitator). -- The facilitator settlement path is **two calls on-chain** (permit + transferFrom), typically batched in a single tx. - -## Common gotchas - -- **Signer domain mismatch**: the permit domain (`name`, `version`, `chainId`, `verifyingContract`) must exactly match what the token contract computes on-chain. Mismatches produce valid signatures that revert on-chain with "ERC20Permit: invalid signature". -- **Replay**: nonces are per-owner. Do not cache nonces across sessions — always re-read. -- **BSC tokens**: some BSC stablecoins use a non-standard permit. Verify against the deployed token's `DOMAIN_SEPARATOR()` before shipping a new network. - -## Testing - -See [specs/schemes/exact-permit.md](../../../specs/schemes/exact-permit.md) for the on-chain flow. Smoke paths live in `examples/bsc-testnet-smoke/`. diff --git a/.claude/rules/schemes/exact.md b/.claude/rules/schemes/exact.md index 89073b58..abf1ee75 100644 --- a/.claude/rules/schemes/exact.md +++ b/.claude/rules/schemes/exact.md @@ -1,29 +1,37 @@ # Scheme: `exact` -**Spec**: [specs/schemes/exact.md](../../../specs/schemes/exact.md) +Code: `mechanisms/{evm,tron}/src/exact/{client,facilitator,server}`. Classes `ExactEvmScheme` / `ExactTronScheme` (same name per role, role = import subpath). -The `exact` scheme pays a fixed, pre-quoted amount in a specified token for a single request. Uses ERC-3009-style `transferWithAuthorization` where the token supports it. +The `exact` scheme pays a fixed, pre-quoted amount in a specified token for a single request. ## When to use -- The resource has a fixed price known in advance. -- The token supports `transferWithAuthorization` (ERC-3009). +- The resource has a fixed price known in advance, single request. - The payer has the token balance and can sign. +## Transfer method — `extra.assetTransferMethod` + +The single `exact` scheme covers both authorization shapes; the client picks based on `paymentRequirements.extra.assetTransferMethod` (defaults to `"eip3009"`): + +- **`eip3009`** — ERC-3009 `transferWithAuthorization`. Token must support ERC-3009. **Signs offline** (no RPC). Single-step settle. +- **`permit2`** — Uniswap Permit2 (canonical singleton `0x0000…78BA3`). For tokens without ERC-3009. **Reads the chain** to sign the gas-sponsored approve, so it needs a working RPC. + +A server may advertise multiple `accepts[]` entries with different `assetTransferMethod` values so the client picks whichever matches its token approvals. There is **no separate `exact_permit` scheme** — permit is a transfer method of `exact`. + ## Key invariants -- `payload.authorization` carries the transfer authorization (aligned with Coinbase x402 v2 wire format). -- `extensions.transferAuthorization` is accepted as a temporary fallback during migration — **do not emit it in new code**. -- Settlement is single-step: the facilitator submits the signed authorization on-chain. +- `payload.authorization` carries the transfer authorization (Coinbase x402 v2 wire format). +- **`validAfter` / `validBefore`**: validate *both* on the server pre-check path. +- **Network match**: the server pre-check must verify `payload.network` matches the configured network; cross-network replay is otherwise possible. ## Common gotchas -- **Token must support ERC-3009**. If it doesn't, use `exact_permit` instead. -- **`validAfter` / `validBefore`**: check both on the server pre-check path. See commit `c9e53b8` — missing `validAfter` check was a review finding. -- **Network match**: the server pre-check must verify `payload.network` matches the configured network; cross-network replay is otherwise possible. See commit `c9e53b8`. +- `eip3009` tokens (e.g. DHLU on BSC testnet) sign offline; `permit2` tokens (e.g. USDC) fail without a reachable RPC — see [networks/evm.md](../networks/evm.md) RPC note. ## Testing -Unit tests live in `python/x402/tests/exact/` and `typescript/packages/x402/src/mechanisms/nativeExactEvm.*`. +Unit tests: `mechanisms/evm/test/unit/exact/**`, `mechanisms/tron/test/unit/` (`flow-integration`, `permit2-digest`). + +## Safety -When writing a new scenario, reuse the `exact_permit_happy_path` template as the skeleton. +Signing + settlement code is **security-reviewer territory**. Route changes through `.claude/agents/security-reviewer.md`. diff --git a/.claude/rules/schemes/upto.md b/.claude/rules/schemes/upto.md new file mode 100644 index 00000000..1fbdb527 --- /dev/null +++ b/.claude/rules/schemes/upto.md @@ -0,0 +1,27 @@ +# Scheme: `upto` + +Code: `mechanisms/{evm,tron}/src/upto/{client,facilitator,server}`. Classes `UptoEvmScheme` / `UptoTronScheme`. EVM + TRON, Permit2-based. No register helper — use `new + register`. + +`upto` is **usage-based billing**. The payer signs an authorization for **up to a maximum** amount; the server decides the **real usage** per request and settles only that (`actual ≤ max`). The facilitator submits the on-chain settlement. + +## When to use + +- Price is not known until after the work is done, but a ceiling is. +- Metered / per-call billing where each request charges a different amount under one signed cap. + +## Key invariants + +- The client signs the **max**; the server chooses `actual ∈ [0, max]` and settles `actual`. Settling more than the signed max must fail. +- Both chains use Permit2, so the EVM path **reads the chain** — needs a working RPC ([networks/evm.md](../networks/evm.md)). + +## Common gotchas + +- **Settle-time verification**: the facilitator's `readContract` simulation of the upto proxy's caller-authorized `settle` must set `account` to the authorized caller, or the simulation fails. This is overlay-wiring, not an upstream bug (see [typescript/CLAUDE.md](../../../typescript/CLAUDE.md)). + +## Testing + +`mechanisms/evm/test/unit/upto/**`, `mechanisms/tron/test/unit/upto-flow.test.ts`. + +## Safety + +Settlement + signing path — **security-reviewer territory**. diff --git a/.claude/rules/testing/conventions.md b/.claude/rules/testing/conventions.md new file mode 100644 index 00000000..4b7f90e2 --- /dev/null +++ b/.claude/rules/testing/conventions.md @@ -0,0 +1,30 @@ +# Testing conventions (x402) + +The SDK is tested with **`vitest`**, co-located per package under `packages/*/test/`. See [typescript/CLAUDE.md](../../../typescript/CLAUDE.md) for the commands. + +## Two tiers + +| Tier | Location | Command | Runs in CI | Needs | +|---|---|---|---|---| +| **Unit** | `packages/*/test/unit/**` | `pnpm test` (`turbo run test`) | yes | nothing — offline, no secrets | +| **Integration** | `packages/*/test/integrations/*.nile.test.ts` | `pnpm test:integration` | no | real-chain `.env` creds | + +- **Unit tests must pass offline with no secrets.** No real RPC, no network. Mock the chain client builder (`createEvmPublicClient` / `buildTronWeb`) rather than hitting a node. +- **Integration tests self-skip** via `describe.skipIf(...)` when their `.env` credentials are absent. **Never gate CI on them.** + +## Layout + +- One suite directory per scheme/area: `test/unit/exact`, `test/unit/upto`, `test/unit/batch-settlement`, `test/unit/auth-capture`, plus top-level `*.test.ts` for signer/server/utils. +- TRON GasFree suites are the `gasfree-*.test.ts` files in `mechanisms/tron/test/unit/`. +- Run one suite: `pnpm --filter @bankofai/x402-evm exec vitest run test/unit/upto`. + +## Conventions + +- **Mock at the boundary, not the chain.** Drive `scheme.()` methods with a mocked facilitator / fetch / `readContract`, and assert on the shaped output — see `batch-refund-network.test.ts` (mocks a multi-network 402 + a `readContract` sentinel to prove the correct VM family is selected). +- **`BigInt` everywhere** in fixtures; never compare amounts through `Number`. +- **Deterministic offline equivalents** of on-chain digests are fair game (e.g. `permit2-digest.test.ts`, `gasfree-digest.test.ts`) — they pin the signing math without a node. +- Co-locate `*.test.ts` with the package; don't add a central test app. Don't shell out to other test runners from a vitest test. + +## Examples as smoke tests + +`examples/typescript/` is a runnable workspace (client + server + facilitator trios per scheme), not part of `pnpm test`. Use it for manual end-to-end smoke runs against a local or hosted facilitator; it builds against local SDK source via workspace links. diff --git a/.claude/rules/testing/scenarios.md b/.claude/rules/testing/scenarios.md deleted file mode 100644 index bb28caad..00000000 --- a/.claude/rules/testing/scenarios.md +++ /dev/null @@ -1,70 +0,0 @@ -# Testing: scenarios & e2e - -Conventions for scenario-style e2e tests under [`e2e/scenarios/`](../../../e2e/scenarios/), driven by [`integration/run.py`](../../../integration/run.py). - -## Philosophy - -- **Declarative JSON, no scenario code.** A scenario is `config.json` + `expected_*.json` + optional `scenario_*.json` (mock-chainrpc). The scenario runner is generic; tests stay readable and add-new is a copy-paste. -- **Diff, don't script.** Use `@json_diff` from `integration/commands.py`. Ordering of JSON keys must not matter — the diff normalizes. -- **Self-contained.** Every scenario starts its own mock facilitator + resource server, on its own ports. A scenario that assumes "the previous scenario left X running" is broken. - -## File conventions - -Under `e2e/scenarios//`: - -| File | Required | Purpose | -|---|---|---| -| `config.json` | yes | Steps for `integration/run.py` | -| `expected_