@agirails/sdk 2.0.0-beta

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 (154) hide show
  1. package/README.md +183 -0
  2. package/dist/ACTPClient.d.ts +52 -0
  3. package/dist/ACTPClient.d.ts.map +1 -0
  4. package/dist/ACTPClient.js +120 -0
  5. package/dist/ACTPClient.js.map +1 -0
  6. package/dist/abi/ACTPKernel.json +1340 -0
  7. package/dist/abi/ERC20.json +38 -0
  8. package/dist/abi/EscrowVault.json +64 -0
  9. package/dist/builders/DeliveryProofBuilder.d.ts +37 -0
  10. package/dist/builders/DeliveryProofBuilder.d.ts.map +1 -0
  11. package/dist/builders/DeliveryProofBuilder.js +165 -0
  12. package/dist/builders/DeliveryProofBuilder.js.map +1 -0
  13. package/dist/builders/QuoteBuilder.d.ts +68 -0
  14. package/dist/builders/QuoteBuilder.d.ts.map +1 -0
  15. package/dist/builders/QuoteBuilder.js +255 -0
  16. package/dist/builders/QuoteBuilder.js.map +1 -0
  17. package/dist/builders/index.d.ts +3 -0
  18. package/dist/builders/index.d.ts.map +1 -0
  19. package/dist/builders/index.js +10 -0
  20. package/dist/builders/index.js.map +1 -0
  21. package/dist/config/networks.d.ts +27 -0
  22. package/dist/config/networks.d.ts.map +1 -0
  23. package/dist/config/networks.js +103 -0
  24. package/dist/config/networks.js.map +1 -0
  25. package/dist/errors/index.d.ts +38 -0
  26. package/dist/errors/index.d.ts.map +1 -0
  27. package/dist/errors/index.js +87 -0
  28. package/dist/errors/index.js.map +1 -0
  29. package/dist/index.d.ts +19 -0
  30. package/dist/index.d.ts.map +1 -0
  31. package/dist/index.js +68 -0
  32. package/dist/index.js.map +1 -0
  33. package/dist/protocol/ACTPKernel.d.ts +30 -0
  34. package/dist/protocol/ACTPKernel.d.ts.map +1 -0
  35. package/dist/protocol/ACTPKernel.js +261 -0
  36. package/dist/protocol/ACTPKernel.js.map +1 -0
  37. package/dist/protocol/EASHelper.d.ts +23 -0
  38. package/dist/protocol/EASHelper.d.ts.map +1 -0
  39. package/dist/protocol/EASHelper.js +106 -0
  40. package/dist/protocol/EASHelper.js.map +1 -0
  41. package/dist/protocol/EscrowVault.d.ts +24 -0
  42. package/dist/protocol/EscrowVault.d.ts.map +1 -0
  43. package/dist/protocol/EscrowVault.js +114 -0
  44. package/dist/protocol/EscrowVault.js.map +1 -0
  45. package/dist/protocol/EventMonitor.d.ts +18 -0
  46. package/dist/protocol/EventMonitor.d.ts.map +1 -0
  47. package/dist/protocol/EventMonitor.js +92 -0
  48. package/dist/protocol/EventMonitor.js.map +1 -0
  49. package/dist/protocol/MessageSigner.d.ts +23 -0
  50. package/dist/protocol/MessageSigner.d.ts.map +1 -0
  51. package/dist/protocol/MessageSigner.js +178 -0
  52. package/dist/protocol/MessageSigner.js.map +1 -0
  53. package/dist/protocol/ProofGenerator.d.ts +22 -0
  54. package/dist/protocol/ProofGenerator.d.ts.map +1 -0
  55. package/dist/protocol/ProofGenerator.js +64 -0
  56. package/dist/protocol/ProofGenerator.js.map +1 -0
  57. package/dist/protocol/QuoteBuilder.d.ts +2 -0
  58. package/dist/protocol/QuoteBuilder.d.ts.map +1 -0
  59. package/dist/protocol/QuoteBuilder.js +7 -0
  60. package/dist/protocol/QuoteBuilder.js.map +1 -0
  61. package/dist/types/eip712.d.ts +106 -0
  62. package/dist/types/eip712.d.ts.map +1 -0
  63. package/dist/types/eip712.js +84 -0
  64. package/dist/types/eip712.js.map +1 -0
  65. package/dist/types/escrow.d.ts +18 -0
  66. package/dist/types/escrow.d.ts.map +1 -0
  67. package/dist/types/escrow.js +3 -0
  68. package/dist/types/escrow.js.map +1 -0
  69. package/dist/types/index.d.ts +6 -0
  70. package/dist/types/index.d.ts.map +1 -0
  71. package/dist/types/index.js +22 -0
  72. package/dist/types/index.js.map +1 -0
  73. package/dist/types/message.d.ts +109 -0
  74. package/dist/types/message.d.ts.map +1 -0
  75. package/dist/types/message.js +3 -0
  76. package/dist/types/message.js.map +1 -0
  77. package/dist/types/state.d.ts +19 -0
  78. package/dist/types/state.d.ts.map +1 -0
  79. package/dist/types/state.js +49 -0
  80. package/dist/types/state.js.map +1 -0
  81. package/dist/types/transaction.d.ts +36 -0
  82. package/dist/types/transaction.d.ts.map +1 -0
  83. package/dist/types/transaction.js +3 -0
  84. package/dist/types/transaction.js.map +1 -0
  85. package/dist/utils/IPFSClient.d.ts +37 -0
  86. package/dist/utils/IPFSClient.d.ts.map +1 -0
  87. package/dist/utils/IPFSClient.js +128 -0
  88. package/dist/utils/IPFSClient.js.map +1 -0
  89. package/dist/utils/NonceManager.d.ts +34 -0
  90. package/dist/utils/NonceManager.d.ts.map +1 -0
  91. package/dist/utils/NonceManager.js +114 -0
  92. package/dist/utils/NonceManager.js.map +1 -0
  93. package/dist/utils/ReceivedNonceTracker.d.ts +35 -0
  94. package/dist/utils/ReceivedNonceTracker.d.ts.map +1 -0
  95. package/dist/utils/ReceivedNonceTracker.js +196 -0
  96. package/dist/utils/ReceivedNonceTracker.js.map +1 -0
  97. package/dist/utils/canonicalJson.d.ts +4 -0
  98. package/dist/utils/canonicalJson.d.ts.map +1 -0
  99. package/dist/utils/canonicalJson.js +21 -0
  100. package/dist/utils/canonicalJson.js.map +1 -0
  101. package/dist/utils/computeTypeHash.d.ts +3 -0
  102. package/dist/utils/computeTypeHash.d.ts.map +1 -0
  103. package/dist/utils/computeTypeHash.js +30 -0
  104. package/dist/utils/computeTypeHash.js.map +1 -0
  105. package/dist/utils/validation.d.ts +6 -0
  106. package/dist/utils/validation.d.ts.map +1 -0
  107. package/dist/utils/validation.js +46 -0
  108. package/dist/utils/validation.js.map +1 -0
  109. package/package.json +73 -0
  110. package/src/ACTPClient.ts +276 -0
  111. package/src/__tests__/ProofGenerator.test.ts +124 -0
  112. package/src/__tests__/QuoteBuilder.test.ts +516 -0
  113. package/src/__tests__/StateMachine.test.ts +82 -0
  114. package/src/__tests__/builders/DeliveryProofBuilder.test.ts +581 -0
  115. package/src/__tests__/integration/ACTPClient.test.ts +263 -0
  116. package/src/__tests__/integration.test.ts +289 -0
  117. package/src/__tests__/protocol/EASHelper.test.ts +472 -0
  118. package/src/__tests__/protocol/EventMonitor.test.ts +382 -0
  119. package/src/__tests__/security/ACTPKernel.security.test.ts +1167 -0
  120. package/src/__tests__/security/EscrowVault.security.test.ts +570 -0
  121. package/src/__tests__/security/MessageSigner.security.test.ts +286 -0
  122. package/src/__tests__/security/NonceReplay.security.test.ts +501 -0
  123. package/src/__tests__/security/validation.security.test.ts +376 -0
  124. package/src/__tests__/utils/IPFSClient.test.ts +262 -0
  125. package/src/__tests__/utils/NonceManager.test.ts +205 -0
  126. package/src/__tests__/utils/canonicalJson.test.ts +153 -0
  127. package/src/abi/ACTPKernel.json +1340 -0
  128. package/src/abi/ERC20.json +40 -0
  129. package/src/abi/EscrowVault.json +66 -0
  130. package/src/builders/DeliveryProofBuilder.ts +326 -0
  131. package/src/builders/QuoteBuilder.ts +483 -0
  132. package/src/builders/index.ts +17 -0
  133. package/src/config/networks.ts +165 -0
  134. package/src/errors/index.ts +130 -0
  135. package/src/index.ts +108 -0
  136. package/src/protocol/ACTPKernel.ts +625 -0
  137. package/src/protocol/EASHelper.ts +197 -0
  138. package/src/protocol/EscrowVault.ts +237 -0
  139. package/src/protocol/EventMonitor.ts +161 -0
  140. package/src/protocol/MessageSigner.ts +336 -0
  141. package/src/protocol/ProofGenerator.ts +119 -0
  142. package/src/protocol/QuoteBuilder.ts +15 -0
  143. package/src/types/eip712.ts +175 -0
  144. package/src/types/escrow.ts +26 -0
  145. package/src/types/index.ts +10 -0
  146. package/src/types/message.ts +145 -0
  147. package/src/types/state.ts +77 -0
  148. package/src/types/transaction.ts +54 -0
  149. package/src/utils/IPFSClient.ts +248 -0
  150. package/src/utils/NonceManager.ts +293 -0
  151. package/src/utils/ReceivedNonceTracker.ts +397 -0
  152. package/src/utils/canonicalJson.ts +38 -0
  153. package/src/utils/computeTypeHash.ts +50 -0
  154. package/src/utils/validation.ts +82 -0
@@ -0,0 +1,516 @@
1
+ /**
2
+ * QuoteBuilder Tests
3
+ * Reference: AIP-2 §9 (Test Vectors), §10.2 (Validation Checklist)
4
+ */
5
+
6
+ import { Wallet, parseUnits, HDNodeWallet } from 'ethers';
7
+ import { QuoteBuilder, QuoteParams, QuoteMessage } from '../builders/QuoteBuilder';
8
+ import { NonceManager, InMemoryNonceManager } from '../utils/NonceManager';
9
+ import { IPFSClient } from '../utils/IPFSClient';
10
+
11
+ describe('QuoteBuilder', () => {
12
+ let quoteBuilder: QuoteBuilder;
13
+ let signer: HDNodeWallet;
14
+ let nonceManager: NonceManager;
15
+ let mockIPFS: jest.Mocked<IPFSClient>;
16
+
17
+ // Test constants
18
+ const TEST_CHAIN_ID = 84532; // Base Sepolia
19
+ const TEST_KERNEL_ADDRESS = '0x1234567890123456789012345678901234567890';
20
+ const TEST_TX_ID = '0x7d87c3b8e23a5c9d1f4e6b2a8c5d9e3f1a7b4c6d8e2f5a3b9c1d7e4f6a8b2c5d';
21
+ const PROVIDER_WALLET = Wallet.createRandom();
22
+ const CONSUMER_ADDRESS = '0x1234567890abcdef1234567890abcdef12345678';
23
+
24
+ const PROVIDER_DID = `did:ethr:${TEST_CHAIN_ID}:${PROVIDER_WALLET.address}`;
25
+ const CONSUMER_DID = `did:ethr:${TEST_CHAIN_ID}:${CONSUMER_ADDRESS}`;
26
+
27
+ beforeEach(() => {
28
+ signer = PROVIDER_WALLET;
29
+ nonceManager = new InMemoryNonceManager();
30
+
31
+ // Mock IPFS client
32
+ mockIPFS = {
33
+ add: jest.fn().mockResolvedValue('bafybeigtest123'),
34
+ pin: jest.fn().mockResolvedValue(undefined),
35
+ get: jest.fn(),
36
+ unpin: jest.fn()
37
+ } as any;
38
+
39
+ quoteBuilder = new QuoteBuilder(signer, nonceManager, mockIPFS);
40
+ });
41
+
42
+ describe('build()', () => {
43
+ it('should build valid quote with all required fields', async () => {
44
+ const params: QuoteParams = {
45
+ txId: TEST_TX_ID,
46
+ provider: PROVIDER_DID,
47
+ consumer: CONSUMER_DID,
48
+ quotedAmount: parseUnits('7.5', 6).toString(), // $7.50
49
+ originalAmount: parseUnits('5.0', 6).toString(), // $5.00
50
+ maxPrice: parseUnits('10.0', 6).toString(), // $10.00
51
+ chainId: TEST_CHAIN_ID,
52
+ kernelAddress: TEST_KERNEL_ADDRESS
53
+ };
54
+
55
+ const quote = await quoteBuilder.build(params);
56
+
57
+ // Verify structure
58
+ expect(quote.type).toBe('agirails.quote.v1');
59
+ expect(quote.version).toBe('1.0.0');
60
+ expect(quote.txId).toBe(TEST_TX_ID);
61
+ expect(quote.provider).toBe(PROVIDER_DID);
62
+ expect(quote.consumer).toBe(CONSUMER_DID);
63
+ expect(quote.quotedAmount).toBe(params.quotedAmount);
64
+ expect(quote.originalAmount).toBe(params.originalAmount);
65
+ expect(quote.maxPrice).toBe(params.maxPrice);
66
+ expect(quote.currency).toBe('USDC');
67
+ expect(quote.decimals).toBe(6);
68
+ expect(quote.chainId).toBe(TEST_CHAIN_ID);
69
+ expect(quote.nonce).toBe(1);
70
+ expect(quote.signature).toMatch(/^0x[a-fA-F0-9]{130}$/);
71
+
72
+ // Verify timestamps
73
+ const now = Math.floor(Date.now() / 1000);
74
+ expect(quote.quotedAt).toBeGreaterThanOrEqual(now - 5);
75
+ expect(quote.quotedAt).toBeLessThanOrEqual(now + 5);
76
+ expect(quote.expiresAt).toBe(quote.quotedAt + 3600); // Default 1 hour
77
+ });
78
+
79
+ it('should build quote with custom expiry', async () => {
80
+ const customExpiry = Math.floor(Date.now() / 1000) + 7200; // 2 hours
81
+
82
+ const params: QuoteParams = {
83
+ txId: TEST_TX_ID,
84
+ provider: PROVIDER_DID,
85
+ consumer: CONSUMER_DID,
86
+ quotedAmount: '7500000',
87
+ originalAmount: '5000000',
88
+ maxPrice: '10000000',
89
+ expiresAt: customExpiry,
90
+ chainId: TEST_CHAIN_ID,
91
+ kernelAddress: TEST_KERNEL_ADDRESS
92
+ };
93
+
94
+ const quote = await quoteBuilder.build(params);
95
+ expect(quote.expiresAt).toBe(customExpiry);
96
+ });
97
+
98
+ it('should build quote with justification', async () => {
99
+ const params: QuoteParams = {
100
+ txId: TEST_TX_ID,
101
+ provider: PROVIDER_DID,
102
+ consumer: CONSUMER_DID,
103
+ quotedAmount: '7500000',
104
+ originalAmount: '5000000',
105
+ maxPrice: '10000000',
106
+ justification: {
107
+ reason: 'Dataset size requires additional compute',
108
+ estimatedTime: 300,
109
+ computeCost: 2.5,
110
+ breakdown: {
111
+ basePrice: 5.0,
112
+ additionalCompute: 2.5
113
+ }
114
+ },
115
+ chainId: TEST_CHAIN_ID,
116
+ kernelAddress: TEST_KERNEL_ADDRESS
117
+ };
118
+
119
+ const quote = await quoteBuilder.build(params);
120
+ expect(quote.justification).toEqual(params.justification);
121
+ });
122
+
123
+ it('should increment nonce for subsequent quotes', async () => {
124
+ const params: QuoteParams = {
125
+ txId: TEST_TX_ID,
126
+ provider: PROVIDER_DID,
127
+ consumer: CONSUMER_DID,
128
+ quotedAmount: '7500000',
129
+ originalAmount: '5000000',
130
+ maxPrice: '10000000',
131
+ chainId: TEST_CHAIN_ID,
132
+ kernelAddress: TEST_KERNEL_ADDRESS
133
+ };
134
+
135
+ const quote1 = await quoteBuilder.build(params);
136
+ const quote2 = await quoteBuilder.build({ ...params, txId: TEST_TX_ID.replace('5d', '6e') });
137
+
138
+ expect(quote1.nonce).toBe(1);
139
+ expect(quote2.nonce).toBe(2);
140
+ });
141
+ });
142
+
143
+ describe('build() - validation errors', () => {
144
+ it('should reject quote below originalAmount', async () => {
145
+ const params: QuoteParams = {
146
+ txId: TEST_TX_ID,
147
+ provider: PROVIDER_DID,
148
+ consumer: CONSUMER_DID,
149
+ quotedAmount: '4000000', // $4.00 (below $5.00 original)
150
+ originalAmount: '5000000',
151
+ maxPrice: '10000000',
152
+ chainId: TEST_CHAIN_ID,
153
+ kernelAddress: TEST_KERNEL_ADDRESS
154
+ };
155
+
156
+ await expect(quoteBuilder.build(params)).rejects.toThrow('quotedAmount must be >= originalAmount');
157
+ });
158
+
159
+ it('should reject quote above maxPrice', async () => {
160
+ const params: QuoteParams = {
161
+ txId: TEST_TX_ID,
162
+ provider: PROVIDER_DID,
163
+ consumer: CONSUMER_DID,
164
+ quotedAmount: '11000000', // $11.00 (above $10.00 max)
165
+ originalAmount: '5000000',
166
+ maxPrice: '10000000',
167
+ chainId: TEST_CHAIN_ID,
168
+ kernelAddress: TEST_KERNEL_ADDRESS
169
+ };
170
+
171
+ await expect(quoteBuilder.build(params)).rejects.toThrow('quotedAmount must be <= maxPrice');
172
+ });
173
+
174
+ it('should reject quote below platform minimum ($0.05)', async () => {
175
+ const params: QuoteParams = {
176
+ txId: TEST_TX_ID,
177
+ provider: PROVIDER_DID,
178
+ consumer: CONSUMER_DID,
179
+ quotedAmount: '40000', // $0.04 (below $0.05 minimum)
180
+ originalAmount: '30000',
181
+ maxPrice: '100000',
182
+ chainId: TEST_CHAIN_ID,
183
+ kernelAddress: TEST_KERNEL_ADDRESS
184
+ };
185
+
186
+ await expect(quoteBuilder.build(params)).rejects.toThrow('quotedAmount must be >= $0.05');
187
+ });
188
+
189
+ it('should reject expiry in the past', async () => {
190
+ const pastExpiry = Math.floor(Date.now() / 1000) - 3600;
191
+
192
+ const params: QuoteParams = {
193
+ txId: TEST_TX_ID,
194
+ provider: PROVIDER_DID,
195
+ consumer: CONSUMER_DID,
196
+ quotedAmount: '7500000',
197
+ originalAmount: '5000000',
198
+ maxPrice: '10000000',
199
+ expiresAt: pastExpiry,
200
+ chainId: TEST_CHAIN_ID,
201
+ kernelAddress: TEST_KERNEL_ADDRESS
202
+ };
203
+
204
+ await expect(quoteBuilder.build(params)).rejects.toThrow('expiresAt must be in the future');
205
+ });
206
+
207
+ it('should reject expiry beyond 24 hours', async () => {
208
+ const farFutureExpiry = Math.floor(Date.now() / 1000) + 86401; // 24h + 1 second
209
+
210
+ const params: QuoteParams = {
211
+ txId: TEST_TX_ID,
212
+ provider: PROVIDER_DID,
213
+ consumer: CONSUMER_DID,
214
+ quotedAmount: '7500000',
215
+ originalAmount: '5000000',
216
+ maxPrice: '10000000',
217
+ expiresAt: farFutureExpiry,
218
+ chainId: TEST_CHAIN_ID,
219
+ kernelAddress: TEST_KERNEL_ADDRESS
220
+ };
221
+
222
+ await expect(quoteBuilder.build(params)).rejects.toThrow('expiresAt cannot be more than 24 hours');
223
+ });
224
+
225
+ it('should reject invalid provider DID format', async () => {
226
+ const params: QuoteParams = {
227
+ txId: TEST_TX_ID,
228
+ provider: 'invalid-did',
229
+ consumer: CONSUMER_DID,
230
+ quotedAmount: '7500000',
231
+ originalAmount: '5000000',
232
+ maxPrice: '10000000',
233
+ chainId: TEST_CHAIN_ID,
234
+ kernelAddress: TEST_KERNEL_ADDRESS
235
+ };
236
+
237
+ await expect(quoteBuilder.build(params)).rejects.toThrow('provider must be valid did:ethr format');
238
+ });
239
+
240
+ it('should reject invalid transaction ID format', async () => {
241
+ const params: QuoteParams = {
242
+ txId: '0xinvalid',
243
+ provider: PROVIDER_DID,
244
+ consumer: CONSUMER_DID,
245
+ quotedAmount: '7500000',
246
+ originalAmount: '5000000',
247
+ maxPrice: '10000000',
248
+ chainId: TEST_CHAIN_ID,
249
+ kernelAddress: TEST_KERNEL_ADDRESS
250
+ };
251
+
252
+ await expect(quoteBuilder.build(params)).rejects.toThrow('txId must be valid bytes32 hex string');
253
+ });
254
+
255
+ it('should reject invalid chainId', async () => {
256
+ const params: QuoteParams = {
257
+ txId: TEST_TX_ID,
258
+ provider: PROVIDER_DID,
259
+ consumer: CONSUMER_DID,
260
+ quotedAmount: '7500000',
261
+ originalAmount: '5000000',
262
+ maxPrice: '10000000',
263
+ chainId: 1, // Ethereum mainnet (not supported)
264
+ kernelAddress: TEST_KERNEL_ADDRESS
265
+ };
266
+
267
+ await expect(quoteBuilder.build(params)).rejects.toThrow('chainId must be 84532 (Base Sepolia) or 8453 (Base Mainnet)');
268
+ });
269
+ });
270
+
271
+ describe('verify()', () => {
272
+ let validQuote: QuoteMessage;
273
+
274
+ beforeEach(async () => {
275
+ const params: QuoteParams = {
276
+ txId: TEST_TX_ID,
277
+ provider: PROVIDER_DID,
278
+ consumer: CONSUMER_DID,
279
+ quotedAmount: '7500000',
280
+ originalAmount: '5000000',
281
+ maxPrice: '10000000',
282
+ chainId: TEST_CHAIN_ID,
283
+ kernelAddress: TEST_KERNEL_ADDRESS
284
+ };
285
+
286
+ validQuote = await quoteBuilder.build(params);
287
+ });
288
+
289
+ it('should verify valid quote', async () => {
290
+ const result = await quoteBuilder.verify(validQuote, TEST_KERNEL_ADDRESS);
291
+ expect(result).toBe(true);
292
+ });
293
+
294
+ it('should reject quote with tampered quotedAmount', async () => {
295
+ const tamperedQuote = { ...validQuote, quotedAmount: '99999999' };
296
+
297
+ await expect(quoteBuilder.verify(tamperedQuote, TEST_KERNEL_ADDRESS)).rejects.toThrow('Invalid signature');
298
+ });
299
+
300
+ it('should reject quote with invalid signature', async () => {
301
+ const invalidQuote = {
302
+ ...validQuote,
303
+ signature: '0x' + '0'.repeat(130)
304
+ };
305
+
306
+ await expect(quoteBuilder.verify(invalidQuote, TEST_KERNEL_ADDRESS)).rejects.toThrow('Signature verification failed');
307
+ });
308
+
309
+ it('should reject expired quote', async () => {
310
+ const expiredQuote = {
311
+ ...validQuote,
312
+ expiresAt: Math.floor(Date.now() / 1000) - 3600 // 1 hour ago
313
+ };
314
+
315
+ // Need to rebuild with new expiry for valid signature
316
+ const params: QuoteParams = {
317
+ txId: TEST_TX_ID,
318
+ provider: PROVIDER_DID,
319
+ consumer: CONSUMER_DID,
320
+ quotedAmount: '7500000',
321
+ originalAmount: '5000000',
322
+ maxPrice: '10000000',
323
+ expiresAt: expiredQuote.expiresAt,
324
+ chainId: TEST_CHAIN_ID,
325
+ kernelAddress: TEST_KERNEL_ADDRESS
326
+ };
327
+
328
+ // Mock Date.now to make quote appear valid at build time
329
+ const realDateNow = Date.now;
330
+ Date.now = jest.fn(() => (expiredQuote.expiresAt - 1800) * 1000);
331
+
332
+ const expiredButSignedQuote = await quoteBuilder.build(params);
333
+
334
+ // Restore Date.now
335
+ Date.now = realDateNow;
336
+
337
+ await expect(quoteBuilder.verify(expiredButSignedQuote, TEST_KERNEL_ADDRESS)).rejects.toThrow('Quote expired');
338
+ });
339
+ });
340
+
341
+ describe('computeHash()', () => {
342
+ it('should compute deterministic hash', async () => {
343
+ const params: QuoteParams = {
344
+ txId: TEST_TX_ID,
345
+ provider: PROVIDER_DID,
346
+ consumer: CONSUMER_DID,
347
+ quotedAmount: '7500000',
348
+ originalAmount: '5000000',
349
+ maxPrice: '10000000',
350
+ chainId: TEST_CHAIN_ID,
351
+ kernelAddress: TEST_KERNEL_ADDRESS
352
+ };
353
+
354
+ const quote = await quoteBuilder.build(params);
355
+ const hash1 = quoteBuilder.computeHash(quote);
356
+ const hash2 = quoteBuilder.computeHash(quote);
357
+
358
+ expect(hash1).toBe(hash2);
359
+ expect(hash1).toMatch(/^0x[a-fA-F0-9]{64}$/);
360
+ });
361
+
362
+ it('should produce different hashes for different quotes', async () => {
363
+ const params1: QuoteParams = {
364
+ txId: TEST_TX_ID,
365
+ provider: PROVIDER_DID,
366
+ consumer: CONSUMER_DID,
367
+ quotedAmount: '7500000',
368
+ originalAmount: '5000000',
369
+ maxPrice: '10000000',
370
+ chainId: TEST_CHAIN_ID,
371
+ kernelAddress: TEST_KERNEL_ADDRESS
372
+ };
373
+
374
+ const params2 = { ...params1, quotedAmount: '8000000' };
375
+
376
+ const quote1 = await quoteBuilder.build(params1);
377
+ const quote2 = await quoteBuilder.build(params2);
378
+
379
+ const hash1 = quoteBuilder.computeHash(quote1);
380
+ const hash2 = quoteBuilder.computeHash(quote2);
381
+
382
+ expect(hash1).not.toBe(hash2);
383
+ });
384
+ });
385
+
386
+ describe('uploadToIPFS()', () => {
387
+ it('should upload quote to IPFS and return CID', async () => {
388
+ const params: QuoteParams = {
389
+ txId: TEST_TX_ID,
390
+ provider: PROVIDER_DID,
391
+ consumer: CONSUMER_DID,
392
+ quotedAmount: '7500000',
393
+ originalAmount: '5000000',
394
+ maxPrice: '10000000',
395
+ chainId: TEST_CHAIN_ID,
396
+ kernelAddress: TEST_KERNEL_ADDRESS
397
+ };
398
+
399
+ const quote = await quoteBuilder.build(params);
400
+ const cid = await quoteBuilder.uploadToIPFS(quote);
401
+
402
+ expect(cid).toBe('bafybeigtest123');
403
+ expect(mockIPFS.add).toHaveBeenCalledWith(JSON.stringify(quote));
404
+ expect(mockIPFS.pin).toHaveBeenCalledWith(cid);
405
+ });
406
+
407
+ it('should throw error if IPFS client not configured', async () => {
408
+ const builderWithoutIPFS = new QuoteBuilder(signer, nonceManager);
409
+
410
+ const params: QuoteParams = {
411
+ txId: TEST_TX_ID,
412
+ provider: PROVIDER_DID,
413
+ consumer: CONSUMER_DID,
414
+ quotedAmount: '7500000',
415
+ originalAmount: '5000000',
416
+ maxPrice: '10000000',
417
+ chainId: TEST_CHAIN_ID,
418
+ kernelAddress: TEST_KERNEL_ADDRESS
419
+ };
420
+
421
+ const quote = await builderWithoutIPFS.build(params);
422
+
423
+ await expect(builderWithoutIPFS.uploadToIPFS(quote)).rejects.toThrow('IPFS client not configured');
424
+ });
425
+ });
426
+
427
+ describe('Edge cases (AIP-2 §9.2)', () => {
428
+ it('should accept quote at maxPrice boundary', async () => {
429
+ const params: QuoteParams = {
430
+ txId: TEST_TX_ID,
431
+ provider: PROVIDER_DID,
432
+ consumer: CONSUMER_DID,
433
+ quotedAmount: '10000000', // Exactly at maxPrice
434
+ originalAmount: '5000000',
435
+ maxPrice: '10000000',
436
+ chainId: TEST_CHAIN_ID,
437
+ kernelAddress: TEST_KERNEL_ADDRESS
438
+ };
439
+
440
+ const quote = await quoteBuilder.build(params);
441
+ expect(quote.quotedAmount).toBe('10000000');
442
+
443
+ const result = await quoteBuilder.verify(quote, TEST_KERNEL_ADDRESS);
444
+ expect(result).toBe(true);
445
+ });
446
+
447
+ it('should accept quote at platform minimum boundary ($0.05)', async () => {
448
+ const params: QuoteParams = {
449
+ txId: TEST_TX_ID,
450
+ provider: PROVIDER_DID,
451
+ consumer: CONSUMER_DID,
452
+ quotedAmount: '50000', // Exactly $0.05
453
+ originalAmount: '50000',
454
+ maxPrice: '100000',
455
+ chainId: TEST_CHAIN_ID,
456
+ kernelAddress: TEST_KERNEL_ADDRESS
457
+ };
458
+
459
+ const quote = await quoteBuilder.build(params);
460
+ const result = await quoteBuilder.verify(quote, TEST_KERNEL_ADDRESS);
461
+ expect(result).toBe(true);
462
+ });
463
+
464
+ it('should accept quote expiring exactly at 24-hour limit', async () => {
465
+ const exactExpiry = Math.floor(Date.now() / 1000) + 86400;
466
+
467
+ const params: QuoteParams = {
468
+ txId: TEST_TX_ID,
469
+ provider: PROVIDER_DID,
470
+ consumer: CONSUMER_DID,
471
+ quotedAmount: '7500000',
472
+ originalAmount: '5000000',
473
+ maxPrice: '10000000',
474
+ expiresAt: exactExpiry,
475
+ chainId: TEST_CHAIN_ID,
476
+ kernelAddress: TEST_KERNEL_ADDRESS
477
+ };
478
+
479
+ const quote = await quoteBuilder.build(params);
480
+ expect(quote.expiresAt).toBe(exactExpiry);
481
+ });
482
+
483
+ it('should handle quote with empty justification', async () => {
484
+ const params: QuoteParams = {
485
+ txId: TEST_TX_ID,
486
+ provider: PROVIDER_DID,
487
+ consumer: CONSUMER_DID,
488
+ quotedAmount: '7500000',
489
+ originalAmount: '5000000',
490
+ maxPrice: '10000000',
491
+ justification: {},
492
+ chainId: TEST_CHAIN_ID,
493
+ kernelAddress: TEST_KERNEL_ADDRESS
494
+ };
495
+
496
+ const quote = await quoteBuilder.build(params);
497
+ expect(quote.justification).toEqual({});
498
+ });
499
+
500
+ it('should handle quote with undefined justification', async () => {
501
+ const params: QuoteParams = {
502
+ txId: TEST_TX_ID,
503
+ provider: PROVIDER_DID,
504
+ consumer: CONSUMER_DID,
505
+ quotedAmount: '7500000',
506
+ originalAmount: '5000000',
507
+ maxPrice: '10000000',
508
+ chainId: TEST_CHAIN_ID,
509
+ kernelAddress: TEST_KERNEL_ADDRESS
510
+ };
511
+
512
+ const quote = await quoteBuilder.build(params);
513
+ expect(quote.justification).toBeUndefined();
514
+ });
515
+ });
516
+ });
@@ -0,0 +1,82 @@
1
+ import { State, StateMachine } from '../types/state';
2
+
3
+ describe('StateMachine', () => {
4
+ describe('isValidTransition', () => {
5
+ it('should allow INITIATED → QUOTED', () => {
6
+ expect(StateMachine.isValidTransition(State.INITIATED, State.QUOTED)).toBe(true);
7
+ });
8
+
9
+ it('should allow INITIATED → CANCELLED', () => {
10
+ expect(StateMachine.isValidTransition(State.INITIATED, State.CANCELLED)).toBe(true);
11
+ });
12
+
13
+ it('should reject INITIATED → SETTLED', () => {
14
+ expect(StateMachine.isValidTransition(State.INITIATED, State.SETTLED)).toBe(false);
15
+ });
16
+
17
+ it('should allow DELIVERED → SETTLED', () => {
18
+ expect(StateMachine.isValidTransition(State.DELIVERED, State.SETTLED)).toBe(true);
19
+ });
20
+
21
+ it('should allow DELIVERED → DISPUTED', () => {
22
+ expect(StateMachine.isValidTransition(State.DELIVERED, State.DISPUTED)).toBe(true);
23
+ });
24
+
25
+ it('should reject SETTLED → any state (terminal)', () => {
26
+ expect(StateMachine.isValidTransition(State.SETTLED, State.INITIATED)).toBe(false);
27
+ expect(StateMachine.isValidTransition(State.SETTLED, State.DISPUTED)).toBe(false);
28
+ });
29
+ });
30
+
31
+ describe('isTerminalState', () => {
32
+ it('should identify SETTLED as terminal', () => {
33
+ expect(StateMachine.isTerminalState(State.SETTLED)).toBe(true);
34
+ });
35
+
36
+ it('should identify CANCELLED as terminal', () => {
37
+ expect(StateMachine.isTerminalState(State.CANCELLED)).toBe(true);
38
+ });
39
+
40
+ it('should identify IN_PROGRESS as not terminal', () => {
41
+ expect(StateMachine.isTerminalState(State.IN_PROGRESS)).toBe(false);
42
+ });
43
+ });
44
+
45
+ describe('getStateName', () => {
46
+ it('should return correct state names', () => {
47
+ expect(StateMachine.getStateName(State.INITIATED)).toBe('INITIATED');
48
+ expect(StateMachine.getStateName(State.SETTLED)).toBe('SETTLED');
49
+ });
50
+ });
51
+
52
+ describe('getNextValidStates', () => {
53
+ it('should return valid next states for INITIATED', () => {
54
+ const next = StateMachine.getNextValidStates(State.INITIATED);
55
+ expect(next).toContain(State.QUOTED);
56
+ expect(next).toContain(State.COMMITTED);
57
+ expect(next).toContain(State.CANCELLED);
58
+ expect(next).toHaveLength(3); // INITIATED can transition to QUOTED, COMMITTED, or CANCELLED
59
+ });
60
+
61
+ it('should return empty array for terminal states', () => {
62
+ expect(StateMachine.getNextValidStates(State.SETTLED)).toEqual([]);
63
+ expect(StateMachine.getNextValidStates(State.CANCELLED)).toEqual([]);
64
+ });
65
+ });
66
+
67
+ describe('validateTransition', () => {
68
+ it('should not throw for valid transitions', () => {
69
+ expect(() => {
70
+ StateMachine.validateTransition(State.INITIATED, State.QUOTED);
71
+ }).not.toThrow();
72
+ });
73
+
74
+ it('should throw for invalid transitions', () => {
75
+ expect(() => {
76
+ StateMachine.validateTransition(State.INITIATED, State.SETTLED);
77
+ }).toThrow('Invalid state transition');
78
+ });
79
+ });
80
+ });
81
+
82
+