@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.
- package/CHANGELOG.md +62 -0
- package/adapters/ethers5.test.ts +174 -0
- package/adapters/ethers5.ts +36 -0
- package/adapters/ethers6.test.ts +169 -0
- package/adapters/ethers6.ts +36 -0
- package/adapters/hardhat-node.ts +167 -0
- package/adapters/hardhat.hh2.test.ts +159 -0
- package/adapters/hardhat.ts +36 -0
- package/adapters/index.test.ts +20 -0
- package/adapters/index.ts +5 -0
- package/adapters/smartWallet.ts +99 -0
- package/adapters/test-utils.ts +53 -0
- package/adapters/types.ts +6 -0
- package/adapters/wagmi.test.ts +156 -0
- package/adapters/wagmi.ts +17 -0
- package/chains/chains/arbSepolia.ts +14 -0
- package/chains/chains/baseSepolia.ts +14 -0
- package/chains/chains/hardhat.ts +15 -0
- package/chains/chains/localcofhe.ts +14 -0
- package/chains/chains/sepolia.ts +14 -0
- package/chains/chains.test.ts +50 -0
- package/chains/defineChain.ts +18 -0
- package/chains/index.ts +35 -0
- package/chains/types.ts +32 -0
- package/core/baseBuilder.ts +119 -0
- package/core/client.test.ts +315 -0
- package/core/client.ts +292 -0
- package/core/clientTypes.ts +108 -0
- package/core/config.test.ts +235 -0
- package/core/config.ts +220 -0
- package/core/decrypt/MockQueryDecrypterAbi.ts +129 -0
- package/core/decrypt/cofheMocksSealOutput.ts +57 -0
- package/core/decrypt/decryptHandleBuilder.ts +287 -0
- package/core/decrypt/decryptUtils.ts +28 -0
- package/core/decrypt/tnSealOutputV1.ts +59 -0
- package/core/decrypt/tnSealOutputV2.ts +298 -0
- package/core/encrypt/MockZkVerifierAbi.ts +106 -0
- package/core/encrypt/cofheMocksZkVerifySign.ts +284 -0
- package/core/encrypt/encryptInputsBuilder.test.ts +751 -0
- package/core/encrypt/encryptInputsBuilder.ts +560 -0
- package/core/encrypt/encryptUtils.ts +67 -0
- package/core/encrypt/zkPackProveVerify.ts +335 -0
- package/core/error.ts +168 -0
- package/core/fetchKeys.test.ts +195 -0
- package/core/fetchKeys.ts +144 -0
- package/core/index.ts +89 -0
- package/core/keyStore.test.ts +226 -0
- package/core/keyStore.ts +154 -0
- package/core/permits.test.ts +494 -0
- package/core/permits.ts +200 -0
- package/core/types.ts +398 -0
- package/core/utils.ts +130 -0
- package/dist/adapters.cjs +88 -0
- package/dist/adapters.d.cts +14576 -0
- package/dist/adapters.d.ts +14576 -0
- package/dist/adapters.js +83 -0
- package/dist/chains.cjs +114 -0
- package/dist/chains.d.cts +121 -0
- package/dist/chains.d.ts +121 -0
- package/dist/chains.js +1 -0
- package/dist/chunk-UGBVZNRT.js +818 -0
- package/dist/chunk-WEAZ25JO.js +105 -0
- package/dist/chunk-WGCRJCBR.js +2523 -0
- package/dist/clientTypes-5_1nwtUe.d.cts +914 -0
- package/dist/clientTypes-Es7fyi65.d.ts +914 -0
- package/dist/core.cjs +3414 -0
- package/dist/core.d.cts +111 -0
- package/dist/core.d.ts +111 -0
- package/dist/core.js +3 -0
- package/dist/node.cjs +3286 -0
- package/dist/node.d.cts +22 -0
- package/dist/node.d.ts +22 -0
- package/dist/node.js +91 -0
- package/dist/permit-fUSe6KKq.d.cts +349 -0
- package/dist/permit-fUSe6KKq.d.ts +349 -0
- package/dist/permits.cjs +871 -0
- package/dist/permits.d.cts +1045 -0
- package/dist/permits.d.ts +1045 -0
- package/dist/permits.js +1 -0
- package/dist/types-KImPrEIe.d.cts +48 -0
- package/dist/types-KImPrEIe.d.ts +48 -0
- package/dist/web.cjs +3478 -0
- package/dist/web.d.cts +38 -0
- package/dist/web.d.ts +38 -0
- package/dist/web.js +240 -0
- package/dist/zkProve.worker.cjs +93 -0
- package/dist/zkProve.worker.d.cts +2 -0
- package/dist/zkProve.worker.d.ts +2 -0
- package/dist/zkProve.worker.js +91 -0
- package/node/client.test.ts +147 -0
- package/node/config.test.ts +68 -0
- package/node/encryptInputs.test.ts +155 -0
- package/node/index.ts +97 -0
- package/node/storage.ts +51 -0
- package/package.json +27 -15
- package/permits/index.ts +68 -0
- package/permits/localstorage.test.ts +117 -0
- package/permits/permit.test.ts +477 -0
- package/permits/permit.ts +405 -0
- package/permits/sealing.test.ts +84 -0
- package/permits/sealing.ts +131 -0
- package/permits/signature.ts +79 -0
- package/permits/store.test.ts +128 -0
- package/permits/store.ts +166 -0
- package/permits/test-utils.ts +20 -0
- package/permits/types.ts +191 -0
- package/permits/utils.ts +62 -0
- package/permits/validation.test.ts +288 -0
- package/permits/validation.ts +369 -0
- package/web/client.web.test.ts +147 -0
- package/web/config.web.test.ts +69 -0
- package/web/encryptInputs.web.test.ts +172 -0
- package/web/index.ts +161 -0
- package/web/storage.ts +34 -0
- package/web/worker.builder.web.test.ts +148 -0
- package/web/worker.config.web.test.ts +329 -0
- package/web/worker.output.web.test.ts +84 -0
- package/web/workerManager.test.ts +80 -0
- package/web/workerManager.ts +214 -0
- package/web/workerManager.web.test.ts +114 -0
- 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
|
+
});
|
package/core/keyStore.ts
ADDED
|
@@ -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
|
+
}
|