Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .changeset/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,5 @@
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": []
"ignore": ["examples"]
}
12 changes: 5 additions & 7 deletions .changeset/sdk-authcapture-autocapture-wireformat.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,15 @@
"@x402r/cli": minor
---

authCapture wire format + autoCapture (PR 2/4 of authCapture migration).
authCapture wire format glue and autoCapture builder.

**Breaking**

- `@x402r/helpers` `forwardToArbiter` skips settlements whose scheme is not `'authCapture'` (was `'commerce'`).
- `@x402r/evm` peerDep widened to `>=0.2.0-alpha.0 <0.3.0` on `@x402r/core`, `@x402r/helpers`, `@x402r/cli`.
- `@x402r/core/payment` `toPaymentInfo()` now takes `PaymentInfoStruct` from `@x402r/evm` (was `EscrowPayload`).
- `@x402r/cli` switches `@x402r/evm` import from `registerCommerceEvmScheme` to `registerAuthCaptureEvmScheme`.
- `@x402r/helpers` and `@x402r/cli` widen `@x402r/evm` to `>=0.2.0-alpha.0 <0.3.0`.
- `@x402r/cli` switches from `registerCommerceEvmScheme` to `registerAuthCaptureEvmScheme`.

**New**

- `x402rDefaults(input) → AuthCaptureExtra` from `@x402r/helpers`. Only `captureAuthorizer` is required.
- Wire-format type re-exports from `@x402r/helpers`: `AuthCaptureExtra`, `AuthCapturePayload`, `Eip3009Payload`, `Permit2Payload`, `PaymentInfoStruct` + payload type guards.
- New scenarios: `examples/scenarios/atomic-charge.ts` (atomic `payment.charge()`) and `partial-refund-flow.ts` (`capture(partial)` then `voidPayment()`).
- `x402rDefaults(input) → AuthCaptureExtra` from `@x402r/helpers` — only `captureAuthorizer` is required.
- Wire-format types re-exported from `@x402r/helpers`: `AuthCaptureExtra`, `AuthCapturePayload`, `Eip3009Payload`, `Permit2Payload`, `PaymentInfoStruct`, plus payload type guards.
114 changes: 24 additions & 90 deletions .changeset/sdk-authcapture-lift.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,110 +4,44 @@
"@x402r/helpers": minor
---

Lift SDK to the authCapture contract surface (BackTrackCo/x402r-contracts#34, merged at `9786579`).
Lift the SDK to the authCapture contract surface.

Breaking changes — clean break, no shims:
**Breaking — operator methods**

**Operator method renames**
- `release()` → `capture()`. SDK function and on-chain method both renamed; no back-compat alias.
- `refundInEscrow(paymentInfo, amount, data)` → `voidPayment(paymentInfo, data?)`. The `amount` argument is dropped — the new `escrow.void()` is full-only and empties the entire authorization regardless of any partial value the caller intends. See migration note below.
- `refundPostEscrow()` → `refund()` on both SDK and contract. Renamed helpers: `approvePostEscrowRefund` → `approveRefundAllowance`, `getPostEscrowRefundAllowance` → `getRefundAllowance`.
- `feeRecipient` → `feeReceiver` on `OperatorConfig` (auto-derived from new ABI). `OperatorSlots` return shape from `getOperatorConfig()` also renamed to short-form fields (`authorizeHook`, `captureCondition`, `feeReceiver`, etc.).
- `release()` → `capture()` (SDK + on-chain).
- `refundInEscrow(paymentInfo, amount, data)` → `voidPayment(paymentInfo, data?)`. `void` is full-only and drops the `amount` argument; use partial capture + void remainder for the old partial-refund flow.
- `refundPostEscrow()` → `refund()`. Allowance helpers renamed: `approvePostEscrowRefund` → `approveRefundAllowance`, `getPostEscrowRefundAllowance` → `getRefundAllowance`.
- `OperatorConfig.feeRecipient` → `feeReceiver`. `OperatorSlots` fields renamed to `authorizeHook`, `captureCondition`, `feeReceiver`, etc.

**Plugin terminology**
- `recorder`/`Recorder` → `hook`/`Hook` everywhere (ABIs, factories, types, files, exports, addresses).
- `actions/recorder/` directory renamed to `actions/hook/`. Action functions: `getRecorderPaymentInfo` → `getHookPaymentInfo`, `getPayerPaymentsFromRecorder` → `getPayerPaymentsFromHook`, `getReceiverPaymentsFromRecorder` → `getReceiverPaymentsFromHook`.
- `recorderCombinator`/`recorderCombinatorFactory` → `hookCombinator`/`hookCombinatorFactory` in factory function names, types, and `factories.*` config field.
- `paymentIndexRecorder` → `paymentIndexRecorderHook`; `authorizationTimeRecorder` → `authorizationTimeRecorderHook`. (Tracks the `BackTrackCo/x402r-contracts#36` rename — the post-action slot needs to distinguish recorder-style hooks from arbitrary ones, so `RecorderHook` is back as a suffix.)
- `recorders` config field on `X402rChainConfig` → `hooks`; `RecorderSingletonAddresses` → `HookSingletonAddresses`; `getRecorderSingletons` → `getHookSingletons`.
- `recorderCombinatorCodehash` → `hookCombinatorCodehash`.
- `iRecorderAbi` → `iHookAbi` (and the rest of the recorder→hook ABI consts).
**Breaking — plugin terminology**

**Type renames**
- `ConditionConfig` (constructor's plugin-config arg) → `PluginConfig`. Field shape changed too: now `{authorize,charge,capture,void,refund} × {PreActionCondition, PostActionHook}` per action, `feeReceiver`, `feeCalculator`. Use named-field syntax — positional struct literals will misalign.
- `recorder` / `Recorder` → `hook` / `Hook` across ABIs, factories, types, exports, and addresses.
- `actions/recorder/` → `actions/hook/`. `getRecorderPaymentInfo` → `getHookPaymentInfo`. `getPayerPaymentsFromRecorder` → `getPayerPaymentsFromHook`. `getReceiverPaymentsFromRecorder` → `getReceiverPaymentsFromHook`.
- `recorderCombinator*` → `hookCombinator*`. `paymentIndexRecorder` → `paymentIndexRecorderHook`. `authorizationTimeRecorder` → `authorizationTimeRecorderHook`.
- `X402rChainConfig.recorders` → `hooks`. `RecorderSingletonAddresses` → `HookSingletonAddresses`. `getRecorderSingletons` → `getHookSingletons`. `iRecorderAbi` → `iHookAbi`.

**Slot getters**
- `AUTHORIZE_CONDITION` → `AUTHORIZE_PRE_ACTION_CONDITION`, `AUTHORIZE_RECORDER` → `AUTHORIZE_POST_ACTION_HOOK`, same pattern for charge/capture/void/refund slots. `FEE_RECIPIENT` → `FEE_RECEIVER`. `ConditionSlot` union updated.
**Breaking — types, slots, events, errors**

**Events**
- All five action events renamed to `<Verb>Executed`: `AuthorizationCreated` → `AuthorizeExecuted`, `ReleaseExecuted` → `CaptureExecuted`, `RefundInEscrowExecuted` → `VoidExecuted`, `RefundPostEscrowExecuted` → `RefundExecuted`.
- `VoidExecuted` no longer carries an `amount` field.
- `FeesDistributed.arbiterAmount` → `operatorAmount`.
- `ConditionConfig` (constructor plugin-config arg) → `PluginConfig`. Shape is now `{authorize, charge, capture, void, refund} × {PreActionCondition, PostActionHook}` per action plus `feeReceiver` and `feeCalculator`. Use named-field syntax.
- Slot getters renamed: `AUTHORIZE_CONDITION` → `AUTHORIZE_PRE_ACTION_CONDITION`, `AUTHORIZE_RECORDER` → `AUTHORIZE_POST_ACTION_HOOK`. `FEE_RECIPIENT` → `FEE_RECEIVER`.
- Events renamed to `<Verb>Executed`: `AuthorizationCreated` → `AuthorizeExecuted`, `ReleaseExecuted` → `CaptureExecuted`, `RefundInEscrowExecuted` → `VoidExecuted`, `RefundPostEscrowExecuted` → `RefundExecuted`. `VoidExecuted` no longer carries `amount`. `FeesDistributed.arbiterAmount` → `operatorAmount`.
- Errors: `ConditionNotMet` → `PreActionConditionNotMet`.

**Errors**
- `ConditionNotMet` → `PreActionConditionNotMet`. (`ReleaseLocked` → `CaptureLocked` flows through the new ABI.)
**Breaking — chains and addresses**

**Drops**
- `usdcTvlLimit` removed from canonical config + ABI exports + helpers re-export. Source remains in `x402r-contracts` for ad-hoc per-chain integrations; SDK no longer ships the canonical address. Marketplace/delivery-protection presets now wire `authorizePreActionCondition: zeroAddress` (was the TVL gate).
- `SKALE Base` chain (chainId `1187947933`) dropped from `x402rChains`. SKALE runs Shanghai EVM but the canonical commerce-payments bytecode targets Cancun (TSTORE/TLOAD via Solady's `ReentrancyGuardTransient`). CREATE2 binds bytecode, so Shanghai-recompiled bytecode lands at a different address than the canonical one — single-canonical-address is incompatible without shipping a chain-specific island.
- All chains except Base mainnet (8453) and Base Sepolia (84532) dropped from `x402rChains`. The SDK now points at the canonical `commerce-payments at v1.0.0` AuthCaptureEscrow, which lives on those two chains today; other EVMs return as the canonical primitives extend coverage.
- `x402rChains` reduced to Base mainnet (8453) and Base Sepolia (84532). Canonical addresses now point at the audited `commerce-payments` v1.0.0 deployment of `AuthCaptureEscrow`.
- `usdcTvlLimit` removed from the canonical config and helpers re-export. Marketplace and delivery-protection presets now wire `authorizePreActionCondition: zeroAddress`.

**Canonical addresses**
- `authCaptureEscrow`, `tokenCollector`, and the `commercePayments*` exports point at the canonical `commerce-payments at v1.0.0` deployment (`0xBdEA0D…420cff` / `0x0E3dF951…7A7757` / `0x992476B9…0aB26`).
- `protocolFeeConfig`, `conditions.*`, and the escrow-free entries of `factories.*` (`staticFeeCalculator`, `staticAddressCondition`, `andCondition`, `orCondition`, `notCondition`, `hookCombinator`, `signatureCondition`, `refundRequestEvidence`) live at salt namespace `x402r-canonical-v1::*` — unchanged from prior canonical deployments.
- `receiverRefundCollector` and the escrow-bound entries of `factories.*` (`paymentOperator`, `escrowPeriod`, `freeze`, `refundRequest`) plus `hooks.paymentIndexRecorderHook` are at the new `x402r-canonical-v1.0.1::*` salt namespace, rebound to the canonical escrow. `hooks.paymentIndexRecorderHook` lands at `0x358ECA14fFD51e63D2Bb8DDE3aBAA14f8D5274C3`. See `x402r-contracts/deployments/canonical-v1.0.1.json` for the full set + tx hashes.
- Owner / fee recipient on `ProtocolFeeConfig` is `0x773dBcB5BDb3Df8359ba4e42D7Ce7AE3fC9Ee235`; protocol-fee calculator is unset (`address(0)`), so `getProtocolFeeBps()` returns `0`.
**Breaking — query scoping**

**Workspace dev**
- `@x402r/sdk` and `@x402r/helpers` deps on `@x402r/core` switched to the `workspace:^` protocol (was `^0.2.0`) so local edits resolve through the workspace. `pnpm publish` substitutes the resolved version at publish time; consumer-facing `package.json` is unchanged.
- SDK `query.*` methods (`getPayerPayments`, `getReceiverPayments`, `getPayment`) auto-scope hook reads to the SDK's configured `operatorAddress`. Direct callers of `@x402r/core/actions/hook/*` must pass `operatorAddress` explicitly to opt in. `getPayerPayment` and `getReceiverPayment` narrow to `Promise<PaymentInfo | null>`.

---

## Migration: partial in-escrow refund (R-25)
**Partial in-escrow refund migration**

The new `escrow.void()` is full-only — calling it empties the entire authorization in one transaction. Partial refunds while funds are still in escrow no longer have a single-call equivalent. The replacement is **partial capture** — capture only the amount the merchant intends to keep, then void the remainder back to the payer:
The old `refundInEscrow(paymentInfo, amount)` has no single-call replacement. Use partial capture + void remainder:

```ts
// Old (single call, no longer supported):
// await client.payment.refundInEscrow(paymentInfo, partialAmount)

// New (two calls, no allowance / collector setup):
const { capturableAmount } = await client.payment.getAmounts(paymentInfo)
const merchantAmount = capturableAmount - refundToPayer

// 1. Capture only what the merchant keeps
// (decrements escrow.capturableAmount by merchantAmount)
await client.payment.capture(paymentInfo, merchantAmount, '0x')

// 2. Void what's left — returns the remaining escrowed amount to the payer
// (atomically settles any pending RefundRequest via the voidPostActionHook)
await client.payment.capture(paymentInfo, capturableAmount - refundToPayer, '0x')
await client.payment.voidPayment(paymentInfo)
```

This matches the canonical authCapture semantics: `capture` is incremental (can be called multiple times up to the cumulative authorized amount), and `void` zeros out the remaining `capturableAmount`. No `ReceiverRefundCollector` allowance, no merchant capital movement, no separate refund flow.

For **post-capture refunds** (after the merchant has already captured and wants to refund a customer), use the post-escrow flow:

```ts
// One-time setup: pre-stake an ERC-20 allowance on ReceiverRefundCollector
await client.payment.approveRefundAllowance(token, allowanceAmount)

// Refund any amount up to the standing allowance
await client.payment.refund(paymentInfo, refundAmount, receiverRefundCollector, encodedData)
```

**Recovery if `voidPayment` doesn't land.** The partial-capture flow is two transactions. If the second tx (`voidPayment`) never executes — crash, gas exhaustion, key loss — the payer's remainder sits in escrow under the original authorization. Recovery is on-chain via `AuthCaptureEscrow.reclaim(paymentInfo)`, callable by the payer after `paymentInfo.refundExpiry`. The SDK does not currently ship a `payment.reclaim()` wrapper; call the contract directly via `walletClient.writeContract({ address: authCaptureEscrow, abi: authCaptureEscrowAbi, functionName: 'reclaim', args: [paymentInfo] })`. A typed wrapper is on the PR 2/4 backlog.

---

## Cross-operator data scoping

`PaymentIndexRecorderHook` is a chain singleton — one address per chain shared by every operator that routes through `HookCombinator`. The SDK's `query.*` methods (`getPayerPayments`, `getReceiverPayments`, `getPayment`) automatically scope hook reads to the SDK's configured `operatorAddress`, matching how `createEventProvider` already works. Multi-operator deployments no longer get mingled records by default.

Direct callers of `@x402r/core/actions/hook/*` (`getPayerPaymentsFromHook`, `getReceiverPaymentsFromHook`, `getHookPaymentInfo`, `getPayerPayment`, `getReceiverPayment`) must pass `operatorAddress` explicitly to opt into filtering — by default these return unfiltered (mingled) data matching the contract behavior.

Caveat: the on-chain `total` count returned by `getPayerPaymentsFromHook` and `getReceiverPaymentsFromHook` reflects the unfiltered count even when the returned `payments` array is filtered. Callers needing per-operator-accurate totals must paginate fully and count filtered results client-side. Per-operator-accurate totals would require contract changes (out of scope).

Breaking change: `getPayerPayment` and `getReceiverPayment` (single-record-by-index reads) return type narrowed from `Promise<PaymentInfo>` to `Promise<PaymentInfo | null>` to surface mismatches when `operatorAddress` is set.

`createHookProvider` signature changed: third arg is now `options?: { pageSize?, operatorAddress? }` instead of positional `pageSize?`. Internal-only — not exported from `@x402r/sdk`.

---

## Out of scope (PR 2/3/4 per `x402r-notes/plans/AUTHCAPTURE_SDK_MIGRATION.md`)

- `@x402r/helpers` scheme filter `'commerce' → 'authCapture'` (PR 2)
- `autoCapture` plumbing (PR 2)
- Permit2 support (PR 3)
- `x402rDefaults()` helper + new wire-format type re-exports (PR 4)
- Bumping `@x402r/evm` peerDep version (PR 2)
9 changes: 4 additions & 5 deletions .changeset/sdk-authcapture-permit2.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,10 @@
"@x402r/cli": patch
---

Permit2 support (PR 3 of authCapture migration).
Add Permit2 payer-side helpers.

**New**

- `@x402r/core/payment/permit2`: `signPermit2Authorization`, `createPermit2ApprovalTx`, `getPermit2AllowanceReadParams`, `PERMIT2_ADDRESS`. Parallels `signReceiveAuthorization` — returns `{collectorData, tokenCollector}` suitable for direct `payment.charge` / `payment.authorize`. `collectorData` is the raw 65-byte EOA signature (commerce-payments' Permit2PaymentCollector forwards it straight to `permit2.permitTransferFrom`).
- `@x402r/sdk`: re-exports the four `@x402r/core` Permit2 surfaces above.
- `@x402r/cli`: `--asset-transfer-method <eip3009|permit2>` flag — filters `accepts[]` in addition to `--chain`. Errors on unknown value or empty match set with a `Malformed402Error` (exit code 2).
- New scenario: `examples/scenarios/permit2-charge.ts`. Demonstrates the one-time `ERC20.approve(PERMIT2, MAX)` step and an atomic Permit2 charge with balance-delta assertions.
- `@x402r/core/payment/permit2`: `signPermit2Authorization`, `createPermit2ApprovalTx`, `getPermit2AllowanceReadParams`, and the `PERMIT2_ADDRESS` constant. Returns `{collectorData, tokenCollector}` suitable for `payment.charge` / `payment.authorize`.
- `@x402r/sdk` re-exports the four Permit2 surfaces.
- `@x402r/cli` adds `--asset-transfer-method <eip3009|permit2>` to filter `accepts[]` alongside `--chain`. Invalid value or empty match set errors with a `Malformed402Error` (exit code 2).
19 changes: 8 additions & 11 deletions .changeset/sdk-authcapture-reconstruct-payment-info.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,17 @@
"@x402r/helpers": minor
---

Add the `PaymentInfo` namespace in `@x402r/core` (re-exported from `@x402r/sdk`) and add `reconstructPaymentInfoWire` in `@x402r/helpers`. Together they bridge the authCapture wire format to the bigint shape SDK actions accept.
Add the `PaymentInfo` namespace and `reconstructPaymentInfoWire` to bridge the authCapture wire format to the bigint shape SDK actions accept.

**New**

- `PaymentInfo` is now both a type and a namespace const in `@x402r/core` (re-exported from `@x402r/sdk`). Use `PaymentInfo.fromWire(wire)` to convert a JSON-form `PaymentInfoWire` to the bigint `PaymentInfo`, and `PaymentInfo.toWire(info)` for the reverse direction. The TypeScript "type + const sharing a name" pattern lets the same identifier serve both type annotations and value namespace access (same shape as built-in `Date`, `Buffer`, etc.).
- `PaymentInfoWire` type — derived from the contract ABI via the new `AbiPrimitiveToWire<T>` type helper. Stays in sync with `PaymentInfo` at compile time whenever the ABI changes.
- `reconstructPaymentInfoWire(context)` in `@x402r/helpers` — builds the `PaymentInfoWire` JSON form from a verified `SettleResultContext`. Handles the 6 wire→struct field renames and the EIP-3009/Permit2 branch internally.
- `PaymentInfo` is now both a type and a namespace const in `@x402r/core` (re-exported from `@x402r/sdk`). Use `PaymentInfo.fromWire(wire)` to convert a JSON-form `PaymentInfoWire` to the bigint `PaymentInfo`, and `PaymentInfo.toWire(info)` for the reverse.
- `PaymentInfoWire` type in `@x402r/core` — derived from the contract ABI, stays in sync at compile time when the ABI changes.
- `reconstructPaymentInfoWire(context)` in `@x402r/helpers` — builds the wire JSON form from a verified `SettleResultContext`.
- `@x402r/core` no longer declares `@x402r/evm` as a peer dependency. Consumers using only `@x402r/core` (arbiters, standalone wallet code) no longer need `@x402r/evm` installed.

**Breaking**

- **`@x402r/helpers`: `toPaymentInfo` removed.** Use `PaymentInfo.fromWire` from `@x402r/sdk` or `@x402r/core` instead — same conversion logic, new home. Arbiters and standalone workers can now drop the `@x402r/helpers` dep entirely; they only need `@x402r/sdk`.
- **`@x402r/helpers`: `reconstructPaymentInfoStruct` renamed to `reconstructPaymentInfoWire`.** Same function, return type renamed from `PaymentInfoStruct` to `PaymentInfoWire`. Mechanical search/replace migration.
- **`@x402r/helpers`: `forwardToArbiter`'s POST body field renamed from `paymentInfoStruct` to `paymentInfoWire`.** Arbiters consuming the helper's output should switch from `req.body.paymentInfoStruct` to `req.body.paymentInfoWire` and run it through `PaymentInfo.fromWire` to get bigints. The old `paymentPayload` field (legacy `commerce` scheme) is also gone.

**Why these live where they do**

`PaymentInfo` + converters live in `@x402r/core` because the conversion is scheme-agnostic; the wire type is ABI-derived in core, so there's no `@x402r/evm` dependency. `reconstructPaymentInfoWire` stays in `@x402r/helpers` because it encodes scheme-specific protocol logic (the wire→struct field renames and EIP-3009/Permit2 branching) that doesn't belong in core.
- `@x402r/helpers`: `toPaymentInfo` removed. Use `PaymentInfo.fromWire` from `@x402r/sdk` or `@x402r/core`. Arbiters and standalone workers can now drop the `@x402r/helpers` dependency entirely.
- `@x402r/helpers`: `reconstructPaymentInfoStruct` renamed to `reconstructPaymentInfoWire`. Return type renamed from `PaymentInfoStruct` to `PaymentInfoWire`.
- `@x402r/helpers`: `forwardToArbiter` POST body field renamed from `paymentInfoStruct` to `paymentInfoWire`. Arbiters should consume `req.body.paymentInfoWire` and run it through `PaymentInfo.fromWire`.
Loading