@cofhe/sdk 0.3.1 → 0.4.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.
@@ -0,0 +1,343 @@
1
+ import { type Permission } from '@/permits';
2
+
3
+ import { CofheError, CofheErrorCode } from '../error';
4
+ import { normalizeTnSignature, parseDecryptedBytesToBigInt } from './tnDecryptUtils';
5
+
6
+ // Polling configuration
7
+ const POLL_INTERVAL_MS = 1000; // 1 second
8
+ const POLL_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
9
+
10
+ type DecryptSubmitResponseV2 = {
11
+ request_id: string;
12
+ };
13
+
14
+ type DecryptStatusResponseV2 = {
15
+ request_id: string;
16
+ status: 'PROCESSING' | 'COMPLETED';
17
+ submitted_at: string;
18
+ completed_at?: string;
19
+ is_succeed?: boolean;
20
+ decrypted?: number[];
21
+ signature?: string;
22
+ encryption_type?: number;
23
+ error_message?: string | null;
24
+ };
25
+
26
+ function assertDecryptSubmitResponseV2(value: unknown): DecryptSubmitResponseV2 {
27
+ if (value == null || typeof value !== 'object') {
28
+ throw new CofheError({
29
+ code: CofheErrorCode.DecryptFailed,
30
+ message: 'decrypt submit response must be a JSON object',
31
+ context: {
32
+ value,
33
+ },
34
+ });
35
+ }
36
+
37
+ const v = value as Record<string, unknown>;
38
+ if (typeof v.request_id !== 'string' || v.request_id.trim().length === 0) {
39
+ throw new CofheError({
40
+ code: CofheErrorCode.DecryptFailed,
41
+ message: 'decrypt submit response missing request_id',
42
+ context: {
43
+ value,
44
+ },
45
+ });
46
+ }
47
+
48
+ return { request_id: v.request_id };
49
+ }
50
+
51
+ function assertDecryptStatusResponseV2(value: unknown): DecryptStatusResponseV2 {
52
+ if (value == null || typeof value !== 'object') {
53
+ throw new CofheError({
54
+ code: CofheErrorCode.DecryptFailed,
55
+ message: 'decrypt status response must be a JSON object',
56
+ context: {
57
+ value,
58
+ },
59
+ });
60
+ }
61
+
62
+ const v = value as Record<string, unknown>;
63
+
64
+ const requestId = v.request_id;
65
+ const status = v.status;
66
+ const submittedAt = v.submitted_at;
67
+
68
+ if (typeof requestId !== 'string' || requestId.trim().length === 0) {
69
+ throw new CofheError({
70
+ code: CofheErrorCode.DecryptFailed,
71
+ message: 'decrypt status response missing request_id',
72
+ context: {
73
+ value,
74
+ },
75
+ });
76
+ }
77
+
78
+ if (status !== 'PROCESSING' && status !== 'COMPLETED') {
79
+ throw new CofheError({
80
+ code: CofheErrorCode.DecryptFailed,
81
+ message: 'decrypt status response has invalid status',
82
+ context: {
83
+ value,
84
+ status,
85
+ },
86
+ });
87
+ }
88
+
89
+ if (typeof submittedAt !== 'string' || submittedAt.trim().length === 0) {
90
+ throw new CofheError({
91
+ code: CofheErrorCode.DecryptFailed,
92
+ message: 'decrypt status response missing submitted_at',
93
+ context: {
94
+ value,
95
+ },
96
+ });
97
+ }
98
+
99
+ return value as DecryptStatusResponseV2;
100
+ }
101
+
102
+ async function submitDecryptRequestV2(
103
+ thresholdNetworkUrl: string,
104
+ ctHash: bigint | string,
105
+ chainId: number,
106
+ permission: Permission | null
107
+ ): Promise<string> {
108
+ const body: {
109
+ ct_tempkey: string;
110
+ host_chain_id: number;
111
+ permit?: Permission;
112
+ } = {
113
+ ct_tempkey: BigInt(ctHash).toString(16).padStart(64, '0'),
114
+ host_chain_id: chainId,
115
+ };
116
+
117
+ if (permission) {
118
+ body.permit = permission;
119
+ }
120
+
121
+ let response: Response;
122
+ try {
123
+ response = await fetch(`${thresholdNetworkUrl}/v2/decrypt`, {
124
+ method: 'POST',
125
+ headers: {
126
+ 'Content-Type': 'application/json',
127
+ },
128
+ body: JSON.stringify(body),
129
+ });
130
+ } catch (e) {
131
+ throw new CofheError({
132
+ code: CofheErrorCode.DecryptFailed,
133
+ message: `decrypt request failed`,
134
+ hint: 'Ensure the threshold network URL is valid and reachable.',
135
+ cause: e instanceof Error ? e : undefined,
136
+ context: {
137
+ thresholdNetworkUrl,
138
+ body,
139
+ },
140
+ });
141
+ }
142
+
143
+ if (!response.ok) {
144
+ let errorMessage = `HTTP ${response.status}`;
145
+ try {
146
+ const errorBody = (await response.json()) as Record<string, unknown>;
147
+ const maybeMessage = (errorBody.error_message || errorBody.message) as unknown;
148
+ if (typeof maybeMessage === 'string' && maybeMessage.length > 0) errorMessage = maybeMessage;
149
+ } catch {
150
+ errorMessage = response.statusText || errorMessage;
151
+ }
152
+
153
+ throw new CofheError({
154
+ code: CofheErrorCode.DecryptFailed,
155
+ message: `decrypt request failed: ${errorMessage}`,
156
+ hint: 'Check the threshold network URL and request parameters.',
157
+ context: {
158
+ thresholdNetworkUrl,
159
+ status: response.status,
160
+ statusText: response.statusText,
161
+ body,
162
+ },
163
+ });
164
+ }
165
+
166
+ let rawJson: unknown;
167
+ try {
168
+ rawJson = (await response.json()) as unknown;
169
+ } catch (e) {
170
+ throw new CofheError({
171
+ code: CofheErrorCode.DecryptFailed,
172
+ message: `Failed to parse decrypt submit response`,
173
+ cause: e instanceof Error ? e : undefined,
174
+ context: {
175
+ thresholdNetworkUrl,
176
+ body,
177
+ },
178
+ });
179
+ }
180
+
181
+ const submitResponse = assertDecryptSubmitResponseV2(rawJson);
182
+ return submitResponse.request_id;
183
+ }
184
+
185
+ async function pollDecryptStatusV2(
186
+ thresholdNetworkUrl: string,
187
+ requestId: string
188
+ ): Promise<{ decryptedValue: bigint; signature: `0x${string}` }> {
189
+ const startTime = Date.now();
190
+ let completed = false;
191
+
192
+ while (!completed) {
193
+ if (Date.now() - startTime > POLL_TIMEOUT_MS) {
194
+ throw new CofheError({
195
+ code: CofheErrorCode.DecryptFailed,
196
+ message: `decrypt polling timed out after ${POLL_TIMEOUT_MS}ms`,
197
+ hint: 'The request may still be processing. Try again later.',
198
+ context: {
199
+ thresholdNetworkUrl,
200
+ requestId,
201
+ timeoutMs: POLL_TIMEOUT_MS,
202
+ },
203
+ });
204
+ }
205
+
206
+ let response: Response;
207
+ try {
208
+ response = await fetch(`${thresholdNetworkUrl}/v2/decrypt/${requestId}`, {
209
+ method: 'GET',
210
+ headers: {
211
+ 'Content-Type': 'application/json',
212
+ },
213
+ });
214
+ } catch (e) {
215
+ throw new CofheError({
216
+ code: CofheErrorCode.DecryptFailed,
217
+ message: `decrypt status poll failed`,
218
+ hint: 'Ensure the threshold network URL is valid and reachable.',
219
+ cause: e instanceof Error ? e : undefined,
220
+ context: {
221
+ thresholdNetworkUrl,
222
+ requestId,
223
+ },
224
+ });
225
+ }
226
+
227
+ if (response.status === 404) {
228
+ throw new CofheError({
229
+ code: CofheErrorCode.DecryptFailed,
230
+ message: `decrypt request not found: ${requestId}`,
231
+ hint: 'The request may have expired or been invalid.',
232
+ context: {
233
+ thresholdNetworkUrl,
234
+ requestId,
235
+ },
236
+ });
237
+ }
238
+
239
+ if (!response.ok) {
240
+ let errorMessage = `HTTP ${response.status}`;
241
+ try {
242
+ const errorBody = (await response.json()) as Record<string, unknown>;
243
+ const maybeMessage = (errorBody.error_message || errorBody.message) as unknown;
244
+ if (typeof maybeMessage === 'string' && maybeMessage.length > 0) errorMessage = maybeMessage;
245
+ } catch {
246
+ errorMessage = response.statusText || errorMessage;
247
+ }
248
+
249
+ throw new CofheError({
250
+ code: CofheErrorCode.DecryptFailed,
251
+ message: `decrypt status poll failed: ${errorMessage}`,
252
+ context: {
253
+ thresholdNetworkUrl,
254
+ requestId,
255
+ status: response.status,
256
+ statusText: response.statusText,
257
+ },
258
+ });
259
+ }
260
+
261
+ let rawJson: unknown;
262
+ try {
263
+ rawJson = (await response.json()) as unknown;
264
+ } catch (e) {
265
+ throw new CofheError({
266
+ code: CofheErrorCode.DecryptFailed,
267
+ message: `Failed to parse decrypt status response`,
268
+ cause: e instanceof Error ? e : undefined,
269
+ context: {
270
+ thresholdNetworkUrl,
271
+ requestId,
272
+ },
273
+ });
274
+ }
275
+
276
+ const statusResponse = assertDecryptStatusResponseV2(rawJson);
277
+
278
+ if (statusResponse.status === 'COMPLETED') {
279
+ if (statusResponse.is_succeed === false) {
280
+ const errorMessage = statusResponse.error_message || 'Unknown error';
281
+ throw new CofheError({
282
+ code: CofheErrorCode.DecryptFailed,
283
+ message: `decrypt request failed: ${errorMessage}`,
284
+ context: {
285
+ thresholdNetworkUrl,
286
+ requestId,
287
+ statusResponse,
288
+ },
289
+ });
290
+ }
291
+
292
+ if (statusResponse.error_message) {
293
+ throw new CofheError({
294
+ code: CofheErrorCode.DecryptFailed,
295
+ message: `decrypt request failed: ${statusResponse.error_message}`,
296
+ context: {
297
+ thresholdNetworkUrl,
298
+ requestId,
299
+ statusResponse,
300
+ },
301
+ });
302
+ }
303
+
304
+ if (!Array.isArray(statusResponse.decrypted)) {
305
+ throw new CofheError({
306
+ code: CofheErrorCode.DecryptReturnedNull,
307
+ message: 'decrypt completed but response missing <decrypted> byte array',
308
+ context: {
309
+ thresholdNetworkUrl,
310
+ requestId,
311
+ statusResponse,
312
+ },
313
+ });
314
+ }
315
+
316
+ const decryptedValue = parseDecryptedBytesToBigInt(statusResponse.decrypted);
317
+ const signature = normalizeTnSignature(statusResponse.signature);
318
+ return { decryptedValue, signature };
319
+ }
320
+
321
+ await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
322
+ }
323
+
324
+ // This should never be reached, but keeps TS and linters happy.
325
+ throw new CofheError({
326
+ code: CofheErrorCode.DecryptFailed,
327
+ message: 'Polling loop exited unexpectedly',
328
+ context: {
329
+ thresholdNetworkUrl,
330
+ requestId,
331
+ },
332
+ });
333
+ }
334
+
335
+ export async function tnDecryptV2(
336
+ ctHash: bigint | string,
337
+ chainId: number,
338
+ permission: Permission | null,
339
+ thresholdNetworkUrl: string
340
+ ): Promise<{ decryptedValue: bigint; signature: `0x${string}` }> {
341
+ const requestId = await submitDecryptRequestV2(thresholdNetworkUrl, ctHash, chainId, permission);
342
+ return await pollDecryptStatusV2(thresholdNetworkUrl, requestId);
343
+ }
@@ -57,12 +57,12 @@ function convertSealedData(sealed: SealOutputStatusResponse['sealed']): EthEncry
57
57
  */
58
58
  async function submitSealOutputRequest(
59
59
  thresholdNetworkUrl: string,
60
- ctHash: bigint,
60
+ ctHash: bigint | string,
61
61
  chainId: number,
62
62
  permission: Permission
63
63
  ): Promise<string> {
64
64
  const body = {
65
- ct_tempkey: ctHash.toString(16).padStart(64, '0'),
65
+ ct_tempkey: BigInt(ctHash).toString(16).padStart(64, '0'),
66
66
  host_chain_id: chainId,
67
67
  permit: permission,
68
68
  };
@@ -285,7 +285,7 @@ async function pollSealOutputStatus(thresholdNetworkUrl: string, requestId: stri
285
285
  }
286
286
 
287
287
  export async function tnSealOutputV2(
288
- ctHash: bigint,
288
+ ctHash: bigint | string,
289
289
  chainId: number,
290
290
  permission: Permission,
291
291
  thresholdNetworkUrl: string
@@ -13,7 +13,7 @@ import {
13
13
  import { MockZkVerifierAbi } from './MockZkVerifierAbi.js';
14
14
  import { hardhat } from 'viem/chains';
15
15
  import { CofheError, CofheErrorCode } from '../error.js';
16
- import { privateKeyToAccount } from 'viem/accounts';
16
+ import { privateKeyToAccount, sign } from 'viem/accounts';
17
17
  import { MOCKS_ZK_VERIFIER_SIGNER_PRIVATE_KEY, MOCKS_ZK_VERIFIER_ADDRESS } from '../consts.js';
18
18
 
19
19
  type EncryptableItemWithCtHash = EncryptableItem & {
@@ -181,7 +181,11 @@ async function insertCtHashes(items: EncryptableItemWithCtHash[], walletClient:
181
181
  * The mocks verify the EncryptedInputs' signature against the known proof signer account.
182
182
  * Locally, we create the proof signatures from the known proof signer account.
183
183
  */
184
- async function createProofSignatures(items: EncryptableItemWithCtHash[], securityZone: number): Promise<string[]> {
184
+ async function createProofSignatures(
185
+ items: EncryptableItemWithCtHash[],
186
+ securityZone: number,
187
+ account: string
188
+ ): Promise<string[]> {
185
189
  let signatures: string[] = [];
186
190
 
187
191
  // Create wallet client for the encrypted input signer
@@ -205,16 +209,16 @@ async function createProofSignatures(items: EncryptableItemWithCtHash[], securit
205
209
  try {
206
210
  for (const item of items) {
207
211
  // Pack the data into bytes and hash it
208
- const packedData = encodePacked(['uint256', 'int32', 'uint8'], [BigInt(item.data), securityZone, item.utype]);
212
+ const packedData = encodePacked(
213
+ ['uint256', 'uint8', 'uint8', 'address', 'uint256'],
214
+ [BigInt(item.ctHash), item.utype, securityZone, account as `0x${string}`, BigInt(hardhat.id)]
215
+ );
209
216
  const messageHash = keccak256(packedData);
210
217
 
211
- // Convert to EthSignedMessageHash (adds "\x19Ethereum Signed Message:\n32" prefix)
212
- const ethSignedHash = hashMessage({ raw: toBytes(messageHash) });
213
-
214
- // Sign the message
215
- const signature = await encInputSignerClient.signMessage({
216
- message: { raw: toBytes(ethSignedHash) },
217
- account: encInputSignerClient.account!,
218
+ const signature = await sign({
219
+ hash: messageHash,
220
+ privateKey: MOCKS_ZK_VERIFIER_SIGNER_PRIVATE_KEY,
221
+ to: 'hex',
218
222
  });
219
223
 
220
224
  signatures.push(signature);
@@ -267,7 +271,7 @@ export async function cofheMocksZkVerifySign(
267
271
  await insertCtHashes(encryptableItems, _walletClient);
268
272
 
269
273
  // Locally create the proof signatures from the known proof signer account
270
- const signatures = await createProofSignatures(encryptableItems, securityZone);
274
+ const signatures = await createProofSignatures(encryptableItems, securityZone, account);
271
275
 
272
276
  // Return the ctHashes and signatures in the same format as CoFHE
273
277
  return encryptableItems.map((item, index) => ({
package/core/permits.ts CHANGED
@@ -85,15 +85,15 @@ const deserialize = (serialized: SerializedPermit) => {
85
85
 
86
86
  // GET
87
87
 
88
- const getPermit = async (chainId: number, account: string, hash: string): Promise<Permit | undefined> => {
88
+ const getPermit = (chainId: number, account: string, hash: string): Permit | undefined => {
89
89
  return permitStore.getPermit(chainId, account, hash);
90
90
  };
91
91
 
92
- const getPermits = async (chainId: number, account: string): Promise<Record<string, Permit>> => {
92
+ const getPermits = (chainId: number, account: string): Record<string, Permit> => {
93
93
  return permitStore.getPermits(chainId, account);
94
94
  };
95
95
 
96
- const getActivePermit = async (chainId: number, account: string): Promise<Permit | undefined> => {
96
+ const getActivePermit = (chainId: number, account: string): Permit | undefined => {
97
97
  return permitStore.getActivePermit(chainId, account);
98
98
  };
99
99
 
package/core/types.ts CHANGED
@@ -389,6 +389,14 @@ export type EncryptStepCallbackFunction = (state: EncryptStep, context?: Encrypt
389
389
 
390
390
  // DECRYPT
391
391
 
392
+ /**
393
+ * Decrypted plaintext value returned by view-decryption helpers.
394
+ *
395
+ * This is a scalar JS value (not a wrapper object):
396
+ * - `boolean` for `FheTypes.Bool`
397
+ * - checksummed address `string` for `FheTypes.Uint160`
398
+ * - `bigint` for supported integer utypes
399
+ */
392
400
  export type UnsealedItem<U extends FheTypes> = U extends FheTypes.Bool
393
401
  ? boolean
394
402
  : U extends FheTypes.Uint160