@cofhe/sdk 1.0.1 → 1.1.0-alpha.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 (41) hide show
  1. package/CHANGELOG.md +0 -8
  2. package/core/acps.ts +64 -23
  3. package/core/client.ts +32 -1
  4. package/core/clientTypes.ts +22 -1
  5. package/core/decrypt/decryptForTxBuilder.ts +16 -0
  6. package/core/decrypt/decryptForViewBuilder.ts +14 -0
  7. package/core/decrypt/submitRetry.ts +16 -0
  8. package/core/decrypt/tnDecryptV2.ts +19 -9
  9. package/core/decrypt/tnSealOutputV2.ts +19 -9
  10. package/core/encrypt/encryptInputsBuilder.ts +13 -31
  11. package/core/encrypt/zkPackProveVerify.ts +4 -16
  12. package/core/error.ts +0 -1
  13. package/core/test/acpsPrepare.test.ts +77 -0
  14. package/core/test/decryptBuilders.test.ts +51 -0
  15. package/core/test/encryptInputsBuilder.test.ts +4 -56
  16. package/dist/acps.d.cts +4 -358
  17. package/dist/acps.d.ts +4 -358
  18. package/dist/acps.js +2 -2
  19. package/dist/{chunk-WAA2U7CT.js → chunk-3L2IVPMI.js} +1 -1
  20. package/dist/{chunk-OC5TGQGG.js → chunk-J23FYAI2.js} +0 -1
  21. package/dist/{chunk-GKEJHZ52.js → chunk-VE7DKXSF.js} +132 -75
  22. package/dist/{chunk-3SD2UCRE.js → chunk-WJGTPW7K.js} +1 -1
  23. package/dist/{clientTypes-DEKYh0cw.d.ts → clientTypes-B-qOZCx0.d.ts} +55 -7
  24. package/dist/{clientTypes-C2b_qub5.d.cts → clientTypes-D2djRDoH.d.cts} +55 -7
  25. package/dist/core.cjs +128 -72
  26. package/dist/core.d.cts +3 -4
  27. package/dist/core.d.ts +3 -4
  28. package/dist/core.js +4 -4
  29. package/dist/node.cjs +128 -71
  30. package/dist/node.d.cts +2 -2
  31. package/dist/node.d.ts +2 -2
  32. package/dist/node.js +4 -4
  33. package/dist/{onchain-utils-CgOslubH.d.ts → onchain-utils-Ns56cBm0.d.cts} +356 -2
  34. package/dist/{onchain-utils-CgOslubH.d.cts → onchain-utils-Ns56cBm0.d.ts} +356 -2
  35. package/dist/web.cjs +128 -71
  36. package/dist/web.d.cts +2 -2
  37. package/dist/web.d.ts +2 -2
  38. package/dist/web.js +4 -4
  39. package/dist/zkProve.worker.js +2 -2
  40. package/package.json +1 -1
  41. package/core/test/zkPoKMetadata.test.ts +0 -27
package/CHANGELOG.md CHANGED
@@ -1,13 +1,5 @@
1
1
  # @cofhe/sdk Changelog
2
2
 
3
- ## 1.0.1
4
-
5
- ### Patch Changes
6
-
7
- - fcc819c: **Breaking:** `encryptInputs(...).execute()` now binds the consuming contract into the ZK proof itself, and the CoFHE verifier requires it. Upgrade to 1.0.1: 1.0.0 is deprecated because of an issue in ZK verification, and once the verifier is updated a proof from 1.0.0 fails with `ZK_VERIFY_FAILED`. No code change is needed beyond the upgrade; `setConsumingContract(...)` was already required.
8
-
9
- `execute()` now throws `CofheError` with code `CONSUMING_CONTRACT_INVALID` when `setConsumingContract(...)` was given something that is not an address, before TFHE is initialised or any proving starts. The check is byte-level, 20 bytes of hex in any case, and does not validate the EIP-55 checksum, so every address 1.0.0 accepted still passes. Previously the bad address reached the verifier and failed there, after the proof was generated. `CofheErrorCode` gains `ConsumingContractInvalid`; an exhaustive `switch` over the enum needs the new arm.
10
-
11
3
  ## 1.0.0
12
4
 
13
5
  ### Major Changes
package/core/acps.ts CHANGED
@@ -26,7 +26,7 @@ import {
26
26
  clearAclServedAddresses,
27
27
  } from '@/acps';
28
28
 
29
- import { type Hex, type PublicClient, type WalletClient, zeroAddress } from 'viem';
29
+ import { type ContractFunctionParameters, type Hex, type PublicClient, type WalletClient, zeroAddress } from 'viem';
30
30
 
31
31
  // HELPERS
32
32
 
@@ -311,23 +311,39 @@ const getBlockRevokerData = async (publicClient: PublicClient): Promise<number>
311
311
  *
312
312
  * @returns the revocation transaction hash
313
313
  */
314
- const revokeACP = async (acp: ACP, walletClient: WalletClient): Promise<Hex> => {
314
+ /**
315
+ * A registry or revoker write built but not sent: what a simulation or `writeContract` takes,
316
+ * minus the sender. For a caller that sends through its own path (simulate first, record the
317
+ * transaction) instead of the plain `writeContract` the senders below do.
318
+ */
319
+ export type ShareRegistryWrite<functionName extends 'share' | 'removeShare'> = ContractFunctionParameters<
320
+ typeof ACP_SHARE_REGISTRY_ABI,
321
+ 'nonpayable',
322
+ functionName
323
+ >;
324
+ export type RevokerWrite = ContractFunctionParameters<typeof ACP_REVOKER_ABI, 'nonpayable', 'revokeSingle'>;
325
+
326
+ /** The revoker write that revokes one acp, built but not sent. Throws for an acp without a revoker. */
327
+ const prepareRevokeACP = (acp: ACP): RevokerWrite => {
315
328
  if (acp.revokerContract === zeroAddress || acp.revokerData === 0) {
316
329
  throw new Error('ACP is not revocable: it has no revoker (revokerContract/revokerData unset)');
317
330
  }
331
+ return {
332
+ address: acp.revokerContract,
333
+ abi: ACP_REVOKER_ABI,
334
+ functionName: 'revokeSingle',
335
+ args: [BigInt(acp.revokerData)],
336
+ };
337
+ };
338
+
339
+ const revokeACP = async (acp: ACP, walletClient: WalletClient): Promise<Hex> => {
340
+ const write = prepareRevokeACP(acp);
318
341
  if (walletClient.account == null) throw new Error('Missing walletClient account');
319
342
  if (walletClient.account.address.toLowerCase() !== acp.issuer.toLowerCase()) {
320
343
  throw new Error('Only the acp issuer can revoke it');
321
344
  }
322
345
 
323
- return walletClient.writeContract({
324
- address: acp.revokerContract,
325
- abi: ACP_REVOKER_ABI,
326
- functionName: 'revokeSingle',
327
- args: [BigInt(acp.revokerData)],
328
- account: walletClient.account,
329
- chain: walletClient.chain,
330
- });
346
+ return walletClient.writeContract({ ...write, account: walletClient.account, chain: walletClient.chain });
331
347
  };
332
348
 
333
349
  /**
@@ -416,34 +432,51 @@ const metadataOf = (acp: ACP, { labels, metadata, appKey }: ShareOnChainOptions)
416
432
  * to discover and import — the on-chain alternative to `export()` — optionally
417
433
  * with the labels of its handles.
418
434
  */
419
- const shareOnChain = async (
435
+ /**
436
+ * The registry `share` write for a signed sharing acp, built but not sent, with the share id it
437
+ * gets. Validates the acp, not the sender: the registry takes the write from the issuer only.
438
+ */
439
+ const prepareShareOnChain = (
420
440
  acp: ACP,
421
- walletClient: WalletClient,
422
441
  registry: Hex,
423
442
  options: ShareOnChainOptions = {}
424
- ): Promise<{ txHash: Hex; shareId: Hex }> => {
443
+ ): { write: ShareRegistryWrite<'share'>; shareId: Hex } => {
425
444
  if (acp.type !== 'sharing') {
426
445
  throw new Error(`Cannot share a '${acp.type}' ACP on-chain — only 'sharing' ACPs are shareable.`);
427
446
  }
428
447
  if (acp.issuerSignature === '0x') {
429
448
  throw new Error('Cannot share an unsigned sharing ACP — sign it first.');
430
449
  }
450
+ return {
451
+ write: {
452
+ address: registry,
453
+ abi: ACP_SHARE_REGISTRY_ABI,
454
+ functionName: 'share',
455
+ args: [toChainShare(acp), metadataOf(acp, options)],
456
+ },
457
+ shareId: computeShareId(acp),
458
+ };
459
+ };
460
+
461
+ const shareOnChain = async (
462
+ acp: ACP,
463
+ walletClient: WalletClient,
464
+ registry: Hex,
465
+ options: ShareOnChainOptions = {}
466
+ ): Promise<{ txHash: Hex; shareId: Hex }> => {
467
+ const { write, shareId } = prepareShareOnChain(acp, registry, options);
431
468
  if (walletClient.account == null) throw new Error('Missing walletClient account');
432
469
  if (walletClient.account.address.toLowerCase() !== acp.issuer.toLowerCase()) {
433
470
  throw new Error('Only the ACP issuer can share it on-chain');
434
471
  }
435
- const metadata = metadataOf(acp, options);
436
472
 
437
473
  const txHash = await walletClient.writeContract({
438
- address: registry,
439
- abi: ACP_SHARE_REGISTRY_ABI,
440
- functionName: 'share',
441
- args: [toChainShare(acp), metadata],
474
+ ...write,
442
475
  account: walletClient.account,
443
476
  chain: walletClient.chain ?? null,
444
477
  });
445
478
 
446
- return { txHash, shareId: computeShareId(acp) };
479
+ return { txHash, shareId };
447
480
  };
448
481
 
449
482
  const toIncomingShare = ({ shareId, share: s, metadata }: PostedShare): IncomingShare => ({
@@ -514,14 +547,19 @@ const importFromChain = async (
514
547
  return importShared({ ...options, type: 'sharing' }, publicClient, walletClient, importOptions);
515
548
  };
516
549
 
550
+ /** The registry `removeShare` write for one share id, built but not sent. */
551
+ const prepareRemoveShare = (shareId: Hex, registry: Hex): ShareRegistryWrite<'removeShare'> => ({
552
+ address: registry,
553
+ abi: ACP_SHARE_REGISTRY_ABI,
554
+ functionName: 'removeShare',
555
+ args: [shareId],
556
+ });
557
+
517
558
  /** Remove a share from the registry (issuer retracts / recipient dismisses). */
518
559
  const removeShareOnChain = async (shareId: Hex, walletClient: WalletClient, registry: Hex): Promise<Hex> => {
519
560
  if (walletClient.account == null) throw new Error('Missing walletClient account');
520
561
  return walletClient.writeContract({
521
- address: registry,
522
- abi: ACP_SHARE_REGISTRY_ABI,
523
- functionName: 'removeShare',
524
- args: [shareId],
562
+ ...prepareRemoveShare(shareId, registry),
525
563
  account: walletClient.account,
526
564
  chain: walletClient.chain ?? null,
527
565
  });
@@ -562,15 +600,18 @@ export const acps = {
562
600
  removeActiveACP,
563
601
 
564
602
  revokeACP,
603
+ prepareRevokeACP,
565
604
  revokeAllACPs,
566
605
  isACPRevoked,
567
606
  checkAccess,
568
607
 
569
608
  shareOnChain,
609
+ prepareShareOnChain,
570
610
  getIncomingShares,
571
611
  getShareFromChain,
572
612
  importFromChain,
573
613
  removeShareOnChain,
614
+ prepareRemoveShare,
574
615
  computeShareId,
575
616
 
576
617
  applyACPDefaultsFromChain,
package/core/client.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { CreateSelfACPOptions, CreateSharingACPOptions, ImportSharedACPOptions } from '@/acps';
1
+ import type { ACP, CreateSelfACPOptions, CreateSharingACPOptions, ImportSharedACPOptions } from '@/acps';
2
2
  import { decodeShareMetadata, shareAppKey, verifyShareLabels } from '@/acps';
3
3
 
4
4
  import { createStore } from 'zustand/vanilla';
@@ -90,6 +90,14 @@ export function createCofheClientBase<TConfig extends CofheConfig>(
90
90
  }
91
91
  };
92
92
 
93
+ /** The sharing writes the registry and the revoker take from the issuer only; say so before building one. */
94
+ const _requireIssuer = (acp: ACP, what: string) => {
95
+ const sender = connectStore.getState().walletClient?.account?.address;
96
+ if (sender == null || sender.toLowerCase() !== acp.issuer.toLowerCase()) {
97
+ throw new Error(`Only the acp issuer can ${what}`);
98
+ }
99
+ };
100
+
93
101
  // LIFECYCLE
94
102
 
95
103
  async function connect(publicClient: PublicClient, walletClient: WalletClient) {
@@ -306,6 +314,12 @@ export function createCofheClientBase<TConfig extends CofheConfig>(
306
314
  return acps.revokeACP(acp, walletClient!);
307
315
  },
308
316
 
317
+ prepareRevokeACP: (acp) => {
318
+ _requireConnected();
319
+ _requireIssuer(acp, 'revoke it');
320
+ return acps.prepareRevokeACP(acp);
321
+ },
322
+
309
323
  revokeAllACPs: async (revokerContract?: `0x${string}`) => {
310
324
  _requireConnected();
311
325
  const { publicClient, walletClient } = connectStore.getState();
@@ -333,6 +347,23 @@ export function createCofheClientBase<TConfig extends CofheConfig>(
333
347
  return acps.shareOnChain(acp, walletClient!, sharingRegistry, { ...withAppKey(), ...options });
334
348
  },
335
349
 
350
+ prepareShareOnChain: async (acp, options) => {
351
+ _requireConnected();
352
+ _requireIssuer(acp, 'share it on-chain');
353
+ const { publicClient } = connectStore.getState();
354
+ const chainId = await publicClient!.getChainId();
355
+ const sharingRegistry = await _resolveSharingRegistry(publicClient!, chainId);
356
+ return acps.prepareShareOnChain(acp, sharingRegistry, { ...withAppKey(), ...options });
357
+ },
358
+
359
+ prepareRemoveShare: async (shareId) => {
360
+ _requireConnected();
361
+ const { publicClient } = connectStore.getState();
362
+ const chainId = await publicClient!.getChainId();
363
+ const sharingRegistry = await _resolveSharingRegistry(publicClient!, chainId);
364
+ return acps.prepareRemoveShare(shareId, sharingRegistry);
365
+ },
366
+
336
367
  getIncomingShares: async () => {
337
368
  _requireConnected();
338
369
  const { publicClient, walletClient } = connectStore.getState();
@@ -6,7 +6,13 @@ import { type DecryptForTxBuilderUnset } from './decrypt/decryptForTxBuilder.js'
6
6
  import { type EncryptInputsBuilderUnset } from './encrypt/encryptInputsBuilder.js';
7
7
  import { type ZkBuilderAndCrsGenerator, type ZkProveWorkerFunction } from './encrypt/zkPackProveVerify.js';
8
8
  import { type FheKeyDeserializer } from './fetchKeys.js';
9
- import { acps, type ImportSharedOptions, type ShareOnChainOptions } from './acps.js';
9
+ import {
10
+ acps,
11
+ type ImportSharedOptions,
12
+ type RevokerWrite,
13
+ type ShareOnChainOptions,
14
+ type ShareRegistryWrite,
15
+ } from './acps.js';
10
16
  import type { EncryptableItem, FheTypes, TfheInitializer } from './types.js';
11
17
  import type { ACPUtils } from 'acps/acp.js';
12
18
  import type {
@@ -114,6 +120,11 @@ export type CofheClientACPs = {
114
120
 
115
121
  // Revocation (on-chain, require connection)
116
122
  revokeACP: (acp: ACP) => Promise<`0x${string}`>;
123
+ /**
124
+ * The revoker write `revokeACP` sends, built but not sent — for an app that sends through its own
125
+ * path (a simulation first, a transaction store). Issuer only; throws for an acp without a revoker.
126
+ */
127
+ prepareRevokeACP: (acp: ACP) => RevokerWrite;
117
128
  revokeAllACPs: (revokerContract?: `0x${string}`) => Promise<`0x${string}`>;
118
129
  isACPRevoked: (acp: ACP) => Promise<boolean>;
119
130
  /**
@@ -129,6 +140,16 @@ export type CofheClientACPs = {
129
140
  * optionally with the labels of its handles (`options.labels`, one per handle) or an encoded blob.
130
141
  */
131
142
  shareOnChain: (acp: ACP, options?: ShareOnChainOptions) => Promise<{ txHash: `0x${string}`; shareId: `0x${string}` }>;
143
+ /**
144
+ * The registry write `shareOnChain` sends and the share id it gets, built but not sent — for an
145
+ * app that sends through its own path (a simulation first, a transaction store). Issuer only.
146
+ */
147
+ prepareShareOnChain: (
148
+ acp: ACP,
149
+ options?: ShareOnChainOptions
150
+ ) => Promise<{ write: ShareRegistryWrite<'share'>; shareId: `0x${string}` }>;
151
+ /** The registry write `dismissShare` / `cancelShare` sends, built but not sent. */
152
+ prepareRemoveShare: (shareId: `0x${string}`) => Promise<ShareRegistryWrite<'removeShare'>>;
132
153
  /** Importable shares addressed to the connected account (unexpired, not revoked), with their metadata. */
133
154
  getIncomingShares: () => Promise<IncomingShare[]>;
134
155
  /** One share by id, as posted; null when unknown or removed. */
@@ -11,6 +11,7 @@ import { cofheMocksDecryptForTx } from './cofheMocksDecryptForTx';
11
11
  import { getPublicClientChainID, sleep } from '../utils';
12
12
  import { type DecryptPollCallbackFunction } from '../types';
13
13
  import { tnDecryptV2 } from './tnDecryptV2';
14
+ import { assertDecryptTimeoutMs } from './submitRetry';
14
15
 
15
16
  const DEFAULT_404_RETRY_TIMEOUT_MS = 10_000;
16
17
 
@@ -65,6 +66,7 @@ export class DecryptForTxBuilder extends BaseBuilder {
65
66
  private acpSelection: DecryptForTxACPSelection = 'unset';
66
67
  private pollCallback?: DecryptPollCallbackFunction;
67
68
  private retry404TimeoutMs = DEFAULT_404_RETRY_TIMEOUT_MS;
69
+ private timeoutMs?: number;
68
70
 
69
71
  constructor(params: DecryptForTxBuilderParams) {
70
72
  super({
@@ -153,6 +155,19 @@ export class DecryptForTxBuilder extends BaseBuilder {
153
155
  return this;
154
156
  }
155
157
 
158
+ /**
159
+ * The whole budget of this decrypt in milliseconds, submit retries and status polling together
160
+ * (5 minutes by default). Past it, `execute()` rejects with `DecryptFailed`; a request still
161
+ * processing on the threshold network is not cancelled.
162
+ */
163
+ setTimeout(this: DecryptForTxBuilderUnset, timeoutMs: number): DecryptForTxBuilderUnset;
164
+ setTimeout(this: DecryptForTxBuilderSelected, timeoutMs: number): DecryptForTxBuilderSelected;
165
+ setTimeout(timeoutMs: number): DecryptForTxBuilder {
166
+ assertDecryptTimeoutMs(timeoutMs, 'decryptForTx');
167
+ this.timeoutMs = timeoutMs;
168
+ return this;
169
+ }
170
+
156
171
  /**
157
172
  * Select "use acp" mode.
158
173
  *
@@ -326,6 +341,7 @@ export class DecryptForTxBuilder extends BaseBuilder {
326
341
  acp: wireAcp,
327
342
  thresholdNetworkUrl,
328
343
  retry404TimeoutMs: this.retry404TimeoutMs,
344
+ timeoutMs: this.timeoutMs,
329
345
  onPoll: this.pollCallback,
330
346
  });
331
347
 
@@ -13,6 +13,7 @@ import { cofheMocksDecryptForView } from './cofheMocksDecryptForView.js';
13
13
  import { tnSealOutputV2 } from './tnSealOutputV2.js';
14
14
  import { sleep } from '../utils.js';
15
15
  import { type DecryptPollCallbackFunction } from '../types.js';
16
+ import { assertDecryptTimeoutMs } from './submitRetry.js';
16
17
 
17
18
  const DEFAULT_404_RETRY_TIMEOUT_MS = 10_000;
18
19
 
@@ -51,6 +52,7 @@ export class DecryptForViewBuilder<U extends FheTypes> extends BaseBuilder {
51
52
  private acp?: ACP;
52
53
  private pollCallback?: DecryptPollCallbackFunction;
53
54
  private retry404TimeoutMs = DEFAULT_404_RETRY_TIMEOUT_MS;
55
+ private timeoutMs?: number;
54
56
 
55
57
  constructor(params: DecryptForViewBuilderParams<U>) {
56
58
  super({
@@ -134,6 +136,17 @@ export class DecryptForViewBuilder<U extends FheTypes> extends BaseBuilder {
134
136
  return this;
135
137
  }
136
138
 
139
+ /**
140
+ * The whole budget of this decrypt in milliseconds, submit retries and status polling together
141
+ * (5 minutes by default). Past it, `execute()` rejects with `SealOutputFailed`; a request still
142
+ * processing on the threshold network is not cancelled.
143
+ */
144
+ setTimeout(timeoutMs: number): DecryptForViewBuilder<U> {
145
+ assertDecryptTimeoutMs(timeoutMs, 'decryptForView');
146
+ this.timeoutMs = timeoutMs;
147
+ return this;
148
+ }
149
+
137
150
  /**
138
151
  * Select "use acp" mode (optional).
139
152
  *
@@ -295,6 +308,7 @@ export class DecryptForViewBuilder<U extends FheTypes> extends BaseBuilder {
295
308
  acp: wireAcp,
296
309
  thresholdNetworkUrl,
297
310
  retry404TimeoutMs: this.retry404TimeoutMs,
311
+ timeoutMs: this.timeoutMs,
298
312
  onPoll: this.pollCallback,
299
313
  });
300
314
  return ACPUtils.unseal(acp, sealed);
@@ -2,6 +2,22 @@ import { CofheError, CofheErrorCode } from '../error.js';
2
2
  import { mapApiErrorCodeToCofheErrorCode, parseApiErrorResponseBody } from './apiError.js';
3
3
 
4
4
  export const DEFAULT_404_RETRY_TIMEOUT_MS = 10_000;
5
+ /** The default budget of one decrypt, submit retries and status polling together. */
6
+ export const DEFAULT_DECRYPT_TIMEOUT_MS = 5 * 60 * 1000;
7
+
8
+ /**
9
+ * Validates a builder's `setTimeout(timeoutMs)` argument: the overall budget must be a positive,
10
+ * finite number of milliseconds (0 would time out before the first attempt).
11
+ */
12
+ export function assertDecryptTimeoutMs(timeoutMs: number, operationLabel: string): void {
13
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
14
+ throw new CofheError({
15
+ code: CofheErrorCode.InternalError,
16
+ message: `${operationLabel}: setTimeout(timeoutMs) expects a finite number greater than 0.`,
17
+ context: { timeoutMs },
18
+ });
19
+ }
20
+ }
5
21
 
6
22
  export type SubmitRetryableStatus = 204 | 404;
7
23
 
@@ -6,12 +6,16 @@ import { type DecryptPollCallbackFunction } from '../types';
6
6
  import { normalizeTnSignature, parseDecryptedBytesToBigInt } from './tnDecryptUtils';
7
7
  import { computeMinuteRampPollIntervalMs } from './polling.js';
8
8
  import { mapApiErrorCodeToCofheErrorCode, parseApiErrorResponseBody } from './apiError.js';
9
- import { classifySubmitResponse, normalize404RetryTimeoutMs, throwIfSubmitRetryTimedOut } from './submitRetry.js';
9
+ import {
10
+ DEFAULT_DECRYPT_TIMEOUT_MS,
11
+ classifySubmitResponse,
12
+ normalize404RetryTimeoutMs,
13
+ throwIfSubmitRetryTimedOut,
14
+ } from './submitRetry.js';
10
15
 
11
16
  // Polling configuration
12
17
  const POLL_INTERVAL_MS = 1000; // 1 second
13
18
  const POLL_MAX_INTERVAL_MS = 10_000; // 10 seconds
14
- const DECRYPT_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes total across submit + poll
15
19
  const SUBMIT_RETRY_INTERVAL_MS = 1000; // 1 second
16
20
 
17
21
  type DecryptSubmitResponseV2 = {
@@ -182,6 +186,7 @@ async function submitDecryptRequestV2(
182
186
  acp: ACPPublic | null,
183
187
  overallStartTime: number,
184
188
  retry404TimeoutMs: number,
189
+ timeoutMs: number,
185
190
  onPoll?: DecryptPollCallbackFunction
186
191
  ): Promise<DecryptSubmitResultV2> {
187
192
  const body: {
@@ -306,7 +311,7 @@ async function submitDecryptRequestV2(
306
311
  status: responseClassification.status,
307
312
  elapsedMs,
308
313
  retry404TimeoutMs,
309
- overallTimeoutMs: DECRYPT_TIMEOUT_MS,
314
+ overallTimeoutMs: timeoutMs,
310
315
  thresholdNetworkUrl,
311
316
  body,
312
317
  attemptIndex,
@@ -319,7 +324,7 @@ async function submitDecryptRequestV2(
319
324
  attemptIndex,
320
325
  elapsedMs,
321
326
  intervalMs: SUBMIT_RETRY_INTERVAL_MS,
322
- timeoutMs: DECRYPT_TIMEOUT_MS,
327
+ timeoutMs,
323
328
  });
324
329
 
325
330
  await new Promise((resolve) => setTimeout(resolve, SUBMIT_RETRY_INTERVAL_MS));
@@ -331,6 +336,7 @@ async function pollDecryptStatusV2(
331
336
  thresholdNetworkUrl: string,
332
337
  requestId: string,
333
338
  overallStartTime: number,
339
+ timeoutMs: number,
334
340
  onPoll?: DecryptPollCallbackFunction
335
341
  ): Promise<{ decryptedValue: bigint; signature: `0x${string}` }> {
336
342
  let attemptIndex = 0;
@@ -348,18 +354,18 @@ async function pollDecryptStatusV2(
348
354
  attemptIndex,
349
355
  elapsedMs,
350
356
  intervalMs,
351
- timeoutMs: DECRYPT_TIMEOUT_MS,
357
+ timeoutMs,
352
358
  });
353
359
 
354
- if (elapsedMs > DECRYPT_TIMEOUT_MS) {
360
+ if (elapsedMs > timeoutMs) {
355
361
  throw new CofheError({
356
362
  code: CofheErrorCode.DecryptFailed,
357
- message: `decrypt polling timed out after ${DECRYPT_TIMEOUT_MS}ms`,
363
+ message: `decrypt polling timed out after ${timeoutMs}ms`,
358
364
  hint: 'The request may still be processing. Try again later.',
359
365
  context: {
360
366
  thresholdNetworkUrl,
361
367
  requestId,
362
- timeoutMs: DECRYPT_TIMEOUT_MS,
368
+ timeoutMs,
363
369
  },
364
370
  });
365
371
  }
@@ -462,9 +468,12 @@ export async function tnDecryptV2(params: {
462
468
  acp: ACPPublic | null;
463
469
  thresholdNetworkUrl: string;
464
470
  retry404TimeoutMs?: number;
471
+ /** The whole budget, submit retries and status polling together; `DEFAULT_DECRYPT_TIMEOUT_MS` by default. */
472
+ timeoutMs?: number;
465
473
  onPoll?: DecryptPollCallbackFunction;
466
474
  }): Promise<{ decryptedValue: bigint; signature: `0x${string}` }> {
467
475
  const { thresholdNetworkUrl, ctHash, chainId, acp, retry404TimeoutMs, onPoll } = params;
476
+ const timeoutMs = params.timeoutMs ?? DEFAULT_DECRYPT_TIMEOUT_MS;
468
477
  const normalized404RetryTimeoutMs = normalize404RetryTimeoutMs({
469
478
  timeoutMs: retry404TimeoutMs,
470
479
  operationLabel: 'decrypt',
@@ -478,6 +487,7 @@ export async function tnDecryptV2(params: {
478
487
  acp,
479
488
  overallStartTime,
480
489
  normalized404RetryTimeoutMs,
490
+ timeoutMs,
481
491
  onPoll
482
492
  );
483
493
 
@@ -485,5 +495,5 @@ export async function tnDecryptV2(params: {
485
495
  return submitResult;
486
496
  }
487
497
 
488
- return await pollDecryptStatusV2(thresholdNetworkUrl, submitResult.requestId, overallStartTime, onPoll);
498
+ return await pollDecryptStatusV2(thresholdNetworkUrl, submitResult.requestId, overallStartTime, timeoutMs, onPoll);
489
499
  }
@@ -5,13 +5,17 @@ import { CofheError, CofheErrorCode } from '../error.js';
5
5
  import { type DecryptPollCallbackFunction } from '../types.js';
6
6
  import { computeMinuteRampPollIntervalMs } from './polling.js';
7
7
  import { mapApiErrorCodeToCofheErrorCode, parseApiErrorResponseBody } from './apiError.js';
8
- import { classifySubmitResponse, normalize404RetryTimeoutMs, throwIfSubmitRetryTimedOut } from './submitRetry.js';
8
+ import {
9
+ DEFAULT_DECRYPT_TIMEOUT_MS,
10
+ classifySubmitResponse,
11
+ normalize404RetryTimeoutMs,
12
+ throwIfSubmitRetryTimedOut,
13
+ } from './submitRetry.js';
9
14
  import {} from './tnDecryptUtils.js';
10
15
 
11
16
  // Polling configuration
12
17
  const POLL_INTERVAL_MS = 1000; // 1 second
13
18
  const POLL_MAX_INTERVAL_MS = 10_000; // 10 seconds
14
- const SEAL_OUTPUT_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes total across submit + poll
15
19
  const SUBMIT_RETRY_INTERVAL_MS = 1000; // 1 second
16
20
 
17
21
  // V2 API response types
@@ -141,6 +145,7 @@ async function submitSealOutputRequest(
141
145
  acp: ACPPublic,
142
146
  overallStartTime: number,
143
147
  retry404TimeoutMs: number,
148
+ timeoutMs: number,
144
149
  onPoll?: DecryptPollCallbackFunction
145
150
  ): Promise<SealOutputSubmitResult> {
146
151
  const body = {
@@ -254,7 +259,7 @@ async function submitSealOutputRequest(
254
259
  status: responseClassification.status,
255
260
  elapsedMs,
256
261
  retry404TimeoutMs,
257
- overallTimeoutMs: SEAL_OUTPUT_TIMEOUT_MS,
262
+ overallTimeoutMs: timeoutMs,
258
263
  thresholdNetworkUrl,
259
264
  body,
260
265
  attemptIndex,
@@ -267,7 +272,7 @@ async function submitSealOutputRequest(
267
272
  attemptIndex,
268
273
  elapsedMs,
269
274
  intervalMs: SUBMIT_RETRY_INTERVAL_MS,
270
- timeoutMs: SEAL_OUTPUT_TIMEOUT_MS,
275
+ timeoutMs,
271
276
  });
272
277
 
273
278
  await new Promise((resolve) => setTimeout(resolve, SUBMIT_RETRY_INTERVAL_MS));
@@ -282,6 +287,7 @@ async function pollSealOutputStatus(
282
287
  thresholdNetworkUrl: string,
283
288
  requestId: string,
284
289
  overallStartTime: number,
290
+ timeoutMs: number,
285
291
  onPoll?: DecryptPollCallbackFunction
286
292
  ): Promise<EthEncryptedData> {
287
293
  let attemptIndex = 0;
@@ -299,19 +305,19 @@ async function pollSealOutputStatus(
299
305
  attemptIndex,
300
306
  elapsedMs,
301
307
  intervalMs,
302
- timeoutMs: SEAL_OUTPUT_TIMEOUT_MS,
308
+ timeoutMs,
303
309
  });
304
310
 
305
311
  // Check timeout
306
- if (elapsedMs > SEAL_OUTPUT_TIMEOUT_MS) {
312
+ if (elapsedMs > timeoutMs) {
307
313
  throw new CofheError({
308
314
  code: CofheErrorCode.SealOutputFailed,
309
- message: `sealOutput polling timed out after ${SEAL_OUTPUT_TIMEOUT_MS}ms`,
315
+ message: `sealOutput polling timed out after ${timeoutMs}ms`,
310
316
  hint: 'The request may still be processing. Try again later.',
311
317
  context: {
312
318
  thresholdNetworkUrl,
313
319
  requestId,
314
- timeoutMs: SEAL_OUTPUT_TIMEOUT_MS,
320
+ timeoutMs,
315
321
  },
316
322
  });
317
323
  }
@@ -416,9 +422,12 @@ export async function tnSealOutputV2(params: {
416
422
  acp: ACPPublic;
417
423
  thresholdNetworkUrl: string;
418
424
  retry404TimeoutMs?: number;
425
+ /** The whole budget, submit retries and status polling together; `DEFAULT_DECRYPT_TIMEOUT_MS` by default. */
426
+ timeoutMs?: number;
419
427
  onPoll?: DecryptPollCallbackFunction;
420
428
  }): Promise<EthEncryptedData> {
421
429
  const { thresholdNetworkUrl, ctHash, chainId, acp, retry404TimeoutMs, onPoll } = params;
430
+ const timeoutMs = params.timeoutMs ?? DEFAULT_DECRYPT_TIMEOUT_MS;
422
431
  const normalized404RetryTimeoutMs = normalize404RetryTimeoutMs({
423
432
  timeoutMs: retry404TimeoutMs,
424
433
  operationLabel: 'sealOutput',
@@ -434,6 +443,7 @@ export async function tnSealOutputV2(params: {
434
443
  acp,
435
444
  overallStartTime,
436
445
  normalized404RetryTimeoutMs,
446
+ timeoutMs,
437
447
  onPoll
438
448
  );
439
449
 
@@ -442,5 +452,5 @@ export async function tnSealOutputV2(params: {
442
452
  }
443
453
 
444
454
  // Step 2: Poll for status until completed
445
- return await pollSealOutputStatus(thresholdNetworkUrl, submitResult.requestId, overallStartTime, onPoll);
455
+ return await pollSealOutputStatus(thresholdNetworkUrl, submitResult.requestId, overallStartTime, timeoutMs, onPoll);
446
456
  }
@@ -21,7 +21,7 @@ import { cofheMocksCheckEncryptableBits, cofheMocksZkVerifySign } from './cofheM
21
21
  import { hardhat } from 'viem/chains';
22
22
  import { fetchKeys, type FheKeyDeserializer } from '../fetchKeys.js';
23
23
  import { getZkVerifierUrlOrThrow } from '../config.js';
24
- import { type WalletClient, isAddress } from 'viem';
24
+ import { type WalletClient } from 'viem';
25
25
  import { sleep } from '../utils.js';
26
26
  import { BaseBuilder, type BaseBuilderParams } from '../baseBuilder.js';
27
27
  import { type KeysStorage } from '../keyStore.js';
@@ -256,37 +256,19 @@ export class EncryptInputsBuilder<T extends EncryptableItem[]> extends BaseBuild
256
256
  }
257
257
 
258
258
  /**
259
- * Asserts that this.consumingContract is populated and is an address.
260
- * Runs before TFHE init, key fetching and proving, so a bad address costs
261
- * nothing: the contract is bound into the proof, and a proof built for a
262
- * non-address could only fail at the verifier, after the expensive step.
263
- * @throws {CofheError} If consumingContract is not set or is not an address
259
+ * Asserts that this.consumingContract is populated
260
+ * @throws {CofheError} If consumingContract is not set
264
261
  */
265
262
  private assertConsumingContract(): asserts this is this & { consumingContract: string } {
266
- if (!this.consumingContract) {
267
- throw new CofheError({
268
- code: CofheErrorCode.ConsumingContractUninitialized,
269
- message: 'Consuming contract is not set',
270
- hint: 'Use setConsumingContract(...) to set the contract that will consume the encrypted inputs.',
271
- context: {
272
- consumingContract: this.consumingContract,
273
- },
274
- });
275
- }
276
- // strict: false — 20 bytes of hex in any case, no EIP-55 checksum check. The
277
- // verifier decodes the address case-insensitively and the chain compares bytes,
278
- // so a mixed-case address with a wrong checksum works end to end and a patch
279
- // release must keep accepting it.
280
- if (!isAddress(this.consumingContract, { strict: false })) {
281
- throw new CofheError({
282
- code: CofheErrorCode.ConsumingContractInvalid,
283
- message: 'Consuming contract is not an address',
284
- hint: 'setConsumingContract(...) takes the 20-byte address of the contract that will consume the encrypted inputs.',
285
- context: {
286
- consumingContract: this.consumingContract,
287
- },
288
- });
289
- }
263
+ if (this.consumingContract) return;
264
+ throw new CofheError({
265
+ code: CofheErrorCode.ConsumingContractUninitialized,
266
+ message: 'Consuming contract is not set',
267
+ hint: 'Use setConsumingContract(...) to set the contract that will consume the encrypted inputs.',
268
+ context: {
269
+ consumingContract: this.consumingContract,
270
+ },
271
+ });
290
272
  }
291
273
 
292
274
  /**
@@ -585,7 +567,7 @@ export class EncryptInputsBuilder<T extends EncryptableItem[]> extends BaseBuild
585
567
  this.fireStepStart(EncryptStep.Prove);
586
568
 
587
569
  // Construct metadata once (used by both worker and main thread paths)
588
- const metadata = constructZkPoKMetadata(this.account, this.securityZone, this.chainId, this.consumingContract);
570
+ const metadata = constructZkPoKMetadata(this.account, this.securityZone, this.chainId);
589
571
 
590
572
  let proof: Uint8Array | null = null;
591
573
  let usedWorker = false;