@cofhe/sdk 0.8.0 → 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/acps/acp.ts +25 -6
- package/acps/index.ts +20 -15
- package/acps/registry.ts +166 -8
- 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/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 +43 -6
- package/acps/utils.ts +15 -2
- package/acps/validation.ts +3 -1
- package/core/acps.ts +123 -74
- package/core/client.ts +33 -7
- package/core/clientTypes.ts +25 -7
- package/core/config.ts +8 -0
- package/core/decrypt/apiError.ts +4 -1
- package/core/decrypt/verifyDecryptResult.ts +4 -1
- package/core/encrypt/cofheMocksZkVerifySign.ts +19 -2
- package/core/test/acpDefaults.test.ts +108 -19
- package/core/test/acpStoreAccountCase.test.ts +80 -0
- package/core/test/acps.test.ts +38 -3
- package/core/test/apiError.test.ts +8 -0
- package/core/test/appKeyConfig.test.ts +94 -0
- package/core/test/config.test.ts +8 -0
- package/core/test/decryptBuilders.test.ts +110 -2
- package/core/test/incomingShares.test.ts +84 -0
- package/core/test/mocksEncryptRange.test.ts +90 -0
- package/core/test/utils.test.ts +128 -0
- package/core/test/verifyDecryptResult.test.ts +51 -0
- package/core/utils.ts +16 -9
- package/dist/acps.cjs +674 -33
- package/dist/acps.d.cts +367 -35
- package/dist/acps.d.ts +367 -35
- package/dist/acps.js +2 -2
- package/dist/chunk-6ZW76ZKB.js +228 -0
- package/dist/{chunk-UJYOBHYA.js → chunk-BQWHPIYU.js} +660 -198
- package/dist/{chunk-GGI7Y6AW.js → chunk-OL3DZN7A.js} +110 -269
- package/dist/chunk-XQ32JG3T.js +174 -0
- package/dist/{clientTypes-D3aIXmUp.d.cts → clientTypes-C0A9ji5O.d.cts} +61 -21
- package/dist/{clientTypes-5YPPR16m.d.ts → clientTypes-CcU5WvdV.d.ts} +61 -21
- package/dist/core.cjs +3239 -2731
- package/dist/core.d.cts +6 -3
- package/dist/core.d.ts +6 -3
- package/dist/core.js +4 -3
- package/dist/node.cjs +3046 -2538
- package/dist/node.d.cts +2 -2
- package/dist/node.d.ts +2 -2
- package/dist/node.js +18 -17
- package/dist/{onchain-utils-CUAK8glx.d.cts → onchain-utils-D9Z33IP7.d.cts} +136 -12
- package/dist/{onchain-utils-CUAK8glx.d.ts → onchain-utils-D9Z33IP7.d.ts} +136 -12
- package/dist/web.cjs +3074 -2573
- package/dist/web.d.cts +2 -2
- package/dist/web.d.ts +2 -2
- package/dist/web.js +4 -10
- package/dist/zkProve.worker.cjs +14 -7
- package/dist/zkProve.worker.js +2 -8
- package/node/index.ts +1 -10
- package/node/storage.ts +18 -7
- package/node/test/storage-permissions.test.ts +40 -0
- package/package.json +4 -4
- package/web/index.ts +1 -10
- package/web/test/threadPoolBenchHelper.ts +1 -7
- package/web/zkProve.worker.ts +1 -10
- package/dist/chunk-NMKMCAI3.js +0 -14
package/core/acps.ts
CHANGED
|
@@ -16,7 +16,12 @@ import {
|
|
|
16
16
|
ACP_SHARE_REGISTRY_ABI,
|
|
17
17
|
toChainShare,
|
|
18
18
|
computeShareId,
|
|
19
|
-
|
|
19
|
+
readSharesFor,
|
|
20
|
+
readShare,
|
|
21
|
+
decodeShareAppKey,
|
|
22
|
+
encodeShareMetadata,
|
|
23
|
+
type PostedShare,
|
|
24
|
+
type ShareLabel,
|
|
20
25
|
getAclServedAddresses,
|
|
21
26
|
clearAclServedAddresses,
|
|
22
27
|
} from '@/acps';
|
|
@@ -98,13 +103,28 @@ export type ImportActivationOptions = {
|
|
|
98
103
|
activate?: boolean;
|
|
99
104
|
};
|
|
100
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
|
+
|
|
101
115
|
const importShared = async (
|
|
102
116
|
options: ImportSharedACPOptions | string,
|
|
103
117
|
publicClient: PublicClient,
|
|
104
118
|
walletClient: WalletClient,
|
|
105
|
-
{ activate = true }:
|
|
119
|
+
{ activate = true, name }: ImportSharedOptions = {}
|
|
106
120
|
): Promise<RecipientACP> => {
|
|
107
|
-
return createACPWithSign(
|
|
121
|
+
return createACPWithSign(
|
|
122
|
+
options,
|
|
123
|
+
publicClient,
|
|
124
|
+
walletClient,
|
|
125
|
+
(o, pc, wc) => ACPUtils.importSharedAndSign(o, pc, wc, { name }),
|
|
126
|
+
activate
|
|
127
|
+
);
|
|
108
128
|
};
|
|
109
129
|
|
|
110
130
|
// ACP UTILS
|
|
@@ -222,15 +242,14 @@ const getOrCreateSharingACP = async (
|
|
|
222
242
|
// CONFIG DEFAULTS
|
|
223
243
|
|
|
224
244
|
/**
|
|
225
|
-
* Applies the config's ACP
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
* ("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")
|
|
230
249
|
* - contracts: injected only when the options carry NO scope fields at all —
|
|
231
250
|
* injecting scope makes the created acp non-global by default
|
|
232
251
|
*/
|
|
233
|
-
const
|
|
252
|
+
const applyACPDefaultsFromChain = async <
|
|
234
253
|
T extends {
|
|
235
254
|
revokerData?: number;
|
|
236
255
|
revokerContract?: string;
|
|
@@ -241,20 +260,19 @@ const applyACPDefaults = <
|
|
|
241
260
|
>(
|
|
242
261
|
options: T,
|
|
243
262
|
acpConfig: { defaultRevoker?: Record<number, Hex>; defaultContractScopes?: Record<number, Hex[]> } | undefined,
|
|
263
|
+
publicClient: PublicClient,
|
|
244
264
|
chainId: number
|
|
245
|
-
): T => {
|
|
265
|
+
): Promise<T> => {
|
|
246
266
|
const result = { ...options };
|
|
247
267
|
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
// (revokeAllExisting at time T still kills this acp for any T >= id).
|
|
257
|
-
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
|
+
}
|
|
258
276
|
}
|
|
259
277
|
|
|
260
278
|
const defaultContracts = acpConfig?.defaultContractScopes?.[chainId];
|
|
@@ -267,34 +285,20 @@ const applyACPDefaults = <
|
|
|
267
285
|
};
|
|
268
286
|
|
|
269
287
|
/**
|
|
270
|
-
*
|
|
271
|
-
*
|
|
272
|
-
*
|
|
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.
|
|
273
292
|
*/
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
options: T,
|
|
284
|
-
acpConfig: { defaultRevoker?: Record<number, Hex>; defaultContractScopes?: Record<number, Hex[]> } | undefined,
|
|
285
|
-
publicClient: PublicClient,
|
|
286
|
-
chainId: number
|
|
287
|
-
): Promise<T> => {
|
|
288
|
-
const hasExplicitRevoker =
|
|
289
|
-
acpConfig?.defaultRevoker?.[chainId] != null || options.revokerData != null || options.revokerContract != null;
|
|
290
|
-
if (hasExplicitRevoker) return applyACPDefaults(options, acpConfig, chainId);
|
|
291
|
-
|
|
292
|
-
const served = await getAclServedAddresses(publicClient, chainId);
|
|
293
|
-
const effectiveConfig =
|
|
294
|
-
served.defaultRevoker != null
|
|
295
|
-
? { ...acpConfig, defaultRevoker: { ...acpConfig?.defaultRevoker, [chainId]: served.defaultRevoker } }
|
|
296
|
-
: acpConfig;
|
|
297
|
-
return applyACPDefaults(options, effectiveConfig, chainId);
|
|
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
|
+
);
|
|
300
|
+
}
|
|
301
|
+
return revokerData;
|
|
298
302
|
};
|
|
299
303
|
|
|
300
304
|
// REVOKE (on-chain, via the acp's revoker contract)
|
|
@@ -390,14 +394,33 @@ const isACPRevoked = async (acp: ACP, publicClient: PublicClient): Promise<boole
|
|
|
390
394
|
});
|
|
391
395
|
};
|
|
392
396
|
|
|
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 };
|
|
404
|
+
|
|
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 });
|
|
412
|
+
};
|
|
413
|
+
|
|
393
414
|
/**
|
|
394
415
|
* Post a signed sharing ACP to the on-chain share registry for its recipient
|
|
395
|
-
* 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.
|
|
396
418
|
*/
|
|
397
419
|
const shareOnChain = async (
|
|
398
420
|
acp: ACP,
|
|
399
421
|
walletClient: WalletClient,
|
|
400
|
-
registry: Hex
|
|
422
|
+
registry: Hex,
|
|
423
|
+
options: ShareOnChainOptions = {}
|
|
401
424
|
): Promise<{ txHash: Hex; shareId: Hex }> => {
|
|
402
425
|
if (acp.type !== 'sharing') {
|
|
403
426
|
throw new Error(`Cannot share a '${acp.type}' ACP on-chain — only 'sharing' ACPs are shareable.`);
|
|
@@ -409,12 +432,13 @@ const shareOnChain = async (
|
|
|
409
432
|
if (walletClient.account.address.toLowerCase() !== acp.issuer.toLowerCase()) {
|
|
410
433
|
throw new Error('Only the ACP issuer can share it on-chain');
|
|
411
434
|
}
|
|
435
|
+
const metadata = metadataOf(acp, options);
|
|
412
436
|
|
|
413
437
|
const txHash = await walletClient.writeContract({
|
|
414
438
|
address: registry,
|
|
415
439
|
abi: ACP_SHARE_REGISTRY_ABI,
|
|
416
440
|
functionName: 'share',
|
|
417
|
-
args: [toChainShare(acp)],
|
|
441
|
+
args: [toChainShare(acp), metadata],
|
|
418
442
|
account: walletClient.account,
|
|
419
443
|
chain: walletClient.chain ?? null,
|
|
420
444
|
});
|
|
@@ -422,46 +446,71 @@ const shareOnChain = async (
|
|
|
422
446
|
return { txHash, shareId: computeShareId(acp) };
|
|
423
447
|
};
|
|
424
448
|
|
|
425
|
-
|
|
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
|
+
*/
|
|
426
480
|
const getIncomingShares = async (
|
|
427
481
|
publicClient: PublicClient,
|
|
428
482
|
registry: Hex,
|
|
429
|
-
recipient: Hex
|
|
483
|
+
recipient: Hex,
|
|
484
|
+
{ appKey }: { appKey?: Hex } = {}
|
|
430
485
|
): Promise<IncomingShare[]> => {
|
|
431
|
-
const
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
functionName: 'sharesFor',
|
|
435
|
-
args: [recipient],
|
|
436
|
-
});
|
|
486
|
+
const shares = (await readSharesFor(publicClient, registry, recipient)).posted.map(toIncomingShare);
|
|
487
|
+
return appKey == null ? shares : shares.filter((share) => share.appKey === appKey);
|
|
488
|
+
};
|
|
437
489
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
contracts: [...s.contracts],
|
|
447
|
-
handles: [...s.handles],
|
|
448
|
-
issuerSignature: s.issuerSignature,
|
|
449
|
-
}));
|
|
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);
|
|
450
498
|
};
|
|
451
499
|
|
|
452
500
|
/**
|
|
453
501
|
* Import a share read from the registry: fills the recipient's sealing key,
|
|
454
502
|
* signs, stores and (unless `activate: false`) activates — the on-chain
|
|
455
|
-
* counterpart of importing an exported JSON blob.
|
|
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
|
|
456
505
|
* until dismissed.
|
|
457
506
|
*/
|
|
458
507
|
const importFromChain = async (
|
|
459
508
|
share: IncomingShare,
|
|
460
509
|
publicClient: PublicClient,
|
|
461
510
|
walletClient: WalletClient,
|
|
462
|
-
importOptions:
|
|
511
|
+
importOptions: ImportSharedOptions = {}
|
|
463
512
|
): Promise<RecipientACP> => {
|
|
464
|
-
const { shareId: _shareId, ...options } = share;
|
|
513
|
+
const { shareId: _shareId, appKey: _appKey, ...options } = share;
|
|
465
514
|
return importShared({ ...options, type: 'sharing' }, publicClient, walletClient, importOptions);
|
|
466
515
|
};
|
|
467
516
|
|
|
@@ -519,11 +568,11 @@ export const acps = {
|
|
|
519
568
|
|
|
520
569
|
shareOnChain,
|
|
521
570
|
getIncomingShares,
|
|
571
|
+
getShareFromChain,
|
|
522
572
|
importFromChain,
|
|
523
573
|
removeShareOnChain,
|
|
524
574
|
computeShareId,
|
|
525
575
|
|
|
526
|
-
applyACPDefaults,
|
|
527
576
|
applyACPDefaultsFromChain,
|
|
528
577
|
getAclServedAddresses,
|
|
529
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;
|
|
@@ -254,7 +259,7 @@ export function createCofheClientBase<TConfig extends CofheConfig>(
|
|
|
254
259
|
const { publicClient, walletClient } = clients ?? connectStore.getState();
|
|
255
260
|
const chainId = await publicClient!.getChainId();
|
|
256
261
|
return acps.createSharing(
|
|
257
|
-
await acps.applyACPDefaultsFromChain(options, opts.config.acp, publicClient!, chainId),
|
|
262
|
+
await acps.applyACPDefaultsFromChain({ ...withAppKey(), ...options }, opts.config.acp, publicClient!, chainId),
|
|
258
263
|
publicClient!,
|
|
259
264
|
walletClient!
|
|
260
265
|
);
|
|
@@ -262,9 +267,9 @@ export function createCofheClientBase<TConfig extends CofheConfig>(
|
|
|
262
267
|
|
|
263
268
|
importShared: async (options: ImportSharedACPOptions | string, importOptions = {}) => {
|
|
264
269
|
_requireConnected();
|
|
265
|
-
const { activate, ...clients } = importOptions;
|
|
270
|
+
const { activate, name, ...clients } = importOptions;
|
|
266
271
|
const { publicClient, walletClient } = clients.publicClient ? clients : connectStore.getState();
|
|
267
|
-
return acps.importShared(options, publicClient!, walletClient!, { activate });
|
|
272
|
+
return acps.importShared(options, publicClient!, walletClient!, { activate, name });
|
|
268
273
|
},
|
|
269
274
|
|
|
270
275
|
// Get or create methods (require connection)
|
|
@@ -288,7 +293,7 @@ export function createCofheClientBase<TConfig extends CofheConfig>(
|
|
|
288
293
|
return acps.getOrCreateSharingACP(
|
|
289
294
|
publicClient!,
|
|
290
295
|
walletClient!,
|
|
291
|
-
await acps.applyACPDefaultsFromChain(options, opts.config.acp, publicClient!, _chainId),
|
|
296
|
+
await acps.applyACPDefaultsFromChain({ ...withAppKey(), ...options }, opts.config.acp, publicClient!, _chainId),
|
|
292
297
|
_chainId,
|
|
293
298
|
_account
|
|
294
299
|
);
|
|
@@ -320,12 +325,12 @@ export function createCofheClientBase<TConfig extends CofheConfig>(
|
|
|
320
325
|
},
|
|
321
326
|
|
|
322
327
|
// On-chain sharing (require connection + config.acp.sharingRegistry)
|
|
323
|
-
shareOnChain: async (acp) => {
|
|
328
|
+
shareOnChain: async (acp, options) => {
|
|
324
329
|
_requireConnected();
|
|
325
330
|
const { publicClient, walletClient } = connectStore.getState();
|
|
326
331
|
const chainId = await publicClient!.getChainId();
|
|
327
332
|
const sharingRegistry = await _resolveSharingRegistry(publicClient!, chainId);
|
|
328
|
-
return acps.shareOnChain(acp, walletClient!, sharingRegistry);
|
|
333
|
+
return acps.shareOnChain(acp, walletClient!, sharingRegistry, { ...withAppKey(), ...options });
|
|
329
334
|
},
|
|
330
335
|
|
|
331
336
|
getIncomingShares: async () => {
|
|
@@ -334,7 +339,28 @@ export function createCofheClientBase<TConfig extends CofheConfig>(
|
|
|
334
339
|
const chainId = await publicClient!.getChainId();
|
|
335
340
|
const account = walletClient!.account!.address;
|
|
336
341
|
const sharingRegistry = await _resolveSharingRegistry(publicClient!, chainId);
|
|
337
|
-
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
|
+
});
|
|
338
364
|
},
|
|
339
365
|
|
|
340
366
|
importFromChain: async (share, options) => {
|
package/core/clientTypes.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { type DecryptForTxBuilderUnset } from './decrypt/decryptForTxBuilder.js'
|
|
|
6
6
|
import { type EncryptInputsBuilderUnset } from './encrypt/encryptInputsBuilder.js';
|
|
7
7
|
import { type ZkBuilderAndCrsGenerator, type ZkProveWorkerFunction } from './encrypt/zkPackProveVerify.js';
|
|
8
8
|
import { type FheKeyDeserializer } from './fetchKeys.js';
|
|
9
|
-
import { acps, type
|
|
9
|
+
import { acps, type ImportSharedOptions, type ShareOnChainOptions } from './acps.js';
|
|
10
10
|
import type { EncryptableItem, FheTypes, TfheInitializer } from './types.js';
|
|
11
11
|
import type { ACPUtils } from 'acps/acp.js';
|
|
12
12
|
import type {
|
|
@@ -19,7 +19,9 @@ import type {
|
|
|
19
19
|
RecipientACP,
|
|
20
20
|
SelfACP,
|
|
21
21
|
IncomingShare,
|
|
22
|
+
LabelledShare,
|
|
22
23
|
} from 'acps/types.js';
|
|
24
|
+
import type { ShareLabelCheck, ShareLabelVerifyMode } from 'acps/share-metadata/verify.js';
|
|
23
25
|
|
|
24
26
|
// CLIENT
|
|
25
27
|
|
|
@@ -77,9 +79,10 @@ export type CofheClientACPsClients = {
|
|
|
77
79
|
|
|
78
80
|
/**
|
|
79
81
|
* Options for `importShared`. `activate: false` stores the imported acp without making it the active
|
|
80
|
-
* one (default `true`).
|
|
82
|
+
* one (default `true`). `name` names it (default "Shared by 0x1234…abcd"). Clients default to the
|
|
83
|
+
* connected ones; pass both to override.
|
|
81
84
|
*/
|
|
82
|
-
export type CofheClientImportSharedOptions =
|
|
85
|
+
export type CofheClientImportSharedOptions = ImportSharedOptions &
|
|
83
86
|
(CofheClientACPsClients | { publicClient?: undefined; walletClient?: undefined });
|
|
84
87
|
|
|
85
88
|
export type CofheClientACPs = {
|
|
@@ -121,12 +124,27 @@ export type CofheClientACPs = {
|
|
|
121
124
|
*/
|
|
122
125
|
checkAccess: (acp: ACP, handle?: bigint | `0x${string}`) => Promise<ACPAccessStatus>;
|
|
123
126
|
|
|
124
|
-
/**
|
|
125
|
-
|
|
126
|
-
|
|
127
|
+
/**
|
|
128
|
+
* Post a signed sharing ACP to the on-chain share registry (ACL-served, or config `acp.sharingRegistry`),
|
|
129
|
+
* optionally with the labels of its handles (`options.labels`, one per handle) or an encoded blob.
|
|
130
|
+
*/
|
|
131
|
+
shareOnChain: (acp: ACP, options?: ShareOnChainOptions) => Promise<{ txHash: `0x${string}`; shareId: `0x${string}` }>;
|
|
132
|
+
/** Importable shares addressed to the connected account (unexpired, not revoked), with their metadata. */
|
|
127
133
|
getIncomingShares: () => Promise<IncomingShare[]>;
|
|
134
|
+
/** One share by id, as posted; null when unknown or removed. */
|
|
135
|
+
getShareFromChain: (shareId: `0x${string}`) => Promise<IncomingShare | null>;
|
|
136
|
+
/**
|
|
137
|
+
* Check a share's labels against the chain, index for index with its handles, as far as
|
|
138
|
+
* `verify` says (default 'all'). Takes a share read from the registry or an acp imported from
|
|
139
|
+
* exported JSON (or the issuer's sharing acp). Null for a share without metadata. Throws on a
|
|
140
|
+
* malformed blob.
|
|
141
|
+
*/
|
|
142
|
+
verifyShareLabels: (
|
|
143
|
+
share: LabelledShare,
|
|
144
|
+
options?: { verify?: ShareLabelVerifyMode }
|
|
145
|
+
) => Promise<ShareLabelCheck[] | null>;
|
|
128
146
|
/** Import a share read from the registry: sign as recipient, store and (unless `activate: false`) activate. */
|
|
129
|
-
importFromChain: (share: IncomingShare, options?:
|
|
147
|
+
importFromChain: (share: IncomingShare, options?: ImportSharedOptions) => Promise<RecipientACP>;
|
|
130
148
|
/** Recipient-side: remove a share from the registry (after import, or to decline). */
|
|
131
149
|
dismissShare: (shareId: `0x${string}`) => Promise<`0x${string}`>;
|
|
132
150
|
/** Issuer-side: retract a pending share from the registry. */
|
package/core/config.ts
CHANGED
|
@@ -73,6 +73,13 @@ export type CofheConfig = {
|
|
|
73
73
|
* names a registry.
|
|
74
74
|
*/
|
|
75
75
|
sharingRegistry?: Record<number, `0x${string}`>;
|
|
76
|
+
/**
|
|
77
|
+
* The name of this app, e.g. 'explorer.fhenix.io'. Set, every share the client creates
|
|
78
|
+
* carries the app's key (`shareAppKey(app)`, kept in the share metadata), and
|
|
79
|
+
* `getIncomingShares` lists only the shares posted for this app. Unset, shares carry no
|
|
80
|
+
* key and every importable share is listed.
|
|
81
|
+
*/
|
|
82
|
+
app?: string;
|
|
76
83
|
};
|
|
77
84
|
/** Mocks configs */
|
|
78
85
|
mocks: {
|
|
@@ -138,6 +145,7 @@ export const CofheConfigSchema = z.object({
|
|
|
138
145
|
defaultRevoker: z.custom<Record<number, `0x${string}`>>().optional(),
|
|
139
146
|
defaultContractScopes: z.custom<Record<number, `0x${string}`[]>>().optional(),
|
|
140
147
|
sharingRegistry: z.custom<Record<number, `0x${string}`>>().optional(),
|
|
148
|
+
app: z.string().min(1).optional(),
|
|
141
149
|
})
|
|
142
150
|
.optional()
|
|
143
151
|
.default({}),
|
package/core/decrypt/apiError.ts
CHANGED
|
@@ -48,7 +48,10 @@ const BACKEND_ERROR_CODE_TO_COFHE_ERROR_CODE: Record<BackendApiErrorCode, CofheE
|
|
|
48
48
|
};
|
|
49
49
|
|
|
50
50
|
function isBackendApiErrorCode(value: string): value is BackendApiErrorCode {
|
|
51
|
-
|
|
51
|
+
// Own-property check, not `in`: `in` also matches Object.prototype keys, so codes like
|
|
52
|
+
// `toString` or `constructor` would be treated as recognised and the lookup would return a
|
|
53
|
+
// function instead of a CofheErrorCode.
|
|
54
|
+
return Object.prototype.hasOwnProperty.call(BACKEND_ERROR_CODE_TO_COFHE_ERROR_CODE, value);
|
|
52
55
|
}
|
|
53
56
|
|
|
54
57
|
export type ParsedApiError = {
|
|
@@ -30,6 +30,9 @@ const buildDecryptResultHash = (ctHash: bigint, cleartext: bigint, chainId: numb
|
|
|
30
30
|
*
|
|
31
31
|
* The recovered signer must equal the on-chain configured `decryptResultSigner`.
|
|
32
32
|
*
|
|
33
|
+
* Fails closed: if the chain has no `decryptResultSigner` set (zero address), this returns `false`.
|
|
34
|
+
* The on-chain `verifyDecryptResult` instead accepts any signature in that case.
|
|
35
|
+
*
|
|
33
36
|
* This mirrors the TaskManager decrypt-result hash format:
|
|
34
37
|
* `keccak256(abi.encodePacked(result, encType, chainId, ctHash))`
|
|
35
38
|
*
|
|
@@ -52,7 +55,7 @@ export async function verifyDecryptResult(
|
|
|
52
55
|
args: [],
|
|
53
56
|
});
|
|
54
57
|
|
|
55
|
-
if (isAddressEqual(expectedSigner, zeroAddress)) return
|
|
58
|
+
if (isAddressEqual(expectedSigner, zeroAddress)) return false;
|
|
56
59
|
|
|
57
60
|
const ctHash = BigInt(handle);
|
|
58
61
|
const messageHash = buildDecryptResultHash(ctHash, cleartext, chainId);
|
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import { type EncryptableItem, FheTypes } from '../types.js';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
type VerifyBatchResult,
|
|
4
|
+
MAX_UINT8,
|
|
5
|
+
MAX_UINT16,
|
|
6
|
+
MAX_UINT32,
|
|
7
|
+
MAX_UINT64,
|
|
8
|
+
MAX_UINT128,
|
|
9
|
+
MAX_UINT160,
|
|
10
|
+
} from './zkPackProveVerify.js';
|
|
11
|
+
import { toBigIntOrThrow, validateBigIntInRange } from '../utils.js';
|
|
3
12
|
import { createWalletClient, http, encodePacked, keccak256, type PublicClient, type WalletClient } from 'viem';
|
|
4
13
|
import { MockZkVerifierAbi } from './MockZkVerifierAbi.js';
|
|
5
14
|
import { hardhat } from 'viem/chains';
|
|
@@ -19,7 +28,9 @@ function createMockZkVerifierSigner() {
|
|
|
19
28
|
});
|
|
20
29
|
}
|
|
21
30
|
/**
|
|
22
|
-
* The mocks don't use a tfhe builder, so we check the encryptable bits here to
|
|
31
|
+
* The mocks don't use a tfhe builder, so we check the encryptable bits and value ranges here to
|
|
32
|
+
* preserve parity with `zkPack`. Without the range check, MockCoFHE masks an out-of-range value
|
|
33
|
+
* with the utype mask (e.g. uint8(256) -> 0) instead of rejecting it like the production path.
|
|
23
34
|
*/
|
|
24
35
|
export async function cofheMocksCheckEncryptableBits(items: EncryptableItem[]): Promise<void> {
|
|
25
36
|
let totalBits = 0;
|
|
@@ -30,22 +41,27 @@ export async function cofheMocksCheckEncryptableBits(items: EncryptableItem[]):
|
|
|
30
41
|
break;
|
|
31
42
|
}
|
|
32
43
|
case FheTypes.Uint8: {
|
|
44
|
+
validateBigIntInRange(toBigIntOrThrow(item.data), MAX_UINT8);
|
|
33
45
|
totalBits += 8;
|
|
34
46
|
break;
|
|
35
47
|
}
|
|
36
48
|
case FheTypes.Uint16: {
|
|
49
|
+
validateBigIntInRange(toBigIntOrThrow(item.data), MAX_UINT16);
|
|
37
50
|
totalBits += 16;
|
|
38
51
|
break;
|
|
39
52
|
}
|
|
40
53
|
case FheTypes.Uint32: {
|
|
54
|
+
validateBigIntInRange(toBigIntOrThrow(item.data), MAX_UINT32);
|
|
41
55
|
totalBits += 32;
|
|
42
56
|
break;
|
|
43
57
|
}
|
|
44
58
|
case FheTypes.Uint64: {
|
|
59
|
+
validateBigIntInRange(toBigIntOrThrow(item.data), MAX_UINT64);
|
|
45
60
|
totalBits += 64;
|
|
46
61
|
break;
|
|
47
62
|
}
|
|
48
63
|
case FheTypes.Uint128: {
|
|
64
|
+
validateBigIntInRange(toBigIntOrThrow(item.data), MAX_UINT128);
|
|
49
65
|
totalBits += 128;
|
|
50
66
|
break;
|
|
51
67
|
}
|
|
@@ -55,6 +71,7 @@ export async function cofheMocksCheckEncryptableBits(items: EncryptableItem[]):
|
|
|
55
71
|
// break;
|
|
56
72
|
// }
|
|
57
73
|
case FheTypes.Uint160: {
|
|
74
|
+
validateBigIntInRange(toBigIntOrThrow(item.data), MAX_UINT160);
|
|
58
75
|
totalBits += 160;
|
|
59
76
|
break;
|
|
60
77
|
}
|