@cofhe/sdk 0.5.2 → 0.6.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 (42) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/chains/chains/hardhat.ts +3 -3
  3. package/core/consts.ts +0 -3
  4. package/core/debug.ts +72 -0
  5. package/core/decrypt/tnDecryptV2.ts +20 -13
  6. package/core/decrypt/tnSealOutputV2.ts +20 -13
  7. package/core/encrypt/cofheMocksZkVerifySign.ts +2 -2
  8. package/core/encrypt/encryptInputsBuilder.ts +46 -11
  9. package/core/encrypt/zkPackProveVerify.ts +3 -3
  10. package/core/index.ts +23 -1
  11. package/core/permits.ts +17 -7
  12. package/core/test/encryptInputsBuilder.test.ts +35 -0
  13. package/core/test/permits.test.ts +62 -0
  14. package/core/types.ts +65 -5
  15. package/dist/chains.cjs +3 -3
  16. package/dist/chains.js +1 -1
  17. package/dist/{chunk-4FP4V35O.js → chunk-ESMZCFJY.js} +1 -2
  18. package/dist/{chunk-TBLR7NNE.js → chunk-MTRAXQXC.js} +3 -3
  19. package/dist/{chunk-YDOK4BDL.js → chunk-NOC3PYB7.js} +108 -33
  20. package/dist/{chunk-MRCKUMOS.js → chunk-VB62WYPL.js} +1 -1
  21. package/dist/{clientTypes-BJbFeeno.d.cts → clientTypes-BDy1qIBu.d.cts} +73 -11
  22. package/dist/{clientTypes-CEno_BEf.d.ts → clientTypes-CyUvRRzA.d.ts} +73 -11
  23. package/dist/core.cjs +110 -34
  24. package/dist/core.d.cts +26 -5
  25. package/dist/core.d.ts +26 -5
  26. package/dist/core.js +4 -4
  27. package/dist/node.cjs +100 -32
  28. package/dist/node.d.cts +1 -1
  29. package/dist/node.d.ts +1 -1
  30. package/dist/node.js +4 -4
  31. package/dist/permits.d.cts +10 -6
  32. package/dist/permits.d.ts +10 -6
  33. package/dist/permits.js +2 -2
  34. package/dist/web.cjs +100 -32
  35. package/dist/web.d.cts +1 -1
  36. package/dist/web.d.ts +1 -1
  37. package/dist/web.js +4 -4
  38. package/dist/zkProve.worker.js +1 -1
  39. package/package.json +2 -2
  40. package/permits/store.ts +1 -0
  41. package/web/test/ssr.test.ts +23 -0
  42. package/web/test/tfheinit.web.test.ts +81 -5
package/CHANGELOG.md CHANGED
@@ -1,5 +1,35 @@
1
1
  # @cofhe/sdk Changelog
2
2
 
3
+ ## 0.6.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 670cda8: Prepare an alpha snapshot release.
8
+
9
+ ## 0.6.0
10
+
11
+ ### Minor Changes
12
+
13
+ - 566f126: Remove the legacy `TestBed` mock surface and stop auto-deploying `SimpleTest` through the Hardhat plugins. Core mock contracts still deploy automatically, while tests that need `SimpleTest` should deploy it explicitly from their own artifacts.
14
+
15
+ This also removes `TEST_BED_ADDRESS` and cleans up duplicate `SimpleTest` exports from `@cofhe/mock-contracts`.
16
+
17
+ ### Patch Changes
18
+
19
+ - bf23270: Upgrade `zustand` to 5.0.13 to pick up the upstream persist storage fix used by the SDK and React package.
20
+ - 2711f9b: Add hash-plus-proof (HPP) support to `EncryptInputsBuilder`.
21
+
22
+ Calling `.asHashPlusProof()` on the builder transitions its type parameter to `HPP = true`, causing `.execute()` to return `HashPlusProofResult<T>` — a typed tuple of per-input `External*Hash` values followed by a single `ExternalHashProof`, matching the Solidity `externalEbool` / `externalEuint*` / `externalEaddress` type aliases.
23
+
24
+ New public types exported from `@cofhe/sdk`:
25
+
26
+ - `ExternalBoolHash`, `ExternalUint8Hash`, `ExternalUint16Hash`, `ExternalUint32Hash`, `ExternalUint64Hash`, `ExternalUint128Hash`, `ExternalAddressHash` — branded `0x${string}` types discriminated by `utype`
27
+ - `ExternalHashProof` — branded `0x${string}` proof blob
28
+ - `AnyExternalHash` — union of all `External*Hash` types
29
+ - `EncryptableToExternalHashMap<E>` — maps a single `EncryptableItem` to its `External*Hash`
30
+ - `ExternalItemHashes<T>` — maps an `EncryptableItem[]` tuple to the corresponding hash tuple
31
+ - `HashPlusProofResult<T>` — `[...ExternalItemHashes<T>, ExternalHashProof]`
32
+
3
33
  ## 0.5.2
4
34
 
5
35
  ### Patch Changes
@@ -8,8 +8,8 @@ export const hardhat = defineChain({
8
8
  name: 'Hardhat',
9
9
  network: 'localhost',
10
10
  // These are unused in the mock environment
11
- coFheUrl: 'http://127.0.0.1:8448',
12
- verifierUrl: 'http://127.0.0.1:3001',
13
- thresholdNetworkUrl: 'http://127.0.0.1:3000',
11
+ coFheUrl: 'http://ignored-in-mock-environment',
12
+ verifierUrl: 'http://ignored-in-mock-environment',
13
+ thresholdNetworkUrl: 'http://ignored-in-mock-environment',
14
14
  environment: 'MOCK',
15
15
  });
package/core/consts.ts CHANGED
@@ -7,9 +7,6 @@ export const MOCKS_ZK_VERIFIER_ADDRESS = '0x000000000000000000000000000000000000
7
7
  /** Mock Threshold Network contract address (used for testing) */
8
8
  export const MOCKS_THRESHOLD_NETWORK_ADDRESS = '0x0000000000000000000000000000000000005002' as const;
9
9
 
10
- /** Test Bed contract address (used for testing) */
11
- export const TEST_BED_ADDRESS = '0x0000000000000000000000000000000000005003' as const;
12
-
13
10
  /** Private key for the Mock ZK Verifier signer account */
14
11
  export const MOCKS_ZK_VERIFIER_SIGNER_PRIVATE_KEY =
15
12
  '0x6C8D7F768A6BB4AAFE85E8A2F5A9680355239C7E14646ED62B044E39DE154512' as const;
package/core/debug.ts ADDED
@@ -0,0 +1,72 @@
1
+ // Neutral, opt-in debug interceptors around every low-level SDK network request.
2
+ // Purpose-agnostic: a registered handler can observe, rewrite the URL, mutate the
3
+ // request body/init, replace the response, or throw — enabling fault-injection,
4
+ // logging, latency simulation, endpoint redirection, etc. without any
5
+ // scenario-specific code in the SDK.
6
+ //
7
+ // Nothing is active unless an interceptor is registered (setCofheDebugInterceptors),
8
+ // so this is a no-op in production by default.
9
+
10
+ export interface CofheRequestContext {
11
+ /** Coarse label for the call site, e.g. 'decrypt' | 'sealoutput' | 'fetchKeys'. */
12
+ op: string;
13
+ }
14
+
15
+ export interface CofheRequestOverride {
16
+ url?: string;
17
+ init?: RequestInit;
18
+ }
19
+
20
+ export type CofheOnRequest = (
21
+ url: string,
22
+ init: RequestInit | undefined,
23
+ ctx: CofheRequestContext
24
+ ) => CofheRequestOverride | void | Promise<CofheRequestOverride | void>;
25
+
26
+ export type CofheOnResponse = (
27
+ response: Response,
28
+ ctx: CofheRequestContext
29
+ ) => Response | void | Promise<Response | void>;
30
+
31
+ export interface CofheDebugInterceptors {
32
+ onRequest?: CofheOnRequest;
33
+ onResponse?: CofheOnResponse;
34
+ }
35
+
36
+ const interceptors: CofheDebugInterceptors = {};
37
+
38
+ /** Register (or clear, with null) the debug interceptors. */
39
+ export function setCofheDebugInterceptors(next: CofheDebugInterceptors | null): void {
40
+ interceptors.onRequest = next?.onRequest;
41
+ interceptors.onResponse = next?.onResponse;
42
+ }
43
+
44
+ export function getCofheDebugInterceptors(): CofheDebugInterceptors {
45
+ return interceptors;
46
+ }
47
+
48
+ /**
49
+ * fetch() wrapper used by all low-level SDK network ops. Applies the registered
50
+ * debug interceptors when present; otherwise behaves exactly like fetch().
51
+ */
52
+ export async function cofheFetch(
53
+ url: string,
54
+ init?: RequestInit,
55
+ ctx: CofheRequestContext = { op: 'request' }
56
+ ): Promise<Response> {
57
+ let finalUrl = url;
58
+ let finalInit = init;
59
+ if (interceptors.onRequest) {
60
+ const override = await interceptors.onRequest(finalUrl, finalInit, ctx);
61
+ if (override) {
62
+ if (override.url !== undefined) finalUrl = override.url;
63
+ if (override.init !== undefined) finalInit = override.init;
64
+ }
65
+ }
66
+ let response = await fetch(finalUrl, finalInit);
67
+ if (interceptors.onResponse) {
68
+ const replaced = await interceptors.onResponse(response, ctx);
69
+ if (replaced) response = replaced;
70
+ }
71
+ return response;
72
+ }
@@ -1,4 +1,5 @@
1
1
  import { type Permission } from '@/permits';
2
+ import { cofheFetch } from '../debug.js';
2
3
 
3
4
  import { CofheError, CofheErrorCode } from '../error';
4
5
  import { type DecryptPollCallbackFunction } from '../types';
@@ -200,13 +201,16 @@ async function submitDecryptRequestV2(
200
201
  for (;;) {
201
202
  let response: Response;
202
203
  try {
203
- response = await fetch(`${thresholdNetworkUrl}/v2/decrypt`, {
204
- method: 'POST',
205
- headers: {
206
- 'Content-Type': 'application/json',
207
- },
208
- body: JSON.stringify(body),
209
- });
204
+ response = await cofheFetch(
205
+ `${thresholdNetworkUrl}/v2/decrypt`,
206
+ /*op:decrypt*/ {
207
+ method: 'POST',
208
+ headers: {
209
+ 'Content-Type': 'application/json',
210
+ },
211
+ body: JSON.stringify(body),
212
+ }
213
+ );
210
214
  } catch (e) {
211
215
  throw new CofheError({
212
216
  code: CofheErrorCode.DecryptFailed,
@@ -352,12 +356,15 @@ async function pollDecryptStatusV2(
352
356
 
353
357
  let response: Response;
354
358
  try {
355
- response = await fetch(`${thresholdNetworkUrl}/v2/decrypt/${requestId}`, {
356
- method: 'GET',
357
- headers: {
358
- 'Content-Type': 'application/json',
359
- },
360
- });
359
+ response = await cofheFetch(
360
+ `${thresholdNetworkUrl}/v2/decrypt/${requestId}`,
361
+ /*op:decrypt-poll*/ {
362
+ method: 'GET',
363
+ headers: {
364
+ 'Content-Type': 'application/json',
365
+ },
366
+ }
367
+ );
361
368
  } catch (e) {
362
369
  throw new CofheError({
363
370
  code: CofheErrorCode.DecryptFailed,
@@ -1,4 +1,5 @@
1
1
  import { type Permission, type EthEncryptedData } from '@/permits';
2
+ import { cofheFetch } from '../debug.js';
2
3
 
3
4
  import { CofheError, CofheErrorCode } from '../error.js';
4
5
  import { type DecryptPollCallbackFunction } from '../types.js';
@@ -150,13 +151,16 @@ async function submitSealOutputRequest(
150
151
  for (;;) {
151
152
  let response: Response;
152
153
  try {
153
- response = await fetch(`${thresholdNetworkUrl}/v2/sealoutput`, {
154
- method: 'POST',
155
- headers: {
156
- 'Content-Type': 'application/json',
157
- },
158
- body: JSON.stringify(body),
159
- });
154
+ response = await cofheFetch(
155
+ `${thresholdNetworkUrl}/v2/sealoutput`,
156
+ /*op:sealoutput*/ {
157
+ method: 'POST',
158
+ headers: {
159
+ 'Content-Type': 'application/json',
160
+ },
161
+ body: JSON.stringify(body),
162
+ }
163
+ );
160
164
  } catch (e) {
161
165
  throw new CofheError({
162
166
  code: CofheErrorCode.SealOutputFailed,
@@ -303,12 +307,15 @@ async function pollSealOutputStatus(
303
307
 
304
308
  let response: Response;
305
309
  try {
306
- response = await fetch(`${thresholdNetworkUrl}/v2/sealoutput/${requestId}`, {
307
- method: 'GET',
308
- headers: {
309
- 'Content-Type': 'application/json',
310
- },
311
- });
310
+ response = await cofheFetch(
311
+ `${thresholdNetworkUrl}/v2/sealoutput/${requestId}`,
312
+ /*op:sealoutput-poll*/ {
313
+ method: 'GET',
314
+ headers: {
315
+ 'Content-Type': 'application/json',
316
+ },
317
+ }
318
+ );
312
319
  } catch (e) {
313
320
  throw new CofheError({
314
321
  code: CofheErrorCode.SealOutputFailed,
@@ -185,8 +185,8 @@ async function createProofSignatures(
185
185
  items: EncryptableItemWithCtHash[],
186
186
  securityZone: number,
187
187
  account: string
188
- ): Promise<string[]> {
189
- let signatures: string[] = [];
188
+ ): Promise<`0x${string}`[]> {
189
+ let signatures: `0x${string}`[] = [];
190
190
 
191
191
  // Create wallet client for the encrypted input signer
192
192
  // This wallet won't send a transaction, so gas isn't needed
@@ -16,6 +16,7 @@ import {
16
16
  type EncryptedItemInputs,
17
17
  type TfheInitializer,
18
18
  type EncryptStepCallbackContext,
19
+ type HashPlusProofResult,
19
20
  } from '../types.js';
20
21
  import { cofheMocksCheckEncryptableBits, cofheMocksZkVerifySign } from './cofheMocksZkVerifySign.js';
21
22
  import { hardhat } from 'viem/chains';
@@ -47,10 +48,11 @@ type EncryptInputsBuilderParams<T extends EncryptableItem[]> = BaseBuilderParams
47
48
  * config, tfhePublicKeyDeserializer, compactPkeCrsDeserializer, and zkBuilderAndCrsGenerator are required to be set in the builder.
48
49
  */
49
50
 
50
- export class EncryptInputsBuilder<T extends EncryptableItem[]> extends BaseBuilder {
51
+ export class EncryptInputsBuilder<T extends EncryptableItem[], HPP extends boolean = false> extends BaseBuilder {
51
52
  private securityZone: number;
52
53
  private stepCallback?: EncryptStepCallbackFunction;
53
54
  private inputItems: [...T];
55
+ private hpp: boolean = false;
54
56
 
55
57
  private zkvWalletClient: WalletClient | undefined;
56
58
 
@@ -154,7 +156,7 @@ export class EncryptInputsBuilder<T extends EncryptableItem[]> extends BaseBuild
154
156
  *
155
157
  * @returns The chainable EncryptInputsBuilder instance.
156
158
  */
157
- setAccount(account: string): EncryptInputsBuilder<T> {
159
+ setAccount(account: string): EncryptInputsBuilder<T, HPP> {
158
160
  this.account = account;
159
161
  return this;
160
162
  }
@@ -177,7 +179,7 @@ export class EncryptInputsBuilder<T extends EncryptableItem[]> extends BaseBuild
177
179
  *
178
180
  * @returns The chainable EncryptInputsBuilder instance.
179
181
  */
180
- setChainId(chainId: number): EncryptInputsBuilder<T> {
182
+ setChainId(chainId: number): EncryptInputsBuilder<T, HPP> {
181
183
  this.chainId = chainId;
182
184
  return this;
183
185
  }
@@ -200,7 +202,7 @@ export class EncryptInputsBuilder<T extends EncryptableItem[]> extends BaseBuild
200
202
  *
201
203
  * @returns The chainable EncryptInputsBuilder instance.
202
204
  */
203
- setSecurityZone(securityZone: number): EncryptInputsBuilder<T> {
205
+ setSecurityZone(securityZone: number): EncryptInputsBuilder<T, HPP> {
204
206
  this.securityZone = securityZone;
205
207
  return this;
206
208
  }
@@ -209,6 +211,21 @@ export class EncryptInputsBuilder<T extends EncryptableItem[]> extends BaseBuild
209
211
  return this.securityZone;
210
212
  }
211
213
 
214
+ /**
215
+ * Example:
216
+ * ```typescript
217
+ * const encrypted = await encryptInputs([Encryptable.uint128(10n)])
218
+ * .asHashPlusProof()
219
+ * .execute();
220
+ * ```
221
+ *
222
+ * @returns Chainable EncryptInputsBuilder instance that will return a HashPlusProofResult instead of an array of EncryptedItemInputs.
223
+ */
224
+ asHashPlusProof(): EncryptInputsBuilder<T, true> {
225
+ this.hpp = true;
226
+ return this as unknown as EncryptInputsBuilder<T, true>;
227
+ }
228
+
212
229
  /**
213
230
  * @param useWorker - Whether to use Web Workers for ZK proof generation.
214
231
  *
@@ -223,7 +240,7 @@ export class EncryptInputsBuilder<T extends EncryptableItem[]> extends BaseBuild
223
240
  *
224
241
  * @returns The chainable EncryptInputsBuilder instance.
225
242
  */
226
- setUseWorker(useWorker: boolean): EncryptInputsBuilder<T> {
243
+ setUseWorker(useWorker: boolean): EncryptInputsBuilder<T, HPP> {
227
244
  this.useWorker = useWorker;
228
245
  return this;
229
246
  }
@@ -260,7 +277,7 @@ export class EncryptInputsBuilder<T extends EncryptableItem[]> extends BaseBuild
260
277
  *
261
278
  * @returns The EncryptInputsBuilder instance.
262
279
  */
263
- onStep(callback: EncryptStepCallbackFunction): EncryptInputsBuilder<T> {
280
+ onStep(callback: EncryptStepCallbackFunction): EncryptInputsBuilder<T, HPP> {
264
281
  this.stepCallback = callback;
265
282
  return this;
266
283
  }
@@ -541,7 +558,7 @@ export class EncryptInputsBuilder<T extends EncryptableItem[]> extends BaseBuild
541
558
  const verifyResults = await zkVerify(zkVerifierUrl, proof, this.account, this.securityZone, this.chainId);
542
559
  // Add securityZone and utype to the verify results
543
560
  const encryptedInputs: EncryptedItemInput[] = verifyResults.map(
544
- ({ ct_hash, signature }: { ct_hash: string; signature: string }, index: number) => ({
561
+ ({ ct_hash, signature }: { ct_hash: string; signature: `0x${string}` }, index: number) => ({
545
562
  ctHash: BigInt(ct_hash),
546
563
  securityZone: this.securityZone,
547
564
  utype: this.inputItems[index].utype,
@@ -554,6 +571,18 @@ export class EncryptInputsBuilder<T extends EncryptableItem[]> extends BaseBuild
554
571
  return encryptedInputs as [...EncryptedItemInputs<T>];
555
572
  }
556
573
 
574
+ private structsToHashPlusProof(inItems: [...EncryptedItemInputs<T>]): HashPlusProofResult<T> {
575
+ let hashes: string[] = [];
576
+ let proof: string = '';
577
+
578
+ for (const item of inItems) {
579
+ hashes.push('0x' + item.ctHash.toString(16).padStart(64, '0'));
580
+ proof += item.signature;
581
+ }
582
+
583
+ return [...hashes, proof] as unknown as HashPlusProofResult<T>;
584
+ }
585
+
557
586
  /**
558
587
  * Final step of the encryption process. MUST BE CALLED LAST IN THE CHAIN.
559
588
  *
@@ -573,11 +602,17 @@ export class EncryptInputsBuilder<T extends EncryptableItem[]> extends BaseBuild
573
602
  *
574
603
  * @returns The encrypted inputs.
575
604
  */
576
- async execute(): Promise<[...EncryptedItemInputs<T>]> {
577
- // On hardhat chain, interact with MockZkVerifier contract instead of CoFHE
578
- if (this.chainId === hardhat.id) return this.mocksExecute();
605
+ async execute(): Promise<HPP extends true ? HashPlusProofResult<T> : [...EncryptedItemInputs<T>]> {
606
+ type Result = HPP extends true ? HashPlusProofResult<T> : [...EncryptedItemInputs<T>];
607
+
608
+ let items: [...EncryptedItemInputs<T>];
579
609
 
610
+ // On hardhat chain, interact with MockZkVerifier contract instead of CoFHE
611
+ if (this.chainId === hardhat.id) items = await this.mocksExecute();
580
612
  // On other chains, interact with CoFHE coprocessor
581
- return this.productionExecute();
613
+ else items = await this.productionExecute();
614
+
615
+ if (this.hpp) return this.structsToHashPlusProof(items) as unknown as Result;
616
+ return items as unknown as Result;
582
617
  }
583
618
  }
@@ -49,7 +49,7 @@ export type VerifyResultRaw = {
49
49
 
50
50
  export type VerifyResult = {
51
51
  ct_hash: string;
52
- signature: string;
52
+ signature: `0x${string}`;
53
53
  };
54
54
 
55
55
  export type ZkProvenCiphertextList = {
@@ -321,6 +321,6 @@ export const zkVerify = async (
321
321
  }
322
322
  };
323
323
 
324
- const concatSigRecid = (signature: string, recid: number): string => {
325
- return signature + (recid + 27).toString(16).padStart(2, '0');
324
+ const concatSigRecid = (signature: string, recid: number): `0x${string}` => {
325
+ return `${signature}${(recid + 27).toString(16).padStart(2, '0')}` as `0x${string}`;
326
326
  };
package/core/index.ts CHANGED
@@ -1,6 +1,16 @@
1
1
  // Client (base implementations)
2
2
  export { createCofheClientBase, InitialConnectStore as CONNECT_STORE_DEFAULTS } from './client.js';
3
3
 
4
+ // Debug interceptors (neutral, opt-in network hooks for fault injection / logging)
5
+ export { setCofheDebugInterceptors, getCofheDebugInterceptors, cofheFetch } from './debug.js';
6
+ export type {
7
+ CofheDebugInterceptors,
8
+ CofheOnRequest,
9
+ CofheOnResponse,
10
+ CofheRequestContext,
11
+ CofheRequestOverride,
12
+ } from './debug.js';
13
+
4
14
  // Configuration (base implementations)
5
15
  export { createCofheConfigBase, getCofheConfigItem } from './config.js';
6
16
  export type { CofheConfig, CofheInputConfig, CofheInternalConfig } from './config.js';
@@ -41,6 +51,19 @@ export type {
41
51
  EncryptedItemInputs,
42
52
  EncryptableToEncryptedItemInputMap,
43
53
  FheTypeValue,
54
+ // Hash-plus-proof external types
55
+ ExternalBoolHash,
56
+ ExternalUint8Hash,
57
+ ExternalUint16Hash,
58
+ ExternalUint32Hash,
59
+ ExternalUint64Hash,
60
+ ExternalUint128Hash,
61
+ ExternalAddressHash,
62
+ ExternalHashProof,
63
+ AnyExternalHash,
64
+ EncryptableToExternalHashMap,
65
+ ExternalItemHashes,
66
+ HashPlusProofResult,
44
67
  // Decryption types
45
68
  UnsealedItem,
46
69
  DecryptPollCallbackFunction,
@@ -96,7 +119,6 @@ export {
96
119
  MOCKS_ZK_VERIFIER_SIGNER_PRIVATE_KEY,
97
120
  MOCKS_DECRYPT_RESULT_SIGNER_PRIVATE_KEY,
98
121
  MOCKS_THRESHOLD_NETWORK_ADDRESS,
99
- TEST_BED_ADDRESS,
100
122
  TFHE_RS_ZK_MAX_BITS,
101
123
  TFHE_RS_SAFE_SERIALIZATION_SIZE_LIMIT,
102
124
  } from './consts.js';
package/core/permits.ts CHANGED
@@ -112,13 +112,18 @@ const selectActivePermit = (chainId: number, account: string, hash: string): voi
112
112
  // GET OR CREATE
113
113
 
114
114
  /**
115
- * Get the active self permit or create a new one if it doesn't exist
115
+ * Get the active self permit if a valid one exists, otherwise create a new one.
116
+ *
117
+ * An active permit is reused only when it is a self permit and is still valid
118
+ * (signed and not expired). An expired or otherwise invalid active permit is
119
+ * treated as missing and a fresh permit is created.
120
+ *
116
121
  * @param publicClient - The public client
117
122
  * @param walletClient - The wallet client
118
123
  * @param chainId - Optional chain ID (will use publicClient if not provided)
119
124
  * @param account - Optional account (will use walletClient if not provided)
120
125
  * @param options - The options for creating a self permit
121
- * @returns The existing or newly created permit
126
+ * @returns The existing valid permit or a newly created one
122
127
  */
123
128
  const getOrCreateSelfPermit = async (
124
129
  publicClient: PublicClient,
@@ -133,22 +138,27 @@ const getOrCreateSelfPermit = async (
133
138
  // Try to get active permit first
134
139
  const activePermit = await getActivePermit(_chainId, _account);
135
140
 
136
- if (activePermit && activePermit.type === 'self') {
141
+ if (activePermit && activePermit.type === 'self' && PermitUtils.isValid(activePermit).valid) {
137
142
  return activePermit;
138
143
  }
139
144
 
140
- // No active permit or wrong type, create new one
145
+ // No active permit, wrong type, or expired/invalid - create new one
141
146
  return createSelf(options ?? { issuer: _account, name: 'Autogenerated Self Permit' }, publicClient, walletClient);
142
147
  };
143
148
 
144
149
  /**
145
- * Get the active sharing permit or create a new one if it doesn't exist
150
+ * Get the active sharing permit if a valid one exists, otherwise create a new one.
151
+ *
152
+ * An active permit is reused only when it is a sharing permit and is still valid
153
+ * (signed and not expired). An expired or otherwise invalid active permit is
154
+ * treated as missing and a fresh permit is created.
155
+ *
146
156
  * @param publicClient - The public client
147
157
  * @param walletClient - The wallet client
148
158
  * @param options - The options for creating a sharing permit (required)
149
159
  * @param chainId - Optional chain ID (will use publicClient if not provided)
150
160
  * @param account - Optional account (will use walletClient if not provided)
151
- * @returns The existing or newly created permit
161
+ * @returns The existing valid permit or a newly created one
152
162
  */
153
163
  const getOrCreateSharingPermit = async (
154
164
  publicClient: PublicClient,
@@ -163,7 +173,7 @@ const getOrCreateSharingPermit = async (
163
173
  // Try to get active permit first
164
174
  const activePermit = await getActivePermit(_chainId, _account);
165
175
 
166
- if (activePermit && activePermit.type === 'sharing') {
176
+ if (activePermit && activePermit.type === 'sharing' && PermitUtils.isValid(activePermit).valid) {
167
177
  return activePermit;
168
178
  }
169
179
 
@@ -744,6 +744,41 @@ describe('EncryptInputsBuilder', () => {
744
744
  });
745
745
  });
746
746
 
747
+ describe('asHashPlusProof', () => {
748
+ it('should return the same builder instance for chaining', () => {
749
+ expect(builder.asHashPlusProof()).toBe(builder);
750
+ });
751
+
752
+ it('execute() returns [hash, proof] for a single input', async () => {
753
+ const result = await builder.asHashPlusProof().execute();
754
+ expect(Array.isArray(result)).toBe(true);
755
+ expect(result).toHaveLength(2); // 1 hash + 1 proof
756
+ });
757
+
758
+ it('execute() returns [hash1, hash2, proof] for two inputs', async () => {
759
+ const result = await new EncryptInputsBuilder({
760
+ ...createDefaultParams(),
761
+ inputs: [Encryptable.uint128(100n), Encryptable.bool(true)] as [
762
+ ReturnType<typeof Encryptable.uint128>,
763
+ ReturnType<typeof Encryptable.bool>,
764
+ ],
765
+ })
766
+ .asHashPlusProof()
767
+ .execute();
768
+
769
+ expect(Array.isArray(result)).toBe(true);
770
+ expect(result).toHaveLength(3); // 2 hashes + 1 proof
771
+ });
772
+
773
+ it('should be composable with other builder methods', async () => {
774
+ const overriddenSender = '0x5555555555555555555555555555555555555555';
775
+ const result = await builder.asHashPlusProof().setAccount(overriddenSender).execute();
776
+
777
+ expect(Array.isArray(result)).toBe(true);
778
+ expect(result).toHaveLength(2);
779
+ });
780
+ });
781
+
747
782
  describe('setUseWorker and getUseWorker', () => {
748
783
  it('should have setUseWorker method', () => {
749
784
  expect(builder).toHaveProperty('setUseWorker');
@@ -304,6 +304,33 @@ describe('Core Permits Tests', () => {
304
304
  expect(Object.keys(allPermits).length).toBe(2);
305
305
  });
306
306
 
307
+ it('should create a new self permit when active permit is expired', async () => {
308
+ // Create an expired self permit (expiration in the past)
309
+ const expiredPermit = await permits.createSelf(
310
+ { name: 'Expired Self Permit', issuer: bobAddress, expiration: Math.floor(Date.now() / 1000) - 3600 },
311
+ publicClient,
312
+ bobWalletClient
313
+ );
314
+
315
+ // Sanity check - it is the active permit and is expired
316
+ const activeBefore = await permits.getActivePermit(chainId, bobAddress);
317
+ expect(activeBefore?.hash).toBe(expiredPermit.hash);
318
+
319
+ // getOrCreateSelfPermit should treat the expired permit as missing and create a fresh one
320
+ const permit = await permits.getOrCreateSelfPermit(publicClient, bobWalletClient, chainId, bobAddress, {
321
+ issuer: bobAddress,
322
+ name: 'Fresh Self Permit',
323
+ });
324
+
325
+ expect(permit.name).toBe('Fresh Self Permit');
326
+ expect(permit.type).toBe('self');
327
+ expect(permit.hash).not.toBe(expiredPermit.hash);
328
+
329
+ // The fresh permit should now be active
330
+ const activeAfter = await permits.getActivePermit(chainId, bobAddress);
331
+ expect(activeAfter?.hash).toBe(permit.hash);
332
+ });
333
+
307
334
  it('should use default options when none provided', async () => {
308
335
  const permit = await permits.getOrCreateSelfPermit(publicClient, bobWalletClient, chainId, bobAddress);
309
336
 
@@ -412,6 +439,41 @@ describe('Core Permits Tests', () => {
412
439
  expect(Object.keys(allPermits).length).toBe(2);
413
440
  });
414
441
 
442
+ it('should create a new sharing permit when active permit is expired', async () => {
443
+ // Create an expired sharing permit (expiration in the past)
444
+ const expiredPermit = await permits.createSharing(
445
+ {
446
+ name: 'Expired Sharing Permit',
447
+ issuer: bobAddress,
448
+ recipient: aliceAddress,
449
+ expiration: Math.floor(Date.now() / 1000) - 3600,
450
+ },
451
+ publicClient,
452
+ bobWalletClient
453
+ );
454
+
455
+ // getOrCreateSharingPermit should treat the expired permit as missing and create a fresh one
456
+ const permit = await permits.getOrCreateSharingPermit(
457
+ publicClient,
458
+ bobWalletClient,
459
+ {
460
+ issuer: bobAddress,
461
+ recipient: aliceAddress,
462
+ name: 'Fresh Sharing Permit',
463
+ },
464
+ chainId,
465
+ bobAddress
466
+ );
467
+
468
+ expect(permit.name).toBe('Fresh Sharing Permit');
469
+ expect(permit.type).toBe('sharing');
470
+ expect(permit.hash).not.toBe(expiredPermit.hash);
471
+
472
+ // The fresh permit should now be active
473
+ const activeAfter = await permits.getActivePermit(chainId, bobAddress);
474
+ expect(activeAfter?.hash).toBe(permit.hash);
475
+ });
476
+
415
477
  it('should use default chainId and account when not provided', async () => {
416
478
  const permit = await permits.getOrCreateSharingPermit(
417
479
  publicClient,