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