@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.
- package/CHANGELOG.md +35 -0
- package/acps/acp.ts +35 -7
- package/acps/index.ts +28 -12
- package/acps/onchain-utils.ts +188 -49
- package/acps/registry.ts +231 -0
- package/acps/sealing.ts +9 -3
- package/acps/share-metadata/appKey.ts +8 -0
- package/acps/share-metadata/decode.ts +120 -0
- package/acps/share-metadata/describe.ts +198 -0
- package/acps/share-metadata/encode.ts +147 -0
- package/acps/share-metadata/erc7984.ts +26 -0
- package/acps/share-metadata/index.ts +9 -0
- package/acps/share-metadata/layout.ts +69 -0
- package/acps/share-metadata/presets.ts +116 -0
- package/acps/share-metadata/schema.ts +47 -0
- package/acps/share-metadata/test/codec.test.ts +140 -0
- package/acps/share-metadata/test/describe.test.ts +112 -0
- package/acps/share-metadata/test/fixtures.ts +157 -0
- package/acps/share-metadata/test/live.test.ts +56 -0
- package/acps/share-metadata/test/presets.test.ts +119 -0
- package/acps/share-metadata/test/verify.test.ts +144 -0
- package/acps/share-metadata/verify.ts +155 -0
- package/acps/store.ts +48 -18
- package/acps/test/acp.test.ts +102 -7
- package/acps/test/localstorage.test.ts +28 -1
- package/acps/test/onchainAccessStatus.test.ts +116 -0
- package/acps/test/registry.test.ts +144 -0
- package/acps/test/sealing.test.ts +23 -0
- package/acps/test/store.test.ts +71 -0
- package/acps/test/utils.test.ts +63 -0
- package/acps/test/validation.test.ts +0 -4
- package/acps/types.ts +59 -6
- package/acps/utils.ts +15 -2
- package/acps/validation.ts +3 -1
- package/chains/chains/arbitrum.ts +14 -0
- package/chains/chains/ethereum.ts +14 -0
- package/chains/index.ts +5 -1
- package/chains/test/chains.test.ts +24 -3
- package/core/acps.ts +164 -207
- package/core/client.ts +44 -13
- package/core/clientTypes.ts +50 -7
- package/core/config.ts +33 -1
- package/core/consts.ts +14 -2
- package/core/decrypt/apiError.ts +4 -1
- package/core/decrypt/verifyDecryptResult.ts +8 -4
- package/core/encrypt/cofheMocksZkVerifySign.ts +19 -2
- package/core/encrypt/encryptInputsBuilder.ts +6 -0
- package/core/encrypt/zkPackProveVerify.ts +7 -1
- package/core/error.ts +1 -0
- package/core/fetchKeys.ts +2 -2
- package/core/index.ts +9 -1
- package/core/keyStore.ts +23 -10
- package/core/taskManager.ts +48 -0
- package/core/test/acpDefaults.test.ts +108 -19
- package/core/test/acpStoreAccountCase.test.ts +80 -0
- package/core/test/acps.test.ts +128 -3
- package/core/test/apiError.test.ts +8 -0
- package/core/test/appKeyConfig.test.ts +94 -0
- package/core/test/client.test.ts +41 -0
- package/core/test/config.test.ts +24 -0
- package/core/test/decrypt.test.ts +11 -4
- package/core/test/decryptBuilders.test.ts +110 -2
- package/core/test/encryptInputsBuilder.test.ts +1 -1
- package/core/test/fetchKeys.test.ts +23 -2
- package/core/test/incomingShares.test.ts +84 -0
- package/core/test/keyStore.test.ts +18 -6
- package/core/test/mocksEncryptRange.test.ts +90 -0
- package/core/test/taskManager.test.ts +57 -0
- package/core/test/utils.test.ts +128 -0
- package/core/test/verifyDecryptResult.test.ts +51 -0
- package/core/types.ts +8 -0
- package/core/utils.ts +16 -9
- package/dist/acps.cjs +984 -64
- package/dist/acps.d.cts +589 -10
- package/dist/acps.d.ts +589 -10
- package/dist/acps.js +2 -2
- package/dist/chains.cjs +26 -0
- package/dist/chains.d.cts +45 -1
- package/dist/chains.d.ts +45 -1
- package/dist/chains.js +1 -1
- package/dist/chunk-6ZW76ZKB.js +228 -0
- package/dist/chunk-BQWHPIYU.js +1828 -0
- package/dist/chunk-KSOV5B4B.js +50 -0
- package/dist/{chunk-43USWPEH.js → chunk-OL3DZN7A.js} +159 -523
- package/dist/{chunk-N6IDQRRU.js → chunk-UKQAJK4N.js} +25 -1
- package/dist/chunk-XQ32JG3T.js +174 -0
- package/dist/{clientTypes-CYZjFznO.d.cts → clientTypes-C0A9ji5O.d.cts} +132 -29
- package/dist/{clientTypes-BN3nbzYM.d.ts → clientTypes-CcU5WvdV.d.ts} +132 -29
- package/dist/core.cjs +3269 -2626
- package/dist/core.d.cts +32 -8
- package/dist/core.d.ts +32 -8
- package/dist/core.js +5 -4
- package/dist/node.cjs +3074 -2436
- package/dist/node.d.cts +2 -2
- package/dist/node.d.ts +2 -2
- package/dist/node.js +20 -19
- package/dist/{acp-Wi6isVQI.d.cts → onchain-utils-D9Z33IP7.d.cts} +166 -12
- package/dist/{acp-Wi6isVQI.d.ts → onchain-utils-D9Z33IP7.d.ts} +166 -12
- package/dist/web.cjs +3204 -2493
- package/dist/web.d.cts +49 -3
- package/dist/web.d.ts +49 -3
- package/dist/web.js +52 -27
- package/dist/zkProve.worker.cjs +79 -18
- package/dist/zkProve.worker.js +24 -24
- package/node/index.ts +1 -10
- package/node/storage.ts +20 -9
- package/node/test/inherited.test.ts +5 -1
- package/node/test/storage-memory-fallback.test.ts +47 -0
- package/node/test/storage-permissions.test.ts +40 -0
- package/package.json +3 -1
- package/web/index.ts +101 -36
- package/web/test/countingZkProveWorker.ts +31 -0
- package/web/test/inherited.web.test.ts +5 -1
- package/web/test/mainThreadPool.web.test.ts +174 -0
- package/web/test/threadPoolBenchHelper.ts +134 -0
- package/web/test/threadPoolOff.web.test.ts +17 -0
- package/web/test/threadPoolOn.web.test.ts +80 -0
- package/web/test/workerInitRace.web.test.ts +79 -0
- package/web/tfheThreadPool.ts +140 -0
- package/web/workerManager.ts +4 -2
- package/web/zkProve.worker.ts +42 -26
- package/dist/chunk-ESMZCFJY.js +0 -12
- 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
|
|
18
|
+
} from './types';
|
|
18
19
|
|
|
19
20
|
// Main utilities
|
|
20
|
-
export { ACPUtils } from './acp
|
|
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
|
|
57
|
+
} from './validation';
|
|
42
58
|
|
|
43
59
|
// Signature utilities
|
|
44
|
-
export { SignatureUtils, getSignatureTypesAndMessage, SignatureTypes } from './signature
|
|
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
|
|
75
|
+
} from './store';
|
|
60
76
|
|
|
61
77
|
// Sealing utilities
|
|
62
|
-
export { GenerateSealingKey, seal, unsealWithPrivateKey, type SealingKeyPair } from './sealing
|
|
63
|
-
export type { EthEncryptedData } from './sealing
|
|
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
|
|
67
|
-
export * from './acp
|
|
68
|
-
export * from './validation
|
|
69
|
-
export * from './signature
|
|
70
|
-
export * from './store
|
|
82
|
+
export * from './types';
|
|
83
|
+
export * from './acp';
|
|
84
|
+
export * from './validation';
|
|
85
|
+
export * from './signature';
|
|
86
|
+
export * from './store';
|
package/acps/onchain-utils.ts
CHANGED
|
@@ -1,13 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
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:
|
|
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
|
-
|
|
90
|
-
if (
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
};
|