@cofhe/sdk 0.7.1 → 0.8.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 (79) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/acps/acp.ts +10 -1
  3. package/acps/index.ts +11 -0
  4. package/acps/onchain-utils.ts +188 -49
  5. package/acps/registry.ts +73 -0
  6. package/acps/test/onchainAccessStatus.test.ts +116 -0
  7. package/acps/types.ts +16 -0
  8. package/chains/chains/arbitrum.ts +14 -0
  9. package/chains/chains/ethereum.ts +14 -0
  10. package/chains/index.ts +5 -1
  11. package/chains/test/chains.test.ts +24 -3
  12. package/core/acps.ts +51 -143
  13. package/core/client.ts +13 -8
  14. package/core/clientTypes.ts +29 -4
  15. package/core/config.ts +25 -1
  16. package/core/consts.ts +14 -2
  17. package/core/decrypt/verifyDecryptResult.ts +4 -3
  18. package/core/encrypt/encryptInputsBuilder.ts +6 -0
  19. package/core/encrypt/zkPackProveVerify.ts +7 -1
  20. package/core/error.ts +1 -0
  21. package/core/fetchKeys.ts +2 -2
  22. package/core/index.ts +9 -1
  23. package/core/keyStore.ts +23 -10
  24. package/core/taskManager.ts +48 -0
  25. package/core/test/acps.test.ts +91 -1
  26. package/core/test/client.test.ts +41 -0
  27. package/core/test/config.test.ts +16 -0
  28. package/core/test/decrypt.test.ts +11 -4
  29. package/core/test/encryptInputsBuilder.test.ts +1 -1
  30. package/core/test/fetchKeys.test.ts +23 -2
  31. package/core/test/keyStore.test.ts +18 -6
  32. package/core/test/taskManager.test.ts +57 -0
  33. package/core/types.ts +8 -0
  34. package/dist/acps.cjs +314 -35
  35. package/dist/acps.d.cts +251 -4
  36. package/dist/acps.d.ts +251 -4
  37. package/dist/acps.js +2 -2
  38. package/dist/chains.cjs +26 -0
  39. package/dist/chains.d.cts +45 -1
  40. package/dist/chains.d.ts +45 -1
  41. package/dist/chains.js +1 -1
  42. package/dist/{chunk-43USWPEH.js → chunk-GGI7Y6AW.js} +59 -264
  43. package/dist/chunk-KSOV5B4B.js +50 -0
  44. package/dist/{chunk-ESMZCFJY.js → chunk-NMKMCAI3.js} +4 -2
  45. package/dist/{chunk-Q7CBWGQX.js → chunk-UJYOBHYA.js} +374 -37
  46. package/dist/{chunk-N6IDQRRU.js → chunk-UKQAJK4N.js} +25 -1
  47. package/dist/{clientTypes-BN3nbzYM.d.ts → clientTypes-5YPPR16m.d.ts} +77 -14
  48. package/dist/{clientTypes-CYZjFznO.d.cts → clientTypes-D3aIXmUp.d.cts} +77 -14
  49. package/dist/core.cjs +271 -136
  50. package/dist/core.d.cts +29 -8
  51. package/dist/core.d.ts +29 -8
  52. package/dist/core.js +4 -4
  53. package/dist/node.cjs +266 -136
  54. package/dist/node.d.cts +2 -2
  55. package/dist/node.d.ts +2 -2
  56. package/dist/node.js +5 -5
  57. package/dist/{acp-Wi6isVQI.d.cts → onchain-utils-CUAK8glx.d.cts} +31 -1
  58. package/dist/{acp-Wi6isVQI.d.ts → onchain-utils-CUAK8glx.d.ts} +31 -1
  59. package/dist/web.cjs +360 -150
  60. package/dist/web.d.cts +49 -3
  61. package/dist/web.d.ts +49 -3
  62. package/dist/web.js +51 -20
  63. package/dist/zkProve.worker.cjs +69 -15
  64. package/dist/zkProve.worker.js +23 -17
  65. package/node/storage.ts +2 -2
  66. package/node/test/inherited.test.ts +5 -1
  67. package/node/test/storage-memory-fallback.test.ts +47 -0
  68. package/package.json +5 -3
  69. package/web/index.ts +100 -26
  70. package/web/test/countingZkProveWorker.ts +31 -0
  71. package/web/test/inherited.web.test.ts +5 -1
  72. package/web/test/mainThreadPool.web.test.ts +174 -0
  73. package/web/test/threadPoolBenchHelper.ts +140 -0
  74. package/web/test/threadPoolOff.web.test.ts +17 -0
  75. package/web/test/threadPoolOn.web.test.ts +80 -0
  76. package/web/test/workerInitRace.web.test.ts +79 -0
  77. package/web/tfheThreadPool.ts +140 -0
  78. package/web/workerManager.ts +4 -2
  79. package/web/zkProve.worker.ts +42 -17
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,7 @@ 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';
29
30
 
30
31
  /**
31
32
  * Main ACP utilities - functional approach for React compatibility
@@ -408,4 +409,12 @@ export const ACPUtils = {
408
409
  const publicAcp = ACPUtils.getPublic(acp);
409
410
  return checkACPValidityOnChain(publicAcp, publicClient);
410
411
  },
412
+
413
+ /**
414
+ * The acp on-chain status as a value instead of a revert: 'valid' or why not, and given a
415
+ * handle, whether the acp may read it (see `ACPAccessStatus`).
416
+ */
417
+ checkAccessOnChain: async (acp: ACP, publicClient: PublicClient, handle?: bigint | Hex): Promise<ACPAccessStatus> => {
418
+ return getACPAccessStatusOnChain(ACPUtils.getPublic(acp), publicClient, handle);
419
+ },
411
420
  };
package/acps/index.ts CHANGED
@@ -19,6 +19,17 @@ export type {
19
19
  // Main utilities
20
20
  export { ACPUtils } from './acp.js';
21
21
 
22
+ // On-chain sharing: registry / revoker ABIs, the registry payload and share id of a sharing ACP
23
+ export * from './registry.js';
24
+
25
+ // Addresses the chain ACL serves (TaskManager -> acl() -> default revoker, share registry)
26
+ export {
27
+ getAclAddress,
28
+ getAclServedAddresses,
29
+ clearAclServedAddresses,
30
+ type AclServedAddresses,
31
+ } from './onchain-utils.js';
32
+
22
33
  // Validation utilities
23
34
  export {
24
35
  // Self ACP validators
@@ -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
+ };
@@ -0,0 +1,73 @@
1
+ import { encodeAbiParameters, keccak256, parseAbi, type AbiParameterToPrimitiveType, type Hex } from 'viem';
2
+ import { ACPUtils } from './acp.js';
3
+ import type { ACP } from './types.js';
4
+
5
+ /**
6
+ * The on-chain sharing pieces: the share registry and revoker ABIs, the registry payload of a
7
+ * sharing ACP and its share id. Exported so an app can send these writes through its own
8
+ * transaction path and still agree with the registry on ids.
9
+ */
10
+
11
+ // ACP default revoker (timestamp-based revocation) — interface shared by all revokers
12
+ export const ACP_REVOKER_ABI = parseAbi([
13
+ 'function revokeSingle(uint256 id)',
14
+ 'function revokeAllExisting()',
15
+ 'function disabled(address issuer, uint256 id) view returns (bool)',
16
+ ]);
17
+
18
+ // SHARE (on-chain, via the ACPShareRegistry)
19
+
20
+ export const ACP_SHARE_REGISTRY_ABI = parseAbi([
21
+ 'struct ACP { address issuer; uint64 expiration; address recipient; uint256 revokerData; address revokerContract; uint8 scope; address[] contracts; bytes32[] handles; bytes32 sealingKey; bytes issuerSignature; bytes recipientSignature; }',
22
+ 'function share(ACP calldata acp) external returns (bytes32)',
23
+ 'function removeShare(bytes32 shareId) external',
24
+ 'function sharesFor(address recipient) external view returns (ACP[] memory)',
25
+ 'function getShare(bytes32 shareId) external view returns (ACP memory)',
26
+ 'function isShareValid(bytes32 shareId) external view returns (bool)',
27
+ ]);
28
+
29
+ export const ACP_SHARE_TUPLE_ABI = [
30
+ {
31
+ type: 'tuple',
32
+ components: [
33
+ { name: 'issuer', type: 'address' },
34
+ { name: 'expiration', type: 'uint64' },
35
+ { name: 'recipient', type: 'address' },
36
+ { name: 'revokerData', type: 'uint256' },
37
+ { name: 'revokerContract', type: 'address' },
38
+ { name: 'scope', type: 'uint8' },
39
+ { name: 'contracts', type: 'address[]' },
40
+ { name: 'handles', type: 'bytes32[]' },
41
+ { name: 'sealingKey', type: 'bytes32' },
42
+ { name: 'issuerSignature', type: 'bytes' },
43
+ { name: 'recipientSignature', type: 'bytes' },
44
+ ],
45
+ },
46
+ ] as const;
47
+
48
+ const ZERO_BYTES32 = `0x${'0'.repeat(64)}` as Hex;
49
+
50
+ /** The on-chain payload for a sharing ACP: recipient-side fields empty. */
51
+ export const toChainShare = (acp: ACP) => {
52
+ // Same public struct as the off-chain export flow (ACPUtils.getPublic), with
53
+ // the recipient-side fields blanked — the recipient supplies them at import —
54
+ // and uint fields widened for the ABI encoder.
55
+ const pub = ACPUtils.getPublic(acp, true);
56
+ return {
57
+ ...pub,
58
+ expiration: BigInt(pub.expiration),
59
+ revokerData: BigInt(pub.revokerData),
60
+ sealingKey: ZERO_BYTES32,
61
+ recipientSignature: '0x' as Hex,
62
+ };
63
+ };
64
+
65
+ /** Mirrors the registry's `keccak256(abi.encode(acp))` share id. */
66
+ export const computeShareId = (acp: ACP): Hex => {
67
+ const p = toChainShare(acp);
68
+ return keccak256(encodeAbiParameters(ACP_SHARE_TUPLE_ABI, [p]));
69
+ };
70
+
71
+ /** The share id of a registry payload as read back from `sharesFor` / `getShare`. */
72
+ export const shareIdOfChainShare = (share: AbiParameterToPrimitiveType<(typeof ACP_SHARE_TUPLE_ABI)[0]>): Hex =>
73
+ keccak256(encodeAbiParameters(ACP_SHARE_TUPLE_ABI, [share]));
@@ -0,0 +1,116 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import type { PublicClient } from 'viem';
3
+ import { checkACPValidityOnChain, getACPAccessStatusOnChain } from '../onchain-utils';
4
+ import type { ACPPublic } from '../types';
5
+
6
+ const TASK_MANAGER = '0x0000000000000000000000000000000000005000';
7
+ const ACL = '0x00000000000000000000000000000000000000ac';
8
+ const ISSUER = '0x00000000000000000000000000000000000000b0';
9
+
10
+ const acp: ACPPublic = {
11
+ issuer: ISSUER,
12
+ expiration: 2_000_000_000,
13
+ recipient: '0x00000000000000000000000000000000000000a1',
14
+ revokerData: 1,
15
+ revokerContract: '0x00000000000000000000000000000000000000cc',
16
+ scope: 2,
17
+ contracts: [],
18
+ handles: [],
19
+ sealingKey: `0x${'0'.repeat(64)}`,
20
+ issuerSignature: '0x01',
21
+ recipientSignature: '0x02',
22
+ } as unknown as ACPPublic;
23
+
24
+ /**
25
+ * A revert as a viem copy OTHER than the SDK one reports it: the same shape, but no class
26
+ * identity the SDK could match with `instanceof`.
27
+ */
28
+ function foreignRevert(errorName: string) {
29
+ const reverted = { name: 'ContractFunctionRevertedError', data: { errorName } };
30
+ return Object.assign(new Error(`The contract function reverted: ${errorName}`), {
31
+ walk: (fn: (e: unknown) => boolean) => (fn(reverted) ? reverted : null),
32
+ });
33
+ }
34
+
35
+ function client({ simulate, reads = {} }: { simulate?: () => unknown; reads?: Record<string, () => unknown> }) {
36
+ return {
37
+ chain: { id: 31337 },
38
+ readContract: vi.fn(async ({ functionName }: { functionName: string }) => {
39
+ if (functionName === 'getTm') return TASK_MANAGER;
40
+ if (functionName === 'acl') return ACL;
41
+ const read = reads[functionName];
42
+ if (!read) throw new Error(`unexpected read ${functionName}`);
43
+ return read();
44
+ }),
45
+ simulateContract: vi.fn(async () => simulate?.()),
46
+ } as unknown as PublicClient;
47
+ }
48
+
49
+ describe('getACPAccessStatusOnChain', () => {
50
+ it('returns valid when the ACL accepts the ACP', async () => {
51
+ expect(await getACPAccessStatusOnChain(acp, client({}))).toBe('valid');
52
+ });
53
+
54
+ it.each([
55
+ ['PermissionInvalid_Expired', 'expired'],
56
+ ['PermissionInvalid_Disabled', 'revoked'],
57
+ ['PermissionInvalid_IssuerSignature', 'invalid-issuer-signature'],
58
+ ['PermissionInvalid_RecipientSignature', 'invalid-recipient-signature'],
59
+ ])('maps %s to %s, even from another viem copy', async (errorName, status) => {
60
+ const publicClient = client({
61
+ simulate: () => {
62
+ throw foreignRevert(errorName);
63
+ },
64
+ });
65
+ expect(await getACPAccessStatusOnChain(acp, publicClient)).toBe(status);
66
+ });
67
+
68
+ it('tells out-of-scope from issuer-not-allowed with the issuer own allowance', async () => {
69
+ const outOfScope = client({ reads: { isAllowedWithPermission: () => false, isAllowed: () => true } });
70
+ const notAllowed = client({ reads: { isAllowedWithPermission: () => false, isAllowed: () => false } });
71
+ const allowed = client({ reads: { isAllowedWithPermission: () => true } });
72
+ expect(await getACPAccessStatusOnChain(acp, outOfScope, 1n)).toBe('out-of-scope');
73
+ expect(await getACPAccessStatusOnChain(acp, notAllowed, '0x01')).toBe('issuer-not-allowed');
74
+ expect(await getACPAccessStatusOnChain(acp, allowed, 1n)).toBe('allowed');
75
+ });
76
+
77
+ it('maps a revert on the handle check too', async () => {
78
+ const publicClient = client({
79
+ reads: {
80
+ isAllowedWithPermission: () => {
81
+ throw foreignRevert('PermissionInvalid_Disabled');
82
+ },
83
+ },
84
+ });
85
+ expect(await getACPAccessStatusOnChain(acp, publicClient, 1n)).toBe('revoked');
86
+ });
87
+
88
+ it('reads a revert reported only as text with the error selector', async () => {
89
+ const publicClient = client({
90
+ simulate: () => {
91
+ throw new Error('Execution reverted with reason: RPC eth_call failed: custom error 0xcbd3a966.');
92
+ },
93
+ });
94
+ expect(await getACPAccessStatusOnChain(acp, publicClient)).toBe('revoked');
95
+ });
96
+
97
+ it('still throws what is not an ACL revert', async () => {
98
+ const publicClient = client({
99
+ simulate: () => {
100
+ throw new Error('fetch failed');
101
+ },
102
+ });
103
+ await expect(getACPAccessStatusOnChain(acp, publicClient)).rejects.toThrow('fetch failed');
104
+ });
105
+ });
106
+
107
+ describe('checkACPValidityOnChain', () => {
108
+ it('names the revert from another viem copy instead of rethrowing it raw', async () => {
109
+ const publicClient = client({
110
+ simulate: () => {
111
+ throw foreignRevert('PermissionInvalid_Disabled');
112
+ },
113
+ });
114
+ await expect(checkACPValidityOnChain(acp, publicClient)).rejects.toThrow(/^PermissionInvalid_Disabled$/);
115
+ });
116
+ });
package/acps/types.ts CHANGED
@@ -221,6 +221,22 @@ export type SharedACP = Expand<
221
221
  }
222
222
  >;
223
223
 
224
+ /**
225
+ * An ACP on-chain status, as `client.acp.checkAccess` reports it instead of a revert.
226
+ * Without a handle: 'valid' or the reason it is not. With a handle, a valid ACP reports
227
+ * 'allowed', 'out-of-scope' (the ACP scope does not cover the handle) or
228
+ * 'issuer-not-allowed' (the issuer itself may not read the handle, so no ACP of theirs can).
229
+ */
230
+ export type ACPAccessStatus =
231
+ | 'valid'
232
+ | 'allowed'
233
+ | 'out-of-scope'
234
+ | 'issuer-not-allowed'
235
+ | 'expired'
236
+ | 'revoked'
237
+ | 'invalid-issuer-signature'
238
+ | 'invalid-recipient-signature';
239
+
224
240
  /**
225
241
  * A share read back from the on-chain ACPShareRegistry: the posted payload
226
242
  * (SharedACP minus the client-side name/type) plus its registry id.
@@ -0,0 +1,14 @@
1
+ import { defineChain } from '../defineChain.js';
2
+
3
+ /**
4
+ * Arbitrum One mainnet chain configuration
5
+ */
6
+ export const arbitrum = defineChain({
7
+ id: 42161,
8
+ name: 'Arbitrum One',
9
+ network: 'arbitrum',
10
+ coFheUrl: 'https://cofhe.mainnet.fhenix.zone',
11
+ verifierUrl: 'https://cofhe-vrf.mainnet.fhenix.zone',
12
+ thresholdNetworkUrl: 'https://cofhe-tn.mainnet.fhenix.zone',
13
+ environment: 'MAINNET',
14
+ });
@@ -0,0 +1,14 @@
1
+ import { defineChain } from '../defineChain.js';
2
+
3
+ /**
4
+ * Ethereum mainnet chain configuration
5
+ */
6
+ export const ethereum = defineChain({
7
+ id: 1,
8
+ name: 'Ethereum',
9
+ network: 'ethereum',
10
+ coFheUrl: 'https://cofhe.mainnet.fhenix.zone',
11
+ verifierUrl: 'https://cofhe-vrf.mainnet.fhenix.zone',
12
+ thresholdNetworkUrl: 'https://cofhe-tn.mainnet.fhenix.zone',
13
+ environment: 'MAINNET',
14
+ });
package/chains/index.ts CHANGED
@@ -5,17 +5,21 @@ export type { CofheChain, Environment } from './types.js';
5
5
  import { sepolia } from './chains/sepolia.js';
6
6
  import { arbSepolia } from './chains/arbSepolia.js';
7
7
  import { baseSepolia } from './chains/baseSepolia.js';
8
+ import { ethereum } from './chains/ethereum.js';
9
+ import { arbitrum } from './chains/arbitrum.js';
8
10
  import { hardhat } from './chains/hardhat.js';
9
11
  import { localcofhe } from './chains/localcofhe.js';
10
12
  import { stagingCofhe } from './chains/stagingCofhe.js';
11
13
 
12
- export { sepolia, arbSepolia, baseSepolia, hardhat, localcofhe, stagingCofhe };
14
+ export { sepolia, arbSepolia, baseSepolia, ethereum, arbitrum, hardhat, localcofhe, stagingCofhe };
13
15
 
14
16
  // Export all chains as a collection
15
17
  export const chains = {
16
18
  sepolia,
17
19
  arbSepolia,
18
20
  baseSepolia,
21
+ ethereum,
22
+ arbitrum,
19
23
  hardhat,
20
24
  localcofhe,
21
25
  stagingCofhe,