@cofhe/sdk 0.1.1 → 0.2.1

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 (107) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/adapters/ethers6.ts +28 -28
  3. package/adapters/hardhat.ts +0 -1
  4. package/adapters/index.test.ts +14 -19
  5. package/adapters/smartWallet.ts +81 -73
  6. package/adapters/test-utils.ts +45 -45
  7. package/adapters/types.ts +3 -3
  8. package/chains/chains/localcofhe.ts +14 -0
  9. package/chains/chains.test.ts +2 -1
  10. package/chains/defineChain.ts +2 -2
  11. package/chains/index.ts +3 -1
  12. package/chains/types.ts +3 -3
  13. package/core/baseBuilder.ts +30 -49
  14. package/core/client.test.ts +200 -72
  15. package/core/client.ts +152 -148
  16. package/core/clientTypes.ts +114 -0
  17. package/core/config.test.ts +30 -11
  18. package/core/config.ts +26 -13
  19. package/core/consts.ts +18 -0
  20. package/core/decrypt/cofheMocksSealOutput.ts +2 -4
  21. package/core/decrypt/decryptHandleBuilder.ts +51 -45
  22. package/core/decrypt/{tnSealOutput.ts → tnSealOutputV1.ts} +1 -1
  23. package/core/decrypt/tnSealOutputV2.ts +298 -0
  24. package/core/encrypt/cofheMocksZkVerifySign.ts +15 -16
  25. package/core/encrypt/encryptInputsBuilder.test.ts +132 -116
  26. package/core/encrypt/encryptInputsBuilder.ts +159 -111
  27. package/core/encrypt/encryptUtils.ts +6 -3
  28. package/core/encrypt/zkPackProveVerify.ts +70 -8
  29. package/core/error.ts +0 -2
  30. package/core/fetchKeys.test.ts +1 -18
  31. package/core/fetchKeys.ts +0 -26
  32. package/core/index.ts +37 -17
  33. package/core/keyStore.ts +65 -38
  34. package/core/permits.test.ts +255 -4
  35. package/core/permits.ts +83 -18
  36. package/core/types.ts +198 -152
  37. package/core/utils.ts +43 -1
  38. package/dist/adapters.d.cts +38 -20
  39. package/dist/adapters.d.ts +38 -20
  40. package/dist/chains.cjs +18 -8
  41. package/dist/chains.d.cts +31 -9
  42. package/dist/chains.d.ts +31 -9
  43. package/dist/chains.js +1 -1
  44. package/dist/{chunk-KFGPTJ6X.js → chunk-I5WFEYXX.js} +1768 -1526
  45. package/dist/{chunk-LU7BMUUT.js → chunk-R3B5TMVX.js} +330 -197
  46. package/dist/{chunk-GZCQQYVI.js → chunk-TBLR7NNE.js} +18 -9
  47. package/dist/{types-PhwGgQvs.d.ts → clientTypes-RqkgkV2i.d.ts} +331 -429
  48. package/dist/{types-bB7wLj0q.d.cts → clientTypes-e4filDzK.d.cts} +331 -429
  49. package/dist/core.cjs +3000 -2625
  50. package/dist/core.d.cts +113 -7
  51. package/dist/core.d.ts +113 -7
  52. package/dist/core.js +3 -3
  53. package/dist/node.cjs +2851 -2526
  54. package/dist/node.d.cts +4 -4
  55. package/dist/node.d.ts +4 -4
  56. package/dist/node.js +4 -3
  57. package/dist/{permit-S9CnI6MF.d.cts → permit-MZ502UBl.d.cts} +54 -41
  58. package/dist/{permit-S9CnI6MF.d.ts → permit-MZ502UBl.d.ts} +54 -41
  59. package/dist/permits.cjs +328 -195
  60. package/dist/permits.d.cts +113 -825
  61. package/dist/permits.d.ts +113 -825
  62. package/dist/permits.js +1 -1
  63. package/dist/types-YiAC4gig.d.cts +33 -0
  64. package/dist/types-YiAC4gig.d.ts +33 -0
  65. package/dist/web.cjs +3067 -2527
  66. package/dist/web.d.cts +22 -6
  67. package/dist/web.d.ts +22 -6
  68. package/dist/web.js +185 -9
  69. package/dist/zkProve.worker.cjs +93 -0
  70. package/dist/zkProve.worker.d.cts +2 -0
  71. package/dist/zkProve.worker.d.ts +2 -0
  72. package/dist/zkProve.worker.js +91 -0
  73. package/node/client.test.ts +20 -25
  74. package/node/encryptInputs.test.ts +18 -38
  75. package/node/index.ts +1 -0
  76. package/package.json +15 -15
  77. package/permits/index.ts +1 -0
  78. package/permits/localstorage.test.ts +9 -14
  79. package/permits/onchain-utils.ts +221 -0
  80. package/permits/permit.test.ts +76 -27
  81. package/permits/permit.ts +58 -95
  82. package/permits/sealing.test.ts +3 -3
  83. package/permits/sealing.ts +2 -2
  84. package/permits/store.test.ts +10 -50
  85. package/permits/store.ts +9 -21
  86. package/permits/test-utils.ts +11 -3
  87. package/permits/types.ts +39 -9
  88. package/permits/utils.ts +0 -5
  89. package/permits/validation.test.ts +29 -32
  90. package/permits/validation.ts +114 -176
  91. package/web/client.web.test.ts +20 -25
  92. package/web/config.web.test.ts +0 -2
  93. package/web/encryptInputs.web.test.ts +31 -54
  94. package/web/index.ts +65 -1
  95. package/web/storage.ts +19 -5
  96. package/web/worker.builder.web.test.ts +148 -0
  97. package/web/worker.config.web.test.ts +329 -0
  98. package/web/worker.output.web.test.ts +84 -0
  99. package/web/workerManager.test.ts +80 -0
  100. package/web/workerManager.ts +214 -0
  101. package/web/workerManager.web.test.ts +114 -0
  102. package/web/zkProve.worker.ts +133 -0
  103. package/core/result.test.ts +0 -180
  104. package/core/result.ts +0 -67
  105. package/core/test-utils.ts +0 -45
  106. package/dist/types-KImPrEIe.d.cts +0 -48
  107. package/dist/types-KImPrEIe.d.ts +0 -48
package/core/config.ts CHANGED
@@ -5,18 +5,16 @@ import { type WalletClient } from 'viem';
5
5
  import { CofhesdkError, CofhesdkErrorCode } from './error.js';
6
6
  import { type IStorage } from './types.js';
7
7
 
8
+ export type CofhesdkEnvironment = 'node' | 'hardhat' | 'web' | 'react';
9
+
8
10
  /**
9
11
  * Usable config type inferred from the schema
10
12
  */
11
13
  export type CofhesdkConfig = {
14
+ /** Environment that the SDK is running in */
15
+ environment: 'node' | 'hardhat' | 'web' | 'react';
16
+ /** List of supported chains */
12
17
  supportedChains: CofheChain[];
13
- /**
14
- * Strategy for fetching FHE keys
15
- * - CONNECTED_CHAIN: Fetch keys for the connected chain (provided by the publicClient)
16
- * - SUPPORTED_CHAINS: Fetch keys for all supported chains (provided by the supportedChains config)
17
- * - OFF: Do not fetch keys (fetching occurs during encryptInputs)
18
- * */
19
- fheKeysPrefetching: 'CONNECTED_CHAIN' | 'SUPPORTED_CHAINS' | 'OFF';
20
18
  /**
21
19
  * How permits are generated
22
20
  * - ON_CONNECT: Generate a permit when client.connect() is called
@@ -32,6 +30,12 @@ export type CofhesdkConfig = {
32
30
  * (defaults to indexedDB on web, filesystem on node)
33
31
  */
34
32
  fheKeyStorage: IStorage | null;
33
+ /**
34
+ * Whether to use Web Workers for ZK proof generation (web platform only)
35
+ * When enabled, heavy WASM computation is offloaded to prevent UI freezing
36
+ * Default: true
37
+ */
38
+ useWorkers: boolean;
35
39
  /** Mocks configs */
36
40
  mocks: {
37
41
  /**
@@ -52,10 +56,10 @@ export type CofhesdkInternalConfig = {
52
56
  * Zod schema for configuration validation
53
57
  */
54
58
  export const CofhesdkConfigSchema = z.object({
59
+ /** Environment that the SDK is running in */
60
+ environment: z.enum(['node', 'hardhat', 'web', 'react']).optional().default('node'),
55
61
  /** List of supported chain configurations */
56
62
  supportedChains: z.array(z.custom<CofheChain>()),
57
- /** Strategy for fetching FHE keys */
58
- fheKeysPrefetching: z.enum(['CONNECTED_CHAIN', 'SUPPORTED_CHAINS', 'OFF']).optional().default('OFF'),
59
63
  /** How permits are generated */
60
64
  permitGeneration: z.enum(['ON_CONNECT', 'ON_DECRYPT_HANDLES', 'MANUAL']).optional().default('ON_CONNECT'),
61
65
  /** Default permit expiration in seconds, default is 30 days */
@@ -66,12 +70,20 @@ export const CofhesdkConfigSchema = z.object({
66
70
  /** Storage method for fhe keys (defaults to indexedDB on web, filesystem on node) */
67
71
  fheKeyStorage: z
68
72
  .object({
69
- getItem: z.function().args(z.string()).returns(z.promise(z.any())),
70
- setItem: z.function().args(z.string(), z.any()).returns(z.promise(z.void())),
71
- removeItem: z.function().args(z.string()).returns(z.promise(z.void())),
73
+ getItem: z.custom<IStorage['getItem']>((val) => typeof val === 'function', {
74
+ message: 'getItem must be a function',
75
+ }),
76
+ setItem: z.custom<IStorage['setItem']>((val) => typeof val === 'function', {
77
+ message: 'setItem must be a function',
78
+ }),
79
+ removeItem: z.custom<IStorage['removeItem']>((val) => typeof val === 'function', {
80
+ message: 'removeItem must be a function',
81
+ }),
72
82
  })
73
83
  .or(z.null())
74
84
  .default(null),
85
+ /** Whether to use Web Workers for ZK proof generation (web platform only) */
86
+ useWorkers: z.boolean().optional().default(true),
75
87
  /** Mocks configs */
76
88
  mocks: z
77
89
  .object({
@@ -91,6 +103,7 @@ export const CofhesdkConfigSchema = z.object({
91
103
  * Input config type inferred from the schema
92
104
  */
93
105
  export type CofhesdkInputConfig = z.input<typeof CofhesdkConfigSchema>;
106
+
94
107
  /**
95
108
  * Creates and validates a cofhesdk configuration (base implementation)
96
109
  * @param config - The configuration object to validate
@@ -101,7 +114,7 @@ export function createCofhesdkConfigBase(config: CofhesdkInputConfig): CofhesdkC
101
114
  const result = CofhesdkConfigSchema.safeParse(config);
102
115
 
103
116
  if (!result.success) {
104
- throw new Error(`Invalid cofhesdk configuration: ${result.error.message}`);
117
+ throw new Error(`Invalid cofhesdk configuration: ${z.prettifyError(result.error)}`, { cause: result.error });
105
118
  }
106
119
 
107
120
  return result.data;
package/core/consts.ts ADDED
@@ -0,0 +1,18 @@
1
+ /** Main Task Manager contract address */
2
+ export const TASK_MANAGER_ADDRESS = '0xeA30c4B8b44078Bbf8a6ef5b9f1eC1626C7848D9' as const;
3
+
4
+ /** Mock ZK Verifier contract address (used for testing) */
5
+ export const MOCKS_ZK_VERIFIER_ADDRESS = '0x0000000000000000000000000000000000005001' as const;
6
+
7
+ /** Mock Query Decrypter contract address (used for testing) */
8
+ export const MOCKS_QUERY_DECRYPTER_ADDRESS = '0x0000000000000000000000000000000000005002' as const;
9
+
10
+ /** Test Bed contract address (used for testing) */
11
+ export const TEST_BED_ADDRESS = '0x0000000000000000000000000000000000005003' as const;
12
+
13
+ /** Private key for the Mock ZK Verifier signer account */
14
+ export const MOCKS_ZK_VERIFIER_SIGNER_PRIVATE_KEY =
15
+ '0x6C8D7F768A6BB4AAFE85E8A2F5A9680355239C7E14646ED62B044E39DE154512' as const;
16
+
17
+ /** Address for the Mock ZK Verifier signer account */
18
+ export const MOCKS_ZK_VERIFIER_SIGNER_ADDRESS = '0x6E12D8C87503D4287c294f2Fdef96ACd9DFf6bd2' as const;
@@ -5,9 +5,7 @@ import { sleep } from '../utils.js';
5
5
  import { MockQueryDecrypterAbi } from './MockQueryDecrypterAbi.js';
6
6
  import { FheTypes } from '../types.js';
7
7
  import { CofhesdkError, CofhesdkErrorCode } from '../error.js';
8
-
9
- // Address the Mock Query Decrypter contract is deployed to on the Hardhat chain
10
- export const MockQueryDecrypterAddress = '0x0000000000000000000000000000000000000200';
8
+ import { MOCKS_QUERY_DECRYPTER_ADDRESS } from '../consts.js';
11
9
 
12
10
  export async function cofheMocksSealOutput(
13
11
  ctHash: bigint,
@@ -29,7 +27,7 @@ export async function cofheMocksSealOutput(
29
27
  };
30
28
 
31
29
  const [allowed, error, result] = await publicClient.readContract({
32
- address: MockQueryDecrypterAddress,
30
+ address: MOCKS_QUERY_DECRYPTER_ADDRESS,
33
31
  abi: MockQueryDecrypterAbi,
34
32
  functionName: 'querySealOutput',
35
33
  args: [ctHash, BigInt(utype), permissionWithBigInts],
@@ -3,13 +3,13 @@ import { type Permit, PermitUtils } from '@/permits';
3
3
 
4
4
  import { FheTypes, type UnsealedItem } from '../types.js';
5
5
  import { getThresholdNetworkUrlOrThrow } from '../config.js';
6
- import { type Result, resultWrapper } from '../result.js';
7
6
  import { CofhesdkError, CofhesdkErrorCode } from '../error.js';
8
7
  import { permits } from '../permits.js';
9
8
  import { isValidUtype, convertViaUtype } from './decryptUtils.js';
10
9
  import { BaseBuilder, type BaseBuilderParams } from '../baseBuilder.js';
11
10
  import { cofheMocksSealOutput } from './cofheMocksSealOutput.js';
12
- import { tnSealOutput } from './tnSealOutput.js';
11
+ // import { tnSealOutputV1 } from './tnSealOutputV1.js';
12
+ import { tnSealOutputV2 } from './tnSealOutputV2.js';
13
13
 
14
14
  /**
15
15
  * API
@@ -26,7 +26,7 @@ import { tnSealOutput } from './tnSealOutput.js';
26
26
  * If permitHash not set, uses chainId and account to get active permit
27
27
  * If permit is set, uses permit to decrypt regardless of chainId, account, or permitHash
28
28
  *
29
- * Returns a Result<UnsealedItem<U>>
29
+ * Returns the unsealed item.
30
30
  */
31
31
 
32
32
  type DecryptHandlesBuilderParams<U extends FheTypes> = BaseBuilderParams & {
@@ -151,9 +151,9 @@ export class DecryptHandlesBuilder<U extends FheTypes> extends BaseBuilder {
151
151
  return this.permit;
152
152
  }
153
153
 
154
- private getThresholdNetworkUrl(chainId: number): string {
155
- const config = this.getConfigOrThrow();
156
- return getThresholdNetworkUrlOrThrow(config, chainId);
154
+ private async getThresholdNetworkUrl(): Promise<string> {
155
+ this.assertChainId();
156
+ return getThresholdNetworkUrlOrThrow(this.config, this.chainId);
157
157
  }
158
158
 
159
159
  private validateUtypeOrThrow(): void {
@@ -170,20 +170,20 @@ export class DecryptHandlesBuilder<U extends FheTypes> extends BaseBuilder {
170
170
  private async getResolvedPermit(): Promise<Permit> {
171
171
  if (this.permit) return this.permit;
172
172
 
173
- const chainId = await this.getChainIdOrThrow();
174
- const account = await this.getAccountOrThrow();
173
+ this.assertChainId();
174
+ this.assertAccount();
175
175
 
176
176
  // Fetch with permit hash
177
177
  if (this.permitHash) {
178
- const permit = await permits.getPermit(chainId, account, this.permitHash);
178
+ const permit = await permits.getPermit(this.chainId, this.account, this.permitHash);
179
179
  if (!permit) {
180
180
  throw new CofhesdkError({
181
181
  code: CofhesdkErrorCode.PermitNotFound,
182
- message: `Permit with hash <${this.permitHash}> not found for account <${account}> and chainId <${chainId}>`,
182
+ message: `Permit with hash <${this.permitHash}> not found for account <${this.account}> and chainId <${this.chainId}>`,
183
183
  hint: 'Ensure the permit exists and is valid.',
184
184
  context: {
185
- chainId,
186
- account,
185
+ chainId: this.chainId,
186
+ account: this.account,
187
187
  permitHash: this.permitHash,
188
188
  },
189
189
  });
@@ -192,15 +192,15 @@ export class DecryptHandlesBuilder<U extends FheTypes> extends BaseBuilder {
192
192
  }
193
193
 
194
194
  // Fetch with active permit
195
- const permit = await permits.getActivePermit(chainId, account);
195
+ const permit = await permits.getActivePermit(this.chainId, this.account);
196
196
  if (!permit) {
197
197
  throw new CofhesdkError({
198
198
  code: CofhesdkErrorCode.PermitNotFound,
199
- message: `Active permit not found for chainId <${chainId}> and account <${account}>`,
199
+ message: `Active permit not found for chainId <${this.chainId}> and account <${this.account}>`,
200
200
  hint: 'Ensure a permit exists for this account on this chain.',
201
201
  context: {
202
- chainId,
203
- account,
202
+ chainId: this.chainId,
203
+ account: this.account,
204
204
  },
205
205
  });
206
206
  }
@@ -211,18 +211,23 @@ export class DecryptHandlesBuilder<U extends FheTypes> extends BaseBuilder {
211
211
  * On hardhat, interact with MockZkVerifier contract instead of CoFHE
212
212
  */
213
213
  private async mocksSealOutput(permit: Permit): Promise<bigint> {
214
- const config = this.getConfigOrThrow();
215
- const mocksSealOutputDelay = config.mocks.sealOutputDelay;
216
- return cofheMocksSealOutput(this.ctHash, this.utype, permit, this.getPublicClientOrThrow(), mocksSealOutputDelay);
214
+ this.assertPublicClient();
215
+
216
+ const mocksSealOutputDelay = this.config.mocks.sealOutputDelay;
217
+ return cofheMocksSealOutput(this.ctHash, this.utype, permit, this.publicClient, mocksSealOutputDelay);
217
218
  }
218
219
 
219
220
  /**
220
221
  * In the production context, perform a true decryption with the CoFHE coprocessor.
221
222
  */
222
- private async productionSealOutput(chainId: number, permit: Permit): Promise<bigint> {
223
- const thresholdNetworkUrl = this.getThresholdNetworkUrl(chainId);
223
+ private async productionSealOutput(permit: Permit): Promise<bigint> {
224
+ this.assertChainId();
225
+ this.assertPublicClient();
226
+
227
+ const thresholdNetworkUrl = await this.getThresholdNetworkUrl();
224
228
  const permission = PermitUtils.getPermission(permit, true);
225
- const sealed = await tnSealOutput(this.ctHash, chainId, permission, thresholdNetworkUrl);
229
+ // const sealed = await tnSealOutputV1(this.ctHash, this.chainId, permission, thresholdNetworkUrl);
230
+ const sealed = await tnSealOutputV2(this.ctHash, this.chainId, permission, thresholdNetworkUrl);
226
231
  return PermitUtils.unseal(permit, sealed);
227
232
  }
228
233
 
@@ -246,36 +251,37 @@ export class DecryptHandlesBuilder<U extends FheTypes> extends BaseBuilder {
246
251
  *
247
252
  * @returns The unsealed item.
248
253
  */
249
- decrypt(): Promise<Result<UnsealedItem<U>>> {
250
- return resultWrapper(async () => {
251
- // Ensure cofhe client is connected
252
- await this.requireConnectedOrThrow();
254
+ async decrypt(): Promise<UnsealedItem<U>> {
255
+ // Ensure utype is valid
256
+ this.validateUtypeOrThrow();
253
257
 
254
- // Ensure utype is valid
255
- this.validateUtypeOrThrow();
258
+ // Resolve permit
259
+ const permit = await this.getResolvedPermit();
256
260
 
257
- // Resolve permit
258
- const permit = await this.getResolvedPermit();
261
+ // Ensure permit validity
262
+ // TODO: This doesn't validate permit expiration
263
+ // TODO: This doesn't throw, returns a validation result instead
264
+ PermitUtils.validate(permit);
259
265
 
260
- // Ensure permit validity
261
- await PermitUtils.validate(permit);
266
+ // TODO: Add this further validation step for the permit
267
+ // TODO: Ensure this throws if the permit is invalid
268
+ PermitUtils.isValid(permit);
262
269
 
263
- // Extract chainId from signed permit
264
- // Use this chainId to fetch the threshold network URL since this.chainId may be undefined
265
- const chainId = permit._signedDomain!.chainId;
270
+ // Extract chainId from signed permit
271
+ // Use this chainId to fetch the threshold network URL since this.chainId may be undefined
272
+ const chainId = permit._signedDomain!.chainId;
266
273
 
267
- // Check permit validity on-chain
268
- // TODO: PermitUtils.validateOnChain(permit, this.publicClient);
274
+ // Check permit validity on-chain
275
+ // TODO: PermitUtils.validateOnChain(permit, this.publicClient);
269
276
 
270
- let unsealed: bigint;
277
+ let unsealed: bigint;
271
278
 
272
- if (chainId === hardhat.id) {
273
- unsealed = await this.mocksSealOutput(permit);
274
- } else {
275
- unsealed = await this.productionSealOutput(chainId, permit);
276
- }
279
+ if (chainId === hardhat.id) {
280
+ unsealed = await this.mocksSealOutput(permit);
281
+ } else {
282
+ unsealed = await this.productionSealOutput(permit);
283
+ }
277
284
 
278
- return convertViaUtype(this.utype, unsealed);
279
- });
285
+ return convertViaUtype(this.utype, unsealed);
280
286
  }
281
287
  }
@@ -2,7 +2,7 @@ import { type Permission, type EthEncryptedData } from '@/permits';
2
2
 
3
3
  import { CofhesdkError, CofhesdkErrorCode } from '../error.js';
4
4
 
5
- export async function tnSealOutput(
5
+ export async function tnSealOutputV1(
6
6
  ctHash: bigint,
7
7
  chainId: number,
8
8
  permission: Permission,
@@ -0,0 +1,298 @@
1
+ import { type Permission, type EthEncryptedData } from '@/permits';
2
+
3
+ import { CofhesdkError, CofhesdkErrorCode } from '../error.js';
4
+
5
+ // Polling configuration
6
+ const POLL_INTERVAL_MS = 1000; // 1 second
7
+ const POLL_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
8
+
9
+ // V2 API response types
10
+ type SealOutputSubmitResponse = {
11
+ request_id: string;
12
+ };
13
+
14
+ type SealOutputStatusResponse = {
15
+ request_id: string;
16
+ status: 'PROCESSING' | 'COMPLETED';
17
+ submitted_at: string;
18
+ completed_at?: string;
19
+ is_succeed?: boolean;
20
+ sealed?: {
21
+ data: number[];
22
+ public_key: number[];
23
+ nonce: number[];
24
+ };
25
+ signature?: string;
26
+ encryption_type?: number;
27
+ error_message?: string | null;
28
+ };
29
+
30
+ /**
31
+ * Converts a number array to Uint8Array
32
+ */
33
+ function numberArrayToUint8Array(arr: number[]): Uint8Array {
34
+ return new Uint8Array(arr);
35
+ }
36
+
37
+ /**
38
+ * Converts the sealed data from the API response to EthEncryptedData
39
+ */
40
+ function convertSealedData(sealed: SealOutputStatusResponse['sealed']): EthEncryptedData {
41
+ if (!sealed) {
42
+ throw new CofhesdkError({
43
+ code: CofhesdkErrorCode.SealOutputReturnedNull,
44
+ message: 'Sealed data is missing from completed response',
45
+ });
46
+ }
47
+
48
+ return {
49
+ data: numberArrayToUint8Array(sealed.data),
50
+ public_key: numberArrayToUint8Array(sealed.public_key),
51
+ nonce: numberArrayToUint8Array(sealed.nonce),
52
+ };
53
+ }
54
+
55
+ /**
56
+ * Submits a sealoutput request to the v2 API and returns the request_id
57
+ */
58
+ async function submitSealOutputRequest(
59
+ thresholdNetworkUrl: string,
60
+ ctHash: bigint,
61
+ chainId: number,
62
+ permission: Permission
63
+ ): Promise<string> {
64
+ const body = {
65
+ ct_tempkey: ctHash.toString(16).padStart(64, '0'),
66
+ host_chain_id: chainId,
67
+ permit: permission,
68
+ };
69
+
70
+ let response: Response;
71
+ try {
72
+ response = await fetch(`${thresholdNetworkUrl}/v2/sealoutput`, {
73
+ method: 'POST',
74
+ headers: {
75
+ 'Content-Type': 'application/json',
76
+ },
77
+ body: JSON.stringify(body),
78
+ });
79
+ } catch (e) {
80
+ throw new CofhesdkError({
81
+ code: CofhesdkErrorCode.SealOutputFailed,
82
+ message: `sealOutput request failed`,
83
+ hint: 'Ensure the threshold network URL is valid and reachable.',
84
+ cause: e instanceof Error ? e : undefined,
85
+ context: {
86
+ thresholdNetworkUrl,
87
+ body,
88
+ },
89
+ });
90
+ }
91
+
92
+ // Handle non-200 status codes
93
+ if (!response.ok) {
94
+ let errorMessage = `HTTP ${response.status}`;
95
+ try {
96
+ const errorBody = await response.json();
97
+ errorMessage = errorBody.error_message || errorBody.message || errorMessage;
98
+ } catch {
99
+ // Ignore JSON parse errors, use status text
100
+ errorMessage = response.statusText || errorMessage;
101
+ }
102
+
103
+ throw new CofhesdkError({
104
+ code: CofhesdkErrorCode.SealOutputFailed,
105
+ message: `sealOutput request failed: ${errorMessage}`,
106
+ hint: 'Check the threshold network URL and request parameters.',
107
+ context: {
108
+ thresholdNetworkUrl,
109
+ status: response.status,
110
+ statusText: response.statusText,
111
+ body,
112
+ },
113
+ });
114
+ }
115
+
116
+ let submitResponse: SealOutputSubmitResponse;
117
+ try {
118
+ submitResponse = (await response.json()) as SealOutputSubmitResponse;
119
+ } catch (e) {
120
+ throw new CofhesdkError({
121
+ code: CofhesdkErrorCode.SealOutputFailed,
122
+ message: `Failed to parse sealOutput submit response`,
123
+ cause: e instanceof Error ? e : undefined,
124
+ context: {
125
+ thresholdNetworkUrl,
126
+ body,
127
+ },
128
+ });
129
+ }
130
+
131
+ if (!submitResponse.request_id) {
132
+ throw new CofhesdkError({
133
+ code: CofhesdkErrorCode.SealOutputFailed,
134
+ message: `sealOutput submit response missing request_id`,
135
+ context: {
136
+ thresholdNetworkUrl,
137
+ body,
138
+ submitResponse,
139
+ },
140
+ });
141
+ }
142
+
143
+ return submitResponse.request_id;
144
+ }
145
+
146
+ /**
147
+ * Polls for the sealoutput status until completed or timeout
148
+ */
149
+ async function pollSealOutputStatus(thresholdNetworkUrl: string, requestId: string): Promise<EthEncryptedData> {
150
+ const startTime = Date.now();
151
+ let completed = false;
152
+
153
+ while (!completed) {
154
+ // Check timeout
155
+ if (Date.now() - startTime > POLL_TIMEOUT_MS) {
156
+ throw new CofhesdkError({
157
+ code: CofhesdkErrorCode.SealOutputFailed,
158
+ message: `sealOutput polling timed out after ${POLL_TIMEOUT_MS}ms`,
159
+ hint: 'The request may still be processing. Try again later.',
160
+ context: {
161
+ thresholdNetworkUrl,
162
+ requestId,
163
+ timeoutMs: POLL_TIMEOUT_MS,
164
+ },
165
+ });
166
+ }
167
+
168
+ let response: Response;
169
+ try {
170
+ response = await fetch(`${thresholdNetworkUrl}/v2/sealoutput/${requestId}`, {
171
+ method: 'GET',
172
+ headers: {
173
+ 'Content-Type': 'application/json',
174
+ },
175
+ });
176
+ } catch (e) {
177
+ throw new CofhesdkError({
178
+ code: CofhesdkErrorCode.SealOutputFailed,
179
+ message: `sealOutput status poll failed`,
180
+ hint: 'Ensure the threshold network URL is valid and reachable.',
181
+ cause: e instanceof Error ? e : undefined,
182
+ context: {
183
+ thresholdNetworkUrl,
184
+ requestId,
185
+ },
186
+ });
187
+ }
188
+
189
+ // Handle 404 - request not found
190
+ if (response.status === 404) {
191
+ throw new CofhesdkError({
192
+ code: CofhesdkErrorCode.SealOutputFailed,
193
+ message: `sealOutput request not found: ${requestId}`,
194
+ hint: 'The request may have expired or been invalid.',
195
+ context: {
196
+ thresholdNetworkUrl,
197
+ requestId,
198
+ },
199
+ });
200
+ }
201
+
202
+ // Handle other non-200 status codes
203
+ if (!response.ok) {
204
+ let errorMessage = `HTTP ${response.status}`;
205
+ try {
206
+ const errorBody = await response.json();
207
+ errorMessage = errorBody.error_message || errorBody.message || errorMessage;
208
+ } catch {
209
+ errorMessage = response.statusText || errorMessage;
210
+ }
211
+
212
+ throw new CofhesdkError({
213
+ code: CofhesdkErrorCode.SealOutputFailed,
214
+ message: `sealOutput status poll failed: ${errorMessage}`,
215
+ context: {
216
+ thresholdNetworkUrl,
217
+ requestId,
218
+ status: response.status,
219
+ statusText: response.statusText,
220
+ },
221
+ });
222
+ }
223
+
224
+ let statusResponse: SealOutputStatusResponse;
225
+ try {
226
+ statusResponse = (await response.json()) as SealOutputStatusResponse;
227
+ } catch (e) {
228
+ throw new CofhesdkError({
229
+ code: CofhesdkErrorCode.SealOutputFailed,
230
+ message: `Failed to parse sealOutput status response`,
231
+ cause: e instanceof Error ? e : undefined,
232
+ context: {
233
+ thresholdNetworkUrl,
234
+ requestId,
235
+ },
236
+ });
237
+ }
238
+
239
+ // Check if completed
240
+ if (statusResponse.status === 'COMPLETED') {
241
+ // Check if succeeded
242
+ if (statusResponse.is_succeed === false) {
243
+ const errorMessage = statusResponse.error_message || 'Unknown error';
244
+ throw new CofhesdkError({
245
+ code: CofhesdkErrorCode.SealOutputFailed,
246
+ message: `sealOutput request failed: ${errorMessage}`,
247
+ context: {
248
+ thresholdNetworkUrl,
249
+ requestId,
250
+ statusResponse,
251
+ },
252
+ });
253
+ }
254
+
255
+ // Check if sealed data exists
256
+ if (!statusResponse.sealed) {
257
+ throw new CofhesdkError({
258
+ code: CofhesdkErrorCode.SealOutputReturnedNull,
259
+ message: `sealOutput request completed but returned no sealed data`,
260
+ context: {
261
+ thresholdNetworkUrl,
262
+ requestId,
263
+ statusResponse,
264
+ },
265
+ });
266
+ }
267
+
268
+ // Convert and return the sealed data
269
+ return convertSealedData(statusResponse.sealed);
270
+ }
271
+
272
+ // Still processing, wait before next poll
273
+ await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
274
+ }
275
+
276
+ // This should never be reached, but TypeScript requires it
277
+ throw new CofhesdkError({
278
+ code: CofhesdkErrorCode.SealOutputFailed,
279
+ message: 'Polling loop exited unexpectedly',
280
+ context: {
281
+ thresholdNetworkUrl,
282
+ requestId,
283
+ },
284
+ });
285
+ }
286
+
287
+ export async function tnSealOutputV2(
288
+ ctHash: bigint,
289
+ chainId: number,
290
+ permission: Permission,
291
+ thresholdNetworkUrl: string
292
+ ): Promise<EthEncryptedData> {
293
+ // Step 1: Submit the request and get request_id
294
+ const requestId = await submitSealOutputRequest(thresholdNetworkUrl, ctHash, chainId, permission);
295
+
296
+ // Step 2: Poll for status until completed
297
+ return await pollSealOutputStatus(thresholdNetworkUrl, requestId);
298
+ }