Skip to content

Commit 958afdb

Browse files
A1igatorclaude
andauthored
Add expiry fields to PaymentRequirements example (#33)
## Summary - Adds `preApprovalExpirySeconds`, `authorizationExpirySeconds`, and `refundExpirySeconds` to the PaymentRequirements JSON example in the EVM spec - These fields were already documented in the extra fields table but missing from the example, making them easy to overlook ## Context Addresses review feedback asking whether expiries should be surfaced in PaymentRequirements. They already were in the table, but the JSON example didn't show them. ## Test plan - [ ] Spec-only change, no code impact - [ ] Verify example JSON is valid and fields match the table descriptions 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent de179d4 commit 958afdb

3 files changed

Lines changed: 54 additions & 12 deletions

File tree

packages/evm/src/commerce/client/scheme.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,15 +64,18 @@ export class CommerceEvmScheme implements SchemeNetworkClient {
6464

6565
const chainId = parseChainId(requirements.network)
6666
const maxAmount = requirements.amount
67+
const nowSeconds = Math.floor(Date.now() / 1000)
6768

6869
const paymentInfo = {
6970
operator: operatorAddress,
7071
receiver: requirements.payTo as `0x${string}`,
7172
token: requirements.asset as `0x${string}`,
7273
maxAmount,
73-
preApprovalExpiry: preApprovalExpirySeconds ?? MAX_UINT48,
74-
authorizationExpiry: authorizationExpirySeconds ?? MAX_UINT48,
75-
refundExpiry: refundExpirySeconds ?? MAX_UINT48,
74+
preApprovalExpiry:
75+
preApprovalExpirySeconds != null ? nowSeconds + preApprovalExpirySeconds : MAX_UINT48,
76+
authorizationExpiry:
77+
authorizationExpirySeconds != null ? nowSeconds + authorizationExpirySeconds : MAX_UINT48,
78+
refundExpiry: refundExpirySeconds != null ? nowSeconds + refundExpirySeconds : MAX_UINT48,
7679
minFeeBps,
7780
maxFeeBps,
7881
feeReceiver: feeReceiver ?? zeroAddress,

packages/evm/test/unit/commerce/client.test.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import { describe, it, expect, vi, beforeEach } from 'vitest'
1+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
22
import { CommerceEvmScheme } from '../../../src/commerce/client/index'
33
import { x402Client } from '@x402/core/client'
44
import { registerCommerceEvmScheme } from '../../../src/commerce/client/index'
5+
import { MAX_UINT48 } from '../../../src/commerce/shared/constants'
56

67
describe('CommerceEvmScheme', () => {
78
const createMockSigner = () => ({
@@ -15,6 +16,10 @@ describe('CommerceEvmScheme', () => {
1516
mockSigner = createMockSigner()
1617
})
1718

19+
afterEach(() => {
20+
vi.restoreAllMocks()
21+
})
22+
1823
const mockRequirements = {
1924
scheme: 'commerce',
2025
network: 'eip155:84532',
@@ -153,6 +158,37 @@ describe('CommerceEvmScheme', () => {
153158
)
154159
})
155160

161+
it('should convert expirySeconds to absolute timestamps', async () => {
162+
const fakeNow = 1700000000000 // fixed ms
163+
vi.spyOn(Date, 'now').mockReturnValue(fakeNow)
164+
const nowSeconds = 1700000000
165+
166+
const scheme = new CommerceEvmScheme(mockSigner)
167+
const requirementsWithExpiries = {
168+
...mockRequirements,
169+
extra: {
170+
...mockRequirements.extra,
171+
preApprovalExpirySeconds: 3600,
172+
authorizationExpirySeconds: 86400,
173+
refundExpirySeconds: 604800,
174+
},
175+
}
176+
const result = await scheme.createPaymentPayload(2, requirementsWithExpiries)
177+
178+
expect(result.payload.paymentInfo.preApprovalExpiry).toBe(nowSeconds + 3600)
179+
expect(result.payload.paymentInfo.authorizationExpiry).toBe(nowSeconds + 86400)
180+
expect(result.payload.paymentInfo.refundExpiry).toBe(nowSeconds + 604800)
181+
})
182+
183+
it('should default expiries to MAX_UINT48 when not specified', async () => {
184+
const scheme = new CommerceEvmScheme(mockSigner)
185+
const result = await scheme.createPaymentPayload(2, mockRequirements)
186+
187+
expect(result.payload.paymentInfo.preApprovalExpiry).toBe(MAX_UINT48)
188+
expect(result.payload.paymentInfo.authorizationExpiry).toBe(MAX_UINT48)
189+
expect(result.payload.paymentInfo.refundExpiry).toBe(MAX_UINT48)
190+
})
191+
156192
it('should throw for invalid network format', async () => {
157193
const scheme = new CommerceEvmScheme(mockSigner)
158194
const badNetworkRequirements = {

specs/schemes/commerce/scheme_commerce_evm.md

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,10 @@ Commerce-accepting servers advertise with scheme `commerce`:
3636
"settlementMethod": "authorize",
3737
"minFeeBps": 0,
3838
"maxFeeBps": 1000,
39-
"feeReceiver": "0xOperatorAddress"
39+
"feeReceiver": "0xOperatorAddress",
40+
"preApprovalExpirySeconds": 3600,
41+
"authorizationExpirySeconds": 86400,
42+
"refundExpirySeconds": 604800
4043
}
4144
}
4245
]
@@ -56,9 +59,9 @@ Commerce-accepting servers advertise with scheme `commerce`:
5659
| `minFeeBps` | No | `uint16` | Minimum fee in basis points. Default: `0` |
5760
| `maxFeeBps` | No | `uint16` | Maximum fee in basis points. Default: `0` |
5861
| `feeReceiver` | No | `address` | Fee recipient. Default: `address(0)` (no fees) |
59-
| `preApprovalExpirySeconds` | No | `uint48` | ERC-3009 signature validity / pre-approval expiry |
60-
| `authorizationExpirySeconds` | No | `uint48` | Deadline for capturing escrowed funds |
61-
| `refundExpirySeconds` | No | `uint48` | Deadline for refund requests |
62+
| `preApprovalExpirySeconds` | No | `uint48` | Seconds until pre-approval / ERC-3009 sig expires |
63+
| `authorizationExpirySeconds` | No | `uint48` | Seconds until capture deadline for escrowed funds |
64+
| `refundExpirySeconds` | No | `uint48` | Seconds until refund request deadline |
6265

6366
## PaymentPayload
6467

@@ -94,7 +97,7 @@ Commerce-accepting servers advertise with scheme `commerce`:
9497
"to": "0xCollectorAddress",
9598
"value": "1000000",
9699
"validAfter": "0",
97-
"validBefore": "1740672154",
100+
"validBefore": "1740675754",
98101
"nonce": "0xf374...3480"
99102
},
100103
"signature": "0x2d6a...571c",
@@ -103,9 +106,9 @@ Commerce-accepting servers advertise with scheme `commerce`:
103106
"receiver": "0xReceiverAddress",
104107
"token": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
105108
"maxAmount": "1000000",
106-
"preApprovalExpiry": 1740672154,
107-
"authorizationExpiry": 4294967295,
108-
"refundExpiry": 281474976710655,
109+
"preApprovalExpiry": 1740675754,
110+
"authorizationExpiry": 1740758554,
111+
"refundExpiry": 1741276954,
109112
"minFeeBps": 0,
110113
"maxFeeBps": 1000,
111114
"feeReceiver": "0xOperatorAddress",

0 commit comments

Comments
 (0)