@veridex/agentic-payments 0.1.1-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/CHANGELOG.md +108 -0
  2. package/MIGRATION.md +307 -0
  3. package/README.md +395 -0
  4. package/dist/index.d.mts +2327 -0
  5. package/dist/index.d.ts +2327 -0
  6. package/dist/index.js +5815 -0
  7. package/dist/index.js.map +1 -0
  8. package/dist/index.mjs +5759 -0
  9. package/dist/index.mjs.map +1 -0
  10. package/examples/basic-agent.ts +126 -0
  11. package/examples/mcp-claude.ts +75 -0
  12. package/examples/ucp-checkout.ts +92 -0
  13. package/examples/x402-integration.ts +75 -0
  14. package/package.json +36 -0
  15. package/src/AgentWallet.ts +432 -0
  16. package/src/chains/AptosChainClient.ts +29 -0
  17. package/src/chains/ChainClient.ts +73 -0
  18. package/src/chains/ChainClientFactory.ts +113 -0
  19. package/src/chains/EVMChainClient.ts +39 -0
  20. package/src/chains/SolanaChainClient.ts +37 -0
  21. package/src/chains/StarknetChainClient.ts +36 -0
  22. package/src/chains/SuiChainClient.ts +28 -0
  23. package/src/index.ts +83 -0
  24. package/src/mcp/MCPServer.ts +73 -0
  25. package/src/mcp/schemas.ts +60 -0
  26. package/src/monitoring/AlertManager.ts +258 -0
  27. package/src/monitoring/AuditLogger.ts +86 -0
  28. package/src/monitoring/BalanceCache.ts +44 -0
  29. package/src/monitoring/ComplianceExporter.ts +52 -0
  30. package/src/oracle/PythFeeds.ts +60 -0
  31. package/src/oracle/PythOracle.ts +121 -0
  32. package/src/performance/ConnectionPool.ts +217 -0
  33. package/src/performance/NonceManager.ts +91 -0
  34. package/src/performance/ParallelRouteFinder.ts +438 -0
  35. package/src/performance/TransactionPoller.ts +201 -0
  36. package/src/performance/TransactionQueue.ts +565 -0
  37. package/src/performance/index.ts +46 -0
  38. package/src/react/hooks.ts +298 -0
  39. package/src/routing/BridgeOrchestrator.ts +18 -0
  40. package/src/routing/CrossChainRouter.ts +501 -0
  41. package/src/routing/DEXAggregator.ts +448 -0
  42. package/src/routing/FeeEstimator.ts +43 -0
  43. package/src/session/SessionKeyManager.ts +312 -0
  44. package/src/session/SessionStorage.ts +80 -0
  45. package/src/session/SpendingTracker.ts +71 -0
  46. package/src/types/agent.ts +105 -0
  47. package/src/types/errors.ts +115 -0
  48. package/src/types/mcp.ts +22 -0
  49. package/src/types/ucp.ts +47 -0
  50. package/src/types/x402.ts +170 -0
  51. package/src/ucp/CapabilityNegotiator.ts +44 -0
  52. package/src/ucp/CredentialProvider.ts +73 -0
  53. package/src/ucp/PaymentTokenizer.ts +169 -0
  54. package/src/ucp/TransportAdapter.ts +18 -0
  55. package/src/ucp/UCPClient.ts +143 -0
  56. package/src/x402/NonceManager.ts +26 -0
  57. package/src/x402/PaymentParser.ts +225 -0
  58. package/src/x402/PaymentSigner.ts +305 -0
  59. package/src/x402/X402Client.ts +364 -0
  60. package/src/x402/adapters/CronosFacilitatorAdapter.ts +109 -0
  61. package/tests/alerts.test.ts +208 -0
  62. package/tests/chains.test.ts +242 -0
  63. package/tests/integration.test.ts +315 -0
  64. package/tests/monitoring.test.ts +435 -0
  65. package/tests/performance.test.ts +303 -0
  66. package/tests/property.test.ts +186 -0
  67. package/tests/react-hooks.test.ts +262 -0
  68. package/tests/session.test.ts +376 -0
  69. package/tests/ucp.test.ts +253 -0
  70. package/tests/x402.test.ts +385 -0
  71. package/tsconfig.json +26 -0
  72. package/tsup.config.ts +10 -0
  73. package/vitest.config.ts +16 -0
@@ -0,0 +1,253 @@
1
+ /**
2
+ * UCP Module Unit Tests
3
+ *
4
+ * Tests for PaymentTokenizer, CredentialProvider, and CapabilityNegotiator.
5
+ */
6
+
7
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
8
+ import { PaymentTokenizer } from '../src/ucp/PaymentTokenizer';
9
+ import { UCPCredentialProvider } from '../src/ucp/CredentialProvider';
10
+ import { CapabilityNegotiator } from '../src/ucp/CapabilityNegotiator';
11
+ import { StoredSession } from '../src/session/SessionStorage';
12
+
13
+ // Helper to create mock sessions
14
+ function createMockSession(overrides: Partial<{
15
+ keyHash: string;
16
+ dailyLimitUSD: number;
17
+ perTransactionLimitUSD: number;
18
+ expiryTimestamp: number;
19
+ }> = {}): StoredSession {
20
+ const {
21
+ keyHash = '0x' + 'a'.repeat(64),
22
+ dailyLimitUSD = 100,
23
+ perTransactionLimitUSD = 25,
24
+ expiryTimestamp = Date.now() + 3600000,
25
+ } = overrides;
26
+
27
+ return {
28
+ keyHash,
29
+ encryptedPrivateKey: '0x' + 'b'.repeat(64),
30
+ publicKey: '0x04' + 'c'.repeat(128),
31
+ config: {
32
+ dailyLimitUSD,
33
+ perTransactionLimitUSD,
34
+ expiryTimestamp,
35
+ allowedChains: [30],
36
+ },
37
+ metadata: {
38
+ createdAt: Date.now(),
39
+ lastUsedAt: Date.now(),
40
+ totalSpentUSD: 0,
41
+ dailySpentUSD: 0,
42
+ dailyResetAt: Date.now() + 86400000,
43
+ transactionCount: 0,
44
+ },
45
+ masterKeyHash: '0x' + 'd'.repeat(64),
46
+ };
47
+ }
48
+
49
+ describe('PaymentTokenizer', () => {
50
+ let tokenizer: PaymentTokenizer;
51
+
52
+ beforeEach(() => {
53
+ tokenizer = new PaymentTokenizer();
54
+ });
55
+
56
+ describe('tokenize', () => {
57
+ it('should generate a valid token from a session', async () => {
58
+ const session = createMockSession();
59
+ const token = await tokenizer.tokenize(session);
60
+
61
+ expect(token).toBeDefined();
62
+ expect(token.token).toBeDefined();
63
+ expect(token.keyHash).toBe(session.keyHash);
64
+ expect(token.limits.dailyLimitUSD).toBe(session.config.dailyLimitUSD);
65
+ expect(token.limits.perTransactionLimitUSD).toBe(session.config.perTransactionLimitUSD);
66
+ expect(token.expiresAt).toBeLessThanOrEqual(session.config.expiryTimestamp);
67
+ });
68
+
69
+ it('should respect custom TTL', async () => {
70
+ const session = createMockSession({ expiryTimestamp: Date.now() + 3600000 });
71
+ const customTtl = 5 * 60 * 1000; // 5 minutes
72
+ const token = await tokenizer.tokenize(session, customTtl);
73
+
74
+ const expectedExpiry = Date.now() + customTtl;
75
+ expect(token.expiresAt).toBeLessThanOrEqual(expectedExpiry + 100); // Allow 100ms tolerance
76
+ });
77
+
78
+ it('should not exceed session expiry', async () => {
79
+ const shortExpiry = Date.now() + 60000; // 1 minute
80
+ const session = createMockSession({ expiryTimestamp: shortExpiry });
81
+ const longTtl = 3600000; // 1 hour
82
+
83
+ const token = await tokenizer.tokenize(session, longTtl);
84
+
85
+ expect(token.expiresAt).toBeLessThanOrEqual(shortExpiry);
86
+ });
87
+ });
88
+
89
+ describe('validate', () => {
90
+ it('should validate a valid token', async () => {
91
+ const session = createMockSession();
92
+ const token = await tokenizer.tokenize(session);
93
+
94
+ const result = tokenizer.validate(token.token);
95
+
96
+ expect(result.valid).toBe(true);
97
+ expect(result.session).toBeDefined();
98
+ expect(result.session?.keyHash).toBe(session.keyHash);
99
+ });
100
+
101
+ it('should reject malformed tokens', () => {
102
+ const result = tokenizer.validate('not-a-valid-token');
103
+
104
+ expect(result.valid).toBe(false);
105
+ expect(result.reason).toBeDefined();
106
+ });
107
+
108
+ it('should reject tokens not in cache', () => {
109
+ // Create a structurally valid token that's not in the cache
110
+ const fakeTokenData = {
111
+ keyHash: '0xfake',
112
+ type: 'VERIDEX_SESSION_TOKEN',
113
+ limits: { dailyLimitUSD: 100, perTransactionLimitUSD: 25 },
114
+ expiresAt: Date.now() + 60000,
115
+ nonce: '0x123',
116
+ };
117
+ const fakeToken = Buffer.from(JSON.stringify(fakeTokenData)).toString('base64url');
118
+
119
+ const result = tokenizer.validate(fakeToken);
120
+
121
+ expect(result.valid).toBe(false);
122
+ expect(result.reason).toContain('not found');
123
+ });
124
+ });
125
+
126
+ describe('revoke', () => {
127
+ it('should revoke a token', async () => {
128
+ const session = createMockSession();
129
+ const token = await tokenizer.tokenize(session);
130
+
131
+ // Token should be valid
132
+ expect(tokenizer.validate(token.token).valid).toBe(true);
133
+
134
+ // Revoke
135
+ const revoked = tokenizer.revoke(token.token);
136
+ expect(revoked).toBe(true);
137
+
138
+ // Token should now be invalid
139
+ expect(tokenizer.validate(token.token).valid).toBe(false);
140
+ });
141
+
142
+ it('should return false for non-existent token', () => {
143
+ const revoked = tokenizer.revoke('non-existent-token');
144
+ expect(revoked).toBe(false);
145
+ });
146
+ });
147
+
148
+ describe('revokeAllForSession', () => {
149
+ it('should revoke all tokens for a session', async () => {
150
+ const session = createMockSession({ keyHash: '0xsession1' });
151
+
152
+ // Create multiple tokens
153
+ const token1 = await tokenizer.tokenize(session);
154
+ const token2 = await tokenizer.tokenize(session);
155
+
156
+ // All should be valid
157
+ expect(tokenizer.validate(token1.token).valid).toBe(true);
158
+ expect(tokenizer.validate(token2.token).valid).toBe(true);
159
+
160
+ // Revoke all for session
161
+ const count = tokenizer.revokeAllForSession('0xsession1');
162
+ expect(count).toBe(2);
163
+
164
+ // All should now be invalid
165
+ expect(tokenizer.validate(token1.token).valid).toBe(false);
166
+ expect(tokenizer.validate(token2.token).valid).toBe(false);
167
+ });
168
+ });
169
+
170
+ describe('refresh', () => {
171
+ it('should refresh a valid token', async () => {
172
+ const session = createMockSession();
173
+ const oldToken = await tokenizer.tokenize(session);
174
+
175
+ const newToken = await tokenizer.refresh(oldToken.token, session);
176
+
177
+ expect(newToken).not.toBeNull();
178
+ expect(newToken?.token).not.toBe(oldToken.token);
179
+ expect(newToken?.keyHash).toBe(session.keyHash);
180
+
181
+ // Old token should be invalid
182
+ expect(tokenizer.validate(oldToken.token).valid).toBe(false);
183
+
184
+ // New token should be valid
185
+ expect(tokenizer.validate(newToken!.token).valid).toBe(true);
186
+ });
187
+
188
+ it('should return null for invalid token', async () => {
189
+ const session = createMockSession();
190
+ const result = await tokenizer.refresh('invalid-token', session);
191
+ expect(result).toBeNull();
192
+ });
193
+ });
194
+ });
195
+
196
+ describe('CapabilityNegotiator', () => {
197
+ let negotiator: CapabilityNegotiator;
198
+
199
+ beforeEach(() => {
200
+ negotiator = new CapabilityNegotiator();
201
+ });
202
+
203
+ it('should return intersection of requested and supported capabilities', () => {
204
+ const requested = ['checkout', 'identity_linking', 'orders', 'unknown_capability'];
205
+ const result = negotiator.negotiate(requested);
206
+
207
+ expect(result.agreed).toContain('checkout');
208
+ expect(result.agreed).not.toContain('unknown_capability');
209
+ });
210
+
211
+ it('should handle empty requested capabilities', () => {
212
+ const result = negotiator.negotiate([]);
213
+ expect(result.agreed).toHaveLength(0);
214
+ });
215
+
216
+ it('should return all supported when requesting all', () => {
217
+ const requested = ['checkout', 'identity_linking', 'orders'];
218
+ const result = negotiator.negotiate(requested);
219
+
220
+ expect(result.agreed.length).toBeGreaterThan(0);
221
+ });
222
+ });
223
+
224
+ describe('UCPCredentialProvider', () => {
225
+ it('should generate a UCP profile', () => {
226
+ // UCPCredentialProvider requires a SessionKeyManager
227
+ // For this test, we'll mock it
228
+ const mockSessionManager = {
229
+ checkLimits: vi.fn().mockReturnValue({ allowed: true }),
230
+ recordSpending: vi.fn(),
231
+ } as any;
232
+
233
+ const provider = new UCPCredentialProvider(mockSessionManager);
234
+ const profile = provider.getProfile();
235
+
236
+ expect(profile).toBeDefined();
237
+ expect(profile.version).toBeDefined();
238
+ expect(profile.capabilities).toBeDefined();
239
+ expect(Array.isArray(profile.capabilities)).toBe(true);
240
+ });
241
+
242
+ it('should include expected capabilities in profile', () => {
243
+ const mockSessionManager = {
244
+ checkLimits: vi.fn().mockReturnValue({ allowed: true }),
245
+ recordSpending: vi.fn(),
246
+ } as any;
247
+
248
+ const provider = new UCPCredentialProvider(mockSessionManager);
249
+ const profile = provider.getProfile();
250
+
251
+ expect(profile.capabilities).toContain('checkout');
252
+ });
253
+ });
@@ -0,0 +1,385 @@
1
+ /**
2
+ * x402 Protocol Tests
3
+ *
4
+ * Tests for x402 payment parsing, signing, and flow handling.
5
+ */
6
+
7
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
8
+ import * as fc from 'fast-check';
9
+ import { PaymentParser } from '../src/x402/PaymentParser';
10
+ import { PaymentSigner } from '../src/x402/PaymentSigner';
11
+ import { StoredSession } from '../src/session/SessionStorage';
12
+ import { ethers } from 'ethers';
13
+
14
+ describe('PaymentParser', () => {
15
+ let parser: PaymentParser;
16
+
17
+ beforeEach(() => {
18
+ parser = new PaymentParser();
19
+ });
20
+
21
+ describe('Header Parsing', () => {
22
+ it('should parse valid PAYMENT-REQUIRED header', () => {
23
+ const requirement = {
24
+ paymentRequirements: [{
25
+ scheme: 'exact',
26
+ network: 'base-mainnet',
27
+ maxAmountRequired: '1000000', // 1 USDC
28
+ asset: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
29
+ payTo: '0x0000000000000000000000000000000000000001',
30
+ }],
31
+ };
32
+
33
+ const encoded = Buffer.from(JSON.stringify(requirement)).toString('base64');
34
+ const headers = { 'payment-required': encoded };
35
+
36
+ const result = parser.parseHeaders(headers);
37
+
38
+ expect(result).toBeDefined();
39
+ expect(result!.amount).toBe('1000000');
40
+ expect(result!.token).toBe('0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913');
41
+ expect(result!.recipient).toBe('0x0000000000000000000000000000000000000001');
42
+ expect(result!.chain).toBe(30); // Base
43
+ expect(result!.scheme).toBe('exact');
44
+ });
45
+
46
+ it('should handle uppercase header names', () => {
47
+ const requirement = {
48
+ paymentRequirements: [{
49
+ scheme: 'exact',
50
+ network: 'base',
51
+ maxAmountRequired: '500000',
52
+ asset: 'USDC',
53
+ payTo: '0x0000000000000000000000000000000000000123',
54
+ }],
55
+ };
56
+
57
+ const encoded = Buffer.from(JSON.stringify(requirement)).toString('base64');
58
+ const headers = { 'PAYMENT-REQUIRED': encoded };
59
+
60
+ const result = parser.parseHeaders(headers);
61
+ expect(result).toBeDefined();
62
+ expect(result!.amount).toBe('500000');
63
+ });
64
+
65
+ it('should return null for missing header', () => {
66
+ const result = parser.parseHeaders({});
67
+ expect(result).toBeNull();
68
+ });
69
+
70
+ it('should return null for invalid base64', () => {
71
+ const headers = { 'payment-required': 'not-valid-base64!!!' };
72
+ const result = parser.parseHeaders(headers);
73
+ expect(result).toBeNull();
74
+ });
75
+
76
+ it('should return null for invalid JSON', () => {
77
+ const encoded = Buffer.from('not json').toString('base64');
78
+ const headers = { 'payment-required': encoded };
79
+ const result = parser.parseHeaders(headers);
80
+ expect(result).toBeNull();
81
+ });
82
+ });
83
+
84
+ describe('Network Mapping', () => {
85
+ it('should map ethereum-mainnet to chain ID 2', () => {
86
+ const requirement = createPaymentRequirement('ethereum-mainnet');
87
+ const encoded = Buffer.from(JSON.stringify(requirement)).toString('base64');
88
+ const result = parser.parseHeaders({ 'payment-required': encoded });
89
+
90
+ expect(result!.chain).toBe(2);
91
+ });
92
+
93
+ it('should map base-mainnet to chain ID 30', () => {
94
+ const requirement = createPaymentRequirement('base-mainnet');
95
+ const encoded = Buffer.from(JSON.stringify(requirement)).toString('base64');
96
+ const result = parser.parseHeaders({ 'payment-required': encoded });
97
+
98
+ expect(result!.chain).toBe(30);
99
+ });
100
+
101
+ it('should map solana-mainnet to chain ID 1', () => {
102
+ const requirement = createPaymentRequirement('solana-mainnet');
103
+ const encoded = Buffer.from(JSON.stringify(requirement)).toString('base64');
104
+ const result = parser.parseHeaders({ 'payment-required': encoded });
105
+
106
+ expect(result!.chain).toBe(1);
107
+ });
108
+
109
+ it('should handle numeric network IDs', () => {
110
+ const requirement = createPaymentRequirement('8453'); // Base EVM chain ID
111
+ const encoded = Buffer.from(JSON.stringify(requirement)).toString('base64');
112
+ const result = parser.parseHeaders({ 'payment-required': encoded });
113
+
114
+ expect(result!.chain).toBe(30); // Should map to Wormhole Base ID
115
+ });
116
+ });
117
+
118
+ describe('Amount Parsing', () => {
119
+ it('should parse integer amounts', () => {
120
+ const amount = parser.parseAmount('1000000', 6);
121
+ expect(amount).toBe(1000000n);
122
+ });
123
+
124
+ it('should parse decimal amounts', () => {
125
+ const amount = parser.parseAmount('1.5', 6);
126
+ expect(amount).toBe(1500000n);
127
+ });
128
+
129
+ it('should handle decimals correctly for 18 decimal tokens', () => {
130
+ const amount = parser.parseAmount('1.0', 18);
131
+ expect(amount).toBe(1000000000000000000n);
132
+ });
133
+ });
134
+
135
+ describe('Amount Formatting', () => {
136
+ it('should format whole numbers', () => {
137
+ const formatted = parser.formatAmount(1000000n, 6);
138
+ expect(formatted).toBe('1');
139
+ });
140
+
141
+ it('should format decimal amounts', () => {
142
+ const formatted = parser.formatAmount(1500000n, 6);
143
+ expect(formatted).toBe('1.5');
144
+ });
145
+
146
+ it('should handle small amounts', () => {
147
+ const formatted = parser.formatAmount(1n, 6);
148
+ expect(formatted).toBe('0.000001');
149
+ });
150
+ });
151
+ });
152
+
153
+ describe('PaymentSigner', () => {
154
+ let signer: PaymentSigner;
155
+ let testSession: StoredSession;
156
+ let testWallet: ethers.Wallet;
157
+
158
+ beforeEach(() => {
159
+ signer = new PaymentSigner();
160
+ testWallet = ethers.Wallet.createRandom();
161
+ testSession = {
162
+ keyHash: '0x' + 'a'.repeat(64),
163
+ encryptedPrivateKey: testWallet.privateKey, // Unencrypted for tests
164
+ publicKey: testWallet.signingKey.publicKey,
165
+ config: {
166
+ dailyLimitUSD: 100,
167
+ perTransactionLimitUSD: 25,
168
+ expiryTimestamp: Date.now() + 3600000,
169
+ allowedChains: [30],
170
+ },
171
+ metadata: {
172
+ createdAt: Date.now(),
173
+ lastUsedAt: Date.now(),
174
+ totalSpentUSD: 0,
175
+ dailySpentUSD: 0,
176
+ dailyResetAt: Date.now() + 86400000,
177
+ transactionCount: 0,
178
+ },
179
+ masterKeyHash: '0x' + 'b'.repeat(64),
180
+ };
181
+ });
182
+
183
+ describe('Signature Generation', () => {
184
+ it('should generate a valid EIP-712 signature', async () => {
185
+ const request = {
186
+ amount: '1000000',
187
+ token: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
188
+ recipient: '0x0000000000000000000000000000000000000001',
189
+ chain: 30,
190
+ network: 'base-mainnet',
191
+ scheme: 'exact' as const,
192
+ original: {} as any,
193
+ };
194
+
195
+ const result = await signer.sign(request, testSession);
196
+
197
+ expect(result.signature).toBeDefined();
198
+ expect(result.signature.length).toBe(132); // 0x + 65 bytes hex
199
+ expect(result.nonce).toBeDefined();
200
+ expect(result.deadline).toBeGreaterThan(Date.now() / 1000);
201
+ expect(result.paymentPayload).toBeDefined();
202
+ });
203
+
204
+ it('should include correct deadline in signature', async () => {
205
+ const now = Math.floor(Date.now() / 1000);
206
+ const request = {
207
+ amount: '1000000',
208
+ token: 'USDC',
209
+ recipient: '0x0000000000000000000000000000000000000123',
210
+ chain: 30,
211
+ network: 'base',
212
+ scheme: 'exact' as const,
213
+ original: {} as any,
214
+ };
215
+
216
+ const result = await signer.sign(request, testSession);
217
+
218
+ // Default deadline should be 5 minutes from now
219
+ expect(result.deadline).toBeGreaterThanOrEqual(now + 280);
220
+ expect(result.deadline).toBeLessThanOrEqual(now + 320);
221
+ });
222
+
223
+ it('should use provided deadline if specified', async () => {
224
+ const customDeadline = Math.floor(Date.now() / 1000) + 600;
225
+ const request = {
226
+ amount: '1000000',
227
+ token: 'USDC',
228
+ recipient: '0x0000000000000000000000000000000000000123',
229
+ chain: 30,
230
+ network: 'base',
231
+ scheme: 'exact' as const,
232
+ deadline: customDeadline,
233
+ original: {} as any,
234
+ };
235
+
236
+ const result = await signer.sign(request, testSession);
237
+
238
+ expect(result.deadline).toBe(customDeadline);
239
+ });
240
+
241
+ it('should generate unique nonces', async () => {
242
+ const request = {
243
+ amount: '1000000',
244
+ token: 'USDC',
245
+ recipient: '0x0000000000000000000000000000000000000123',
246
+ chain: 30,
247
+ network: 'base',
248
+ scheme: 'exact' as const,
249
+ original: {} as any,
250
+ };
251
+
252
+ const result1 = await signer.sign(request, testSession);
253
+ const result2 = await signer.sign(request, testSession);
254
+
255
+ expect(result1.nonce).not.toBe(result2.nonce);
256
+ });
257
+ });
258
+
259
+ describe('Signature Verification', () => {
260
+ it('should verify its own signatures', async () => {
261
+ const request = {
262
+ amount: '1000000',
263
+ token: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
264
+ recipient: '0x0000000000000000000000000000000000000001',
265
+ chain: 30,
266
+ network: 'base',
267
+ scheme: 'exact' as const,
268
+ original: {} as any,
269
+ };
270
+
271
+ const result = await signer.sign(request, testSession);
272
+
273
+ // Decode the payload to get the authorization
274
+ const payload = JSON.parse(Buffer.from(result.paymentPayload, 'base64').toString());
275
+ const authorization = payload.payload.authorization;
276
+
277
+ const isValid = signer.verifySignature(
278
+ result.signature,
279
+ authorization,
280
+ testWallet.address,
281
+ 8453, // Base EVM chain ID
282
+ '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913' // Base USDC address
283
+ );
284
+
285
+ expect(isValid).toBe(true);
286
+ });
287
+
288
+ it('should reject signatures from different signers', async () => {
289
+ const request = {
290
+ amount: '1000000',
291
+ token: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', // Base USDC
292
+ recipient: '0x0000000000000000000000000000000000000123',
293
+ chain: 30,
294
+ network: 'base',
295
+ scheme: 'exact' as const,
296
+ original: {} as any,
297
+ };
298
+
299
+ const result = await signer.sign(request, testSession);
300
+ const payload = JSON.parse(Buffer.from(result.paymentPayload, 'base64').toString());
301
+ const authorization = payload.payload.authorization;
302
+
303
+ const wrongAddress = '0x0000000000000000000000000000000000000000';
304
+ const isValid = signer.verifySignature(
305
+ result.signature,
306
+ authorization,
307
+ wrongAddress,
308
+ 8453,
309
+ '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913' // Base USDC address
310
+ );
311
+
312
+ expect(isValid).toBe(false);
313
+ });
314
+ });
315
+ });
316
+
317
+ // Property-based tests
318
+ describe('x402 - Property Tests', () => {
319
+ describe('Property 5: Nonce Sequencing Prevents Replay Attacks', () => {
320
+ it('should never generate duplicate nonces in sequence', () => {
321
+ fc.assert(
322
+ fc.asyncProperty(
323
+ fc.integer({ min: 2, max: 50 }), // number of signatures to generate
324
+ async (count) => {
325
+ const signer = new PaymentSigner();
326
+ const wallet = ethers.Wallet.createRandom();
327
+ const session: StoredSession = {
328
+ keyHash: '0x' + 'a'.repeat(64),
329
+ encryptedPrivateKey: wallet.privateKey,
330
+ publicKey: wallet.signingKey.publicKey,
331
+ config: {
332
+ dailyLimitUSD: 100,
333
+ perTransactionLimitUSD: 25,
334
+ expiryTimestamp: Date.now() + 3600000,
335
+ allowedChains: [30],
336
+ },
337
+ metadata: {
338
+ createdAt: Date.now(),
339
+ lastUsedAt: Date.now(),
340
+ totalSpentUSD: 0,
341
+ dailySpentUSD: 0,
342
+ dailyResetAt: Date.now() + 86400000,
343
+ transactionCount: 0,
344
+ },
345
+ masterKeyHash: '0x' + 'b'.repeat(64),
346
+ };
347
+
348
+ const request = {
349
+ amount: '1000000',
350
+ token: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', // Base USDC
351
+ recipient: '0x0000000000000000000000000000000000000123',
352
+ chain: 30,
353
+ network: 'base',
354
+ scheme: 'exact' as const,
355
+ original: {} as any,
356
+ };
357
+
358
+ const nonces = new Set<string>();
359
+ for (let i = 0; i < count; i++) {
360
+ const result = await signer.sign(request, session);
361
+ expect(nonces.has(result.nonce)).toBe(false);
362
+ nonces.add(result.nonce);
363
+ }
364
+
365
+ expect(nonces.size).toBe(count);
366
+ }
367
+ ),
368
+ { numRuns: 20 }
369
+ );
370
+ });
371
+ });
372
+ });
373
+
374
+ // Helper functions
375
+ function createPaymentRequirement(network: string) {
376
+ return {
377
+ paymentRequirements: [{
378
+ scheme: 'exact',
379
+ network,
380
+ maxAmountRequired: '1000000',
381
+ asset: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
382
+ payTo: '0x0000000000000000000000000000000000000001',
383
+ }],
384
+ };
385
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "ESNext",
5
+ "lib": [
6
+ "ES2020",
7
+ "DOM"
8
+ ],
9
+ "declaration": true,
10
+ "sourceMap": true,
11
+ "moduleResolution": "bundler",
12
+ "strict": true,
13
+ "esModuleInterop": true,
14
+ "skipLibCheck": true,
15
+ "forceConsistentCasingInFileNames": true,
16
+ "outDir": "./dist"
17
+ },
18
+ "include": [
19
+ "src/**/*"
20
+ ],
21
+ "exclude": [
22
+ "node_modules",
23
+ "dist",
24
+ "**/*.test.ts"
25
+ ]
26
+ }
package/tsup.config.ts ADDED
@@ -0,0 +1,10 @@
1
+ import { defineConfig } from 'tsup';
2
+
3
+ export default defineConfig({
4
+ entry: ['src/index.ts'],
5
+ format: ['cjs', 'esm'],
6
+ dts: true,
7
+ splitting: false,
8
+ sourcemap: true,
9
+ clean: true,
10
+ });
@@ -0,0 +1,16 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ globals: true,
6
+ environment: 'node',
7
+ include: ['src/**/*.test.ts', 'tests/**/*.test.ts'],
8
+ coverage: {
9
+ provider: 'v8',
10
+ reporter: ['text', 'json', 'html'],
11
+ include: ['src/**/*.ts'],
12
+ exclude: ['src/**/*.test.ts', 'src/types/**'],
13
+ },
14
+ testTimeout: 30000,
15
+ },
16
+ });