@cofhe/sdk 1.1.0-alpha.3 → 1.1.0-alpha.4

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,72 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { createDecryptLimiter } from '../decrypt/decryptLimiter.js';
3
+ import { CofheErrorCode } from '../error.js';
4
+
5
+ function deferred<T>() {
6
+ let resolve!: (value: T) => void;
7
+ const promise = new Promise<T>((r) => (resolve = r));
8
+ return { promise, resolve };
9
+ }
10
+
11
+ describe('createDecryptLimiter', () => {
12
+ it('runs at most `max` tasks at a time, in call order', async () => {
13
+ const limit = createDecryptLimiter(2);
14
+ const gates = [deferred<number>(), deferred<number>(), deferred<number>()];
15
+ const started = vi.fn();
16
+ const run = (i: number) =>
17
+ limit(() => {
18
+ started(i);
19
+ return gates[i].promise;
20
+ }, 'decrypt');
21
+
22
+ const all = Promise.all([run(0), run(1), run(2)]);
23
+ await Promise.resolve();
24
+ expect(started.mock.calls.map(([i]) => i)).toEqual([0, 1]);
25
+
26
+ gates[0].resolve(10);
27
+ await Promise.resolve();
28
+ await Promise.resolve();
29
+ expect(started.mock.calls.map(([i]) => i)).toEqual([0, 1, 2]);
30
+
31
+ gates[1].resolve(11);
32
+ gates[2].resolve(12);
33
+ await expect(all).resolves.toEqual([10, 11, 12]);
34
+ });
35
+
36
+ it('a task aborted while waiting never starts and frees its place in the queue', async () => {
37
+ const limit = createDecryptLimiter(1);
38
+ const gate = deferred<number>();
39
+ const controller = new AbortController();
40
+ const started = vi.fn();
41
+
42
+ const first = limit(() => gate.promise, 'decrypt');
43
+ const second = limit(async () => started('second'), 'decrypt', controller.signal);
44
+ const third = limit(async () => started('third'), 'decrypt');
45
+
46
+ controller.abort();
47
+ await expect(second).rejects.toMatchObject({ code: CofheErrorCode.Aborted });
48
+ expect(started).not.toHaveBeenCalled();
49
+
50
+ gate.resolve(1);
51
+ await expect(first).resolves.toBe(1);
52
+ await third;
53
+ expect(started).toHaveBeenCalledWith('third');
54
+ expect(started).not.toHaveBeenCalledWith('second');
55
+ });
56
+
57
+ it('a signal that already fired rejects before the task is queued', async () => {
58
+ const limit = createDecryptLimiter(1);
59
+ const controller = new AbortController();
60
+ controller.abort();
61
+ const task = vi.fn(async () => 1);
62
+
63
+ await expect(limit(task, 'sealOutput', controller.signal)).rejects.toMatchObject({ code: CofheErrorCode.Aborted });
64
+ expect(task).not.toHaveBeenCalled();
65
+ });
66
+
67
+ it('a failed task releases its slot', async () => {
68
+ const limit = createDecryptLimiter(1);
69
+ await expect(limit(async () => Promise.reject(new Error('boom')), 'decrypt')).rejects.toThrow('boom');
70
+ await expect(limit(async () => 2, 'decrypt')).resolves.toBe(2);
71
+ });
72
+ });
@@ -1,5 +1,10 @@
1
1
  import { describe, it, expect, vi } from 'vitest';
2
- import { classifySubmitResponse, throwIfSubmitRetryTimedOut } from '../decrypt/submitRetry.js';
2
+ import {
3
+ classifySubmitResponse,
4
+ sleepUnlessAborted,
5
+ throwIfAborted,
6
+ throwIfSubmitRetryTimedOut,
7
+ } from '../decrypt/submitRetry.js';
3
8
  import { CofheError, CofheErrorCode } from '../error.js';
4
9
 
5
10
  const makeResponse = (opts: {
@@ -180,3 +185,58 @@ describe('throwIfSubmitRetryTimedOut', () => {
180
185
  }
181
186
  });
182
187
  });
188
+
189
+ describe('abort helpers', () => {
190
+ it('throwIfAborted is a no-op without a signal or before it fires, and throws Aborted after', () => {
191
+ const controller = new AbortController();
192
+ expect(() => throwIfAborted(undefined, 'decrypt')).not.toThrow();
193
+ expect(() => throwIfAborted(controller.signal, 'decrypt')).not.toThrow();
194
+
195
+ controller.abort(new Error('navigated away'));
196
+
197
+ let thrown: unknown;
198
+ try {
199
+ throwIfAborted(controller.signal, 'decrypt');
200
+ } catch (e) {
201
+ thrown = e;
202
+ }
203
+ expect(thrown).toBeInstanceOf(CofheError);
204
+ expect((thrown as CofheError).code).toBe(CofheErrorCode.Aborted);
205
+ expect((thrown as CofheError).message).toMatch(/^decrypt aborted by the caller/);
206
+ expect((thrown as CofheError).cause?.message).toBe('navigated away');
207
+ });
208
+
209
+ it('sleepUnlessAborted resolves after the wait, and rejects the moment the signal fires', async () => {
210
+ vi.useFakeTimers();
211
+ try {
212
+ const resolved = vi.fn();
213
+ sleepUnlessAborted(1_000, undefined, 'decrypt').then(resolved);
214
+ await vi.advanceTimersByTimeAsync(1_000);
215
+ expect(resolved).toHaveBeenCalledOnce();
216
+
217
+ const controller = new AbortController();
218
+ const waiting = sleepUnlessAborted(60_000, controller.signal, 'sealOutput');
219
+ const rejected = vi.fn();
220
+ waiting.catch(rejected);
221
+ await vi.advanceTimersByTimeAsync(10);
222
+ expect(rejected).not.toHaveBeenCalled();
223
+
224
+ controller.abort();
225
+ await vi.advanceTimersByTimeAsync(0);
226
+ expect(rejected).toHaveBeenCalledOnce();
227
+ expect(rejected.mock.calls[0][0]).toMatchObject({ code: CofheErrorCode.Aborted });
228
+ expect(rejected.mock.calls[0][0].message).toMatch(/^sealOutput aborted by the caller/);
229
+ expect(vi.getTimerCount()).toBe(0);
230
+ } finally {
231
+ vi.useRealTimers();
232
+ }
233
+ });
234
+
235
+ it('sleepUnlessAborted rejects at once for a signal that already fired', async () => {
236
+ const controller = new AbortController();
237
+ controller.abort();
238
+ await expect(sleepUnlessAborted(60_000, controller.signal, 'decrypt')).rejects.toMatchObject({
239
+ code: CofheErrorCode.Aborted,
240
+ });
241
+ });
242
+ });
package/dist/acps.js CHANGED
@@ -1,2 +1,2 @@
1
- export { ACPScope, ACPUtils, ACP_REVOKER_ABI, ACP_SHARE_REGISTRY_ABI, ACP_SHARE_TUPLE_ABI, ACP_STORE_DEFAULTS, ERC7984_ABI, GenerateSealingKey, ImportACPOptionsValidator, ImportACPValidator, LAYOUT_VERSION as SHARE_METADATA_VERSION, SelfACPOptionsValidator, SelfACPValidator, SharingACPOptionsValidator, SharingACPValidator, SignatureTypes, SignatureUtils, UNLABELLED, ValidationUtils, _acpStore, acpStore, addressNotZeroSchema, addressSchema, bytesNotEmptySchema, bytesSchema, clearAclServedAddresses, clearStaleStore, computeShareId, contractsSchema, decodeShareAppKey, decodeShareMetadata, decodeShareMetadataFull, describeShareLabels, describeShareMetadata, encodeShareMetadata, eventLogLabel, getACP, getACPs, getAclAddress, getAclServedAddresses, getActiveACP, getActiveACPHash, getSignatureTypesAndMessage, handlesSchema, readPostedShares, readShare, readSharesFor, removeACP, removeActiveACPHash, resetStore, seal, setACP, setActiveACPHash, shareAppKey, shareIdOfChainShare, toChainShare, unsealWithPrivateKey, validateImportACP, validateImportACPOptions, validateSelfACP, validateSelfACPOptions, validateSharingACP, validateSharingACPOptions, verifyShareLabel, verifyShareLabels, viewFunctionLabel } from './chunk-WAA2U7CT.js';
2
- import './chunk-OC5TGQGG.js';
1
+ export { ACPScope, ACPUtils, ACP_REVOKER_ABI, ACP_SHARE_REGISTRY_ABI, ACP_SHARE_TUPLE_ABI, ACP_STORE_DEFAULTS, ERC7984_ABI, GenerateSealingKey, ImportACPOptionsValidator, ImportACPValidator, LAYOUT_VERSION as SHARE_METADATA_VERSION, SelfACPOptionsValidator, SelfACPValidator, SharingACPOptionsValidator, SharingACPValidator, SignatureTypes, SignatureUtils, UNLABELLED, ValidationUtils, _acpStore, acpStore, addressNotZeroSchema, addressSchema, bytesNotEmptySchema, bytesSchema, clearAclServedAddresses, clearStaleStore, computeShareId, contractsSchema, decodeShareAppKey, decodeShareMetadata, decodeShareMetadataFull, describeShareLabels, describeShareMetadata, encodeShareMetadata, eventLogLabel, getACP, getACPs, getAclAddress, getAclServedAddresses, getActiveACP, getActiveACPHash, getSignatureTypesAndMessage, handlesSchema, readPostedShares, readShare, readSharesFor, removeACP, removeActiveACPHash, resetStore, seal, setACP, setActiveACPHash, shareAppKey, shareIdOfChainShare, toChainShare, unsealWithPrivateKey, validateImportACP, validateImportACPOptions, validateSelfACP, validateSelfACPOptions, validateSharingACP, validateSharingACPOptions, verifyShareLabel, verifyShareLabels, viewFunctionLabel } from './chunk-37SG44CR.js';
2
+ import './chunk-NJTYZ3CW.js';
@@ -1,4 +1,4 @@
1
- import { CofheError, TASK_MANAGER_ID, COFHE_ADDRESS_BOOK_ADDRESS } from './chunk-OC5TGQGG.js';
1
+ import { CofheError, TASK_MANAGER_ID, COFHE_ADDRESS_BOOK_ADDRESS } from './chunk-NJTYZ3CW.js';
2
2
  import { isAddress, getAddress, zeroAddress, isHex, parseAbi, keccak256, padHex, numberToHex, concatHex, toFunctionSelector, size, sliceHex, toHex, encodeAbiParameters, hexToBigInt, stringToHex, hexToNumber, toEventSignature, toFunctionSignature, decodeErrorResult, toEventSelector, decodeAbiParameters } from 'viem';
3
3
  import nacl from 'tweetnacl';
4
4
  import { z } from 'zod';
@@ -64,6 +64,7 @@ var CofheErrorCode = /* @__PURE__ */ ((CofheErrorCode2) => {
64
64
  CofheErrorCode2["ACPVerifierTimeout"] = "ACP_VERIFIER_TIMEOUT";
65
65
  CofheErrorCode2["ACPRequired"] = "ACP_REQUIRED";
66
66
  CofheErrorCode2["SealFailed"] = "SEAL_FAILED";
67
+ CofheErrorCode2["Aborted"] = "ABORTED";
67
68
  return CofheErrorCode2;
68
69
  })(CofheErrorCode || {});
69
70
  var CofheError = class _CofheError extends Error {
@@ -1,4 +1,4 @@
1
- import { CofheError } from './chunk-OC5TGQGG.js';
1
+ import { CofheError } from './chunk-NJTYZ3CW.js';
2
2
  import 'viem';
3
3
 
4
4
  // core/types.ts
@@ -1,7 +1,7 @@
1
1
  import { hardhat as hardhat$1 } from './chunk-UKQAJK4N.js';
2
- import { acpStore, clearAclServedAddresses, getAclServedAddresses, computeShareId, ACP_SHARE_REGISTRY_ABI, ACPUtils, getTaskManagerAddress, verifyShareLabels, decodeShareMetadata, shareAppKey, readShare, readSharesFor, toChainShare, ACP_REVOKER_ABI, decodeShareAppKey, encodeShareMetadata } from './chunk-WAA2U7CT.js';
3
- import { sleep, getPublicClientChainID, getWalletClientAccount, validateBigIntInRange, toBigIntOrThrow, hexToBytes, toHexString, FheUintUTypes } from './chunk-3SD2UCRE.js';
4
- import { TFHE_RS_KEY_VERSION, CofheError, TFHE_RS_ZK_MAX_BITS, TFHE_RS_SAFE_SERIALIZATION_SIZE_LIMIT, MOCKS_THRESHOLD_NETWORK_ADDRESS, MOCKS_DECRYPT_RESULT_SIGNER_PRIVATE_KEY, MOCKS_ZK_VERIFIER_SIGNER_PRIVATE_KEY, MOCKS_ZK_VERIFIER_ADDRESS } from './chunk-OC5TGQGG.js';
2
+ import { acpStore, clearAclServedAddresses, getAclServedAddresses, computeShareId, ACP_SHARE_REGISTRY_ABI, ACPUtils, getTaskManagerAddress, verifyShareLabels, decodeShareMetadata, shareAppKey, readShare, readSharesFor, toChainShare, ACP_REVOKER_ABI, decodeShareAppKey, encodeShareMetadata } from './chunk-37SG44CR.js';
3
+ import { sleep, getPublicClientChainID, getWalletClientAccount, validateBigIntInRange, toBigIntOrThrow, hexToBytes, toHexString, FheUintUTypes } from './chunk-PLHQYVND.js';
4
+ import { TFHE_RS_KEY_VERSION, CofheError, TFHE_RS_ZK_MAX_BITS, TFHE_RS_SAFE_SERIALIZATION_SIZE_LIMIT, MOCKS_THRESHOLD_NETWORK_ADDRESS, MOCKS_DECRYPT_RESULT_SIGNER_PRIVATE_KEY, MOCKS_ZK_VERIFIER_SIGNER_PRIVATE_KEY, MOCKS_ZK_VERIFIER_ADDRESS } from './chunk-NJTYZ3CW.js';
5
5
  import { createStore } from 'zustand/vanilla';
6
6
  import { parseAbi, isAddress, isAddressEqual, zeroAddress, recoverAddress, encodePacked, keccak256, createWalletClient, http, getAddress, parseSignature, serializeSignature } from 'viem';
7
7
  import { hardhat } from 'viem/chains';
@@ -481,6 +481,8 @@ var CofheConfigSchema = z.object({
481
481
  supportedChains: z.array(z.custom()),
482
482
  /** Default acp expiration in seconds, default is 30 days */
483
483
  defaultACPExpiration: z.number().optional().default(60 * 60 * 24 * 30),
484
+ /** Decrypts sent to the threshold network at a time per client; `Infinity` disables the cap (default 8) */
485
+ decryptConcurrency: z.union([z.number().min(1), z.literal(Infinity)]).optional().default(8),
484
486
  /** Storage method for fhe keys (defaults to indexedDB on web, filesystem on node) */
485
487
  fheKeyStorage: z.object({
486
488
  getItem: z.custom((val) => typeof val === "function", {
@@ -2033,6 +2035,32 @@ function throwIfSubmitRetryTimedOut(params) {
2033
2035
  });
2034
2036
  }
2035
2037
  }
2038
+ function decryptAbortedError(operationLabel, signal) {
2039
+ return new CofheError({
2040
+ code: "ABORTED" /* Aborted */,
2041
+ message: `${operationLabel} aborted by the caller`,
2042
+ cause: signal.reason instanceof Error ? signal.reason : void 0
2043
+ });
2044
+ }
2045
+ function throwIfAborted(signal, operationLabel) {
2046
+ if (signal?.aborted)
2047
+ throw decryptAbortedError(operationLabel, signal);
2048
+ }
2049
+ function sleepUnlessAborted(ms, signal, operationLabel) {
2050
+ return new Promise((resolve, reject) => {
2051
+ if (signal?.aborted)
2052
+ return reject(decryptAbortedError(operationLabel, signal));
2053
+ const onAbort = () => {
2054
+ clearTimeout(timer);
2055
+ reject(decryptAbortedError(operationLabel, signal));
2056
+ };
2057
+ const timer = setTimeout(() => {
2058
+ signal?.removeEventListener("abort", onAbort);
2059
+ resolve();
2060
+ }, ms);
2061
+ signal?.addEventListener("abort", onAbort, { once: true });
2062
+ });
2063
+ }
2036
2064
  function normalizeTnSignature(signature) {
2037
2065
  if (typeof signature !== "string") {
2038
2066
  throw new CofheError({
@@ -2150,7 +2178,7 @@ function parseCompletedSealOutputResponse(params) {
2150
2178
  }
2151
2179
  return convertSealedData(sealed);
2152
2180
  }
2153
- async function submitSealOutputRequest(thresholdNetworkUrl, ctHash, chainId, acp, overallStartTime, retry404TimeoutMs, timeoutMs, onPoll) {
2181
+ async function submitSealOutputRequest(thresholdNetworkUrl, ctHash, chainId, acp, overallStartTime, retry404TimeoutMs, timeoutMs, onPoll, signal) {
2154
2182
  const body = {
2155
2183
  ct_tempkey: BigInt(ctHash).toString(16).padStart(64, "0"),
2156
2184
  host_chain_id: chainId,
@@ -2159,6 +2187,7 @@ async function submitSealOutputRequest(thresholdNetworkUrl, ctHash, chainId, acp
2159
2187
  let attemptIndex = 0;
2160
2188
  let last404ApiErrorMessage;
2161
2189
  for (; ; ) {
2190
+ throwIfAborted(signal, "sealOutput");
2162
2191
  let response;
2163
2192
  try {
2164
2193
  response = await cofheFetch(
@@ -2169,10 +2198,12 @@ async function submitSealOutputRequest(thresholdNetworkUrl, ctHash, chainId, acp
2169
2198
  headers: {
2170
2199
  "Content-Type": "application/json"
2171
2200
  },
2172
- body: JSON.stringify(body)
2201
+ body: JSON.stringify(body),
2202
+ signal
2173
2203
  }
2174
2204
  );
2175
2205
  } catch (e) {
2206
+ throwIfAborted(signal, "sealOutput");
2176
2207
  throw new CofheError({
2177
2208
  code: "SEAL_OUTPUT_FAILED" /* SealOutputFailed */,
2178
2209
  message: `sealOutput request failed`,
@@ -2267,13 +2298,14 @@ async function submitSealOutputRequest(thresholdNetworkUrl, ctHash, chainId, acp
2267
2298
  intervalMs: SUBMIT_RETRY_INTERVAL_MS,
2268
2299
  timeoutMs
2269
2300
  });
2270
- await new Promise((resolve) => setTimeout(resolve, SUBMIT_RETRY_INTERVAL_MS));
2301
+ await sleepUnlessAborted(SUBMIT_RETRY_INTERVAL_MS, signal, "sealOutput");
2271
2302
  attemptIndex += 1;
2272
2303
  }
2273
2304
  }
2274
- async function pollSealOutputStatus(thresholdNetworkUrl, requestId, overallStartTime, timeoutMs, onPoll) {
2305
+ async function pollSealOutputStatus(thresholdNetworkUrl, requestId, overallStartTime, timeoutMs, onPoll, signal) {
2275
2306
  let attemptIndex = 0;
2276
2307
  while (true) {
2308
+ throwIfAborted(signal, "sealOutput");
2277
2309
  const elapsedMs = Date.now() - overallStartTime;
2278
2310
  const intervalMs = computeMinuteRampPollIntervalMs(elapsedMs, {
2279
2311
  minIntervalMs: POLL_INTERVAL_MS,
@@ -2308,7 +2340,8 @@ async function pollSealOutputStatus(thresholdNetworkUrl, requestId, overallStart
2308
2340
  method: "GET",
2309
2341
  headers: {
2310
2342
  "Content-Type": "application/json"
2311
- }
2343
+ },
2344
+ signal
2312
2345
  }
2313
2346
  );
2314
2347
  } catch (e) {
@@ -2369,7 +2402,7 @@ async function pollSealOutputStatus(thresholdNetworkUrl, requestId, overallStart
2369
2402
  requestId
2370
2403
  });
2371
2404
  }
2372
- await new Promise((resolve) => setTimeout(resolve, intervalMs));
2405
+ await sleepUnlessAborted(intervalMs, signal, "sealOutput");
2373
2406
  attemptIndex += 1;
2374
2407
  }
2375
2408
  throw new CofheError({
@@ -2382,7 +2415,7 @@ async function pollSealOutputStatus(thresholdNetworkUrl, requestId, overallStart
2382
2415
  });
2383
2416
  }
2384
2417
  async function tnSealOutputV2(params) {
2385
- const { thresholdNetworkUrl, ctHash, chainId, acp, retry404TimeoutMs, onPoll } = params;
2418
+ const { thresholdNetworkUrl, ctHash, chainId, acp, retry404TimeoutMs, onPoll, signal } = params;
2386
2419
  const timeoutMs = params.timeoutMs ?? DEFAULT_DECRYPT_TIMEOUT_MS;
2387
2420
  const normalized404RetryTimeoutMs = normalize404RetryTimeoutMs({
2388
2421
  timeoutMs: retry404TimeoutMs,
@@ -2398,12 +2431,20 @@ async function tnSealOutputV2(params) {
2398
2431
  overallStartTime,
2399
2432
  normalized404RetryTimeoutMs,
2400
2433
  timeoutMs,
2401
- onPoll
2434
+ onPoll,
2435
+ signal
2402
2436
  );
2403
2437
  if (submitResult.kind === "completed") {
2404
2438
  return submitResult.sealed;
2405
2439
  }
2406
- return await pollSealOutputStatus(thresholdNetworkUrl, submitResult.requestId, overallStartTime, timeoutMs, onPoll);
2440
+ return await pollSealOutputStatus(
2441
+ thresholdNetworkUrl,
2442
+ submitResult.requestId,
2443
+ overallStartTime,
2444
+ timeoutMs,
2445
+ onPoll,
2446
+ signal
2447
+ );
2407
2448
  }
2408
2449
 
2409
2450
  // core/decrypt/decryptForViewBuilder.ts
@@ -2416,6 +2457,8 @@ var DecryptForViewBuilder = class extends BaseBuilder {
2416
2457
  pollCallback;
2417
2458
  retry404TimeoutMs = DEFAULT_404_RETRY_TIMEOUT_MS2;
2418
2459
  timeoutMs;
2460
+ signal;
2461
+ limit;
2419
2462
  constructor(params) {
2420
2463
  super({
2421
2464
  config: params.config,
@@ -2429,6 +2472,7 @@ var DecryptForViewBuilder = class extends BaseBuilder {
2429
2472
  this.utype = params.utype;
2430
2473
  this.acpHash = params.acpHash;
2431
2474
  this.acp = params.acp;
2475
+ this.limit = params.limit ?? ((task) => task());
2432
2476
  }
2433
2477
  /**
2434
2478
  * @param chainId - Chain to decrypt values from. Used to fetch the threshold network URL and use the correct acp.
@@ -2499,6 +2543,15 @@ var DecryptForViewBuilder = class extends BaseBuilder {
2499
2543
  this.timeoutMs = timeoutMs;
2500
2544
  return this;
2501
2545
  }
2546
+ /**
2547
+ * Stops the decrypt when `signal` fires: the pending request is aborted, no further attempt is
2548
+ * sent, and `execute()` rejects with `Aborted`. A request already processing on the threshold
2549
+ * network is not cancelled.
2550
+ */
2551
+ setSignal(signal) {
2552
+ this.signal = signal;
2553
+ return this;
2554
+ }
2502
2555
  withACP(acpOrACPHash) {
2503
2556
  if (typeof acpOrACPHash === "string") {
2504
2557
  this.acpHash = acpOrACPHash;
@@ -2622,15 +2675,20 @@ var DecryptForViewBuilder = class extends BaseBuilder {
2622
2675
  this.assertPublicClient();
2623
2676
  const thresholdNetworkUrl = await this.getThresholdNetworkUrl();
2624
2677
  const wireAcp = ACPUtils.getPublic(acp, true);
2625
- const sealed = await tnSealOutputV2({
2626
- ctHash: this.ctHash,
2627
- chainId: this.chainId,
2628
- acp: wireAcp,
2629
- thresholdNetworkUrl,
2630
- retry404TimeoutMs: this.retry404TimeoutMs,
2631
- timeoutMs: this.timeoutMs,
2632
- onPoll: this.pollCallback
2633
- });
2678
+ const sealed = await this.limit(
2679
+ () => tnSealOutputV2({
2680
+ ctHash: this.ctHash,
2681
+ chainId: this.chainId,
2682
+ acp: wireAcp,
2683
+ thresholdNetworkUrl,
2684
+ retry404TimeoutMs: this.retry404TimeoutMs,
2685
+ timeoutMs: this.timeoutMs,
2686
+ onPoll: this.pollCallback,
2687
+ signal: this.signal
2688
+ }),
2689
+ "sealOutput",
2690
+ this.signal
2691
+ );
2634
2692
  return ACPUtils.unseal(acp, sealed);
2635
2693
  }
2636
2694
  /**
@@ -2847,7 +2905,7 @@ function assertDecryptStatusResponseV2(value) {
2847
2905
  }
2848
2906
  return value;
2849
2907
  }
2850
- async function submitDecryptRequestV2(thresholdNetworkUrl, ctHash, chainId, acp, overallStartTime, retry404TimeoutMs, timeoutMs, onPoll) {
2908
+ async function submitDecryptRequestV2(thresholdNetworkUrl, ctHash, chainId, acp, overallStartTime, retry404TimeoutMs, timeoutMs, onPoll, signal) {
2851
2909
  const body = {
2852
2910
  ct_tempkey: BigInt(ctHash).toString(16).padStart(64, "0"),
2853
2911
  host_chain_id: chainId
@@ -2858,6 +2916,7 @@ async function submitDecryptRequestV2(thresholdNetworkUrl, ctHash, chainId, acp,
2858
2916
  let attemptIndex = 0;
2859
2917
  let last404ApiErrorMessage;
2860
2918
  for (; ; ) {
2919
+ throwIfAborted(signal, "decrypt");
2861
2920
  let response;
2862
2921
  try {
2863
2922
  response = await cofheFetch(
@@ -2868,10 +2927,12 @@ async function submitDecryptRequestV2(thresholdNetworkUrl, ctHash, chainId, acp,
2868
2927
  headers: {
2869
2928
  "Content-Type": "application/json"
2870
2929
  },
2871
- body: JSON.stringify(body)
2930
+ body: JSON.stringify(body),
2931
+ signal
2872
2932
  }
2873
2933
  );
2874
2934
  } catch (e) {
2935
+ throwIfAborted(signal, "decrypt");
2875
2936
  throw new CofheError({
2876
2937
  code: "DECRYPT_FAILED" /* DecryptFailed */,
2877
2938
  message: `decrypt request failed`,
@@ -2968,13 +3029,14 @@ async function submitDecryptRequestV2(thresholdNetworkUrl, ctHash, chainId, acp,
2968
3029
  intervalMs: SUBMIT_RETRY_INTERVAL_MS2,
2969
3030
  timeoutMs
2970
3031
  });
2971
- await new Promise((resolve) => setTimeout(resolve, SUBMIT_RETRY_INTERVAL_MS2));
3032
+ await sleepUnlessAborted(SUBMIT_RETRY_INTERVAL_MS2, signal, "decrypt");
2972
3033
  attemptIndex += 1;
2973
3034
  }
2974
3035
  }
2975
- async function pollDecryptStatusV2(thresholdNetworkUrl, requestId, overallStartTime, timeoutMs, onPoll) {
3036
+ async function pollDecryptStatusV2(thresholdNetworkUrl, requestId, overallStartTime, timeoutMs, onPoll, signal) {
2976
3037
  let attemptIndex = 0;
2977
3038
  while (true) {
3039
+ throwIfAborted(signal, "decrypt");
2978
3040
  const elapsedMs = Date.now() - overallStartTime;
2979
3041
  const intervalMs = computeMinuteRampPollIntervalMs(elapsedMs, {
2980
3042
  minIntervalMs: POLL_INTERVAL_MS2,
@@ -3009,7 +3071,8 @@ async function pollDecryptStatusV2(thresholdNetworkUrl, requestId, overallStartT
3009
3071
  method: "GET",
3010
3072
  headers: {
3011
3073
  "Content-Type": "application/json"
3012
- }
3074
+ },
3075
+ signal
3013
3076
  }
3014
3077
  );
3015
3078
  } catch (e) {
@@ -3071,7 +3134,7 @@ async function pollDecryptStatusV2(thresholdNetworkUrl, requestId, overallStartT
3071
3134
  requestId
3072
3135
  });
3073
3136
  }
3074
- await new Promise((resolve) => setTimeout(resolve, intervalMs));
3137
+ await sleepUnlessAborted(intervalMs, signal, "decrypt");
3075
3138
  attemptIndex += 1;
3076
3139
  }
3077
3140
  throw new CofheError({
@@ -3084,7 +3147,7 @@ async function pollDecryptStatusV2(thresholdNetworkUrl, requestId, overallStartT
3084
3147
  });
3085
3148
  }
3086
3149
  async function tnDecryptV2(params) {
3087
- const { thresholdNetworkUrl, ctHash, chainId, acp, retry404TimeoutMs, onPoll } = params;
3150
+ const { thresholdNetworkUrl, ctHash, chainId, acp, retry404TimeoutMs, onPoll, signal } = params;
3088
3151
  const timeoutMs = params.timeoutMs ?? DEFAULT_DECRYPT_TIMEOUT_MS;
3089
3152
  const normalized404RetryTimeoutMs = normalize404RetryTimeoutMs({
3090
3153
  timeoutMs: retry404TimeoutMs,
@@ -3100,12 +3163,20 @@ async function tnDecryptV2(params) {
3100
3163
  overallStartTime,
3101
3164
  normalized404RetryTimeoutMs,
3102
3165
  timeoutMs,
3103
- onPoll
3166
+ onPoll,
3167
+ signal
3104
3168
  );
3105
3169
  if (submitResult.kind === "completed") {
3106
3170
  return submitResult;
3107
3171
  }
3108
- return await pollDecryptStatusV2(thresholdNetworkUrl, submitResult.requestId, overallStartTime, timeoutMs, onPoll);
3172
+ return await pollDecryptStatusV2(
3173
+ thresholdNetworkUrl,
3174
+ submitResult.requestId,
3175
+ overallStartTime,
3176
+ timeoutMs,
3177
+ onPoll,
3178
+ signal
3179
+ );
3109
3180
  }
3110
3181
 
3111
3182
  // core/decrypt/decryptForTxBuilder.ts
@@ -3118,6 +3189,8 @@ var DecryptForTxBuilder = class extends BaseBuilder {
3118
3189
  pollCallback;
3119
3190
  retry404TimeoutMs = DEFAULT_404_RETRY_TIMEOUT_MS3;
3120
3191
  timeoutMs;
3192
+ signal;
3193
+ limit;
3121
3194
  constructor(params) {
3122
3195
  super({
3123
3196
  config: params.config,
@@ -3128,6 +3201,7 @@ var DecryptForTxBuilder = class extends BaseBuilder {
3128
3201
  requireConnected: params.requireConnected
3129
3202
  });
3130
3203
  this.ctHash = params.ctHash;
3204
+ this.limit = params.limit ?? ((task) => task());
3131
3205
  }
3132
3206
  setChainId(chainId) {
3133
3207
  this.chainId = chainId;
@@ -3165,6 +3239,10 @@ var DecryptForTxBuilder = class extends BaseBuilder {
3165
3239
  this.timeoutMs = timeoutMs;
3166
3240
  return this;
3167
3241
  }
3242
+ setSignal(signal) {
3243
+ this.signal = signal;
3244
+ return this;
3245
+ }
3168
3246
  withACP(acpOrACPHash) {
3169
3247
  if (this.acpSelection === "with-acp") {
3170
3248
  throw new CofheError({
@@ -3292,15 +3370,20 @@ var DecryptForTxBuilder = class extends BaseBuilder {
3292
3370
  this.assertPublicClient();
3293
3371
  const thresholdNetworkUrl = await this.getThresholdNetworkUrl();
3294
3372
  const wireAcp = acp ? ACPUtils.getPublic(acp, true) : null;
3295
- const { decryptedValue, signature } = await tnDecryptV2({
3296
- ctHash: this.ctHash,
3297
- chainId: this.chainId,
3298
- acp: wireAcp,
3299
- thresholdNetworkUrl,
3300
- retry404TimeoutMs: this.retry404TimeoutMs,
3301
- timeoutMs: this.timeoutMs,
3302
- onPoll: this.pollCallback
3303
- });
3373
+ const { decryptedValue, signature } = await this.limit(
3374
+ () => tnDecryptV2({
3375
+ ctHash: this.ctHash,
3376
+ chainId: this.chainId,
3377
+ acp: wireAcp,
3378
+ thresholdNetworkUrl,
3379
+ retry404TimeoutMs: this.retry404TimeoutMs,
3380
+ timeoutMs: this.timeoutMs,
3381
+ onPoll: this.pollCallback,
3382
+ signal: this.signal
3383
+ }),
3384
+ "decrypt",
3385
+ this.signal
3386
+ );
3304
3387
  return {
3305
3388
  ctHash: this.ctHash,
3306
3389
  decryptedValue,
@@ -3338,6 +3421,39 @@ var DecryptForTxBuilder = class extends BaseBuilder {
3338
3421
  }
3339
3422
  }
3340
3423
  };
3424
+
3425
+ // core/decrypt/decryptLimiter.ts
3426
+ function createDecryptLimiter(max) {
3427
+ let active = 0;
3428
+ const waiting = [];
3429
+ return async (task, operationLabel, signal) => {
3430
+ if (signal?.aborted)
3431
+ throw decryptAbortedError(operationLabel, signal);
3432
+ if (active >= max) {
3433
+ await new Promise((resume, reject) => {
3434
+ const onAbort = () => {
3435
+ const index = waiting.indexOf(start);
3436
+ if (index >= 0)
3437
+ waiting.splice(index, 1);
3438
+ reject(decryptAbortedError(operationLabel, signal));
3439
+ };
3440
+ const start = () => {
3441
+ signal?.removeEventListener("abort", onAbort);
3442
+ resume();
3443
+ };
3444
+ signal?.addEventListener("abort", onAbort, { once: true });
3445
+ waiting.push(start);
3446
+ });
3447
+ }
3448
+ active += 1;
3449
+ try {
3450
+ return await task();
3451
+ } finally {
3452
+ active -= 1;
3453
+ waiting.shift()?.();
3454
+ }
3455
+ };
3456
+ }
3341
3457
  var decryptResultSignerAbi = parseAbi(["function decryptResultSigner() view returns (address)"]);
3342
3458
  var UINT_TYPE_MASK2 = 0x7fn;
3343
3459
  var TYPE_BYTE_OFFSET2 = 8n;
@@ -3483,11 +3599,13 @@ function createCofheClientBase(opts) {
3483
3599
  requireConnected: _requireConnected
3484
3600
  });
3485
3601
  }
3602
+ const decryptLimiter = createDecryptLimiter(opts.config.decryptConcurrency);
3486
3603
  function decryptForView(ctHash, utype) {
3487
3604
  const state = connectStore.getState();
3488
3605
  return new DecryptForViewBuilder({
3489
3606
  ctHash,
3490
3607
  utype,
3608
+ limit: decryptLimiter,
3491
3609
  chainId: state.chainId,
3492
3610
  account: state.account,
3493
3611
  config: opts.config,
@@ -3500,6 +3618,7 @@ function createCofheClientBase(opts) {
3500
3618
  const state = connectStore.getState();
3501
3619
  return new DecryptForTxBuilder({
3502
3620
  ctHash,
3621
+ limit: decryptLimiter,
3503
3622
  chainId: state.chainId,
3504
3623
  account: state.account,
3505
3624
  config: opts.config,