@cofhe/sdk 0.6.0 → 0.7.0

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 (114) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/acps/acp.ts +411 -0
  3. package/acps/index.ts +70 -0
  4. package/{permits → acps}/onchain-utils.ts +49 -24
  5. package/acps/sealing.ts +90 -0
  6. package/acps/signature.ts +89 -0
  7. package/acps/store.ts +172 -0
  8. package/acps/test/acp.test.ts +615 -0
  9. package/acps/test/localstorage.test.ts +105 -0
  10. package/acps/test/sealing.test.ts +77 -0
  11. package/acps/test/store.test.ts +88 -0
  12. package/acps/test/validation.test.ts +361 -0
  13. package/acps/test-utils.ts +32 -0
  14. package/acps/types.ts +252 -0
  15. package/acps/validation.ts +392 -0
  16. package/adapters/test/ethers5.test.ts +4 -1
  17. package/adapters/test/ethers6.test.ts +4 -1
  18. package/adapters/test/wagmi.test.ts +5 -2
  19. package/chains/chains/stagingCofhe.ts +21 -0
  20. package/chains/index.ts +3 -1
  21. package/chains/test/chains.test.ts +2 -1
  22. package/core/acps.ts +625 -0
  23. package/core/client.ts +136 -39
  24. package/core/clientTypes.ts +52 -41
  25. package/core/config.ts +66 -5
  26. package/core/debug.ts +72 -0
  27. package/core/decrypt/MockThresholdNetworkAbi.ts +20 -11
  28. package/core/decrypt/apiError.ts +104 -0
  29. package/core/decrypt/cofheMocksDecryptForTx.ts +11 -11
  30. package/core/decrypt/cofheMocksDecryptForView.ts +7 -7
  31. package/core/decrypt/decryptForTxBuilder.ts +102 -102
  32. package/core/decrypt/decryptForViewBuilder.ts +90 -90
  33. package/core/decrypt/submitRetry.ts +38 -30
  34. package/core/decrypt/tnDecryptV1.ts +5 -5
  35. package/core/decrypt/tnDecryptV2.ts +45 -34
  36. package/core/decrypt/tnSealOutputV1.ts +4 -3
  37. package/core/decrypt/tnSealOutputV2.ts +44 -31
  38. package/core/encrypt/cofheMocksZkVerifySign.ts +59 -74
  39. package/core/encrypt/encryptInputsBuilder.ts +86 -52
  40. package/core/encrypt/zkPackProveVerify.ts +25 -18
  41. package/core/error.ts +34 -6
  42. package/core/index.ts +14 -14
  43. package/core/test/acpDefaults.test.ts +52 -0
  44. package/core/test/acps.test.ts +596 -0
  45. package/core/test/apiError.test.ts +130 -0
  46. package/core/test/client.test.ts +22 -19
  47. package/core/test/config.test.ts +25 -5
  48. package/core/test/decrypt.test.ts +40 -35
  49. package/core/test/decryptBuilders.test.ts +68 -68
  50. package/core/test/decryptErrorCodes.test.ts +217 -0
  51. package/core/test/encryptInputsBuilder.test.ts +72 -41
  52. package/core/test/pollCallbacks.test.ts +89 -18
  53. package/core/test/stagingRedirect.ts +18 -0
  54. package/core/test/submitRetry.test.ts +182 -0
  55. package/core/types.ts +9 -69
  56. package/dist/acp-Wi6isVQI.d.cts +407 -0
  57. package/dist/acp-Wi6isVQI.d.ts +407 -0
  58. package/dist/acps.cjs +1081 -0
  59. package/dist/acps.d.cts +480 -0
  60. package/dist/acps.d.ts +480 -0
  61. package/dist/acps.js +2 -0
  62. package/dist/chains.cjs +14 -1
  63. package/dist/chains.d.cts +30 -1
  64. package/dist/chains.d.ts +30 -1
  65. package/dist/chains.js +1 -1
  66. package/dist/{chunk-PE5V5CCV.js → chunk-43USWPEH.js} +1000 -604
  67. package/dist/{chunk-MTRAXQXC.js → chunk-N6IDQRRU.js} +14 -2
  68. package/dist/chunk-Q7CBWGQX.js +1029 -0
  69. package/dist/{clientTypes-CyUvRRzA.d.ts → clientTypes-BN3nbzYM.d.ts} +342 -165
  70. package/dist/{clientTypes-BDy1qIBu.d.cts → clientTypes-CYZjFznO.d.cts} +342 -165
  71. package/dist/core.cjs +1431 -1026
  72. package/dist/core.d.cts +55 -10
  73. package/dist/core.d.ts +55 -10
  74. package/dist/core.js +3 -3
  75. package/dist/node.cjs +1356 -976
  76. package/dist/node.d.cts +2 -2
  77. package/dist/node.d.ts +2 -2
  78. package/dist/node.js +3 -3
  79. package/dist/web.cjs +1356 -976
  80. package/dist/web.d.cts +2 -2
  81. package/dist/web.d.ts +2 -2
  82. package/dist/web.js +3 -3
  83. package/node/test/inherited.test.ts +75 -65
  84. package/node/test/tfheinit.test.ts +23 -8
  85. package/package.json +6 -6
  86. package/web/test/client.web.test.ts +5 -1
  87. package/web/test/inherited.web.test.ts +75 -65
  88. package/web/test/tfheinit.web.test.ts +14 -5
  89. package/web/test/worker.config.web.test.ts +38 -23
  90. package/web/test/worker.output.web.test.ts +25 -24
  91. package/core/encrypt/encryptUtils.ts +0 -67
  92. package/core/permits.ts +0 -206
  93. package/core/test/permits.test.ts +0 -534
  94. package/dist/chunk-VB62WYPL.js +0 -978
  95. package/dist/permit-DnVMDT5h.d.cts +0 -376
  96. package/dist/permit-DnVMDT5h.d.ts +0 -376
  97. package/dist/permits.cjs +0 -1026
  98. package/dist/permits.d.cts +0 -353
  99. package/dist/permits.d.ts +0 -353
  100. package/dist/permits.js +0 -2
  101. package/permits/index.ts +0 -68
  102. package/permits/permit.ts +0 -385
  103. package/permits/sealing.ts +0 -131
  104. package/permits/signature.ts +0 -79
  105. package/permits/store.ts +0 -157
  106. package/permits/test/localstorage.test.ts +0 -113
  107. package/permits/test/permit.test.ts +0 -557
  108. package/permits/test/sealing.test.ts +0 -84
  109. package/permits/test/store.test.ts +0 -88
  110. package/permits/test/validation.test.ts +0 -361
  111. package/permits/test-utils.ts +0 -28
  112. package/permits/types.ts +0 -204
  113. package/permits/validation.ts +0 -327
  114. /package/{permits → acps}/utils.ts +0 -0
@@ -0,0 +1,596 @@
1
+ /**
2
+ * @vitest-environment happy-dom
3
+ */
4
+ import { acpStore } from '@/acps';
5
+
6
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
7
+
8
+ // ACP domain resolution requires an upgraded on-chain ACL (domain ("ACL","2") served by the ACL itself).
9
+ // Public testnets still run the V2 contracts, so the domain fetch is stubbed here —
10
+ // these tests cover the acp orchestration flow, not on-chain domain resolution
11
+ // (exercised e2e against mocks in test/hardhat-plugin-test).
12
+ vi.mock('../../acps/onchain-utils.js', async (importOriginal) => ({
13
+ ...(await importOriginal<typeof import('../../acps/onchain-utils.js')>()),
14
+ getAclEIP712Domain: async () => ({
15
+ name: 'ACL',
16
+ version: '2',
17
+ chainId: 421614,
18
+ // arbitrary non-zero address — signatures are never verified on-chain in these tests
19
+ verifyingContract: '0x1111111111111111111111111111111111111111' as `0x${string}`,
20
+ }),
21
+ }));
22
+ import { createPublicClient, createWalletClient, http, type PublicClient, type WalletClient } from 'viem';
23
+ import { arbitrumSepolia } from 'viem/chains';
24
+ import { privateKeyToAccount } from 'viem/accounts';
25
+ import { acps } from '../acps.js';
26
+
27
+ // Type declarations for happy-dom environment
28
+ declare const localStorage: {
29
+ clear: () => void;
30
+ getItem: (name: string) => string | null;
31
+ setItem: (name: string, value: string) => void;
32
+ };
33
+
34
+ // Test private keys (well-known test keys from Anvil/Hardhat)
35
+ const BOB_PRIVATE_KEY = '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80'; // Bob - always issuer
36
+ const ALICE_PRIVATE_KEY = '0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d'; // Alice - always recipient
37
+
38
+ // Create real viem clients for Arbitrum Sepolia
39
+ const publicClient: PublicClient = createPublicClient({
40
+ chain: arbitrumSepolia,
41
+ transport: http(),
42
+ });
43
+
44
+ const bobWalletClient: WalletClient = createWalletClient({
45
+ chain: arbitrumSepolia,
46
+ transport: http(),
47
+ account: privateKeyToAccount(BOB_PRIVATE_KEY),
48
+ });
49
+
50
+ const aliceWalletClient: WalletClient = createWalletClient({
51
+ chain: arbitrumSepolia,
52
+ transport: http(),
53
+ account: privateKeyToAccount(ALICE_PRIVATE_KEY),
54
+ });
55
+
56
+ // Helper to get the wallet addresses
57
+ const bobAddress = bobWalletClient.account!.address;
58
+ const aliceAddress = aliceWalletClient.account!.address;
59
+ const chainId = 421614; // Arbitrum Sepolia
60
+
61
+ describe('Core ACPs Tests', () => {
62
+ beforeEach(() => {
63
+ // Clear localStorage and reset stores
64
+ localStorage.clear();
65
+ acpStore.store.setState({ acps: {}, activeACPHash: {} });
66
+ });
67
+
68
+ afterEach(() => {
69
+ localStorage.clear();
70
+ acpStore.store.setState({ acps: {}, activeACPHash: {} });
71
+ });
72
+
73
+ describe('ACP Creation', () => {
74
+ it('should create and store self acp', async () => {
75
+ const acp = await acps.createSelf({ name: 'Test Self ACP', issuer: bobAddress }, publicClient, bobWalletClient);
76
+
77
+ expect(acp).toBeDefined();
78
+ expect(acp.name).toBe('Test Self ACP');
79
+ expect(acp.type).toBe('self');
80
+ expect(acp.issuer).toBe(bobAddress);
81
+ expect(acp.issuerSignature).toBeDefined();
82
+ expect(acp.issuerSignature).not.toBe('0x');
83
+
84
+ // Verify localStorage
85
+ const storedData = localStorage.getItem('cofhesdk-acps');
86
+ expect(storedData).toBeDefined();
87
+ const parsedData = JSON.parse(storedData!);
88
+ expect(parsedData.state.acps[chainId][bobAddress]).toBeDefined();
89
+ expect(parsedData.state.activeACPHash[chainId][bobAddress]).toBeDefined();
90
+ });
91
+
92
+ it('should create and store sharing acp', async () => {
93
+ const acp = await acps.createSharing(
94
+ {
95
+ name: 'Test Sharing ACP',
96
+ issuer: bobAddress,
97
+ recipient: aliceAddress,
98
+ },
99
+ publicClient,
100
+ bobWalletClient
101
+ );
102
+
103
+ expect(acp.name).toBe('Test Sharing ACP');
104
+ expect(acp.type).toBe('sharing');
105
+ expect(acp.issuer).toBe(bobAddress);
106
+ expect(acp.recipient).toBe(aliceAddress);
107
+ expect(acp.issuerSignature).toBeDefined();
108
+ expect(acp.issuerSignature).not.toBe('0x');
109
+
110
+ // Verify localStorage: the sharing acp is stored, but it must NOT become the issuer's
111
+ // active acp (it's delegated to the recipient).
112
+ const storedData = localStorage.getItem('cofhesdk-acps');
113
+ expect(storedData).toBeDefined();
114
+ const parsedData = JSON.parse(storedData!);
115
+ expect(parsedData.state.acps[chainId][bobAddress]).toBeDefined();
116
+ expect(parsedData.state.activeACPHash[chainId]?.[bobAddress]).toBeUndefined();
117
+ });
118
+
119
+ it('should import shared acp from JSON string', async () => {
120
+ // First create a sharing acp to import
121
+ const sharingACP = await acps.createSharing(
122
+ {
123
+ name: 'Original Sharing ACP',
124
+ issuer: bobAddress,
125
+ recipient: aliceAddress,
126
+ },
127
+ publicClient,
128
+ bobWalletClient
129
+ );
130
+
131
+ // Export the acp as JSON string
132
+ const acpJson = JSON.stringify({
133
+ name: sharingACP.name,
134
+ type: sharingACP.type,
135
+ issuer: sharingACP.issuer,
136
+ expiration: sharingACP.expiration,
137
+ recipient: sharingACP.recipient,
138
+ revokerData: sharingACP.revokerData,
139
+ revokerContract: sharingACP.revokerContract,
140
+ issuerSignature: sharingACP.issuerSignature,
141
+ });
142
+
143
+ // Import the acp as Alice (recipient)
144
+ const acp = await acps.importShared(acpJson, publicClient, aliceWalletClient);
145
+
146
+ expect(acp.name).toBe('Original Sharing ACP');
147
+ expect(acp.type).toBe('recipient');
148
+ expect(acp.issuer).toBe(bobAddress);
149
+ expect(acp.recipient).toBe(aliceAddress);
150
+ expect(acp.recipientSignature).toBeDefined();
151
+ expect(acp.recipientSignature).not.toBe('0x');
152
+ });
153
+ });
154
+
155
+ describe('ACP Retrieval', () => {
156
+ let createdACP: any;
157
+ let acpHash: string;
158
+
159
+ beforeEach(async () => {
160
+ // Create a real acp for testing
161
+ createdACP = await acps.createSelf({ name: 'Test ACP', issuer: bobAddress }, publicClient, bobWalletClient);
162
+ acpHash = createdACP.hash;
163
+ });
164
+
165
+ it('should get acp by hash', async () => {
166
+ const acp = await acps.getACP(chainId, bobAddress, acpHash);
167
+ expect(acp?.name).toBe('Test ACP');
168
+ expect(acp?.type).toBe('self');
169
+ });
170
+
171
+ it('should get all acps', async () => {
172
+ const allACPs = await acps.getACPs(chainId, bobAddress);
173
+ expect(Object.keys(allACPs).length).toBeGreaterThan(0);
174
+ });
175
+
176
+ it('should get active acp', async () => {
177
+ const acp = await acps.getActiveACP(chainId, bobAddress);
178
+ expect(acp?.name).toBe('Test ACP');
179
+ });
180
+
181
+ it('should get active acp hash', async () => {
182
+ const hash = await acps.getActiveACPHash(chainId, bobAddress);
183
+ expect(typeof hash).toBe('string');
184
+ });
185
+ });
186
+
187
+ describe('localStorage Integration', () => {
188
+ it('should persist acps to localStorage', async () => {
189
+ const createdACP = await acps.createSelf({ name: 'Test ACP', issuer: bobAddress }, publicClient, bobWalletClient);
190
+
191
+ const storedData = localStorage.getItem('cofhesdk-acps');
192
+ expect(storedData).toBeDefined();
193
+
194
+ const parsedData = JSON.parse(storedData!);
195
+ expect(parsedData.state.acps[chainId][bobAddress]).toBeDefined();
196
+ expect(parsedData.state.activeACPHash[chainId][bobAddress]).toBeDefined();
197
+
198
+ // Verify the acp data structure
199
+ const acpKeys = Object.keys(parsedData.state.acps[chainId][bobAddress]);
200
+ expect(acpKeys.length).toBeGreaterThan(0);
201
+
202
+ const serializedACP = acps.serialize(createdACP);
203
+ expect(parsedData.state.acps[chainId][bobAddress][createdACP.hash]).toEqual(serializedACP);
204
+ });
205
+ });
206
+
207
+ describe('Real Network Integration', () => {
208
+ it('should create acp with real EIP712 domain from Arbitrum Sepolia', async () => {
209
+ const acp = await acps.createSelf(
210
+ { name: 'Real Network ACP', issuer: bobAddress },
211
+ publicClient,
212
+ bobWalletClient
213
+ );
214
+
215
+ expect(acp._signedDomain).toBeDefined();
216
+ expect(acp._signedDomain?.chainId).toBe(chainId);
217
+ expect(acp._signedDomain?.name).toBeDefined();
218
+ expect(acp._signedDomain?.version).toBeDefined();
219
+ expect(acp._signedDomain?.verifyingContract).toBeDefined();
220
+ });
221
+
222
+ it('should handle multiple acps on real network', async () => {
223
+ // Create multiple acps
224
+ await acps.createSelf({ name: 'ACP 1', issuer: bobAddress }, publicClient, bobWalletClient);
225
+ await acps.createSharing(
226
+ {
227
+ name: 'ACP 2',
228
+ issuer: bobAddress,
229
+ recipient: aliceAddress,
230
+ },
231
+ publicClient,
232
+ bobWalletClient
233
+ );
234
+
235
+ // Verify both acps exist
236
+ const allACPs = await acps.getACPs(chainId, bobAddress);
237
+ expect(Object.keys(allACPs).length).toBeGreaterThanOrEqual(2);
238
+
239
+ // The self acp stays active — creating the sharing (delegated) acp must not change it.
240
+ const activeACP = await acps.getActiveACP(chainId, bobAddress);
241
+ expect(activeACP?.name).toBe('ACP 1');
242
+ });
243
+ });
244
+
245
+ describe('getOrCreateSelfACP', () => {
246
+ it('should create a new self acp when none exists', async () => {
247
+ const acp = await acps.getOrCreateSelfACP(publicClient, bobWalletClient, chainId, bobAddress, {
248
+ issuer: bobAddress,
249
+ name: 'New Self ACP',
250
+ });
251
+
252
+ expect(acp).toBeDefined();
253
+ expect(acp.name).toBe('New Self ACP');
254
+ expect(acp.type).toBe('self');
255
+ expect(acp.issuer).toBe(bobAddress);
256
+ expect(acp.issuerSignature).toBeDefined();
257
+
258
+ // Verify it was stored and set as active
259
+ const activeACP = await acps.getActiveACP(chainId, bobAddress);
260
+ expect(activeACP?.name).toBe('New Self ACP');
261
+ });
262
+
263
+ it('should return existing self acp when one exists', async () => {
264
+ // Create an initial self acp
265
+ const firstACP = await acps.createSelf(
266
+ { name: 'First Self ACP', issuer: bobAddress },
267
+ publicClient,
268
+ bobWalletClient
269
+ );
270
+
271
+ // Call getOrCreateSelfACP - should return existing
272
+ const acp = await acps.getOrCreateSelfACP(publicClient, bobWalletClient, chainId, bobAddress, {
273
+ issuer: bobAddress,
274
+ name: 'Should Not Create This',
275
+ });
276
+
277
+ expect(acp.name).toBe('First Self ACP');
278
+ expect(acp.hash).toBe(firstACP.hash);
279
+
280
+ // Verify no new acp was created
281
+ const allACPs = await acps.getACPs(chainId, bobAddress);
282
+ expect(Object.keys(allACPs).length).toBe(1);
283
+ });
284
+
285
+ it('should create new self acp when active acp is sharing type', async () => {
286
+ // Create a sharing acp first
287
+ await acps.createSharing(
288
+ {
289
+ name: 'Sharing ACP',
290
+ issuer: bobAddress,
291
+ recipient: aliceAddress,
292
+ },
293
+ publicClient,
294
+ bobWalletClient
295
+ );
296
+
297
+ // Call getOrCreateSelfACP - should create new since active is sharing type
298
+ const acp = await acps.getOrCreateSelfACP(publicClient, bobWalletClient, chainId, bobAddress, {
299
+ issuer: bobAddress,
300
+ name: 'New Self ACP',
301
+ });
302
+
303
+ expect(acp.name).toBe('New Self ACP');
304
+ expect(acp.type).toBe('self');
305
+
306
+ // Verify two acps exist now
307
+ const allACPs = await acps.getACPs(chainId, bobAddress);
308
+ expect(Object.keys(allACPs).length).toBe(2);
309
+ });
310
+
311
+ it('should create a new self acp when active acp is expired', async () => {
312
+ // Create an expired self acp (expiration in the past)
313
+ const expiredACP = await acps.createSelf(
314
+ { name: 'Expired Self ACP', issuer: bobAddress, expiration: Math.floor(Date.now() / 1000) - 3600 },
315
+ publicClient,
316
+ bobWalletClient
317
+ );
318
+
319
+ // Sanity check - it is the active acp and is expired
320
+ const activeBefore = await acps.getActiveACP(chainId, bobAddress);
321
+ expect(activeBefore?.hash).toBe(expiredACP.hash);
322
+
323
+ // getOrCreateSelfACP should treat the expired acp as missing and create a fresh one
324
+ const acp = await acps.getOrCreateSelfACP(publicClient, bobWalletClient, chainId, bobAddress, {
325
+ issuer: bobAddress,
326
+ name: 'Fresh Self ACP',
327
+ });
328
+
329
+ expect(acp.name).toBe('Fresh Self ACP');
330
+ expect(acp.type).toBe('self');
331
+ expect(acp.hash).not.toBe(expiredACP.hash);
332
+
333
+ // The fresh acp should now be active
334
+ const activeAfter = await acps.getActiveACP(chainId, bobAddress);
335
+ expect(activeAfter?.hash).toBe(acp.hash);
336
+ });
337
+
338
+ it('should use default options when none provided', async () => {
339
+ const acp = await acps.getOrCreateSelfACP(publicClient, bobWalletClient, chainId, bobAddress);
340
+
341
+ expect(acp).toBeDefined();
342
+ expect(acp.type).toBe('self');
343
+ expect(acp.issuer).toBe(bobAddress);
344
+ expect(acp.name).toBe('Autogenerated Self ACP');
345
+ });
346
+
347
+ it('should use default chainId and account when not provided', async () => {
348
+ const acp = await acps.getOrCreateSelfACP(publicClient, bobWalletClient, undefined, undefined, {
349
+ issuer: bobAddress,
350
+ name: 'Test ACP',
351
+ });
352
+
353
+ expect(acp).toBeDefined();
354
+ expect(acp.issuer).toBe(bobAddress);
355
+
356
+ // Verify it was stored with the chain's actual chainId
357
+ const activeACP = await acps.getActiveACP(chainId, bobAddress);
358
+ expect(activeACP?.name).toBe('Test ACP');
359
+ });
360
+ });
361
+
362
+ describe('getOrCreateSharingACP', () => {
363
+ it('should create a new sharing acp when none exists', async () => {
364
+ const acp = await acps.getOrCreateSharingACP(
365
+ publicClient,
366
+ bobWalletClient,
367
+ {
368
+ issuer: bobAddress,
369
+ recipient: aliceAddress,
370
+ name: 'New Sharing ACP',
371
+ },
372
+ chainId,
373
+ bobAddress
374
+ );
375
+
376
+ expect(acp).toBeDefined();
377
+ expect(acp.name).toBe('New Sharing ACP');
378
+ expect(acp.type).toBe('sharing');
379
+ expect(acp.issuer).toBe(bobAddress);
380
+ expect(acp.recipient).toBe(aliceAddress);
381
+ expect(acp.issuerSignature).toBeDefined();
382
+
383
+ // Stored but NOT activated — a sharing acp is delegated, never the issuer's active acp.
384
+ const allACPs = await acps.getACPs(chainId, bobAddress);
385
+ expect(allACPs[acp.hash]?.name).toBe('New Sharing ACP');
386
+ expect(await acps.getActiveACP(chainId, bobAddress)).toBeUndefined();
387
+ });
388
+
389
+ it('should return existing sharing acp when one exists', async () => {
390
+ // Create an initial sharing acp, then make it active by hand (sharing acps are not
391
+ // auto-activated), so getOrCreateSharingACP reuses it instead of creating another.
392
+ const firstACP = await acps.createSharing(
393
+ {
394
+ name: 'First Sharing ACP',
395
+ issuer: bobAddress,
396
+ recipient: aliceAddress,
397
+ },
398
+ publicClient,
399
+ bobWalletClient
400
+ );
401
+ acps.selectActiveACP(chainId, bobAddress, firstACP.hash);
402
+
403
+ // Call getOrCreateSharingACP - should return existing
404
+ const acp = await acps.getOrCreateSharingACP(
405
+ publicClient,
406
+ bobWalletClient,
407
+ {
408
+ issuer: bobAddress,
409
+ recipient: aliceAddress,
410
+ name: 'Should Not Create This',
411
+ },
412
+ chainId,
413
+ bobAddress
414
+ );
415
+
416
+ expect(acp.name).toBe('First Sharing ACP');
417
+ expect(acp.hash).toBe(firstACP.hash);
418
+
419
+ // Verify no new acp was created
420
+ const allACPs = await acps.getACPs(chainId, bobAddress);
421
+ expect(Object.keys(allACPs).length).toBe(1);
422
+ });
423
+
424
+ it('should create new sharing acp when active acp is self type', async () => {
425
+ // Create a self acp first
426
+ await acps.createSelf({ name: 'Self ACP', issuer: bobAddress }, publicClient, bobWalletClient);
427
+
428
+ // Call getOrCreateSharingACP - should create new since active is self type
429
+ const acp = await acps.getOrCreateSharingACP(
430
+ publicClient,
431
+ bobWalletClient,
432
+ {
433
+ issuer: bobAddress,
434
+ recipient: aliceAddress,
435
+ name: 'New Sharing ACP',
436
+ },
437
+ chainId,
438
+ bobAddress
439
+ );
440
+
441
+ expect(acp.name).toBe('New Sharing ACP');
442
+ expect(acp.type).toBe('sharing');
443
+
444
+ // Verify two acps exist now
445
+ const allACPs = await acps.getACPs(chainId, bobAddress);
446
+ expect(Object.keys(allACPs).length).toBe(2);
447
+ });
448
+
449
+ it('should create a new sharing acp when active acp is expired', async () => {
450
+ // Create an expired sharing acp and make it the active acp (by hand — sharing acps
451
+ // aren't auto-activated), so the "active acp is expired" branch is exercised.
452
+ const expiredACP = await acps.createSharing(
453
+ {
454
+ name: 'Expired Sharing ACP',
455
+ issuer: bobAddress,
456
+ recipient: aliceAddress,
457
+ expiration: Math.floor(Date.now() / 1000) - 3600,
458
+ },
459
+ publicClient,
460
+ bobWalletClient
461
+ );
462
+ acps.selectActiveACP(chainId, bobAddress, expiredACP.hash);
463
+
464
+ // getOrCreateSharingACP should treat the expired acp as missing and create a fresh one
465
+ const acp = await acps.getOrCreateSharingACP(
466
+ publicClient,
467
+ bobWalletClient,
468
+ {
469
+ issuer: bobAddress,
470
+ recipient: aliceAddress,
471
+ name: 'Fresh Sharing ACP',
472
+ },
473
+ chainId,
474
+ bobAddress
475
+ );
476
+
477
+ expect(acp.name).toBe('Fresh Sharing ACP');
478
+ expect(acp.type).toBe('sharing');
479
+ expect(acp.hash).not.toBe(expiredACP.hash);
480
+
481
+ // The fresh sharing acp is stored (but not auto-activated).
482
+ const allACPs = await acps.getACPs(chainId, bobAddress);
483
+ expect(allACPs[acp.hash]).toBeDefined();
484
+ });
485
+
486
+ it('should use default chainId and account when not provided', async () => {
487
+ const acp = await acps.getOrCreateSharingACP(
488
+ publicClient,
489
+ bobWalletClient,
490
+ {
491
+ issuer: bobAddress,
492
+ recipient: aliceAddress,
493
+ name: 'Test Sharing ACP',
494
+ },
495
+ undefined,
496
+ undefined
497
+ );
498
+
499
+ expect(acp).toBeDefined();
500
+ expect(acp.issuer).toBe(bobAddress);
501
+ expect(acp.recipient).toBe(aliceAddress);
502
+
503
+ // Stored under the connected wallet's chainId/account (not activated).
504
+ const allACPs = await acps.getACPs(chainId, bobAddress);
505
+ expect(allACPs[acp.hash]?.name).toBe('Test Sharing ACP');
506
+ });
507
+ });
508
+
509
+ describe('Export', () => {
510
+ it('throws when exporting a self acp', async () => {
511
+ const acp = await acps.createSelf({ name: 'Test Self ACP', issuer: bobAddress }, publicClient, bobWalletClient);
512
+
513
+ // export includes the issuer signature — only sharing acps are exportable
514
+ expect(() => acps.export(acp)).toThrow(/only 'sharing' ACPs are exportable/);
515
+ });
516
+
517
+ it('should export sharing acp data with recipient and issuerSignature', async () => {
518
+ const acp = await acps.createSharing(
519
+ {
520
+ name: 'Test Sharing ACP',
521
+ issuer: bobAddress,
522
+ recipient: aliceAddress,
523
+ },
524
+ publicClient,
525
+ bobWalletClient
526
+ );
527
+
528
+ const exported = acps.export(acp);
529
+ const parsed = JSON.parse(exported);
530
+
531
+ expect(parsed.name).toBe('Test Sharing ACP');
532
+ expect(parsed.type).toBe('sharing');
533
+ expect(parsed.issuer).toBe(bobAddress);
534
+ expect(parsed.recipient).toBe(aliceAddress);
535
+ expect(parsed.issuerSignature).toBeDefined();
536
+ expect(parsed.issuerSignature).not.toBe('0x');
537
+ expect(parsed).not.toHaveProperty('sealingPair');
538
+ });
539
+ });
540
+
541
+ describe('getOrCreate - Multiple Types Scenarios', () => {
542
+ it('should keep the self acp active when a sharing acp is also created', async () => {
543
+ // Create self acp (this one activates)
544
+ const selfACP = await acps.getOrCreateSelfACP(publicClient, bobWalletClient, chainId, bobAddress, {
545
+ issuer: bobAddress,
546
+ name: 'Self ACP',
547
+ });
548
+ expect(selfACP.type).toBe('self');
549
+
550
+ // Create sharing acp (creates a new one, but does NOT activate it)
551
+ const sharingACP = await acps.getOrCreateSharingACP(
552
+ publicClient,
553
+ bobWalletClient,
554
+ {
555
+ issuer: bobAddress,
556
+ recipient: aliceAddress,
557
+ name: 'Sharing ACP',
558
+ },
559
+ chainId,
560
+ bobAddress
561
+ );
562
+ expect(sharingACP.type).toBe('sharing');
563
+
564
+ // Both should exist
565
+ const allACPs = await acps.getACPs(chainId, bobAddress);
566
+ expect(Object.keys(allACPs).length).toBe(2);
567
+
568
+ // The active acp stays the self acp — creating the sharing acp doesn't hijack it.
569
+ const activeACP = await acps.getActiveACP(chainId, bobAddress);
570
+ expect(activeACP?.type).toBe('self');
571
+ expect(activeACP?.name).toBe('Self ACP');
572
+ });
573
+
574
+ it('should correctly handle sequential getOrCreate calls', async () => {
575
+ // First call - creates new
576
+ const acp1 = await acps.getOrCreateSelfACP(publicClient, bobWalletClient, chainId, bobAddress, {
577
+ issuer: bobAddress,
578
+ name: 'ACP 1',
579
+ });
580
+
581
+ // Second call - returns existing
582
+ const acp2 = await acps.getOrCreateSelfACP(publicClient, bobWalletClient, chainId, bobAddress, {
583
+ issuer: bobAddress,
584
+ name: 'ACP 2',
585
+ });
586
+
587
+ // Should be the same acp
588
+ expect(acp1.hash).toBe(acp2.hash);
589
+ expect(acp2.name).toBe('ACP 1'); // Original name
590
+
591
+ // Only one acp should exist
592
+ const allACPs = await acps.getACPs(chainId, bobAddress);
593
+ expect(Object.keys(allACPs).length).toBe(1);
594
+ });
595
+ });
596
+ });