feat(rpc): add optional paymentIdentifier to RpcSubmitPaymentArgsSchema for V2 parity#29
Conversation
…ma for V2 parity Resolves #28. Adds canonical x402 V2 idempotency parity to the RPC transport: - **`PaymentIdentifierSchema`** (new, `src/core/primitives.ts`): shared caller-controlled idempotency key schema, `[a-zA-Z0-9_-]{16,128}`. Distinct from `PaymentIdSchema` (relay-assigned, `pay_` prefix). Exported from `@aibtc/tx-schemas/core`. - **`RpcSubmitPaymentRequestSchema.paymentIdentifier`** (new, optional): lets callers attach a client-controlled idempotency key to `submitPayment`. Same identifier + same `txHex` reuses the existing `paymentId`; same identifier + different `txHex` is rejected with the new error code. Existing callers that omit the field are unaffected. - **`RPC_PAYMENT_IDENTIFIER_CONFLICT`** (new error code): RPC-transport parity for the HTTP-side `payment_identifier_conflict` error reason. Follows the `RPC_*` naming convention already used in `RpcErrorCodeSchema`. - **`CanonicalDomainBoundary.transportBoundaries.sharedDomain`** updated to include `"paymentIdentifier idempotency"`, reflecting that the idempotency input is now shared across both HTTP (payment-identifier extension) and RPC transports. - **`HttpPaymentIdentifierExtensionSchema`** DRYed: extension `.info.id` now uses the new shared `PaymentIdentifierSchema` instead of the relay-assigned `PaymentIdSchema`. Downstream: relay PR #351 will consume `paymentIdentifier` in `submitPayment`; landing-page/#635 and agent-news/#624 adopt after. Closes #28 Co-Authored-By: Claude <noreply@anthropic.com>
- Rename RPC_PAYMENT_IDENTIFIER_CONFLICT → PAYMENT_IDENTIFIER_CONFLICT to match the bare naming convention of all other RPC error codes in the array (none carry an RPC_ prefix; the schema name already provides that context) - Extract repeated stub txHex literal to a STUB_TX_HEX const in the paymentIdentifier test suite - Trim inline comments to non-obvious WHY only Co-Authored-By: Claude <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds a shared caller-controlled idempotency identifier schema and wires it into the RPC submitPayment request to match x402 V2 HTTP behavior, including an RPC-side conflict error code and an explicit note in the canonical cross-transport boundary.
Changes:
- Introduce
PaymentIdentifierSchema([a-zA-Z0-9_-]{16,128}) as a shared primitive for caller-provided idempotency keys. - Add optional
paymentIdentifiertoRpcSubmitPaymentRequestSchemaand addRPC_PAYMENT_IDENTIFIER_CONFLICTtoRpcErrorCodeSchema. - Update HTTP
payment-identifierextension to validate withPaymentIdentifierSchemaand document parity inCanonicalDomainBoundary.transportBoundaries.sharedDomain.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
src/core/primitives.ts |
Adds PaymentIdentifierSchema primitive for caller-provided idempotency keys. |
src/rpc/schemas.ts |
Extends RPC submit request with optional paymentIdentifier; adds new RPC conflict error code. |
src/http/schemas.ts |
Switches HTTP payment-identifier extension validation to PaymentIdentifierSchema. |
src/core/enums.ts |
Updates canonical boundary metadata to explicitly include paymentIdentifier parity across transports. |
tests/rpc.test.ts |
Adds tests for optional paymentIdentifier validation and new RPC error code. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Uses PaymentIdentifierSchema rather than PaymentIdSchema: extension ids are caller-provided, | ||
| // not relay-assigned, so the pay_ prefix requirement was inappropriate. | ||
| export const HttpPaymentIdentifierExtensionSchema = z.object({ | ||
| info: z.object({ | ||
| id: PaymentIdSchema, | ||
| id: PaymentIdentifierSchema, | ||
| }), | ||
| }); |
There was a problem hiding this comment.
Switching HttpPaymentIdentifierExtensionSchema.info.id from PaymentIdSchema to PaymentIdentifierSchema is a stricter validation change for some previously-accepted inputs (e.g., "pay_123" would have parsed before but now fails due to the 16+ length requirement). That makes the PR not purely additive as described; either (a) update the backward-compatibility notes/versioning to reflect the tightened HTTP validation, or (b) consider accepting both schemas here (e.g., union) if you need to preserve old callers while migrating.
| paymentIdentifier: "pay_01JMVP9QE8XA3BDGM5", | ||
| }); | ||
| expect(req.paymentIdentifier).toBe("pay_01JMVP9QE8XA3BDGM5"); |
There was a problem hiding this comment.
The acceptance test for PaymentIdentifierSchema uses a value with a "pay_" prefix, which can unintentionally suggest that the prefix is required (even though PaymentIdentifierSchema is meant to be distinct from relay-assigned PaymentIdSchema). Consider using a clearly caller-generated identifier without the "pay_" prefix in this test to better capture the intended contract.
| paymentIdentifier: "pay_01JMVP9QE8XA3BDGM5", | |
| }); | |
| expect(req.paymentIdentifier).toBe("pay_01JMVP9QE8XA3BDGM5"); | |
| paymentIdentifier: "client_01JMVP9QE8XA3BDGM5", | |
| }); | |
| expect(req.paymentIdentifier).toBe("client_01JMVP9QE8XA3BDGM5"); |
| "ORIGIN_CHAINING_LIMIT", | ||
| "BROADCAST_RATE_LIMITED", | ||
| "SENDER_HAND_EXPIRED", | ||
| "NONCE_OCCUPIED", | ||
| "PAYMENT_IDENTIFIER_CONFLICT", | ||
| ] as const; |
There was a problem hiding this comment.
RPC_PAYMENT_IDENTIFIER_CONFLICT is the only entry in RPC_ERROR_CODES with an RPC_ prefix; all other RPC error codes in this enum use the unprefixed UPPER_SNAKE style (e.g., INVALID_TRANSACTION, NONCE_CONFLICT). For consistency (and to avoid downstream clients needing special-casing), consider renaming this code to match the existing pattern (or document why the prefix exception is necessary).
Motivation
CanonicalDomainBoundary.transportBoundaries.sharedDomaindefines the semantic contract that must hold across both HTTP and RPC transports. The x402 V2 HTTP transport already has caller-controlled idempotency via thepayment-identifierextension and apayment_identifier_conflicterror reason. The RPC transport had no equivalent — this PR closes that gap.Closes #28.
What's added
PaymentIdentifierSchema(src/core/primitives.ts, exported from@aibtc/tx-schemas/core): shared Zod schema for caller-controlled idempotency keys. Charset[a-zA-Z0-9_-]{16,128}. Distinct fromPaymentIdSchema(relay-assigned, requirespay_prefix) — the identifier is caller-provided, not relay-generated.RpcSubmitPaymentRequestSchema.paymentIdentifier(optional field): lets RPC callers attach an idempotency key tosubmitPayment. Semantics mirror the HTTP extension: same identifier + sametxHexreuses the existingpaymentId; same identifier + differenttxHexis rejected with the new error code.RPC_PAYMENT_IDENTIFIER_CONFLICTadded toRpcErrorCodeSchema: RPC-transport parity for the HTTP-sidepayment_identifier_conflicterror reason. Follows existingRPC_*naming convention.CanonicalDomainBoundary.transportBoundaries.sharedDomainupdated to include"paymentIdentifier idempotency", making the cross-transport contract explicit in the canonical boundary definition.DRY improvement:
HttpPaymentIdentifierExtensionSchema.info.idnow uses the new sharedPaymentIdentifierSchemainstead ofPaymentIdSchema. This corrects a subtle mismatch — extension IDs are caller-provided, so thepay_prefix requirement was inappropriate.Backward compatibility
Purely additive — the
paymentIdentifierfield is optional. Existing callers that don't set it are unaffected. No breaking changes to any existing schema.Downstream chain
paymentIdentifierin the RPCsubmitPaymenthandler viaPaymentIdServiceTest plan
npm run typecheck— passes cleannpm run build— passes cleannpm test— 225 tests pass (6 new tests coveringpaymentIdentifieraccept/reject cases andRPC_PAYMENT_IDENTIFIER_CONFLICT)paymentIdentifierparses successfully