@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,376 @@
1
+ /**
2
+ * Validation Security Test Suite
3
+ *
4
+ * CRITICAL: Input validation is the first line of defense against attacks
5
+ * Coverage Target: 100% (statements, functions, lines, branches)
6
+ *
7
+ * Security Test Categories:
8
+ * 1. Address Validation (8 tests)
9
+ * 2. Amount Validation (7 tests)
10
+ * 3. Deadline Validation (5 tests)
11
+ * 4. Dispute Window Validation (6 tests)
12
+ * 5. Transaction ID Validation (6 tests)
13
+ *
14
+ * References:
15
+ * - Security Analysis: /Testnet/tests/SDK_SECURITY_ANALYSIS-Ultra-Think.md
16
+ * - V3: Input Validation - Zero Address Bypass vulnerability
17
+ */
18
+
19
+ import {
20
+ validateAddress,
21
+ validateAmount,
22
+ validateDeadline,
23
+ validateDisputeWindow,
24
+ validateTxId
25
+ } from '../../utils/validation';
26
+
27
+ describe('Validation - Security Tests', () => {
28
+ describe('validateAddress - Address Security', () => {
29
+ it('should accept valid Ethereum address (lowercase)', () => {
30
+ const validAddress = '0x742d35cc6634c0532925a3b844bc9e7595f0beb0';
31
+ expect(() => validateAddress(validAddress)).not.toThrow();
32
+ });
33
+
34
+ it('should accept checksummed address', () => {
35
+ const checksummedAddress = '0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed';
36
+ expect(() => validateAddress(checksummedAddress)).not.toThrow();
37
+ });
38
+
39
+ it('should accept all lowercase address', () => {
40
+ const lowercaseAddress = '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
41
+ expect(() => validateAddress(lowercaseAddress)).not.toThrow();
42
+ });
43
+
44
+ it('should reject zero address (0x0000...)', () => {
45
+ const zeroAddress = '0x0000000000000000000000000000000000000000';
46
+ expect(() => validateAddress(zeroAddress)).toThrow('zero address');
47
+ });
48
+
49
+ it('should reject invalid address format', () => {
50
+ const invalidAddress = 'not-an-address';
51
+ expect(() => validateAddress(invalidAddress)).toThrow('Invalid Ethereum address');
52
+ });
53
+
54
+ it('should reject address with invalid length', () => {
55
+ const shortAddress = '0x742d35cc6634c053';
56
+ expect(() => validateAddress(shortAddress)).toThrow('Invalid Ethereum address');
57
+ });
58
+
59
+ it('should reject non-hex characters in address', () => {
60
+ const invalidHex = '0x742d35cc6634c0532925a3b844bc9e7595f0bebZ';
61
+ expect(() => validateAddress(invalidHex)).toThrow('Invalid Ethereum address');
62
+ });
63
+
64
+ it('should reject empty string', () => {
65
+ expect(() => validateAddress('')).toThrow('Invalid Ethereum address');
66
+ });
67
+
68
+ it('should reject null/undefined', () => {
69
+ expect(() => validateAddress(null as any)).toThrow('Invalid Ethereum address');
70
+ expect(() => validateAddress(undefined as any)).toThrow('Invalid Ethereum address');
71
+ });
72
+
73
+ it('should include address in error message', () => {
74
+ try {
75
+ validateAddress('invalid-addr', 'providerAddress');
76
+ fail('Should have thrown error');
77
+ } catch (error: any) {
78
+ expect(error.message).toContain('Invalid Ethereum address');
79
+ expect(error.message).toContain('invalid-addr');
80
+ }
81
+ });
82
+
83
+ it('should always use "address" as field name', () => {
84
+ try {
85
+ validateAddress('invalid');
86
+ fail('Should have thrown error');
87
+ } catch (error: any) {
88
+ expect(error.details.field).toBe('address');
89
+ }
90
+ });
91
+ });
92
+
93
+ describe('validateAmount - Amount Security', () => {
94
+ it('should accept positive amount', () => {
95
+ const amount = BigInt('100000000');
96
+ expect(() => validateAmount(amount)).not.toThrow();
97
+ });
98
+
99
+ it('should accept minimum amount (1 wei)', () => {
100
+ const minAmount = BigInt(1);
101
+ expect(() => validateAmount(minAmount)).not.toThrow();
102
+ });
103
+
104
+ it('should accept maximum uint256 amount', () => {
105
+ const maxAmount = BigInt('0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff');
106
+ expect(() => validateAmount(maxAmount)).not.toThrow();
107
+ });
108
+
109
+ it('should reject zero amount', () => {
110
+ const zeroAmount = BigInt(0);
111
+ expect(() => validateAmount(zeroAmount)).toThrow('must be > 0');
112
+ });
113
+
114
+ it('should reject negative amount', () => {
115
+ const negativeAmount = BigInt(-1);
116
+ expect(() => validateAmount(negativeAmount)).toThrow('Invalid amount');
117
+ });
118
+
119
+ it('should reject null/undefined', () => {
120
+ expect(() => validateAmount(null as any)).toThrow('Invalid amount');
121
+ expect(() => validateAmount(undefined as any)).toThrow('Invalid amount');
122
+ });
123
+
124
+ it('should include amount value in error message', () => {
125
+ try {
126
+ validateAmount(BigInt(0));
127
+ fail('Should have thrown error');
128
+ } catch (error: any) {
129
+ expect(error.message).toContain('0');
130
+ }
131
+ });
132
+
133
+ it('should always use "amount" as field name', () => {
134
+ try {
135
+ validateAmount(BigInt(0), 'escrowAmount');
136
+ fail('Should have thrown error');
137
+ } catch (error: any) {
138
+ expect(error.details.field).toBe('amount');
139
+ }
140
+ });
141
+ });
142
+
143
+ describe('validateDeadline - Deadline Security', () => {
144
+ it('should accept future deadline', () => {
145
+ const futureDeadline = Math.floor(Date.now() / 1000) + 3600; // 1 hour from now
146
+ expect(() => validateDeadline(futureDeadline)).not.toThrow();
147
+ });
148
+
149
+ it('should accept deadline far in the future', () => {
150
+ const farFuture = Math.floor(Date.now() / 1000) + (365 * 24 * 60 * 60); // 1 year from now
151
+ expect(() => validateDeadline(farFuture)).not.toThrow();
152
+ });
153
+
154
+ it('should reject past deadline', () => {
155
+ const pastDeadline = Math.floor(Date.now() / 1000) - 3600; // 1 hour ago
156
+ expect(() => validateDeadline(pastDeadline)).toThrow('must be in the future');
157
+ });
158
+
159
+ it('should reject current timestamp (not in future)', () => {
160
+ const now = Math.floor(Date.now() / 1000);
161
+ expect(() => validateDeadline(now)).toThrow('must be in the future');
162
+ });
163
+
164
+ it('should include current time and deadline in error message', () => {
165
+ const pastDeadline = Math.floor(Date.now() / 1000) - 3600;
166
+ try {
167
+ validateDeadline(pastDeadline);
168
+ fail('Should have thrown error');
169
+ } catch (error: any) {
170
+ expect(error.message).toContain('now:');
171
+ expect(error.message).toContain('deadline:');
172
+ }
173
+ });
174
+
175
+ it('should use field name parameter', () => {
176
+ const past = Math.floor(Date.now() / 1000) - 1;
177
+ expect(() => validateDeadline(past, 'transactionDeadline')).toThrow('must be in the future');
178
+ });
179
+
180
+ it('should use default field name when not provided', () => {
181
+ const past = Math.floor(Date.now() / 1000) - 1;
182
+ try {
183
+ validateDeadline(past);
184
+ fail('Should have thrown error');
185
+ } catch (error: any) {
186
+ expect(error.code).toBe('VALIDATION_ERROR');
187
+ }
188
+ });
189
+ });
190
+
191
+ describe('validateDisputeWindow - Dispute Window Security', () => {
192
+ const MAX_DISPUTE_WINDOW = 30 * 24 * 60 * 60; // 30 days in seconds
193
+
194
+ it('should accept valid dispute window (2 hours)', () => {
195
+ const twoHours = 2 * 60 * 60;
196
+ expect(() => validateDisputeWindow(twoHours)).not.toThrow();
197
+ });
198
+
199
+ it('should accept minimum dispute window (0 seconds)', () => {
200
+ expect(() => validateDisputeWindow(0)).not.toThrow();
201
+ });
202
+
203
+ it('should accept maximum dispute window (30 days)', () => {
204
+ expect(() => validateDisputeWindow(MAX_DISPUTE_WINDOW)).not.toThrow();
205
+ });
206
+
207
+ it('should accept typical 2-day dispute window', () => {
208
+ const twoDays = 2 * 24 * 60 * 60;
209
+ expect(() => validateDisputeWindow(twoDays)).not.toThrow();
210
+ });
211
+
212
+ it('should reject negative dispute window', () => {
213
+ expect(() => validateDisputeWindow(-1)).toThrow('cannot be negative');
214
+ });
215
+
216
+ it('should reject dispute window exceeding 30 days', () => {
217
+ const thirtyOneDays = 31 * 24 * 60 * 60;
218
+ expect(() => validateDisputeWindow(thirtyOneDays)).toThrow('exceeds maximum');
219
+ });
220
+
221
+ it('should include maximum allowed value in error message', () => {
222
+ const excessive = MAX_DISPUTE_WINDOW + 1;
223
+ try {
224
+ validateDisputeWindow(excessive);
225
+ fail('Should have thrown error');
226
+ } catch (error: any) {
227
+ expect(error.message).toContain('30 days');
228
+ }
229
+ });
230
+
231
+ it('should use field name parameter', () => {
232
+ expect(() => validateDisputeWindow(-1, 'windowDuration')).toThrow('cannot be negative');
233
+ });
234
+
235
+ it('should use default field name when not provided', () => {
236
+ try {
237
+ validateDisputeWindow(-1);
238
+ fail('Should have thrown error');
239
+ } catch (error: any) {
240
+ expect(error.details.field).toBe('disputeWindow');
241
+ }
242
+ });
243
+ });
244
+
245
+ describe('validateTxId - Transaction ID Security', () => {
246
+ it('should accept valid bytes32 transaction ID', () => {
247
+ const validTxId = '0x' + '1'.repeat(64);
248
+ expect(() => validateTxId(validTxId)).not.toThrow();
249
+ });
250
+
251
+ it('should accept transaction ID with mixed hex characters', () => {
252
+ const mixedTxId = '0x' + 'a1b2c3d4e5f6'.repeat(5) + '1234';
253
+ expect(() => validateTxId(mixedTxId)).not.toThrow();
254
+ });
255
+
256
+ it('should accept uppercase hex characters', () => {
257
+ const uppercaseTxId = '0x' + 'A'.repeat(64);
258
+ expect(() => validateTxId(uppercaseTxId)).not.toThrow();
259
+ });
260
+
261
+ it('should reject invalid format (not bytes32)', () => {
262
+ const invalidTxId = 'invalid-tx-id';
263
+ expect(() => validateTxId(invalidTxId)).toThrow('Invalid transaction ID format');
264
+ });
265
+
266
+ it('should reject transaction ID with wrong length', () => {
267
+ const shortTxId = '0x' + '1'.repeat(32); // Only 32 hex chars instead of 64
268
+ expect(() => validateTxId(shortTxId)).toThrow('Invalid transaction ID format');
269
+ });
270
+
271
+ it('should reject transaction ID without 0x prefix', () => {
272
+ const noPrefixTxId = '1'.repeat(64);
273
+ expect(() => validateTxId(noPrefixTxId)).toThrow('Invalid transaction ID format');
274
+ });
275
+
276
+ it('should reject transaction ID with invalid hex characters', () => {
277
+ const invalidHex = '0x' + '1'.repeat(63) + 'Z';
278
+ expect(() => validateTxId(invalidHex)).toThrow('Invalid transaction ID format');
279
+ });
280
+
281
+ it('should reject empty string', () => {
282
+ expect(() => validateTxId('')).toThrow('Invalid transaction ID format');
283
+ });
284
+
285
+ it('should reject null/undefined', () => {
286
+ expect(() => validateTxId(null as any)).toThrow('Invalid transaction ID format');
287
+ expect(() => validateTxId(undefined as any)).toThrow('Invalid transaction ID format');
288
+ });
289
+
290
+ it('should include expected format in error message', () => {
291
+ try {
292
+ validateTxId('invalid');
293
+ fail('Should have thrown error');
294
+ } catch (error: any) {
295
+ expect(error.message).toContain('bytes32');
296
+ }
297
+ });
298
+
299
+ it('should use field name parameter', () => {
300
+ expect(() => validateTxId('invalid', 'transactionId')).toThrow('Invalid transaction ID format');
301
+ });
302
+
303
+ it('should use default field name when not provided', () => {
304
+ try {
305
+ validateTxId('invalid');
306
+ fail('Should have thrown error');
307
+ } catch (error: any) {
308
+ expect(error.details.field).toBe('txId');
309
+ }
310
+ });
311
+ });
312
+
313
+ describe('Edge Cases - Cross-Function Validation', () => {
314
+ it('should handle all validation functions with custom field names', () => {
315
+ const validAddress = '0x742d35cc6634c0532925a3b844bc9e7595f0beb0';
316
+ const validAmount = BigInt('100000000');
317
+ const validDeadline = Math.floor(Date.now() / 1000) + 3600;
318
+ const validDisputeWindow = 7200;
319
+ const validTxId = '0x' + '1'.repeat(64);
320
+
321
+ expect(() => validateAddress(validAddress, 'customAddress')).not.toThrow();
322
+ expect(() => validateAmount(validAmount, 'customAmount')).not.toThrow();
323
+ expect(() => validateDeadline(validDeadline, 'customDeadline')).not.toThrow();
324
+ expect(() => validateDisputeWindow(validDisputeWindow, 'customWindow')).not.toThrow();
325
+ expect(() => validateTxId(validTxId, 'customTxId')).not.toThrow();
326
+ });
327
+
328
+ it('should throw appropriate error types for each validator', () => {
329
+ expect(() => validateAddress('invalid')).toThrow('Invalid Ethereum address');
330
+ expect(() => validateAmount(BigInt(0))).toThrow('Invalid amount');
331
+ expect(() => validateDeadline(0)).toThrow('must be in the future');
332
+ expect(() => validateDisputeWindow(-1)).toThrow('cannot be negative');
333
+ expect(() => validateTxId('invalid')).toThrow('Invalid transaction ID format');
334
+ });
335
+
336
+ it('should validate boundary conditions across all validators', () => {
337
+ // Address: zero address is the boundary
338
+ expect(() => validateAddress('0x0000000000000000000000000000000000000000')).toThrow();
339
+
340
+ // Amount: 0 is the boundary (must be > 0)
341
+ expect(() => validateAmount(BigInt(0))).toThrow();
342
+ expect(() => validateAmount(BigInt(1))).not.toThrow();
343
+
344
+ // Deadline: now is the boundary (must be in future)
345
+ const now = Math.floor(Date.now() / 1000);
346
+ expect(() => validateDeadline(now)).toThrow();
347
+ expect(() => validateDeadline(now + 1)).not.toThrow();
348
+
349
+ // Dispute window: 0 and 30 days are boundaries
350
+ expect(() => validateDisputeWindow(-1)).toThrow();
351
+ expect(() => validateDisputeWindow(0)).not.toThrow();
352
+ expect(() => validateDisputeWindow(30 * 24 * 60 * 60)).not.toThrow();
353
+ expect(() => validateDisputeWindow(30 * 24 * 60 * 60 + 1)).toThrow();
354
+ });
355
+
356
+ it('should preserve error details across all validators', () => {
357
+ const testCases = [
358
+ { fn: () => validateAddress('invalid', 'testAddress'), field: 'address' }, // InvalidAddressError hardcodes field
359
+ { fn: () => validateAmount(BigInt(0), 'testAmount'), field: 'amount' }, // InvalidAmountError hardcodes field
360
+ { fn: () => validateDeadline(0, 'testDeadline'), field: 'testDeadline' },
361
+ { fn: () => validateDisputeWindow(-1, 'testWindow'), field: 'testWindow' },
362
+ { fn: () => validateTxId('invalid', 'testTxId'), field: 'testTxId' }
363
+ ];
364
+
365
+ testCases.forEach(({ fn, field }) => {
366
+ try {
367
+ fn();
368
+ fail('Should have thrown error');
369
+ } catch (error: any) {
370
+ expect(error.details).toBeDefined();
371
+ expect(error.details.field).toBe(field);
372
+ }
373
+ });
374
+ });
375
+ });
376
+ });
@@ -0,0 +1,262 @@
1
+ /**
2
+ * IPFSClient Test Suite
3
+ *
4
+ * Coverage Target: 80%+ (statements, functions, lines, branches)
5
+ *
6
+ * Test Categories:
7
+ * 1. Upload Operations (3 tests)
8
+ * 2. Pin Operations (2 tests)
9
+ * 3. Retrieval Operations (2 tests)
10
+ * 4. Connection Tests (2 tests)
11
+ * 5. Factory Function (1 test)
12
+ *
13
+ * References:
14
+ * - IPFSClient.ts implementation
15
+ * - ipfs-http-client library
16
+ */
17
+
18
+ // Mock ipfs-http-client before importing IPFSClient
19
+ const mockIPFSClient = {
20
+ add: jest.fn(),
21
+ pin: {
22
+ add: jest.fn()
23
+ },
24
+ cat: jest.fn(),
25
+ id: jest.fn()
26
+ };
27
+
28
+ const mockCreate = jest.fn(() => mockIPFSClient);
29
+
30
+ jest.mock('kubo-rpc-client', () => ({
31
+ create: mockCreate,
32
+ IPFSHTTPClient: jest.fn(),
33
+ Options: jest.fn()
34
+ }));
35
+
36
+ import { IPFSHTTPClientImpl, createIPFSClient } from '../../utils/IPFSClient';
37
+
38
+ describe('IPFSClient - Upload Operations', () => {
39
+ let ipfsClient: IPFSHTTPClientImpl;
40
+
41
+ beforeEach(() => {
42
+ jest.clearAllMocks();
43
+ ipfsClient = new IPFSHTTPClientImpl({ url: 'http://localhost:5001' });
44
+ });
45
+
46
+ it('should upload string data successfully', async () => {
47
+ const testData = JSON.stringify({ result: 'success', output: 'test' });
48
+ const mockCID = 'bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi';
49
+
50
+ mockIPFSClient.add.mockResolvedValue({
51
+ cid: {
52
+ toString: () => mockCID
53
+ }
54
+ });
55
+
56
+ const result = await ipfsClient.add(testData);
57
+
58
+ expect(mockIPFSClient.add).toHaveBeenCalledWith(
59
+ Buffer.from(testData, 'utf-8'),
60
+ expect.objectContaining({
61
+ cidVersion: 1,
62
+ hashAlg: 'sha2-256',
63
+ pin: true
64
+ })
65
+ );
66
+
67
+ expect(result).toBe(mockCID);
68
+ });
69
+
70
+ it('should upload buffer data successfully', async () => {
71
+ const testBuffer = Buffer.from('test binary data');
72
+ const mockCID = 'bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi';
73
+
74
+ mockIPFSClient.add.mockResolvedValue({
75
+ cid: {
76
+ toString: () => mockCID
77
+ }
78
+ });
79
+
80
+ const result = await ipfsClient.add(testBuffer);
81
+
82
+ expect(mockIPFSClient.add).toHaveBeenCalledWith(
83
+ testBuffer,
84
+ expect.objectContaining({
85
+ cidVersion: 1,
86
+ hashAlg: 'sha2-256',
87
+ pin: true
88
+ })
89
+ );
90
+
91
+ expect(result).toBe(mockCID);
92
+ });
93
+
94
+ it('should handle upload errors', async () => {
95
+ const testData = 'test data';
96
+
97
+ mockIPFSClient.add.mockRejectedValue(new Error('Network timeout'));
98
+
99
+ await expect(ipfsClient.add(testData)).rejects.toThrow('IPFS upload failed: Network timeout');
100
+ });
101
+ });
102
+
103
+ describe('IPFSClient - Pin Operations', () => {
104
+ let ipfsClient: IPFSHTTPClientImpl;
105
+
106
+ beforeEach(() => {
107
+ jest.clearAllMocks();
108
+ ipfsClient = new IPFSHTTPClientImpl({ url: 'http://localhost:5001' });
109
+ });
110
+
111
+ it('should pin content successfully', async () => {
112
+ const testCID = 'bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi';
113
+
114
+ mockIPFSClient.pin.add.mockResolvedValue(undefined);
115
+
116
+ await ipfsClient.pin(testCID);
117
+
118
+ expect(mockIPFSClient.pin.add).toHaveBeenCalledWith(testCID);
119
+ });
120
+
121
+ it('should handle pin errors', async () => {
122
+ const testCID = 'bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi';
123
+
124
+ mockIPFSClient.pin.add.mockRejectedValue(new Error('Pin quota exceeded'));
125
+
126
+ await expect(ipfsClient.pin(testCID)).rejects.toThrow('IPFS pin failed: Pin quota exceeded');
127
+ });
128
+ });
129
+
130
+ describe('IPFSClient - Retrieval Operations', () => {
131
+ let ipfsClient: IPFSHTTPClientImpl;
132
+
133
+ beforeEach(() => {
134
+ jest.clearAllMocks();
135
+ ipfsClient = new IPFSHTTPClientImpl({ url: 'http://localhost:5001' });
136
+ });
137
+
138
+ it('should retrieve content successfully', async () => {
139
+ const testCID = 'bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi';
140
+ const testContent = JSON.stringify({ result: 'success' });
141
+
142
+ // Mock async iterator
143
+ async function* mockAsyncIterator() {
144
+ yield new Uint8Array(Buffer.from(testContent));
145
+ }
146
+
147
+ mockIPFSClient.cat.mockReturnValue(mockAsyncIterator());
148
+
149
+ const result = await ipfsClient.get(testCID);
150
+
151
+ expect(mockIPFSClient.cat).toHaveBeenCalledWith(testCID);
152
+ expect(result).toBe(testContent);
153
+ });
154
+
155
+ it('should handle retrieval errors', async () => {
156
+ const testCID = 'bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi';
157
+
158
+ // Mock async iterator that throws
159
+ async function* mockErrorIterator() {
160
+ throw new Error('Content not found');
161
+ }
162
+
163
+ mockIPFSClient.cat.mockReturnValue(mockErrorIterator());
164
+
165
+ await expect(ipfsClient.get(testCID)).rejects.toThrow('IPFS retrieval failed: Content not found');
166
+ });
167
+
168
+ it('should concatenate multiple chunks correctly', async () => {
169
+ const testCID = 'bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi';
170
+ const chunk1 = 'Hello ';
171
+ const chunk2 = 'World!';
172
+
173
+ // Mock async iterator with multiple chunks
174
+ async function* mockChunkedIterator() {
175
+ yield new Uint8Array(Buffer.from(chunk1));
176
+ yield new Uint8Array(Buffer.from(chunk2));
177
+ }
178
+
179
+ mockIPFSClient.cat.mockReturnValue(mockChunkedIterator());
180
+
181
+ const result = await ipfsClient.get(testCID);
182
+
183
+ expect(result).toBe('Hello World!');
184
+ });
185
+ });
186
+
187
+ describe('IPFSClient - Connection Tests', () => {
188
+ let ipfsClient: IPFSHTTPClientImpl;
189
+
190
+ beforeEach(() => {
191
+ jest.clearAllMocks();
192
+ ipfsClient = new IPFSHTTPClientImpl({ url: 'http://localhost:5001' });
193
+ });
194
+
195
+ it('should check if daemon is online', async () => {
196
+ mockIPFSClient.id.mockResolvedValue({
197
+ id: 'QmTestNodeId123',
198
+ publicKey: 'mockPublicKey'
199
+ });
200
+
201
+ const isOnline = await ipfsClient.isOnline();
202
+
203
+ expect(isOnline).toBe(true);
204
+ expect(mockIPFSClient.id).toHaveBeenCalled();
205
+ });
206
+
207
+ it('should return false when daemon is offline', async () => {
208
+ mockIPFSClient.id.mockRejectedValue(new Error('Connection refused'));
209
+
210
+ const isOnline = await ipfsClient.isOnline();
211
+
212
+ expect(isOnline).toBe(false);
213
+ });
214
+
215
+ it('should get node ID successfully', async () => {
216
+ const mockNodeId = 'QmTestNodeId123456789';
217
+
218
+ mockIPFSClient.id.mockResolvedValue({
219
+ id: {
220
+ toString: () => mockNodeId
221
+ }
222
+ });
223
+
224
+ const nodeId = await ipfsClient.getNodeId();
225
+
226
+ expect(nodeId).toBe(mockNodeId);
227
+ });
228
+
229
+ it('should handle get node ID errors', async () => {
230
+ mockIPFSClient.id.mockRejectedValue(new Error('Daemon not running'));
231
+
232
+ await expect(ipfsClient.getNodeId()).rejects.toThrow('Failed to get node ID: Daemon not running');
233
+ });
234
+ });
235
+
236
+ describe('IPFSClient - Factory Function', () => {
237
+ beforeEach(() => {
238
+ jest.clearAllMocks();
239
+ // Clear environment variables
240
+ delete process.env.INFURA_PROJECT_ID;
241
+ delete process.env.INFURA_PROJECT_SECRET;
242
+ delete process.env.PINATA_API_KEY;
243
+ delete process.env.PINATA_SECRET_API_KEY;
244
+ delete process.env.IPFS_URL;
245
+ });
246
+
247
+ it('should create client with default config', () => {
248
+ const client = createIPFSClient();
249
+
250
+ expect(client).toBeDefined();
251
+ expect(client).toBeInstanceOf(IPFSHTTPClientImpl);
252
+ });
253
+
254
+ it('should create client with custom IPFS_URL', () => {
255
+ process.env.IPFS_URL = 'https://custom-ipfs.example.com';
256
+
257
+ const client = createIPFSClient();
258
+
259
+ expect(client).toBeDefined();
260
+ expect(client).toBeInstanceOf(IPFSHTTPClientImpl);
261
+ });
262
+ });