@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,335 @@
1
+ import { CofhesdkError, CofhesdkErrorCode } from '../error.js';
2
+ import { type EncryptableItem, FheTypes } from '../types.js';
3
+ import { toBigIntOrThrow, validateBigIntInRange, toHexString, hexToBytes } from '../utils.js';
4
+
5
+ // ===== TYPES =====
6
+
7
+ /**
8
+ * Worker function type for ZK proof generation
9
+ * Platform-specific implementations (web) can provide this to enable worker-based proofs
10
+ */
11
+ export type ZkProveWorkerFunction = (
12
+ fheKeyHex: string,
13
+ crsHex: string,
14
+ items: EncryptableItem[],
15
+ metadata: Uint8Array
16
+ ) => Promise<Uint8Array>;
17
+
18
+ /**
19
+ * Message sent from main thread to worker to request proof generation
20
+ */
21
+ export interface ZkProveWorkerRequest {
22
+ id: string;
23
+ type: 'zkProve';
24
+ fheKeyHex: string;
25
+ crsHex: string;
26
+ items: Array<{
27
+ utype: string;
28
+ data: any;
29
+ }>;
30
+ metadata: number[]; // Uint8Array serialized as array
31
+ }
32
+
33
+ /**
34
+ * Message sent from worker back to main thread with proof result
35
+ */
36
+ export interface ZkProveWorkerResponse {
37
+ id: string;
38
+ type: 'success' | 'error' | 'ready';
39
+ result?: number[]; // Uint8Array serialized as array
40
+ error?: string;
41
+ }
42
+
43
+ export type VerifyResultRaw = {
44
+ ct_hash: string;
45
+ signature: string;
46
+ recid: number;
47
+ };
48
+
49
+ export type VerifyResult = {
50
+ ct_hash: string;
51
+ signature: string;
52
+ };
53
+
54
+ export type ZkProvenCiphertextList = {
55
+ serialize(): Uint8Array;
56
+ };
57
+
58
+ export type ZkCompactPkeCrs = {
59
+ free(): void;
60
+ serialize(compress: boolean): Uint8Array;
61
+ safe_serialize(serialized_size_limit: bigint): Uint8Array;
62
+ };
63
+
64
+ export type ZkCompactPkeCrsConstructor = {
65
+ deserialize(buffer: Uint8Array): ZkCompactPkeCrs;
66
+ safe_deserialize(buffer: Uint8Array, serialized_size_limit: bigint): ZkCompactPkeCrs;
67
+ from_config(config: unknown, max_num_bits: number): ZkCompactPkeCrs;
68
+ deserialize_from_public_params(buffer: Uint8Array): ZkCompactPkeCrs;
69
+ safe_deserialize_from_public_params(buffer: Uint8Array, serialized_size_limit: bigint): ZkCompactPkeCrs;
70
+ };
71
+
72
+ export type ZkCiphertextListBuilder = {
73
+ push_boolean(data: boolean): void;
74
+ push_u8(data: number): void;
75
+ push_u16(data: number): void;
76
+ push_u32(data: number): void;
77
+ push_u64(data: bigint): void;
78
+ push_u128(data: bigint): void;
79
+ push_u160(data: bigint): void;
80
+ build_with_proof_packed(
81
+ crs: ZkCompactPkeCrs,
82
+ metadata: Uint8Array,
83
+ computeLoad: 1 // ZkComputeLoad.Verify
84
+ ): ZkProvenCiphertextList;
85
+ };
86
+
87
+ export type ZkBuilderAndCrsGenerator = (
88
+ fhe: string,
89
+ crs: string
90
+ ) => { zkBuilder: ZkCiphertextListBuilder; zkCrs: ZkCompactPkeCrs };
91
+
92
+ // ===== CONSTANTS =====
93
+
94
+ export const MAX_UINT8: bigint = 255n;
95
+ export const MAX_UINT16: bigint = 65535n;
96
+ export const MAX_UINT32: bigint = 4294967295n;
97
+ export const MAX_UINT64: bigint = 18446744073709551615n; // 2^64 - 1
98
+ export const MAX_UINT128: bigint = 340282366920938463463374607431768211455n; // 2^128 - 1
99
+ export const MAX_UINT256: bigint = 115792089237316195423570985008687907853269984665640564039457584007913129640319n; // 2^256 - 1
100
+ export const MAX_UINT160: bigint = 1461501637330902918203684832716283019655932542975n; // 2^160 - 1
101
+ export const MAX_ENCRYPTABLE_BITS: number = 2048;
102
+
103
+ // ===== CORE FUNCTIONS =====
104
+
105
+ export const zkPack = (items: EncryptableItem[], builder: ZkCiphertextListBuilder): ZkCiphertextListBuilder => {
106
+ let totalBits = 0;
107
+ for (const item of items) {
108
+ switch (item.utype) {
109
+ case FheTypes.Bool: {
110
+ builder.push_boolean(item.data);
111
+ totalBits += 1;
112
+ break;
113
+ }
114
+ case FheTypes.Uint8: {
115
+ const bint = toBigIntOrThrow(item.data);
116
+ validateBigIntInRange(bint, MAX_UINT8);
117
+ builder.push_u8(parseInt(bint.toString()));
118
+ totalBits += 8;
119
+ break;
120
+ }
121
+ case FheTypes.Uint16: {
122
+ const bint = toBigIntOrThrow(item.data);
123
+ validateBigIntInRange(bint, MAX_UINT16);
124
+ builder.push_u16(parseInt(bint.toString()));
125
+ totalBits += 16;
126
+ break;
127
+ }
128
+ case FheTypes.Uint32: {
129
+ const bint = toBigIntOrThrow(item.data);
130
+ validateBigIntInRange(bint, MAX_UINT32);
131
+ builder.push_u32(parseInt(bint.toString()));
132
+ totalBits += 32;
133
+ break;
134
+ }
135
+ case FheTypes.Uint64: {
136
+ const bint = toBigIntOrThrow(item.data);
137
+ validateBigIntInRange(bint, MAX_UINT64);
138
+ builder.push_u64(bint);
139
+ totalBits += 64;
140
+ break;
141
+ }
142
+ case FheTypes.Uint128: {
143
+ const bint = toBigIntOrThrow(item.data);
144
+ validateBigIntInRange(bint, MAX_UINT128);
145
+ builder.push_u128(bint);
146
+ totalBits += 128;
147
+ break;
148
+ }
149
+ // [U256-DISABLED]
150
+ // case FheTypes.Uint256: {
151
+ // const bint = toBigIntOrThrow(item.data);
152
+ // validateBigIntInRange(bint, MAX_UINT256);
153
+ // builder.push_u256(bint);
154
+ // totalBits += 256;
155
+ // break;
156
+ // }
157
+ case FheTypes.Uint160: {
158
+ const bint = toBigIntOrThrow(item.data);
159
+ validateBigIntInRange(bint, MAX_UINT160);
160
+ builder.push_u160(bint);
161
+ totalBits += 160;
162
+ break;
163
+ }
164
+ default: {
165
+ throw new CofhesdkError({
166
+ code: CofhesdkErrorCode.ZkPackFailed,
167
+ message: `Invalid utype: ${(item as any).utype}`,
168
+ hint: `Ensure that the utype is valid, using the Encryptable type, for example: Encryptable.uint128(100n)`,
169
+ context: {
170
+ item,
171
+ },
172
+ });
173
+ }
174
+ }
175
+ }
176
+
177
+ if (totalBits > MAX_ENCRYPTABLE_BITS) {
178
+ throw new CofhesdkError({
179
+ code: CofhesdkErrorCode.ZkPackFailed,
180
+ message: `Total bits ${totalBits} exceeds ${MAX_ENCRYPTABLE_BITS}`,
181
+ hint: `Ensure that the total bits of the items to encrypt does not exceed ${MAX_ENCRYPTABLE_BITS}`,
182
+ context: {
183
+ totalBits,
184
+ maxBits: MAX_ENCRYPTABLE_BITS,
185
+ items,
186
+ },
187
+ });
188
+ }
189
+
190
+ return builder;
191
+ };
192
+
193
+ /**
194
+ * Generates ZK proof using Web Worker (offloads heavy WASM computation)
195
+ * Serializes items and calls the platform-specific worker function
196
+ * @param workerFn - Platform-specific worker function (provided by web/index.ts)
197
+ * @param fheKeyHex - Hex-encoded FHE public key for worker deserialization
198
+ * @param crsHex - Hex-encoded CRS for worker deserialization
199
+ * @param items - Encryptable items to pack in the worker
200
+ * @param metadata - Pre-constructed ZK PoK metadata
201
+ * @returns The serialized proven ciphertext list
202
+ */
203
+ export const zkProveWithWorker = async (
204
+ workerFn: ZkProveWorkerFunction,
205
+ fheKeyHex: string,
206
+ crsHex: string,
207
+ items: EncryptableItem[],
208
+ metadata: Uint8Array
209
+ ): Promise<Uint8Array> => {
210
+ return await workerFn(fheKeyHex, crsHex, items, metadata);
211
+ };
212
+
213
+ /**
214
+ * Generates ZK proof using main thread (synchronous WASM)
215
+ * This is the fallback when workers are disabled or unavailable
216
+ * @param builder - The ZK ciphertext list builder with packed inputs
217
+ * @param crs - The Compact PKE CRS for proof generation
218
+ * @param metadata - Pre-constructed ZK PoK metadata
219
+ * @returns The serialized proven ciphertext list
220
+ */
221
+ export const zkProve = async (
222
+ builder: ZkCiphertextListBuilder,
223
+ crs: ZkCompactPkeCrs,
224
+ metadata: Uint8Array
225
+ ): Promise<Uint8Array> => {
226
+ return new Promise((resolve) => {
227
+ setTimeout(() => {
228
+ const compactList = builder.build_with_proof_packed(
229
+ crs,
230
+ metadata,
231
+ 1 // ZkComputeLoad.Verify
232
+ );
233
+
234
+ resolve(compactList.serialize());
235
+ }, 0);
236
+ });
237
+ };
238
+
239
+ /**
240
+ * Constructs the ZK Proof of Knowledge metadata for the proof
241
+ * @internal - Used internally within the encrypt module
242
+ */
243
+ export const constructZkPoKMetadata = (accountAddr: string, securityZone: number, chainId: number): Uint8Array => {
244
+ // Decode the account address from hex
245
+ const accountAddrNoPrefix = accountAddr.startsWith('0x') ? accountAddr.slice(2) : accountAddr;
246
+ const accountBytes = hexToBytes(accountAddrNoPrefix);
247
+
248
+ // Encode chainId as 32 bytes (u256) in big-endian format
249
+ const chainIdBytes = new Uint8Array(32);
250
+
251
+ // Since chain IDs are typically small numbers, we can just encode them
252
+ // directly without BigInt operations, filling only the necessary bytes
253
+ // from the right (least significant)
254
+ let value = chainId;
255
+ for (let i = 31; i >= 0 && value > 0; i--) {
256
+ chainIdBytes[i] = value & 0xff;
257
+ value = value >>> 8;
258
+ }
259
+
260
+ const metadata = new Uint8Array(1 + accountBytes.length + 32);
261
+ metadata[0] = securityZone;
262
+ metadata.set(accountBytes, 1);
263
+ metadata.set(chainIdBytes, 1 + accountBytes.length);
264
+
265
+ return metadata;
266
+ };
267
+
268
+ export const zkVerify = async (
269
+ verifierUrl: string,
270
+ serializedBytes: Uint8Array,
271
+ address: string,
272
+ securityZone: number,
273
+ chainId: number
274
+ ): Promise<VerifyResult[]> => {
275
+ // Convert bytearray to hex string
276
+ const packed_list = toHexString(serializedBytes);
277
+
278
+ const sz_byte = new Uint8Array([securityZone]);
279
+
280
+ // Construct request payload
281
+ const payload = {
282
+ packed_list,
283
+ account_addr: address,
284
+ security_zone: sz_byte[0],
285
+ chain_id: chainId,
286
+ };
287
+
288
+ const body = JSON.stringify(payload);
289
+
290
+ // Send request to verification server
291
+ try {
292
+ const response = await fetch(`${verifierUrl}/verify`, {
293
+ method: 'POST',
294
+ headers: {
295
+ 'Content-Type': 'application/json',
296
+ },
297
+ body,
298
+ });
299
+
300
+ if (!response.ok) {
301
+ // Get the response body as text for better error details
302
+ const errorBody = await response.text();
303
+ throw new CofhesdkError({
304
+ code: CofhesdkErrorCode.ZkVerifyFailed,
305
+ message: `HTTP error! ZK proof verification failed - ${errorBody}`,
306
+ });
307
+ }
308
+
309
+ const json = (await response.json()) as { status: string; data: VerifyResultRaw[]; error: string };
310
+
311
+ if (json.status !== 'success') {
312
+ throw new CofhesdkError({
313
+ code: CofhesdkErrorCode.ZkVerifyFailed,
314
+ message: `ZK proof verification response malformed - ${json.error}`,
315
+ });
316
+ }
317
+
318
+ return json.data.map(({ ct_hash, signature, recid }) => {
319
+ return {
320
+ ct_hash,
321
+ signature: concatSigRecid(signature, recid),
322
+ };
323
+ });
324
+ } catch (e) {
325
+ throw new CofhesdkError({
326
+ code: CofhesdkErrorCode.ZkVerifyFailed,
327
+ message: `ZK proof verification failed`,
328
+ cause: e instanceof Error ? e : undefined,
329
+ });
330
+ }
331
+ };
332
+
333
+ const concatSigRecid = (signature: string, recid: number): string => {
334
+ return signature + (recid + 27).toString(16).padStart(2, '0');
335
+ };
package/core/error.ts ADDED
@@ -0,0 +1,168 @@
1
+ export enum CofhesdkErrorCode {
2
+ InternalError = 'INTERNAL_ERROR',
3
+ UnknownEnvironment = 'UNKNOWN_ENVIRONMENT',
4
+ InitTfheFailed = 'INIT_TFHE_FAILED',
5
+ InitViemFailed = 'INIT_VIEM_FAILED',
6
+ InitEthersFailed = 'INIT_ETHERS_FAILED',
7
+ NotConnected = 'NOT_CONNECTED',
8
+ MissingPublicClient = 'MISSING_PUBLIC_CLIENT',
9
+ MissingWalletClient = 'MISSING_WALLET_CLIENT',
10
+ MissingProviderParam = 'MISSING_PROVIDER_PARAM',
11
+ EmptySecurityZonesParam = 'EMPTY_SECURITY_ZONES_PARAM',
12
+ InvalidPermitData = 'INVALID_PERMIT_DATA',
13
+ InvalidPermitDomain = 'INVALID_PERMIT_DOMAIN',
14
+ PermitNotFound = 'PERMIT_NOT_FOUND',
15
+ CannotRemoveLastPermit = 'CANNOT_REMOVE_LAST_PERMIT',
16
+ AccountUninitialized = 'ACCOUNT_UNINITIALIZED',
17
+ ChainIdUninitialized = 'CHAIN_ID_UNINITIALIZED',
18
+ SealOutputFailed = 'SEAL_OUTPUT_FAILED',
19
+ SealOutputReturnedNull = 'SEAL_OUTPUT_RETURNED_NULL',
20
+ InvalidUtype = 'INVALID_UTYPE',
21
+ DecryptFailed = 'DECRYPT_FAILED',
22
+ DecryptReturnedNull = 'DECRYPT_RETURNED_NULL',
23
+ ZkMocksInsertCtHashesFailed = 'ZK_MOCKS_INSERT_CT_HASHES_FAILED',
24
+ ZkMocksCalcCtHashesFailed = 'ZK_MOCKS_CALC_CT_HASHES_FAILED',
25
+ ZkMocksVerifySignFailed = 'ZK_MOCKS_VERIFY_SIGN_FAILED',
26
+ ZkMocksCreateProofSignatureFailed = 'ZK_MOCKS_CREATE_PROOF_SIGNATURE_FAILED',
27
+ ZkVerifyFailed = 'ZK_VERIFY_FAILED',
28
+ ZkPackFailed = 'ZK_PACK_FAILED',
29
+ ZkProveFailed = 'ZK_PROVE_FAILED',
30
+ EncryptRemainingInItems = 'ENCRYPT_REMAINING_IN_ITEMS',
31
+ ZkUninitialized = 'ZK_UNINITIALIZED',
32
+ ZkVerifierUrlUninitialized = 'ZK_VERIFIER_URL_UNINITIALIZED',
33
+ ThresholdNetworkUrlUninitialized = 'THRESHOLD_NETWORK_URL_UNINITIALIZED',
34
+ MissingConfig = 'MISSING_CONFIG',
35
+ UnsupportedChain = 'UNSUPPORTED_CHAIN',
36
+ MissingZkBuilderAndCrsGenerator = 'MISSING_ZK_BUILDER_AND_CRS_GENERATOR',
37
+ MissingTfhePublicKeyDeserializer = 'MISSING_TFHE_PUBLIC_KEY_DESERIALIZER',
38
+ MissingCompactPkeCrsDeserializer = 'MISSING_COMPACT_PKE_CRS_DESERIALIZER',
39
+ MissingFheKey = 'MISSING_FHE_KEY',
40
+ MissingCrs = 'MISSING_CRS',
41
+ FetchKeysFailed = 'FETCH_KEYS_FAILED',
42
+ PublicWalletGetChainIdFailed = 'PUBLIC_WALLET_GET_CHAIN_ID_FAILED',
43
+ PublicWalletGetAddressesFailed = 'PUBLIC_WALLET_GET_ADDRESSES_FAILED',
44
+ RehydrateKeysStoreFailed = 'REHYDRATE_KEYS_STORE_FAILED',
45
+ }
46
+
47
+ export type CofhesdkErrorParams = {
48
+ code: CofhesdkErrorCode;
49
+ message: string;
50
+ cause?: Error;
51
+ hint?: string;
52
+ context?: Record<string, unknown>;
53
+ };
54
+
55
+ /**
56
+ * CofhesdkError class
57
+ * This class is used to create errors that are specific to the CoFHE SDK
58
+ * It extends the Error class and adds a code, cause, hint, and context
59
+ * The code is used to identify the type of error
60
+ * The cause is used to indicate the inner error that caused the CofhesdkError
61
+ * The hint is used to provide a hint about how to fix the error
62
+ * The context is used to provide additional context about the state that caused the error
63
+ * The serialize method is used to serialize the error to a JSON string
64
+ * The toString method is used to provide a human-readable string representation of the error
65
+ */
66
+ export class CofhesdkError extends Error {
67
+ public readonly code: CofhesdkErrorCode;
68
+ public readonly cause?: Error;
69
+ public readonly hint?: string;
70
+ public readonly context?: Record<string, unknown>;
71
+
72
+ constructor({ code, message, cause, hint, context }: CofhesdkErrorParams) {
73
+ // If there's a cause, append its message to provide full context
74
+ const fullMessage = cause ? `${message} | Caused by: ${cause.message}` : message;
75
+
76
+ super(fullMessage);
77
+ this.name = 'CofhesdkError';
78
+ this.code = code;
79
+ this.cause = cause;
80
+ this.hint = hint;
81
+ this.context = context;
82
+
83
+ // Maintains proper stack trace for where our error was thrown (only available on V8)
84
+ if (Error.captureStackTrace) {
85
+ Error.captureStackTrace(this, CofhesdkError);
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Creates a CofhesdkError from an unknown error
91
+ * If the error is a CofhesdkError, it is returned unchanged, else a new CofhesdkError is created
92
+ * If a wrapperError is provided, it is used to create the new CofhesdkError, else a default is used
93
+ */
94
+ static fromError(error: unknown, wrapperError?: CofhesdkErrorParams): CofhesdkError {
95
+ if (isCofhesdkError(error)) return error;
96
+
97
+ const cause = error instanceof Error ? error : new Error(`${error}`);
98
+
99
+ return new CofhesdkError({
100
+ code: wrapperError?.code ?? CofhesdkErrorCode.InternalError,
101
+ message: wrapperError?.message ?? 'An internal error occurred',
102
+ hint: wrapperError?.hint,
103
+ context: wrapperError?.context,
104
+ cause: cause,
105
+ });
106
+ }
107
+
108
+ /**
109
+ * Serializes the error to JSON string with proper handling of Error objects
110
+ */
111
+ serialize(): string {
112
+ return bigintSafeJsonStringify({
113
+ name: this.name,
114
+ code: this.code,
115
+ message: this.message,
116
+ hint: this.hint,
117
+ context: this.context,
118
+ cause: this.cause
119
+ ? {
120
+ name: this.cause.name,
121
+ message: this.cause.message,
122
+ stack: this.cause.stack,
123
+ }
124
+ : undefined,
125
+ stack: this.stack,
126
+ });
127
+ }
128
+
129
+ /**
130
+ * Returns a human-readable string representation of the error
131
+ */
132
+ toString(): string {
133
+ const parts = [`${this.name} [${this.code}]: ${this.message}`];
134
+
135
+ if (this.hint) {
136
+ parts.push(`Hint: ${this.hint}`);
137
+ }
138
+
139
+ if (this.context && Object.keys(this.context).length > 0) {
140
+ parts.push(`Context: ${bigintSafeJsonStringify(this.context)}`);
141
+ }
142
+
143
+ if (this.stack) {
144
+ parts.push(`\nStack trace:`);
145
+ parts.push(this.stack);
146
+ }
147
+
148
+ if (this.cause) {
149
+ parts.push(`\nCaused by: ${this.cause.name}: ${this.cause.message}`);
150
+ if (this.cause.stack) {
151
+ parts.push(this.cause.stack);
152
+ }
153
+ }
154
+
155
+ return parts.join('\n');
156
+ }
157
+ }
158
+
159
+ const bigintSafeJsonStringify = (value: unknown): string => {
160
+ return JSON.stringify(value, (key, value) => {
161
+ if (typeof value === 'bigint') {
162
+ return `${value}n`;
163
+ }
164
+ return value;
165
+ });
166
+ };
167
+
168
+ export const isCofhesdkError = (error: unknown): error is CofhesdkError => error instanceof CofhesdkError;
@@ -0,0 +1,195 @@
1
+ /* eslint-disable turbo/no-undeclared-env-vars */
2
+ /* eslint-disable no-undef */
3
+
4
+ import { sepolia, arbSepolia } from '@/chains';
5
+
6
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
7
+ import { fetchKeys } from './fetchKeys.js';
8
+ import { type CofhesdkConfig, createCofhesdkConfigBase } from './config.js';
9
+ import { createKeysStore, type KeysStorage } from './keyStore.js';
10
+
11
+ describe('fetchKeys', () => {
12
+ let config: CofhesdkConfig;
13
+ let mockTfhePublicKeyDeserializer: any;
14
+ let mockCompactPkeCrsDeserializer: any;
15
+ let keysStorage: KeysStorage;
16
+
17
+ beforeEach(async () => {
18
+ // Reset all mocks
19
+ vi.clearAllMocks();
20
+
21
+ // Setup config with real chains
22
+ config = createCofhesdkConfigBase({
23
+ supportedChains: [sepolia, arbSepolia],
24
+ });
25
+
26
+ // Setup mock serializers
27
+ mockTfhePublicKeyDeserializer = vi.fn();
28
+ mockCompactPkeCrsDeserializer = vi.fn();
29
+
30
+ // Create a fresh keysStorage instance for each test (non-persisted)
31
+ keysStorage = createKeysStore(null);
32
+ });
33
+
34
+ afterEach(async () => {
35
+ vi.restoreAllMocks();
36
+ });
37
+
38
+ it('should fetch and store FHE public key and CRS for Sepolia when not cached', async () => {
39
+ const [[fheKey, fheKeyFetchedFromCoFHE], [crs, crsFetchedFromCoFHE]] = await fetchKeys(
40
+ config,
41
+ sepolia.id,
42
+ 0,
43
+ mockTfhePublicKeyDeserializer,
44
+ mockCompactPkeCrsDeserializer,
45
+ keysStorage
46
+ );
47
+
48
+ expect(fheKeyFetchedFromCoFHE).toBe(true);
49
+ expect(crsFetchedFromCoFHE).toBe(true);
50
+
51
+ // Verify keys were stored
52
+ const storedFheKey = keysStorage.getFheKey(sepolia.id, 0);
53
+ const storedCrs = keysStorage.getCrs(sepolia.id);
54
+
55
+ expect(storedFheKey).toBeDefined();
56
+ expect(storedCrs).toBeDefined();
57
+ expect(mockTfhePublicKeyDeserializer).toHaveBeenCalled();
58
+ expect(mockCompactPkeCrsDeserializer).toHaveBeenCalled();
59
+ });
60
+
61
+ it('should fetch and store FHE public key and CRS for Arbitrum Sepolia when not cached', async () => {
62
+ const [[fheKey, fheKeyFetchedFromCoFHE], [crs, crsFetchedFromCoFHE]] = await fetchKeys(
63
+ config,
64
+ arbSepolia.id,
65
+ 0,
66
+ mockTfhePublicKeyDeserializer,
67
+ mockCompactPkeCrsDeserializer,
68
+ keysStorage
69
+ );
70
+
71
+ expect(fheKeyFetchedFromCoFHE).toBe(true);
72
+ expect(crsFetchedFromCoFHE).toBe(true);
73
+
74
+ // Verify keys were stored
75
+ const storedFheKey = keysStorage.getFheKey(arbSepolia.id, 0);
76
+ const storedCrs = keysStorage.getCrs(arbSepolia.id);
77
+
78
+ expect(storedFheKey).toBeDefined();
79
+ expect(storedCrs).toBeDefined();
80
+ expect(mockTfhePublicKeyDeserializer).toHaveBeenCalled();
81
+ expect(mockCompactPkeCrsDeserializer).toHaveBeenCalled();
82
+ });
83
+
84
+ it('should not fetch FHE key if already cached', async () => {
85
+ // Pre-populate with a cached key
86
+ const mockCachedKey = '0x1234567890';
87
+ keysStorage.setFheKey(sepolia.id, 0, mockCachedKey);
88
+
89
+ const [[fheKey, fheKeyFetchedFromCoFHE], [crs, crsFetchedFromCoFHE]] = await fetchKeys(
90
+ config,
91
+ sepolia.id,
92
+ 0,
93
+ mockTfhePublicKeyDeserializer,
94
+ mockCompactPkeCrsDeserializer,
95
+ keysStorage
96
+ );
97
+
98
+ expect(fheKeyFetchedFromCoFHE).toBe(false);
99
+ expect(crsFetchedFromCoFHE).toBe(true);
100
+
101
+ // Verify the cached key wasn't overwritten
102
+ const retrievedKey = keysStorage.getFheKey(sepolia.id, 0);
103
+ expect(retrievedKey).toEqual(mockCachedKey);
104
+
105
+ // Verify CRS was still fetched
106
+ const retrievedCrs = keysStorage.getCrs(sepolia.id);
107
+ expect(retrievedCrs).toBeDefined();
108
+ });
109
+
110
+ it('should not fetch CRS if already cached', async () => {
111
+ // Pre-populate with a cached CRS
112
+ const mockCachedCrs = '0x2345678901';
113
+ keysStorage.setCrs(sepolia.id, mockCachedCrs);
114
+
115
+ const [[fheKey, fheKeyFetchedFromCoFHE], [crs, crsFetchedFromCoFHE]] = await fetchKeys(
116
+ config,
117
+ sepolia.id,
118
+ 0,
119
+ mockTfhePublicKeyDeserializer,
120
+ mockCompactPkeCrsDeserializer,
121
+ keysStorage
122
+ );
123
+
124
+ expect(fheKeyFetchedFromCoFHE).toBe(true);
125
+ expect(crsFetchedFromCoFHE).toBe(false);
126
+
127
+ // Verify the cached CRS wasn't overwritten
128
+ const retrievedCrs = keysStorage.getCrs(sepolia.id);
129
+ expect(retrievedCrs).toEqual(mockCachedCrs);
130
+
131
+ // Verify FHE key was still fetched
132
+ const retrievedKey = keysStorage.getFheKey(sepolia.id, 0);
133
+ expect(retrievedKey).toBeDefined();
134
+ });
135
+
136
+ it('should not make any network calls if both keys are cached', async () => {
137
+ // Pre-populate both keys
138
+ const mockCachedKey = '0x1234567890';
139
+ const mockCachedCrs = '0x2345678901';
140
+ keysStorage.setFheKey(sepolia.id, 0, mockCachedKey);
141
+ keysStorage.setCrs(sepolia.id, mockCachedCrs);
142
+
143
+ const [[fheKey, fheKeyFetchedFromCoFHE], [crs, crsFetchedFromCoFHE]] = await fetchKeys(
144
+ config,
145
+ sepolia.id,
146
+ 0,
147
+ mockTfhePublicKeyDeserializer,
148
+ mockCompactPkeCrsDeserializer,
149
+ keysStorage
150
+ );
151
+
152
+ expect(fheKeyFetchedFromCoFHE).toBe(false);
153
+ expect(crsFetchedFromCoFHE).toBe(false);
154
+
155
+ // Verify both keys remain unchanged
156
+ const retrievedKey = keysStorage.getFheKey(sepolia.id, 0);
157
+ const retrievedCrs = keysStorage.getCrs(sepolia.id);
158
+
159
+ expect(retrievedKey).toEqual(mockCachedKey);
160
+ expect(retrievedCrs).toEqual(mockCachedCrs);
161
+ });
162
+
163
+ it('should throw error for unsupported chain ID', async () => {
164
+ await expect(
165
+ fetchKeys(
166
+ config,
167
+ 999, // Non-existent chain
168
+ 0,
169
+ mockTfhePublicKeyDeserializer,
170
+ mockCompactPkeCrsDeserializer,
171
+ keysStorage
172
+ )
173
+ ).rejects.toThrow('Config does not support chain <999>');
174
+ });
175
+
176
+ it('should throw error when FHE public key serialization fails', async () => {
177
+ mockTfhePublicKeyDeserializer.mockImplementation(() => {
178
+ throw new Error('Serialization failed');
179
+ });
180
+
181
+ await expect(
182
+ fetchKeys(config, sepolia.id, 0, mockTfhePublicKeyDeserializer, mockCompactPkeCrsDeserializer, keysStorage)
183
+ ).rejects.toThrow('Error serializing FHE publicKey; Error: Serialization failed');
184
+ });
185
+
186
+ it('should throw error when CRS serialization fails', async () => {
187
+ mockCompactPkeCrsDeserializer.mockImplementation(() => {
188
+ throw new Error('Serialization failed');
189
+ });
190
+
191
+ await expect(
192
+ fetchKeys(config, sepolia.id, 0, mockTfhePublicKeyDeserializer, mockCompactPkeCrsDeserializer, keysStorage)
193
+ ).rejects.toThrow('Error serializing CRS; Error: Serialization failed');
194
+ });
195
+ });