@cofhe/sdk 0.8.0 → 1.0.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 +51 -0
- package/acps/acp.ts +25 -6
- package/acps/index.ts +20 -15
- package/acps/onchain-utils.ts +15 -10
- package/acps/registry.ts +166 -8
- package/acps/sealing.ts +31 -5
- 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/aclServedAddresses.test.ts +65 -0
- 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 +74 -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 +796 -132
- package/dist/acps.d.cts +367 -35
- package/dist/acps.d.ts +367 -35
- package/dist/acps.js +2 -2
- package/dist/{chunk-UJYOBHYA.js → chunk-3L2IVPMI.js} +688 -203
- package/dist/chunk-J23FYAI2.js +174 -0
- package/dist/{chunk-GGI7Y6AW.js → chunk-O2ITEUEU.js} +110 -269
- package/dist/chunk-WJGTPW7K.js +228 -0
- package/dist/{clientTypes-D3aIXmUp.d.cts → clientTypes-IO6b5Um8.d.cts} +61 -21
- package/dist/{clientTypes-5YPPR16m.d.ts → clientTypes-b1pOxOUE.d.ts} +61 -21
- package/dist/core.cjs +3115 -2584
- package/dist/core.d.cts +6 -3
- package/dist/core.d.ts +6 -3
- package/dist/core.js +4 -3
- package/dist/node.cjs +3001 -2470
- 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-CgOslubH.d.cts} +139 -15
- package/dist/{onchain-utils-CUAK8glx.d.ts → onchain-utils-CgOslubH.d.ts} +139 -15
- package/dist/web.cjs +3011 -2487
- 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/acps/types.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type EthEncryptedData } from './sealing.js';
|
|
2
2
|
import { type Hex } from 'viem';
|
|
3
|
+
import type { ShareLabel } from './share-metadata/schema.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* EIP712 related types
|
|
@@ -46,6 +47,12 @@ export interface ACPPrivate {
|
|
|
46
47
|
* Never leaves the client.
|
|
47
48
|
*/
|
|
48
49
|
sealingPrivateKey: Hex;
|
|
50
|
+
/**
|
|
51
|
+
* (sharing / recipient) The share metadata: the labels of its handles, a v3 blob
|
|
52
|
+
* (`decodeShareMetadata` / `describeShareMetadata`). Not signed; travels with the share, in the
|
|
53
|
+
* exported JSON or in the registry's `Shared` event. Absent when the share has none.
|
|
54
|
+
*/
|
|
55
|
+
metadata?: Hex;
|
|
49
56
|
/**
|
|
50
57
|
* EIP712 domain used to sign this acp.
|
|
51
58
|
* Should not be set manually, included in metadata as part of serialization flows.
|
|
@@ -191,17 +198,30 @@ export type CreateSharingACPOptions = ACPScopeOptions & {
|
|
|
191
198
|
expiration?: number;
|
|
192
199
|
revokerData?: number;
|
|
193
200
|
revokerContract?: string;
|
|
201
|
+
/**
|
|
202
|
+
* The labels of the handles, one per handle in order. Kept on the acp as `metadata`, then
|
|
203
|
+
* written into `export()` and posted by `shareOnChain`.
|
|
204
|
+
*/
|
|
205
|
+
labels?: readonly ShareLabel[];
|
|
206
|
+
/**
|
|
207
|
+
* The key of the app the share is for (`shareAppKey(app)`), kept in `metadata` with the labels.
|
|
208
|
+
* A client configured with `acp: { app }` fills it in; a recipient client with the same `app`
|
|
209
|
+
* lists the share, one with another app does not.
|
|
210
|
+
*/
|
|
211
|
+
appKey?: Hex;
|
|
194
212
|
};
|
|
195
213
|
|
|
214
|
+
/** A share as exported (`SharedACP`) or read from the registry. A `name` in it is ignored. */
|
|
196
215
|
export type ImportSharedACPOptions = ACPScopeOptions & {
|
|
197
216
|
type?: 'sharing';
|
|
198
217
|
issuer: string;
|
|
199
218
|
recipient: string;
|
|
200
219
|
issuerSignature: string;
|
|
201
|
-
name?: string;
|
|
202
220
|
expiration: number;
|
|
203
221
|
revokerData?: number;
|
|
204
222
|
revokerContract?: string;
|
|
223
|
+
/** The share metadata (labels blob), kept on the recipient acp; `0x` or absent for none. */
|
|
224
|
+
metadata?: string;
|
|
205
225
|
};
|
|
206
226
|
|
|
207
227
|
/** An ACP is plain JSON-serializable data — the serialized form is the ACP itself. */
|
|
@@ -210,14 +230,16 @@ export type SerializedACP = ACP;
|
|
|
210
230
|
/**
|
|
211
231
|
* The share payload produced by `ACPUtils.export()` — the full public component
|
|
212
232
|
* with `sealingKey`/`recipientSignature` left for the recipient to fill, plus
|
|
213
|
-
* the
|
|
233
|
+
* the acp type. The issuer's `name` is not exported: it is their own note, and the
|
|
234
|
+
* recipient names the share themselves. Fixed shape: every field is always present
|
|
214
235
|
* (zero-values instead of omissions), so importers can parse a single schema.
|
|
215
|
-
* Mirrors the on-chain sharing payload struct field-for-field.
|
|
236
|
+
* Mirrors the on-chain sharing payload struct field-for-field. The one optional
|
|
237
|
+
* field is `metadata`, the labels of the handles, present only when the share has them.
|
|
216
238
|
*/
|
|
217
239
|
export type SharedACP = Expand<
|
|
218
240
|
Omit<ACPPublic, 'sealingKey' | 'recipientSignature'> & {
|
|
219
|
-
name: string;
|
|
220
241
|
type: 'sharing';
|
|
242
|
+
metadata?: Hex;
|
|
221
243
|
}
|
|
222
244
|
>;
|
|
223
245
|
|
|
@@ -239,9 +261,24 @@ export type ACPAccessStatus =
|
|
|
239
261
|
|
|
240
262
|
/**
|
|
241
263
|
* A share read back from the on-chain ACPShareRegistry: the posted payload
|
|
242
|
-
* (SharedACP minus
|
|
264
|
+
* (SharedACP minus its type), its registry id, and the
|
|
265
|
+
* metadata blob posted with it (`0x` when none) — for a SNAPSHOT share, the
|
|
266
|
+
* labels of its handles (`decodeShareMetadata` / `describeShareMetadata`).
|
|
267
|
+
*/
|
|
268
|
+
export type IncomingShare = Expand<
|
|
269
|
+
Omit<SharedACP, 'type' | 'metadata'> & {
|
|
270
|
+
shareId: Hex;
|
|
271
|
+
metadata: Hex;
|
|
272
|
+
/** The key of the app the share was posted for (`shareAppKey(app)`), when it names one. */
|
|
273
|
+
appKey?: Hex;
|
|
274
|
+
}
|
|
275
|
+
>;
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* What the share label helpers read: the handles, their issuer and the metadata blob. An
|
|
279
|
+
* `IncomingShare` (on-chain route) and a sharing or recipient `ACP` (exported JSON route) both fit.
|
|
243
280
|
*/
|
|
244
|
-
export type
|
|
281
|
+
export type LabelledShare = { issuer: Hex; handles: readonly Hex[]; metadata?: Hex };
|
|
245
282
|
|
|
246
283
|
/**
|
|
247
284
|
* A type representing the acp fields that are used to generate the hash
|
package/acps/utils.ts
CHANGED
|
@@ -2,9 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
declare const BigInt: (value: string | number | bigint) => bigint;
|
|
4
4
|
|
|
5
|
+
const HEX_BODY_REGEX = /^[0-9a-fA-F]*$/;
|
|
6
|
+
|
|
5
7
|
export const fromHexString = (hexString: string): Uint8Array => {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
+
// Strip `0x` before padding: padding first turns `0x123` into `00x123`, which no
|
|
9
|
+
// longer matches the prefix and decodes to garbage bytes.
|
|
10
|
+
const body = hexString.replace(/^0x/, '');
|
|
11
|
+
|
|
12
|
+
// `parseInt` maps a non-hex pair to NaN, which `Uint8Array` stores as 0. Without this check a
|
|
13
|
+
// malformed sealing key silently becomes zero bytes instead of failing.
|
|
14
|
+
if (!HEX_BODY_REGEX.test(body)) {
|
|
15
|
+
// The value is omitted on purpose: this helper also receives sealing private keys.
|
|
16
|
+
throw new Error('Invalid hex string: contains non-hexadecimal characters');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const padded = body.length % 2 === 1 ? `0${body}` : body;
|
|
20
|
+
const arr = padded.match(/.{1,2}/g);
|
|
8
21
|
if (!arr) return new Uint8Array();
|
|
9
22
|
return new Uint8Array(arr.map((byte) => parseInt(byte, 16)));
|
|
10
23
|
};
|
package/acps/validation.ts
CHANGED
|
@@ -85,6 +85,7 @@ const zACPWithDefaults = z.object({
|
|
|
85
85
|
handles: handlesSchema,
|
|
86
86
|
issuerSignature: bytesSchema.optional().default('0x'),
|
|
87
87
|
recipientSignature: bytesSchema.optional().default('0x'),
|
|
88
|
+
metadata: bytesSchema.optional(),
|
|
88
89
|
});
|
|
89
90
|
|
|
90
91
|
const zACPWithSealingKeys = zACPWithDefaults.extend({
|
|
@@ -228,7 +229,6 @@ export const ImportACPOptionsValidator = z
|
|
|
228
229
|
type: z.literal('recipient').optional().default('recipient'),
|
|
229
230
|
issuer: addressNotZeroSchema,
|
|
230
231
|
recipient: addressNotZeroSchema,
|
|
231
|
-
name: z.string().optional().default('Unnamed ACP'),
|
|
232
232
|
expiration: z.int(),
|
|
233
233
|
revokerData: z.int().optional().default(0),
|
|
234
234
|
revokerContract: addressSchema.optional().default(zeroAddress),
|
|
@@ -237,6 +237,8 @@ export const ImportACPOptionsValidator = z
|
|
|
237
237
|
handles: handlesSchema,
|
|
238
238
|
issuerSignature: bytesNotEmptySchema,
|
|
239
239
|
recipientSignature: bytesSchema.optional().default('0x'),
|
|
240
|
+
/** The share metadata (labels blob); `0x` means none and is dropped */
|
|
241
|
+
metadata: bytesSchema.optional().transform((val) => (val === '0x' ? undefined : val)),
|
|
240
242
|
})
|
|
241
243
|
.refine(...ExternalValidatorRefinement)
|
|
242
244
|
.transform(withDerivedScope)
|
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);
|