@cofhe/sdk 0.7.1 → 1.0.0-alpha.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 (123) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/acps/acp.ts +35 -7
  3. package/acps/index.ts +28 -12
  4. package/acps/onchain-utils.ts +188 -49
  5. package/acps/registry.ts +231 -0
  6. package/acps/sealing.ts +9 -3
  7. package/acps/share-metadata/appKey.ts +8 -0
  8. package/acps/share-metadata/decode.ts +120 -0
  9. package/acps/share-metadata/describe.ts +198 -0
  10. package/acps/share-metadata/encode.ts +147 -0
  11. package/acps/share-metadata/erc7984.ts +26 -0
  12. package/acps/share-metadata/index.ts +9 -0
  13. package/acps/share-metadata/layout.ts +69 -0
  14. package/acps/share-metadata/presets.ts +116 -0
  15. package/acps/share-metadata/schema.ts +47 -0
  16. package/acps/share-metadata/test/codec.test.ts +140 -0
  17. package/acps/share-metadata/test/describe.test.ts +112 -0
  18. package/acps/share-metadata/test/fixtures.ts +157 -0
  19. package/acps/share-metadata/test/live.test.ts +56 -0
  20. package/acps/share-metadata/test/presets.test.ts +119 -0
  21. package/acps/share-metadata/test/verify.test.ts +144 -0
  22. package/acps/share-metadata/verify.ts +155 -0
  23. package/acps/store.ts +48 -18
  24. package/acps/test/acp.test.ts +102 -7
  25. package/acps/test/localstorage.test.ts +28 -1
  26. package/acps/test/onchainAccessStatus.test.ts +116 -0
  27. package/acps/test/registry.test.ts +144 -0
  28. package/acps/test/sealing.test.ts +23 -0
  29. package/acps/test/store.test.ts +71 -0
  30. package/acps/test/utils.test.ts +63 -0
  31. package/acps/test/validation.test.ts +0 -4
  32. package/acps/types.ts +59 -6
  33. package/acps/utils.ts +15 -2
  34. package/acps/validation.ts +3 -1
  35. package/chains/chains/arbitrum.ts +14 -0
  36. package/chains/chains/ethereum.ts +14 -0
  37. package/chains/index.ts +5 -1
  38. package/chains/test/chains.test.ts +24 -3
  39. package/core/acps.ts +164 -207
  40. package/core/client.ts +44 -13
  41. package/core/clientTypes.ts +50 -7
  42. package/core/config.ts +33 -1
  43. package/core/consts.ts +14 -2
  44. package/core/decrypt/apiError.ts +4 -1
  45. package/core/decrypt/verifyDecryptResult.ts +8 -4
  46. package/core/encrypt/cofheMocksZkVerifySign.ts +19 -2
  47. package/core/encrypt/encryptInputsBuilder.ts +6 -0
  48. package/core/encrypt/zkPackProveVerify.ts +7 -1
  49. package/core/error.ts +1 -0
  50. package/core/fetchKeys.ts +2 -2
  51. package/core/index.ts +9 -1
  52. package/core/keyStore.ts +23 -10
  53. package/core/taskManager.ts +48 -0
  54. package/core/test/acpDefaults.test.ts +108 -19
  55. package/core/test/acpStoreAccountCase.test.ts +80 -0
  56. package/core/test/acps.test.ts +128 -3
  57. package/core/test/apiError.test.ts +8 -0
  58. package/core/test/appKeyConfig.test.ts +94 -0
  59. package/core/test/client.test.ts +41 -0
  60. package/core/test/config.test.ts +24 -0
  61. package/core/test/decrypt.test.ts +11 -4
  62. package/core/test/decryptBuilders.test.ts +110 -2
  63. package/core/test/encryptInputsBuilder.test.ts +1 -1
  64. package/core/test/fetchKeys.test.ts +23 -2
  65. package/core/test/incomingShares.test.ts +84 -0
  66. package/core/test/keyStore.test.ts +18 -6
  67. package/core/test/mocksEncryptRange.test.ts +90 -0
  68. package/core/test/taskManager.test.ts +57 -0
  69. package/core/test/utils.test.ts +128 -0
  70. package/core/test/verifyDecryptResult.test.ts +51 -0
  71. package/core/types.ts +8 -0
  72. package/core/utils.ts +16 -9
  73. package/dist/acps.cjs +984 -64
  74. package/dist/acps.d.cts +589 -10
  75. package/dist/acps.d.ts +589 -10
  76. package/dist/acps.js +2 -2
  77. package/dist/chains.cjs +26 -0
  78. package/dist/chains.d.cts +45 -1
  79. package/dist/chains.d.ts +45 -1
  80. package/dist/chains.js +1 -1
  81. package/dist/chunk-6ZW76ZKB.js +228 -0
  82. package/dist/chunk-BQWHPIYU.js +1828 -0
  83. package/dist/chunk-KSOV5B4B.js +50 -0
  84. package/dist/{chunk-43USWPEH.js → chunk-OL3DZN7A.js} +159 -523
  85. package/dist/{chunk-N6IDQRRU.js → chunk-UKQAJK4N.js} +25 -1
  86. package/dist/chunk-XQ32JG3T.js +174 -0
  87. package/dist/{clientTypes-CYZjFznO.d.cts → clientTypes-C0A9ji5O.d.cts} +132 -29
  88. package/dist/{clientTypes-BN3nbzYM.d.ts → clientTypes-CcU5WvdV.d.ts} +132 -29
  89. package/dist/core.cjs +3269 -2626
  90. package/dist/core.d.cts +32 -8
  91. package/dist/core.d.ts +32 -8
  92. package/dist/core.js +5 -4
  93. package/dist/node.cjs +3074 -2436
  94. package/dist/node.d.cts +2 -2
  95. package/dist/node.d.ts +2 -2
  96. package/dist/node.js +20 -19
  97. package/dist/{acp-Wi6isVQI.d.cts → onchain-utils-D9Z33IP7.d.cts} +166 -12
  98. package/dist/{acp-Wi6isVQI.d.ts → onchain-utils-D9Z33IP7.d.ts} +166 -12
  99. package/dist/web.cjs +3204 -2493
  100. package/dist/web.d.cts +49 -3
  101. package/dist/web.d.ts +49 -3
  102. package/dist/web.js +52 -27
  103. package/dist/zkProve.worker.cjs +79 -18
  104. package/dist/zkProve.worker.js +24 -24
  105. package/node/index.ts +1 -10
  106. package/node/storage.ts +20 -9
  107. package/node/test/inherited.test.ts +5 -1
  108. package/node/test/storage-memory-fallback.test.ts +47 -0
  109. package/node/test/storage-permissions.test.ts +40 -0
  110. package/package.json +3 -1
  111. package/web/index.ts +101 -36
  112. package/web/test/countingZkProveWorker.ts +31 -0
  113. package/web/test/inherited.web.test.ts +5 -1
  114. package/web/test/mainThreadPool.web.test.ts +174 -0
  115. package/web/test/threadPoolBenchHelper.ts +134 -0
  116. package/web/test/threadPoolOff.web.test.ts +17 -0
  117. package/web/test/threadPoolOn.web.test.ts +80 -0
  118. package/web/test/workerInitRace.web.test.ts +79 -0
  119. package/web/tfheThreadPool.ts +140 -0
  120. package/web/workerManager.ts +4 -2
  121. package/web/zkProve.worker.ts +42 -26
  122. package/dist/chunk-ESMZCFJY.js +0 -12
  123. package/dist/chunk-Q7CBWGQX.js +0 -1029
@@ -1,5 +1,6 @@
1
- import { describe, it, expect } from 'vitest';
1
+ import { beforeEach, describe, it, expect, vi } from 'vitest';
2
2
  import { acps } from '../acps.js';
3
+ import { clearTaskManagerAddressCache } from '../taskManager.js';
3
4
 
4
5
  type ScopeOpts = {
5
6
  revokerData?: number;
@@ -12,7 +13,10 @@ const opts = (o: ScopeOpts): ScopeOpts => o;
12
13
 
13
14
  const CHAIN = 31337;
14
15
  const REVOKER = '0x00000000000000000000000000000000000000aa' as `0x${string}`;
16
+ const ACL_REVOKER = '0x00000000000000000000000000000000000000cc' as `0x${string}`;
15
17
  const CONTRACT_A = '0x00000000000000000000000000000000000000bb' as `0x${string}`;
18
+ const ACL = '0x00000000000000000000000000000000000000dd' as `0x${string}`;
19
+ const TM = '0x0000000000000000000000000000000000005000' as `0x${string}`;
16
20
 
17
21
  const acpConfig = {
18
22
  defaultRevoker: { [CHAIN]: REVOKER },
@@ -20,33 +24,118 @@ const acpConfig = {
20
24
  };
21
25
 
22
26
  describe('applyACPDefaults', () => {
23
- it('injects default revoker (contract + creation timestamp) when no revoker options given', () => {
24
- const before = Math.round(Date.now() / 1000);
25
- const result = acps.applyACPDefaults(opts({}), acpConfig, CHAIN);
27
+ beforeEach(() => {
28
+ acps.clearAclServedAddresses();
29
+ clearTaskManagerAddressCache();
30
+ });
31
+
32
+ it('uses the exact chain block timestamp for an ACL-served revoker', async () => {
33
+ const latestBlockTimestamp = 1_700_000_000;
34
+ const publicClient = {
35
+ readContract: vi.fn(({ functionName }: { functionName: string }) => {
36
+ if (functionName === 'getTm') return Promise.resolve(TM);
37
+ if (functionName === 'acl') return Promise.resolve(ACL);
38
+ if (functionName === 'defaultRevokerContract') return Promise.resolve(ACL_REVOKER);
39
+ return Promise.reject(new Error(`unexpected ACL read: ${functionName}`));
40
+ }),
41
+ getBlock: vi.fn().mockResolvedValue({ timestamp: BigInt(latestBlockTimestamp) }),
42
+ } as any;
43
+
44
+ const result = await acps.applyACPDefaultsFromChain(opts({}), undefined, publicClient, CHAIN);
45
+
46
+ expect(publicClient.getBlock).toHaveBeenCalledWith();
47
+ expect(result.revokerContract).toBe(ACL_REVOKER);
48
+ expect(result.revokerData).toBe(latestBlockTimestamp);
49
+ });
50
+
51
+ it('prefers configured revoker over ACL but still uses the latest block timestamp', async () => {
52
+ const latestBlockTimestamp = 1_700_000_001;
53
+ const publicClient = {
54
+ readContract: vi.fn(({ functionName }: { functionName: string }) => {
55
+ if (functionName === 'getTm') return Promise.resolve(TM);
56
+ if (functionName === 'acl') return Promise.resolve(ACL);
57
+ if (functionName === 'defaultRevokerContract') return Promise.resolve(ACL_REVOKER);
58
+ return Promise.reject(new Error(`unexpected ACL read: ${functionName}`));
59
+ }),
60
+ getBlock: vi.fn().mockResolvedValue({ timestamp: BigInt(latestBlockTimestamp) }),
61
+ } as any;
62
+
63
+ const result = await acps.applyACPDefaultsFromChain(opts({}), acpConfig, publicClient, CHAIN);
64
+
65
+ expect(publicClient.readContract).not.toHaveBeenCalled();
66
+ expect(publicClient.getBlock).toHaveBeenCalledWith();
26
67
  expect(result.revokerContract).toBe(REVOKER);
27
- expect(result.revokerData).toBeGreaterThanOrEqual(before - 60); // 60s clock-skew backdating
28
- expect(result.revokerData).toBeLessThanOrEqual(Math.round(Date.now() / 1000) - 59);
68
+ expect(result.revokerData).toBe(latestBlockTimestamp);
69
+ });
70
+
71
+ it('avoids getBlock for explicit revoker options', async () => {
72
+ const publicClient = {
73
+ readContract: vi.fn(),
74
+ getBlock: vi.fn(),
75
+ } as any;
76
+
77
+ const result = await acps.applyACPDefaultsFromChain(
78
+ opts({ revokerData: 42, revokerContract: CONTRACT_A }),
79
+ acpConfig,
80
+ publicClient,
81
+ CHAIN
82
+ );
83
+
84
+ expect(publicClient.readContract).not.toHaveBeenCalled();
85
+ expect(publicClient.getBlock).not.toHaveBeenCalled();
86
+ expect(result).toMatchObject({ revokerData: 42, revokerContract: CONTRACT_A });
29
87
  });
30
88
 
31
- it('does not override an explicit revoker pair', () => {
32
- const result = acps.applyACPDefaults({ revokerData: 42, revokerContract: CONTRACT_A }, acpConfig, CHAIN);
33
- expect(result.revokerData).toBe(42);
34
- expect(result.revokerContract).toBe(CONTRACT_A);
89
+ it('avoids getBlock when neither config nor ACL provide a revoker', async () => {
90
+ const publicClient = {
91
+ readContract: vi.fn().mockRejectedValue(new Error('no ACL deployment')),
92
+ getBlock: vi.fn(),
93
+ } as any;
94
+
95
+ const result = await acps.applyACPDefaultsFromChain(opts({}), undefined, publicClient, CHAIN);
96
+
97
+ expect(publicClient.getBlock).not.toHaveBeenCalled();
98
+ expect(result).toEqual({});
35
99
  });
36
100
 
37
- it('injects default contract scopes when no scope options given', () => {
38
- const result = acps.applyACPDefaults(opts({}), acpConfig, CHAIN);
101
+ it('rejects a block timestamp outside JavaScript safe integer range', async () => {
102
+ const publicClient = {
103
+ readContract: vi.fn(({ functionName }: { functionName: string }) => {
104
+ if (functionName === 'getTm') return Promise.resolve(TM);
105
+ if (functionName === 'acl') return Promise.resolve(ACL);
106
+ if (functionName === 'defaultRevokerContract') return Promise.resolve(ACL_REVOKER);
107
+ return Promise.reject(new Error(`unexpected ACL read: ${functionName}`));
108
+ }),
109
+ getBlock: vi.fn().mockResolvedValue({ timestamp: BigInt(Number.MAX_SAFE_INTEGER) + 1n }),
110
+ } as any;
111
+
112
+ await expect(acps.applyACPDefaultsFromChain(opts({}), undefined, publicClient, CHAIN)).rejects.toThrow(
113
+ /ACP revokerData cannot represent the chain block timestamp .* as a safe integer/
114
+ );
115
+ });
116
+
117
+ it('injects default contract scopes when no scope options given', async () => {
118
+ const result = await acps.applyACPDefaultsFromChain(opts({}), acpConfig, noAclClient(), CHAIN);
39
119
  expect(result.contracts).toEqual([CONTRACT_A]);
40
120
  });
41
121
 
42
- it('does not inject scopes when the caller provides any scope option (incl. explicit scope)', () => {
43
- expect(acps.applyACPDefaults(opts({ scope: 0 }), acpConfig, CHAIN).contracts).toBeUndefined();
44
- expect(acps.applyACPDefaults(opts({ handles: [1n] }), acpConfig, CHAIN).contracts).toBeUndefined();
45
- expect(acps.applyACPDefaults(opts({ contracts: [] }), acpConfig, CHAIN).contracts).toEqual([]);
122
+ it('does not inject scopes when the caller provides any scope option (incl. explicit scope)', async () => {
123
+ const apply = (o: ScopeOpts) => acps.applyACPDefaultsFromChain(o, acpConfig, noAclClient(), CHAIN);
124
+ expect((await apply(opts({ scope: 0 }))).contracts).toBeUndefined();
125
+ expect((await apply(opts({ handles: [1n] }))).contracts).toBeUndefined();
126
+ expect((await apply(opts({ contracts: [] }))).contracts).toEqual([]);
46
127
  });
47
128
 
48
- it('no-op for chains without defaults and for missing config', () => {
49
- expect(acps.applyACPDefaults(opts({}), acpConfig, 999)).toEqual({});
50
- expect(acps.applyACPDefaults(opts({}), undefined, CHAIN)).toEqual({});
129
+ it('no-op for chains without defaults and for missing config', async () => {
130
+ expect(await acps.applyACPDefaultsFromChain(opts({}), acpConfig, noAclClient(), 999)).toEqual({});
131
+ expect(await acps.applyACPDefaultsFromChain(opts({}), undefined, noAclClient(), CHAIN)).toEqual({});
51
132
  });
52
133
  });
134
+
135
+ /** A chain with no ACL deployment: the ACL lookup serves no revoker. */
136
+ function noAclClient() {
137
+ return {
138
+ readContract: vi.fn().mockRejectedValue(new Error('no ACL deployment')),
139
+ getBlock: vi.fn().mockResolvedValue({ timestamp: 1_700_000_000n }),
140
+ } as any;
141
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * @vitest-environment happy-dom
3
+ */
4
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
5
+
6
+ // Stub the on-chain EIP-712 domain read so the test needs no RPC (same approach as acps.test.ts).
7
+ vi.mock('../../acps/onchain-utils.js', async (importOriginal) => ({
8
+ ...(await importOriginal<typeof import('../../acps/onchain-utils.js')>()),
9
+ getAclEIP712Domain: async () => ({
10
+ name: 'ACL',
11
+ version: '2',
12
+ chainId: 421614,
13
+ verifyingContract: '0x1111111111111111111111111111111111111111' as `0x${string}`,
14
+ }),
15
+ }));
16
+
17
+ import { custom, createWalletClient, type PublicClient, type WalletClient } from 'viem';
18
+ import { arbitrumSepolia } from 'viem/chains';
19
+ import { privateKeyToAccount } from 'viem/accounts';
20
+ import { acpStore } from '@/acps';
21
+ import { acps } from '../acps.js';
22
+
23
+ const chainId = 421614;
24
+ // Offline clients: the wallet signs typed data locally, and any RPC call fails the test.
25
+ const noRpc = custom({
26
+ request: async ({ method }) => {
27
+ throw new Error(`unexpected RPC call: ${method}`);
28
+ },
29
+ });
30
+ const publicClient = { getChainId: async () => chainId } as unknown as PublicClient;
31
+ const walletClient: WalletClient = createWalletClient({
32
+ chain: arbitrumSepolia,
33
+ transport: noRpc,
34
+ account: privateKeyToAccount('0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80'),
35
+ });
36
+ const checksummed = walletClient.account!.address; // 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
37
+ const lowercase = checksummed.toLowerCase();
38
+
39
+ describe('ACP store account keys are case-insensitive', () => {
40
+ beforeEach(() => {
41
+ acpStore.store.setState({ acps: {}, activeACPHash: {} });
42
+ });
43
+
44
+ it('finds the active ACP when the account is passed in lowercase', async () => {
45
+ const created = await acps.createSelf({ issuer: checksummed }, publicClient, walletClient);
46
+
47
+ expect(acps.getActiveACP(chainId, checksummed)?.hash).toBe(created.hash);
48
+ expect(acps.getActiveACP(chainId, lowercase)?.hash).toBe(created.hash);
49
+ expect(acps.getACP(chainId, lowercase, created.hash)?.hash).toBe(created.hash);
50
+ expect(Object.keys(acps.getACPs(chainId, lowercase))).toEqual([created.hash]);
51
+ });
52
+
53
+ it('getOrCreateSelfACP with a lowercase account reuses the stored ACP instead of signing a new one', async () => {
54
+ const signSpy = vi.spyOn(walletClient, 'signTypedData');
55
+
56
+ const first = await acps.getOrCreateSelfACP(publicClient, walletClient, chainId, lowercase);
57
+ const second = await acps.getOrCreateSelfACP(publicClient, walletClient, chainId, lowercase);
58
+
59
+ expect(second.hash).toBe(first.hash);
60
+ expect(signSpy).toHaveBeenCalledTimes(1);
61
+ expect(Object.keys(acps.getACPs(chainId, checksummed))).toHaveLength(1);
62
+ });
63
+
64
+ it('keeps one bucket per account whatever case it is written and read in', async () => {
65
+ const created = await acps.createSelf({ issuer: checksummed }, publicClient, walletClient);
66
+ acpStore.store.setState({ acps: {}, activeACPHash: {} });
67
+
68
+ // Stored under the lowercase spelling (e.g. a wallet client built from a lowercase address)...
69
+ acpStore.setACP(chainId, lowercase, created);
70
+ acpStore.setActiveACPHash(chainId, lowercase, created.hash);
71
+ // ...still found with the checksummed one, and a write in that case reuses the same bucket.
72
+ expect(acpStore.getActiveACP(chainId, checksummed)?.hash).toBe(created.hash);
73
+ acpStore.setACP(chainId, checksummed, created);
74
+ expect(Object.keys(acpStore.store.getState().acps[chainId])).toEqual([lowercase]);
75
+
76
+ acpStore.removeACP(chainId, checksummed, created.hash);
77
+ expect(acpStore.getActiveACPHash(chainId, lowercase)).toBeUndefined();
78
+ expect(acpStore.getACP(chainId, lowercase, created.hash)).toBeUndefined();
79
+ });
80
+ });
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * @vitest-environment happy-dom
3
3
  */
4
- import { acpStore } from '@/acps';
4
+ import { ACPUtils, acpStore, type IncomingShare, type SharingACP } from '@/acps';
5
5
 
6
6
  import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
7
7
 
@@ -143,7 +143,8 @@ describe('Core ACPs Tests', () => {
143
143
  // Import the acp as Alice (recipient)
144
144
  const acp = await acps.importShared(acpJson, publicClient, aliceWalletClient);
145
145
 
146
- expect(acp.name).toBe('Original Sharing ACP');
146
+ // the issuer's name is not taken over: the acp is named after its issuer
147
+ expect(acp.name).toBe(`Shared by ${bobAddress.slice(0, 6)}…${bobAddress.slice(-4)}`);
147
148
  expect(acp.type).toBe('recipient');
148
149
  expect(acp.issuer).toBe(bobAddress);
149
150
  expect(acp.recipient).toBe(aliceAddress);
@@ -152,6 +153,130 @@ describe('Core ACPs Tests', () => {
152
153
  });
153
154
  });
154
155
 
156
+ describe('checkAccess on the issuer copy of a share', () => {
157
+ it('is valid while unexpired and not revoked, without needing the recipient signature', async () => {
158
+ const sharing = await acps.createSharing(
159
+ { name: 'To Alice', issuer: bobAddress, recipient: aliceAddress },
160
+ publicClient,
161
+ bobWalletClient
162
+ );
163
+ expect(sharing.recipientSignature).toBe('0x');
164
+ // No revoker configured here, so nothing on chain can disable it: valid without any RPC.
165
+ expect(await acps.checkAccess(sharing, publicClient)).toBe('valid');
166
+ });
167
+
168
+ it('reports expiry locally', async () => {
169
+ const sharing = await acps.createSharing(
170
+ { name: 'Old', issuer: bobAddress, recipient: aliceAddress, expiration: 1 },
171
+ publicClient,
172
+ bobWalletClient
173
+ );
174
+ expect(await acps.checkAccess(sharing, publicClient)).toBe('expired');
175
+ });
176
+ });
177
+
178
+ describe('Import without activation', () => {
179
+ const ZERO_SHARE_ID = `0x${'0'.repeat(64)}` as const;
180
+
181
+ // Alice already decrypts with her own self acp; Bob then shares with her.
182
+ const setUp = async () => {
183
+ const self = await acps.createSelf({ name: 'Alice self', issuer: aliceAddress }, publicClient, aliceWalletClient);
184
+ const sharing = await acps.createSharing(
185
+ { name: 'Shared with Alice', issuer: bobAddress, recipient: aliceAddress },
186
+ publicClient,
187
+ bobWalletClient
188
+ );
189
+ return { self, sharing };
190
+ };
191
+
192
+ const toIncomingShare = (sharing: SharingACP): IncomingShare => {
193
+ const { sealingKey: _sealingKey, recipientSignature: _recipientSignature, ...pub } = ACPUtils.getPublic(sharing);
194
+ return { ...pub, shareId: ZERO_SHARE_ID, metadata: '0x' };
195
+ };
196
+
197
+ it('importShared activates the imported acp by default', async () => {
198
+ const { sharing } = await setUp();
199
+ const imported = await acps.importShared(ACPUtils.export(sharing), publicClient, aliceWalletClient);
200
+
201
+ expect(acps.getActiveACPHash(chainId, aliceAddress)).toBe(imported.hash);
202
+ });
203
+
204
+ it('importShared with activate: false stores the acp and keeps the active one', async () => {
205
+ const { self, sharing } = await setUp();
206
+ const imported = await acps.importShared(ACPUtils.export(sharing), publicClient, aliceWalletClient, {
207
+ activate: false,
208
+ });
209
+
210
+ expect(acps.getACPs(chainId, aliceAddress)?.[imported.hash]).toBeDefined();
211
+ expect(acps.getActiveACPHash(chainId, aliceAddress)).toBe(self.hash);
212
+ });
213
+
214
+ it('importShared with activate: false leaves no active acp when there was none', async () => {
215
+ const sharing = await acps.createSharing(
216
+ { name: 'Shared with Alice', issuer: bobAddress, recipient: aliceAddress },
217
+ publicClient,
218
+ bobWalletClient
219
+ );
220
+ const imported = await acps.importShared(ACPUtils.export(sharing), publicClient, aliceWalletClient, {
221
+ activate: false,
222
+ });
223
+
224
+ expect(acps.getACPs(chainId, aliceAddress)?.[imported.hash]).toBeDefined();
225
+ expect(acps.getActiveACPHash(chainId, aliceAddress)).toBeUndefined();
226
+ });
227
+
228
+ it('importFromChain activates the imported acp by default', async () => {
229
+ const { sharing } = await setUp();
230
+ const imported = await acps.importFromChain(toIncomingShare(sharing), publicClient, aliceWalletClient);
231
+
232
+ expect(acps.getActiveACPHash(chainId, aliceAddress)).toBe(imported.hash);
233
+ });
234
+
235
+ it('importFromChain keeps the share metadata on the stored recipient acp', async () => {
236
+ const { sharing } = await setUp();
237
+ const imported = await acps.importFromChain(
238
+ { ...toIncomingShare(sharing), metadata: '0x03aa' },
239
+ publicClient,
240
+ aliceWalletClient
241
+ );
242
+
243
+ expect(imported.metadata).toBe('0x03aa');
244
+ expect(acps.getACPs(chainId, aliceAddress)?.[imported.hash]?.metadata).toBe('0x03aa');
245
+ });
246
+
247
+ it('importShared and importFromChain take the recipient name as an option', async () => {
248
+ const { sharing } = await setUp();
249
+ const fromJson = await acps.importShared(ACPUtils.export(sharing), publicClient, aliceWalletClient, {
250
+ name: 'From Bob (JSON)',
251
+ });
252
+ const fromChain = await acps.importFromChain(toIncomingShare(sharing), publicClient, aliceWalletClient, {
253
+ name: 'From Bob (chain)',
254
+ });
255
+
256
+ expect(fromJson.name).toBe('From Bob (JSON)');
257
+ expect(fromChain.name).toBe('From Bob (chain)');
258
+ // the same share, so the second import replaced the first in the store
259
+ expect(acps.getACPs(chainId, aliceAddress)?.[fromChain.hash]?.name).toBe('From Bob (chain)');
260
+ });
261
+
262
+ it('importFromChain of a share without metadata leaves none on the acp', async () => {
263
+ const { sharing } = await setUp();
264
+ const imported = await acps.importFromChain(toIncomingShare(sharing), publicClient, aliceWalletClient);
265
+
266
+ expect(imported.metadata).toBeUndefined();
267
+ });
268
+
269
+ it('importFromChain with activate: false stores the acp and keeps the active one', async () => {
270
+ const { self, sharing } = await setUp();
271
+ const imported = await acps.importFromChain(toIncomingShare(sharing), publicClient, aliceWalletClient, {
272
+ activate: false,
273
+ });
274
+
275
+ expect(acps.getACPs(chainId, aliceAddress)?.[imported.hash]).toBeDefined();
276
+ expect(acps.getActiveACPHash(chainId, aliceAddress)).toBe(self.hash);
277
+ });
278
+ });
279
+
155
280
  describe('ACP Retrieval', () => {
156
281
  let createdACP: any;
157
282
  let acpHash: string;
@@ -528,7 +653,7 @@ describe('Core ACPs Tests', () => {
528
653
  const exported = acps.export(acp);
529
654
  const parsed = JSON.parse(exported);
530
655
 
531
- expect(parsed.name).toBe('Test Sharing ACP');
656
+ expect(parsed).not.toHaveProperty('name');
532
657
  expect(parsed.type).toBe('sharing');
533
658
  expect(parsed.issuer).toBe(bobAddress);
534
659
  expect(parsed.recipient).toBe(aliceAddress);
@@ -47,6 +47,14 @@ describe('mapApiErrorCodeToCofheErrorCode', () => {
47
47
  CofheErrorCode.SealOutputFailed
48
48
  );
49
49
  });
50
+
51
+ it('falls back for a code that only exists on Object.prototype', () => {
52
+ for (const inherited of ['toString', 'constructor', 'valueOf', 'hasOwnProperty']) {
53
+ expect(mapApiErrorCodeToCofheErrorCode(inherited, CofheErrorCode.DecryptFailed)).toBe(
54
+ CofheErrorCode.DecryptFailed
55
+ );
56
+ }
57
+ });
50
58
  });
51
59
 
52
60
  describe('parseApiErrorResponseBody', () => {
@@ -0,0 +1,94 @@
1
+ import { describe, it, expect, vi, afterEach } from 'vitest';
2
+ import { type PublicClient, type WalletClient } from 'viem';
3
+ import { createCofheClientBase } from '../client';
4
+ import { type CofheClient } from '../clientTypes';
5
+ import { createCofheConfigBase, type CofheInputConfig } from '../config';
6
+ import { acps } from '../acps';
7
+ import { shareAppKey } from '../../acps/share-metadata';
8
+
9
+ vi.mock('../keyStore', () => ({
10
+ createKeysStore: vi.fn(() => ({
11
+ rehydrateKeysStore: vi.fn().mockResolvedValue(undefined),
12
+ getFheKey: vi.fn(),
13
+ getCrs: vi.fn(),
14
+ setFheKey: vi.fn(),
15
+ setCrs: vi.fn(),
16
+ clearKeysStorage: vi.fn(),
17
+ store: {} as any,
18
+ })),
19
+ }));
20
+
21
+ const CHAIN_ID = 11155111;
22
+ const ACCOUNT = '0x1234567890123456789012345678901234567890';
23
+ const REGISTRY = '0x00000000000000000000000000000000000000e0';
24
+ const APP = 'explorer.fhenix.io';
25
+ const APP_KEY = shareAppKey(APP);
26
+
27
+ const publicClient = { getChainId: vi.fn().mockResolvedValue(CHAIN_ID) } as unknown as PublicClient;
28
+ const walletClient = {
29
+ getAddresses: vi.fn().mockResolvedValue([ACCOUNT]),
30
+ account: { address: ACCOUNT },
31
+ } as unknown as WalletClient;
32
+
33
+ /** A connected client; `acp.sharingRegistry` is set so the on-chain calls need no ACL lookup. */
34
+ const connectedClient = async (acp: CofheInputConfig['acp']): Promise<CofheClient> => {
35
+ const client = createCofheClientBase({
36
+ config: createCofheConfigBase({ supportedChains: [], acp: { sharingRegistry: { [CHAIN_ID]: REGISTRY }, ...acp } }),
37
+ zkBuilderAndCrsGenerator: {} as any,
38
+ tfhePublicKeyDeserializer: {} as any,
39
+ compactPkeCrsDeserializer: {} as any,
40
+ initTfhe: () => Promise.resolve(false),
41
+ });
42
+ await client.connect(publicClient, walletClient);
43
+ return client;
44
+ };
45
+
46
+ const sharingOptions = { issuer: ACCOUNT, recipient: '0x00000000000000000000000000000000000000a1' };
47
+
48
+ describe('the app key from config.acp.app', () => {
49
+ const spies = {
50
+ defaults: vi.spyOn(acps, 'applyACPDefaultsFromChain').mockImplementation(async (options) => options),
51
+ createSharing: vi.spyOn(acps, 'createSharing').mockResolvedValue({} as any),
52
+ getOrCreate: vi.spyOn(acps, 'getOrCreateSharingACP').mockResolvedValue({} as any),
53
+ shareOnChain: vi.spyOn(acps, 'shareOnChain').mockResolvedValue({ txHash: '0x', shareId: '0x' }),
54
+ getIncoming: vi.spyOn(acps, 'getIncomingShares').mockResolvedValue([]),
55
+ };
56
+ afterEach(() => Object.values(spies).forEach((spy) => spy.mockClear()));
57
+
58
+ it('goes on every share the client creates', async () => {
59
+ const client = await connectedClient({ app: APP });
60
+ await client.acp.createSharing(sharingOptions);
61
+ expect(spies.createSharing.mock.calls[0][0]).toMatchObject({ ...sharingOptions, appKey: APP_KEY });
62
+
63
+ await client.acp.getOrCreateSharingACP(sharingOptions);
64
+ expect(spies.getOrCreate.mock.calls[0][2]).toMatchObject({ ...sharingOptions, appKey: APP_KEY });
65
+ });
66
+
67
+ it('goes with labels posted by shareOnChain', async () => {
68
+ const client = await connectedClient({ app: APP });
69
+ await client.acp.shareOnChain({} as any, { labels: [] });
70
+ expect(spies.shareOnChain.mock.calls[0][3]).toEqual({ labels: [], appKey: APP_KEY });
71
+ });
72
+
73
+ it('filters the inbox', async () => {
74
+ const client = await connectedClient({ app: APP });
75
+ await client.acp.getIncomingShares();
76
+ expect(spies.getIncoming).toHaveBeenCalledWith(publicClient, REGISTRY, ACCOUNT, { appKey: APP_KEY });
77
+ });
78
+
79
+ it('is the keccak256 of the app name, and an explicit appKey wins over it', async () => {
80
+ const client = await connectedClient({ app: APP });
81
+ const other = shareAppKey('other.app');
82
+ await client.acp.createSharing({ ...sharingOptions, appKey: other });
83
+ expect(spies.createSharing.mock.calls[0][0]).toMatchObject({ appKey: other });
84
+ expect(APP_KEY).toMatch(/^0x[0-9a-f]{64}$/);
85
+ });
86
+
87
+ it('is absent without an app: shares carry no key and every share is listed', async () => {
88
+ const client = await connectedClient({});
89
+ await client.acp.createSharing(sharingOptions);
90
+ expect(spies.createSharing.mock.calls[0][0]).not.toHaveProperty('appKey');
91
+ await client.acp.getIncomingShares();
92
+ expect(spies.getIncoming).toHaveBeenCalledWith(publicClient, REGISTRY, ACCOUNT, {});
93
+ });
94
+ });
@@ -6,6 +6,7 @@ import { CofheError, CofheErrorCode } from '../error.js';
6
6
  import { type PublicClient, type WalletClient } from 'viem';
7
7
  import { EncryptInputsBuilder } from '../encrypt/encryptInputsBuilder.js';
8
8
  import { Encryptable } from '../types.js';
9
+ import { acps } from '../acps.js';
9
10
 
10
11
  // Mock dependencies
11
12
  vi.mock('../keyStore', () => ({
@@ -428,5 +429,45 @@ describe('createCofheClientBase', () => {
428
429
  expect(client.acp).toHaveProperty('selectActiveACP');
429
430
  expect(client.acp).toHaveProperty('removeActiveACP');
430
431
  });
432
+
433
+ describe('import activation', () => {
434
+ const share = { shareId: `0x${'0'.repeat(64)}` } as any;
435
+
436
+ it('importShared forwards activate with the connected clients', async () => {
437
+ const publicClient = createMockPublicClient();
438
+ const walletClient = createMockWalletClient();
439
+ await client.connect(publicClient, walletClient);
440
+ const spy = vi.spyOn(acps, 'importShared').mockResolvedValue({} as any);
441
+
442
+ await client.acp.importShared('{}', { activate: false });
443
+
444
+ expect(spy).toHaveBeenCalledWith('{}', publicClient, walletClient, { activate: false });
445
+ spy.mockRestore();
446
+ });
447
+
448
+ it('importShared still takes explicit clients, activating by default', async () => {
449
+ await client.connect(createMockPublicClient(), createMockWalletClient());
450
+ const publicClient = createMockPublicClient();
451
+ const walletClient = createMockWalletClient();
452
+ const spy = vi.spyOn(acps, 'importShared').mockResolvedValue({} as any);
453
+
454
+ await client.acp.importShared('{}', { publicClient, walletClient });
455
+
456
+ expect(spy).toHaveBeenCalledWith('{}', publicClient, walletClient, { activate: undefined });
457
+ spy.mockRestore();
458
+ });
459
+
460
+ it('importFromChain forwards activate', async () => {
461
+ const publicClient = createMockPublicClient();
462
+ const walletClient = createMockWalletClient();
463
+ await client.connect(publicClient, walletClient);
464
+ const spy = vi.spyOn(acps, 'importFromChain').mockResolvedValue({} as any);
465
+
466
+ await client.acp.importFromChain(share, { activate: false });
467
+
468
+ expect(spy).toHaveBeenCalledWith(share, publicClient, walletClient, { activate: false });
469
+ spy.mockRestore();
470
+ });
471
+ });
431
472
  });
432
473
  });
@@ -62,6 +62,14 @@ describe('createCofheConfigBase', () => {
62
62
  expectValidConfigItem('environment', 'react', 'react');
63
63
  });
64
64
 
65
+ it('acp.app', () => {
66
+ expectInvalidConfigItem('acp.app', '');
67
+ expectInvalidConfigItem('acp.app', 123);
68
+
69
+ expectValidConfigItem('acp.app', 'explorer.fhenix.io', 'explorer.fhenix.io');
70
+ expectValidConfigItem('acp.app', undefined, undefined);
71
+ });
72
+
65
73
  it('supportedChains', () => {
66
74
  expectInvalidConfigItem('supportedChains', {});
67
75
  expectInvalidConfigItem('supportedChains', 'not-an-array');
@@ -162,6 +170,22 @@ describe('createCofheConfigBase', () => {
162
170
  expectValidConfigItem('useWorkers', undefined, true); // defaults to true
163
171
  });
164
172
 
173
+ it('tfheThreads', () => {
174
+ expectInvalidConfigItem('tfheThreads', 'not-a-setting');
175
+ expectInvalidConfigItem('tfheThreads', null);
176
+ expectInvalidConfigItem('tfheThreads', true);
177
+ expectInvalidConfigItem('tfheThreads', 0);
178
+ expectInvalidConfigItem('tfheThreads', -4);
179
+ expectInvalidConfigItem('tfheThreads', 2.5);
180
+ expectInvalidConfigItem('tfheThreads', {});
181
+
182
+ expectValidConfigItem('tfheThreads', 'auto', 'auto');
183
+ expectValidConfigItem('tfheThreads', false, false);
184
+ expectValidConfigItem('tfheThreads', 1, 1);
185
+ expectValidConfigItem('tfheThreads', 4, 4);
186
+ expectValidConfigItem('tfheThreads', undefined, 'auto'); // defaults to 'auto'
187
+ });
188
+
165
189
  it('should get config item', () => {
166
190
  const config: CofheInputConfig = {
167
191
  supportedChains: [sepolia],
@@ -1,5 +1,5 @@
1
1
  import { STAGING_TESTS, stagingViemChain } from './stagingRedirect';
2
- import { FheTypes, verifyDecryptResult, createCofheConfigBase, TASK_MANAGER_ADDRESS } from '@/core';
2
+ import { FheTypes, verifyDecryptResult, createCofheConfigBase, getTaskManagerAddress } from '@/core';
3
3
  import { getChainById, stagingCofhe } from '@/chains';
4
4
  import {
5
5
  TEST_PRIVATE_KEY,
@@ -19,7 +19,12 @@ import { createPublicClient, createWalletClient, http, parseAbi } from 'viem';
19
19
  import { privateKeyToAccount } from 'viem/accounts';
20
20
  import { arbitrumSepolia, baseSepolia, sepolia } from 'viem/chains';
21
21
 
22
- const account = privateKeyToAccount(TEST_PRIVATE_KEY);
22
+ // Live-chain suite: needs the funded TEST_PRIVATE_KEY (signs decrypt requests against
23
+ // pre-deployed testnet fixtures). Without it — fork PRs, where secrets are unavailable —
24
+ // the suite skips; CI runs it separately with the key (test-live job).
25
+ const LIVE_TESTS = Boolean(TEST_PRIVATE_KEY);
26
+ const DEFAULT_TEST_PRIVATE_KEY = '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80';
27
+ const account = privateKeyToAccount(TEST_PRIVATE_KEY || DEFAULT_TEST_PRIVATE_KEY);
23
28
 
24
29
  const TEST_CHAIN_ID = STAGING_TESTS ? stagingViemChain.id : PRIMARY_TEST_CHAIN;
25
30
 
@@ -29,7 +34,7 @@ const VIEM_CHAINS: Record<number, Chain> = {
29
34
  11155111: sepolia,
30
35
  };
31
36
 
32
- describe('Core – Decrypt Tests', () => {
37
+ describe.skipIf(!LIVE_TESTS)('Core – Decrypt Tests', () => {
33
38
  let publicClient: PublicClient;
34
39
  let walletClient: WalletClient;
35
40
  let config: ReturnType<typeof createCofheConfigBase>;
@@ -236,6 +241,8 @@ describe('Core – Decrypt Tests', () => {
236
241
  },
237
242
  ] as const;
238
243
 
244
+ const taskManagerAddress = await getTaskManagerAddress(publicClient);
245
+
239
246
  for (const sample of samples) {
240
247
  const sdkResult = await verifyDecryptResult(sample.handle, sample.cleartext, sample.signature, publicClient);
241
248
 
@@ -243,7 +250,7 @@ describe('Core – Decrypt Tests', () => {
243
250
  'function verifyDecryptResultSafe(uint256 ctHash, uint256 cleartext, bytes signature) view returns (bool)',
244
251
  ]);
245
252
  const tmResult = await publicClient.readContract({
246
- address: TASK_MANAGER_ADDRESS,
253
+ address: taskManagerAddress,
247
254
  abi: verifyDecryptResultSafeAbi,
248
255
  functionName: 'verifyDecryptResultSafe',
249
256
  args: [sample.handle, sample.cleartext, sample.signature],