@cofhe/sdk 0.6.1 → 0.7.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 (113) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/acps/acp.ts +411 -0
  3. package/acps/index.ts +70 -0
  4. package/{permits → acps}/onchain-utils.ts +49 -24
  5. package/acps/sealing.ts +90 -0
  6. package/acps/signature.ts +89 -0
  7. package/acps/store.ts +172 -0
  8. package/acps/test/acp.test.ts +615 -0
  9. package/acps/test/localstorage.test.ts +105 -0
  10. package/acps/test/sealing.test.ts +77 -0
  11. package/acps/test/store.test.ts +88 -0
  12. package/acps/test/validation.test.ts +361 -0
  13. package/acps/test-utils.ts +32 -0
  14. package/acps/types.ts +252 -0
  15. package/acps/validation.ts +392 -0
  16. package/adapters/test/ethers5.test.ts +4 -1
  17. package/adapters/test/ethers6.test.ts +4 -1
  18. package/adapters/test/wagmi.test.ts +5 -2
  19. package/chains/chains/stagingCofhe.ts +21 -0
  20. package/chains/index.ts +3 -1
  21. package/chains/test/chains.test.ts +2 -1
  22. package/core/acps.ts +625 -0
  23. package/core/client.ts +136 -39
  24. package/core/clientTypes.ts +52 -41
  25. package/core/config.ts +66 -5
  26. package/core/decrypt/MockThresholdNetworkAbi.ts +20 -11
  27. package/core/decrypt/apiError.ts +104 -0
  28. package/core/decrypt/cofheMocksDecryptForTx.ts +11 -11
  29. package/core/decrypt/cofheMocksDecryptForView.ts +7 -7
  30. package/core/decrypt/decryptForTxBuilder.ts +102 -102
  31. package/core/decrypt/decryptForViewBuilder.ts +90 -90
  32. package/core/decrypt/submitRetry.ts +38 -30
  33. package/core/decrypt/tnDecryptV1.ts +5 -5
  34. package/core/decrypt/tnDecryptV2.ts +25 -21
  35. package/core/decrypt/tnSealOutputV1.ts +4 -3
  36. package/core/decrypt/tnSealOutputV2.ts +24 -18
  37. package/core/encrypt/cofheMocksZkVerifySign.ts +59 -74
  38. package/core/encrypt/encryptInputsBuilder.ts +86 -52
  39. package/core/encrypt/zkPackProveVerify.ts +25 -18
  40. package/core/error.ts +34 -6
  41. package/core/index.ts +4 -14
  42. package/core/test/acpDefaults.test.ts +52 -0
  43. package/core/test/acps.test.ts +596 -0
  44. package/core/test/apiError.test.ts +130 -0
  45. package/core/test/client.test.ts +22 -19
  46. package/core/test/config.test.ts +25 -5
  47. package/core/test/decrypt.test.ts +40 -35
  48. package/core/test/decryptBuilders.test.ts +68 -68
  49. package/core/test/decryptErrorCodes.test.ts +217 -0
  50. package/core/test/encryptInputsBuilder.test.ts +72 -41
  51. package/core/test/pollCallbacks.test.ts +89 -18
  52. package/core/test/stagingRedirect.ts +18 -0
  53. package/core/test/submitRetry.test.ts +182 -0
  54. package/core/types.ts +9 -69
  55. package/dist/acp-Wi6isVQI.d.cts +407 -0
  56. package/dist/acp-Wi6isVQI.d.ts +407 -0
  57. package/dist/acps.cjs +1081 -0
  58. package/dist/acps.d.cts +480 -0
  59. package/dist/acps.d.ts +480 -0
  60. package/dist/acps.js +2 -0
  61. package/dist/chains.cjs +14 -1
  62. package/dist/chains.d.cts +30 -1
  63. package/dist/chains.d.ts +30 -1
  64. package/dist/chains.js +1 -1
  65. package/dist/{chunk-NOC3PYB7.js → chunk-43USWPEH.js} +930 -580
  66. package/dist/{chunk-MTRAXQXC.js → chunk-N6IDQRRU.js} +14 -2
  67. package/dist/chunk-Q7CBWGQX.js +1029 -0
  68. package/dist/{clientTypes-CyUvRRzA.d.ts → clientTypes-BN3nbzYM.d.ts} +342 -165
  69. package/dist/{clientTypes-BDy1qIBu.d.cts → clientTypes-CYZjFznO.d.cts} +342 -165
  70. package/dist/core.cjs +1358 -1002
  71. package/dist/core.d.cts +31 -9
  72. package/dist/core.d.ts +31 -9
  73. package/dist/core.js +3 -3
  74. package/dist/node.cjs +1293 -952
  75. package/dist/node.d.cts +2 -2
  76. package/dist/node.d.ts +2 -2
  77. package/dist/node.js +3 -3
  78. package/dist/web.cjs +1293 -952
  79. package/dist/web.d.cts +2 -2
  80. package/dist/web.d.ts +2 -2
  81. package/dist/web.js +3 -3
  82. package/node/test/inherited.test.ts +75 -65
  83. package/node/test/tfheinit.test.ts +23 -8
  84. package/package.json +6 -6
  85. package/web/test/client.web.test.ts +5 -1
  86. package/web/test/inherited.web.test.ts +75 -65
  87. package/web/test/tfheinit.web.test.ts +14 -5
  88. package/web/test/worker.config.web.test.ts +38 -23
  89. package/web/test/worker.output.web.test.ts +25 -24
  90. package/core/encrypt/encryptUtils.ts +0 -67
  91. package/core/permits.ts +0 -216
  92. package/core/test/permits.test.ts +0 -596
  93. package/dist/chunk-VB62WYPL.js +0 -978
  94. package/dist/permit-DnVMDT5h.d.cts +0 -376
  95. package/dist/permit-DnVMDT5h.d.ts +0 -376
  96. package/dist/permits.cjs +0 -1026
  97. package/dist/permits.d.cts +0 -353
  98. package/dist/permits.d.ts +0 -353
  99. package/dist/permits.js +0 -2
  100. package/permits/index.ts +0 -68
  101. package/permits/permit.ts +0 -385
  102. package/permits/sealing.ts +0 -131
  103. package/permits/signature.ts +0 -79
  104. package/permits/store.ts +0 -157
  105. package/permits/test/localstorage.test.ts +0 -113
  106. package/permits/test/permit.test.ts +0 -557
  107. package/permits/test/sealing.test.ts +0 -84
  108. package/permits/test/store.test.ts +0 -88
  109. package/permits/test/validation.test.ts +0 -361
  110. package/permits/test-utils.ts +0 -28
  111. package/permits/types.ts +0 -204
  112. package/permits/validation.ts +0 -327
  113. /package/{permits → acps}/utils.ts +0 -0
@@ -1,11 +1,11 @@
1
1
  /* eslint-disable no-dupe-class-members */
2
2
  import { hardhat } from '@/chains';
3
- import { type Permit, PermitUtils } from '@/permits';
3
+ import { type ACP, ACPUtils } from '@/acps';
4
4
 
5
5
  import { FheTypes, type UnsealedItem } from '../types.js';
6
6
  import { getThresholdNetworkUrlOrThrow } from '../config.js';
7
7
  import { CofheError, CofheErrorCode } from '../error.js';
8
- import { permits } from '../permits.js';
8
+ import { acps } from '../acps.js';
9
9
  import { isValidUtype, convertViaUtype } from './decryptUtils.js';
10
10
  import { BaseBuilder, type BaseBuilderParams } from '../baseBuilder.js';
11
11
  import { cofheMocksDecryptForView } from './cofheMocksDecryptForView.js';
@@ -22,17 +22,17 @@ const DEFAULT_404_RETRY_TIMEOUT_MS = 10_000;
22
22
  * await client.decryptForView(ctHash, utype)
23
23
  * .setChainId(chainId)
24
24
  * .setAccount(account)
25
- * .withPermit() // optional (active permit)
26
- * // or .withPermit(permitHash) / .withPermit(permit)
25
+ * .withACP() // optional (active acp)
26
+ * // or .withACP(acpHash) / .withACP(acp)
27
27
  * .execute()
28
28
  *
29
29
  * If chainId not set, uses client's chainId
30
30
  * If account not set, uses client's account
31
- * withPermit() uses chainId + account to get the active permit.
32
- * withPermit(permitHash) fetches that permit using chainId + account.
33
- * withPermit(permit) uses the provided permit regardless of chainId/account.
31
+ * withACP() uses chainId + account to get the active acp.
32
+ * withACP(acpHash) fetches that acp using chainId + account.
33
+ * withACP(acp) uses the provided acp regardless of chainId/account.
34
34
  *
35
- * Note: decryptForView always requires a permit (no global-allowance mode).
35
+ * Note: decryptForView always requires an ACP (no global-allowance mode).
36
36
  *
37
37
  * Returns the unsealed item.
38
38
  */
@@ -40,15 +40,15 @@ const DEFAULT_404_RETRY_TIMEOUT_MS = 10_000;
40
40
  type DecryptForViewBuilderParams<U extends FheTypes> = BaseBuilderParams & {
41
41
  ctHash: bigint | string;
42
42
  utype: U;
43
- permitHash?: string;
44
- permit?: Permit;
43
+ acpHash?: string;
44
+ acp?: ACP;
45
45
  };
46
46
 
47
47
  export class DecryptForViewBuilder<U extends FheTypes> extends BaseBuilder {
48
48
  private ctHash: bigint | string;
49
49
  private utype: U;
50
- private permitHash?: string;
51
- private permit?: Permit;
50
+ private acpHash?: string;
51
+ private acp?: ACP;
52
52
  private pollCallback?: DecryptPollCallbackFunction;
53
53
  private retry404TimeoutMs = DEFAULT_404_RETRY_TIMEOUT_MS;
54
54
 
@@ -64,12 +64,12 @@ export class DecryptForViewBuilder<U extends FheTypes> extends BaseBuilder {
64
64
 
65
65
  this.ctHash = params.ctHash;
66
66
  this.utype = params.utype;
67
- this.permitHash = params.permitHash;
68
- this.permit = params.permit;
67
+ this.acpHash = params.acpHash;
68
+ this.acp = params.acp;
69
69
  }
70
70
 
71
71
  /**
72
- * @param chainId - Chain to decrypt values from. Used to fetch the threshold network URL and use the correct permit.
72
+ * @param chainId - Chain to decrypt values from. Used to fetch the threshold network URL and use the correct acp.
73
73
  *
74
74
  * If not provided, the chainId will be fetched from the connected publicClient.
75
75
  *
@@ -92,7 +92,7 @@ export class DecryptForViewBuilder<U extends FheTypes> extends BaseBuilder {
92
92
  }
93
93
 
94
94
  /**
95
- * @param account - Account to decrypt values from. Used to fetch the correct permit.
95
+ * @param account - Account to decrypt values from. Used to fetch the correct acp.
96
96
  *
97
97
  * If not provided, the account will be fetched from the connected walletClient.
98
98
  *
@@ -135,77 +135,77 @@ export class DecryptForViewBuilder<U extends FheTypes> extends BaseBuilder {
135
135
  }
136
136
 
137
137
  /**
138
- * Select "use permit" mode (optional).
138
+ * Select "use acp" mode (optional).
139
139
  *
140
- * - `withPermit(permit)` uses the provided permit.
141
- * - `withPermit(permitHash)` fetches that permit.
142
- * - `withPermit()` uses the active permit for the resolved `chainId + account`.
140
+ * - `withACP(acp)` uses the provided acp.
141
+ * - `withACP(acpHash)` fetches that acp.
142
+ * - `withACP()` uses the active acp for the resolved `chainId + account`.
143
143
  */
144
- withPermit(): DecryptForViewBuilder<U>;
145
- withPermit(permitHash: string): DecryptForViewBuilder<U>;
146
- withPermit(permit: Permit): DecryptForViewBuilder<U>;
147
- withPermit(permitOrPermitHash?: Permit | string): DecryptForViewBuilder<U> {
148
- if (typeof permitOrPermitHash === 'string') {
149
- this.permitHash = permitOrPermitHash;
150
- this.permit = undefined;
151
- } else if (permitOrPermitHash === undefined) {
152
- // Explicitly choose "active permit" resolution at execute()
153
- this.permitHash = undefined;
154
- this.permit = undefined;
144
+ withACP(): DecryptForViewBuilder<U>;
145
+ withACP(acpHash: string): DecryptForViewBuilder<U>;
146
+ withACP(acp: ACP): DecryptForViewBuilder<U>;
147
+ withACP(acpOrACPHash?: ACP | string): DecryptForViewBuilder<U> {
148
+ if (typeof acpOrACPHash === 'string') {
149
+ this.acpHash = acpOrACPHash;
150
+ this.acp = undefined;
151
+ } else if (acpOrACPHash === undefined) {
152
+ // Explicitly choose "active acp" resolution at execute()
153
+ this.acpHash = undefined;
154
+ this.acp = undefined;
155
155
  } else {
156
- // Permit object
157
- this.permit = permitOrPermitHash;
158
- this.permitHash = undefined;
156
+ // ACP object
157
+ this.acp = acpOrACPHash;
158
+ this.acpHash = undefined;
159
159
  }
160
160
 
161
161
  return this;
162
162
  }
163
163
 
164
164
  /**
165
- * @param permitHash - Permit hash to decrypt values from. Used to fetch the correct permit.
165
+ * @param acpHash - ACP hash to decrypt values from. Used to fetch the correct acp.
166
166
  *
167
- * If not provided, the active permit for the chainId and account will be used.
168
- * If `setPermit()` is called, it will be used regardless of chainId, account, or permitHash.
167
+ * If not provided, the active acp for the chainId and account will be used.
168
+ * If `setACP()` is called, it will be used regardless of chainId, account, or acpHash.
169
169
  *
170
170
  * Example:
171
171
  * ```typescript
172
172
  * const unsealed = await client.decryptForView(ctHash, utype)
173
- * .setPermitHash('0x1234567890123456789012345678901234567890')
173
+ * .setACPHash('0x1234567890123456789012345678901234567890')
174
174
  * .execute();
175
175
  * ```
176
176
  *
177
177
  * @returns The chainable DecryptForViewBuilder instance.
178
178
  */
179
- /** @deprecated Use `withPermit(permitHash)` instead. */
180
- setPermitHash(permitHash: string): DecryptForViewBuilder<U> {
181
- return this.withPermit(permitHash);
179
+ /** @deprecated Use `withACP(acpHash)` instead. */
180
+ setACPHash(acpHash: string): DecryptForViewBuilder<U> {
181
+ return this.withACP(acpHash);
182
182
  }
183
183
 
184
- getPermitHash(): string | undefined {
185
- return this.permitHash;
184
+ getACPHash(): string | undefined {
185
+ return this.acpHash;
186
186
  }
187
187
 
188
188
  /**
189
- * @param permit - Permit to decrypt values with. If provided, it will be used regardless of chainId, account, or permitHash.
189
+ * @param acp - ACP to decrypt values with. If provided, it will be used regardless of chainId, account, or acpHash.
190
190
  *
191
- * If not provided, the permit will be determined by chainId, account, and permitHash.
191
+ * If not provided, the acp will be determined by chainId, account, and acpHash.
192
192
  *
193
193
  * Example:
194
194
  * ```typescript
195
195
  * const unsealed = await client.decryptForView(ctHash, utype)
196
- * .setPermit(permit)
196
+ * .setACP(acp)
197
197
  * .execute();
198
198
  * ```
199
199
  *
200
200
  * @returns The chainable DecryptForViewBuilder instance.
201
201
  */
202
- /** @deprecated Use `withPermit(permit)` instead. */
203
- setPermit(permit: Permit): DecryptForViewBuilder<U> {
204
- return this.withPermit(permit);
202
+ /** @deprecated Use `withACP(acp)` instead. */
203
+ setACP(acp: ACP): DecryptForViewBuilder<U> {
204
+ return this.withACP(acp);
205
205
  }
206
206
 
207
- getPermit(): Permit | undefined {
208
- return this.permit;
207
+ getACP(): ACP | undefined {
208
+ return this.acp;
209
209
  }
210
210
 
211
211
  private async getThresholdNetworkUrl(): Promise<string> {
@@ -224,50 +224,50 @@ export class DecryptForViewBuilder<U extends FheTypes> extends BaseBuilder {
224
224
  });
225
225
  }
226
226
 
227
- private async getResolvedPermit(): Promise<Permit> {
228
- if (this.permit) return this.permit;
227
+ private async getResolvedACP(): Promise<ACP> {
228
+ if (this.acp) return this.acp;
229
229
 
230
230
  this.assertChainId();
231
231
  this.assertAccount();
232
232
 
233
- // Fetch with permit hash
234
- if (this.permitHash) {
235
- const permit = await permits.getPermit(this.chainId, this.account, this.permitHash);
236
- if (!permit) {
233
+ // Fetch with acp hash
234
+ if (this.acpHash) {
235
+ const acp = await acps.getACP(this.chainId, this.account, this.acpHash);
236
+ if (!acp) {
237
237
  throw new CofheError({
238
- code: CofheErrorCode.PermitNotFound,
239
- message: `Permit with hash <${this.permitHash}> not found for account <${this.account}> and chainId <${this.chainId}>`,
240
- hint: 'Ensure the permit exists and is valid.',
238
+ code: CofheErrorCode.ACPNotFound,
239
+ message: `ACP with hash <${this.acpHash}> not found for account <${this.account}> and chainId <${this.chainId}>`,
240
+ hint: 'Ensure the acp exists and is valid.',
241
241
  context: {
242
242
  chainId: this.chainId,
243
243
  account: this.account,
244
- permitHash: this.permitHash,
244
+ acpHash: this.acpHash,
245
245
  },
246
246
  });
247
247
  }
248
- return permit;
248
+ return acp;
249
249
  }
250
250
 
251
- // Fetch with active permit
252
- const permit = await permits.getActivePermit(this.chainId, this.account);
253
- if (!permit) {
251
+ // Fetch with active acp
252
+ const acp = await acps.getActiveACP(this.chainId, this.account);
253
+ if (!acp) {
254
254
  throw new CofheError({
255
- code: CofheErrorCode.PermitNotFound,
256
- message: `Active permit not found for chainId <${this.chainId}> and account <${this.account}>`,
257
- hint: 'Ensure a permit exists for this account on this chain.',
255
+ code: CofheErrorCode.ACPNotFound,
256
+ message: `Active acp not found for chainId <${this.chainId}> and account <${this.account}>`,
257
+ hint: 'Ensure an ACP exists for this account on this chain.',
258
258
  context: {
259
259
  chainId: this.chainId,
260
260
  account: this.account,
261
261
  },
262
262
  });
263
263
  }
264
- return permit;
264
+ return acp;
265
265
  }
266
266
 
267
267
  /**
268
268
  * On hardhat, interact with MockZkVerifier contract instead of CoFHE
269
269
  */
270
- private async mocksSealOutput(permit: Permit): Promise<bigint> {
270
+ private async mocksSealOutput(acp: ACP): Promise<bigint> {
271
271
  this.assertPublicClient();
272
272
 
273
273
  // Configurable delay before decrypting the output to simulate the CoFHE decrypt processing time
@@ -276,38 +276,38 @@ export class DecryptForViewBuilder<U extends FheTypes> extends BaseBuilder {
276
276
  const mocksDecryptDelay = this.config.mocks.decryptDelay;
277
277
  if (mocksDecryptDelay > 0) await sleep(mocksDecryptDelay);
278
278
 
279
- return cofheMocksDecryptForView(this.ctHash, this.utype, permit, this.publicClient);
279
+ return cofheMocksDecryptForView(this.ctHash, this.utype, acp, this.publicClient);
280
280
  }
281
281
 
282
282
  /**
283
283
  * In the production context, perform a true decryption with the CoFHE coprocessor.
284
284
  */
285
- private async productionSealOutput(permit: Permit): Promise<bigint> {
285
+ private async productionSealOutput(acp: ACP): Promise<bigint> {
286
286
  this.assertChainId();
287
287
  this.assertPublicClient();
288
288
 
289
289
  const thresholdNetworkUrl = await this.getThresholdNetworkUrl();
290
- const permission = PermitUtils.getPermission(permit, true);
290
+ const wireAcp = ACPUtils.getPublic(acp, true);
291
291
  // const sealed = await tnSealOutputV1(this.ctHash, this.chainId, permission, thresholdNetworkUrl);
292
292
  const sealed = await tnSealOutputV2({
293
293
  ctHash: this.ctHash,
294
294
  chainId: this.chainId,
295
- permission,
295
+ acp: wireAcp,
296
296
  thresholdNetworkUrl,
297
297
  retry404TimeoutMs: this.retry404TimeoutMs,
298
298
  onPoll: this.pollCallback,
299
299
  });
300
- return PermitUtils.unseal(permit, sealed);
300
+ return ACPUtils.unseal(acp, sealed);
301
301
  }
302
302
 
303
303
  /**
304
304
  * Final step of the decryption process. MUST BE CALLED LAST IN THE CHAIN.
305
305
  *
306
306
  * This will:
307
- * - Use a permit based on provided permit OR chainId + account + permitHash
308
- * - Check permit validity
309
- * - Call CoFHE `/sealoutput` with the permit, which returns a sealed (encrypted) item
310
- * - Unseal the sealed item with the permit
307
+ * - Use an ACP based on provided acp OR chainId + account + acpHash
308
+ * - Check acp validity
309
+ * - Call CoFHE `/sealoutput` with the acp, which returns a sealed (encrypted) item
310
+ * - Unseal the sealed item with the acp
311
311
  * - Return the unsealed item
312
312
  *
313
313
  * Example:
@@ -315,7 +315,7 @@ export class DecryptForViewBuilder<U extends FheTypes> extends BaseBuilder {
315
315
  * const unsealed = await client.decryptForView(ctHash, utype)
316
316
  * .setChainId(11155111) // optional
317
317
  * .setAccount('0x123...890') // optional
318
- * .withPermit() // optional
318
+ * .withACP() // optional
319
319
  * .execute(); // execute
320
320
  * ```
321
321
  *
@@ -325,25 +325,25 @@ export class DecryptForViewBuilder<U extends FheTypes> extends BaseBuilder {
325
325
  // Ensure utype is valid
326
326
  this.validateUtypeOrThrow();
327
327
 
328
- // Resolve permit
329
- const permit = await this.getResolvedPermit();
328
+ // Resolve acp
329
+ const acp = await this.getResolvedACP();
330
330
 
331
- // Ensure permit validity
332
- PermitUtils.validate(permit);
331
+ // Ensure acp validity
332
+ ACPUtils.validate(acp);
333
333
 
334
- // Extract chainId from signed permit
334
+ // Extract chainId from signed acp
335
335
  // Use this chainId to fetch the threshold network URL since this.chainId may be undefined
336
- const chainId = permit._signedDomain!.chainId;
336
+ const chainId = acp._signedDomain!.chainId;
337
337
 
338
- // Check permit validity on-chain
339
- // TODO: PermitUtils.validateOnChain(permit, this.publicClient);
338
+ // Check acp validity on-chain
339
+ // TODO: ACPUtils.validateOnChain(acp, this.publicClient);
340
340
 
341
341
  let unsealed: bigint;
342
342
 
343
343
  if (chainId === hardhat.id) {
344
- unsealed = await this.mocksSealOutput(permit);
344
+ unsealed = await this.mocksSealOutput(acp);
345
345
  } else {
346
- unsealed = await this.productionSealOutput(permit);
346
+ unsealed = await this.productionSealOutput(acp);
347
347
  }
348
348
 
349
349
  return convertViaUtype(this.utype, unsealed);
@@ -1,17 +1,21 @@
1
1
  import { CofheError, CofheErrorCode } from '../error.js';
2
+ import { mapApiErrorCodeToCofheErrorCode, parseApiErrorResponseBody } from './apiError.js';
2
3
 
3
4
  export const DEFAULT_404_RETRY_TIMEOUT_MS = 10_000;
4
5
 
5
6
  export type SubmitRetryableStatus = 204 | 404;
6
7
 
7
8
  export type SubmitResponseClassification =
8
- | { kind: 'retryable'; status: SubmitRetryableStatus }
9
+ | { kind: 'retryable'; status: 204 }
10
+ | { kind: 'retryable'; status: 404; apiErrorMessage: string }
9
11
  | { kind: 'parse-json' }
10
- | { kind: 'fatal-http'; errorMessage: string };
11
-
12
- export function isRetryableSubmitStatus(status: number): status is SubmitRetryableStatus {
13
- return status === 204 || status === 404;
14
- }
12
+ | {
13
+ kind: 'fatal-http';
14
+ status: number;
15
+ cofheErrorCode: CofheErrorCode;
16
+ apiErrorCode?: string;
17
+ errorMessage: string;
18
+ };
15
19
 
16
20
  export function normalize404RetryTimeoutMs(params: {
17
21
  timeoutMs: number | undefined;
@@ -35,40 +39,39 @@ export function normalize404RetryTimeoutMs(params: {
35
39
  return timeoutMs;
36
40
  }
37
41
 
42
+ /**
43
+ * Classifies a submit (POST) response into: bodyless-retryable 204, any 404 (still-indexing /
44
+ * not-found — retryable under the 404 retry budget), a 200 to be JSON-parsed, or a fatal error
45
+ * carrying the mapped CofheErrorCode.
46
+ */
38
47
  export async function classifySubmitResponse(params: {
39
48
  response: Response;
40
- extractErrorMessage?: (errorBody: unknown) => string | undefined;
49
+ fallbackErrorCode: CofheErrorCode;
41
50
  }): Promise<SubmitResponseClassification> {
42
- const { response, extractErrorMessage } = params;
51
+ const { response, fallbackErrorCode } = params;
43
52
 
44
- if (isRetryableSubmitStatus(response.status)) {
45
- return { kind: 'retryable', status: response.status };
53
+ // 204 is documented bodyless — never touch response.json() for it.
54
+ if (response.status === 204) {
55
+ return { kind: 'retryable', status: 204 };
46
56
  }
47
57
 
48
58
  if (response.ok) {
49
59
  return { kind: 'parse-json' };
50
60
  }
51
61
 
52
- let errorMessage = `HTTP ${response.status}`;
53
- try {
54
- const errorBody = (await response.json()) as unknown;
55
- const maybeErrorMessage = extractErrorMessage?.(errorBody);
56
- if (typeof maybeErrorMessage === 'string' && maybeErrorMessage.length > 0) {
57
- errorMessage = maybeErrorMessage;
58
- } else if (errorBody && typeof errorBody === 'object') {
59
- const defaultMessage = (errorBody as Record<string, unknown>).error_message;
60
- const fallbackMessage = (errorBody as Record<string, unknown>).message;
61
- if (typeof defaultMessage === 'string' && defaultMessage.length > 0) {
62
- errorMessage = defaultMessage;
63
- } else if (typeof fallbackMessage === 'string' && fallbackMessage.length > 0) {
64
- errorMessage = fallbackMessage;
65
- }
66
- }
67
- } catch {
68
- errorMessage = response.statusText || errorMessage;
62
+ const { apiErrorCode, errorMessage } = await parseApiErrorResponseBody(response);
63
+
64
+ if (response.status === 404) {
65
+ return { kind: 'retryable', status: 404, apiErrorMessage: errorMessage };
69
66
  }
70
67
 
71
- return { kind: 'fatal-http', errorMessage };
68
+ return {
69
+ kind: 'fatal-http',
70
+ status: response.status,
71
+ cofheErrorCode: mapApiErrorCodeToCofheErrorCode(apiErrorCode, fallbackErrorCode),
72
+ apiErrorCode,
73
+ errorMessage,
74
+ };
72
75
  }
73
76
 
74
77
  export function throwIfSubmitRetryTimedOut(params: {
@@ -81,6 +84,7 @@ export function throwIfSubmitRetryTimedOut(params: {
81
84
  thresholdNetworkUrl: string;
82
85
  body: unknown;
83
86
  attemptIndex: number;
87
+ lastKnownErrorMessage?: string;
84
88
  }): void {
85
89
  const {
86
90
  operationLabel,
@@ -92,12 +96,16 @@ export function throwIfSubmitRetryTimedOut(params: {
92
96
  thresholdNetworkUrl,
93
97
  body,
94
98
  attemptIndex,
99
+ lastKnownErrorMessage,
95
100
  } = params;
96
101
 
97
102
  if (status === 404 && elapsedMs > retry404TimeoutMs) {
98
103
  throw new CofheError({
99
- code: errorCode,
100
- message: `${operationLabel} submit retried 404 responses without receiving request_id for ${retry404TimeoutMs}ms`,
104
+ code: CofheErrorCode.CtNotFound,
105
+ apiErrorCode: 'ct_not_found',
106
+ message:
107
+ `${operationLabel} ciphertext not found after retrying for ${retry404TimeoutMs}ms` +
108
+ (lastKnownErrorMessage ? `: ${lastKnownErrorMessage}` : ''),
101
109
  hint: 'The ciphertext may not be indexed yet. Increase set404RetryTimeout(...) if the backend is slow to index ciphertexts.',
102
110
  context: {
103
111
  thresholdNetworkUrl,
@@ -1,4 +1,4 @@
1
- import { type Permission } from '@/permits';
1
+ import { type ACPPublic } from '@/acps';
2
2
 
3
3
  import { CofheError, CofheErrorCode } from '../error';
4
4
  import { normalizeTnSignature, parseDecryptedBytesToBigInt } from './tnDecryptUtils';
@@ -68,20 +68,20 @@ function assertTnDecryptResponseV1(value: unknown): TnDecryptResponseV1 {
68
68
  export async function tnDecryptV1(
69
69
  ctHash: bigint | string,
70
70
  chainId: number,
71
- permission: Permission | null,
71
+ acp: ACPPublic | null,
72
72
  thresholdNetworkUrl: string
73
73
  ): Promise<{ decryptedValue: bigint; signature: `0x${string}` }> {
74
74
  const body: {
75
75
  ct_tempkey: string;
76
76
  host_chain_id: number;
77
- permit?: Permission;
77
+ acp?: ACPPublic;
78
78
  } = {
79
79
  ct_tempkey: BigInt(ctHash).toString(16).padStart(64, '0'),
80
80
  host_chain_id: chainId,
81
81
  };
82
82
 
83
- if (permission) {
84
- body.permit = permission;
83
+ if (acp) {
84
+ body.acp = acp;
85
85
  }
86
86
 
87
87
  let response: Response;
@@ -1,10 +1,11 @@
1
- import { type Permission } from '@/permits';
1
+ import { type ACPPublic } from '@/acps';
2
2
  import { cofheFetch } from '../debug.js';
3
3
 
4
4
  import { CofheError, CofheErrorCode } from '../error';
5
5
  import { type DecryptPollCallbackFunction } from '../types';
6
6
  import { normalizeTnSignature, parseDecryptedBytesToBigInt } from './tnDecryptUtils';
7
7
  import { computeMinuteRampPollIntervalMs } from './polling.js';
8
+ import { mapApiErrorCodeToCofheErrorCode, parseApiErrorResponseBody } from './apiError.js';
8
9
  import { classifySubmitResponse, normalize404RetryTimeoutMs, throwIfSubmitRetryTimedOut } from './submitRetry.js';
9
10
 
10
11
  // Polling configuration
@@ -178,7 +179,7 @@ async function submitDecryptRequestV2(
178
179
  thresholdNetworkUrl: string,
179
180
  ctHash: bigint | string,
180
181
  chainId: number,
181
- permission: Permission | null,
182
+ acp: ACPPublic | null,
182
183
  overallStartTime: number,
183
184
  retry404TimeoutMs: number,
184
185
  onPoll?: DecryptPollCallbackFunction
@@ -186,17 +187,18 @@ async function submitDecryptRequestV2(
186
187
  const body: {
187
188
  ct_tempkey: string;
188
189
  host_chain_id: number;
189
- permit?: Permission;
190
+ acp?: ACPPublic;
190
191
  } = {
191
192
  ct_tempkey: BigInt(ctHash).toString(16).padStart(64, '0'),
192
193
  host_chain_id: chainId,
193
194
  };
194
195
 
195
- if (permission) {
196
- body.permit = permission;
196
+ if (acp) {
197
+ body.acp = acp;
197
198
  }
198
199
 
199
200
  let attemptIndex = 0;
201
+ let last404ApiErrorMessage: string | undefined;
200
202
 
201
203
  for (;;) {
202
204
  let response: Response;
@@ -225,17 +227,20 @@ async function submitDecryptRequestV2(
225
227
  });
226
228
  }
227
229
 
228
- const responseClassification = await classifySubmitResponse({ response });
230
+ const responseClassification = await classifySubmitResponse({
231
+ response,
232
+ fallbackErrorCode: CofheErrorCode.DecryptFailed,
233
+ });
229
234
 
230
235
  if (responseClassification.kind === 'fatal-http') {
231
236
  throw new CofheError({
232
- code: CofheErrorCode.DecryptFailed,
237
+ code: responseClassification.cofheErrorCode,
238
+ apiErrorCode: responseClassification.apiErrorCode,
233
239
  message: `decrypt request failed: ${responseClassification.errorMessage}`,
234
240
  hint: 'Check the threshold network URL and request parameters.',
235
241
  context: {
236
242
  thresholdNetworkUrl,
237
- status: response.status,
238
- statusText: response.statusText,
243
+ status: responseClassification.status,
239
244
  body,
240
245
  attemptIndex,
241
246
  },
@@ -289,6 +294,10 @@ async function submitDecryptRequestV2(
289
294
  });
290
295
  }
291
296
 
297
+ if (responseClassification.status === 404) {
298
+ last404ApiErrorMessage = responseClassification.apiErrorMessage;
299
+ }
300
+
292
301
  const elapsedMs = Date.now() - overallStartTime;
293
302
 
294
303
  throwIfSubmitRetryTimedOut({
@@ -301,6 +310,7 @@ async function submitDecryptRequestV2(
301
310
  thresholdNetworkUrl,
302
311
  body,
303
312
  attemptIndex,
313
+ lastKnownErrorMessage: last404ApiErrorMessage,
304
314
  });
305
315
 
306
316
  onPoll?.({
@@ -391,17 +401,11 @@ async function pollDecryptStatusV2(
391
401
  }
392
402
 
393
403
  if (!response.ok) {
394
- let errorMessage = `HTTP ${response.status}`;
395
- try {
396
- const errorBody = (await response.json()) as Record<string, unknown>;
397
- const maybeMessage = (errorBody.error_message || errorBody.message) as unknown;
398
- if (typeof maybeMessage === 'string' && maybeMessage.length > 0) errorMessage = maybeMessage;
399
- } catch {
400
- errorMessage = response.statusText || errorMessage;
401
- }
404
+ const { apiErrorCode, errorMessage } = await parseApiErrorResponseBody(response);
402
405
 
403
406
  throw new CofheError({
404
- code: CofheErrorCode.DecryptFailed,
407
+ code: mapApiErrorCodeToCofheErrorCode(apiErrorCode, CofheErrorCode.DecryptFailed),
408
+ apiErrorCode,
405
409
  message: `decrypt status poll failed: ${errorMessage}`,
406
410
  context: {
407
411
  thresholdNetworkUrl,
@@ -455,12 +459,12 @@ async function pollDecryptStatusV2(
455
459
  export async function tnDecryptV2(params: {
456
460
  ctHash: bigint | string;
457
461
  chainId: number;
458
- permission: Permission | null;
462
+ acp: ACPPublic | null;
459
463
  thresholdNetworkUrl: string;
460
464
  retry404TimeoutMs?: number;
461
465
  onPoll?: DecryptPollCallbackFunction;
462
466
  }): Promise<{ decryptedValue: bigint; signature: `0x${string}` }> {
463
- const { thresholdNetworkUrl, ctHash, chainId, permission, retry404TimeoutMs, onPoll } = params;
467
+ const { thresholdNetworkUrl, ctHash, chainId, acp, retry404TimeoutMs, onPoll } = params;
464
468
  const normalized404RetryTimeoutMs = normalize404RetryTimeoutMs({
465
469
  timeoutMs: retry404TimeoutMs,
466
470
  operationLabel: 'decrypt',
@@ -471,7 +475,7 @@ export async function tnDecryptV2(params: {
471
475
  thresholdNetworkUrl,
472
476
  ctHash,
473
477
  chainId,
474
- permission,
478
+ acp,
475
479
  overallStartTime,
476
480
  normalized404RetryTimeoutMs,
477
481
  onPoll
@@ -1,11 +1,12 @@
1
- import { type Permission, type EthEncryptedData } from '@/permits';
1
+ import { type ACPPublic, type EthEncryptedData } from '@/acps';
2
2
 
3
3
  import { CofheError, CofheErrorCode } from '../error.js';
4
+ import {} from './tnDecryptUtils.js';
4
5
 
5
6
  export async function tnSealOutputV1(
6
7
  ctHash: bigint,
7
8
  chainId: number,
8
- permission: Permission,
9
+ acp: ACPPublic,
9
10
  thresholdNetworkUrl: string
10
11
  ): Promise<EthEncryptedData> {
11
12
  let sealed: EthEncryptedData | undefined;
@@ -15,7 +16,7 @@ export async function tnSealOutputV1(
15
16
  const body = {
16
17
  ct_tempkey: ctHash.toString(16).padStart(64, '0'),
17
18
  host_chain_id: chainId,
18
- permit: permission,
19
+ acp: acp,
19
20
  };
20
21
 
21
22
  try {