@cofhe/sdk 0.3.2 → 0.5.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 (97) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/adapters/{ethers5.test.ts → test/ethers5.test.ts} +2 -2
  3. package/adapters/{ethers6.test.ts → test/ethers6.test.ts} +2 -2
  4. package/adapters/{hardhat.hh2.test.ts → test/hardhat.hh2.test.ts} +2 -2
  5. package/adapters/{index.test.ts → test/index.test.ts} +1 -1
  6. package/adapters/{wagmi.test.ts → test/wagmi.test.ts} +1 -1
  7. package/chains/{chains.test.ts → test/chains.test.ts} +1 -1
  8. package/core/client.ts +15 -5
  9. package/core/clientTypes.ts +7 -5
  10. package/core/consts.ts +9 -0
  11. package/core/decrypt/cofheMocksDecryptForTx.ts +14 -3
  12. package/core/decrypt/decryptForTxBuilder.ts +24 -10
  13. package/core/decrypt/decryptForViewBuilder.ts +14 -7
  14. package/core/decrypt/polling.ts +14 -0
  15. package/core/decrypt/tnDecryptUtils.ts +65 -0
  16. package/core/decrypt/{tnDecrypt.ts → tnDecryptV1.ts} +7 -70
  17. package/core/decrypt/tnDecryptV2.ts +483 -0
  18. package/core/decrypt/tnSealOutputV2.ts +245 -104
  19. package/core/decrypt/verifyDecryptResult.ts +65 -0
  20. package/core/encrypt/cofheMocksZkVerifySign.ts +6 -6
  21. package/core/encrypt/zkPackProveVerify.ts +10 -19
  22. package/core/fetchKeys.ts +0 -2
  23. package/core/index.ts +9 -1
  24. package/core/keyStore.ts +5 -2
  25. package/core/permits.ts +8 -3
  26. package/core/{client.test.ts → test/client.test.ts} +7 -7
  27. package/core/{config.test.ts → test/config.test.ts} +1 -1
  28. package/core/test/decrypt.test.ts +252 -0
  29. package/core/test/decryptBuilders.test.ts +390 -0
  30. package/core/{encrypt → test}/encryptInputsBuilder.test.ts +61 -6
  31. package/core/{fetchKeys.test.ts → test/fetchKeys.test.ts} +3 -3
  32. package/core/{keyStore.test.ts → test/keyStore.test.ts} +5 -3
  33. package/core/{permits.test.ts → test/permits.test.ts} +42 -1
  34. package/core/test/pollCallbacks.test.ts +563 -0
  35. package/core/types.ts +21 -0
  36. package/dist/chains.d.cts +2 -2
  37. package/dist/chains.d.ts +2 -2
  38. package/dist/chunk-4FP4V35O.js +13 -0
  39. package/dist/{chunk-NWDKXBIP.js → chunk-MRCKUMOS.js} +62 -22
  40. package/dist/{chunk-LWMRB6SD.js → chunk-S7OKGLFD.js} +615 -198
  41. package/dist/{clientTypes-Y43CKbOz.d.cts → clientTypes-BSbwairE.d.cts} +38 -13
  42. package/dist/{clientTypes-PQha8zes.d.ts → clientTypes-DDmcgZ0a.d.ts} +38 -13
  43. package/dist/core.cjs +691 -235
  44. package/dist/core.d.cts +24 -6
  45. package/dist/core.d.ts +24 -6
  46. package/dist/core.js +3 -2
  47. package/dist/node.cjs +696 -237
  48. package/dist/node.d.cts +3 -3
  49. package/dist/node.d.ts +3 -3
  50. package/dist/node.js +14 -7
  51. package/dist/{permit-MZ502UBl.d.ts → permit-DnVMDT5h.d.cts} +34 -4
  52. package/dist/{permit-MZ502UBl.d.cts → permit-DnVMDT5h.d.ts} +34 -4
  53. package/dist/permits.cjs +66 -29
  54. package/dist/permits.d.cts +18 -13
  55. package/dist/permits.d.ts +18 -13
  56. package/dist/permits.js +2 -1
  57. package/dist/web.cjs +718 -242
  58. package/dist/web.d.cts +8 -4
  59. package/dist/web.d.ts +8 -4
  60. package/dist/web.js +34 -11
  61. package/dist/zkProve.worker.cjs +6 -3
  62. package/dist/zkProve.worker.js +5 -3
  63. package/node/index.ts +13 -4
  64. package/node/test/client.test.ts +25 -0
  65. package/node/test/config.test.ts +16 -0
  66. package/node/test/inherited.test.ts +244 -0
  67. package/node/test/tfheinit.test.ts +56 -0
  68. package/package.json +24 -22
  69. package/permits/permit.ts +31 -5
  70. package/permits/sealing.ts +1 -1
  71. package/permits/{localstorage.test.ts → test/localstorage.test.ts} +2 -2
  72. package/permits/{permit.test.ts → test/permit.test.ts} +35 -1
  73. package/permits/{sealing.test.ts → test/sealing.test.ts} +1 -1
  74. package/permits/{store.test.ts → test/store.test.ts} +2 -2
  75. package/permits/{validation.test.ts → test/validation.test.ts} +82 -6
  76. package/permits/types.ts +1 -1
  77. package/permits/validation.ts +42 -2
  78. package/web/const.ts +2 -0
  79. package/web/index.ts +20 -6
  80. package/web/storage.ts +18 -3
  81. package/web/{client.web.test.ts → test/client.web.test.ts} +13 -1
  82. package/web/test/config.web.test.ts +16 -0
  83. package/web/test/inherited.web.test.ts +245 -0
  84. package/web/test/tfheinit.web.test.ts +62 -0
  85. package/web/{worker.config.web.test.ts → test/worker.config.web.test.ts} +1 -1
  86. package/web/{worker.output.web.test.ts → test/worker.output.web.test.ts} +1 -1
  87. package/web/{workerManager.test.ts → test/workerManager.test.ts} +1 -1
  88. package/web/{workerManager.web.test.ts → test/workerManager.web.test.ts} +1 -1
  89. package/web/zkProve.worker.ts +4 -3
  90. package/node/client.test.ts +0 -147
  91. package/node/config.test.ts +0 -68
  92. package/node/encryptInputs.test.ts +0 -155
  93. package/web/config.web.test.ts +0 -69
  94. package/web/encryptInputs.web.test.ts +0 -172
  95. package/web/worker.builder.web.test.ts +0 -148
  96. /package/dist/{types-YiAC4gig.d.cts → types-C07FK-cL.d.cts} +0 -0
  97. /package/dist/{types-YiAC4gig.d.ts → types-C07FK-cL.d.ts} +0 -0
@@ -0,0 +1,483 @@
1
+ import { type Permission } from '@/permits';
2
+
3
+ import { CofheError, CofheErrorCode } from '../error';
4
+ import { type DecryptPollCallbackFunction } from '../types';
5
+ import { normalizeTnSignature, parseDecryptedBytesToBigInt } from './tnDecryptUtils';
6
+ import { computeMinuteRampPollIntervalMs } from './polling.js';
7
+
8
+ // Polling configuration
9
+ const POLL_INTERVAL_MS = 1000; // 1 second
10
+ const POLL_MAX_INTERVAL_MS = 10_000; // 10 seconds
11
+ const DECRYPT_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes total across submit + poll
12
+ const SUBMIT_RETRY_INTERVAL_MS = 1000; // 1 second
13
+
14
+ type DecryptSubmitResponseV2 = {
15
+ request_id: string | null;
16
+ status?: string;
17
+ is_succeed?: boolean;
18
+ decrypted?: number[];
19
+ signature?: string;
20
+ encryption_type?: number;
21
+ error_message?: string | null;
22
+ message?: string;
23
+ };
24
+
25
+ type DecryptSubmitResultV2 =
26
+ | { kind: 'request_id'; requestId: string }
27
+ | { kind: 'completed'; decryptedValue: bigint; signature: `0x${string}` };
28
+
29
+ type DecryptStatusResponseV2 = {
30
+ request_id: string;
31
+ status: 'PROCESSING' | 'COMPLETED';
32
+ submitted_at: string;
33
+ completed_at?: string;
34
+ is_succeed?: boolean;
35
+ decrypted?: number[];
36
+ signature?: string;
37
+ encryption_type?: number;
38
+ error_message?: string | null;
39
+ };
40
+
41
+ function assertDecryptSubmitResponseV2(value: unknown): DecryptSubmitResponseV2 {
42
+ if (value == null || typeof value !== 'object') {
43
+ throw new CofheError({
44
+ code: CofheErrorCode.DecryptFailed,
45
+ message: 'decrypt submit response must be a JSON object',
46
+ context: {
47
+ value,
48
+ },
49
+ });
50
+ }
51
+
52
+ const v = value as Record<string, unknown>;
53
+ if (v.request_id !== null && typeof v.request_id !== 'string') {
54
+ throw new CofheError({
55
+ code: CofheErrorCode.DecryptFailed,
56
+ message: 'decrypt submit response has invalid request_id',
57
+ context: {
58
+ value,
59
+ },
60
+ });
61
+ }
62
+
63
+ return {
64
+ request_id: v.request_id ?? null,
65
+ status: typeof v.status === 'string' ? v.status : undefined,
66
+ is_succeed: typeof v.is_succeed === 'boolean' ? v.is_succeed : undefined,
67
+ decrypted: Array.isArray(v.decrypted) ? (v.decrypted as number[]) : undefined,
68
+ signature: typeof v.signature === 'string' ? v.signature : undefined,
69
+ encryption_type: typeof v.encryption_type === 'number' ? v.encryption_type : undefined,
70
+ error_message: typeof v.error_message === 'string' || v.error_message === null ? v.error_message : undefined,
71
+ message: typeof v.message === 'string' ? v.message : undefined,
72
+ };
73
+ }
74
+
75
+ function parseCompletedDecryptResponseV2(params: {
76
+ value: Pick<DecryptStatusResponseV2, 'decrypted' | 'signature' | 'error_message' | 'is_succeed'>;
77
+ thresholdNetworkUrl: string;
78
+ requestId?: string | null;
79
+ }): { decryptedValue: bigint; signature: `0x${string}` } {
80
+ const { value, thresholdNetworkUrl, requestId } = params;
81
+
82
+ if (value.is_succeed === false) {
83
+ const errorMessage = value.error_message || 'Unknown error';
84
+ throw new CofheError({
85
+ code: CofheErrorCode.DecryptFailed,
86
+ message: `decrypt request failed: ${errorMessage}`,
87
+ context: {
88
+ thresholdNetworkUrl,
89
+ requestId,
90
+ response: value,
91
+ },
92
+ });
93
+ }
94
+
95
+ if (value.error_message) {
96
+ throw new CofheError({
97
+ code: CofheErrorCode.DecryptFailed,
98
+ message: `decrypt request failed: ${value.error_message}`,
99
+ context: {
100
+ thresholdNetworkUrl,
101
+ requestId,
102
+ response: value,
103
+ },
104
+ });
105
+ }
106
+
107
+ if (!Array.isArray(value.decrypted)) {
108
+ throw new CofheError({
109
+ code: CofheErrorCode.DecryptReturnedNull,
110
+ message: 'decrypt completed but response missing <decrypted> byte array',
111
+ context: {
112
+ thresholdNetworkUrl,
113
+ requestId,
114
+ response: value,
115
+ },
116
+ });
117
+ }
118
+
119
+ const decryptedValue = parseDecryptedBytesToBigInt(value.decrypted);
120
+ const signature = normalizeTnSignature(value.signature);
121
+ return { decryptedValue, signature };
122
+ }
123
+
124
+ function assertDecryptStatusResponseV2(value: unknown): DecryptStatusResponseV2 {
125
+ if (value == null || typeof value !== 'object') {
126
+ throw new CofheError({
127
+ code: CofheErrorCode.DecryptFailed,
128
+ message: 'decrypt status response must be a JSON object',
129
+ context: {
130
+ value,
131
+ },
132
+ });
133
+ }
134
+
135
+ const v = value as Record<string, unknown>;
136
+
137
+ const requestId = v.request_id;
138
+ const status = v.status;
139
+ const submittedAt = v.submitted_at;
140
+
141
+ if (typeof requestId !== 'string' || requestId.trim().length === 0) {
142
+ throw new CofheError({
143
+ code: CofheErrorCode.DecryptFailed,
144
+ message: 'decrypt status response missing request_id',
145
+ context: {
146
+ value,
147
+ },
148
+ });
149
+ }
150
+
151
+ if (status !== 'PROCESSING' && status !== 'COMPLETED') {
152
+ throw new CofheError({
153
+ code: CofheErrorCode.DecryptFailed,
154
+ message: 'decrypt status response has invalid status',
155
+ context: {
156
+ value,
157
+ status,
158
+ },
159
+ });
160
+ }
161
+
162
+ if (typeof submittedAt !== 'string' || submittedAt.trim().length === 0) {
163
+ throw new CofheError({
164
+ code: CofheErrorCode.DecryptFailed,
165
+ message: 'decrypt status response missing submitted_at',
166
+ context: {
167
+ value,
168
+ },
169
+ });
170
+ }
171
+
172
+ return value as DecryptStatusResponseV2;
173
+ }
174
+
175
+ async function submitDecryptRequestV2(
176
+ thresholdNetworkUrl: string,
177
+ ctHash: bigint | string,
178
+ chainId: number,
179
+ permission: Permission | null,
180
+ overallStartTime: number,
181
+ onPoll?: DecryptPollCallbackFunction
182
+ ): Promise<DecryptSubmitResultV2> {
183
+ const body: {
184
+ ct_tempkey: string;
185
+ host_chain_id: number;
186
+ permit?: Permission;
187
+ } = {
188
+ ct_tempkey: BigInt(ctHash).toString(16).padStart(64, '0'),
189
+ host_chain_id: chainId,
190
+ };
191
+
192
+ if (permission) {
193
+ body.permit = permission;
194
+ }
195
+
196
+ let attemptIndex = 0;
197
+
198
+ for (;;) {
199
+ let response: Response;
200
+ try {
201
+ response = await fetch(`${thresholdNetworkUrl}/v2/decrypt`, {
202
+ method: 'POST',
203
+ headers: {
204
+ 'Content-Type': 'application/json',
205
+ },
206
+ body: JSON.stringify(body),
207
+ });
208
+ } catch (e) {
209
+ throw new CofheError({
210
+ code: CofheErrorCode.DecryptFailed,
211
+ message: `decrypt request failed`,
212
+ hint: 'Ensure the threshold network URL is valid and reachable.',
213
+ cause: e instanceof Error ? e : undefined,
214
+ context: {
215
+ thresholdNetworkUrl,
216
+ body,
217
+ attemptIndex,
218
+ },
219
+ });
220
+ }
221
+
222
+ if (!response.ok) {
223
+ let errorMessage = `HTTP ${response.status}`;
224
+ try {
225
+ const errorBody = (await response.json()) as Record<string, unknown>;
226
+ const maybeMessage = (errorBody.error_message || errorBody.message) as unknown;
227
+ if (typeof maybeMessage === 'string' && maybeMessage.length > 0) errorMessage = maybeMessage;
228
+ } catch {
229
+ errorMessage = response.statusText || errorMessage;
230
+ }
231
+
232
+ throw new CofheError({
233
+ code: CofheErrorCode.DecryptFailed,
234
+ message: `decrypt request failed: ${errorMessage}`,
235
+ hint: 'Check the threshold network URL and request parameters.',
236
+ context: {
237
+ thresholdNetworkUrl,
238
+ status: response.status,
239
+ statusText: response.statusText,
240
+ body,
241
+ attemptIndex,
242
+ },
243
+ });
244
+ }
245
+
246
+ let submitResponse: DecryptSubmitResponseV2 | undefined;
247
+ if (response.status !== 204) {
248
+ let rawJson: unknown;
249
+ try {
250
+ rawJson = (await response.json()) as unknown;
251
+ } catch (e) {
252
+ throw new CofheError({
253
+ code: CofheErrorCode.DecryptFailed,
254
+ message: `Failed to parse decrypt submit response`,
255
+ cause: e instanceof Error ? e : undefined,
256
+ context: {
257
+ thresholdNetworkUrl,
258
+ body,
259
+ attemptIndex,
260
+ },
261
+ });
262
+ }
263
+
264
+ submitResponse = assertDecryptSubmitResponseV2(rawJson);
265
+
266
+ if (Array.isArray(submitResponse.decrypted) && typeof submitResponse.signature === 'string') {
267
+ return {
268
+ kind: 'completed',
269
+ ...parseCompletedDecryptResponseV2({
270
+ value: submitResponse,
271
+ thresholdNetworkUrl,
272
+ requestId: submitResponse.request_id,
273
+ }),
274
+ };
275
+ }
276
+
277
+ if (submitResponse.request_id) {
278
+ return { kind: 'request_id', requestId: submitResponse.request_id };
279
+ }
280
+ }
281
+
282
+ // 204 means backend is aware of ct hash but didn't calculate it yet
283
+ if (response.status === 204) {
284
+ const elapsedMs = Date.now() - overallStartTime;
285
+ if (elapsedMs > DECRYPT_TIMEOUT_MS) {
286
+ throw new CofheError({
287
+ code: CofheErrorCode.DecryptFailed,
288
+ message: `decrypt submit retried without receiving request_id for ${DECRYPT_TIMEOUT_MS}ms`,
289
+ hint: 'The ciphertext may still be propagating. Try again later.',
290
+ context: {
291
+ thresholdNetworkUrl,
292
+ body,
293
+ attemptIndex,
294
+ timeoutMs: DECRYPT_TIMEOUT_MS,
295
+ submitResponse,
296
+ status: response.status,
297
+ },
298
+ });
299
+ }
300
+
301
+ onPoll?.({
302
+ operation: 'decrypt',
303
+ requestId: '',
304
+ attemptIndex,
305
+ elapsedMs,
306
+ intervalMs: SUBMIT_RETRY_INTERVAL_MS,
307
+ timeoutMs: DECRYPT_TIMEOUT_MS,
308
+ });
309
+
310
+ await new Promise((resolve) => setTimeout(resolve, SUBMIT_RETRY_INTERVAL_MS));
311
+ attemptIndex += 1;
312
+ continue;
313
+ }
314
+
315
+ throw new CofheError({
316
+ code: CofheErrorCode.DecryptFailed,
317
+ message: `decrypt submit response missing request_id`,
318
+ context: {
319
+ thresholdNetworkUrl,
320
+ body,
321
+ submitResponse,
322
+ attemptIndex,
323
+ },
324
+ });
325
+ }
326
+ }
327
+
328
+ async function pollDecryptStatusV2(
329
+ thresholdNetworkUrl: string,
330
+ requestId: string,
331
+ overallStartTime: number,
332
+ onPoll?: DecryptPollCallbackFunction
333
+ ): Promise<{ decryptedValue: bigint; signature: `0x${string}` }> {
334
+ let attemptIndex = 0;
335
+ let completed = false;
336
+
337
+ while (!completed) {
338
+ const elapsedMs = Date.now() - overallStartTime;
339
+ const intervalMs = computeMinuteRampPollIntervalMs(elapsedMs, {
340
+ minIntervalMs: POLL_INTERVAL_MS,
341
+ maxIntervalMs: POLL_MAX_INTERVAL_MS,
342
+ });
343
+ onPoll?.({
344
+ operation: 'decrypt',
345
+ requestId,
346
+ attemptIndex,
347
+ elapsedMs,
348
+ intervalMs,
349
+ timeoutMs: DECRYPT_TIMEOUT_MS,
350
+ });
351
+
352
+ if (elapsedMs > DECRYPT_TIMEOUT_MS) {
353
+ throw new CofheError({
354
+ code: CofheErrorCode.DecryptFailed,
355
+ message: `decrypt polling timed out after ${DECRYPT_TIMEOUT_MS}ms`,
356
+ hint: 'The request may still be processing. Try again later.',
357
+ context: {
358
+ thresholdNetworkUrl,
359
+ requestId,
360
+ timeoutMs: DECRYPT_TIMEOUT_MS,
361
+ },
362
+ });
363
+ }
364
+
365
+ let response: Response;
366
+ try {
367
+ response = await fetch(`${thresholdNetworkUrl}/v2/decrypt/${requestId}`, {
368
+ method: 'GET',
369
+ headers: {
370
+ 'Content-Type': 'application/json',
371
+ },
372
+ });
373
+ } catch (e) {
374
+ throw new CofheError({
375
+ code: CofheErrorCode.DecryptFailed,
376
+ message: `decrypt status poll failed`,
377
+ hint: 'Ensure the threshold network URL is valid and reachable.',
378
+ cause: e instanceof Error ? e : undefined,
379
+ context: {
380
+ thresholdNetworkUrl,
381
+ requestId,
382
+ },
383
+ });
384
+ }
385
+
386
+ if (response.status === 404) {
387
+ throw new CofheError({
388
+ code: CofheErrorCode.DecryptFailed,
389
+ message: `decrypt request not found: ${requestId}`,
390
+ hint: 'The request may have expired or been invalid.',
391
+ context: {
392
+ thresholdNetworkUrl,
393
+ requestId,
394
+ },
395
+ });
396
+ }
397
+
398
+ if (!response.ok) {
399
+ let errorMessage = `HTTP ${response.status}`;
400
+ try {
401
+ const errorBody = (await response.json()) as Record<string, unknown>;
402
+ const maybeMessage = (errorBody.error_message || errorBody.message) as unknown;
403
+ if (typeof maybeMessage === 'string' && maybeMessage.length > 0) errorMessage = maybeMessage;
404
+ } catch {
405
+ errorMessage = response.statusText || errorMessage;
406
+ }
407
+
408
+ throw new CofheError({
409
+ code: CofheErrorCode.DecryptFailed,
410
+ message: `decrypt status poll failed: ${errorMessage}`,
411
+ context: {
412
+ thresholdNetworkUrl,
413
+ requestId,
414
+ status: response.status,
415
+ statusText: response.statusText,
416
+ },
417
+ });
418
+ }
419
+
420
+ let rawJson: unknown;
421
+ try {
422
+ rawJson = (await response.json()) as unknown;
423
+ } catch (e) {
424
+ throw new CofheError({
425
+ code: CofheErrorCode.DecryptFailed,
426
+ message: `Failed to parse decrypt status response`,
427
+ cause: e instanceof Error ? e : undefined,
428
+ context: {
429
+ thresholdNetworkUrl,
430
+ requestId,
431
+ },
432
+ });
433
+ }
434
+
435
+ const statusResponse = assertDecryptStatusResponseV2(rawJson);
436
+
437
+ if (statusResponse.status === 'COMPLETED') {
438
+ return parseCompletedDecryptResponseV2({
439
+ value: statusResponse,
440
+ thresholdNetworkUrl,
441
+ requestId,
442
+ });
443
+ }
444
+
445
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
446
+ attemptIndex += 1;
447
+ }
448
+
449
+ // This should never be reached, but keeps TS and linters happy.
450
+ throw new CofheError({
451
+ code: CofheErrorCode.DecryptFailed,
452
+ message: 'Polling loop exited unexpectedly',
453
+ context: {
454
+ thresholdNetworkUrl,
455
+ requestId,
456
+ },
457
+ });
458
+ }
459
+
460
+ export async function tnDecryptV2(params: {
461
+ ctHash: bigint | string;
462
+ chainId: number;
463
+ permission: Permission | null;
464
+ thresholdNetworkUrl: string;
465
+ onPoll?: DecryptPollCallbackFunction;
466
+ }): Promise<{ decryptedValue: bigint; signature: `0x${string}` }> {
467
+ const { thresholdNetworkUrl, ctHash, chainId, permission, onPoll } = params;
468
+ const overallStartTime = Date.now();
469
+ const submitResult = await submitDecryptRequestV2(
470
+ thresholdNetworkUrl,
471
+ ctHash,
472
+ chainId,
473
+ permission,
474
+ overallStartTime,
475
+ onPoll
476
+ );
477
+
478
+ if (submitResult.kind === 'completed') {
479
+ return submitResult;
480
+ }
481
+
482
+ return await pollDecryptStatusV2(thresholdNetworkUrl, submitResult.requestId, overallStartTime, onPoll);
483
+ }