@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/CHANGELOG.md CHANGED
@@ -1,5 +1,40 @@
1
1
  # @cofhe/sdk Changelog
2
2
 
3
+ ## 0.8.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 0fe40c3: Add the `ethereum` (chain id 1) and `arbitrum` (Arbitrum One, chain id 42161) mainnet chains to `@cofhe/sdk/chains`, both with `environment: 'MAINNET'`. They are included in `chains`, `getChainById` and `getChainByName`.
8
+ - 6f7a4c5: Resolve the Task Manager through the `CoFHEAddressBook` (`@fhenixprotocol/cofhe-contracts@1.0.0`). **Breaking:** `TASK_MANAGER_ADDRESS` is removed from `@cofhe/sdk`. Use `getTaskManagerAddress(publicClient)`, which reads `getTm(TASK_MANAGER_ID)` from the book at `COFHE_ADDRESS_BOOK_ADDRESS` (both exported) and caches the result per chain. If the chain has no book, or the id is unset, it throws `TASK_MANAGER_UNRESOLVED`. `verifyDecryptResult`, the ACL/ACP lookups and `useCofheEnabled` / `useCofheReadDecryptionResults` now resolve the Task Manager this way. `@cofhe/react` adds `useCofheTaskManagerAddress()`.
9
+
10
+ Mocks: a new `MockCoFHEAddressBook` is deployed at FHE.sol's `COFHE_ADDRESS_BOOK`, and `MockTaskManager` moves to `MOCKS_TASK_MANAGER_ADDRESS` (`0x…5000`) and is registered in the book. `MockACL` now stores its Task Manager (`setTaskManager` / `getTaskManagerAddress`) instead of reading a constant, so its `TASK_MANAGER_ADDRESS_()` getter is gone. All three plugins deploy and wire the book. Hardhat 2 exposes `hre.cofhe.mocks.getMockCoFHEAddressBook()`, Hardhat 3 exposes `conn.cofhe.mocks.MockCoFHEAddressBook`, and Foundry exposes `CofheTest.mockAddressBook`. Contracts must be built against `@fhenixprotocol/cofhe-contracts@1.0.0` or later (the hardhat plugin's peer range is now `>=1.0.0`). Each FHE op now makes one extra `getTm` call on the book, as it does on real networks, so expect slightly higher gas numbers.
11
+
12
+ - 552d118: The CRS is now cached per security zone. It was fetched per zone but cached under the chain id alone, so after encrypting on zone 0, encrypting on zone 1 reused zone 0's CRS and produced a proof the ZK verifier rejects.
13
+
14
+ **Breaking (`KeysStorage`):** `crs` is now keyed by chain and zone, like `fhe`. `getCrs(chainId, securityZone = 0)` takes the zone, and `setCrs(chainId, securityZone, crs)` matches `setFheKey`'s argument order. A CRS persisted in the old per-chain shape is dropped on rehydrate and refetched.
15
+
16
+ ### Patch Changes
17
+
18
+ - 17ff23e: Scoped decryption with shared ACPs.
19
+
20
+ **@cofhe/sdk**
21
+
22
+ - `client.acp.importShared` and `client.acp.importFromChain` take an `activate` option (default `true`). With `activate: false` the imported ACP is stored without becoming the active ACP, so decrypts that use the active ACP are unaffected; use the imported ACP explicitly, e.g. `.withACP(acp)`. `importShared`'s second argument is now `{ activate?, publicClient?, walletClient? }`; passing `{ publicClient, walletClient }` works as before.
23
+ - New `client.acp.checkAccess(acp, handle?)` (and `ACPUtils.checkAccessOnChain`) returns an ACP on-chain status instead of reverting: `valid`, `expired`, `revoked` or an invalid signature, and with a handle `allowed`, `out-of-scope` or `issuer-not-allowed`. New exported type: `ACPAccessStatus`. ACL reverts are now recognized when the public client comes from another copy of viem than the SDK, which also fixes `checkValidityOnChain` throwing a generic error in that setup. ACL reverts reported only as text (`custom error 0x…`, e.g. by a transport that drops the error data) are decoded too. For the issuer copy of a share (type `sharing`, no recipient signature yet) the handle-less check reports expiry and revocation instead of failing on the missing signature.
24
+ - `@cofhe/sdk/acps` exports the on-chain sharing pieces: `ACP_SHARE_REGISTRY_ABI`, `ACP_REVOKER_ABI`, `ACP_SHARE_TUPLE_ABI`, `toChainShare`, `computeShareId`, `shareIdOfChainShare`, `getAclAddress`, `getAclServedAddresses` and `clearAclServedAddresses`, so an app can send registry and revoker writes through its own transaction path and still agree with the registry on share ids.
25
+
26
+ **@cofhe/react**
27
+
28
+ - Decrypt with a chosen ACP in part of the tree without changing the active ACP. New `<CofheACPScope acp={acp | hash}>` and `useCofheACPScope()`; `useCofheReadContractAndDecrypt`, `useCofheReadContract`, `useCofheReadContracts` and `useCofheTokenDecryptedBalance` take an `acp` option that wins over the scope. Inside a scope `useCofheTokenDecryptedBalance` reads the ACP issuer balance unless given an account, and an invalid or unknown scoped ACP disables the reads (`disabledDueToMissingValidACP`) instead of falling back to the active ACP. The decrypt cache key gains a fifth segment, the hash of the ACP that decrypts (the chosen one, else the active one), so a value decrypted with one ACP never answers for another. New exported types: `CofheACPInput`, `CofheACPScopeValue`.
29
+ - `<CofheACPScope>` and the per-hook `acp` option check the chosen ACP on chain (`client.acp.checkAccess`) when mounted, every minute and on window focus, so a revoked share turns the scope off and drops its decrypted values while the view is open. `useCofheACPScope()` gains `status` (`checking`, `valid`, `expired`, `revoked`, `invalid`, `unverified`); the scope decrypts only while it is `valid`. For a SNAPSHOT (handle-scope) ACP, `useCofheReadContractAndDecrypt` and `useCofheTokenDecryptedBalance` report `isOutOfScope` for a value the share does not list and never send it for decryption. New exported type: `CofheACPStatus`.
30
+ - Plaintext decrypted with a shared (non-self) ACP is kept in memory only: it is no longer written to the persisted query cache. Decrypts are cached under the ACP that made them and dropped when that ACP leaves the ACP store (e.g. `removeACP`) or when a non-self ACP expires while in use, so a revoked or expired share does not keep serving its values from cache.
31
+ - The sharing hooks are exported: `useCofheIncomingShares` (registry inbox, polled; shares already imported by the connected account are left out), `useCofheShareOnChain`, `useCofheImportShared({ activate })` (exported JSON or an `IncomingShare`), `useCofheRemoveShare`, `useCofheRevokeACP` and `useCofheACPStatus` (an ACP on-chain status, re-read after a revoke mines). The write hooks resolve once mined. The package-internal names (`useIncomingShares`, `useShareOnChain`, `useImportFromChain`, `useRemoveShare`) stay as deprecated aliases.
32
+ - New `useCofheACPs({ chainId?, type? })`: the connected account stored ACPs on a chain (the connected chain by default), optionally narrowed to one type, e.g. `useCofheACPs({ type: 'recipient' })` for received shares. `useCofheAllACPs` is deprecated: it returned only the connected account ACPs on the connected chain, not all ACPs, and is now an alias of `useCofheACPs()`.
33
+ - `useCofheDecrypt` is exported: decrypt a ciphertext handle directly (no contract read), with the same `acp` option and scope behaviour as `useCofheReadContractAndDecrypt`.
34
+
35
+ - 22eebcc: The Node in-memory storage fallback (used when the filesystem key cache is not writable) no longer JSON-encodes values a second time. Previously cached FHE keys and CRS read back from the fallback as strings instead of objects, so they were effectively lost and refetched.
36
+ - 42fccae: Add `tfheThreads` config option (`'auto'` | number | `false`, default `'auto'`) to multi-thread ZK proof generation on cross-origin-isolated web pages, and export the `TfheThreadsSetting` and `TfheInitializer` types.
37
+
3
38
  ## 0.7.1
4
39
 
5
40
  ## 0.7.0
package/acps/acp.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { keccak256, toHex, zeroAddress, parseAbi, type Hex, type PublicClient, type WalletClient } from 'viem';
2
2
  import {
3
+ type ACPAccessStatus,
3
4
  type ACP,
4
5
  type SelfACP,
5
6
  type SharingACP,
@@ -25,7 +26,12 @@ import {
25
26
  } from './validation.js';
26
27
  import { SignatureUtils } from './signature.js';
27
28
  import { GenerateSealingKey, unsealWithPrivateKey } from './sealing.js';
28
- import { checkACPValidityOnChain, getAclEIP712Domain } from './onchain-utils.js';
29
+ import { checkACPValidityOnChain, getACPAccessStatusOnChain, getAclEIP712Domain } from './onchain-utils.js';
30
+ import { encodeShareMetadata } from './share-metadata/encode.js';
31
+ import { UNLABELLED } from './share-metadata/presets';
32
+
33
+ /** The name of an imported acp unless the recipient gives one: who it came from. */
34
+ const sharedByName = (issuer: Hex): string => `Shared by ${issuer.slice(0, 6)}…${issuer.slice(-4)}`;
29
35
 
30
36
  /**
31
37
  * Main ACP utilities - functional approach for React compatibility
@@ -56,6 +62,15 @@ export const ACPUtils = {
56
62
  */
57
63
  createSharing: (options: CreateSharingACPOptions): SharingACP => {
58
64
  const validation = validateSharingACPOptions(options);
65
+ const { labels, appKey } = options;
66
+ if (labels != null && labels.length !== validation.handles.length) {
67
+ throw new Error(`Cannot label a share of ${validation.handles.length} handles with ${labels.length} labels`);
68
+ }
69
+ // With an app key but no labels, the handles are listed as unlabelled so the key has a blob to sit in.
70
+ const metadata =
71
+ labels != null || appKey != null
72
+ ? encodeShareMetadata(labels ?? validation.handles.map(() => UNLABELLED), { issuer: validation.issuer, appKey })
73
+ : undefined;
59
74
 
60
75
  // Always generate a new sealing key - users cannot provide their own
61
76
  const sealingPair = GenerateSealingKey();
@@ -63,6 +78,7 @@ export const ACPUtils = {
63
78
  const acp = {
64
79
  hash: ACPUtils.getHash(validation),
65
80
  ...validation,
81
+ ...(metadata != null && { metadata }),
66
82
  sealingPrivateKey: sealingPair.privateKey,
67
83
  sealingKey: sealingPair.publicKey,
68
84
  _signedDomain: undefined,
@@ -72,9 +88,10 @@ export const ACPUtils = {
72
88
  },
73
89
 
74
90
  /**
75
- * Import a shared acp from various input formats
91
+ * Import a shared acp from various input formats. The acp is named `name`, or after its issuer
92
+ * ("Shared by 0x1234…abcd"); a `name` in the share itself is ignored.
76
93
  */
77
- importShared: (options: ImportSharedACPOptions | string): RecipientACP => {
94
+ importShared: (options: ImportSharedACPOptions | string, { name }: { name?: string } = {}): RecipientACP => {
78
95
  let parsedOptions: ImportSharedACPOptions;
79
96
 
80
97
  // Handle different input types
@@ -105,6 +122,7 @@ export const ACPUtils = {
105
122
  const acp = {
106
123
  hash: ACPUtils.getHash(validation),
107
124
  ...validation,
125
+ name: name?.trim() || sharedByName(validation.issuer),
108
126
  sealingPrivateKey: sealingPair.privateKey,
109
127
  sealingKey: sealingPair.publicKey,
110
128
  _signedDomain: undefined,
@@ -183,9 +201,10 @@ export const ACPUtils = {
183
201
  importSharedAndSign: async (
184
202
  options: ImportSharedACPOptions | string,
185
203
  publicClient: PublicClient,
186
- walletClient: WalletClient
204
+ walletClient: WalletClient,
205
+ importOptions: { name?: string } = {}
187
206
  ): Promise<RecipientACP> => {
188
- const acp = ACPUtils.importShared(options);
207
+ const acp = ACPUtils.importShared(options, importOptions);
189
208
  return ACPUtils.sign(acp, publicClient, walletClient);
190
209
  },
191
210
 
@@ -217,6 +236,7 @@ export const ACPUtils = {
217
236
  recipientSignature: acp.recipientSignature,
218
237
  _signedDomain: acp._signedDomain,
219
238
  sealingPrivateKey: acp.sealingPrivateKey,
239
+ ...(acp.metadata != null && { metadata: acp.metadata }),
220
240
  };
221
241
  },
222
242
 
@@ -296,7 +316,7 @@ export const ACPUtils = {
296
316
  /**
297
317
  * Export acp data for sharing (strips the private component).
298
318
  * Fixed `SharedACP` shape — every field always present, aligned with
299
- * `ACPPublic` and the on-chain sharing payload.
319
+ * `ACPPublic` and the on-chain sharing payload — plus `metadata` when the acp has labels.
300
320
  */
301
321
  export: (acp: ACP): string => {
302
322
  if (acp.type !== 'sharing') {
@@ -311,7 +331,6 @@ export const ACPUtils = {
311
331
  }
312
332
 
313
333
  const shared: SharedACP = {
314
- name: acp.name,
315
334
  type: acp.type,
316
335
  issuer: acp.issuer,
317
336
  expiration: acp.expiration,
@@ -322,6 +341,7 @@ export const ACPUtils = {
322
341
  contracts: acp.contracts,
323
342
  handles: acp.handles,
324
343
  issuerSignature: acp.issuerSignature,
344
+ ...(acp.metadata != null && acp.metadata !== '0x' && { metadata: acp.metadata }),
325
345
  };
326
346
 
327
347
  return JSON.stringify(shared, undefined, 2);
@@ -408,4 +428,12 @@ export const ACPUtils = {
408
428
  const publicAcp = ACPUtils.getPublic(acp);
409
429
  return checkACPValidityOnChain(publicAcp, publicClient);
410
430
  },
431
+
432
+ /**
433
+ * The acp on-chain status as a value instead of a revert: 'valid' or why not, and given a
434
+ * handle, whether the acp may read it (see `ACPAccessStatus`).
435
+ */
436
+ checkAccessOnChain: async (acp: ACP, publicClient: PublicClient, handle?: bigint | Hex): Promise<ACPAccessStatus> => {
437
+ return getACPAccessStatusOnChain(ACPUtils.getPublic(acp), publicClient, handle);
438
+ },
411
439
  };
package/acps/index.ts CHANGED
@@ -9,15 +9,31 @@ export type {
9
9
  ACPPublic,
10
10
  SharedACP,
11
11
  IncomingShare,
12
+ LabelledShare,
12
13
  EIP712Domain,
13
14
  EIP712Types,
14
15
  EIP712Message,
15
16
  ValidationResult,
16
17
  ACPSignaturePrimaryType,
17
- } from './types.js';
18
+ } from './types';
18
19
 
19
20
  // Main utilities
20
- export { ACPUtils } from './acp.js';
21
+ export { ACPUtils } from './acp';
22
+
23
+ // On-chain sharing: registry / revoker ABIs, the registry payload and share id of a sharing ACP,
24
+ // and the read of posted shares from their Shared events
25
+ export * from './registry';
26
+
27
+ // Share metadata: labels saying where each handle of a SNAPSHOT share came from
28
+ export * from './share-metadata/index';
29
+
30
+ // Addresses the chain ACL serves (TaskManager -> acl() -> default revoker, share registry)
31
+ export {
32
+ getAclAddress,
33
+ getAclServedAddresses,
34
+ clearAclServedAddresses,
35
+ type AclServedAddresses,
36
+ } from './onchain-utils';
21
37
 
22
38
  // Validation utilities
23
39
  export {
@@ -38,10 +54,10 @@ export {
38
54
  validateImportACP,
39
55
  // Common utilities
40
56
  ValidationUtils,
41
- } from './validation.js';
57
+ } from './validation';
42
58
 
43
59
  // Signature utilities
44
- export { SignatureUtils, getSignatureTypesAndMessage, SignatureTypes } from './signature.js';
60
+ export { SignatureUtils, getSignatureTypesAndMessage, SignatureTypes } from './signature';
45
61
 
46
62
  // Storage utilities
47
63
  export {
@@ -56,15 +72,15 @@ export {
56
72
  removeActiveACPHash,
57
73
  clearStaleStore,
58
74
  ACP_STORE_DEFAULTS,
59
- } from './store.js';
75
+ } from './store';
60
76
 
61
77
  // Sealing utilities
62
- export { GenerateSealingKey, seal, unsealWithPrivateKey, type SealingKeyPair } from './sealing.js';
63
- export type { EthEncryptedData } from './sealing.js';
78
+ export { GenerateSealingKey, seal, unsealWithPrivateKey, type SealingKeyPair } from './sealing';
79
+ export type { EthEncryptedData } from './sealing';
64
80
 
65
81
  // Re-export everything for convenience
66
- export * from './types.js';
67
- export * from './acp.js';
68
- export * from './validation.js';
69
- export * from './signature.js';
70
- export * from './store.js';
82
+ export * from './types';
83
+ export * from './acp';
84
+ export * from './validation';
85
+ export * from './signature';
86
+ export * from './store';
@@ -1,13 +1,6 @@
1
- import {
2
- BaseError,
3
- ContractFunctionRevertedError,
4
- type Hex,
5
- type PublicClient,
6
- decodeErrorResult,
7
- parseAbi,
8
- } from 'viem';
9
- import type { EIP712Domain, ACPPublic } from './types';
10
- import { TASK_MANAGER_ADDRESS } from '../core/consts.js';
1
+ import { type Hex, type PublicClient, decodeErrorResult, parseAbi, zeroAddress } from 'viem';
2
+ import type { ACPAccessStatus, EIP712Domain, ACPPublic } from './types';
3
+ import { getTaskManagerAddress } from '../core/taskManager.js';
11
4
 
12
5
  export const getAclAddress = async (publicClient: PublicClient): Promise<Hex> => {
13
6
  const ACL_IFACE = 'function acl() view returns (address)';
@@ -17,7 +10,7 @@ export const getAclAddress = async (publicClient: PublicClient): Promise<Hex> =>
17
10
 
18
11
  // Get the ACL address
19
12
  return (await publicClient.readContract({
20
- address: TASK_MANAGER_ADDRESS as `0x${string}`,
13
+ address: await getTaskManagerAddress(publicClient),
21
14
  abi: aclAbi,
22
15
  functionName: 'acl',
23
16
  })) as `0x${string}`;
@@ -59,6 +52,48 @@ export const getAclEIP712Domain = async (publicClient: PublicClient): Promise<EI
59
52
  };
60
53
  };
61
54
 
55
+ const toACPTuple = (acp: ACPPublic) => ({
56
+ issuer: acp.issuer,
57
+ expiration: BigInt(acp.expiration),
58
+ recipient: acp.recipient,
59
+ revokerData: BigInt(acp.revokerData),
60
+ revokerContract: acp.revokerContract,
61
+ scope: acp.scope,
62
+ contracts: acp.contracts,
63
+ handles: acp.handles,
64
+ sealingKey: acp.sealingKey,
65
+ issuerSignature: acp.issuerSignature,
66
+ recipientSignature: acp.recipientSignature,
67
+ });
68
+
69
+ /**
70
+ * The custom error a reverted ACL call carries, however the node reports it: a viem revert, a
71
+ * Hardhat-style 'reverted with custom error' detail, or raw return data. `undefined` when the
72
+ * failure is not a contract revert (e.g. a network error).
73
+ */
74
+ function revertErrorName(err: unknown, abi: readonly any[]): string | undefined {
75
+ // Viem revert. Matched by name, not `instanceof`: the public client may come from another copy of
76
+ // viem than this package (an app bringing its own), and then no class check ever matches.
77
+ const walk = (err as { walk?: (fn: (e: unknown) => boolean) => unknown } | null)?.walk;
78
+ if (typeof walk === 'function') {
79
+ const revertError = walk.call(err, (e) => (e as { name?: string })?.name === 'ContractFunctionRevertedError') as
80
+ | { data?: { errorName?: string } }
81
+ | null
82
+ | undefined;
83
+ if (revertError) return revertError.data?.errorName ?? '';
84
+ }
85
+
86
+ // Check details field for custom error names (e.g., from Hardhat test nodes)
87
+ const customErrorName = extractCustomErrorFromDetails(err, abi);
88
+ if (customErrorName) return customErrorName;
89
+
90
+ // Hardhat wrapped error will need to be unwrapped to get the return data
91
+ const hhDetailsData = extractReturnData(err);
92
+ if (hhDetailsData != null) return decodeErrorResult({ abi, data: hhDetailsData }).errorName;
93
+
94
+ return undefined;
95
+ }
96
+
62
97
  export const checkACPValidityOnChain = async (acp: ACPPublic, publicClient: PublicClient): Promise<boolean> => {
63
98
  const aclAddress = await getAclAddress(publicClient);
64
99
 
@@ -68,51 +103,69 @@ export const checkACPValidityOnChain = async (acp: ACPPublic, publicClient: Publ
68
103
  address: aclAddress,
69
104
  abi: checkACPValidityAbi,
70
105
  functionName: 'checkPermissionValidity',
71
- args: [
72
- {
73
- issuer: acp.issuer,
74
- expiration: BigInt(acp.expiration),
75
- recipient: acp.recipient,
76
- revokerData: BigInt(acp.revokerData),
77
- revokerContract: acp.revokerContract,
78
- scope: acp.scope,
79
- contracts: acp.contracts,
80
- handles: acp.handles,
81
- sealingKey: acp.sealingKey,
82
- issuerSignature: acp.issuerSignature,
83
- recipientSignature: acp.recipientSignature,
84
- },
85
- ],
106
+ args: [toACPTuple(acp)],
86
107
  });
87
108
  return true;
88
109
  } catch (err: any) {
89
- // Viem default handling
90
- if (err instanceof BaseError) {
91
- const revertError = err.walk((err: any) => err instanceof ContractFunctionRevertedError);
92
- if (revertError instanceof ContractFunctionRevertedError) {
93
- const errorName = revertError.data?.errorName ?? '';
94
- throw new Error(errorName);
95
- }
96
- }
110
+ const errorName = revertErrorName(err, checkACPValidityAbi);
111
+ if (errorName !== undefined) throw new Error(errorName);
112
+ // Fallback throw the original error
113
+ throw err;
114
+ }
115
+ };
97
116
 
98
- // Check details field for custom error names (e.g., from Hardhat test nodes)
99
- const customErrorName = extractCustomErrorFromDetails(err, checkACPValidityAbi);
100
- if (customErrorName) {
101
- throw new Error(customErrorName);
102
- }
117
+ /** The ACL permission reverts, as statuses. */
118
+ const PERMISSION_ERROR_STATUS: Record<string, ACPAccessStatus> = {
119
+ PermissionInvalid_Expired: 'expired',
120
+ PermissionInvalid_Disabled: 'revoked',
121
+ PermissionInvalid_IssuerSignature: 'invalid-issuer-signature',
122
+ PermissionInvalid_RecipientSignature: 'invalid-recipient-signature',
123
+ };
103
124
 
104
- // Hardhat wrapped error will need to be unwrapped to get the return data
105
- const hhDetailsData = extractReturnData(err);
106
- if (hhDetailsData != null) {
107
- const decoded = decodeErrorResult({
108
- abi: checkACPValidityAbi,
109
- data: hhDetailsData,
110
- });
125
+ /**
126
+ * An ACP status on chain, read from the ACL: its validity (expiration, signatures, revocation)
127
+ * and, given a handle, whether the ACP may read it. The ACL reverts for an invalid ACP; those
128
+ * reverts come back as statuses. Anything else (e.g. a network error) still throws.
129
+ */
130
+ export const getACPAccessStatusOnChain = async (
131
+ acp: ACPPublic,
132
+ publicClient: PublicClient,
133
+ handle?: bigint | Hex
134
+ ): Promise<ACPAccessStatus> => {
135
+ const aclAddress = await getAclAddress(publicClient);
111
136
 
112
- throw new Error(decoded.errorName);
137
+ try {
138
+ if (handle === undefined) {
139
+ await publicClient.simulateContract({
140
+ address: aclAddress,
141
+ abi: acpAccessAbi,
142
+ functionName: 'checkPermissionValidity',
143
+ args: [toACPTuple(acp)],
144
+ });
145
+ return 'valid';
113
146
  }
114
147
 
115
- // Fallback throw the original error
148
+ const ctHash = BigInt(handle);
149
+ const allowed = await publicClient.readContract({
150
+ address: aclAddress,
151
+ abi: acpAccessAbi,
152
+ functionName: 'isAllowedWithPermission',
153
+ args: [toACPTuple(acp), ctHash],
154
+ });
155
+ if (allowed) return 'allowed';
156
+
157
+ // The ACL answers false both when the scope misses the handle and when the issuer itself may
158
+ // not read it; the issuer own allowance tells the two apart.
159
+ const issuerAllowed = await publicClient.readContract({
160
+ address: aclAddress,
161
+ abi: acpAccessAbi,
162
+ functionName: 'isAllowed',
163
+ args: [ctHash, acp.issuer],
164
+ });
165
+ return issuerAllowed ? 'out-of-scope' : 'issuer-not-allowed';
166
+ } catch (err) {
167
+ const status = PERMISSION_ERROR_STATUS[revertErrorName(err, acpAccessAbi) ?? ''];
168
+ if (status) return status;
116
169
  throw err;
117
170
  }
118
171
  };
@@ -143,7 +196,10 @@ function extractReturnData(err: unknown): `0x${string}` | undefined {
143
196
  const anyErr = err as any;
144
197
  const s = anyErr?.details ?? anyErr?.cause?.details ?? anyErr?.shortMessage ?? anyErr?.message ?? String(err);
145
198
 
146
- return s.match(/return data:\s*(0x[a-fA-F0-9]+)/)?.[1] as `0x${string}` | undefined;
199
+ // Hardhat: "return data: 0x...". A node that reports only the selector in text (e.g. anvil through
200
+ // a transport that drops the structured error data): "custom error 0x...".
201
+ const match = s.match(/return data:\s*(0x[a-fA-F0-9]+)/) ?? s.match(/custom error (0x[a-fA-F0-9]{8,})/);
202
+ return match?.[1] as `0x${string}` | undefined;
147
203
  }
148
204
 
149
205
  const checkACPValidityAbi = [
@@ -244,3 +300,86 @@ const checkACPValidityAbi = [
244
300
  inputs: [],
245
301
  },
246
302
  ] as const;
303
+
304
+ const acpAccessAbi = [
305
+ ...checkACPValidityAbi,
306
+ {
307
+ type: 'function',
308
+ name: 'isAllowedWithPermission',
309
+ inputs: [checkACPValidityAbi[0].inputs[0], { name: 'handle', type: 'uint256', internalType: 'uint256' }],
310
+ outputs: [{ name: '', type: 'bool', internalType: 'bool' }],
311
+ stateMutability: 'view',
312
+ },
313
+ {
314
+ type: 'function',
315
+ name: 'isAllowed',
316
+ inputs: [
317
+ { name: 'handle', type: 'uint256', internalType: 'uint256' },
318
+ { name: 'account', type: 'address', internalType: 'address' },
319
+ ],
320
+ outputs: [{ name: '', type: 'bool', internalType: 'bool' }],
321
+ stateMutability: 'view',
322
+ },
323
+ ] as const;
324
+
325
+ // ACL-SERVED ADDRESSES (defaultRevokerContract / shareRegistry)
326
+
327
+ const ACL_SERVED_ADDRESSES_ABI = parseAbi([
328
+ 'function acl() view returns (address)',
329
+ 'function defaultRevokerContract() view returns (address)',
330
+ 'function shareRegistry() view returns (address)',
331
+ ]);
332
+
333
+ export interface AclServedAddresses {
334
+ defaultRevoker?: Hex;
335
+ shareRegistry?: Hex;
336
+ }
337
+
338
+ const aclServedAddressesCache = new Map<number, AclServedAddresses>();
339
+
340
+ /** Test hook: forget resolved addresses (e.g. between redeployments on one chainId). */
341
+ export const clearAclServedAddresses = () => aclServedAddressesCache.clear();
342
+
343
+ /**
344
+ * The ACP infrastructure addresses the chain's ACL serves (CoFHEAddressBook ->
345
+ * TaskManager -> acl() -> getters). Zero addresses and pre-upgrade ACLs (getters absent -> revert)
346
+ * resolve to `undefined` — callers fall back to `acp.*` config.
347
+ *
348
+ * Resolutions are cached per chainId. A failure to reach the TaskManager (network
349
+ * error, no CoFHE deployment) is NOT cached, so a transient outage does not pin
350
+ * an empty result for the whole session.
351
+ */
352
+ export const getAclServedAddresses = async (
353
+ publicClient: PublicClient,
354
+ chainId: number
355
+ ): Promise<AclServedAddresses> => {
356
+ const cached = aclServedAddressesCache.get(chainId);
357
+ if (cached != null) return cached;
358
+
359
+ let aclAddress: Hex;
360
+ try {
361
+ aclAddress = await publicClient.readContract({
362
+ address: await getTaskManagerAddress(publicClient, chainId),
363
+ abi: ACL_SERVED_ADDRESSES_ABI,
364
+ functionName: 'acl',
365
+ });
366
+ } catch {
367
+ return {};
368
+ }
369
+
370
+ const [defaultRevoker, shareRegistry] = await Promise.all([
371
+ publicClient
372
+ .readContract({ address: aclAddress, abi: ACL_SERVED_ADDRESSES_ABI, functionName: 'defaultRevokerContract' })
373
+ .catch(() => undefined),
374
+ publicClient
375
+ .readContract({ address: aclAddress, abi: ACL_SERVED_ADDRESSES_ABI, functionName: 'shareRegistry' })
376
+ .catch(() => undefined),
377
+ ]);
378
+
379
+ const resolved: AclServedAddresses = {
380
+ defaultRevoker: defaultRevoker != null && defaultRevoker !== zeroAddress ? defaultRevoker : undefined,
381
+ shareRegistry: shareRegistry != null && shareRegistry !== zeroAddress ? shareRegistry : undefined,
382
+ };
383
+ aclServedAddressesCache.set(chainId, resolved);
384
+ return resolved;
385
+ };