@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.
- package/CHANGELOG.md +108 -0
- package/MIGRATION.md +307 -0
- package/README.md +395 -0
- package/dist/index.d.mts +2327 -0
- package/dist/index.d.ts +2327 -0
- package/dist/index.js +5815 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +5759 -0
- package/dist/index.mjs.map +1 -0
- package/examples/basic-agent.ts +126 -0
- package/examples/mcp-claude.ts +75 -0
- package/examples/ucp-checkout.ts +92 -0
- package/examples/x402-integration.ts +75 -0
- package/package.json +36 -0
- package/src/AgentWallet.ts +432 -0
- package/src/chains/AptosChainClient.ts +29 -0
- package/src/chains/ChainClient.ts +73 -0
- package/src/chains/ChainClientFactory.ts +113 -0
- package/src/chains/EVMChainClient.ts +39 -0
- package/src/chains/SolanaChainClient.ts +37 -0
- package/src/chains/StarknetChainClient.ts +36 -0
- package/src/chains/SuiChainClient.ts +28 -0
- package/src/index.ts +83 -0
- package/src/mcp/MCPServer.ts +73 -0
- package/src/mcp/schemas.ts +60 -0
- package/src/monitoring/AlertManager.ts +258 -0
- package/src/monitoring/AuditLogger.ts +86 -0
- package/src/monitoring/BalanceCache.ts +44 -0
- package/src/monitoring/ComplianceExporter.ts +52 -0
- package/src/oracle/PythFeeds.ts +60 -0
- package/src/oracle/PythOracle.ts +121 -0
- package/src/performance/ConnectionPool.ts +217 -0
- package/src/performance/NonceManager.ts +91 -0
- package/src/performance/ParallelRouteFinder.ts +438 -0
- package/src/performance/TransactionPoller.ts +201 -0
- package/src/performance/TransactionQueue.ts +565 -0
- package/src/performance/index.ts +46 -0
- package/src/react/hooks.ts +298 -0
- package/src/routing/BridgeOrchestrator.ts +18 -0
- package/src/routing/CrossChainRouter.ts +501 -0
- package/src/routing/DEXAggregator.ts +448 -0
- package/src/routing/FeeEstimator.ts +43 -0
- package/src/session/SessionKeyManager.ts +312 -0
- package/src/session/SessionStorage.ts +80 -0
- package/src/session/SpendingTracker.ts +71 -0
- package/src/types/agent.ts +105 -0
- package/src/types/errors.ts +115 -0
- package/src/types/mcp.ts +22 -0
- package/src/types/ucp.ts +47 -0
- package/src/types/x402.ts +170 -0
- package/src/ucp/CapabilityNegotiator.ts +44 -0
- package/src/ucp/CredentialProvider.ts +73 -0
- package/src/ucp/PaymentTokenizer.ts +169 -0
- package/src/ucp/TransportAdapter.ts +18 -0
- package/src/ucp/UCPClient.ts +143 -0
- package/src/x402/NonceManager.ts +26 -0
- package/src/x402/PaymentParser.ts +225 -0
- package/src/x402/PaymentSigner.ts +305 -0
- package/src/x402/X402Client.ts +364 -0
- package/src/x402/adapters/CronosFacilitatorAdapter.ts +109 -0
- package/tests/alerts.test.ts +208 -0
- package/tests/chains.test.ts +242 -0
- package/tests/integration.test.ts +315 -0
- package/tests/monitoring.test.ts +435 -0
- package/tests/performance.test.ts +303 -0
- package/tests/property.test.ts +186 -0
- package/tests/react-hooks.test.ts +262 -0
- package/tests/session.test.ts +376 -0
- package/tests/ucp.test.ts +253 -0
- package/tests/x402.test.ts +385 -0
- package/tsconfig.json +26 -0
- package/tsup.config.ts +10 -0
- package/vitest.config.ts +16 -0
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Performance Module Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for NonceManager, TransactionQueue, and TransactionPoller.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
|
|
8
|
+
import { NonceManager } from '../src/performance/NonceManager';
|
|
9
|
+
import { TransactionQueue } from '../src/performance/TransactionQueue';
|
|
10
|
+
import { TransactionPoller } from '../src/performance/TransactionPoller';
|
|
11
|
+
|
|
12
|
+
describe('NonceManager', () => {
|
|
13
|
+
let manager: NonceManager;
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
manager = new NonceManager();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe('getNextNonce', () => {
|
|
20
|
+
it('should return 0 for fresh key', () => {
|
|
21
|
+
const nonce = manager.getNextNonce('key1');
|
|
22
|
+
expect(nonce).toBe(BigInt(0));
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should use on-chain value when provided', () => {
|
|
26
|
+
const nonce = manager.getNextNonce('key1', BigInt(5));
|
|
27
|
+
expect(nonce).toBe(BigInt(5));
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should use cached value on subsequent calls', () => {
|
|
31
|
+
manager.getNextNonce('key1', BigInt(5));
|
|
32
|
+
const nonce = manager.getNextNonce('key1');
|
|
33
|
+
expect(nonce).toBe(BigInt(5));
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should skip pending nonces', () => {
|
|
37
|
+
manager.reserveNonce('key1', BigInt(0));
|
|
38
|
+
manager.reserveNonce('key1', BigInt(1));
|
|
39
|
+
const nonce = manager.getNextNonce('key1');
|
|
40
|
+
expect(nonce).toBe(BigInt(2));
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('reserveNonce and releaseNonce', () => {
|
|
45
|
+
it('should reserve and release nonces', () => {
|
|
46
|
+
manager.reserveNonce('key1', BigInt(0));
|
|
47
|
+
expect(manager.getPendingNonces('key1')).toContain(BigInt(0));
|
|
48
|
+
|
|
49
|
+
manager.releaseNonce('key1', BigInt(0));
|
|
50
|
+
expect(manager.getPendingNonces('key1')).not.toContain(BigInt(0));
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe('confirmNonce', () => {
|
|
55
|
+
it('should update cache on confirmation', () => {
|
|
56
|
+
manager.reserveNonce('key1', BigInt(0));
|
|
57
|
+
manager.confirmNonce('key1', BigInt(0));
|
|
58
|
+
|
|
59
|
+
expect(manager.getPendingNonces('key1')).not.toContain(BigInt(0));
|
|
60
|
+
|
|
61
|
+
const nextNonce = manager.getNextNonce('key1');
|
|
62
|
+
expect(nextNonce).toBe(BigInt(1));
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe('clearCache', () => {
|
|
67
|
+
it('should clear all data for a key', () => {
|
|
68
|
+
manager.getNextNonce('key1', BigInt(5));
|
|
69
|
+
manager.reserveNonce('key1', BigInt(5));
|
|
70
|
+
|
|
71
|
+
manager.clearCache('key1');
|
|
72
|
+
|
|
73
|
+
expect(manager.getNextNonce('key1')).toBe(BigInt(0));
|
|
74
|
+
expect(manager.getPendingNonces('key1')).toHaveLength(0);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe('TransactionQueue', () => {
|
|
80
|
+
let queue: TransactionQueue;
|
|
81
|
+
|
|
82
|
+
beforeEach(() => {
|
|
83
|
+
queue = new TransactionQueue({ batchSize: 5, batchDelayMs: 10 });
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
afterEach(async () => {
|
|
87
|
+
await queue.shutdown({ waitForPending: false });
|
|
88
|
+
queue.clear();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe('enqueue', () => {
|
|
92
|
+
it('should add transaction to queue', () => {
|
|
93
|
+
const id = queue.enqueue({
|
|
94
|
+
keyHash: 'key1',
|
|
95
|
+
payload: {
|
|
96
|
+
recipient: '0x123',
|
|
97
|
+
amount: '100',
|
|
98
|
+
token: 'USDC',
|
|
99
|
+
chain: 30,
|
|
100
|
+
},
|
|
101
|
+
priority: 'normal',
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
expect(id).toBeDefined();
|
|
105
|
+
expect(id).toMatch(/^tx_/);
|
|
106
|
+
const tx = queue.get(id);
|
|
107
|
+
expect(tx?.status).toBe('pending');
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe('cancel', () => {
|
|
112
|
+
it('should cancel pending transaction', () => {
|
|
113
|
+
const id = queue.enqueue({
|
|
114
|
+
keyHash: 'key1',
|
|
115
|
+
payload: {
|
|
116
|
+
recipient: '0x123',
|
|
117
|
+
amount: '100',
|
|
118
|
+
token: 'USDC',
|
|
119
|
+
chain: 30,
|
|
120
|
+
},
|
|
121
|
+
priority: 'normal',
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const cancelled = queue.cancel(id);
|
|
125
|
+
expect(cancelled).toBe(true);
|
|
126
|
+
expect(queue.get(id)?.status).toBe('cancelled');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('should return false for non-existent transaction', () => {
|
|
130
|
+
const cancelled = queue.cancel('non-existent');
|
|
131
|
+
expect(cancelled).toBe(false);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe('flush', () => {
|
|
136
|
+
it('should process all pending transactions', async () => {
|
|
137
|
+
queue.enqueue({
|
|
138
|
+
keyHash: 'key1',
|
|
139
|
+
payload: { recipient: '0x1', amount: '10', token: 'USDC', chain: 30 },
|
|
140
|
+
priority: 'normal',
|
|
141
|
+
});
|
|
142
|
+
queue.enqueue({
|
|
143
|
+
keyHash: 'key1',
|
|
144
|
+
payload: { recipient: '0x2', amount: '20', token: 'USDC', chain: 30 },
|
|
145
|
+
priority: 'high',
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const results = await queue.flush();
|
|
149
|
+
|
|
150
|
+
expect(results.length).toBeGreaterThan(0);
|
|
151
|
+
expect(results[0].successCount).toBe(2);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe('getStats', () => {
|
|
156
|
+
it('should return queue statistics', async () => {
|
|
157
|
+
queue.enqueue({
|
|
158
|
+
keyHash: 'key1',
|
|
159
|
+
payload: { recipient: '0x1', amount: '10', token: 'USDC', chain: 30 },
|
|
160
|
+
priority: 'normal',
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const statsBefore = queue.getStats();
|
|
164
|
+
expect(statsBefore.pending).toBe(1);
|
|
165
|
+
|
|
166
|
+
await queue.flush();
|
|
167
|
+
|
|
168
|
+
const statsAfter = queue.getStats();
|
|
169
|
+
expect(statsAfter.completed).toBe(1);
|
|
170
|
+
expect(statsAfter.pending).toBe(0);
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
describe('dead letter queue', () => {
|
|
175
|
+
it('should move failed transactions to dead letter queue', async () => {
|
|
176
|
+
// Create queue with executor that always fails
|
|
177
|
+
const failingQueue = new TransactionQueue(
|
|
178
|
+
{ batchSize: 5, batchDelayMs: 10, defaultMaxAttempts: 1 },
|
|
179
|
+
{
|
|
180
|
+
execute: () => Promise.reject(new Error('Always fails')),
|
|
181
|
+
}
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
failingQueue.enqueue({
|
|
185
|
+
keyHash: 'key1',
|
|
186
|
+
payload: { recipient: '0x1', amount: '10', token: 'USDC', chain: 30 },
|
|
187
|
+
priority: 'normal',
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
await failingQueue.flush();
|
|
191
|
+
|
|
192
|
+
const dlq = failingQueue.getDeadLetterQueue();
|
|
193
|
+
expect(dlq.length).toBe(1);
|
|
194
|
+
expect(dlq[0].lastError).toBe('Always fails');
|
|
195
|
+
|
|
196
|
+
await failingQueue.shutdown({ waitForPending: false });
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
describe('events', () => {
|
|
201
|
+
it('should emit events during processing', async () => {
|
|
202
|
+
const events: string[] = [];
|
|
203
|
+
|
|
204
|
+
queue.on('transaction:enqueued', () => events.push('enqueued'));
|
|
205
|
+
queue.on('transaction:processing', () => events.push('processing'));
|
|
206
|
+
queue.on('transaction:completed', () => events.push('completed'));
|
|
207
|
+
queue.on('batch:completed', () => events.push('batch'));
|
|
208
|
+
|
|
209
|
+
queue.enqueue({
|
|
210
|
+
keyHash: 'key1',
|
|
211
|
+
payload: { recipient: '0x1', amount: '10', token: 'USDC', chain: 30 },
|
|
212
|
+
priority: 'normal',
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
expect(events).toContain('enqueued');
|
|
216
|
+
|
|
217
|
+
await queue.flush();
|
|
218
|
+
|
|
219
|
+
expect(events).toContain('processing');
|
|
220
|
+
expect(events).toContain('completed');
|
|
221
|
+
expect(events).toContain('batch');
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
describe('TransactionPoller', () => {
|
|
227
|
+
let poller: TransactionPoller;
|
|
228
|
+
let mockChecker: ReturnType<typeof vi.fn<[string, number], Promise<{ confirmed: boolean; confirmations?: number; blockNumber?: number }>>>;
|
|
229
|
+
|
|
230
|
+
beforeEach(() => {
|
|
231
|
+
mockChecker = vi.fn().mockResolvedValue({ confirmed: false });
|
|
232
|
+
poller = new TransactionPoller(mockChecker);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
afterEach(() => {
|
|
236
|
+
poller.destroy();
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
describe('track', () => {
|
|
240
|
+
it('should start tracking a transaction', () => {
|
|
241
|
+
poller.track('0x123', 30);
|
|
242
|
+
|
|
243
|
+
const status = poller.getStatus('0x123');
|
|
244
|
+
expect(status).toBeDefined();
|
|
245
|
+
expect(status?.status).toBe('pending');
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('should call callback on confirmation', async () => {
|
|
249
|
+
mockChecker.mockResolvedValueOnce({ confirmed: true, confirmations: 1, blockNumber: 100 });
|
|
250
|
+
|
|
251
|
+
const callback = vi.fn();
|
|
252
|
+
poller.track('0x123', 30, callback);
|
|
253
|
+
|
|
254
|
+
// Wait for poll cycle
|
|
255
|
+
await new Promise(resolve => setTimeout(resolve, 2500));
|
|
256
|
+
|
|
257
|
+
expect(callback).toHaveBeenCalled();
|
|
258
|
+
expect(callback).toHaveBeenCalledWith(
|
|
259
|
+
expect.objectContaining({
|
|
260
|
+
txHash: '0x123',
|
|
261
|
+
status: 'confirmed',
|
|
262
|
+
})
|
|
263
|
+
);
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
describe('untrack', () => {
|
|
268
|
+
it('should stop tracking a transaction', () => {
|
|
269
|
+
poller.track('0x123', 30);
|
|
270
|
+
poller.untrack('0x123');
|
|
271
|
+
|
|
272
|
+
expect(poller.getStatus('0x123')).toBeUndefined();
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
describe('onConfirmation', () => {
|
|
277
|
+
it('should subscribe to global events', async () => {
|
|
278
|
+
mockChecker.mockResolvedValueOnce({ confirmed: true, confirmations: 1 });
|
|
279
|
+
|
|
280
|
+
const globalCallback = vi.fn();
|
|
281
|
+
const unsubscribe = poller.onConfirmation(globalCallback);
|
|
282
|
+
|
|
283
|
+
poller.track('0x456', 30);
|
|
284
|
+
|
|
285
|
+
// Wait for poll cycle
|
|
286
|
+
await new Promise(resolve => setTimeout(resolve, 2500));
|
|
287
|
+
|
|
288
|
+
expect(globalCallback).toHaveBeenCalled();
|
|
289
|
+
|
|
290
|
+
unsubscribe();
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
describe('getPending', () => {
|
|
295
|
+
it('should return all pending transactions', () => {
|
|
296
|
+
poller.track('0x111', 30);
|
|
297
|
+
poller.track('0x222', 30);
|
|
298
|
+
|
|
299
|
+
const pending = poller.getPending();
|
|
300
|
+
expect(pending).toHaveLength(2);
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
});
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Property Tests for Agent SDK
|
|
3
|
+
*
|
|
4
|
+
* Comprehensive property-based tests using fast-check.
|
|
5
|
+
* These tests verify invariants that must hold across all inputs.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
9
|
+
import * as fc from 'fast-check';
|
|
10
|
+
import { PaymentTokenizer } from '../src/ucp/PaymentTokenizer';
|
|
11
|
+
import { AlertManager } from '../src/monitoring/AlertManager';
|
|
12
|
+
import { StoredSession } from '../src/session/SessionStorage';
|
|
13
|
+
|
|
14
|
+
// Helper to create mock sessions
|
|
15
|
+
function createMockSession(overrides: Partial<{
|
|
16
|
+
keyHash: string;
|
|
17
|
+
dailyLimitUSD: number;
|
|
18
|
+
perTransactionLimitUSD: number;
|
|
19
|
+
expiryTimestamp: number;
|
|
20
|
+
dailySpentUSD: number;
|
|
21
|
+
}> = {}): StoredSession {
|
|
22
|
+
const {
|
|
23
|
+
keyHash = '0x' + 'a'.repeat(64),
|
|
24
|
+
dailyLimitUSD = 100,
|
|
25
|
+
perTransactionLimitUSD = 25,
|
|
26
|
+
expiryTimestamp = Date.now() + 3600000,
|
|
27
|
+
dailySpentUSD = 0,
|
|
28
|
+
} = overrides;
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
keyHash,
|
|
32
|
+
encryptedPrivateKey: '0x' + 'b'.repeat(64),
|
|
33
|
+
publicKey: '0x04' + 'c'.repeat(128),
|
|
34
|
+
config: {
|
|
35
|
+
dailyLimitUSD,
|
|
36
|
+
perTransactionLimitUSD,
|
|
37
|
+
expiryTimestamp,
|
|
38
|
+
allowedChains: [30],
|
|
39
|
+
},
|
|
40
|
+
metadata: {
|
|
41
|
+
createdAt: Date.now(),
|
|
42
|
+
lastUsedAt: Date.now(),
|
|
43
|
+
totalSpentUSD: dailySpentUSD,
|
|
44
|
+
dailySpentUSD,
|
|
45
|
+
dailyResetAt: Date.now() + 86400000,
|
|
46
|
+
transactionCount: 0,
|
|
47
|
+
},
|
|
48
|
+
masterKeyHash: '0x' + 'd'.repeat(64),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
describe('Property Tests', () => {
|
|
53
|
+
describe('Property 7: Payment Token Enforces Original Session Limits', () => {
|
|
54
|
+
it('token limits should match session limits', async () => {
|
|
55
|
+
await fc.assert(
|
|
56
|
+
fc.asyncProperty(
|
|
57
|
+
fc.integer({ min: 1, max: 10000 }), // dailyLimit
|
|
58
|
+
fc.integer({ min: 1, max: 1000 }), // perTxLimit
|
|
59
|
+
async (dailyLimit, perTxLimit) => {
|
|
60
|
+
const actualPerTxLimit = Math.min(perTxLimit, dailyLimit);
|
|
61
|
+
const tokenizer = new PaymentTokenizer();
|
|
62
|
+
const session = createMockSession({
|
|
63
|
+
dailyLimitUSD: dailyLimit,
|
|
64
|
+
perTransactionLimitUSD: actualPerTxLimit,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const token = await tokenizer.tokenize(session);
|
|
68
|
+
|
|
69
|
+
expect(token.limits.dailyLimitUSD).toBe(dailyLimit);
|
|
70
|
+
expect(token.limits.perTransactionLimitUSD).toBe(actualPerTxLimit);
|
|
71
|
+
}
|
|
72
|
+
),
|
|
73
|
+
{ numRuns: 50 }
|
|
74
|
+
);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe('Property 14: Spending Alert Threshold Accuracy', () => {
|
|
79
|
+
it('should trigger alert at exactly the right threshold', () => {
|
|
80
|
+
fc.assert(
|
|
81
|
+
fc.property(
|
|
82
|
+
fc.integer({ min: 1, max: 10000 }), // dailyLimit
|
|
83
|
+
fc.double({ min: 0.01, max: 1.0, noNaN: true }), // spendingRatio
|
|
84
|
+
(dailyLimit, spendingRatio) => {
|
|
85
|
+
const manager = new AlertManager({
|
|
86
|
+
spendingThresholds: [0.5, 0.8, 0.9, 1.0],
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const alerts: any[] = [];
|
|
90
|
+
manager.onAlert((alert) => alerts.push(alert));
|
|
91
|
+
|
|
92
|
+
const spent = dailyLimit * spendingRatio;
|
|
93
|
+
manager.checkSpending('test-session', spent, dailyLimit);
|
|
94
|
+
|
|
95
|
+
// Check that alerts match thresholds
|
|
96
|
+
if (spendingRatio >= 0.5) {
|
|
97
|
+
expect(alerts.length).toBeGreaterThanOrEqual(1);
|
|
98
|
+
}
|
|
99
|
+
if (spendingRatio >= 0.9) {
|
|
100
|
+
// Should have critical alert
|
|
101
|
+
expect(alerts.some(a => a.type === 'CRITICAL')).toBe(true);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
),
|
|
105
|
+
{ numRuns: 50 }
|
|
106
|
+
);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe('Property 19: Payment Token Expiration Enforcement', () => {
|
|
111
|
+
it('tokens should not be valid after expiration', async () => {
|
|
112
|
+
await fc.assert(
|
|
113
|
+
fc.asyncProperty(
|
|
114
|
+
fc.integer({ min: 1, max: 1000 }), // token TTL in ms (short for testing)
|
|
115
|
+
async (ttlMs) => {
|
|
116
|
+
const tokenizer = new PaymentTokenizer();
|
|
117
|
+
const session = createMockSession({
|
|
118
|
+
expiryTimestamp: Date.now() + ttlMs + 1000,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const token = await tokenizer.tokenize(session, ttlMs);
|
|
122
|
+
|
|
123
|
+
// Token should be valid immediately
|
|
124
|
+
const validResult = tokenizer.validate(token.token);
|
|
125
|
+
expect(validResult.valid).toBe(true);
|
|
126
|
+
}
|
|
127
|
+
),
|
|
128
|
+
{ numRuns: 20 }
|
|
129
|
+
);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe('Property 20: High-Value Transaction Approval Window', () => {
|
|
134
|
+
it('approvals should expire after window', () => {
|
|
135
|
+
fc.assert(
|
|
136
|
+
fc.property(
|
|
137
|
+
fc.integer({ min: 1000, max: 10000 }), // amount
|
|
138
|
+
(amount) => {
|
|
139
|
+
const manager = new AlertManager({
|
|
140
|
+
highValueThresholdUSD: 500,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const approval = manager.requestApproval('tx-123', amount);
|
|
144
|
+
|
|
145
|
+
// Check window
|
|
146
|
+
expect(approval.expiresAt).toBeGreaterThan(Date.now());
|
|
147
|
+
expect(approval.expiresAt - approval.requestedAt).toBeLessThanOrEqual(5 * 60 * 1000);
|
|
148
|
+
}
|
|
149
|
+
),
|
|
150
|
+
{ numRuns: 20 }
|
|
151
|
+
);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('approval check should return not found for non-existent transactions', () => {
|
|
155
|
+
const manager = new AlertManager();
|
|
156
|
+
const result = manager.checkApproval('non-existent');
|
|
157
|
+
expect(result.approved).toBe(false);
|
|
158
|
+
expect(result.expired).toBe(false);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
describe('Property: High-Value Detection Threshold', () => {
|
|
163
|
+
it('should correctly identify high-value transactions', () => {
|
|
164
|
+
fc.assert(
|
|
165
|
+
fc.property(
|
|
166
|
+
fc.integer({ min: 100, max: 5000 }), // threshold
|
|
167
|
+
fc.integer({ min: 1, max: 10000 }), // amount
|
|
168
|
+
(threshold, amount) => {
|
|
169
|
+
const manager = new AlertManager({
|
|
170
|
+
highValueThresholdUSD: threshold,
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const isHighValue = manager.isHighValueTransaction(amount);
|
|
174
|
+
|
|
175
|
+
if (amount >= threshold) {
|
|
176
|
+
expect(isHighValue).toBe(true);
|
|
177
|
+
} else {
|
|
178
|
+
expect(isHighValue).toBe(false);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
),
|
|
182
|
+
{ numRuns: 50 }
|
|
183
|
+
);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
});
|