@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.
- package/CHANGELOG.md +0 -8
- package/core/acps.ts +64 -23
- package/core/client.ts +32 -1
- package/core/clientTypes.ts +22 -1
- package/core/decrypt/decryptForTxBuilder.ts +16 -0
- package/core/decrypt/decryptForViewBuilder.ts +14 -0
- package/core/decrypt/submitRetry.ts +16 -0
- package/core/decrypt/tnDecryptV2.ts +19 -9
- package/core/decrypt/tnSealOutputV2.ts +19 -9
- package/core/encrypt/encryptInputsBuilder.ts +13 -31
- package/core/encrypt/zkPackProveVerify.ts +4 -16
- package/core/error.ts +0 -1
- package/core/test/acpsPrepare.test.ts +77 -0
- package/core/test/decryptBuilders.test.ts +51 -0
- package/core/test/encryptInputsBuilder.test.ts +4 -56
- package/dist/acps.d.cts +4 -358
- package/dist/acps.d.ts +4 -358
- package/dist/acps.js +2 -2
- package/dist/{chunk-WAA2U7CT.js → chunk-3L2IVPMI.js} +1 -1
- package/dist/{chunk-OC5TGQGG.js → chunk-J23FYAI2.js} +0 -1
- package/dist/{chunk-GKEJHZ52.js → chunk-VE7DKXSF.js} +132 -75
- package/dist/{chunk-3SD2UCRE.js → chunk-WJGTPW7K.js} +1 -1
- package/dist/{clientTypes-DEKYh0cw.d.ts → clientTypes-B-qOZCx0.d.ts} +55 -7
- package/dist/{clientTypes-C2b_qub5.d.cts → clientTypes-D2djRDoH.d.cts} +55 -7
- package/dist/core.cjs +128 -72
- package/dist/core.d.cts +3 -4
- package/dist/core.d.ts +3 -4
- package/dist/core.js +4 -4
- package/dist/node.cjs +128 -71
- package/dist/node.d.cts +2 -2
- package/dist/node.d.ts +2 -2
- package/dist/node.js +4 -4
- package/dist/{onchain-utils-CgOslubH.d.ts → onchain-utils-Ns56cBm0.d.cts} +356 -2
- package/dist/{onchain-utils-CgOslubH.d.cts → onchain-utils-Ns56cBm0.d.ts} +356 -2
- package/dist/web.cjs +128 -71
- package/dist/web.d.cts +2 -2
- package/dist/web.d.ts +2 -2
- package/dist/web.js +4 -4
- package/dist/zkProve.worker.js +2 -2
- package/package.json +1 -1
- 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
|
-
|
|
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
|
-
|
|
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
|
-
):
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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();
|
package/core/clientTypes.ts
CHANGED
|
@@ -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 {
|
|
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 {
|
|
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:
|
|
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
|
|
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
|
|
357
|
+
timeoutMs,
|
|
352
358
|
});
|
|
353
359
|
|
|
354
|
-
if (elapsedMs >
|
|
360
|
+
if (elapsedMs > timeoutMs) {
|
|
355
361
|
throw new CofheError({
|
|
356
362
|
code: CofheErrorCode.DecryptFailed,
|
|
357
|
-
message: `decrypt polling timed out after ${
|
|
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
|
|
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 {
|
|
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:
|
|
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
|
|
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
|
|
308
|
+
timeoutMs,
|
|
303
309
|
});
|
|
304
310
|
|
|
305
311
|
// Check timeout
|
|
306
|
-
if (elapsedMs >
|
|
312
|
+
if (elapsedMs > timeoutMs) {
|
|
307
313
|
throw new CofheError({
|
|
308
314
|
code: CofheErrorCode.SealOutputFailed,
|
|
309
|
-
message: `sealOutput polling timed out after ${
|
|
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
|
|
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
|
|
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
|
|
260
|
-
*
|
|
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 (
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
|
570
|
+
const metadata = constructZkPoKMetadata(this.account, this.securityZone, this.chainId);
|
|
589
571
|
|
|
590
572
|
let proof: Uint8Array | null = null;
|
|
591
573
|
let usedWorker = false;
|