@cofhe/sdk 0.1.0 → 0.2.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 (121) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/adapters/ethers5.test.ts +174 -0
  3. package/adapters/ethers5.ts +36 -0
  4. package/adapters/ethers6.test.ts +169 -0
  5. package/adapters/ethers6.ts +36 -0
  6. package/adapters/hardhat-node.ts +167 -0
  7. package/adapters/hardhat.hh2.test.ts +159 -0
  8. package/adapters/hardhat.ts +36 -0
  9. package/adapters/index.test.ts +20 -0
  10. package/adapters/index.ts +5 -0
  11. package/adapters/smartWallet.ts +99 -0
  12. package/adapters/test-utils.ts +53 -0
  13. package/adapters/types.ts +6 -0
  14. package/adapters/wagmi.test.ts +156 -0
  15. package/adapters/wagmi.ts +17 -0
  16. package/chains/chains/arbSepolia.ts +14 -0
  17. package/chains/chains/baseSepolia.ts +14 -0
  18. package/chains/chains/hardhat.ts +15 -0
  19. package/chains/chains/localcofhe.ts +14 -0
  20. package/chains/chains/sepolia.ts +14 -0
  21. package/chains/chains.test.ts +50 -0
  22. package/chains/defineChain.ts +18 -0
  23. package/chains/index.ts +35 -0
  24. package/chains/types.ts +32 -0
  25. package/core/baseBuilder.ts +119 -0
  26. package/core/client.test.ts +315 -0
  27. package/core/client.ts +292 -0
  28. package/core/clientTypes.ts +108 -0
  29. package/core/config.test.ts +235 -0
  30. package/core/config.ts +220 -0
  31. package/core/decrypt/MockQueryDecrypterAbi.ts +129 -0
  32. package/core/decrypt/cofheMocksSealOutput.ts +57 -0
  33. package/core/decrypt/decryptHandleBuilder.ts +287 -0
  34. package/core/decrypt/decryptUtils.ts +28 -0
  35. package/core/decrypt/tnSealOutputV1.ts +59 -0
  36. package/core/decrypt/tnSealOutputV2.ts +298 -0
  37. package/core/encrypt/MockZkVerifierAbi.ts +106 -0
  38. package/core/encrypt/cofheMocksZkVerifySign.ts +284 -0
  39. package/core/encrypt/encryptInputsBuilder.test.ts +751 -0
  40. package/core/encrypt/encryptInputsBuilder.ts +560 -0
  41. package/core/encrypt/encryptUtils.ts +67 -0
  42. package/core/encrypt/zkPackProveVerify.ts +335 -0
  43. package/core/error.ts +168 -0
  44. package/core/fetchKeys.test.ts +195 -0
  45. package/core/fetchKeys.ts +144 -0
  46. package/core/index.ts +89 -0
  47. package/core/keyStore.test.ts +226 -0
  48. package/core/keyStore.ts +154 -0
  49. package/core/permits.test.ts +494 -0
  50. package/core/permits.ts +200 -0
  51. package/core/types.ts +398 -0
  52. package/core/utils.ts +130 -0
  53. package/dist/adapters.cjs +88 -0
  54. package/dist/adapters.d.cts +14576 -0
  55. package/dist/adapters.d.ts +14576 -0
  56. package/dist/adapters.js +83 -0
  57. package/dist/chains.cjs +114 -0
  58. package/dist/chains.d.cts +121 -0
  59. package/dist/chains.d.ts +121 -0
  60. package/dist/chains.js +1 -0
  61. package/dist/chunk-UGBVZNRT.js +818 -0
  62. package/dist/chunk-WEAZ25JO.js +105 -0
  63. package/dist/chunk-WGCRJCBR.js +2523 -0
  64. package/dist/clientTypes-5_1nwtUe.d.cts +914 -0
  65. package/dist/clientTypes-Es7fyi65.d.ts +914 -0
  66. package/dist/core.cjs +3414 -0
  67. package/dist/core.d.cts +111 -0
  68. package/dist/core.d.ts +111 -0
  69. package/dist/core.js +3 -0
  70. package/dist/node.cjs +3286 -0
  71. package/dist/node.d.cts +22 -0
  72. package/dist/node.d.ts +22 -0
  73. package/dist/node.js +91 -0
  74. package/dist/permit-fUSe6KKq.d.cts +349 -0
  75. package/dist/permit-fUSe6KKq.d.ts +349 -0
  76. package/dist/permits.cjs +871 -0
  77. package/dist/permits.d.cts +1045 -0
  78. package/dist/permits.d.ts +1045 -0
  79. package/dist/permits.js +1 -0
  80. package/dist/types-KImPrEIe.d.cts +48 -0
  81. package/dist/types-KImPrEIe.d.ts +48 -0
  82. package/dist/web.cjs +3478 -0
  83. package/dist/web.d.cts +38 -0
  84. package/dist/web.d.ts +38 -0
  85. package/dist/web.js +240 -0
  86. package/dist/zkProve.worker.cjs +93 -0
  87. package/dist/zkProve.worker.d.cts +2 -0
  88. package/dist/zkProve.worker.d.ts +2 -0
  89. package/dist/zkProve.worker.js +91 -0
  90. package/node/client.test.ts +147 -0
  91. package/node/config.test.ts +68 -0
  92. package/node/encryptInputs.test.ts +155 -0
  93. package/node/index.ts +97 -0
  94. package/node/storage.ts +51 -0
  95. package/package.json +27 -15
  96. package/permits/index.ts +68 -0
  97. package/permits/localstorage.test.ts +117 -0
  98. package/permits/permit.test.ts +477 -0
  99. package/permits/permit.ts +405 -0
  100. package/permits/sealing.test.ts +84 -0
  101. package/permits/sealing.ts +131 -0
  102. package/permits/signature.ts +79 -0
  103. package/permits/store.test.ts +128 -0
  104. package/permits/store.ts +166 -0
  105. package/permits/test-utils.ts +20 -0
  106. package/permits/types.ts +191 -0
  107. package/permits/utils.ts +62 -0
  108. package/permits/validation.test.ts +288 -0
  109. package/permits/validation.ts +369 -0
  110. package/web/client.web.test.ts +147 -0
  111. package/web/config.web.test.ts +69 -0
  112. package/web/encryptInputs.web.test.ts +172 -0
  113. package/web/index.ts +161 -0
  114. package/web/storage.ts +34 -0
  115. package/web/worker.builder.web.test.ts +148 -0
  116. package/web/worker.config.web.test.ts +329 -0
  117. package/web/worker.output.web.test.ts +84 -0
  118. package/web/workerManager.test.ts +80 -0
  119. package/web/workerManager.ts +214 -0
  120. package/web/workerManager.web.test.ts +114 -0
  121. package/web/zkProve.worker.ts +133 -0
@@ -0,0 +1,144 @@
1
+ import { hardhat } from '@/chains';
2
+
3
+ import { type CofhesdkConfig, getCoFheUrlOrThrow } from './config.js';
4
+ import { type KeysStorage } from './keyStore.js';
5
+
6
+ const PUBLIC_KEY_LENGTH_MIN = 15_000;
7
+ export type FheKeyDeserializer = (buff: string) => void;
8
+
9
+ const checkKeyValidity = (key: string | undefined, serializer: FheKeyDeserializer) => {
10
+ if (key == null || key.length === 0) return [false, `Key is null or empty <${key}>`];
11
+ try {
12
+ serializer(key);
13
+ return [true, `Key is valid`];
14
+ } catch (err) {
15
+ return [false, `Serialization failed <${err}> key length <${key.length}>`];
16
+ }
17
+ };
18
+
19
+ const fetchFhePublicKey = async (
20
+ coFheUrl: string,
21
+ chainId: number,
22
+ securityZone: number,
23
+ tfhePublicKeyDeserializer: FheKeyDeserializer,
24
+ keysStorage?: KeysStorage | null
25
+ ): Promise<[string, boolean]> => {
26
+ // Escape if key already exists
27
+ const storedKey = keysStorage?.getFheKey(chainId, securityZone);
28
+ const [storedKeyValid] = checkKeyValidity(storedKey, tfhePublicKeyDeserializer);
29
+ if (storedKeyValid) return [storedKey!, false];
30
+
31
+ let pk_data: string | undefined = undefined;
32
+
33
+ try {
34
+ const pk_res = await fetch(`${coFheUrl}/GetNetworkPublicKey`, {
35
+ method: 'POST',
36
+ headers: {
37
+ 'Content-Type': 'application/json',
38
+ },
39
+ body: JSON.stringify({ securityZone }),
40
+ });
41
+ const json = (await pk_res.json()) as { publicKey: string };
42
+ pk_data = json.publicKey;
43
+ } catch (err) {
44
+ throw new Error(`Error fetching FHE publicKey; fetching from CoFHE failed with error ${err}`);
45
+ }
46
+
47
+ if (pk_data == null || typeof pk_data !== 'string') {
48
+ throw new Error(`Error fetching FHE publicKey; fetched result invalid: missing or not a string`);
49
+ }
50
+
51
+ if (pk_data === '0x') {
52
+ throw new Error('Error fetching FHE publicKey; provided chain is not FHE enabled / not found');
53
+ }
54
+
55
+ if (pk_data.length < PUBLIC_KEY_LENGTH_MIN) {
56
+ throw new Error(
57
+ `Error fetching FHE publicKey; got shorter than expected key length: ${pk_data.length}. Expected length >= ${PUBLIC_KEY_LENGTH_MIN}`
58
+ );
59
+ }
60
+
61
+ // Check validity by serializing
62
+ try {
63
+ tfhePublicKeyDeserializer(pk_data);
64
+ } catch (err) {
65
+ throw new Error(`Error serializing FHE publicKey; ${err}`);
66
+ }
67
+
68
+ // Store result
69
+ keysStorage?.setFheKey(chainId, securityZone, pk_data);
70
+
71
+ return [pk_data, true];
72
+ };
73
+
74
+ const fetchCrs = async (
75
+ coFheUrl: string,
76
+ chainId: number,
77
+ securityZone: number,
78
+ compactPkeCrsDeserializer: FheKeyDeserializer,
79
+ keysStorage?: KeysStorage | null
80
+ ): Promise<[string, boolean]> => {
81
+ // Escape if key already exists
82
+ const storedKey = keysStorage?.getCrs(chainId);
83
+ const [storedKeyValid] = checkKeyValidity(storedKey, compactPkeCrsDeserializer);
84
+ if (storedKeyValid) return [storedKey!, false];
85
+
86
+ let crs_data: string | undefined = undefined;
87
+
88
+ try {
89
+ const crs_res = await fetch(`${coFheUrl}/GetCrs`, {
90
+ method: 'POST',
91
+ headers: {
92
+ 'Content-Type': 'application/json',
93
+ },
94
+ body: JSON.stringify({ securityZone }),
95
+ });
96
+ const json = (await crs_res.json()) as { crs: string };
97
+ crs_data = json.crs;
98
+ } catch (err) {
99
+ throw new Error(`Error fetching CRS; fetching failed with error ${err}`);
100
+ }
101
+
102
+ if (crs_data == null || typeof crs_data !== 'string') {
103
+ throw new Error(`Error fetching CRS; invalid: missing or not a string`);
104
+ }
105
+
106
+ try {
107
+ compactPkeCrsDeserializer(crs_data);
108
+ } catch (err) {
109
+ console.error(`Error serializing CRS ${err}`);
110
+ throw new Error(`Error serializing CRS; ${err}`);
111
+ }
112
+
113
+ keysStorage?.setCrs(chainId, crs_data);
114
+
115
+ return [crs_data, true];
116
+ };
117
+
118
+ /**
119
+ * Retrieves the FHE public key and the CRS from the provider.
120
+ * If the key/crs already exists in the store it is returned, else it is fetched, stored, and returned
121
+ * @param {CofhesdkConfig} config - The configuration object for the CoFHE SDK
122
+ * @param {number} chainId - The chain to fetch the FHE key for, if no chainId provided, undefined is returned
123
+ * @param securityZone - The security zone for which to retrieve the key (default 0).
124
+ * @param tfhePublicKeyDeserializer - The serializer for the FHE public key (used for validation).
125
+ * @param compactPkeCrsDeserializer - The serializer for the CRS (used for validation).
126
+ * @param keysStorage - The keys storage instance to use (optional)
127
+ * @returns {Promise<[[string, boolean], [string, boolean]]>} - A promise that resolves to [[fheKey, fheKeyFetchedFromCoFHE], [crs, crsFetchedFromCoFHE]]
128
+ */
129
+ export const fetchKeys = async (
130
+ config: CofhesdkConfig,
131
+ chainId: number,
132
+ securityZone: number = 0,
133
+ tfhePublicKeyDeserializer: FheKeyDeserializer,
134
+ compactPkeCrsDeserializer: FheKeyDeserializer,
135
+ keysStorage?: KeysStorage | null
136
+ ): Promise<[[string, boolean], [string, boolean]]> => {
137
+ // Get cofhe url from config
138
+ const coFheUrl = getCoFheUrlOrThrow(config, chainId);
139
+
140
+ return await Promise.all([
141
+ fetchFhePublicKey(coFheUrl, chainId, securityZone, tfhePublicKeyDeserializer, keysStorage),
142
+ fetchCrs(coFheUrl, chainId, securityZone, compactPkeCrsDeserializer, keysStorage),
143
+ ]);
144
+ };
package/core/index.ts ADDED
@@ -0,0 +1,89 @@
1
+ // Client (base implementations)
2
+ export { createCofhesdkClientBase, InitialConnectStore as CONNECT_STORE_DEFAULTS } from './client.js';
3
+
4
+ // Configuration (base implementations)
5
+ export { createCofhesdkConfigBase, getCofhesdkConfigItem } from './config.js';
6
+ export type { CofhesdkConfig, CofhesdkInputConfig, CofhesdkInternalConfig } from './config.js';
7
+
8
+ // Types
9
+ export type {
10
+ // Client types
11
+ CofhesdkClient,
12
+ CofhesdkClientParams,
13
+ CofhesdkClientConnectionState,
14
+ CofhesdkClientPermits,
15
+ } from './clientTypes.js';
16
+
17
+ export type {
18
+ IStorage,
19
+ // Primitive types
20
+ Primitive,
21
+ LiteralToPrimitive,
22
+ // Encryptable types
23
+ EncryptableItem,
24
+ EncryptableBool,
25
+ EncryptableUint8,
26
+ EncryptableUint16,
27
+ EncryptableUint32,
28
+ EncryptableUint64,
29
+ EncryptableUint128,
30
+ EncryptableAddress,
31
+ // Encrypted types
32
+ EncryptedNumber,
33
+ EncryptedItemInput,
34
+ EncryptedBoolInput,
35
+ EncryptedUint8Input,
36
+ EncryptedUint16Input,
37
+ EncryptedUint32Input,
38
+ EncryptedUint64Input,
39
+ EncryptedUint128Input,
40
+ EncryptedAddressInput,
41
+ EncryptedItemInputs,
42
+ EncryptableToEncryptedItemInputMap,
43
+ FheTypeValue,
44
+ // Decryption types
45
+ UnsealedItem,
46
+ // Util types
47
+ EncryptStepCallbackFunction as EncryptSetStateFn,
48
+ EncryptStepCallbackContext,
49
+ } from './types.js';
50
+ export {
51
+ FheTypes,
52
+ FheUintUTypes,
53
+ FheAllUTypes,
54
+ Encryptable,
55
+ isEncryptableItem,
56
+ EncryptStep,
57
+ isLastEncryptionStep,
58
+ assertCorrectEncryptedItemInput,
59
+ } from './types.js';
60
+
61
+ // Error handling
62
+ export { CofhesdkError, CofhesdkErrorCode, isCofhesdkError } from './error.js';
63
+ export type { CofhesdkErrorParams } from './error.js';
64
+
65
+ // Key fetching
66
+ export { fetchKeys } from './fetchKeys.js';
67
+ export type { FheKeyDeserializer } from './fetchKeys.js';
68
+
69
+ // Key storage
70
+ export { createKeysStore } from './keyStore.js';
71
+ export type { KeysStorage, KeysStore } from './keyStore.js';
72
+
73
+ // Builders (exported via client, but can be imported directly for typing)
74
+ export { EncryptInputsBuilder } from './encrypt/encryptInputsBuilder.js';
75
+ export { DecryptHandlesBuilder } from './decrypt/decryptHandleBuilder.js';
76
+
77
+ // ZK utilities
78
+ export type {
79
+ ZkBuilderAndCrsGenerator,
80
+ ZkProveWorkerFunction,
81
+ ZkProveWorkerRequest,
82
+ ZkProveWorkerResponse,
83
+ } from './encrypt/zkPackProveVerify.js';
84
+ export { zkProveWithWorker } from './encrypt/zkPackProveVerify.js';
85
+
86
+ export { MOCKS_ZK_VERIFIER_SIGNER_ADDRESS } from './encrypt/cofheMocksZkVerifySign.js';
87
+
88
+ // Utils
89
+ export { fheTypeToString } from './utils.js';
@@ -0,0 +1,226 @@
1
+ /* eslint-disable turbo/no-undeclared-env-vars */
2
+ /* eslint-disable no-undef */
3
+
4
+ import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
5
+ import { createKeysStore, type KeysStore, type KeysStorage } from './keyStore.js';
6
+
7
+ // Mock the storage module
8
+ const mockStorage = {
9
+ getItem: vi.fn(),
10
+ setItem: vi.fn(),
11
+ removeItem: vi.fn(),
12
+ };
13
+
14
+ vi.mock('../src/storage', () => ({
15
+ getStorage: () => mockStorage,
16
+ }));
17
+
18
+ describe('KeyStore', () => {
19
+ let keysStorage: KeysStorage;
20
+
21
+ beforeEach(() => {
22
+ vi.clearAllMocks();
23
+ // Create a fresh keysStorage instance for each test (with mock storage)
24
+ keysStorage = createKeysStore(mockStorage as any);
25
+ });
26
+
27
+ afterEach(() => {
28
+ vi.restoreAllMocks();
29
+ });
30
+
31
+ describe('Store Structure', () => {
32
+ it('should have initial empty state', () => {
33
+ const state = keysStorage.store.getState();
34
+ expect(state).toEqual({
35
+ fhe: {},
36
+ crs: {},
37
+ });
38
+ });
39
+
40
+ it('should be a Zustand store with persist', () => {
41
+ expect(keysStorage.store).toBeDefined();
42
+ expect(keysStorage.store.getState).toBeDefined();
43
+ expect(keysStorage.store.setState).toBeDefined();
44
+ // In test environment, persist might not be fully initialized
45
+ if ('persist' in keysStorage.store) {
46
+ expect((keysStorage.store as any).persist).toBeDefined();
47
+ }
48
+ });
49
+ });
50
+
51
+ describe('FHE Key Management', () => {
52
+ const testChainId = 1337;
53
+ const testSecurityZone = 0;
54
+ const testKey = '0x1234567890';
55
+
56
+ it('should set and get FHE key', () => {
57
+ keysStorage.setFheKey(testChainId, testSecurityZone, testKey);
58
+
59
+ const retrievedKey = keysStorage.getFheKey(testChainId, testSecurityZone);
60
+
61
+ expect(retrievedKey).toEqual(testKey);
62
+ });
63
+
64
+ it('should handle multiple security zones', () => {
65
+ const key0 = '0x1234567890';
66
+ const key1 = '0x2345678901';
67
+
68
+ keysStorage.setFheKey(testChainId, 0, key0);
69
+ keysStorage.setFheKey(testChainId, 1, key1);
70
+
71
+ expect(keysStorage.getFheKey(testChainId, 0)).toEqual(key0);
72
+ expect(keysStorage.getFheKey(testChainId, 1)).toEqual(key1);
73
+ });
74
+
75
+ it('should handle multiple chains', () => {
76
+ const chain1Key = '0x1234567890';
77
+ const chain2Key = '0x2345678901';
78
+
79
+ keysStorage.setFheKey(1, testSecurityZone, chain1Key);
80
+ keysStorage.setFheKey(2, testSecurityZone, chain2Key);
81
+
82
+ expect(keysStorage.getFheKey(1, testSecurityZone)).toEqual(chain1Key);
83
+ expect(keysStorage.getFheKey(2, testSecurityZone)).toEqual(chain2Key);
84
+ });
85
+
86
+ it('should return undefined for non-existent keys', () => {
87
+ expect(keysStorage.getFheKey(999, 0)).toBeUndefined();
88
+ expect(keysStorage.getFheKey(testChainId, 999)).toBeUndefined();
89
+ expect(keysStorage.getFheKey(undefined, 0)).toBeUndefined();
90
+ expect(keysStorage.getFheKey(testChainId, undefined as any)).toBeUndefined();
91
+ });
92
+ });
93
+
94
+ describe('CRS Management', () => {
95
+ const testChainId = 1337;
96
+ const testCrs = '0x1234567890';
97
+
98
+ it('should set and get CRS', () => {
99
+ keysStorage.setCrs(testChainId, testCrs);
100
+
101
+ const retrievedCrs = keysStorage.getCrs(testChainId);
102
+
103
+ expect(retrievedCrs).toEqual(testCrs);
104
+ });
105
+
106
+ it('should handle multiple chains for CRS', () => {
107
+ const crs1 = '0x1234567890';
108
+ const crs2 = '0x2345678901';
109
+
110
+ keysStorage.setCrs(1, crs1);
111
+ keysStorage.setCrs(2, crs2);
112
+
113
+ expect(keysStorage.getCrs(1)).toEqual(crs1);
114
+ expect(keysStorage.getCrs(2)).toEqual(crs2);
115
+ });
116
+
117
+ it('should return undefined for non-existent CRS', () => {
118
+ expect(keysStorage.getCrs(999)).toBeUndefined();
119
+ expect(keysStorage.getCrs(undefined)).toBeUndefined();
120
+ });
121
+ });
122
+
123
+ describe('Storage Utilities', () => {
124
+ it('should clear keys storage', async () => {
125
+ await keysStorage.clearKeysStorage();
126
+
127
+ expect(mockStorage.removeItem).toHaveBeenCalledWith('cofhesdk-keys');
128
+ });
129
+
130
+ it('should rehydrate keys store', async () => {
131
+ const mockRehydrate = vi.fn();
132
+
133
+ // Mock the persist object if it doesn't exist
134
+ if (!('persist' in keysStorage.store)) {
135
+ (keysStorage.store as any).persist = {};
136
+ }
137
+ (keysStorage.store as any).persist.rehydrate = mockRehydrate;
138
+
139
+ await keysStorage.rehydrateKeysStore();
140
+
141
+ expect(mockRehydrate).toHaveBeenCalled();
142
+ });
143
+ });
144
+
145
+ describe('keysStorage Object', () => {
146
+ it('should have all required methods', () => {
147
+ expect(keysStorage).toBeDefined();
148
+ expect(keysStorage.store).toBeDefined();
149
+ expect(keysStorage.getFheKey).toBeDefined();
150
+ expect(keysStorage.getCrs).toBeDefined();
151
+ expect(keysStorage.setFheKey).toBeDefined();
152
+ expect(keysStorage.setCrs).toBeDefined();
153
+ expect(keysStorage.clearKeysStorage).toBeDefined();
154
+ expect(keysStorage.rehydrateKeysStore).toBeDefined();
155
+ });
156
+
157
+ it('should work through keysStorage object', () => {
158
+ const testChainId = 1337;
159
+ const testKey = '0x1234567890';
160
+ const testCrs = '0x2345678901';
161
+
162
+ keysStorage.setFheKey(testChainId, 0, testKey);
163
+ keysStorage.setCrs(testChainId, testCrs);
164
+
165
+ expect(keysStorage.getFheKey(testChainId, 0)).toEqual(testKey);
166
+ expect(keysStorage.getCrs(testChainId)).toEqual(testCrs);
167
+ });
168
+ });
169
+
170
+ describe('State Management', () => {
171
+ it('should update state immutably', () => {
172
+ const initialState = keysStorage.store.getState();
173
+ const testChainId = 1337;
174
+ const testKey = '0x1234567890';
175
+
176
+ keysStorage.setFheKey(testChainId, 0, testKey);
177
+
178
+ const newState = keysStorage.store.getState();
179
+
180
+ // State should be different objects
181
+ expect(newState).not.toBe(initialState);
182
+ expect(newState.fhe).not.toBe(initialState.fhe);
183
+
184
+ // But should contain the new key
185
+ expect(newState.fhe[testChainId][0]).toEqual(testKey);
186
+ });
187
+
188
+ it('should preserve existing data when adding new keys', () => {
189
+ const key1 = '0x1234567890';
190
+ const key2 = '0x2345678901';
191
+ const crs1 = '0x3456789012';
192
+
193
+ keysStorage.setFheKey(1, 0, key1);
194
+ keysStorage.setFheKey(2, 0, key2);
195
+ keysStorage.setCrs(1, crs1);
196
+
197
+ const state = keysStorage.store.getState();
198
+
199
+ expect(state.fhe[1][0]).toEqual(key1);
200
+ expect(state.fhe[2][0]).toEqual(key2);
201
+ expect(state.crs[1]).toEqual(crs1);
202
+ });
203
+ });
204
+
205
+ describe('Type Safety', () => {
206
+ it('should have correct TypeScript types', () => {
207
+ const state: KeysStore = keysStorage.store.getState();
208
+
209
+ // These should compile without TypeScript errors
210
+ expect(typeof state.fhe).toBe('object');
211
+ expect(typeof state.crs).toBe('object');
212
+
213
+ // Test that the types allow proper access patterns
214
+ const chainId = 1337;
215
+ const securityZone = 0;
216
+
217
+ if (state.fhe[chainId] && state.fhe[chainId][securityZone]) {
218
+ expect(state.fhe[chainId][securityZone]).toBeInstanceOf(String);
219
+ }
220
+
221
+ if (state.crs[chainId]) {
222
+ expect(state.crs[chainId]).toBeInstanceOf(String);
223
+ }
224
+ });
225
+ });
226
+ });
@@ -0,0 +1,154 @@
1
+ import { createStore, type StoreApi } from 'zustand/vanilla';
2
+ import { persist, createJSONStorage } from 'zustand/middleware';
3
+ import { produce } from 'immer';
4
+ import { type IStorage } from './types.js';
5
+
6
+ // Type definitions
7
+ type ChainRecord<T> = Record<string, T>;
8
+ type SecurityZoneRecord<T> = Record<number, T>;
9
+
10
+ // Keys store for FHE keys and CRS
11
+ export type KeysStore = {
12
+ fhe: ChainRecord<SecurityZoneRecord<string | undefined>>;
13
+ crs: ChainRecord<string | undefined>;
14
+ };
15
+
16
+ export type KeysStorage = {
17
+ store: StoreApi<KeysStore>;
18
+ getFheKey: (chainId: number | undefined, securityZone?: number) => string | undefined;
19
+ getCrs: (chainId: number | undefined) => string | undefined;
20
+ setFheKey: (chainId: number, securityZone: number, key: string) => void;
21
+ setCrs: (chainId: number, crs: string) => void;
22
+ clearKeysStorage: () => Promise<void>;
23
+ rehydrateKeysStore: () => Promise<void>;
24
+ };
25
+
26
+ function isValidPersistedState(state: unknown): state is KeysStore {
27
+ if (state && typeof state === 'object') {
28
+ if ('fhe' in state && 'crs' in state) {
29
+ return true;
30
+ } else {
31
+ throw new Error(
32
+ "Invalid persisted state structure for KeysStore. Is object but doesn't contain required fields 'fhe' and 'crs'."
33
+ );
34
+ }
35
+ }
36
+
37
+ return false;
38
+ }
39
+
40
+ const DEFAULT_KEYS_STORE: KeysStore = {
41
+ fhe: {},
42
+ crs: {},
43
+ };
44
+
45
+ type StoreWithPersist = ReturnType<typeof createStoreWithPersit>;
46
+
47
+ function isStoreWithPersist(store: StoreApi<KeysStore> | StoreWithPersist): store is StoreWithPersist {
48
+ return 'persist' in store;
49
+ }
50
+ /**
51
+ * Creates a keys storage instance using the provided storage implementation
52
+ * @param storage - The storage implementation to use (IStorage interface), or null for non-persisted store
53
+ * @returns A KeysStorage instance with all utility methods
54
+ */
55
+ export function createKeysStore(storage: IStorage | null): KeysStorage {
56
+ // Conditionally create store with or without persist wrapper
57
+ const keysStore = storage
58
+ ? createStoreWithPersit(storage)
59
+ : createStore<KeysStore>()(() => ({
60
+ fhe: {},
61
+ crs: {},
62
+ }));
63
+
64
+ // Utility functions
65
+
66
+ const getFheKey = (chainId: number | undefined, securityZone = 0) => {
67
+ if (chainId == null || securityZone == null) return undefined;
68
+ const stored = keysStore.getState().fhe[chainId]?.[securityZone];
69
+ return stored;
70
+ };
71
+
72
+ const getCrs = (chainId: number | undefined) => {
73
+ if (chainId == null) return undefined;
74
+ const stored = keysStore.getState().crs[chainId];
75
+ return stored;
76
+ };
77
+
78
+ const setFheKey = (chainId: number, securityZone: number, key: string) => {
79
+ keysStore.setState(
80
+ produce<KeysStore>((state: KeysStore) => {
81
+ if (state.fhe[chainId] == null) state.fhe[chainId] = {};
82
+ state.fhe[chainId][securityZone] = key;
83
+ })
84
+ );
85
+ };
86
+
87
+ const setCrs = (chainId: number, crs: string) => {
88
+ keysStore.setState(
89
+ produce<KeysStore>((state: KeysStore) => {
90
+ state.crs[chainId] = crs;
91
+ })
92
+ );
93
+ };
94
+
95
+ const clearKeysStorage = async () => {
96
+ if (storage) {
97
+ await storage.removeItem('cofhesdk-keys');
98
+ }
99
+ // If no storage, this is a no-op
100
+ };
101
+
102
+ const rehydrateKeysStore = async () => {
103
+ if (!isStoreWithPersist(keysStore)) return;
104
+ if (keysStore.persist.hasHydrated()) return;
105
+ await keysStore.persist.rehydrate();
106
+ };
107
+
108
+ return {
109
+ store: keysStore,
110
+ getFheKey,
111
+ getCrs,
112
+ setFheKey,
113
+ setCrs,
114
+ clearKeysStorage,
115
+ rehydrateKeysStore,
116
+ };
117
+ }
118
+
119
+ function createStoreWithPersit(storage: IStorage) {
120
+ const result = createStore<KeysStore>()(
121
+ persist(() => DEFAULT_KEYS_STORE, {
122
+ // because earleir tests were written with on-init hydration skipped (due to the error suppression in zustand), returning this flag to fix test (i.e. KeyStore > Storage Utilities > should rehydrate keys store)
123
+ skipHydration: true,
124
+ // if onRehydrateStorage is not passed here, the errors thrown by storage layer are swallowed by zustand here: https://github.com/pmndrs/zustand/blob/39a391b6c1ff9aa89b81694d9bdb21da37dd4ac6/src/middleware/persist.ts#L321
125
+ onRehydrateStorage: () => (_state?, _error?) => {
126
+ if (_error) throw new Error(`onRehydrateStorage: Error rehydrating keys store: ${_error}`);
127
+ },
128
+ name: 'cofhesdk-keys',
129
+ storage: createJSONStorage(() => storage),
130
+ merge: (persistedState, currentState) => {
131
+ const persisted = isValidPersistedState(persistedState) ? persistedState : DEFAULT_KEYS_STORE;
132
+ const current = currentState as KeysStore;
133
+
134
+ // Deep merge for fhe
135
+ const mergedFhe: KeysStore['fhe'] = { ...persisted.fhe };
136
+ const allChainIds = new Set([...Object.keys(current.fhe), ...Object.keys(persisted.fhe)]);
137
+ for (const chainId of allChainIds) {
138
+ const persistedZones = persisted.fhe[chainId] || {};
139
+ const currentZones = current.fhe[chainId] || {};
140
+ mergedFhe[chainId] = { ...persistedZones, ...currentZones };
141
+ }
142
+
143
+ // Deep merge for crs
144
+ const mergedCrs: KeysStore['crs'] = { ...persisted.crs, ...current.crs };
145
+
146
+ return {
147
+ fhe: mergedFhe,
148
+ crs: mergedCrs,
149
+ };
150
+ },
151
+ })
152
+ );
153
+ return result;
154
+ }