@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,242 @@
1
+ /**
2
+ * Chain Client Unit Tests
3
+ *
4
+ * Tests for multi-chain client implementations and the ChainClientFactory.
5
+ */
6
+
7
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
8
+ import { ChainClientFactory } from '../src/chains/ChainClientFactory';
9
+ import { AgentChainClient } from '../src/chains/ChainClient';
10
+
11
+ // Mock the @veridex/sdk modules
12
+ vi.mock('@veridex/sdk', async () => {
13
+ return {
14
+ getChainPreset: vi.fn().mockImplementation((chain: string) => {
15
+ const presets: Record<string, any> = {
16
+ 'base': {
17
+ type: 'evm',
18
+ testnet: {
19
+ chainId: 84532,
20
+ wormholeChainId: 30,
21
+ rpcUrl: 'https://sepolia.base.org',
22
+ name: 'Base Sepolia',
23
+ explorerUrl: 'https://sepolia.basescan.org',
24
+ contracts: {
25
+ hub: '0x1234567890123456789012345678901234567890',
26
+ wormholeCoreBridge: '0x0987654321098765432109876543210987654321',
27
+ vaultFactory: '0xfactory',
28
+ vaultImplementation: '0ximpl',
29
+ tokenBridge: '0xbridge',
30
+ },
31
+ },
32
+ mainnet: {
33
+ chainId: 8453,
34
+ wormholeChainId: 30,
35
+ rpcUrl: 'https://mainnet.base.org',
36
+ name: 'Base',
37
+ explorerUrl: 'https://basescan.org',
38
+ contracts: {
39
+ hub: '0x1234567890123456789012345678901234567890',
40
+ wormholeCoreBridge: '0x0987654321098765432109876543210987654321',
41
+ vaultFactory: '0xfactory',
42
+ vaultImplementation: '0ximpl',
43
+ tokenBridge: '0xbridge',
44
+ },
45
+ },
46
+ },
47
+ 'solana': {
48
+ type: 'solana',
49
+ testnet: {
50
+ chainId: 0,
51
+ wormholeChainId: 1,
52
+ rpcUrl: 'https://api.devnet.solana.com',
53
+ name: 'Solana Devnet',
54
+ contracts: {
55
+ hub: 'programId123',
56
+ wormholeCoreBridge: 'wormhole123',
57
+ tokenBridge: 'tokenbridge123',
58
+ },
59
+ },
60
+ mainnet: {
61
+ chainId: 0,
62
+ wormholeChainId: 1,
63
+ rpcUrl: 'https://api.mainnet-beta.solana.com',
64
+ name: 'Solana',
65
+ contracts: {
66
+ hub: 'programId123',
67
+ wormholeCoreBridge: 'wormhole123',
68
+ tokenBridge: 'tokenbridge123',
69
+ },
70
+ },
71
+ },
72
+ 'aptos': {
73
+ type: 'aptos',
74
+ testnet: {
75
+ chainId: 0,
76
+ wormholeChainId: 22,
77
+ rpcUrl: 'https://fullnode.testnet.aptoslabs.com',
78
+ name: 'Aptos Testnet',
79
+ contracts: {
80
+ hub: '0xmodule',
81
+ wormholeCoreBridge: '0xwormhole',
82
+ tokenBridge: '0xtokenbridge',
83
+ },
84
+ },
85
+ mainnet: {
86
+ chainId: 0,
87
+ wormholeChainId: 22,
88
+ rpcUrl: 'https://fullnode.mainnet.aptoslabs.com',
89
+ name: 'Aptos',
90
+ contracts: {
91
+ hub: '0xmodule',
92
+ wormholeCoreBridge: '0xwormhole',
93
+ tokenBridge: '0xtokenbridge',
94
+ },
95
+ },
96
+ },
97
+ 'sui': {
98
+ type: 'sui',
99
+ testnet: {
100
+ chainId: 0,
101
+ wormholeChainId: 21,
102
+ rpcUrl: 'https://fullnode.testnet.sui.io',
103
+ name: 'Sui Testnet',
104
+ contracts: {
105
+ hub: '0xpackage',
106
+ wormholeCoreBridge: '0xwormhole',
107
+ },
108
+ },
109
+ mainnet: {
110
+ chainId: 0,
111
+ wormholeChainId: 21,
112
+ rpcUrl: 'https://fullnode.mainnet.sui.io',
113
+ name: 'Sui',
114
+ contracts: {
115
+ hub: '0xpackage',
116
+ wormholeCoreBridge: '0xwormhole',
117
+ },
118
+ },
119
+ },
120
+ 'starknet': {
121
+ type: 'starknet',
122
+ testnet: {
123
+ chainId: 0,
124
+ wormholeChainId: 18,
125
+ rpcUrl: 'https://starknet-sepolia.public.blastapi.io',
126
+ name: 'Starknet Sepolia',
127
+ contracts: {
128
+ hub: '0xspoke',
129
+ wormholeCoreBridge: '0xbridge',
130
+ },
131
+ },
132
+ mainnet: {
133
+ chainId: 0,
134
+ wormholeChainId: 18,
135
+ rpcUrl: 'https://starknet-mainnet.public.blastapi.io',
136
+ name: 'Starknet',
137
+ contracts: {
138
+ hub: '0xspoke',
139
+ wormholeCoreBridge: '0xbridge',
140
+ },
141
+ },
142
+ },
143
+ };
144
+ return presets[chain] || presets['base'];
145
+ }),
146
+ ChainName: {},
147
+ NetworkType: {},
148
+ };
149
+ });
150
+
151
+ // Mock chain-specific clients
152
+ vi.mock('@veridex/sdk/chains/evm', () => ({
153
+ EVMClient: vi.fn().mockImplementation(() => ({
154
+ getConfig: () => ({ name: 'Base Sepolia', chainId: 84532 }),
155
+ getProvider: () => ({}),
156
+ })),
157
+ EVMClientConfig: {},
158
+ }));
159
+
160
+ vi.mock('@veridex/sdk/chains/solana', () => ({
161
+ SolanaClient: vi.fn().mockImplementation(() => ({
162
+ getConfig: () => ({ name: 'Solana Devnet' }),
163
+ getConnection: () => ({}),
164
+ })),
165
+ SolanaClientConfig: {},
166
+ }));
167
+
168
+ vi.mock('@veridex/sdk/chains/aptos', () => ({
169
+ AptosClient: vi.fn().mockImplementation(() => ({
170
+ getConfig: () => ({ name: 'Aptos Testnet' }),
171
+ getClient: () => ({}),
172
+ })),
173
+ AptosClientConfig: {},
174
+ }));
175
+
176
+ vi.mock('@veridex/sdk/chains/sui', () => ({
177
+ SuiClient: vi.fn().mockImplementation(() => ({
178
+ getConfig: () => ({ name: 'Sui Testnet' }),
179
+ getClient: () => ({}),
180
+ })),
181
+ SuiClientConfig: {},
182
+ }));
183
+
184
+ vi.mock('@veridex/sdk/chains/starknet', () => ({
185
+ StarknetClient: vi.fn().mockImplementation(() => ({
186
+ getConfig: () => ({ name: 'Starknet Sepolia' }),
187
+ getProvider: () => ({}),
188
+ })),
189
+ StarknetClientConfig: {},
190
+ }));
191
+
192
+ describe('ChainClientFactory', () => {
193
+ describe('createClient', () => {
194
+ it('should create EVM client for base chain', () => {
195
+ const client = ChainClientFactory.createClient('base', 'testnet');
196
+ expect(client).toBeDefined();
197
+ });
198
+
199
+ it('should create Solana client', () => {
200
+ const client = ChainClientFactory.createClient('solana', 'testnet');
201
+ expect(client).toBeDefined();
202
+ });
203
+
204
+ it('should create Aptos client', () => {
205
+ const client = ChainClientFactory.createClient('aptos', 'testnet');
206
+ expect(client).toBeDefined();
207
+ });
208
+
209
+ it('should create Sui client', () => {
210
+ const client = ChainClientFactory.createClient('sui', 'testnet');
211
+ expect(client).toBeDefined();
212
+ });
213
+
214
+ it('should create Starknet client', () => {
215
+ const client = ChainClientFactory.createClient('starknet', 'testnet');
216
+ expect(client).toBeDefined();
217
+ });
218
+
219
+ it('should use mainnet when specified', () => {
220
+ const client = ChainClientFactory.createClient('base', 'mainnet');
221
+ expect(client).toBeDefined();
222
+ });
223
+
224
+ it('should use custom RPC URL when provided', () => {
225
+ const customRpc = 'https://custom-rpc.example.com';
226
+ const client = ChainClientFactory.createClient('base', 'testnet', customRpc);
227
+ expect(client).toBeDefined();
228
+ });
229
+ });
230
+ });
231
+
232
+ describe('AgentChainClient Interface', () => {
233
+ it('should have getNativeTokenPriceUSD method', () => {
234
+ const client = ChainClientFactory.createClient('base', 'testnet');
235
+ expect(typeof client.getNativeTokenPriceUSD).toBe('function');
236
+ });
237
+
238
+ it('should have getTokenPriceUSD method', () => {
239
+ const client = ChainClientFactory.createClient('base', 'testnet');
240
+ expect(typeof client.getTokenPriceUSD).toBe('function');
241
+ });
242
+ });
@@ -0,0 +1,315 @@
1
+ /**
2
+ * Integration Tests for Agent SDK
3
+ *
4
+ * End-to-end tests verifying complete flows work correctly.
5
+ */
6
+
7
+ import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
8
+ import { SessionKeyManager } from '../src/session/SessionKeyManager';
9
+ import { SpendingTracker } from '../src/session/SpendingTracker';
10
+ import { StoredSession, SessionKeyConfig } from '../src/session/SessionStorage';
11
+ import { PaymentTokenizer } from '../src/ucp/PaymentTokenizer';
12
+ import { AlertManager } from '../src/monitoring/AlertManager';
13
+ import { AuditLogger } from '../src/monitoring/AuditLogger';
14
+
15
+ // Mock the crypto functions from @veridex/sdk
16
+ vi.mock('@veridex/sdk', async () => {
17
+ const { ethers } = await import('ethers');
18
+ return {
19
+ generateSecp256k1KeyPair: vi.fn().mockImplementation(() => {
20
+ const wallet = ethers.Wallet.createRandom();
21
+ return {
22
+ publicKey: wallet.signingKey.publicKey,
23
+ privateKey: new Uint8Array(Buffer.from(wallet.privateKey.slice(2), 'hex'))
24
+ };
25
+ }),
26
+ computeSessionKeyHash: vi.fn().mockImplementation((pubKey) =>
27
+ ethers.keccak256(pubKey)
28
+ ),
29
+ deriveEncryptionKey: vi.fn().mockResolvedValue({} as CryptoKey),
30
+ encrypt: vi.fn().mockImplementation((data: Uint8Array) =>
31
+ Promise.resolve(new Uint8Array([...data, 1, 2, 3]))
32
+ ),
33
+ decrypt: vi.fn().mockImplementation((data: Uint8Array) =>
34
+ Promise.resolve(data.slice(0, data.length - 3))
35
+ ),
36
+ VeridexSDK: class { },
37
+ createSDK: vi.fn(),
38
+ };
39
+ });
40
+
41
+ // Helper to create mock sessions
42
+ function createMockSession(overrides: Partial<{
43
+ keyHash: string;
44
+ dailyLimitUSD: number;
45
+ perTransactionLimitUSD: number;
46
+ expiryTimestamp: number;
47
+ dailySpentUSD: number;
48
+ totalSpentUSD: number;
49
+ }> = {}): StoredSession {
50
+ const {
51
+ keyHash = '0x' + 'a'.repeat(64),
52
+ dailyLimitUSD = 100,
53
+ perTransactionLimitUSD = 25,
54
+ expiryTimestamp = Date.now() + 3600000,
55
+ dailySpentUSD = 0,
56
+ totalSpentUSD = 0,
57
+ } = overrides;
58
+
59
+ return {
60
+ keyHash,
61
+ encryptedPrivateKey: '0x' + 'b'.repeat(64),
62
+ publicKey: '0x04' + 'c'.repeat(128),
63
+ config: {
64
+ dailyLimitUSD,
65
+ perTransactionLimitUSD,
66
+ expiryTimestamp,
67
+ allowedChains: [30],
68
+ },
69
+ metadata: {
70
+ createdAt: Date.now(),
71
+ lastUsedAt: Date.now(),
72
+ totalSpentUSD,
73
+ dailySpentUSD,
74
+ dailyResetAt: Date.now() + 86400000,
75
+ transactionCount: 0,
76
+ },
77
+ masterKeyHash: '0x' + 'd'.repeat(64),
78
+ };
79
+ }
80
+
81
+ describe('Integration Tests', () => {
82
+ describe('17.1: Agent creates session and makes payment', () => {
83
+ let sessionManager: SessionKeyManager;
84
+
85
+ beforeEach(() => {
86
+ sessionManager = new SessionKeyManager();
87
+ if (typeof localStorage !== 'undefined') {
88
+ localStorage.clear();
89
+ }
90
+ });
91
+
92
+ it('should create session and verify limits are enforced', async () => {
93
+ const masterKey = {
94
+ credentialId: 'test-credential-123',
95
+ publicKeyX: BigInt('0x' + '1'.repeat(64)),
96
+ publicKeyY: BigInt('0x' + '2'.repeat(64)),
97
+ keyHash: '0x' + 'a'.repeat(64),
98
+ };
99
+
100
+ const config: SessionKeyConfig = {
101
+ dailyLimitUSD: 100,
102
+ perTransactionLimitUSD: 25,
103
+ expiryTimestamp: Date.now() + 60 * 60 * 1000,
104
+ allowedChains: [30],
105
+ };
106
+
107
+ // Create session
108
+ const session = await sessionManager.createSession(masterKey, config);
109
+ expect(session).toBeDefined();
110
+ expect(session.keyHash).toBeDefined();
111
+
112
+ // Check initial limits
113
+ const result1 = sessionManager.checkLimits(session, 20);
114
+ expect(result1.allowed).toBe(true);
115
+
116
+ // Record spending
117
+ await sessionManager.recordSpending(session, 20);
118
+ expect(session.metadata.dailySpentUSD).toBe(20);
119
+
120
+ // Check limits again - $85 exceeds per-transaction limit ($25)
121
+ const result2 = sessionManager.checkLimits(session, 85);
122
+ expect(result2.allowed).toBe(false);
123
+ expect(result2.reason).toContain('per-transaction limit');
124
+ });
125
+
126
+ it('should reject transactions exceeding per-transaction limit', async () => {
127
+ const session = createMockSession({
128
+ dailyLimitUSD: 100,
129
+ perTransactionLimitUSD: 25,
130
+ });
131
+
132
+ const result = sessionManager.checkLimits(session, 30);
133
+ expect(result.allowed).toBe(false);
134
+ expect(result.reason).toContain('per-transaction limit');
135
+ });
136
+ });
137
+
138
+ describe('17.2: Agent exceeds limit and receives error', () => {
139
+ it('should enforce spending limits and provide clear error', async () => {
140
+ const sessionManager = new SessionKeyManager();
141
+ const session = createMockSession({
142
+ dailyLimitUSD: 50,
143
+ perTransactionLimitUSD: 50,
144
+ dailySpentUSD: 45,
145
+ });
146
+
147
+ const result = sessionManager.checkLimits(session, 10);
148
+
149
+ expect(result.allowed).toBe(false);
150
+ expect(result.reason).toBeDefined();
151
+ expect(result.remainingDailyLimitUSD).toBeDefined();
152
+ });
153
+ });
154
+
155
+ describe('17.3: Master key revokes session', () => {
156
+ it('should immediately invalidate session on revocation', async () => {
157
+ const sessionManager = new SessionKeyManager();
158
+ const masterKey = {
159
+ credentialId: 'test-credential',
160
+ publicKeyX: BigInt(1),
161
+ publicKeyY: BigInt(2),
162
+ keyHash: '0xmaster',
163
+ };
164
+
165
+ const config: SessionKeyConfig = {
166
+ dailyLimitUSD: 100,
167
+ perTransactionLimitUSD: 25,
168
+ expiryTimestamp: Date.now() + 3600000,
169
+ allowedChains: [30],
170
+ };
171
+
172
+ // Create session
173
+ const session = await sessionManager.createSession(masterKey, config);
174
+ expect(session).toBeDefined();
175
+
176
+ // Verify session works initially
177
+ const resultBefore = sessionManager.checkLimits(session, 10);
178
+ expect(resultBefore.allowed).toBe(true);
179
+
180
+ // Revoke session
181
+ await sessionManager.revokeSession(session.keyHash);
182
+
183
+ // Session should no longer be loadable
184
+ const loaded = await sessionManager.loadSession(session.keyHash);
185
+ expect(loaded).toBeNull();
186
+ });
187
+ });
188
+
189
+ describe('17.4: Payment tokenization flow', () => {
190
+ it('should tokenize session and validate tokens', async () => {
191
+ const tokenizer = new PaymentTokenizer();
192
+ const session = createMockSession({
193
+ dailyLimitUSD: 100,
194
+ perTransactionLimitUSD: 25,
195
+ });
196
+
197
+ // Tokenize
198
+ const token = await tokenizer.tokenize(session);
199
+ expect(token.token).toBeDefined();
200
+ expect(token.limits.dailyLimitUSD).toBe(100);
201
+
202
+ // Validate
203
+ const validation = tokenizer.validate(token.token);
204
+ expect(validation.valid).toBe(true);
205
+ expect(validation.session?.keyHash).toBe(session.keyHash);
206
+
207
+ // Revoke
208
+ tokenizer.revoke(token.token);
209
+ const afterRevoke = tokenizer.validate(token.token);
210
+ expect(afterRevoke.valid).toBe(false);
211
+ });
212
+
213
+ it('should revoke all tokens when session is revoked', async () => {
214
+ const tokenizer = new PaymentTokenizer();
215
+ const session = createMockSession({ keyHash: '0xsession123' });
216
+
217
+ // Create multiple tokens
218
+ const token1 = await tokenizer.tokenize(session);
219
+ const token2 = await tokenizer.tokenize(session);
220
+
221
+ // Both should be valid
222
+ expect(tokenizer.validate(token1.token).valid).toBe(true);
223
+ expect(tokenizer.validate(token2.token).valid).toBe(true);
224
+
225
+ // Revoke all for session
226
+ const count = tokenizer.revokeAllForSession('0xsession123');
227
+ expect(count).toBe(2);
228
+
229
+ // Both should be invalid
230
+ expect(tokenizer.validate(token1.token).valid).toBe(false);
231
+ expect(tokenizer.validate(token2.token).valid).toBe(false);
232
+ });
233
+ });
234
+
235
+ describe('17.5: Monitoring and alerts integration', () => {
236
+ it('should log payments and trigger alerts', async () => {
237
+ const logger = new AuditLogger();
238
+ const alertManager = new AlertManager({
239
+ spendingThresholds: [0.8],
240
+ highValueThresholdUSD: 50,
241
+ });
242
+
243
+ const alerts: any[] = [];
244
+ alertManager.onAlert((alert) => alerts.push(alert));
245
+
246
+ // Log a payment
247
+ await logger.log({
248
+ txHash: '0x123',
249
+ status: 'confirmed',
250
+ chain: 30,
251
+ token: 'USDC',
252
+ amount: BigInt(100),
253
+ amountUSD: 100,
254
+ recipient: '0xrecipient',
255
+ timestamp: Date.now(),
256
+ }, 'session-1');
257
+
258
+ // Trigger spending alert
259
+ alertManager.checkSpending('session-1', 85, 100);
260
+
261
+ // Should have warning alert
262
+ expect(alerts.length).toBeGreaterThan(0);
263
+ expect(alerts[0].type).toBe('WARNING');
264
+ });
265
+
266
+ it('should require approval for high-value transactions', () => {
267
+ const alertManager = new AlertManager({
268
+ highValueThresholdUSD: 100,
269
+ });
270
+
271
+ expect(alertManager.isHighValueTransaction(50)).toBe(false);
272
+ expect(alertManager.isHighValueTransaction(150)).toBe(true);
273
+
274
+ // Request approval
275
+ const approval = alertManager.requestApproval('tx-high', 500);
276
+ expect(approval.approved).toBe(false);
277
+
278
+ // Approve
279
+ const approved = alertManager.approveTransaction('tx-high', 'master-key');
280
+ expect(approved).toBe(true);
281
+
282
+ // Verify approval
283
+ const status = alertManager.checkApproval('tx-high');
284
+ expect(status.approved).toBe(true);
285
+ });
286
+ });
287
+
288
+ describe('17.6: Full payment flow with spending tracking', () => {
289
+ it('should track spending across multiple transactions', async () => {
290
+ const sessionManager = new SessionKeyManager();
291
+ const session = createMockSession({
292
+ dailyLimitUSD: 100,
293
+ perTransactionLimitUSD: 50,
294
+ });
295
+
296
+ // Transaction 1: $30
297
+ let result = sessionManager.checkLimits(session, 30);
298
+ expect(result.allowed).toBe(true);
299
+ await sessionManager.recordSpending(session, 30);
300
+
301
+ // Transaction 2: $40
302
+ result = sessionManager.checkLimits(session, 40);
303
+ expect(result.allowed).toBe(true);
304
+ await sessionManager.recordSpending(session, 40);
305
+
306
+ // Transaction 3: $35 (would exceed daily)
307
+ result = sessionManager.checkLimits(session, 35);
308
+ expect(result.allowed).toBe(false);
309
+
310
+ // Transaction 4: $25 (just under remaining)
311
+ result = sessionManager.checkLimits(session, 25);
312
+ expect(result.allowed).toBe(true);
313
+ });
314
+ });
315
+ });