@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.
- package/CHANGELOG.md +49 -0
- package/acps/acp.ts +411 -0
- package/acps/index.ts +70 -0
- package/{permits → acps}/onchain-utils.ts +49 -24
- package/acps/sealing.ts +90 -0
- package/acps/signature.ts +89 -0
- package/acps/store.ts +172 -0
- package/acps/test/acp.test.ts +615 -0
- package/acps/test/localstorage.test.ts +105 -0
- package/acps/test/sealing.test.ts +77 -0
- package/acps/test/store.test.ts +88 -0
- package/acps/test/validation.test.ts +361 -0
- package/acps/test-utils.ts +32 -0
- package/acps/types.ts +252 -0
- package/acps/validation.ts +392 -0
- package/adapters/test/ethers5.test.ts +4 -1
- package/adapters/test/ethers6.test.ts +4 -1
- package/adapters/test/wagmi.test.ts +5 -2
- package/chains/chains/stagingCofhe.ts +21 -0
- package/chains/index.ts +3 -1
- package/chains/test/chains.test.ts +2 -1
- package/core/acps.ts +625 -0
- package/core/client.ts +136 -39
- package/core/clientTypes.ts +52 -41
- package/core/config.ts +66 -5
- package/core/debug.ts +72 -0
- package/core/decrypt/MockThresholdNetworkAbi.ts +20 -11
- package/core/decrypt/apiError.ts +104 -0
- package/core/decrypt/cofheMocksDecryptForTx.ts +11 -11
- package/core/decrypt/cofheMocksDecryptForView.ts +7 -7
- package/core/decrypt/decryptForTxBuilder.ts +102 -102
- package/core/decrypt/decryptForViewBuilder.ts +90 -90
- package/core/decrypt/submitRetry.ts +38 -30
- package/core/decrypt/tnDecryptV1.ts +5 -5
- package/core/decrypt/tnDecryptV2.ts +45 -34
- package/core/decrypt/tnSealOutputV1.ts +4 -3
- package/core/decrypt/tnSealOutputV2.ts +44 -31
- package/core/encrypt/cofheMocksZkVerifySign.ts +59 -74
- package/core/encrypt/encryptInputsBuilder.ts +86 -52
- package/core/encrypt/zkPackProveVerify.ts +25 -18
- package/core/error.ts +34 -6
- package/core/index.ts +14 -14
- package/core/test/acpDefaults.test.ts +52 -0
- package/core/test/acps.test.ts +596 -0
- package/core/test/apiError.test.ts +130 -0
- package/core/test/client.test.ts +22 -19
- package/core/test/config.test.ts +25 -5
- package/core/test/decrypt.test.ts +40 -35
- package/core/test/decryptBuilders.test.ts +68 -68
- package/core/test/decryptErrorCodes.test.ts +217 -0
- package/core/test/encryptInputsBuilder.test.ts +72 -41
- package/core/test/pollCallbacks.test.ts +89 -18
- package/core/test/stagingRedirect.ts +18 -0
- package/core/test/submitRetry.test.ts +182 -0
- package/core/types.ts +9 -69
- package/dist/acp-Wi6isVQI.d.cts +407 -0
- package/dist/acp-Wi6isVQI.d.ts +407 -0
- package/dist/acps.cjs +1081 -0
- package/dist/acps.d.cts +480 -0
- package/dist/acps.d.ts +480 -0
- package/dist/acps.js +2 -0
- package/dist/chains.cjs +14 -1
- package/dist/chains.d.cts +30 -1
- package/dist/chains.d.ts +30 -1
- package/dist/chains.js +1 -1
- package/dist/{chunk-PE5V5CCV.js → chunk-43USWPEH.js} +1000 -604
- package/dist/{chunk-MTRAXQXC.js → chunk-N6IDQRRU.js} +14 -2
- package/dist/chunk-Q7CBWGQX.js +1029 -0
- package/dist/{clientTypes-CyUvRRzA.d.ts → clientTypes-BN3nbzYM.d.ts} +342 -165
- package/dist/{clientTypes-BDy1qIBu.d.cts → clientTypes-CYZjFznO.d.cts} +342 -165
- package/dist/core.cjs +1431 -1026
- package/dist/core.d.cts +55 -10
- package/dist/core.d.ts +55 -10
- package/dist/core.js +3 -3
- package/dist/node.cjs +1356 -976
- package/dist/node.d.cts +2 -2
- package/dist/node.d.ts +2 -2
- package/dist/node.js +3 -3
- package/dist/web.cjs +1356 -976
- package/dist/web.d.cts +2 -2
- package/dist/web.d.ts +2 -2
- package/dist/web.js +3 -3
- package/node/test/inherited.test.ts +75 -65
- package/node/test/tfheinit.test.ts +23 -8
- package/package.json +6 -6
- package/web/test/client.web.test.ts +5 -1
- package/web/test/inherited.web.test.ts +75 -65
- package/web/test/tfheinit.web.test.ts +14 -5
- package/web/test/worker.config.web.test.ts +38 -23
- package/web/test/worker.output.web.test.ts +25 -24
- package/core/encrypt/encryptUtils.ts +0 -67
- package/core/permits.ts +0 -206
- package/core/test/permits.test.ts +0 -534
- package/dist/chunk-VB62WYPL.js +0 -978
- package/dist/permit-DnVMDT5h.d.cts +0 -376
- package/dist/permit-DnVMDT5h.d.ts +0 -376
- package/dist/permits.cjs +0 -1026
- package/dist/permits.d.cts +0 -353
- package/dist/permits.d.ts +0 -353
- package/dist/permits.js +0 -2
- package/permits/index.ts +0 -68
- package/permits/permit.ts +0 -385
- package/permits/sealing.ts +0 -131
- package/permits/signature.ts +0 -79
- package/permits/store.ts +0 -157
- package/permits/test/localstorage.test.ts +0 -113
- package/permits/test/permit.test.ts +0 -557
- package/permits/test/sealing.test.ts +0 -84
- package/permits/test/store.test.ts +0 -88
- package/permits/test/validation.test.ts +0 -361
- package/permits/test-utils.ts +0 -28
- package/permits/types.ts +0 -204
- package/permits/validation.ts +0 -327
- /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(
|
|
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;
|