@cofhe/sdk 0.7.1 → 1.0.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/acps/acp.ts +35 -7
  3. package/acps/index.ts +28 -12
  4. package/acps/onchain-utils.ts +188 -49
  5. package/acps/registry.ts +231 -0
  6. package/acps/sealing.ts +9 -3
  7. package/acps/share-metadata/appKey.ts +8 -0
  8. package/acps/share-metadata/decode.ts +120 -0
  9. package/acps/share-metadata/describe.ts +198 -0
  10. package/acps/share-metadata/encode.ts +147 -0
  11. package/acps/share-metadata/erc7984.ts +26 -0
  12. package/acps/share-metadata/index.ts +9 -0
  13. package/acps/share-metadata/layout.ts +69 -0
  14. package/acps/share-metadata/presets.ts +116 -0
  15. package/acps/share-metadata/schema.ts +47 -0
  16. package/acps/share-metadata/test/codec.test.ts +140 -0
  17. package/acps/share-metadata/test/describe.test.ts +112 -0
  18. package/acps/share-metadata/test/fixtures.ts +157 -0
  19. package/acps/share-metadata/test/live.test.ts +56 -0
  20. package/acps/share-metadata/test/presets.test.ts +119 -0
  21. package/acps/share-metadata/test/verify.test.ts +144 -0
  22. package/acps/share-metadata/verify.ts +155 -0
  23. package/acps/store.ts +48 -18
  24. package/acps/test/acp.test.ts +102 -7
  25. package/acps/test/localstorage.test.ts +28 -1
  26. package/acps/test/onchainAccessStatus.test.ts +116 -0
  27. package/acps/test/registry.test.ts +144 -0
  28. package/acps/test/sealing.test.ts +23 -0
  29. package/acps/test/store.test.ts +71 -0
  30. package/acps/test/utils.test.ts +63 -0
  31. package/acps/test/validation.test.ts +0 -4
  32. package/acps/types.ts +59 -6
  33. package/acps/utils.ts +15 -2
  34. package/acps/validation.ts +3 -1
  35. package/chains/chains/arbitrum.ts +14 -0
  36. package/chains/chains/ethereum.ts +14 -0
  37. package/chains/index.ts +5 -1
  38. package/chains/test/chains.test.ts +24 -3
  39. package/core/acps.ts +164 -207
  40. package/core/client.ts +44 -13
  41. package/core/clientTypes.ts +50 -7
  42. package/core/config.ts +33 -1
  43. package/core/consts.ts +14 -2
  44. package/core/decrypt/apiError.ts +4 -1
  45. package/core/decrypt/verifyDecryptResult.ts +8 -4
  46. package/core/encrypt/cofheMocksZkVerifySign.ts +19 -2
  47. package/core/encrypt/encryptInputsBuilder.ts +6 -0
  48. package/core/encrypt/zkPackProveVerify.ts +7 -1
  49. package/core/error.ts +1 -0
  50. package/core/fetchKeys.ts +2 -2
  51. package/core/index.ts +9 -1
  52. package/core/keyStore.ts +23 -10
  53. package/core/taskManager.ts +48 -0
  54. package/core/test/acpDefaults.test.ts +108 -19
  55. package/core/test/acpStoreAccountCase.test.ts +80 -0
  56. package/core/test/acps.test.ts +128 -3
  57. package/core/test/apiError.test.ts +8 -0
  58. package/core/test/appKeyConfig.test.ts +94 -0
  59. package/core/test/client.test.ts +41 -0
  60. package/core/test/config.test.ts +24 -0
  61. package/core/test/decrypt.test.ts +11 -4
  62. package/core/test/decryptBuilders.test.ts +110 -2
  63. package/core/test/encryptInputsBuilder.test.ts +1 -1
  64. package/core/test/fetchKeys.test.ts +23 -2
  65. package/core/test/incomingShares.test.ts +84 -0
  66. package/core/test/keyStore.test.ts +18 -6
  67. package/core/test/mocksEncryptRange.test.ts +90 -0
  68. package/core/test/taskManager.test.ts +57 -0
  69. package/core/test/utils.test.ts +128 -0
  70. package/core/test/verifyDecryptResult.test.ts +51 -0
  71. package/core/types.ts +8 -0
  72. package/core/utils.ts +16 -9
  73. package/dist/acps.cjs +984 -64
  74. package/dist/acps.d.cts +589 -10
  75. package/dist/acps.d.ts +589 -10
  76. package/dist/acps.js +2 -2
  77. package/dist/chains.cjs +26 -0
  78. package/dist/chains.d.cts +45 -1
  79. package/dist/chains.d.ts +45 -1
  80. package/dist/chains.js +1 -1
  81. package/dist/chunk-6ZW76ZKB.js +228 -0
  82. package/dist/chunk-BQWHPIYU.js +1828 -0
  83. package/dist/chunk-KSOV5B4B.js +50 -0
  84. package/dist/{chunk-43USWPEH.js → chunk-OL3DZN7A.js} +159 -523
  85. package/dist/{chunk-N6IDQRRU.js → chunk-UKQAJK4N.js} +25 -1
  86. package/dist/chunk-XQ32JG3T.js +174 -0
  87. package/dist/{clientTypes-CYZjFznO.d.cts → clientTypes-C0A9ji5O.d.cts} +132 -29
  88. package/dist/{clientTypes-BN3nbzYM.d.ts → clientTypes-CcU5WvdV.d.ts} +132 -29
  89. package/dist/core.cjs +3269 -2626
  90. package/dist/core.d.cts +32 -8
  91. package/dist/core.d.ts +32 -8
  92. package/dist/core.js +5 -4
  93. package/dist/node.cjs +3074 -2436
  94. package/dist/node.d.cts +2 -2
  95. package/dist/node.d.ts +2 -2
  96. package/dist/node.js +20 -19
  97. package/dist/{acp-Wi6isVQI.d.cts → onchain-utils-D9Z33IP7.d.cts} +166 -12
  98. package/dist/{acp-Wi6isVQI.d.ts → onchain-utils-D9Z33IP7.d.ts} +166 -12
  99. package/dist/web.cjs +3204 -2493
  100. package/dist/web.d.cts +49 -3
  101. package/dist/web.d.ts +49 -3
  102. package/dist/web.js +52 -27
  103. package/dist/zkProve.worker.cjs +79 -18
  104. package/dist/zkProve.worker.js +24 -24
  105. package/node/index.ts +1 -10
  106. package/node/storage.ts +20 -9
  107. package/node/test/inherited.test.ts +5 -1
  108. package/node/test/storage-memory-fallback.test.ts +47 -0
  109. package/node/test/storage-permissions.test.ts +40 -0
  110. package/package.json +3 -1
  111. package/web/index.ts +101 -36
  112. package/web/test/countingZkProveWorker.ts +31 -0
  113. package/web/test/inherited.web.test.ts +5 -1
  114. package/web/test/mainThreadPool.web.test.ts +174 -0
  115. package/web/test/threadPoolBenchHelper.ts +134 -0
  116. package/web/test/threadPoolOff.web.test.ts +17 -0
  117. package/web/test/threadPoolOn.web.test.ts +80 -0
  118. package/web/test/workerInitRace.web.test.ts +79 -0
  119. package/web/tfheThreadPool.ts +140 -0
  120. package/web/workerManager.ts +4 -2
  121. package/web/zkProve.worker.ts +42 -26
  122. package/dist/chunk-ESMZCFJY.js +0 -12
  123. package/dist/chunk-Q7CBWGQX.js +0 -1029
package/core/acps.ts CHANGED
@@ -11,26 +11,22 @@ import {
11
11
  type IncomingShare,
12
12
  type SharingACP,
13
13
  type ACPHashFields,
14
+ type ACPAccessStatus,
15
+ ACP_REVOKER_ABI,
16
+ ACP_SHARE_REGISTRY_ABI,
17
+ toChainShare,
18
+ computeShareId,
19
+ readSharesFor,
20
+ readShare,
21
+ decodeShareAppKey,
22
+ encodeShareMetadata,
23
+ type PostedShare,
24
+ type ShareLabel,
25
+ getAclServedAddresses,
26
+ clearAclServedAddresses,
14
27
  } from '@/acps';
15
28
 
16
- import {
17
- type Hex,
18
- type PublicClient,
19
- type WalletClient,
20
- encodeAbiParameters,
21
- keccak256,
22
- parseAbi,
23
- zeroAddress,
24
- } from 'viem';
25
-
26
- import { TASK_MANAGER_ADDRESS } from './consts.js';
27
-
28
- // ACP default revoker (timestamp-based revocation) — interface shared by all revokers
29
- const ACP_VALIDATOR_ABI = parseAbi([
30
- 'function revokeSingle(uint256 id)',
31
- 'function revokeAllExisting()',
32
- 'function disabled(address issuer, uint256 id) view returns (bool)',
33
- ]);
29
+ import { type Hex, type PublicClient, type WalletClient, zeroAddress } from 'viem';
34
30
 
35
31
  // HELPERS
36
32
 
@@ -51,9 +47,10 @@ const storeActiveACP = async (acp: ACP, publicClient: any, walletClient: any) =>
51
47
  };
52
48
 
53
49
  // Generic function to handle acp creation with error handling.
54
- // `activate` controls whether the new acp becomes the issuer's active acp — true for
55
- // self/imported acps (the connected user decrypts with them), false for sharing acps (those
56
- // are delegated to a recipient and are never the issuer's own active acp).
50
+ // `activate` controls whether the new acp becomes the connected account's active acp — true for
51
+ // self acps and, by default, imported acps (the connected user decrypts with them); false for
52
+ // sharing acps (delegated to a recipient, never the issuer's own active acp) and for imports
53
+ // made with `activate: false`.
57
54
  const createACPWithSign = async <T, TACP extends ACP>(
58
55
  options: T,
59
56
  publicClient: PublicClient,
@@ -96,12 +93,38 @@ const createSharing = async (
96
93
  return createACPWithSign(options, publicClient, walletClient, ACPUtils.createSharingAndSign, false);
97
94
  };
98
95
 
96
+ /** Options for importing a shared acp. */
97
+ export type ImportActivationOptions = {
98
+ /**
99
+ * Make the imported acp the active one. Defaults to `true`.
100
+ * With `false` the acp is only stored: decrypts that use the active acp keep using the current one,
101
+ * and the imported acp is used only where it is passed explicitly (e.g. `.withACP(acp)`).
102
+ */
103
+ activate?: boolean;
104
+ };
105
+
106
+ /** Options for importing a shared acp: activation, and the recipient's own name for it. */
107
+ export type ImportSharedOptions = ImportActivationOptions & {
108
+ /**
109
+ * The name of the imported acp. Defaults to "Shared by 0x1234…abcd" (its issuer); the issuer's
110
+ * own name for the share is not exported and is ignored if present.
111
+ */
112
+ name?: string;
113
+ };
114
+
99
115
  const importShared = async (
100
116
  options: ImportSharedACPOptions | string,
101
117
  publicClient: PublicClient,
102
- walletClient: WalletClient
118
+ walletClient: WalletClient,
119
+ { activate = true, name }: ImportSharedOptions = {}
103
120
  ): Promise<RecipientACP> => {
104
- return createACPWithSign(options, publicClient, walletClient, ACPUtils.importSharedAndSign);
121
+ return createACPWithSign(
122
+ options,
123
+ publicClient,
124
+ walletClient,
125
+ (o, pc, wc) => ACPUtils.importSharedAndSign(o, pc, wc, { name }),
126
+ activate
127
+ );
105
128
  };
106
129
 
107
130
  // ACP UTILS
@@ -219,15 +242,14 @@ const getOrCreateSharingACP = async (
219
242
  // CONFIG DEFAULTS
220
243
 
221
244
  /**
222
- * Applies the config's ACP acp defaults to creation options (pure).
223
- * Explicit user options always win:
224
- * - revoker: injected only when the options carry NO revoker pair —
225
- * revokerContract = config default, revokerData = creation timestamp
226
- * ("every acp revocable by default")
245
+ * Applies the config's ACP defaults to creation options. Explicit user options always win:
246
+ * - revoker: injected only when the options carry NO revoker pair. The contract is the
247
+ * config's `defaultRevoker` for the chain, else the one the ACL serves; the id
248
+ * (`revokerData`) is the latest block's timestamp ("every acp revocable by default")
227
249
  * - contracts: injected only when the options carry NO scope fields at all —
228
250
  * injecting scope makes the created acp non-global by default
229
251
  */
230
- const applyACPDefaults = <
252
+ const applyACPDefaultsFromChain = async <
231
253
  T extends {
232
254
  revokerData?: number;
233
255
  revokerContract?: string;
@@ -238,20 +260,19 @@ const applyACPDefaults = <
238
260
  >(
239
261
  options: T,
240
262
  acpConfig: { defaultRevoker?: Record<number, Hex>; defaultContractScopes?: Record<number, Hex[]> } | undefined,
263
+ publicClient: PublicClient,
241
264
  chainId: number
242
- ): T => {
265
+ ): Promise<T> => {
243
266
  const result = { ...options };
244
267
 
245
- const defaultRevoker = acpConfig?.defaultRevoker?.[chainId];
246
- const hasValidatorOptions = options.revokerData != null || options.revokerContract != null;
247
- if (defaultRevoker != null && !hasValidatorOptions) {
248
- result.revokerContract = defaultRevoker;
249
- // Creation timestamp minus a clock-skew allowance: the revoker rejects
250
- // future-dated ids (vs block.timestamp of the LAST block), so a local clock
251
- // ahead of the chain — or a chain with sparse blocks — would otherwise make
252
- // a fresh acp temporarily unusable. 60s of backdating costs nothing
253
- // (revokeAllExisting at time T still kills this acp for any T >= id).
254
- result.revokerData = Math.round(Date.now() / 1000) - 60;
268
+ const hasRevokerOptions = options.revokerData != null || options.revokerContract != null;
269
+ if (!hasRevokerOptions) {
270
+ const defaultRevoker =
271
+ acpConfig?.defaultRevoker?.[chainId] ?? (await getAclServedAddresses(publicClient, chainId)).defaultRevoker;
272
+ if (defaultRevoker != null) {
273
+ result.revokerContract = defaultRevoker;
274
+ result.revokerData = await getBlockRevokerData(publicClient);
275
+ }
255
276
  }
256
277
 
257
278
  const defaultContracts = acpConfig?.defaultContractScopes?.[chainId];
@@ -263,94 +284,21 @@ const applyACPDefaults = <
263
284
  return result;
264
285
  };
265
286
 
266
- // ACL-SERVED ADDRESSES (defaultRevokerContract / shareRegistry)
267
-
268
- const ACL_SERVED_ADDRESSES_ABI = parseAbi([
269
- 'function acl() view returns (address)',
270
- 'function defaultRevokerContract() view returns (address)',
271
- 'function shareRegistry() view returns (address)',
272
- ]);
273
-
274
- export interface AclServedAddresses {
275
- defaultRevoker?: Hex;
276
- shareRegistry?: Hex;
277
- }
278
-
279
- const aclServedAddressesCache = new Map<number, AclServedAddresses>();
280
-
281
- /** Test hook: forget resolved addresses (e.g. between redeployments on one chainId). */
282
- const clearAclServedAddresses = () => aclServedAddressesCache.clear();
283
-
284
287
  /**
285
- * The ACP infrastructure addresses the chain's ACL serves (TaskManager -> acl()
286
- * -> getters). Zero addresses and pre-upgrade ACLs (getters absent -> revert)
287
- * resolve to `undefined` — callers fall back to `acp.*` config.
288
- *
289
- * Resolutions are cached per chainId. A failure to reach the TaskManager (network
290
- * error, no CoFHE deployment) is NOT cached, so a transient outage does not pin
291
- * an empty result for the whole session.
288
+ * The chain-aware revoker id source: the latest block timestamp, not the local
289
+ * clock. The revoker rejects future-dated ids against `block.timestamp` of the
290
+ * LAST block, so a local clock ahead of the chain (or a chain with sparse
291
+ * blocks) would otherwise make a freshly created acp temporarily unusable.
292
292
  */
293
- const getAclServedAddresses = async (publicClient: PublicClient, chainId: number): Promise<AclServedAddresses> => {
294
- const cached = aclServedAddressesCache.get(chainId);
295
- if (cached != null) return cached;
296
-
297
- let aclAddress: Hex;
298
- try {
299
- aclAddress = await publicClient.readContract({
300
- address: TASK_MANAGER_ADDRESS,
301
- abi: ACL_SERVED_ADDRESSES_ABI,
302
- functionName: 'acl',
303
- });
304
- } catch {
305
- return {};
293
+ const getBlockRevokerData = async (publicClient: PublicClient): Promise<number> => {
294
+ const block = await publicClient.getBlock();
295
+ const revokerData = Number(block.timestamp);
296
+ if (!Number.isSafeInteger(revokerData)) {
297
+ throw new Error(
298
+ `ACP revokerData cannot represent the chain block timestamp ${block.timestamp.toString()} as a safe integer`
299
+ );
306
300
  }
307
-
308
- const [defaultRevoker, shareRegistry] = await Promise.all([
309
- publicClient
310
- .readContract({ address: aclAddress, abi: ACL_SERVED_ADDRESSES_ABI, functionName: 'defaultRevokerContract' })
311
- .catch(() => undefined),
312
- publicClient
313
- .readContract({ address: aclAddress, abi: ACL_SERVED_ADDRESSES_ABI, functionName: 'shareRegistry' })
314
- .catch(() => undefined),
315
- ]);
316
-
317
- const resolved: AclServedAddresses = {
318
- defaultRevoker: defaultRevoker != null && defaultRevoker !== zeroAddress ? defaultRevoker : undefined,
319
- shareRegistry: shareRegistry != null && shareRegistry !== zeroAddress ? shareRegistry : undefined,
320
- };
321
- aclServedAddressesCache.set(chainId, resolved);
322
- return resolved;
323
- };
324
-
325
- /**
326
- * `applyACPDefaults` with the ACL consulted for the default revoker when
327
- * `acp.defaultRevoker` config does not name one for this chain — explicit
328
- * config wins over the ACL-served address.
329
- */
330
- const applyACPDefaultsFromChain = async <
331
- T extends {
332
- revokerData?: number;
333
- revokerContract?: string;
334
- scope?: number;
335
- contracts?: string[];
336
- handles?: (bigint | number | string)[];
337
- },
338
- >(
339
- options: T,
340
- acpConfig: { defaultRevoker?: Record<number, Hex>; defaultContractScopes?: Record<number, Hex[]> } | undefined,
341
- publicClient: PublicClient,
342
- chainId: number
343
- ): Promise<T> => {
344
- const hasExplicitRevoker =
345
- acpConfig?.defaultRevoker?.[chainId] != null || options.revokerData != null || options.revokerContract != null;
346
- if (hasExplicitRevoker) return applyACPDefaults(options, acpConfig, chainId);
347
-
348
- const served = await getAclServedAddresses(publicClient, chainId);
349
- const effectiveConfig =
350
- served.defaultRevoker != null
351
- ? { ...acpConfig, defaultRevoker: { ...acpConfig?.defaultRevoker, [chainId]: served.defaultRevoker } }
352
- : acpConfig;
353
- return applyACPDefaults(options, effectiveConfig, chainId);
301
+ return revokerData;
354
302
  };
355
303
 
356
304
  // REVOKE (on-chain, via the acp's revoker contract)
@@ -374,7 +322,7 @@ const revokeACP = async (acp: ACP, walletClient: WalletClient): Promise<Hex> =>
374
322
 
375
323
  return walletClient.writeContract({
376
324
  address: acp.revokerContract,
377
- abi: ACP_VALIDATOR_ABI,
325
+ abi: ACP_REVOKER_ABI,
378
326
  functionName: 'revokeSingle',
379
327
  args: [BigInt(acp.revokerData)],
380
328
  account: walletClient.account,
@@ -410,7 +358,7 @@ const revokeAllACPs = async (
410
358
 
411
359
  return walletClient.writeContract({
412
360
  address: revoker,
413
- abi: ACP_VALIDATOR_ABI,
361
+ abi: ACP_REVOKER_ABI,
414
362
  functionName: 'revokeAllExisting',
415
363
  args: [],
416
364
  account: walletClient.account,
@@ -418,6 +366,20 @@ const revokeAllACPs = async (
418
366
  });
419
367
  };
420
368
 
369
+ /**
370
+ * The ACP on-chain status as a value instead of a revert (see `ACPAccessStatus`): validity
371
+ * without a handle, and whether the ACP may read `handle` with one.
372
+ */
373
+ const checkAccess = async (acp: ACP, publicClient: PublicClient, handle?: bigint | Hex): Promise<ACPAccessStatus> => {
374
+ // The issuer half of a share carries no recipient signature (the recipient adds it on import), so
375
+ // the ACL check would always fail it. What can change for the issuer is expiry and revocation.
376
+ if (acp.type === 'sharing' && handle === undefined) {
377
+ if (ACPUtils.isExpired(acp)) return 'expired';
378
+ return (await isACPRevoked(acp, publicClient)) ? 'revoked' : 'valid';
379
+ }
380
+ return ACPUtils.checkAccessOnChain(acp, publicClient, handle);
381
+ };
382
+
421
383
  /**
422
384
  * Check whether an ACP has been revoked (or is otherwise disabled) by its
423
385
  * revoker. Returns false for acps without a revoker (not revocable).
@@ -426,73 +388,39 @@ const isACPRevoked = async (acp: ACP, publicClient: PublicClient): Promise<boole
426
388
  if (acp.revokerContract === zeroAddress || acp.revokerData === 0) return false;
427
389
  return publicClient.readContract({
428
390
  address: acp.revokerContract,
429
- abi: ACP_VALIDATOR_ABI,
391
+ abi: ACP_REVOKER_ABI,
430
392
  functionName: 'disabled',
431
393
  args: [acp.issuer, BigInt(acp.revokerData)],
432
394
  });
433
395
  };
434
396
 
435
- // SHARE (on-chain, via the ACPShareRegistry)
436
-
437
- const ACP_SHARE_REGISTRY_ABI = parseAbi([
438
- 'struct ACP { address issuer; uint64 expiration; address recipient; uint256 revokerData; address revokerContract; uint8 scope; address[] contracts; bytes32[] handles; bytes32 sealingKey; bytes issuerSignature; bytes recipientSignature; }',
439
- 'function share(ACP calldata acp) external returns (bytes32)',
440
- 'function removeShare(bytes32 shareId) external',
441
- 'function sharesFor(address recipient) external view returns (ACP[] memory)',
442
- 'function getShare(bytes32 shareId) external view returns (ACP memory)',
443
- 'function isShareValid(bytes32 shareId) external view returns (bool)',
444
- ]);
445
-
446
- const ACP_TUPLE = [
447
- {
448
- type: 'tuple',
449
- components: [
450
- { name: 'issuer', type: 'address' },
451
- { name: 'expiration', type: 'uint64' },
452
- { name: 'recipient', type: 'address' },
453
- { name: 'revokerData', type: 'uint256' },
454
- { name: 'revokerContract', type: 'address' },
455
- { name: 'scope', type: 'uint8' },
456
- { name: 'contracts', type: 'address[]' },
457
- { name: 'handles', type: 'bytes32[]' },
458
- { name: 'sealingKey', type: 'bytes32' },
459
- { name: 'issuerSignature', type: 'bytes' },
460
- { name: 'recipientSignature', type: 'bytes' },
461
- ],
462
- },
463
- ] as const;
464
-
465
- const ZERO_BYTES32 = `0x${'0'.repeat(64)}` as Hex;
466
-
467
- /** The on-chain payload for a sharing ACP: recipient-side fields empty. */
468
- const toChainShare = (acp: ACP) => {
469
- // Same public struct as the off-chain export flow (ACPUtils.getPublic), with
470
- // the recipient-side fields blanked — the recipient supplies them at import —
471
- // and uint fields widened for the ABI encoder.
472
- const pub = ACPUtils.getPublic(acp, true);
473
- return {
474
- ...pub,
475
- expiration: BigInt(pub.expiration),
476
- revokerData: BigInt(pub.revokerData),
477
- sealingKey: ZERO_BYTES32,
478
- recipientSignature: '0x' as Hex,
479
- };
480
- };
397
+ /**
398
+ * What `shareOnChain` posts with a share: the labels of its handles (one per handle, in order;
399
+ * encoded with `encodeShareMetadata`), or an already encoded metadata blob. Written once, with the
400
+ * share. Omitted, the labels the acp was created with are posted (`createSharing({ labels })`),
401
+ * or none. `appKey` goes with given labels; a client configured with `acp: { app }` fills it in.
402
+ */
403
+ export type ShareOnChainOptions = { labels?: readonly ShareLabel[]; metadata?: Hex; appKey?: Hex };
481
404
 
482
- /** Mirrors the registry's `keccak256(abi.encode(acp))` share id. */
483
- const computeShareId = (acp: ACP): Hex => {
484
- const p = toChainShare(acp);
485
- return keccak256(encodeAbiParameters(ACP_TUPLE, [p]));
405
+ const metadataOf = (acp: ACP, { labels, metadata, appKey }: ShareOnChainOptions): Hex => {
406
+ if (labels != null && metadata != null) throw new Error('Pass either labels or metadata, not both');
407
+ if (labels == null) return metadata ?? acp.metadata ?? '0x';
408
+ if (labels.length !== acp.handles.length) {
409
+ throw new Error(`Cannot label a share of ${acp.handles.length} handles with ${labels.length} labels`);
410
+ }
411
+ return encodeShareMetadata(labels, { issuer: acp.issuer, appKey });
486
412
  };
487
413
 
488
414
  /**
489
415
  * Post a signed sharing ACP to the on-chain share registry for its recipient
490
- * to discover and import — the on-chain alternative to `export()`.
416
+ * to discover and import — the on-chain alternative to `export()` — optionally
417
+ * with the labels of its handles.
491
418
  */
492
419
  const shareOnChain = async (
493
420
  acp: ACP,
494
421
  walletClient: WalletClient,
495
- registry: Hex
422
+ registry: Hex,
423
+ options: ShareOnChainOptions = {}
496
424
  ): Promise<{ txHash: Hex; shareId: Hex }> => {
497
425
  if (acp.type !== 'sharing') {
498
426
  throw new Error(`Cannot share a '${acp.type}' ACP on-chain — only 'sharing' ACPs are shareable.`);
@@ -504,12 +432,13 @@ const shareOnChain = async (
504
432
  if (walletClient.account.address.toLowerCase() !== acp.issuer.toLowerCase()) {
505
433
  throw new Error('Only the ACP issuer can share it on-chain');
506
434
  }
435
+ const metadata = metadataOf(acp, options);
507
436
 
508
437
  const txHash = await walletClient.writeContract({
509
438
  address: registry,
510
439
  abi: ACP_SHARE_REGISTRY_ABI,
511
440
  functionName: 'share',
512
- args: [toChainShare(acp)],
441
+ args: [toChainShare(acp), metadata],
513
442
  account: walletClient.account,
514
443
  chain: walletClient.chain ?? null,
515
444
  });
@@ -517,45 +446,72 @@ const shareOnChain = async (
517
446
  return { txHash, shareId: computeShareId(acp) };
518
447
  };
519
448
 
520
- /** All importable shares addressed to `recipient` (unexpired, not revoked). */
449
+ const toIncomingShare = ({ shareId, share: s, metadata }: PostedShare): IncomingShare => ({
450
+ shareId,
451
+ issuer: s.issuer,
452
+ expiration: Number(s.expiration),
453
+ recipient: s.recipient,
454
+ revokerData: Number(s.revokerData),
455
+ revokerContract: s.revokerContract,
456
+ scope: Number(s.scope),
457
+ contracts: [...s.contracts],
458
+ handles: [...s.handles],
459
+ issuerSignature: s.issuerSignature,
460
+ metadata,
461
+ ...(appKeyOf(metadata, s.handles) && { appKey: appKeyOf(metadata, s.handles) }),
462
+ });
463
+
464
+ /** The app key of a posted share; undefined when it has none, or its metadata does not decode. */
465
+ const appKeyOf = (metadata: Hex, handles: readonly Hex[]): Hex | undefined => {
466
+ try {
467
+ return decodeShareAppKey(metadata, handles);
468
+ } catch {
469
+ return undefined;
470
+ }
471
+ };
472
+
473
+ /**
474
+ * All importable shares addressed to `recipient` (unexpired, not revoked): the headers from
475
+ * `sharesFor`, then each share's `Shared` event from the block its header names (one
476
+ * single-block `getLogs` per block). A share whose event the node cannot serve yet is left out
477
+ * and turns up on a later read; it never fails the others (`readSharesFor` lists it as
478
+ * unreadable). Given an `appKey`, only the shares posted with that key.
479
+ */
521
480
  const getIncomingShares = async (
522
481
  publicClient: PublicClient,
523
482
  registry: Hex,
524
- recipient: Hex
483
+ recipient: Hex,
484
+ { appKey }: { appKey?: Hex } = {}
525
485
  ): Promise<IncomingShare[]> => {
526
- const raw = await publicClient.readContract({
527
- address: registry,
528
- abi: ACP_SHARE_REGISTRY_ABI,
529
- functionName: 'sharesFor',
530
- args: [recipient],
531
- });
486
+ const shares = (await readSharesFor(publicClient, registry, recipient)).posted.map(toIncomingShare);
487
+ return appKey == null ? shares : shares.filter((share) => share.appKey === appKey);
488
+ };
532
489
 
533
- return raw.map((s) => ({
534
- shareId: keccak256(encodeAbiParameters(ACP_TUPLE, [s])),
535
- issuer: s.issuer,
536
- expiration: Number(s.expiration),
537
- recipient: s.recipient,
538
- revokerData: Number(s.revokerData),
539
- revokerContract: s.revokerContract,
540
- scope: Number(s.scope),
541
- contracts: [...s.contracts],
542
- handles: [...s.handles],
543
- issuerSignature: s.issuerSignature,
544
- }));
490
+ /** One share by id, as posted; null when unknown or removed. Expired or revoked shares are returned as is. */
491
+ const getShareFromChain = async (
492
+ publicClient: PublicClient,
493
+ registry: Hex,
494
+ shareId: Hex
495
+ ): Promise<IncomingShare | null> => {
496
+ const posted = await readShare(publicClient, registry, shareId);
497
+ return posted && toIncomingShare(posted);
545
498
  };
546
499
 
547
500
  /**
548
501
  * Import a share read from the registry: fills the recipient's sealing key,
549
- * signs, stores and activates — the on-chain counterpart of importing an
550
- * exported JSON blob. The share stays on-chain until dismissed.
502
+ * signs, stores and (unless `activate: false`) activates — the on-chain
503
+ * counterpart of importing an exported JSON blob. Its metadata is kept on the
504
+ * recipient acp, as with an import of exported JSON. The share stays on-chain
505
+ * until dismissed.
551
506
  */
552
507
  const importFromChain = async (
553
508
  share: IncomingShare,
554
509
  publicClient: PublicClient,
555
- walletClient: WalletClient
510
+ walletClient: WalletClient,
511
+ importOptions: ImportSharedOptions = {}
556
512
  ): Promise<RecipientACP> => {
557
- const { shareId: _shareId, ...options } = share;
558
- return importShared({ ...options, type: 'sharing' }, publicClient, walletClient);
513
+ const { shareId: _shareId, appKey: _appKey, ...options } = share;
514
+ return importShared({ ...options, type: 'sharing' }, publicClient, walletClient, importOptions);
559
515
  };
560
516
 
561
517
  /** Remove a share from the registry (issuer retracts / recipient dismisses). */
@@ -608,14 +564,15 @@ export const acps = {
608
564
  revokeACP,
609
565
  revokeAllACPs,
610
566
  isACPRevoked,
567
+ checkAccess,
611
568
 
612
569
  shareOnChain,
613
570
  getIncomingShares,
571
+ getShareFromChain,
614
572
  importFromChain,
615
573
  removeShareOnChain,
616
574
  computeShareId,
617
575
 
618
- applyACPDefaults,
619
576
  applyACPDefaultsFromChain,
620
577
  getAclServedAddresses,
621
578
  clearAclServedAddresses,
package/core/client.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { CreateSelfACPOptions, CreateSharingACPOptions, ImportSharedACPOptions } from '@/acps';
2
+ import { decodeShareMetadata, shareAppKey, verifyShareLabels } from '@/acps';
2
3
 
3
4
  import { createStore } from 'zustand/vanilla';
4
5
  import { type Hex, type PublicClient, type WalletClient } from 'viem';
@@ -50,6 +51,10 @@ export function createCofheClientBase<TConfig extends CofheConfig>(
50
51
 
51
52
  // Share registry resolution: explicit `acp.sharingRegistry` config wins,
52
53
  // otherwise the address the chain's ACL serves.
54
+ /** `{ appKey }` of `config.acp.app`, for the shares this client creates, posts and lists; `{}` without an app. */
55
+ const withAppKey = (): { appKey?: `0x${string}` } =>
56
+ opts.config.acp?.app == null ? {} : { appKey: shareAppKey(opts.config.acp.app) };
57
+
53
58
  const _resolveSharingRegistry = async (publicClient: PublicClient, chainId: number): Promise<`0x${string}`> => {
54
59
  const configured = opts.config.acp?.sharingRegistry?.[chainId];
55
60
  if (configured != null) return configured;
@@ -156,6 +161,7 @@ export function createCofheClientBase<TConfig extends CofheConfig>(
156
161
  zkBuilderAndCrsGenerator: opts.zkBuilderAndCrsGenerator,
157
162
  initTfhe: opts.initTfhe,
158
163
  zkProveWorkerFn: opts.zkProveWorkerFn,
164
+ beforeMainThreadProve: opts.beforeMainThreadProve,
159
165
 
160
166
  keysStorage,
161
167
 
@@ -253,19 +259,17 @@ export function createCofheClientBase<TConfig extends CofheConfig>(
253
259
  const { publicClient, walletClient } = clients ?? connectStore.getState();
254
260
  const chainId = await publicClient!.getChainId();
255
261
  return acps.createSharing(
256
- await acps.applyACPDefaultsFromChain(options, opts.config.acp, publicClient!, chainId),
262
+ await acps.applyACPDefaultsFromChain({ ...withAppKey(), ...options }, opts.config.acp, publicClient!, chainId),
257
263
  publicClient!,
258
264
  walletClient!
259
265
  );
260
266
  },
261
267
 
262
- importShared: async (
263
- options: ImportSharedACPOptions | string,
264
- clients?: { publicClient: PublicClient; walletClient: WalletClient }
265
- ) => {
268
+ importShared: async (options: ImportSharedACPOptions | string, importOptions = {}) => {
266
269
  _requireConnected();
267
- const { publicClient, walletClient } = clients ?? connectStore.getState();
268
- return acps.importShared(options, publicClient!, walletClient!);
270
+ const { activate, name, ...clients } = importOptions;
271
+ const { publicClient, walletClient } = clients.publicClient ? clients : connectStore.getState();
272
+ return acps.importShared(options, publicClient!, walletClient!, { activate, name });
269
273
  },
270
274
 
271
275
  // Get or create methods (require connection)
@@ -289,7 +293,7 @@ export function createCofheClientBase<TConfig extends CofheConfig>(
289
293
  return acps.getOrCreateSharingACP(
290
294
  publicClient!,
291
295
  walletClient!,
292
- await acps.applyACPDefaultsFromChain(options, opts.config.acp, publicClient!, _chainId),
296
+ await acps.applyACPDefaultsFromChain({ ...withAppKey(), ...options }, opts.config.acp, publicClient!, _chainId),
293
297
  _chainId,
294
298
  _account
295
299
  );
@@ -314,13 +318,19 @@ export function createCofheClientBase<TConfig extends CofheConfig>(
314
318
  return acps.isACPRevoked(acp, publicClient!);
315
319
  },
316
320
 
321
+ checkAccess: async (acp, handle) => {
322
+ _requireConnected();
323
+ const { publicClient } = connectStore.getState();
324
+ return acps.checkAccess(acp, publicClient!, handle);
325
+ },
326
+
317
327
  // On-chain sharing (require connection + config.acp.sharingRegistry)
318
- shareOnChain: async (acp) => {
328
+ shareOnChain: async (acp, options) => {
319
329
  _requireConnected();
320
330
  const { publicClient, walletClient } = connectStore.getState();
321
331
  const chainId = await publicClient!.getChainId();
322
332
  const sharingRegistry = await _resolveSharingRegistry(publicClient!, chainId);
323
- return acps.shareOnChain(acp, walletClient!, sharingRegistry);
333
+ return acps.shareOnChain(acp, walletClient!, sharingRegistry, { ...withAppKey(), ...options });
324
334
  },
325
335
 
326
336
  getIncomingShares: async () => {
@@ -329,13 +339,34 @@ export function createCofheClientBase<TConfig extends CofheConfig>(
329
339
  const chainId = await publicClient!.getChainId();
330
340
  const account = walletClient!.account!.address;
331
341
  const sharingRegistry = await _resolveSharingRegistry(publicClient!, chainId);
332
- return acps.getIncomingShares(publicClient!, sharingRegistry, account);
342
+ return acps.getIncomingShares(publicClient!, sharingRegistry, account, withAppKey());
343
+ },
344
+
345
+ getShareFromChain: async (shareId) => {
346
+ _requireConnected();
347
+ const { publicClient } = connectStore.getState();
348
+ const chainId = await publicClient!.getChainId();
349
+ const sharingRegistry = await _resolveSharingRegistry(publicClient!, chainId);
350
+ return acps.getShareFromChain(publicClient!, sharingRegistry, shareId);
351
+ },
352
+
353
+ verifyShareLabels: async (share, options) => {
354
+ _requireConnected();
355
+ const { publicClient } = connectStore.getState();
356
+ if (share.metadata == null || share.metadata === '0x') return null;
357
+ return verifyShareLabels({
358
+ labels: decodeShareMetadata(share.metadata, share.handles),
359
+ handles: share.handles,
360
+ issuer: share.issuer,
361
+ publicClient: publicClient!,
362
+ verify: options?.verify,
363
+ });
333
364
  },
334
365
 
335
- importFromChain: async (share) => {
366
+ importFromChain: async (share, options) => {
336
367
  _requireConnected();
337
368
  const { publicClient, walletClient } = connectStore.getState();
338
- return acps.importFromChain(share, publicClient!, walletClient!);
369
+ return acps.importFromChain(share, publicClient!, walletClient!, options);
339
370
  },
340
371
 
341
372
  dismissShare: async (shareId) => {