@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,1167 @@
1
+ /**
2
+ * ACTPKernel Security Test Suite
3
+ *
4
+ * CRITICAL: This module manages transaction state machine and escrow lifecycle
5
+ * Coverage Target: 90%+ (statements, functions, lines), 85%+ (branches)
6
+ *
7
+ * Security Test Categories:
8
+ * 1. State Transition Security (15 tests)
9
+ * 2. Access Control Enforcement (10 tests)
10
+ *
11
+ * References:
12
+ * - Security Analysis: /Testnet/tests/SDK_SECURITY_ANALYSIS-Ultra-Think.md
13
+ * - V2: ACTPKernel State Transition TOCTOU vulnerability
14
+ */
15
+
16
+ import { ACTPKernel } from '../../protocol/ACTPKernel';
17
+ import { State } from '../../types';
18
+ import { AbiCoder } from 'ethers';
19
+
20
+ // Mock ethers Contract
21
+ const mockContract = {
22
+ estimateGas: {
23
+ createTransaction: jest.fn().mockResolvedValue(BigInt(85000)),
24
+ transitionState: jest.fn().mockResolvedValue(BigInt(50000)),
25
+ linkEscrow: jest.fn().mockResolvedValue(BigInt(45000)),
26
+ releaseEscrow: jest.fn().mockResolvedValue(BigInt(50000)),
27
+ releaseMilestone: jest.fn().mockResolvedValue(BigInt(45000))
28
+ },
29
+ createTransaction: jest.fn().mockResolvedValue({
30
+ wait: jest.fn().mockResolvedValue({
31
+ transactionHash: '0x' + '5'.repeat(64),
32
+ logs: [{
33
+ topics: [
34
+ '0x' + '6'.repeat(64), // Event signature hash
35
+ '0x' + '1'.repeat(64) // Transaction ID (indexed param)
36
+ ],
37
+ data: '0x',
38
+ address: '0x' + 'a'.repeat(40)
39
+ }]
40
+ })
41
+ }),
42
+ transitionState: jest.fn().mockResolvedValue({
43
+ wait: jest.fn().mockResolvedValue({})
44
+ }),
45
+ linkEscrow: jest.fn().mockResolvedValue({
46
+ wait: jest.fn().mockResolvedValue({})
47
+ }),
48
+ releaseEscrow: jest.fn().mockResolvedValue({
49
+ wait: jest.fn().mockResolvedValue({})
50
+ }),
51
+ releaseMilestone: jest.fn().mockResolvedValue({
52
+ wait: jest.fn().mockResolvedValue({})
53
+ }),
54
+ transactions: jest.fn().mockResolvedValue({
55
+ transactionId: '0x' + '1'.repeat(64),
56
+ requester: '0x' + 'c'.repeat(40), // REQUESTER_ADDRESS
57
+ provider: '0x' + 'b'.repeat(40), // PROVIDER_ADDRESS
58
+ amount: BigInt('100000000'),
59
+ state: State.INITIATED,
60
+ createdAt: BigInt(Math.floor(Date.now() / 1000)),
61
+ deadline: BigInt(Math.floor(Date.now() / 1000) + 86400),
62
+ disputeWindow: BigInt(7200),
63
+ escrowContract: '0x' + 'd'.repeat(40), // ESCROW_ADDRESS
64
+ escrowId: '0x' + '2'.repeat(64), // ESCROW_ID
65
+ serviceHash: '0x0000000000000000000000000000000000000000000000000000000000000000'
66
+ }),
67
+ getTransaction: jest.fn().mockResolvedValue({
68
+ transactionId: '0x' + '1'.repeat(64),
69
+ requester: '0x' + 'c'.repeat(40), // REQUESTER_ADDRESS
70
+ provider: '0x' + 'b'.repeat(40), // PROVIDER_ADDRESS
71
+ amount: BigInt('100000000'),
72
+ state: State.INITIATED,
73
+ createdAt: BigInt(Math.floor(Date.now() / 1000)),
74
+ updatedAt: BigInt(Math.floor(Date.now() / 1000)),
75
+ deadline: BigInt(Math.floor(Date.now() / 1000) + 86400),
76
+ disputeWindow: BigInt(7200),
77
+ escrowContract: '0x' + 'd'.repeat(40), // ESCROW_ADDRESS
78
+ escrowId: '0x' + '2'.repeat(64), // ESCROW_ID
79
+ serviceHash: '0x0000000000000000000000000000000000000000000000000000000000000000',
80
+ contentHash: '0x0000000000000000000000000000000000000000000000000000000000000000',
81
+ attestationUID: '0x0000000000000000000000000000000000000000000000000000000000000000',
82
+ platformFeeBpsLocked: BigInt(100)
83
+ }),
84
+ getEconomicParams: jest.fn().mockResolvedValue([
85
+ BigInt(100), // platformFeeBps (1%)
86
+ BigInt(500), // requesterPenaltyBps (5%)
87
+ '0x' + 'f'.repeat(40) // feeRecipient
88
+ ]),
89
+ // ethers v6 requires getFunction
90
+ getFunction: jest.fn((name: string) => {
91
+ const functions: any = {
92
+ createTransaction: mockContract.createTransaction,
93
+ transitionState: mockContract.transitionState,
94
+ linkEscrow: mockContract.linkEscrow,
95
+ releaseEscrow: mockContract.releaseEscrow,
96
+ releaseMilestone: mockContract.releaseMilestone,
97
+ getTransaction: mockContract.getTransaction,
98
+ raiseDispute: jest.fn().mockResolvedValue({ wait: jest.fn().mockResolvedValue({}) }),
99
+ resolveDispute: jest.fn().mockResolvedValue({ wait: jest.fn().mockResolvedValue({}) })
100
+ };
101
+ const func = functions[name] || jest.fn().mockResolvedValue({ wait: jest.fn().mockResolvedValue({}) });
102
+ const estimateGasMap: any = mockContract.estimateGas;
103
+ func.estimateGas = estimateGasMap[name] || jest.fn().mockResolvedValue(BigInt(100000));
104
+ return func;
105
+ }),
106
+ // ethers v6 interface.parseLog mock
107
+ interface: {
108
+ parseLog: jest.fn((log: any) => {
109
+ // Mock parsing TransactionCreated event
110
+ if (log.topics && log.topics[0] === '0x' + '6'.repeat(64)) {
111
+ return {
112
+ name: 'TransactionCreated',
113
+ args: {
114
+ transactionId: log.topics[1] || '0x' + '1'.repeat(64),
115
+ 0: log.topics[1] || '0x' + '1'.repeat(64) // Positional access
116
+ }
117
+ };
118
+ }
119
+ // Unknown event - throw to simulate parseLog failure
120
+ throw new Error('Unknown event signature');
121
+ })
122
+ }
123
+ };
124
+
125
+ // Mock signer
126
+ const mockSigner = {
127
+ provider: {}
128
+ };
129
+
130
+ // Mock Contract constructor
131
+ jest.mock('ethers', () => {
132
+ const actual = jest.requireActual('ethers');
133
+ return {
134
+ ...actual,
135
+ Contract: jest.fn().mockImplementation(() => mockContract)
136
+ };
137
+ });
138
+
139
+ describe('ACTPKernel - Security Tests', () => {
140
+ let kernel: ACTPKernel;
141
+
142
+ const KERNEL_ADDRESS = '0x' + 'a'.repeat(40);
143
+ const PROVIDER_ADDRESS = '0x' + 'b'.repeat(40);
144
+ const REQUESTER_ADDRESS = '0x' + 'c'.repeat(40);
145
+ const ESCROW_ADDRESS = '0x' + 'd'.repeat(40);
146
+ const TX_ID = '0x' + '1'.repeat(64);
147
+ const ESCROW_ID = '0x' + '2'.repeat(64);
148
+
149
+ beforeEach(() => {
150
+ jest.clearAllMocks();
151
+ kernel = new ACTPKernel(KERNEL_ADDRESS, mockSigner as any);
152
+ });
153
+
154
+ describe('createTransaction - Input Validation', () => {
155
+ it('should successfully create transaction with valid params', async () => {
156
+ const params = {
157
+ provider: PROVIDER_ADDRESS,
158
+ requester: REQUESTER_ADDRESS,
159
+ amount: BigInt('100000000'),
160
+ deadline: Math.floor(Date.now() / 1000) + 86400, // 24 hours
161
+ disputeWindow: 7200 // 2 hours
162
+ };
163
+
164
+ const txId = await kernel.createTransaction(params);
165
+
166
+ expect(txId).toBe(TX_ID);
167
+ expect(mockContract.createTransaction).toHaveBeenCalled();
168
+ });
169
+
170
+ it('should reject zero address provider', async () => {
171
+ const params = {
172
+ provider: '0x0000000000000000000000000000000000000000',
173
+ requester: REQUESTER_ADDRESS,
174
+ amount: BigInt('100000000'),
175
+ deadline: Math.floor(Date.now() / 1000) + 86400,
176
+ disputeWindow: 7200
177
+ };
178
+
179
+ await expect(kernel.createTransaction(params)).rejects.toThrow('zero address');
180
+ });
181
+
182
+ it('should reject zero address requester', async () => {
183
+ const params = {
184
+ provider: PROVIDER_ADDRESS,
185
+ requester: '0x0000000000000000000000000000000000000000',
186
+ amount: BigInt('100000000'),
187
+ deadline: Math.floor(Date.now() / 1000) + 86400,
188
+ disputeWindow: 7200
189
+ };
190
+
191
+ await expect(kernel.createTransaction(params)).rejects.toThrow('zero address');
192
+ });
193
+
194
+ it('should reject zero amount', async () => {
195
+ const params = {
196
+ provider: PROVIDER_ADDRESS,
197
+ requester: REQUESTER_ADDRESS,
198
+ amount: BigInt(0),
199
+ deadline: Math.floor(Date.now() / 1000) + 86400,
200
+ disputeWindow: 7200
201
+ };
202
+
203
+ await expect(kernel.createTransaction(params)).rejects.toThrow('Invalid amount');
204
+ });
205
+
206
+ it('should reject past deadline', async () => {
207
+ const params = {
208
+ provider: PROVIDER_ADDRESS,
209
+ requester: REQUESTER_ADDRESS,
210
+ amount: BigInt('100000000'),
211
+ deadline: Math.floor(Date.now() / 1000) - 3600, // 1 hour ago
212
+ disputeWindow: 7200
213
+ };
214
+
215
+ await expect(kernel.createTransaction(params)).rejects.toThrow('must be in the future');
216
+ });
217
+
218
+ it('should reject negative dispute window', async () => {
219
+ const params = {
220
+ provider: PROVIDER_ADDRESS,
221
+ requester: REQUESTER_ADDRESS,
222
+ amount: BigInt('100000000'),
223
+ deadline: Math.floor(Date.now() / 1000) + 86400,
224
+ disputeWindow: -1
225
+ };
226
+
227
+ await expect(kernel.createTransaction(params)).rejects.toThrow('cannot be negative');
228
+ });
229
+
230
+ it('should reject dispute window exceeding 30 days', async () => {
231
+ const params = {
232
+ provider: PROVIDER_ADDRESS,
233
+ requester: REQUESTER_ADDRESS,
234
+ amount: BigInt('100000000'),
235
+ deadline: Math.floor(Date.now() / 1000) + 86400,
236
+ disputeWindow: 31 * 24 * 60 * 60 // 31 days
237
+ };
238
+
239
+ await expect(kernel.createTransaction(params)).rejects.toThrow('exceeds maximum');
240
+ });
241
+
242
+ it('should handle TransactionCreated event extraction failure', async () => {
243
+ mockContract.createTransaction.mockResolvedValueOnce({
244
+ wait: jest.fn().mockResolvedValue({
245
+ transactionHash: '0x' + '5'.repeat(64),
246
+ logs: [] // Empty logs - no events emitted
247
+ })
248
+ });
249
+
250
+ const params = {
251
+ provider: PROVIDER_ADDRESS,
252
+ requester: REQUESTER_ADDRESS,
253
+ amount: BigInt('100000000'),
254
+ deadline: Math.floor(Date.now() / 1000) + 86400,
255
+ disputeWindow: 7200
256
+ };
257
+
258
+ await expect(kernel.createTransaction(params)).rejects.toThrow('TransactionCreated event not found');
259
+ });
260
+
261
+ it('should include custom metadata in transaction', async () => {
262
+ const customMetadata = '0x' + '5'.repeat(64);
263
+ const params = {
264
+ provider: PROVIDER_ADDRESS,
265
+ requester: REQUESTER_ADDRESS,
266
+ amount: BigInt('100000000'),
267
+ deadline: Math.floor(Date.now() / 1000) + 86400,
268
+ disputeWindow: 7200,
269
+ metadata: customMetadata
270
+ };
271
+
272
+ await kernel.createTransaction(params);
273
+
274
+ expect(mockContract.createTransaction).toHaveBeenCalledWith(
275
+ PROVIDER_ADDRESS,
276
+ REQUESTER_ADDRESS,
277
+ expect.any(BigInt),
278
+ expect.any(Number),
279
+ expect.any(Number),
280
+ customMetadata,
281
+ expect.any(Object)
282
+ );
283
+ });
284
+
285
+ it('should use default metadata when not provided', async () => {
286
+ const params = {
287
+ provider: PROVIDER_ADDRESS,
288
+ requester: REQUESTER_ADDRESS,
289
+ amount: BigInt('100000000'),
290
+ deadline: Math.floor(Date.now() / 1000) + 86400,
291
+ disputeWindow: 7200
292
+ };
293
+
294
+ await kernel.createTransaction(params);
295
+
296
+ expect(mockContract.createTransaction).toHaveBeenCalledWith(
297
+ PROVIDER_ADDRESS,
298
+ REQUESTER_ADDRESS,
299
+ expect.any(BigInt),
300
+ expect.any(Number),
301
+ expect.any(Number),
302
+ '0x0000000000000000000000000000000000000000000000000000000000000000',
303
+ expect.any(Object)
304
+ );
305
+ });
306
+ });
307
+
308
+ describe('transitionState - State Machine Validation', () => {
309
+ it('should allow valid state transition INITIATED -> QUOTED', async () => {
310
+ mockContract.transactions.mockResolvedValueOnce({
311
+ transactionId: TX_ID,
312
+ requester: REQUESTER_ADDRESS,
313
+ provider: PROVIDER_ADDRESS,
314
+ amount: BigInt('100000000'),
315
+ state: State.INITIATED,
316
+ createdAt: BigInt(Math.floor(Date.now() / 1000)),
317
+ deadline: BigInt(Math.floor(Date.now() / 1000) + 86400),
318
+ disputeWindow: BigInt(7200),
319
+ escrowContract: ESCROW_ADDRESS,
320
+ escrowId: ESCROW_ID,
321
+ serviceHash: '0x0000000000000000000000000000000000000000000000000000000000000000'
322
+ });
323
+
324
+ await kernel.transitionState(TX_ID, State.QUOTED);
325
+
326
+ expect(mockContract.transitionState).toHaveBeenCalledWith(
327
+ TX_ID,
328
+ State.QUOTED,
329
+ '0x',
330
+ expect.any(Object)
331
+ );
332
+ });
333
+
334
+ it('should reject invalid state transition INITIATED -> DELIVERED', async () => {
335
+ mockContract.transactions.mockResolvedValueOnce({
336
+ transactionId: TX_ID,
337
+ requester: REQUESTER_ADDRESS,
338
+ provider: PROVIDER_ADDRESS,
339
+ amount: BigInt('100000000'),
340
+ state: State.INITIATED,
341
+ createdAt: BigInt(Math.floor(Date.now() / 1000)),
342
+ deadline: BigInt(Math.floor(Date.now() / 1000) + 86400),
343
+ disputeWindow: BigInt(7200),
344
+ escrowContract: ESCROW_ADDRESS,
345
+ escrowId: ESCROW_ID,
346
+ serviceHash: '0x0000000000000000000000000000000000000000000000000000000000000000'
347
+ });
348
+
349
+ await expect(kernel.transitionState(TX_ID, State.DELIVERED))
350
+ .rejects.toThrow('Invalid state transition');
351
+ });
352
+
353
+ it('should reject backwards state transition DELIVERED -> COMMITTED', async () => {
354
+ mockContract.getTransaction.mockResolvedValueOnce({
355
+ txId: TX_ID,
356
+ transactionId: TX_ID,
357
+ requester: REQUESTER_ADDRESS,
358
+ provider: PROVIDER_ADDRESS,
359
+ amount: BigInt('100000000'),
360
+ state: State.DELIVERED,
361
+ createdAt: BigInt(Math.floor(Date.now() / 1000)),
362
+ updatedAt: BigInt(Math.floor(Date.now() / 1000)),
363
+ deadline: BigInt(Math.floor(Date.now() / 1000) + 86400),
364
+ disputeWindow: BigInt(7200),
365
+ escrowContract: ESCROW_ADDRESS,
366
+ escrowId: ESCROW_ID,
367
+ serviceHash: '0x0000000000000000000000000000000000000000000000000000000000000000',
368
+ contentHash: '0x0000000000000000000000000000000000000000000000000000000000000000',
369
+ attestationUID: '0x0000000000000000000000000000000000000000000000000000000000000000',
370
+ platformFeeBpsLocked: BigInt(100)
371
+ });
372
+
373
+ await expect(kernel.transitionState(TX_ID, State.COMMITTED))
374
+ .rejects.toThrow('Invalid state transition');
375
+ });
376
+
377
+ it('should reject transition to same state', async () => {
378
+ mockContract.transactions.mockResolvedValueOnce({
379
+ transactionId: TX_ID,
380
+ requester: REQUESTER_ADDRESS,
381
+ provider: PROVIDER_ADDRESS,
382
+ amount: BigInt('100000000'),
383
+ state: State.INITIATED,
384
+ createdAt: BigInt(Math.floor(Date.now() / 1000)),
385
+ deadline: BigInt(Math.floor(Date.now() / 1000) + 86400),
386
+ disputeWindow: BigInt(7200),
387
+ escrowContract: ESCROW_ADDRESS,
388
+ escrowId: ESCROW_ID,
389
+ serviceHash: '0x0000000000000000000000000000000000000000000000000000000000000000'
390
+ });
391
+
392
+ await expect(kernel.transitionState(TX_ID, State.INITIATED))
393
+ .rejects.toThrow('Invalid state transition');
394
+ });
395
+
396
+ it('should accept proof data for DELIVERED state', async () => {
397
+ mockContract.getTransaction.mockResolvedValueOnce({
398
+ transactionId: TX_ID,
399
+ requester: REQUESTER_ADDRESS,
400
+ provider: PROVIDER_ADDRESS,
401
+ amount: BigInt('100000000'),
402
+ state: State.IN_PROGRESS,
403
+ createdAt: BigInt(Math.floor(Date.now() / 1000)),
404
+ updatedAt: BigInt(Math.floor(Date.now() / 1000)),
405
+ deadline: BigInt(Math.floor(Date.now() / 1000) + 86400),
406
+ disputeWindow: BigInt(7200),
407
+ escrowContract: ESCROW_ADDRESS,
408
+ escrowId: ESCROW_ID,
409
+ serviceHash: '0x0000000000000000000000000000000000000000000000000000000000000000',
410
+ contentHash: '0x0000000000000000000000000000000000000000000000000000000000000000',
411
+ attestationUID: '0x0000000000000000000000000000000000000000000000000000000000000000',
412
+ platformFeeBpsLocked: BigInt(100)
413
+ });
414
+
415
+ const abiCoder = AbiCoder.defaultAbiCoder();
416
+ const proofData = abiCoder.encode(
417
+ ['string', 'string'],
418
+ ['https://ipfs.io/ipfs/Qm...', '0x' + '5'.repeat(64)]
419
+ );
420
+
421
+ await kernel.transitionState(TX_ID, State.DELIVERED, proofData);
422
+
423
+ expect(mockContract.transitionState).toHaveBeenCalledWith(
424
+ TX_ID,
425
+ State.DELIVERED,
426
+ proofData,
427
+ expect.any(Object)
428
+ );
429
+ });
430
+
431
+ it('should validate transaction ID format', async () => {
432
+ await expect(kernel.transitionState('invalid-tx-id', State.QUOTED))
433
+ .rejects.toThrow('Invalid transaction ID format');
434
+ });
435
+
436
+ it('should throw error when transaction not found', async () => {
437
+ mockContract.getTransaction.mockResolvedValueOnce({
438
+ transactionId: TX_ID,
439
+ requester: REQUESTER_ADDRESS,
440
+ provider: PROVIDER_ADDRESS,
441
+ amount: BigInt('100000000'),
442
+ state: State.INITIATED,
443
+ createdAt: BigInt(0), // Indicates transaction doesn't exist
444
+ updatedAt: BigInt(0),
445
+ deadline: BigInt(Math.floor(Date.now() / 1000) + 86400),
446
+ disputeWindow: BigInt(7200),
447
+ escrowContract: ESCROW_ADDRESS,
448
+ escrowId: ESCROW_ID,
449
+ serviceHash: '0x0000000000000000000000000000000000000000000000000000000000000000',
450
+ contentHash: '0x0000000000000000000000000000000000000000000000000000000000000000',
451
+ attestationUID: '0x0000000000000000000000000000000000000000000000000000000000000000',
452
+ platformFeeBpsLocked: BigInt(0)
453
+ });
454
+
455
+ await expect(kernel.transitionState(TX_ID, State.QUOTED))
456
+ .rejects.toThrow('Transaction');
457
+ });
458
+
459
+ it('should handle contract revert errors gracefully', async () => {
460
+ mockContract.transitionState.mockRejectedValueOnce({
461
+ transactionHash: '0x' + 'f'.repeat(64),
462
+ reason: 'Unauthorized caller',
463
+ message: 'execution reverted: Unauthorized caller'
464
+ });
465
+
466
+ await expect(kernel.transitionState(TX_ID, State.QUOTED))
467
+ .rejects.toThrow('Transaction reverted');
468
+ });
469
+ });
470
+
471
+ describe('linkEscrow - Escrow Integration', () => {
472
+ it('should successfully link escrow', async () => {
473
+ await kernel.linkEscrow(TX_ID, ESCROW_ADDRESS, ESCROW_ID);
474
+
475
+ expect(mockContract.linkEscrow).toHaveBeenCalledWith(
476
+ TX_ID,
477
+ ESCROW_ADDRESS,
478
+ ESCROW_ID,
479
+ expect.any(Object)
480
+ );
481
+ });
482
+
483
+ it('should reject invalid transaction ID', async () => {
484
+ await expect(kernel.linkEscrow('invalid', ESCROW_ADDRESS, ESCROW_ID))
485
+ .rejects.toThrow('Invalid transaction ID format');
486
+ });
487
+
488
+ it('should reject zero address escrow contract', async () => {
489
+ await expect(kernel.linkEscrow(TX_ID, '0x0000000000000000000000000000000000000000', ESCROW_ID))
490
+ .rejects.toThrow('zero address');
491
+ });
492
+
493
+ it('should reject invalid escrow ID format', async () => {
494
+ await expect(kernel.linkEscrow(TX_ID, ESCROW_ADDRESS, 'invalid-escrow-id'))
495
+ .rejects.toThrow('Invalid transaction ID format');
496
+ });
497
+
498
+ it('should handle link escrow revert', async () => {
499
+ mockContract.linkEscrow.mockRejectedValueOnce({
500
+ transactionHash: '0x' + 'f'.repeat(64),
501
+ reason: 'Escrow already linked',
502
+ message: 'execution reverted: Escrow already linked'
503
+ });
504
+
505
+ await expect(kernel.linkEscrow(TX_ID, ESCROW_ADDRESS, ESCROW_ID))
506
+ .rejects.toThrow('Transaction reverted');
507
+ });
508
+ });
509
+
510
+ describe('releaseEscrow - Fund Distribution', () => {
511
+ it('should successfully release escrow', async () => {
512
+ await kernel.releaseEscrow(TX_ID);
513
+
514
+ expect(mockContract.releaseEscrow).toHaveBeenCalledWith(
515
+ TX_ID,
516
+ expect.any(Object)
517
+ );
518
+ });
519
+
520
+ it('should reject invalid transaction ID', async () => {
521
+ await expect(kernel.releaseEscrow('invalid-id'))
522
+ .rejects.toThrow('Invalid transaction ID format');
523
+ });
524
+
525
+ it('should handle release revert', async () => {
526
+ mockContract.releaseEscrow.mockRejectedValueOnce({
527
+ transactionHash: '0x' + 'f'.repeat(64),
528
+ reason: 'Not in DELIVERED state',
529
+ message: 'execution reverted: Not in DELIVERED state'
530
+ });
531
+
532
+ await expect(kernel.releaseEscrow(TX_ID))
533
+ .rejects.toThrow('Transaction reverted');
534
+ });
535
+ });
536
+
537
+ describe('releaseMilestone - Partial Payments', () => {
538
+ it('should successfully release milestone', async () => {
539
+ const milestoneId = 1;
540
+ const amount = BigInt('50000000');
541
+
542
+ await kernel.releaseMilestone(TX_ID, milestoneId, amount);
543
+
544
+ expect(mockContract.releaseMilestone).toHaveBeenCalledWith(
545
+ TX_ID,
546
+ milestoneId,
547
+ amount,
548
+ expect.any(Object)
549
+ );
550
+ });
551
+
552
+ it('should reject negative milestone ID', async () => {
553
+ const amount = BigInt('50000000');
554
+
555
+ await expect(kernel.releaseMilestone(TX_ID, -1, amount))
556
+ .rejects.toThrow('cannot be negative');
557
+ });
558
+
559
+ it('should reject zero amount', async () => {
560
+ await expect(kernel.releaseMilestone(TX_ID, 1, BigInt(0)))
561
+ .rejects.toThrow('Invalid amount');
562
+ });
563
+
564
+ it('should reject invalid transaction ID', async () => {
565
+ await expect(kernel.releaseMilestone('invalid', 1, BigInt('50000000')))
566
+ .rejects.toThrow('Invalid transaction ID format');
567
+ });
568
+ });
569
+
570
+ describe('raiseDispute - Dispute Mechanism', () => {
571
+ it('should successfully raise dispute with reason and evidence', async () => {
572
+ const reason = 'Work not delivered as specified';
573
+ const evidence = 'ipfs://Qm...';
574
+
575
+ await kernel.raiseDispute(TX_ID, reason, evidence);
576
+
577
+ expect(mockContract.transitionState).toHaveBeenCalledWith(
578
+ TX_ID,
579
+ State.DISPUTED,
580
+ expect.any(String), // Encoded proof
581
+ expect.any(Object)
582
+ );
583
+ });
584
+
585
+ it('should reject invalid transaction ID', async () => {
586
+ await expect(kernel.raiseDispute('invalid', 'reason', 'evidence'))
587
+ .rejects.toThrow('Invalid transaction ID format');
588
+ });
589
+
590
+ it('should encode dispute proof correctly', async () => {
591
+ const reason = 'Test reason';
592
+ const evidence = 'Test evidence';
593
+
594
+ await kernel.raiseDispute(TX_ID, reason, evidence);
595
+
596
+ const abiCoder = AbiCoder.defaultAbiCoder();
597
+ const expectedProof = abiCoder.encode(
598
+ ['string', 'string'],
599
+ [reason, evidence]
600
+ );
601
+
602
+ expect(mockContract.transitionState).toHaveBeenCalledWith(
603
+ TX_ID,
604
+ State.DISPUTED,
605
+ expectedProof,
606
+ expect.any(Object)
607
+ );
608
+ });
609
+ });
610
+
611
+ describe('resolveDispute - Dispute Resolution', () => {
612
+ it('should successfully resolve dispute with split', async () => {
613
+ const resolution = {
614
+ requesterAmount: BigInt('30000000'),
615
+ providerAmount: BigInt('60000000'),
616
+ mediatorAmount: BigInt('10000000'),
617
+ mediator: '0x' + 'e'.repeat(40)
618
+ };
619
+
620
+ await kernel.resolveDispute(TX_ID, resolution);
621
+
622
+ expect(mockContract.transitionState).toHaveBeenCalledWith(
623
+ TX_ID,
624
+ State.SETTLED,
625
+ expect.any(String),
626
+ expect.any(Object)
627
+ );
628
+ });
629
+
630
+ it('should reject negative requester amount', async () => {
631
+ const resolution = {
632
+ requesterAmount: BigInt(-1),
633
+ providerAmount: BigInt('60000000'),
634
+ mediatorAmount: BigInt('10000000'),
635
+ mediator: '0x' + 'e'.repeat(40)
636
+ };
637
+
638
+ await expect(kernel.resolveDispute(TX_ID, resolution))
639
+ .rejects.toThrow('cannot be negative');
640
+ });
641
+
642
+ it('should reject negative provider amount', async () => {
643
+ const resolution = {
644
+ requesterAmount: BigInt('30000000'),
645
+ providerAmount: BigInt(-1),
646
+ mediatorAmount: BigInt('10000000'),
647
+ mediator: '0x' + 'e'.repeat(40)
648
+ };
649
+
650
+ await expect(kernel.resolveDispute(TX_ID, resolution))
651
+ .rejects.toThrow('cannot be negative');
652
+ });
653
+
654
+ it('should reject negative mediator amount', async () => {
655
+ const resolution = {
656
+ requesterAmount: BigInt('30000000'),
657
+ providerAmount: BigInt('60000000'),
658
+ mediatorAmount: BigInt(-1),
659
+ mediator: '0x' + 'e'.repeat(40)
660
+ };
661
+
662
+ await expect(kernel.resolveDispute(TX_ID, resolution))
663
+ .rejects.toThrow('cannot be negative');
664
+ });
665
+
666
+ it('should require mediator address when mediator amount > 0', async () => {
667
+ const resolution = {
668
+ requesterAmount: BigInt('30000000'),
669
+ providerAmount: BigInt('60000000'),
670
+ mediatorAmount: BigInt('10000000'),
671
+ mediator: undefined
672
+ };
673
+
674
+ await expect(kernel.resolveDispute(TX_ID, resolution))
675
+ .rejects.toThrow('Mediator address required');
676
+ });
677
+
678
+ it('should allow zero mediator amount without mediator address', async () => {
679
+ const resolution = {
680
+ requesterAmount: BigInt('50000000'),
681
+ providerAmount: BigInt('50000000'),
682
+ mediatorAmount: BigInt(0),
683
+ mediator: undefined
684
+ };
685
+
686
+ await kernel.resolveDispute(TX_ID, resolution);
687
+
688
+ expect(mockContract.transitionState).toHaveBeenCalled();
689
+ });
690
+ });
691
+
692
+ describe('getTransaction - Data Retrieval', () => {
693
+ it('should retrieve transaction details', async () => {
694
+ const tx = await kernel.getTransaction(TX_ID);
695
+
696
+ expect(tx.txId).toBe(TX_ID);
697
+ expect(tx.requester).toBe(REQUESTER_ADDRESS);
698
+ expect(tx.provider).toBe(PROVIDER_ADDRESS);
699
+ expect(tx.state).toBe(State.INITIATED);
700
+ });
701
+
702
+ it('should throw error when transaction not found', async () => {
703
+ mockContract.getTransaction.mockResolvedValueOnce({
704
+ transactionId: TX_ID,
705
+ requester: REQUESTER_ADDRESS,
706
+ provider: PROVIDER_ADDRESS,
707
+ amount: BigInt('100000000'),
708
+ state: State.INITIATED,
709
+ createdAt: BigInt(0), // Indicates transaction doesn't exist
710
+ updatedAt: BigInt(0),
711
+ deadline: BigInt(0),
712
+ disputeWindow: BigInt(0),
713
+ escrowContract: '0x0000000000000000000000000000000000000000',
714
+ escrowId: '0x0000000000000000000000000000000000000000000000000000000000000000',
715
+ serviceHash: '0x0000000000000000000000000000000000000000000000000000000000000000',
716
+ contentHash: '0x0000000000000000000000000000000000000000000000000000000000000000',
717
+ attestationUID: '0x0000000000000000000000000000000000000000000000000000000000000000',
718
+ platformFeeBpsLocked: BigInt(0)
719
+ });
720
+
721
+ await expect(kernel.getTransaction(TX_ID))
722
+ .rejects.toThrow('not found');
723
+ });
724
+ });
725
+
726
+ describe('getEconomicParams - Fee Structure', () => {
727
+ it('should retrieve economic parameters', async () => {
728
+ const params = await kernel.getEconomicParams();
729
+
730
+ expect(params.baseFeeNumerator).toBe(100); // 1% = 100 bps
731
+ expect(params.baseFeeDenominator).toBe(10000);
732
+ expect(params.requesterPenaltyBps).toBe(500);
733
+ expect(params.feeRecipient).toBe('0x' + 'f'.repeat(40));
734
+ });
735
+
736
+ it('should handle array-based response format', async () => {
737
+ mockContract.getEconomicParams.mockResolvedValueOnce([
738
+ BigInt(100),
739
+ BigInt(500),
740
+ '0x' + 'f'.repeat(40)
741
+ ]);
742
+
743
+ const params = await kernel.getEconomicParams();
744
+
745
+ expect(params.baseFeeNumerator).toBe(100);
746
+ });
747
+ });
748
+
749
+ describe('Gas Estimation - V6 Dynamic Buffers', () => {
750
+ it('should apply 15% gas buffer to createTransaction', async () => {
751
+ const params = {
752
+ provider: PROVIDER_ADDRESS,
753
+ requester: REQUESTER_ADDRESS,
754
+ amount: BigInt('100000000'),
755
+ deadline: Math.floor(Date.now() / 1000) + 86400,
756
+ disputeWindow: 7200
757
+ };
758
+
759
+ await kernel.createTransaction(params);
760
+
761
+ expect(mockContract.createTransaction).toHaveBeenCalledWith(
762
+ PROVIDER_ADDRESS,
763
+ REQUESTER_ADDRESS,
764
+ BigInt('100000000'),
765
+ expect.any(Number),
766
+ 7200,
767
+ '0x0000000000000000000000000000000000000000000000000000000000000000',
768
+ expect.objectContaining({
769
+ gasLimit: BigInt(97750) // 85000 * 1.15 (15% buffer for simple state init)
770
+ })
771
+ );
772
+ });
773
+
774
+ it('should apply 20% gas buffer to transitionState', async () => {
775
+ const txId = TX_ID;
776
+ const newState = State.QUOTED; // Valid transition from INITIATED → QUOTED
777
+
778
+ await kernel.transitionState(txId, newState);
779
+
780
+ expect(mockContract.transitionState).toHaveBeenCalledWith(
781
+ txId,
782
+ newState,
783
+ '0x',
784
+ expect.objectContaining({
785
+ gasLimit: BigInt(60000) // 50000 * 1.20 (20% buffer for standard state change)
786
+ })
787
+ );
788
+ });
789
+
790
+ it('should apply 30% gas buffer to releaseEscrow', async () => {
791
+ const txId = TX_ID;
792
+
793
+ await kernel.releaseEscrow(txId);
794
+
795
+ expect(mockContract.releaseEscrow).toHaveBeenCalledWith(
796
+ txId,
797
+ expect.objectContaining({
798
+ gasLimit: BigInt(65000) // 50000 * 1.30 (30% buffer for multi-recipient disbursement)
799
+ })
800
+ );
801
+ });
802
+
803
+ it('should apply 30% gas buffer to releaseMilestone', async () => {
804
+ const txId = TX_ID;
805
+ const milestoneId = 1;
806
+ const amount = BigInt('50000000');
807
+
808
+ await kernel.releaseMilestone(txId, milestoneId, amount);
809
+
810
+ expect(mockContract.releaseMilestone).toHaveBeenCalledWith(
811
+ txId,
812
+ milestoneId,
813
+ amount,
814
+ expect.objectContaining({
815
+ gasLimit: BigInt(58500) // 45000 * 1.30 (30% buffer for escrow release)
816
+ })
817
+ );
818
+ });
819
+
820
+ it('should apply gas settings when provided', async () => {
821
+ const gasSettings = {
822
+ maxFeePerGas: BigInt('2000000000'),
823
+ maxPriorityFeePerGas: BigInt('1000000000')
824
+ };
825
+
826
+ const kernelWithGas = new ACTPKernel(KERNEL_ADDRESS, mockSigner as any, gasSettings);
827
+
828
+ const params = {
829
+ provider: PROVIDER_ADDRESS,
830
+ requester: REQUESTER_ADDRESS,
831
+ amount: BigInt('100000000'),
832
+ deadline: Math.floor(Date.now() / 1000) + 86400,
833
+ disputeWindow: 7200
834
+ };
835
+
836
+ await kernelWithGas.createTransaction(params);
837
+
838
+ expect(mockContract.createTransaction).toHaveBeenCalledWith(
839
+ PROVIDER_ADDRESS,
840
+ REQUESTER_ADDRESS,
841
+ BigInt('100000000'),
842
+ expect.any(Number),
843
+ 7200,
844
+ '0x0000000000000000000000000000000000000000000000000000000000000000',
845
+ expect.objectContaining({
846
+ maxFeePerGas: gasSettings.maxFeePerGas,
847
+ maxPriorityFeePerGas: gasSettings.maxPriorityFeePerGas
848
+ })
849
+ );
850
+ });
851
+ });
852
+
853
+ describe('getAddress', () => {
854
+ it('should return kernel contract address', () => {
855
+ expect(kernel.getAddress()).toBe(KERNEL_ADDRESS);
856
+ });
857
+ });
858
+
859
+ describe('estimateCreateTransaction', () => {
860
+ it('should estimate gas for transaction creation', async () => {
861
+ const params = {
862
+ provider: PROVIDER_ADDRESS,
863
+ requester: REQUESTER_ADDRESS,
864
+ amount: BigInt('100000000'),
865
+ deadline: Math.floor(Date.now() / 1000) + 86400,
866
+ disputeWindow: 7200
867
+ };
868
+
869
+ const estimatedGas = await kernel.estimateCreateTransaction(params);
870
+
871
+ expect(estimatedGas).toEqual(BigInt(85000));
872
+ });
873
+ });
874
+
875
+ describe('Event Parsing Edge Cases - Coverage Gap 1 (Lines 156-173)', () => {
876
+ it('should handle malformed log that causes parseLog error', async () => {
877
+ mockContract.createTransaction.mockResolvedValueOnce({
878
+ wait: jest.fn().mockResolvedValue({
879
+ transactionHash: '0x' + '5'.repeat(64),
880
+ logs: [
881
+ {
882
+ topics: ['0xINVALID_TOPIC'], // Malformed topic
883
+ data: '0xGARBAGE_DATA',
884
+ address: KERNEL_ADDRESS
885
+ },
886
+ {
887
+ topics: ['0x' + '6'.repeat(64), '0x' + '1'.repeat(64)],
888
+ data: '0x',
889
+ address: KERNEL_ADDRESS
890
+ }
891
+ ]
892
+ })
893
+ });
894
+
895
+ // Mock parseLog to throw on first log, succeed on second
896
+ mockContract.interface.parseLog
897
+ .mockImplementationOnce(() => {
898
+ throw new Error('Invalid log data format');
899
+ })
900
+ .mockReturnValueOnce({
901
+ name: 'TransactionCreated',
902
+ args: {
903
+ transactionId: '0x' + '1'.repeat(64),
904
+ 0: '0x' + '1'.repeat(64)
905
+ }
906
+ });
907
+
908
+ const params = {
909
+ provider: PROVIDER_ADDRESS,
910
+ requester: REQUESTER_ADDRESS,
911
+ amount: BigInt('100000000'),
912
+ deadline: Math.floor(Date.now() / 1000) + 86400,
913
+ disputeWindow: 7200
914
+ };
915
+
916
+ const txId = await kernel.createTransaction(params);
917
+
918
+ // Should skip malformed log via catch block (line 169) and find valid event
919
+ expect(txId).toBe('0x' + '1'.repeat(64));
920
+ expect(mockContract.interface.parseLog).toHaveBeenCalledTimes(2);
921
+ });
922
+
923
+ it('should skip non-matching events and find TransactionCreated in multiple logs', async () => {
924
+ mockContract.createTransaction.mockResolvedValueOnce({
925
+ wait: jest.fn().mockResolvedValue({
926
+ transactionHash: '0x' + '5'.repeat(64),
927
+ logs: [
928
+ {
929
+ topics: ['0x' + '7'.repeat(64)], // Different event signature
930
+ data: '0x',
931
+ address: KERNEL_ADDRESS
932
+ },
933
+ {
934
+ topics: ['0x' + '8'.repeat(64)], // Another different event
935
+ data: '0x',
936
+ address: KERNEL_ADDRESS
937
+ },
938
+ {
939
+ topics: ['0x' + '6'.repeat(64), '0x' + '1'.repeat(64)], // TransactionCreated
940
+ data: '0x',
941
+ address: KERNEL_ADDRESS
942
+ }
943
+ ]
944
+ })
945
+ });
946
+
947
+ mockContract.interface.parseLog
948
+ .mockReturnValueOnce({
949
+ name: 'SomeOtherEvent',
950
+ args: {
951
+ transactionId: null,
952
+ 0: null
953
+ }
954
+ })
955
+ .mockReturnValueOnce({
956
+ name: 'AnotherEvent',
957
+ args: {
958
+ transactionId: null,
959
+ 0: null
960
+ }
961
+ })
962
+ .mockReturnValueOnce({
963
+ name: 'TransactionCreated',
964
+ args: {
965
+ transactionId: '0x' + '1'.repeat(64),
966
+ 0: '0x' + '1'.repeat(64)
967
+ }
968
+ });
969
+
970
+ const params = {
971
+ provider: PROVIDER_ADDRESS,
972
+ requester: REQUESTER_ADDRESS,
973
+ amount: BigInt('100000000'),
974
+ deadline: Math.floor(Date.now() / 1000) + 86400,
975
+ disputeWindow: 7200
976
+ };
977
+
978
+ const txId = await kernel.createTransaction(params);
979
+
980
+ expect(txId).toBe('0x' + '1'.repeat(64));
981
+ expect(mockContract.interface.parseLog).toHaveBeenCalledTimes(3);
982
+ });
983
+
984
+ it('should throw error when no TransactionCreated event found in receipt (line 173)', async () => {
985
+ mockContract.createTransaction.mockResolvedValueOnce({
986
+ wait: jest.fn().mockResolvedValue({
987
+ transactionHash: '0x' + '5'.repeat(64),
988
+ logs: [
989
+ {
990
+ topics: ['0x' + '7'.repeat(64)],
991
+ data: '0x',
992
+ address: KERNEL_ADDRESS
993
+ },
994
+ {
995
+ topics: ['0x' + '8'.repeat(64)],
996
+ data: '0x',
997
+ address: KERNEL_ADDRESS
998
+ }
999
+ ]
1000
+ })
1001
+ });
1002
+
1003
+ // Mock parseLog to return non-matching events
1004
+ mockContract.interface.parseLog
1005
+ .mockReturnValueOnce({
1006
+ name: 'SomeOtherEvent',
1007
+ args: {
1008
+ transactionId: null,
1009
+ 0: null
1010
+ }
1011
+ })
1012
+ .mockReturnValueOnce({
1013
+ name: 'YetAnotherEvent',
1014
+ args: {
1015
+ transactionId: null,
1016
+ 0: null
1017
+ }
1018
+ });
1019
+
1020
+ const params = {
1021
+ provider: PROVIDER_ADDRESS,
1022
+ requester: REQUESTER_ADDRESS,
1023
+ amount: BigInt('100000000'),
1024
+ deadline: Math.floor(Date.now() / 1000) + 86400,
1025
+ disputeWindow: 7200
1026
+ };
1027
+
1028
+ // Should throw "TransactionCreated event not found" (line 173)
1029
+ await expect(kernel.createTransaction(params))
1030
+ .rejects.toThrow('TransactionCreated event not found in receipt');
1031
+ });
1032
+ });
1033
+
1034
+ describe('anchorAttestation - Attestation Anchoring (Lines 502-530)', () => {
1035
+ beforeEach(() => {
1036
+ // Setup anchorAttestation mock
1037
+ const anchorAttestationFunc: any = jest.fn().mockResolvedValue({
1038
+ wait: jest.fn().mockResolvedValue({
1039
+ transactionHash: '0x' + 'a'.repeat(64),
1040
+ logs: []
1041
+ })
1042
+ });
1043
+ anchorAttestationFunc.estimateGas = jest.fn().mockResolvedValue(BigInt(50000));
1044
+
1045
+ mockContract.getFunction.mockImplementation((name: string) => {
1046
+ if (name === 'anchorAttestation') {
1047
+ return anchorAttestationFunc;
1048
+ }
1049
+ // Fallback to existing mock implementation
1050
+ const functions: any = {
1051
+ createTransaction: mockContract.createTransaction,
1052
+ transitionState: mockContract.transitionState,
1053
+ linkEscrow: mockContract.linkEscrow,
1054
+ releaseEscrow: mockContract.releaseEscrow,
1055
+ releaseMilestone: mockContract.releaseMilestone,
1056
+ getTransaction: mockContract.getTransaction,
1057
+ raiseDispute: jest.fn().mockResolvedValue({ wait: jest.fn().mockResolvedValue({}) }),
1058
+ resolveDispute: jest.fn().mockResolvedValue({ wait: jest.fn().mockResolvedValue({}) })
1059
+ };
1060
+ const func = functions[name] || jest.fn().mockResolvedValue({ wait: jest.fn().mockResolvedValue({}) });
1061
+ const estimateGasMap: any = mockContract.estimateGas;
1062
+ func.estimateGas = estimateGasMap[name] || jest.fn().mockResolvedValue(BigInt(100000));
1063
+ return func;
1064
+ });
1065
+ });
1066
+
1067
+ it('should successfully anchor valid attestation', async () => {
1068
+ const txId = '0x' + '1'.repeat(64);
1069
+ const attestationUID = '0x' + '2'.repeat(64);
1070
+
1071
+ await kernel.anchorAttestation(txId, attestationUID);
1072
+
1073
+ const anchorFunc = mockContract.getFunction('anchorAttestation');
1074
+ expect(anchorFunc).toHaveBeenCalledWith(
1075
+ txId,
1076
+ attestationUID,
1077
+ expect.objectContaining({
1078
+ gasLimit: expect.any(BigInt)
1079
+ })
1080
+ );
1081
+ });
1082
+
1083
+ it('should validate attestationUID format - missing 0x prefix', async () => {
1084
+ const txId = '0x' + '1'.repeat(64);
1085
+ const invalidUID = '1'.repeat(64); // Missing 0x prefix
1086
+
1087
+ await expect(kernel.anchorAttestation(txId, invalidUID))
1088
+ .rejects.toThrow('Must be 32-byte hex string');
1089
+ });
1090
+
1091
+ it('should validate attestationUID format - too short', async () => {
1092
+ const txId = '0x' + '1'.repeat(64);
1093
+ const invalidUID = '0x1234'; // Too short
1094
+
1095
+ await expect(kernel.anchorAttestation(txId, invalidUID))
1096
+ .rejects.toThrow('Must be 32-byte hex string');
1097
+ });
1098
+
1099
+ it('should validate attestationUID format - invalid hex characters', async () => {
1100
+ const txId = '0x' + '1'.repeat(64);
1101
+ const invalidUID = '0x' + 'G'.repeat(64); // Invalid hex
1102
+
1103
+ await expect(kernel.anchorAttestation(txId, invalidUID))
1104
+ .rejects.toThrow('Must be 32-byte hex string');
1105
+ });
1106
+
1107
+ it('should validate attestationUID format - empty string', async () => {
1108
+ const txId = '0x' + '1'.repeat(64);
1109
+ const invalidUID = '';
1110
+
1111
+ await expect(kernel.anchorAttestation(txId, invalidUID))
1112
+ .rejects.toThrow('Must be 32-byte hex string');
1113
+ });
1114
+
1115
+ it('should apply 15% gas buffer for anchorAttestation', async () => {
1116
+ const txId = '0x' + '1'.repeat(64);
1117
+ const attestationUID = '0x' + '2'.repeat(64);
1118
+
1119
+ const estimateGasMock = jest.fn().mockResolvedValue(BigInt(100000));
1120
+ const anchorFunc: any = jest.fn().mockResolvedValue({
1121
+ wait: jest.fn().mockResolvedValue({
1122
+ transactionHash: '0x' + 'a'.repeat(64),
1123
+ logs: []
1124
+ })
1125
+ });
1126
+ anchorFunc.estimateGas = estimateGasMock;
1127
+
1128
+ mockContract.getFunction.mockReturnValueOnce(anchorFunc);
1129
+
1130
+ await kernel.anchorAttestation(txId, attestationUID);
1131
+
1132
+ // 15% gas buffer for anchorAttestation (simple attestation anchoring)
1133
+ expect(anchorFunc).toHaveBeenCalledWith(
1134
+ txId,
1135
+ attestationUID,
1136
+ expect.objectContaining({
1137
+ gasLimit: BigInt(115000) // 100k * 1.15
1138
+ })
1139
+ );
1140
+ });
1141
+
1142
+ it('should handle anchorAttestation contract revert', async () => {
1143
+ const txId = '0x' + '1'.repeat(64);
1144
+ const attestationUID = '0x' + '2'.repeat(64);
1145
+
1146
+ const anchorFunc: any = jest.fn().mockRejectedValueOnce({
1147
+ transactionHash: '0x' + 'f'.repeat(64),
1148
+ reason: 'Attestation already anchored',
1149
+ message: 'execution reverted: Attestation already anchored'
1150
+ });
1151
+ anchorFunc.estimateGas = jest.fn().mockResolvedValue(BigInt(50000));
1152
+
1153
+ mockContract.getFunction.mockReturnValueOnce(anchorFunc);
1154
+
1155
+ await expect(kernel.anchorAttestation(txId, attestationUID))
1156
+ .rejects.toThrow('Transaction reverted');
1157
+ });
1158
+
1159
+ it('should validate transaction ID format in anchorAttestation', async () => {
1160
+ const invalidTxId = 'not-a-valid-tx-id';
1161
+ const attestationUID = '0x' + '2'.repeat(64);
1162
+
1163
+ await expect(kernel.anchorAttestation(invalidTxId, attestationUID))
1164
+ .rejects.toThrow('Invalid transaction ID format');
1165
+ });
1166
+ });
1167
+ });