@cofhe/sdk 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/acps/acp.ts +411 -0
  3. package/acps/index.ts +70 -0
  4. package/{permits → acps}/onchain-utils.ts +49 -24
  5. package/acps/sealing.ts +90 -0
  6. package/acps/signature.ts +89 -0
  7. package/acps/store.ts +172 -0
  8. package/acps/test/acp.test.ts +615 -0
  9. package/acps/test/localstorage.test.ts +105 -0
  10. package/acps/test/sealing.test.ts +77 -0
  11. package/acps/test/store.test.ts +88 -0
  12. package/acps/test/validation.test.ts +361 -0
  13. package/acps/test-utils.ts +32 -0
  14. package/acps/types.ts +252 -0
  15. package/acps/validation.ts +392 -0
  16. package/adapters/test/ethers5.test.ts +4 -1
  17. package/adapters/test/ethers6.test.ts +4 -1
  18. package/adapters/test/wagmi.test.ts +5 -2
  19. package/chains/chains/stagingCofhe.ts +21 -0
  20. package/chains/index.ts +3 -1
  21. package/chains/test/chains.test.ts +2 -1
  22. package/core/acps.ts +625 -0
  23. package/core/client.ts +136 -39
  24. package/core/clientTypes.ts +52 -41
  25. package/core/config.ts +66 -5
  26. package/core/debug.ts +72 -0
  27. package/core/decrypt/MockThresholdNetworkAbi.ts +20 -11
  28. package/core/decrypt/apiError.ts +104 -0
  29. package/core/decrypt/cofheMocksDecryptForTx.ts +11 -11
  30. package/core/decrypt/cofheMocksDecryptForView.ts +7 -7
  31. package/core/decrypt/decryptForTxBuilder.ts +102 -102
  32. package/core/decrypt/decryptForViewBuilder.ts +90 -90
  33. package/core/decrypt/submitRetry.ts +38 -30
  34. package/core/decrypt/tnDecryptV1.ts +5 -5
  35. package/core/decrypt/tnDecryptV2.ts +45 -34
  36. package/core/decrypt/tnSealOutputV1.ts +4 -3
  37. package/core/decrypt/tnSealOutputV2.ts +44 -31
  38. package/core/encrypt/cofheMocksZkVerifySign.ts +59 -74
  39. package/core/encrypt/encryptInputsBuilder.ts +86 -52
  40. package/core/encrypt/zkPackProveVerify.ts +25 -18
  41. package/core/error.ts +34 -6
  42. package/core/index.ts +14 -14
  43. package/core/test/acpDefaults.test.ts +52 -0
  44. package/core/test/acps.test.ts +596 -0
  45. package/core/test/apiError.test.ts +130 -0
  46. package/core/test/client.test.ts +22 -19
  47. package/core/test/config.test.ts +25 -5
  48. package/core/test/decrypt.test.ts +40 -35
  49. package/core/test/decryptBuilders.test.ts +68 -68
  50. package/core/test/decryptErrorCodes.test.ts +217 -0
  51. package/core/test/encryptInputsBuilder.test.ts +72 -41
  52. package/core/test/pollCallbacks.test.ts +89 -18
  53. package/core/test/stagingRedirect.ts +18 -0
  54. package/core/test/submitRetry.test.ts +182 -0
  55. package/core/types.ts +9 -69
  56. package/dist/acp-Wi6isVQI.d.cts +407 -0
  57. package/dist/acp-Wi6isVQI.d.ts +407 -0
  58. package/dist/acps.cjs +1081 -0
  59. package/dist/acps.d.cts +480 -0
  60. package/dist/acps.d.ts +480 -0
  61. package/dist/acps.js +2 -0
  62. package/dist/chains.cjs +14 -1
  63. package/dist/chains.d.cts +30 -1
  64. package/dist/chains.d.ts +30 -1
  65. package/dist/chains.js +1 -1
  66. package/dist/{chunk-PE5V5CCV.js → chunk-43USWPEH.js} +1000 -604
  67. package/dist/{chunk-MTRAXQXC.js → chunk-N6IDQRRU.js} +14 -2
  68. package/dist/chunk-Q7CBWGQX.js +1029 -0
  69. package/dist/{clientTypes-CyUvRRzA.d.ts → clientTypes-BN3nbzYM.d.ts} +342 -165
  70. package/dist/{clientTypes-BDy1qIBu.d.cts → clientTypes-CYZjFznO.d.cts} +342 -165
  71. package/dist/core.cjs +1431 -1026
  72. package/dist/core.d.cts +55 -10
  73. package/dist/core.d.ts +55 -10
  74. package/dist/core.js +3 -3
  75. package/dist/node.cjs +1356 -976
  76. package/dist/node.d.cts +2 -2
  77. package/dist/node.d.ts +2 -2
  78. package/dist/node.js +3 -3
  79. package/dist/web.cjs +1356 -976
  80. package/dist/web.d.cts +2 -2
  81. package/dist/web.d.ts +2 -2
  82. package/dist/web.js +3 -3
  83. package/node/test/inherited.test.ts +75 -65
  84. package/node/test/tfheinit.test.ts +23 -8
  85. package/package.json +6 -6
  86. package/web/test/client.web.test.ts +5 -1
  87. package/web/test/inherited.web.test.ts +75 -65
  88. package/web/test/tfheinit.web.test.ts +14 -5
  89. package/web/test/worker.config.web.test.ts +38 -23
  90. package/web/test/worker.output.web.test.ts +25 -24
  91. package/core/encrypt/encryptUtils.ts +0 -67
  92. package/core/permits.ts +0 -206
  93. package/core/test/permits.test.ts +0 -534
  94. package/dist/chunk-VB62WYPL.js +0 -978
  95. package/dist/permit-DnVMDT5h.d.cts +0 -376
  96. package/dist/permit-DnVMDT5h.d.ts +0 -376
  97. package/dist/permits.cjs +0 -1026
  98. package/dist/permits.d.cts +0 -353
  99. package/dist/permits.d.ts +0 -353
  100. package/dist/permits.js +0 -2
  101. package/permits/index.ts +0 -68
  102. package/permits/permit.ts +0 -385
  103. package/permits/sealing.ts +0 -131
  104. package/permits/signature.ts +0 -79
  105. package/permits/store.ts +0 -157
  106. package/permits/test/localstorage.test.ts +0 -113
  107. package/permits/test/permit.test.ts +0 -557
  108. package/permits/test/sealing.test.ts +0 -84
  109. package/permits/test/store.test.ts +0 -88
  110. package/permits/test/validation.test.ts +0 -361
  111. package/permits/test-utils.ts +0 -28
  112. package/permits/types.ts +0 -204
  113. package/permits/validation.ts +0 -327
  114. /package/{permits → acps}/utils.ts +0 -0
@@ -0,0 +1,21 @@
1
+ import { defineChain } from '../defineChain.js';
2
+
3
+ /**
4
+ * CoFHE staging chain configuration.
5
+ *
6
+ * Hosted staging environment for testing the consuming-contract binding fix
7
+ * (`FhenixProtocol/cofhe-contracts#77` / `zee-k-verifier#37`) against a real verifier
8
+ * service. Shares the same chain ID as `localcofhe` (420105) - it appears to be the same
9
+ * devnet genesis, hosted remotely rather than run locally - but is intentionally kept as
10
+ * a separate entry since its services (verifier, in particular) are on a different,
11
+ * fix-specific deployment.
12
+ */
13
+ export const stagingCofhe = defineChain({
14
+ id: 420105,
15
+ name: 'CoFHE Staging',
16
+ network: 'cofhe-staging',
17
+ coFheUrl: 'https://staging-cofhe-v1.sw-dom.co',
18
+ verifierUrl: 'https://staging-zk-verifier-v1.sw-dom.co',
19
+ thresholdNetworkUrl: 'https://staging-dispatcher-v1.sw-dom.co',
20
+ environment: 'TESTNET',
21
+ });
package/chains/index.ts CHANGED
@@ -7,8 +7,9 @@ import { arbSepolia } from './chains/arbSepolia.js';
7
7
  import { baseSepolia } from './chains/baseSepolia.js';
8
8
  import { hardhat } from './chains/hardhat.js';
9
9
  import { localcofhe } from './chains/localcofhe.js';
10
+ import { stagingCofhe } from './chains/stagingCofhe.js';
10
11
 
11
- export { sepolia, arbSepolia, baseSepolia, hardhat, localcofhe };
12
+ export { sepolia, arbSepolia, baseSepolia, hardhat, localcofhe, stagingCofhe };
12
13
 
13
14
  // Export all chains as a collection
14
15
  export const chains = {
@@ -17,6 +18,7 @@ export const chains = {
17
18
  baseSepolia,
18
19
  hardhat,
19
20
  localcofhe,
21
+ stagingCofhe,
20
22
  } as const;
21
23
 
22
24
  // Import CofheChain type for helper functions
@@ -3,12 +3,13 @@ import { sepolia, arbSepolia, baseSepolia, hardhat, chains, getChainById, getCha
3
3
 
4
4
  describe('Chains', () => {
5
5
  it('should export all chains', () => {
6
- expect(Object.keys(chains)).toHaveLength(5);
6
+ expect(Object.keys(chains)).toHaveLength(6);
7
7
  expect(chains).toHaveProperty('sepolia');
8
8
  expect(chains).toHaveProperty('arbSepolia');
9
9
  expect(chains).toHaveProperty('baseSepolia');
10
10
  expect(chains).toHaveProperty('hardhat');
11
11
  expect(chains).toHaveProperty('localcofhe');
12
+ expect(chains).toHaveProperty('stagingCofhe');
12
13
  });
13
14
 
14
15
  it('should have correct chain configurations', () => {
package/core/acps.ts ADDED
@@ -0,0 +1,625 @@
1
+ import {
2
+ type ImportSharedACPOptions,
3
+ ACPUtils,
4
+ type CreateSelfACPOptions,
5
+ type CreateSharingACPOptions,
6
+ type ACP,
7
+ acpStore,
8
+ type SerializedACP,
9
+ type SelfACP,
10
+ type RecipientACP,
11
+ type IncomingShare,
12
+ type SharingACP,
13
+ type ACPHashFields,
14
+ } from '@/acps';
15
+
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
+ ]);
34
+
35
+ // HELPERS
36
+
37
+ // Store an ACP without changing which acp is active.
38
+ const storeACP = async (acp: ACP, publicClient: any, walletClient: any) => {
39
+ const chainId = await publicClient.getChainId();
40
+ const account = walletClient.account!.address;
41
+
42
+ acpStore.setACP(chainId, account, acp);
43
+ };
44
+
45
+ // Store an ACP AND select it as the active acp.
46
+ const storeActiveACP = async (acp: ACP, publicClient: any, walletClient: any) => {
47
+ await storeACP(acp, publicClient, walletClient);
48
+ const chainId = await publicClient.getChainId();
49
+ const account = walletClient.account!.address;
50
+ acpStore.setActiveACPHash(chainId, account, acp.hash);
51
+ };
52
+
53
+ // Generic function to handle acp creation with error handling.
54
+ // `activate` controls whether the new acp becomes the issuer's active acp — true for
55
+ // self/imported acps (the connected user decrypts with them), false for sharing acps (those
56
+ // are delegated to a recipient and are never the issuer's own active acp).
57
+ const createACPWithSign = async <T, TACP extends ACP>(
58
+ options: T,
59
+ publicClient: PublicClient,
60
+ walletClient: WalletClient,
61
+ acpMethod: (options: T, publicClient: PublicClient, walletClient: WalletClient) => Promise<TACP>,
62
+ activate = true
63
+ ): Promise<TACP> => {
64
+ const acp = await acpMethod(options, publicClient, walletClient);
65
+ if (activate) {
66
+ await storeActiveACP(acp, publicClient, walletClient);
67
+ } else {
68
+ await storeACP(acp, publicClient, walletClient);
69
+ }
70
+ return acp;
71
+ };
72
+
73
+ // CREATE
74
+
75
+ /**
76
+ * Create an ACP usable by the connected user
77
+ * Stores the acp and selects it as the active acp
78
+ * @param options - The options for creating a self acp
79
+ * @returns The created acp or error
80
+ */
81
+ const createSelf = async (
82
+ options: CreateSelfACPOptions,
83
+ publicClient: PublicClient,
84
+ walletClient: WalletClient
85
+ ): Promise<SelfACP> => {
86
+ return createACPWithSign(options, publicClient, walletClient, ACPUtils.createSelfAndSign);
87
+ };
88
+
89
+ const createSharing = async (
90
+ options: CreateSharingACPOptions,
91
+ publicClient: PublicClient,
92
+ walletClient: WalletClient
93
+ ): Promise<SharingACP> => {
94
+ // A sharing acp is delegated to a recipient — it is never the issuer's own active acp, so
95
+ // creating one only stores it (unlike self/imported acps, which activate).
96
+ return createACPWithSign(options, publicClient, walletClient, ACPUtils.createSharingAndSign, false);
97
+ };
98
+
99
+ const importShared = async (
100
+ options: ImportSharedACPOptions | string,
101
+ publicClient: PublicClient,
102
+ walletClient: WalletClient
103
+ ): Promise<RecipientACP> => {
104
+ return createACPWithSign(options, publicClient, walletClient, ACPUtils.importSharedAndSign);
105
+ };
106
+
107
+ // ACP UTILS
108
+
109
+ const getHash = (acp: ACPHashFields) => {
110
+ return ACPUtils.getHash(acp);
111
+ };
112
+
113
+ const exportShared = (acp: ACP) => {
114
+ return ACPUtils.export(acp);
115
+ };
116
+
117
+ const serialize = (acp: ACP) => {
118
+ return ACPUtils.serialize(acp);
119
+ };
120
+
121
+ const deserialize = (serialized: SerializedACP) => {
122
+ return ACPUtils.deserialize(serialized);
123
+ };
124
+
125
+ // GET
126
+
127
+ const getACP = (chainId: number, account: string, hash: string): ACP | undefined => {
128
+ return acpStore.getACP(chainId, account, hash);
129
+ };
130
+
131
+ const getACPs = (chainId: number, account: string): Record<string, ACP> => {
132
+ return acpStore.getACPs(chainId, account);
133
+ };
134
+
135
+ const getActiveACP = (chainId: number, account: string): ACP | undefined => {
136
+ return acpStore.getActiveACP(chainId, account);
137
+ };
138
+
139
+ const getActiveACPHash = (chainId: number, account: string): string | undefined => {
140
+ return acpStore.getActiveACPHash(chainId, account);
141
+ };
142
+
143
+ const selectActiveACP = (chainId: number, account: string, hash: string): void => {
144
+ acpStore.setActiveACPHash(chainId, account, hash);
145
+ };
146
+
147
+ // GET OR CREATE
148
+
149
+ /**
150
+ * Get the active self acp if a valid one exists, otherwise create a new one.
151
+ *
152
+ * An active acp is reused only when it is a self acp and is still valid
153
+ * (signed and not expired). An expired or otherwise invalid active acp is
154
+ * treated as missing and a fresh acp is created.
155
+ *
156
+ * @param publicClient - The public client
157
+ * @param walletClient - The wallet client
158
+ * @param chainId - Optional chain ID (will use publicClient if not provided)
159
+ * @param account - Optional account (will use walletClient if not provided)
160
+ * @param options - The options for creating a self acp
161
+ * @returns The existing valid acp or a newly created one
162
+ */
163
+ const getOrCreateSelfACP = async (
164
+ publicClient: PublicClient,
165
+ walletClient: WalletClient,
166
+ chainId?: number,
167
+ account?: string,
168
+ options?: CreateSelfACPOptions
169
+ ): Promise<ACP> => {
170
+ const _chainId = chainId ?? (await publicClient.getChainId());
171
+ const _account = account ?? walletClient.account!.address;
172
+
173
+ // Try to get active acp first
174
+ const activeACP = await getActiveACP(_chainId, _account);
175
+
176
+ if (activeACP && activeACP.type === 'self' && ACPUtils.isValid(activeACP).valid) {
177
+ return activeACP;
178
+ }
179
+
180
+ // No active acp, wrong type, or expired/invalid - create new one
181
+ return createSelf(options ?? { issuer: _account, name: 'Autogenerated Self ACP' }, publicClient, walletClient);
182
+ };
183
+
184
+ /**
185
+ * Return the active acp if it is already a valid sharing acp, otherwise create and store a
186
+ * new sharing acp.
187
+ *
188
+ * The newly created acp is NOT activated: a sharing acp is delegated to a recipient and is
189
+ * never the issuer's own active acp. (The existing-active branch only matches if a sharing
190
+ * acp was made active by other means, e.g. `selectActiveACP`.)
191
+ *
192
+ * @param publicClient - The public client
193
+ * @param walletClient - The wallet client
194
+ * @param options - The options for creating a sharing acp (required)
195
+ * @param chainId - Optional chain ID (will use publicClient if not provided)
196
+ * @param account - Optional account (will use walletClient if not provided)
197
+ * @returns The existing valid active sharing acp, or a newly created (unactivated) one
198
+ */
199
+ const getOrCreateSharingACP = async (
200
+ publicClient: PublicClient,
201
+ walletClient: WalletClient,
202
+ options: CreateSharingACPOptions,
203
+ chainId?: number,
204
+ account?: string
205
+ ): Promise<ACP> => {
206
+ const _chainId = chainId ?? (await publicClient.getChainId());
207
+ const _account = account ?? walletClient.account!.address;
208
+
209
+ // Try to get active acp first
210
+ const activeACP = await getActiveACP(_chainId, _account);
211
+
212
+ if (activeACP && activeACP.type === 'sharing' && ACPUtils.isValid(activeACP).valid) {
213
+ return activeACP;
214
+ }
215
+
216
+ return createSharing(options, publicClient, walletClient);
217
+ };
218
+
219
+ // CONFIG DEFAULTS
220
+
221
+ /**
222
+ * Applies the config's ACP acp defaults to creation options (pure).
223
+ * Explicit user options always win:
224
+ * - revoker: injected only when the options carry NO revoker pair —
225
+ * revokerContract = config default, revokerData = creation timestamp
226
+ * ("every acp revocable by default")
227
+ * - contracts: injected only when the options carry NO scope fields at all —
228
+ * injecting scope makes the created acp non-global by default
229
+ */
230
+ const applyACPDefaults = <
231
+ T extends {
232
+ revokerData?: number;
233
+ revokerContract?: string;
234
+ scope?: number;
235
+ contracts?: string[];
236
+ handles?: (bigint | number | string)[];
237
+ },
238
+ >(
239
+ options: T,
240
+ acpConfig: { defaultRevoker?: Record<number, Hex>; defaultContractScopes?: Record<number, Hex[]> } | undefined,
241
+ chainId: number
242
+ ): T => {
243
+ const result = { ...options };
244
+
245
+ const defaultRevoker = acpConfig?.defaultRevoker?.[chainId];
246
+ const hasValidatorOptions = options.revokerData != null || options.revokerContract != null;
247
+ if (defaultRevoker != null && !hasValidatorOptions) {
248
+ result.revokerContract = defaultRevoker;
249
+ // Creation timestamp minus a clock-skew allowance: the revoker rejects
250
+ // future-dated ids (vs block.timestamp of the LAST block), so a local clock
251
+ // ahead of the chain — or a chain with sparse blocks — would otherwise make
252
+ // a fresh acp temporarily unusable. 60s of backdating costs nothing
253
+ // (revokeAllExisting at time T still kills this acp for any T >= id).
254
+ result.revokerData = Math.round(Date.now() / 1000) - 60;
255
+ }
256
+
257
+ const defaultContracts = acpConfig?.defaultContractScopes?.[chainId];
258
+ const hasScopeOptions = options.scope != null || options.contracts != null || options.handles != null;
259
+ if (defaultContracts != null && defaultContracts.length > 0 && !hasScopeOptions) {
260
+ result.contracts = defaultContracts;
261
+ }
262
+
263
+ return result;
264
+ };
265
+
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
+ /**
285
+ * The ACP infrastructure addresses the chain's ACL serves (TaskManager -> acl()
286
+ * -> getters). Zero addresses and pre-upgrade ACLs (getters absent -> revert)
287
+ * resolve to `undefined` — callers fall back to `acp.*` config.
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.
292
+ */
293
+ const getAclServedAddresses = async (publicClient: PublicClient, chainId: number): Promise<AclServedAddresses> => {
294
+ const cached = aclServedAddressesCache.get(chainId);
295
+ if (cached != null) return cached;
296
+
297
+ let aclAddress: Hex;
298
+ try {
299
+ aclAddress = await publicClient.readContract({
300
+ address: TASK_MANAGER_ADDRESS,
301
+ abi: ACL_SERVED_ADDRESSES_ABI,
302
+ functionName: 'acl',
303
+ });
304
+ } catch {
305
+ return {};
306
+ }
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);
354
+ };
355
+
356
+ // REVOKE (on-chain, via the acp's revoker contract)
357
+
358
+ /**
359
+ * Revoke a single acp on-chain via its revoker contract.
360
+ * Only the acp's issuer can revoke it (enforced by the revoker: revocations
361
+ * are keyed by msg.sender). The acp stays in local storage — on-chain
362
+ * validation will reject it from the next block onwards.
363
+ *
364
+ * @returns the revocation transaction hash
365
+ */
366
+ const revokeACP = async (acp: ACP, walletClient: WalletClient): Promise<Hex> => {
367
+ if (acp.revokerContract === zeroAddress || acp.revokerData === 0) {
368
+ throw new Error('ACP is not revocable: it has no revoker (revokerContract/revokerData unset)');
369
+ }
370
+ if (walletClient.account == null) throw new Error('Missing walletClient account');
371
+ if (walletClient.account.address.toLowerCase() !== acp.issuer.toLowerCase()) {
372
+ throw new Error('Only the acp issuer can revoke it');
373
+ }
374
+
375
+ return walletClient.writeContract({
376
+ address: acp.revokerContract,
377
+ abi: ACP_VALIDATOR_ABI,
378
+ functionName: 'revokeSingle',
379
+ args: [BigInt(acp.revokerData)],
380
+ account: walletClient.account,
381
+ chain: walletClient.chain,
382
+ });
383
+ };
384
+
385
+ /**
386
+ * Revoke ALL of the caller's acps created up to now (O(1) on-chain:
387
+ * a single threshold write on the revoker). ACPs created after this
388
+ * transaction remain valid.
389
+ *
390
+ * @param revokerContract - the revoker to revoke against (defaults to the
391
+ * connected account's active acp's revoker when omitted)
392
+ * @returns the revocation transaction hash
393
+ */
394
+ const revokeAllACPs = async (
395
+ walletClient: WalletClient,
396
+ publicClient: PublicClient,
397
+ revokerContract?: Hex
398
+ ): Promise<Hex> => {
399
+ if (walletClient.account == null) throw new Error('Missing walletClient account');
400
+
401
+ let revoker = revokerContract;
402
+ if (revoker == null) {
403
+ const chainId = await publicClient.getChainId();
404
+ const active = getActiveACP(chainId, walletClient.account.address);
405
+ revoker = active?.revokerContract;
406
+ }
407
+ if (revoker == null || revoker === zeroAddress) {
408
+ throw new Error('No revoker contract: pass `revokerContract` or activate a revocable acp first');
409
+ }
410
+
411
+ return walletClient.writeContract({
412
+ address: revoker,
413
+ abi: ACP_VALIDATOR_ABI,
414
+ functionName: 'revokeAllExisting',
415
+ args: [],
416
+ account: walletClient.account,
417
+ chain: walletClient.chain,
418
+ });
419
+ };
420
+
421
+ /**
422
+ * Check whether an ACP has been revoked (or is otherwise disabled) by its
423
+ * revoker. Returns false for acps without a revoker (not revocable).
424
+ */
425
+ const isACPRevoked = async (acp: ACP, publicClient: PublicClient): Promise<boolean> => {
426
+ if (acp.revokerContract === zeroAddress || acp.revokerData === 0) return false;
427
+ return publicClient.readContract({
428
+ address: acp.revokerContract,
429
+ abi: ACP_VALIDATOR_ABI,
430
+ functionName: 'disabled',
431
+ args: [acp.issuer, BigInt(acp.revokerData)],
432
+ });
433
+ };
434
+
435
+ // SHARE (on-chain, via the ACPShareRegistry)
436
+
437
+ const ACP_SHARE_REGISTRY_ABI = parseAbi([
438
+ 'struct ACP { address issuer; uint64 expiration; address recipient; uint256 revokerData; address revokerContract; uint8 scope; address[] contracts; bytes32[] handles; bytes32 sealingKey; bytes issuerSignature; bytes recipientSignature; }',
439
+ 'function share(ACP calldata acp) external returns (bytes32)',
440
+ 'function removeShare(bytes32 shareId) external',
441
+ 'function sharesFor(address recipient) external view returns (ACP[] memory)',
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
+ };
481
+
482
+ /** Mirrors the registry's `keccak256(abi.encode(acp))` share id. */
483
+ const computeShareId = (acp: ACP): Hex => {
484
+ const p = toChainShare(acp);
485
+ return keccak256(encodeAbiParameters(ACP_TUPLE, [p]));
486
+ };
487
+
488
+ /**
489
+ * 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()`.
491
+ */
492
+ const shareOnChain = async (
493
+ acp: ACP,
494
+ walletClient: WalletClient,
495
+ registry: Hex
496
+ ): Promise<{ txHash: Hex; shareId: Hex }> => {
497
+ if (acp.type !== 'sharing') {
498
+ throw new Error(`Cannot share a '${acp.type}' ACP on-chain — only 'sharing' ACPs are shareable.`);
499
+ }
500
+ if (acp.issuerSignature === '0x') {
501
+ throw new Error('Cannot share an unsigned sharing ACP — sign it first.');
502
+ }
503
+ if (walletClient.account == null) throw new Error('Missing walletClient account');
504
+ if (walletClient.account.address.toLowerCase() !== acp.issuer.toLowerCase()) {
505
+ throw new Error('Only the ACP issuer can share it on-chain');
506
+ }
507
+
508
+ const txHash = await walletClient.writeContract({
509
+ address: registry,
510
+ abi: ACP_SHARE_REGISTRY_ABI,
511
+ functionName: 'share',
512
+ args: [toChainShare(acp)],
513
+ account: walletClient.account,
514
+ chain: walletClient.chain ?? null,
515
+ });
516
+
517
+ return { txHash, shareId: computeShareId(acp) };
518
+ };
519
+
520
+ /** All importable shares addressed to `recipient` (unexpired, not revoked). */
521
+ const getIncomingShares = async (
522
+ publicClient: PublicClient,
523
+ registry: Hex,
524
+ recipient: Hex
525
+ ): Promise<IncomingShare[]> => {
526
+ const raw = await publicClient.readContract({
527
+ address: registry,
528
+ abi: ACP_SHARE_REGISTRY_ABI,
529
+ functionName: 'sharesFor',
530
+ args: [recipient],
531
+ });
532
+
533
+ return raw.map((s) => ({
534
+ shareId: keccak256(encodeAbiParameters(ACP_TUPLE, [s])),
535
+ issuer: s.issuer,
536
+ expiration: Number(s.expiration),
537
+ recipient: s.recipient,
538
+ revokerData: Number(s.revokerData),
539
+ revokerContract: s.revokerContract,
540
+ scope: Number(s.scope),
541
+ contracts: [...s.contracts],
542
+ handles: [...s.handles],
543
+ issuerSignature: s.issuerSignature,
544
+ }));
545
+ };
546
+
547
+ /**
548
+ * Import a share read from the registry: fills the recipient's sealing key,
549
+ * signs, stores and activates — the on-chain counterpart of importing an
550
+ * exported JSON blob. The share stays on-chain until dismissed.
551
+ */
552
+ const importFromChain = async (
553
+ share: IncomingShare,
554
+ publicClient: PublicClient,
555
+ walletClient: WalletClient
556
+ ): Promise<RecipientACP> => {
557
+ const { shareId: _shareId, ...options } = share;
558
+ return importShared({ ...options, type: 'sharing' }, publicClient, walletClient);
559
+ };
560
+
561
+ /** Remove a share from the registry (issuer retracts / recipient dismisses). */
562
+ const removeShareOnChain = async (shareId: Hex, walletClient: WalletClient, registry: Hex): Promise<Hex> => {
563
+ if (walletClient.account == null) throw new Error('Missing walletClient account');
564
+ return walletClient.writeContract({
565
+ address: registry,
566
+ abi: ACP_SHARE_REGISTRY_ABI,
567
+ functionName: 'removeShare',
568
+ args: [shareId],
569
+ account: walletClient.account,
570
+ chain: walletClient.chain ?? null,
571
+ });
572
+ };
573
+
574
+ // REMOVE
575
+
576
+ const removeACP = async (chainId: number, account: string, hash: string): Promise<void> =>
577
+ acpStore.removeACP(chainId, account, hash);
578
+
579
+ const removeActiveACP = async (chainId: number, account: string): Promise<void> =>
580
+ acpStore.removeActiveACPHash(chainId, account);
581
+
582
+ // EXPORT
583
+
584
+ export const acps = {
585
+ getSnapshot: acpStore.store.getState,
586
+ subscribe: acpStore.store.subscribe,
587
+
588
+ createSelf,
589
+ createSharing,
590
+ importShared,
591
+
592
+ getOrCreateSelfACP,
593
+ getOrCreateSharingACP,
594
+
595
+ getHash,
596
+ export: exportShared,
597
+ serialize,
598
+ deserialize,
599
+
600
+ getACP,
601
+ getACPs,
602
+ getActiveACP,
603
+ getActiveACPHash,
604
+ removeACP,
605
+ selectActiveACP,
606
+ removeActiveACP,
607
+
608
+ revokeACP,
609
+ revokeAllACPs,
610
+ isACPRevoked,
611
+
612
+ shareOnChain,
613
+ getIncomingShares,
614
+ importFromChain,
615
+ removeShareOnChain,
616
+ computeShareId,
617
+
618
+ applyACPDefaults,
619
+ applyACPDefaultsFromChain,
620
+ getAclServedAddresses,
621
+ clearAclServedAddresses,
622
+ };
623
+
624
+ /** @deprecated renamed — use `acp` (public terminology: acp -> ACP) */
625
+ export const acp = acps;