@cofhe/sdk 0.6.0 → 0.7.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 (114) hide show
  1. package/CHANGELOG.md +49 -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/debug.ts +72 -0
  27. package/core/decrypt/MockThresholdNetworkAbi.ts +20 -11
  28. package/core/decrypt/apiError.ts +104 -0
  29. package/core/decrypt/cofheMocksDecryptForTx.ts +11 -11
  30. package/core/decrypt/cofheMocksDecryptForView.ts +7 -7
  31. package/core/decrypt/decryptForTxBuilder.ts +102 -102
  32. package/core/decrypt/decryptForViewBuilder.ts +90 -90
  33. package/core/decrypt/submitRetry.ts +38 -30
  34. package/core/decrypt/tnDecryptV1.ts +5 -5
  35. package/core/decrypt/tnDecryptV2.ts +45 -34
  36. package/core/decrypt/tnSealOutputV1.ts +4 -3
  37. package/core/decrypt/tnSealOutputV2.ts +44 -31
  38. package/core/encrypt/cofheMocksZkVerifySign.ts +59 -74
  39. package/core/encrypt/encryptInputsBuilder.ts +86 -52
  40. package/core/encrypt/zkPackProveVerify.ts +25 -18
  41. package/core/error.ts +34 -6
  42. package/core/index.ts +14 -14
  43. package/core/test/acpDefaults.test.ts +52 -0
  44. package/core/test/acps.test.ts +596 -0
  45. package/core/test/apiError.test.ts +130 -0
  46. package/core/test/client.test.ts +22 -19
  47. package/core/test/config.test.ts +25 -5
  48. package/core/test/decrypt.test.ts +40 -35
  49. package/core/test/decryptBuilders.test.ts +68 -68
  50. package/core/test/decryptErrorCodes.test.ts +217 -0
  51. package/core/test/encryptInputsBuilder.test.ts +72 -41
  52. package/core/test/pollCallbacks.test.ts +89 -18
  53. package/core/test/stagingRedirect.ts +18 -0
  54. package/core/test/submitRetry.test.ts +182 -0
  55. package/core/types.ts +9 -69
  56. package/dist/acp-Wi6isVQI.d.cts +407 -0
  57. package/dist/acp-Wi6isVQI.d.ts +407 -0
  58. package/dist/acps.cjs +1081 -0
  59. package/dist/acps.d.cts +480 -0
  60. package/dist/acps.d.ts +480 -0
  61. package/dist/acps.js +2 -0
  62. package/dist/chains.cjs +14 -1
  63. package/dist/chains.d.cts +30 -1
  64. package/dist/chains.d.ts +30 -1
  65. package/dist/chains.js +1 -1
  66. package/dist/{chunk-PE5V5CCV.js → chunk-43USWPEH.js} +1000 -604
  67. package/dist/{chunk-MTRAXQXC.js → chunk-N6IDQRRU.js} +14 -2
  68. package/dist/chunk-Q7CBWGQX.js +1029 -0
  69. package/dist/{clientTypes-CyUvRRzA.d.ts → clientTypes-BN3nbzYM.d.ts} +342 -165
  70. package/dist/{clientTypes-BDy1qIBu.d.cts → clientTypes-CYZjFznO.d.cts} +342 -165
  71. package/dist/core.cjs +1431 -1026
  72. package/dist/core.d.cts +55 -10
  73. package/dist/core.d.ts +55 -10
  74. package/dist/core.js +3 -3
  75. package/dist/node.cjs +1356 -976
  76. package/dist/node.d.cts +2 -2
  77. package/dist/node.d.ts +2 -2
  78. package/dist/node.js +3 -3
  79. package/dist/web.cjs +1356 -976
  80. package/dist/web.d.cts +2 -2
  81. package/dist/web.d.ts +2 -2
  82. package/dist/web.js +3 -3
  83. package/node/test/inherited.test.ts +75 -65
  84. package/node/test/tfheinit.test.ts +23 -8
  85. package/package.json +6 -6
  86. package/web/test/client.web.test.ts +5 -1
  87. package/web/test/inherited.web.test.ts +75 -65
  88. package/web/test/tfheinit.web.test.ts +14 -5
  89. package/web/test/worker.config.web.test.ts +38 -23
  90. package/web/test/worker.output.web.test.ts +25 -24
  91. package/core/encrypt/encryptUtils.ts +0 -67
  92. package/core/permits.ts +0 -206
  93. package/core/test/permits.test.ts +0 -534
  94. package/dist/chunk-VB62WYPL.js +0 -978
  95. package/dist/permit-DnVMDT5h.d.cts +0 -376
  96. package/dist/permit-DnVMDT5h.d.ts +0 -376
  97. package/dist/permits.cjs +0 -1026
  98. package/dist/permits.d.cts +0 -353
  99. package/dist/permits.d.ts +0 -353
  100. package/dist/permits.js +0 -2
  101. package/permits/index.ts +0 -68
  102. package/permits/permit.ts +0 -385
  103. package/permits/sealing.ts +0 -131
  104. package/permits/signature.ts +0 -79
  105. package/permits/store.ts +0 -157
  106. package/permits/test/localstorage.test.ts +0 -113
  107. package/permits/test/permit.test.ts +0 -557
  108. package/permits/test/sealing.test.ts +0 -84
  109. package/permits/test/store.test.ts +0 -88
  110. package/permits/test/validation.test.ts +0 -361
  111. package/permits/test-utils.ts +0 -28
  112. package/permits/types.ts +0 -204
  113. package/permits/validation.ts +0 -327
  114. /package/{permits → acps}/utils.ts +0 -0
@@ -111,58 +111,58 @@ describe('DecryptForTxBuilder', () => {
111
111
  });
112
112
  });
113
113
 
114
- // --- withPermit / withoutPermit selection ---
114
+ // --- withACP / withoutACP selection ---
115
115
 
116
- describe('withPermit / withoutPermit selection', () => {
117
- it('withPermit() should set permit selection', () => {
116
+ describe('withACP / withoutACP selection', () => {
117
+ it('withACP() should set acp selection', () => {
118
118
  const builder = createTxBuilder();
119
- const selected = builder.withPermit();
119
+ const selected = builder.withACP();
120
120
  expect(selected).toBeDefined();
121
- expect(selected.getPermit()).toBeUndefined();
122
- expect(selected.getPermitHash()).toBeUndefined();
121
+ expect(selected.getACP()).toBeUndefined();
122
+ expect(selected.getACPHash()).toBeUndefined();
123
123
  });
124
124
 
125
- it('withPermit(hash) should store the permit hash', () => {
125
+ it('withACP(hash) should store the acp hash', () => {
126
126
  const builder = createTxBuilder();
127
- const selected = builder.withPermit('0xmypermithash');
128
- expect(selected.getPermitHash()).toBe('0xmypermithash');
129
- expect(selected.getPermit()).toBeUndefined();
127
+ const selected = builder.withACP('0xmyacphash');
128
+ expect(selected.getACPHash()).toBe('0xmyacphash');
129
+ expect(selected.getACP()).toBeUndefined();
130
130
  });
131
131
 
132
- it('withoutPermit() should set permit selection', () => {
132
+ it('withoutACP() should set acp selection', () => {
133
133
  const builder = createTxBuilder();
134
- const selected = builder.withoutPermit();
134
+ const selected = builder.withoutACP();
135
135
  expect(selected).toBeDefined();
136
- expect(selected.getPermit()).toBeUndefined();
137
- expect(selected.getPermitHash()).toBeUndefined();
136
+ expect(selected.getACP()).toBeUndefined();
137
+ expect(selected.getACPHash()).toBeUndefined();
138
138
  });
139
139
 
140
- it('should throw when withPermit() is called twice', () => {
140
+ it('should throw when withACP() is called twice', () => {
141
141
  const builder = createTxBuilder();
142
- builder.withPermit();
142
+ builder.withACP();
143
143
 
144
- expect(() => (builder as any).withPermit()).toThrow('withPermit() can only be selected once');
144
+ expect(() => (builder as any).withACP()).toThrow('withACP() can only be selected once');
145
145
  });
146
146
 
147
- it('should throw when withoutPermit() is called twice', () => {
147
+ it('should throw when withoutACP() is called twice', () => {
148
148
  const builder = createTxBuilder();
149
- builder.withoutPermit();
149
+ builder.withoutACP();
150
150
 
151
- expect(() => (builder as any).withoutPermit()).toThrow('withoutPermit() can only be selected once');
151
+ expect(() => (builder as any).withoutACP()).toThrow('withoutACP() can only be selected once');
152
152
  });
153
153
 
154
- it('should throw when withPermit() is called after withoutPermit()', () => {
154
+ it('should throw when withACP() is called after withoutACP()', () => {
155
155
  const builder = createTxBuilder();
156
- builder.withoutPermit();
156
+ builder.withoutACP();
157
157
 
158
- expect(() => (builder as any).withPermit()).toThrow('cannot call withPermit() after withoutPermit()');
158
+ expect(() => (builder as any).withACP()).toThrow('cannot call withACP() after withoutACP()');
159
159
  });
160
160
 
161
- it('should throw when withoutPermit() is called after withPermit()', () => {
161
+ it('should throw when withoutACP() is called after withACP()', () => {
162
162
  const builder = createTxBuilder();
163
- builder.withPermit();
163
+ builder.withACP();
164
164
 
165
- expect(() => (builder as any).withoutPermit()).toThrow('cannot call withoutPermit() after withPermit()');
165
+ expect(() => (builder as any).withoutACP()).toThrow('cannot call withoutACP() after withACP()');
166
166
  });
167
167
  });
168
168
 
@@ -171,16 +171,16 @@ describe('DecryptForTxBuilder', () => {
171
171
  describe('chaining', () => {
172
172
  it('should return the builder from each setter for fluent chaining', () => {
173
173
  const builder = createTxBuilder({ chainId: undefined, account: undefined });
174
- const result = builder.setChainId(TEST_CHAIN_ID).setAccount(account.address).withPermit();
174
+ const result = builder.setChainId(TEST_CHAIN_ID).setAccount(account.address).withACP();
175
175
 
176
176
  expect(result).toBeDefined();
177
177
  expect(result.getChainId()).toBe(TEST_CHAIN_ID);
178
178
  expect(result.getAccount()).toBe(account.address);
179
179
  });
180
180
 
181
- it('should allow setChainId and setAccount after withPermit', () => {
181
+ it('should allow setChainId and setAccount after withACP', () => {
182
182
  const builder = createTxBuilder({ chainId: undefined, account: undefined });
183
- const selected = builder.withPermit();
183
+ const selected = builder.withACP();
184
184
  selected.setChainId(99);
185
185
  selected.setAccount('0xabc');
186
186
 
@@ -188,9 +188,9 @@ describe('DecryptForTxBuilder', () => {
188
188
  expect(selected.getAccount()).toBe('0xabc');
189
189
  });
190
190
 
191
- it('should allow setChainId and setAccount after withoutPermit', () => {
191
+ it('should allow setChainId and setAccount after withoutACP', () => {
192
192
  const builder = createTxBuilder({ chainId: undefined, account: undefined });
193
- const selected = builder.withoutPermit();
193
+ const selected = builder.withoutACP();
194
194
  selected.setChainId(99);
195
195
  selected.setAccount('0xabc');
196
196
 
@@ -216,7 +216,7 @@ describe('DecryptForTxBuilder', () => {
216
216
  // --- execute error paths ---
217
217
 
218
218
  describe('execute – error paths', () => {
219
- it('should throw when execute() is called without permit selection', async () => {
219
+ it('should throw when execute() is called without acp selection', async () => {
220
220
  const builder = createTxBuilder();
221
221
 
222
222
  try {
@@ -224,31 +224,31 @@ describe('DecryptForTxBuilder', () => {
224
224
  expect.fail('Expected error');
225
225
  } catch (error) {
226
226
  expect((error as any).code).toBe(CofheErrorCode.InternalError);
227
- expect((error as Error).message).toContain('missing permit selection');
227
+ expect((error as Error).message).toContain('missing acp selection');
228
228
  }
229
229
  });
230
230
 
231
- it('should throw when withPermit() has no active permit', async () => {
231
+ it('should throw when withACP() has no active acp', async () => {
232
232
  const builder = createTxBuilder();
233
233
 
234
234
  try {
235
- await builder.withPermit().execute();
236
- expect.fail('Expected PermitNotFound error');
235
+ await builder.withACP().execute();
236
+ expect.fail('Expected ACPNotFound error');
237
237
  } catch (error) {
238
- expect((error as any).code).toBe(CofheErrorCode.PermitNotFound);
239
- expect((error as Error).message).toContain('Active permit not found');
238
+ expect((error as any).code).toBe(CofheErrorCode.ACPNotFound);
239
+ expect((error as Error).message).toContain('Active acp not found');
240
240
  }
241
241
  });
242
242
 
243
- it('should throw when withPermit(hash) cannot find permit', async () => {
243
+ it('should throw when withACP(hash) cannot find acp', async () => {
244
244
  const builder = createTxBuilder();
245
245
 
246
246
  try {
247
- await builder.withPermit('0xnonexistent').execute();
248
- expect.fail('Expected PermitNotFound error');
247
+ await builder.withACP('0xnonexistent').execute();
248
+ expect.fail('Expected ACPNotFound error');
249
249
  } catch (error) {
250
- expect((error as any).code).toBe(CofheErrorCode.PermitNotFound);
251
- expect((error as Error).message).toContain('Permit with hash');
250
+ expect((error as any).code).toBe(CofheErrorCode.ACPNotFound);
251
+ expect((error as Error).message).toContain('ACP with hash');
252
252
  }
253
253
  });
254
254
  });
@@ -302,30 +302,30 @@ describe('DecryptForViewBuilder', () => {
302
302
  });
303
303
  });
304
304
 
305
- // --- withPermit ---
305
+ // --- withACP ---
306
306
 
307
- describe('withPermit', () => {
308
- it('withPermit() should clear permit and hash', () => {
307
+ describe('withACP', () => {
308
+ it('withACP() should clear acp and hash', () => {
309
309
  const builder = createViewBuilder(FheTypes.Uint32);
310
- builder.withPermit();
311
- expect(builder.getPermit()).toBeUndefined();
312
- expect(builder.getPermitHash()).toBeUndefined();
310
+ builder.withACP();
311
+ expect(builder.getACP()).toBeUndefined();
312
+ expect(builder.getACPHash()).toBeUndefined();
313
313
  });
314
314
 
315
- it('withPermit(hash) should store the permit hash', () => {
315
+ it('withACP(hash) should store the acp hash', () => {
316
316
  const builder = createViewBuilder(FheTypes.Uint32);
317
- builder.withPermit('0xmypermithash');
318
- expect(builder.getPermitHash()).toBe('0xmypermithash');
319
- expect(builder.getPermit()).toBeUndefined();
317
+ builder.withACP('0xmyacphash');
318
+ expect(builder.getACPHash()).toBe('0xmyacphash');
319
+ expect(builder.getACP()).toBeUndefined();
320
320
  });
321
321
 
322
- it('should allow overriding permit selection', () => {
322
+ it('should allow overriding acp selection', () => {
323
323
  const builder = createViewBuilder(FheTypes.Uint32);
324
- builder.withPermit('0xfirst');
325
- expect(builder.getPermitHash()).toBe('0xfirst');
324
+ builder.withACP('0xfirst');
325
+ expect(builder.getACPHash()).toBe('0xfirst');
326
326
 
327
- builder.withPermit('0xsecond');
328
- expect(builder.getPermitHash()).toBe('0xsecond');
327
+ builder.withACP('0xsecond');
328
+ expect(builder.getACPHash()).toBe('0xsecond');
329
329
  });
330
330
  });
331
331
 
@@ -334,7 +334,7 @@ describe('DecryptForViewBuilder', () => {
334
334
  describe('chaining', () => {
335
335
  it('should return the builder from each setter for fluent chaining', () => {
336
336
  const builder = createViewBuilder(FheTypes.Uint32, { chainId: undefined, account: undefined });
337
- const result = builder.setChainId(TEST_CHAIN_ID).setAccount(account.address).withPermit();
337
+ const result = builder.setChainId(TEST_CHAIN_ID).setAccount(account.address).withACP();
338
338
 
339
339
  expect(result).toBeDefined();
340
340
  expect(result.getChainId()).toBe(TEST_CHAIN_ID);
@@ -359,28 +359,28 @@ describe('DecryptForViewBuilder', () => {
359
359
  // --- execute error paths ---
360
360
 
361
361
  describe('execute – error paths', () => {
362
- it('should throw when active permit is not found', async () => {
362
+ it('should throw when active acp is not found', async () => {
363
363
  const builder = createViewBuilder(FheTypes.Uint32);
364
364
 
365
365
  try {
366
366
  await builder.execute();
367
- expect.fail('Expected PermitNotFound error');
367
+ expect.fail('Expected ACPNotFound error');
368
368
  } catch (error) {
369
- expect((error as any).code).toBe(CofheErrorCode.PermitNotFound);
370
- expect((error as Error).message).toContain('Active permit not found');
369
+ expect((error as any).code).toBe(CofheErrorCode.ACPNotFound);
370
+ expect((error as Error).message).toContain('Active acp not found');
371
371
  }
372
372
  });
373
373
 
374
- it('should throw when withPermit(hash) cannot find permit', async () => {
374
+ it('should throw when withACP(hash) cannot find acp', async () => {
375
375
  const builder = createViewBuilder(FheTypes.Uint32);
376
- builder.withPermit('0xnonexistent');
376
+ builder.withACP('0xnonexistent');
377
377
 
378
378
  try {
379
379
  await builder.execute();
380
- expect.fail('Expected PermitNotFound error');
380
+ expect.fail('Expected ACPNotFound error');
381
381
  } catch (error) {
382
- expect((error as any).code).toBe(CofheErrorCode.PermitNotFound);
383
- expect((error as Error).message).toContain('Permit with hash');
382
+ expect((error as any).code).toBe(CofheErrorCode.ACPNotFound);
383
+ expect((error as Error).message).toContain('ACP with hash');
384
384
  }
385
385
  });
386
386
 
@@ -0,0 +1,217 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { tnDecryptV2 } from '../decrypt/tnDecryptV2.js';
3
+ import { tnSealOutputV2 } from '../decrypt/tnSealOutputV2.js';
4
+ import { CofheErrorCode } from '../error.js';
5
+
6
+ // Cross-reference: API-RESPONSES.md's "Shared errors" table (applies to all decrypt/sealoutput
7
+ // submit and poll endpoints).
8
+ const SHARED_ERROR_CASES: Array<{ status: number; error: string; cofheErrorCode: CofheErrorCode }> = [
9
+ { status: 400, error: 'bad_request', cofheErrorCode: CofheErrorCode.BadRequest },
10
+ { status: 400, error: 'unknown_chain', cofheErrorCode: CofheErrorCode.UnknownChain },
11
+ { status: 400, error: 'acp_malformed', cofheErrorCode: CofheErrorCode.ACPMalformed },
12
+ { status: 401, error: 'acp_denied', cofheErrorCode: CofheErrorCode.ACPDenied },
13
+ { status: 401, error: 'acp_expired', cofheErrorCode: CofheErrorCode.ACPExpired },
14
+ { status: 401, error: 'acp_invalid', cofheErrorCode: CofheErrorCode.ACPInvalid },
15
+ { status: 403, error: 'not_publicly_allowed', cofheErrorCode: CofheErrorCode.NotPubliclyAllowed },
16
+ { status: 422, error: 'unsupported_security_zone', cofheErrorCode: CofheErrorCode.UnsupportedSecurityZone },
17
+ { status: 422, error: 'unsupported_type', cofheErrorCode: CofheErrorCode.UnsupportedType },
18
+ { status: 500, error: 'internal_error', cofheErrorCode: CofheErrorCode.InternalError },
19
+ { status: 500, error: 'signing_failed', cofheErrorCode: CofheErrorCode.SigningFailed },
20
+ { status: 502, error: 'ct_source_error', cofheErrorCode: CofheErrorCode.CtSourceError },
21
+ { status: 502, error: 'acp_verifier_error', cofheErrorCode: CofheErrorCode.ACPVerifierError },
22
+ { status: 504, error: 'ct_source_timeout', cofheErrorCode: CofheErrorCode.CtSourceTimeout },
23
+ { status: 504, error: 'acp_verifier_timeout', cofheErrorCode: CofheErrorCode.ACPVerifierTimeout },
24
+ ];
25
+ // ct_not_found (404) is deliberately excluded here: on the submit endpoints it's the
26
+ // conditionally-retryable case (covered by pollCallbacks.test.ts / submitRetry.test.ts), and on
27
+ // the poll endpoints it's documented bodyless (covered separately below).
28
+
29
+ const makeMockResponse = (opts: { ok: boolean; status?: number; statusText?: string; json: () => Promise<any> }) => {
30
+ return {
31
+ ok: opts.ok,
32
+ status: opts.status ?? (opts.ok ? 200 : 500),
33
+ statusText: opts.statusText ?? '',
34
+ json: opts.json,
35
+ } as unknown as Response;
36
+ };
37
+
38
+ describe('decrypt/sealoutput API-RESPONSES.md error-code coverage', () => {
39
+ const thresholdNetworkUrl = 'http://threshold.local';
40
+
41
+ beforeEach(() => {
42
+ vi.useFakeTimers();
43
+ });
44
+
45
+ afterEach(() => {
46
+ vi.useRealTimers();
47
+ vi.restoreAllMocks();
48
+ });
49
+
50
+ describe.each([
51
+ { operation: 'decrypt' as const, path: '/v2/decrypt', run: tnDecryptV2, fallback: CofheErrorCode.DecryptFailed },
52
+ {
53
+ operation: 'sealoutput' as const,
54
+ path: '/v2/sealoutput',
55
+ run: tnSealOutputV2,
56
+ fallback: CofheErrorCode.SealOutputFailed,
57
+ },
58
+ ])('$operation submit ($path)', ({ path, run, fallback }) => {
59
+ it.each(SHARED_ERROR_CASES)('maps $status $error to the dedicated CofheErrorCode', async ({ status, error }) => {
60
+ const fetchMock = vi.fn(async (url: string) => {
61
+ if (url === `${thresholdNetworkUrl}${path}`) {
62
+ return makeMockResponse({
63
+ ok: false,
64
+ status,
65
+ json: async () => ({ error, error_message: `backend said ${error}` }),
66
+ });
67
+ }
68
+ throw new Error(`Unexpected fetch: ${url}`);
69
+ });
70
+ global.fetch = fetchMock as any;
71
+
72
+ const expectedCode = SHARED_ERROR_CASES.find((c) => c.error === error)!.cofheErrorCode;
73
+
74
+ await expect(run({ ctHash: 1n, chainId: 1, acp: {} as any, thresholdNetworkUrl } as any)).rejects.toMatchObject({
75
+ code: expectedCode,
76
+ apiErrorCode: error,
77
+ message: expect.stringContaining(`backend said ${error}`),
78
+ });
79
+ expect(fetchMock).toHaveBeenCalledTimes(1);
80
+ });
81
+
82
+ it('falls back to the operation-level generic code for an unrecognized future error code', async () => {
83
+ const fetchMock = vi.fn(async () =>
84
+ makeMockResponse({
85
+ ok: false,
86
+ status: 400,
87
+ json: async () => ({ error: 'some_future_code', error_message: 'not yet known to the SDK' }),
88
+ })
89
+ );
90
+ global.fetch = fetchMock as any;
91
+
92
+ await expect(run({ ctHash: 1n, chainId: 1, acp: {} as any, thresholdNetworkUrl } as any)).rejects.toMatchObject({
93
+ code: fallback,
94
+ apiErrorCode: 'some_future_code',
95
+ });
96
+ });
97
+
98
+ it('degrades gracefully on a plain-text framework rejection (e.g. 415)', async () => {
99
+ const fetchMock = vi.fn(async () =>
100
+ makeMockResponse({
101
+ ok: false,
102
+ status: 415,
103
+ statusText: 'Unsupported Media Type',
104
+ json: async () => {
105
+ throw new Error('body is plain text, not JSON');
106
+ },
107
+ })
108
+ );
109
+ global.fetch = fetchMock as any;
110
+
111
+ await expect(run({ ctHash: 1n, chainId: 1, acp: {} as any, thresholdNetworkUrl } as any)).rejects.toMatchObject({
112
+ code: fallback,
113
+ apiErrorCode: undefined,
114
+ message: expect.stringContaining('Unsupported Media Type'),
115
+ });
116
+ });
117
+ });
118
+
119
+ it('sealoutput submit maps 400 acp_required (sealoutput-specific)', async () => {
120
+ global.fetch = vi.fn(async () =>
121
+ makeMockResponse({
122
+ ok: false,
123
+ status: 400,
124
+ json: async () => ({ error: 'acp_required', error_message: 'no acp in request' }),
125
+ })
126
+ ) as any;
127
+
128
+ await expect(tnSealOutputV2({ ctHash: 1n, chainId: 1, acp: {} as any, thresholdNetworkUrl })).rejects.toMatchObject(
129
+ {
130
+ code: CofheErrorCode.ACPRequired,
131
+ apiErrorCode: 'acp_required',
132
+ }
133
+ );
134
+ });
135
+
136
+ it('sealoutput submit maps 500 seal_failed (sealoutput-specific)', async () => {
137
+ global.fetch = vi.fn(async () =>
138
+ makeMockResponse({
139
+ ok: false,
140
+ status: 500,
141
+ json: async () => ({ error: 'seal_failed', error_message: 'sealing crypto failed' }),
142
+ })
143
+ ) as any;
144
+
145
+ await expect(tnSealOutputV2({ ctHash: 1n, chainId: 1, acp: {} as any, thresholdNetworkUrl })).rejects.toMatchObject(
146
+ {
147
+ code: CofheErrorCode.SealFailed,
148
+ apiErrorCode: 'seal_failed',
149
+ }
150
+ );
151
+ });
152
+
153
+ describe.each([
154
+ {
155
+ operation: 'decrypt' as const,
156
+ submitPath: '/v2/decrypt',
157
+ statusPath: (id: string) => `/v2/decrypt/${id}`,
158
+ run: tnDecryptV2,
159
+ fallback: CofheErrorCode.DecryptFailed,
160
+ },
161
+ {
162
+ operation: 'sealoutput' as const,
163
+ submitPath: '/v2/sealoutput',
164
+ statusPath: (id: string) => `/v2/sealoutput/${id}`,
165
+ run: tnSealOutputV2,
166
+ fallback: CofheErrorCode.SealOutputFailed,
167
+ },
168
+ ])('$operation poll (GET status endpoint)', ({ submitPath, statusPath, run, fallback }) => {
169
+ const requestId = 'req-poll-errors';
170
+
171
+ it('maps a non-404 non-ok status via the shared error table (e.g. 500 internal_error)', async () => {
172
+ const fetchMock = vi.fn(async (url: string) => {
173
+ if (url === `${thresholdNetworkUrl}${submitPath}`) {
174
+ return makeMockResponse({ ok: true, json: async () => ({ request_id: requestId }) });
175
+ }
176
+ if (url === `${thresholdNetworkUrl}${statusPath(requestId)}`) {
177
+ return makeMockResponse({
178
+ ok: false,
179
+ status: 500,
180
+ json: async () => ({ error: 'internal_error', error_message: 'decrypt task panicked' }),
181
+ });
182
+ }
183
+ throw new Error(`Unexpected fetch: ${url}`);
184
+ });
185
+ global.fetch = fetchMock as any;
186
+
187
+ const promise = run({ ctHash: 1n, chainId: 1, acp: {} as any, thresholdNetworkUrl } as any);
188
+
189
+ await expect(promise).rejects.toMatchObject({
190
+ code: CofheErrorCode.InternalError,
191
+ apiErrorCode: 'internal_error',
192
+ message: expect.stringContaining('decrypt task panicked'),
193
+ });
194
+ });
195
+
196
+ it('throws immediately on a bodyless 404 (unknown/evicted request_id) without calling response.json()', async () => {
197
+ const statusJson = vi.fn(async () => {
198
+ throw new Error('json() should not be called for a bodyless 404');
199
+ });
200
+ const fetchMock = vi.fn(async (url: string) => {
201
+ if (url === `${thresholdNetworkUrl}${submitPath}`) {
202
+ return makeMockResponse({ ok: true, json: async () => ({ request_id: requestId }) });
203
+ }
204
+ if (url === `${thresholdNetworkUrl}${statusPath(requestId)}`) {
205
+ return makeMockResponse({ ok: false, status: 404, json: statusJson });
206
+ }
207
+ throw new Error(`Unexpected fetch: ${url}`);
208
+ });
209
+ global.fetch = fetchMock as any;
210
+
211
+ const promise = run({ ctHash: 1n, chainId: 1, acp: {} as any, thresholdNetworkUrl } as any);
212
+
213
+ await expect(promise).rejects.toMatchObject({ code: fallback });
214
+ expect(statusJson).not.toHaveBeenCalled();
215
+ });
216
+ });
217
+ });