@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
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,54 @@
|
|
|
1
1
|
# @cofhe/sdk Changelog
|
|
2
2
|
|
|
3
|
+
## 0.7.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- fb87d91: **ACP-era chains only.** The SDK signs and sends ACP (Permit V3) exclusively. Chains whose ACL still serves the pre-upgrade V2 `Permission` protocol are rejected with a clear error at permit-signing time (the ACL's `eip712Domain` version is probed and must be `"2"`). Decryption request bodies carry the ACP object under the `acp` key, and backend error handling recognizes the `acp_*` codes exclusively — seven map 1:1 onto the former `permit_*` codes, while revocation now surfaces as `acp_denied` (see the [0.7.0 migration guide](https://cofhesdk.fhenix.io/migrating-to-0-7-0) for the full table).
|
|
8
|
+
- fb87d91: **Breaking: Permit (V2) → ACP (Access Control Permission).** Permits become scoped, revocable ACPs; old names are removed rather than deprecated. Highlights (full list in the [0.7.0 migration guide](https://cofhesdk.fhenix.io/migrating-to-0-7-0)):
|
|
9
|
+
|
|
10
|
+
- `Permit`/`Permission`/`PermitUtils`/`client.permits` → `ACP`/`ACPPublic`/`ACPUtils`/`client.acp`; `getPermission()` → `getPublic()`
|
|
11
|
+
- `ACPPrivate` & `ACPPublic` are top-level types, `ACP` is the union; the sealing keypair is flattened to `sealingPrivateKey`/`sealingKey` (the `SealingKey` class is removed)
|
|
12
|
+
- New scope fields (`scope`, `contracts`, `handles: bytes32[]`) and revocation fields renamed `validatorId`/`validatorContract` → `revokerData`/`revokerContract`; default revoker `ACPTimestampRevoker` with `revokeACP`/`revokeAllACPs`/`isACPRevoked` on the client
|
|
13
|
+
- EIP-712 domain bumped to `("ACL", "2")` with new `ACPIssuerSelf`/`ACPIssuerShared`/`ACPRecipient` types — previously signed permits no longer verify; the permit store migrates by wiping retired-format permits
|
|
14
|
+
- `ACPUtils.export()` produces a fixed `SharedACP` shape and only accepts signed sharing ACPs
|
|
15
|
+
|
|
16
|
+
- fb87d91: **On-chain ACP sharing.** New `ACPShareRegistry` contract (deployed with the mocks) lets an issuer post a sharing ACP on-chain for its recipient to discover and import — replacing the JSON copy-paste hand-off.
|
|
17
|
+
|
|
18
|
+
- `client.acp.shareOnChain(acp)` posts a signed sharing ACP (issuer-only, same guards as `export()`); `cancelShare(shareId)` retracts it
|
|
19
|
+
- `client.acp.getIncomingShares()` lists importable shares addressed to the connected account (unexpired, not revoked — the registry checks the share's own revoker)
|
|
20
|
+
- `client.acp.importFromChain(share)` imports like the JSON flow (recipient sealing key + signature); `dismissShare(shareId)` cleans up the entry
|
|
21
|
+
- config: `acp.sharingRegistry: Record<chainId, address>`
|
|
22
|
+
- registry exposes `isShareValid(shareId)` as an on-chain verification hook for contracts
|
|
23
|
+
|
|
24
|
+
- fb87d91: Migrate `cofheClient.encryptInputs` from one-signature-per-ciphertext to the new batch verification scheme (one signature per batch, per `FhenixProtocol/cofhe-contracts#78`).
|
|
25
|
+
|
|
26
|
+
**Breaking:** `EncryptInputsBuilder.execute()` now always returns `[...hashes, signature]` (`HashPlusProofResult<T>`) instead of an array of per-item `EncryptedItemInput` structs. `EncryptedItemInput` and its per-type aliases (`EncryptedBoolInput`, `EncryptedUint8Input`, etc.) are removed, along with `EncryptInputsBuilder.asHashPlusProof()` (no longer needed - it's the only shape now). `@cofhe/abi`'s `extractEncryptableValues`/`insertEncryptedValues` now detect `external*` ABI types instead of `struct InEuintXX`, with a new calling convention: any function with encrypted inputs must end with a plain `bytes` parameter to receive the shared batch signature. `@cofhe/foundry-plugin`'s `CofheClient.createIn*` helpers are renamed to `createExternal*` (`createInEuint32` → `createExternalEuint32`, etc.) and now return an `external*` handle plus a batch signature rather than an `InEuintXX` struct; the `createIn*_asHashPlusProof` variants are removed as redundant. See the [0.7.0 migration guide](https://cofhesdk.fhenix.io/migrating-to-0-7-0) for the full list of changes and what contract authors need to update.
|
|
27
|
+
|
|
28
|
+
- fb87d91: Bind encrypted inputs to a consuming contract (`FhenixProtocol/cofhe-contracts#77`). The verifier-signed digest now includes the contract that will consume the ciphertext, closing a replay path where a signed input packet observed on-chain could be reused against a different contract than the one it was signed for.
|
|
29
|
+
|
|
30
|
+
**Breaking:** `EncryptInputsBuilder.setConsumingContract(address)` must be called before `.execute()` - it throws `ConsumingContractUninitialized` otherwise. `@cofhe/mock-contracts`'s `MockTaskManager` signature digest changed to include the consuming contract (external ABI unchanged). `@cofhe/foundry-plugin`'s `CofheClient.createExternal*`/`createEuint32sBatch` helpers now take a required `address consumingContract` as their last parameter (no global setter). `@cofhe/react`'s `useCofheEncryptAndWriteContract` defaults `consumingContract` to the write's target address automatically; `useCofheEncrypt` accepts it as an explicit option. See the [0.7.0 migration guide](https://cofhesdk.fhenix.io/migrating-to-0-7-0) for full details.
|
|
31
|
+
|
|
32
|
+
- fb87d91: **Permit is now ACP everywhere.** All Permit-named API surface is renamed to ACP to avoid confusion with classic DeFi permits: types (`SelfACP`, `SharingACP`, `RecipientACP`, ...), client methods (`getOrCreateSelfACP`, `withACP`, ...), react hooks and components (`useCofheACPs`, `ACPCard`, ...), error codes (`ACP_DENIED`, ...), the `@cofhe/sdk/permits` entrypoint (now `@cofhe/sdk/acps`), and all documentation. English prose words (permitted/permitting) and protocol contract interfaces are unchanged. The persisted store key also changed; previously stored ACPs are not migrated and are transparently re-created on next use.
|
|
33
|
+
|
|
34
|
+
### Patch Changes
|
|
35
|
+
|
|
36
|
+
- d4d662f: fix(sdk): don't activate delegated (sharing) permits on creation
|
|
37
|
+
|
|
38
|
+
`createPermitWithSign` always stored the newly created permit as the issuer's active permit, so creating a delegated/sharing permit hijacked the issuer's own active permit. A delegated permit is for the recipient and must not change the issuer's active permit.
|
|
39
|
+
|
|
40
|
+
`createSharing` now defaults to store-only; `getOrCreateSharingACP` (which genuinely wants an active sharing ACP) opts in via `activate: true`. Self ACPs are unchanged.
|
|
41
|
+
|
|
42
|
+
- f01cac7: Decrypt/sealoutput failures now map the threshold network's stable `error` codes to dedicated `CofheErrorCode` values (e.g. `ACPDenied`, `CtNotFound`, `UnsupportedType`) instead of a generic `DecryptFailed`/`SealOutputFailed`, and `CofheError` gains an `apiErrorCode` field with the raw backend string.
|
|
43
|
+
|
|
44
|
+
Also fixes submit-time `404` retries: a `404` is only retried while the backend reports `ct_not_found` (still indexing); any other error code now fails immediately instead of being blindly retried for up to `set404RetryTimeout`.
|
|
45
|
+
|
|
46
|
+
## 0.6.1
|
|
47
|
+
|
|
48
|
+
### Patch Changes
|
|
49
|
+
|
|
50
|
+
- 670cda8: Prepare an alpha snapshot release.
|
|
51
|
+
|
|
3
52
|
## 0.6.0
|
|
4
53
|
|
|
5
54
|
### Minor Changes
|
package/acps/acp.ts
ADDED
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
import { keccak256, toHex, zeroAddress, parseAbi, type Hex, type PublicClient, type WalletClient } from 'viem';
|
|
2
|
+
import {
|
|
3
|
+
type ACP,
|
|
4
|
+
type SelfACP,
|
|
5
|
+
type SharingACP,
|
|
6
|
+
type RecipientACP,
|
|
7
|
+
type CreateSelfACPOptions,
|
|
8
|
+
type CreateSharingACPOptions,
|
|
9
|
+
type ImportSharedACPOptions,
|
|
10
|
+
type SharedACP,
|
|
11
|
+
type SerializedACP,
|
|
12
|
+
type EIP712Domain,
|
|
13
|
+
type ACPPublic,
|
|
14
|
+
type EthEncryptedData,
|
|
15
|
+
type ACPHashFields,
|
|
16
|
+
} from './types.js';
|
|
17
|
+
import {
|
|
18
|
+
validateSelfACPOptions,
|
|
19
|
+
validateSharingACPOptions,
|
|
20
|
+
validateImportACPOptions,
|
|
21
|
+
validateSelfACP,
|
|
22
|
+
validateSharingACP,
|
|
23
|
+
validateImportACP,
|
|
24
|
+
ValidationUtils,
|
|
25
|
+
} from './validation.js';
|
|
26
|
+
import { SignatureUtils } from './signature.js';
|
|
27
|
+
import { GenerateSealingKey, unsealWithPrivateKey } from './sealing.js';
|
|
28
|
+
import { checkACPValidityOnChain, getAclEIP712Domain } from './onchain-utils.js';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Main ACP utilities - functional approach for React compatibility
|
|
32
|
+
*/
|
|
33
|
+
export const ACPUtils = {
|
|
34
|
+
/**
|
|
35
|
+
* Create a self acp for personal use
|
|
36
|
+
*/
|
|
37
|
+
createSelf: (options: CreateSelfACPOptions): SelfACP => {
|
|
38
|
+
const validation = validateSelfACPOptions(options);
|
|
39
|
+
|
|
40
|
+
// Always generate a new sealing key - users cannot provide their own
|
|
41
|
+
const sealingPair = GenerateSealingKey();
|
|
42
|
+
|
|
43
|
+
const acp = {
|
|
44
|
+
hash: ACPUtils.getHash(validation),
|
|
45
|
+
...validation,
|
|
46
|
+
sealingPrivateKey: sealingPair.privateKey,
|
|
47
|
+
sealingKey: sealingPair.publicKey,
|
|
48
|
+
_signedDomain: undefined,
|
|
49
|
+
} satisfies SelfACP;
|
|
50
|
+
|
|
51
|
+
return acp;
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Create a sharing acp to be shared with another user
|
|
56
|
+
*/
|
|
57
|
+
createSharing: (options: CreateSharingACPOptions): SharingACP => {
|
|
58
|
+
const validation = validateSharingACPOptions(options);
|
|
59
|
+
|
|
60
|
+
// Always generate a new sealing key - users cannot provide their own
|
|
61
|
+
const sealingPair = GenerateSealingKey();
|
|
62
|
+
|
|
63
|
+
const acp = {
|
|
64
|
+
hash: ACPUtils.getHash(validation),
|
|
65
|
+
...validation,
|
|
66
|
+
sealingPrivateKey: sealingPair.privateKey,
|
|
67
|
+
sealingKey: sealingPair.publicKey,
|
|
68
|
+
_signedDomain: undefined,
|
|
69
|
+
} satisfies SharingACP;
|
|
70
|
+
|
|
71
|
+
return acp;
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Import a shared acp from various input formats
|
|
76
|
+
*/
|
|
77
|
+
importShared: (options: ImportSharedACPOptions | string): RecipientACP => {
|
|
78
|
+
let parsedOptions: ImportSharedACPOptions;
|
|
79
|
+
|
|
80
|
+
// Handle different input types
|
|
81
|
+
if (typeof options === 'string') {
|
|
82
|
+
// Parse JSON string
|
|
83
|
+
try {
|
|
84
|
+
parsedOptions = JSON.parse(options);
|
|
85
|
+
} catch (error) {
|
|
86
|
+
throw new Error(`Failed to parse JSON string: ${error}`);
|
|
87
|
+
}
|
|
88
|
+
} else if (typeof options === 'object' && options !== null) {
|
|
89
|
+
// Handle both ImportSharedACPOptions and any object
|
|
90
|
+
parsedOptions = options;
|
|
91
|
+
} else {
|
|
92
|
+
throw new Error('Invalid input type, expected ImportSharedACPOptions, object, or string');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Validate type if provided
|
|
96
|
+
if (parsedOptions.type != null && parsedOptions.type !== 'sharing') {
|
|
97
|
+
throw new Error(`Invalid acp type <${parsedOptions.type}>, must be "sharing"`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const validation = validateImportACPOptions({ ...parsedOptions, type: 'recipient' });
|
|
101
|
+
|
|
102
|
+
// Always generate a new sealing key - users cannot provide their own
|
|
103
|
+
const sealingPair = GenerateSealingKey();
|
|
104
|
+
|
|
105
|
+
const acp = {
|
|
106
|
+
hash: ACPUtils.getHash(validation),
|
|
107
|
+
...validation,
|
|
108
|
+
sealingPrivateKey: sealingPair.privateKey,
|
|
109
|
+
sealingKey: sealingPair.publicKey,
|
|
110
|
+
_signedDomain: undefined,
|
|
111
|
+
} satisfies RecipientACP;
|
|
112
|
+
|
|
113
|
+
return acp;
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Sign an ACP with the provided wallet client
|
|
118
|
+
*/
|
|
119
|
+
sign: async <T extends ACP>(acp: T, publicClient: PublicClient, walletClient: WalletClient): Promise<T> => {
|
|
120
|
+
if (walletClient == null || walletClient.account == null) {
|
|
121
|
+
throw new Error(
|
|
122
|
+
'Missing walletClient, you must pass in a `walletClient` for the connected user to create an ACP signature'
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const primaryType = SignatureUtils.getPrimaryType(acp.type);
|
|
127
|
+
const domain = await getAclEIP712Domain(publicClient);
|
|
128
|
+
const { types, message } = SignatureUtils.getSignatureParams(ACPUtils.getPublic(acp, true), primaryType);
|
|
129
|
+
|
|
130
|
+
const signature = await walletClient.signTypedData({
|
|
131
|
+
domain,
|
|
132
|
+
types,
|
|
133
|
+
primaryType,
|
|
134
|
+
message,
|
|
135
|
+
account: walletClient.account,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
let updatedACP: ACP;
|
|
139
|
+
if (acp.type === 'self' || acp.type === 'sharing') {
|
|
140
|
+
updatedACP = {
|
|
141
|
+
...acp,
|
|
142
|
+
issuerSignature: signature,
|
|
143
|
+
_signedDomain: domain,
|
|
144
|
+
};
|
|
145
|
+
} else {
|
|
146
|
+
updatedACP = {
|
|
147
|
+
...acp,
|
|
148
|
+
recipientSignature: signature,
|
|
149
|
+
_signedDomain: domain,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return updatedACP as T;
|
|
154
|
+
},
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Create and sign a self acp in one operation
|
|
158
|
+
*/
|
|
159
|
+
createSelfAndSign: async (
|
|
160
|
+
options: CreateSelfACPOptions,
|
|
161
|
+
publicClient: PublicClient,
|
|
162
|
+
walletClient: WalletClient
|
|
163
|
+
): Promise<SelfACP> => {
|
|
164
|
+
const acp = ACPUtils.createSelf(options);
|
|
165
|
+
return ACPUtils.sign(acp, publicClient, walletClient);
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Create and sign a sharing acp in one operation
|
|
170
|
+
*/
|
|
171
|
+
createSharingAndSign: async (
|
|
172
|
+
options: CreateSharingACPOptions,
|
|
173
|
+
publicClient: PublicClient,
|
|
174
|
+
walletClient: WalletClient
|
|
175
|
+
): Promise<SharingACP> => {
|
|
176
|
+
const acp = ACPUtils.createSharing(options);
|
|
177
|
+
return ACPUtils.sign(acp, publicClient, walletClient);
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Import and sign a shared acp in one operation from various input formats
|
|
182
|
+
*/
|
|
183
|
+
importSharedAndSign: async (
|
|
184
|
+
options: ImportSharedACPOptions | string,
|
|
185
|
+
publicClient: PublicClient,
|
|
186
|
+
walletClient: WalletClient
|
|
187
|
+
): Promise<RecipientACP> => {
|
|
188
|
+
const acp = ACPUtils.importShared(options);
|
|
189
|
+
return ACPUtils.sign(acp, publicClient, walletClient);
|
|
190
|
+
},
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Deserialize an ACP from serialized data
|
|
194
|
+
*/
|
|
195
|
+
deserialize: (data: SerializedACP): ACP => {
|
|
196
|
+
return { ...data };
|
|
197
|
+
},
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Serialize an ACP for storage
|
|
201
|
+
*/
|
|
202
|
+
serialize: (acp: ACP): SerializedACP => {
|
|
203
|
+
return {
|
|
204
|
+
hash: acp.hash,
|
|
205
|
+
name: acp.name,
|
|
206
|
+
type: acp.type,
|
|
207
|
+
issuer: acp.issuer,
|
|
208
|
+
expiration: acp.expiration,
|
|
209
|
+
recipient: acp.recipient,
|
|
210
|
+
revokerData: acp.revokerData,
|
|
211
|
+
revokerContract: acp.revokerContract,
|
|
212
|
+
scope: acp.scope,
|
|
213
|
+
contracts: acp.contracts,
|
|
214
|
+
handles: acp.handles,
|
|
215
|
+
sealingKey: acp.sealingKey,
|
|
216
|
+
issuerSignature: acp.issuerSignature,
|
|
217
|
+
recipientSignature: acp.recipientSignature,
|
|
218
|
+
_signedDomain: acp._signedDomain,
|
|
219
|
+
sealingPrivateKey: acp.sealingPrivateKey,
|
|
220
|
+
};
|
|
221
|
+
},
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Validate an ACP (schema-level validation)
|
|
225
|
+
*/
|
|
226
|
+
validateSchema: (acp: ACP) => {
|
|
227
|
+
if (acp.type === 'self') {
|
|
228
|
+
return validateSelfACP(acp);
|
|
229
|
+
} else if (acp.type === 'sharing') {
|
|
230
|
+
return validateSharingACP(acp);
|
|
231
|
+
} else if (acp.type === 'recipient') {
|
|
232
|
+
return validateImportACP(acp);
|
|
233
|
+
} else {
|
|
234
|
+
throw new Error('Invalid ACP type');
|
|
235
|
+
}
|
|
236
|
+
},
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Validate an ACP (holistic validation).
|
|
240
|
+
*
|
|
241
|
+
* This validates:
|
|
242
|
+
* - ACP schema (shape + invariants)
|
|
243
|
+
* - ACP is signed
|
|
244
|
+
* - ACP is not expired
|
|
245
|
+
*
|
|
246
|
+
* For schema-only validation, use `validateSchema(acp)`.
|
|
247
|
+
*/
|
|
248
|
+
validate: (acp: ACP) => {
|
|
249
|
+
const validated = ACPUtils.validateSchema(acp);
|
|
250
|
+
ValidationUtils.assertSignedAndNotExpired(validated as ACP);
|
|
251
|
+
return validated;
|
|
252
|
+
},
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Get the public component of an ACP — the signed struct sent on-chain / to the decryption backend.
|
|
256
|
+
* Strips the private component (hash, name, type, sealing pair).
|
|
257
|
+
*/
|
|
258
|
+
getPublic: (acp: ACP, skipValidation = false): ACPPublic => {
|
|
259
|
+
if (!skipValidation) {
|
|
260
|
+
ACPUtils.validateSchema(acp);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return {
|
|
264
|
+
issuer: acp.issuer,
|
|
265
|
+
expiration: acp.expiration,
|
|
266
|
+
recipient: acp.recipient,
|
|
267
|
+
revokerData: acp.revokerData,
|
|
268
|
+
revokerContract: acp.revokerContract,
|
|
269
|
+
scope: acp.scope,
|
|
270
|
+
contracts: acp.contracts,
|
|
271
|
+
handles: acp.handles,
|
|
272
|
+
sealingKey: acp.sealingKey,
|
|
273
|
+
issuerSignature: acp.issuerSignature,
|
|
274
|
+
recipientSignature: acp.recipientSignature,
|
|
275
|
+
};
|
|
276
|
+
},
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Get a stable hash for the acp (used as key in storage)
|
|
280
|
+
*/
|
|
281
|
+
getHash: (acp: ACPHashFields): string => {
|
|
282
|
+
const data = JSON.stringify({
|
|
283
|
+
type: acp.type,
|
|
284
|
+
issuer: acp.issuer,
|
|
285
|
+
expiration: acp.expiration,
|
|
286
|
+
recipient: acp.recipient,
|
|
287
|
+
revokerData: acp.revokerData,
|
|
288
|
+
revokerContract: acp.revokerContract,
|
|
289
|
+
scope: acp.scope,
|
|
290
|
+
contracts: acp.contracts,
|
|
291
|
+
handles: acp.handles,
|
|
292
|
+
});
|
|
293
|
+
return keccak256(toHex(data));
|
|
294
|
+
},
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Export acp data for sharing (strips the private component).
|
|
298
|
+
* Fixed `SharedACP` shape — every field always present, aligned with
|
|
299
|
+
* `ACPPublic` and the on-chain sharing payload.
|
|
300
|
+
*/
|
|
301
|
+
export: (acp: ACP): string => {
|
|
302
|
+
if (acp.type !== 'sharing') {
|
|
303
|
+
throw new Error(
|
|
304
|
+
`Cannot export a '${acp.type}' ACP — only 'sharing' ACPs are exportable. The export includes the issuer signature.`
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
if (acp.issuerSignature === '0x') {
|
|
308
|
+
throw new Error(
|
|
309
|
+
'Cannot export an unsigned sharing ACP — sign it first (the recipient needs the issuer signature).'
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const shared: SharedACP = {
|
|
314
|
+
name: acp.name,
|
|
315
|
+
type: acp.type,
|
|
316
|
+
issuer: acp.issuer,
|
|
317
|
+
expiration: acp.expiration,
|
|
318
|
+
recipient: acp.recipient,
|
|
319
|
+
revokerData: acp.revokerData,
|
|
320
|
+
revokerContract: acp.revokerContract,
|
|
321
|
+
scope: acp.scope,
|
|
322
|
+
contracts: acp.contracts,
|
|
323
|
+
handles: acp.handles,
|
|
324
|
+
issuerSignature: acp.issuerSignature,
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
return JSON.stringify(shared, undefined, 2);
|
|
328
|
+
},
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Unseal encrypted data using the acp's sealing key
|
|
332
|
+
*/
|
|
333
|
+
unseal: (acp: ACP, ciphertext: EthEncryptedData): bigint => {
|
|
334
|
+
return unsealWithPrivateKey(acp.sealingPrivateKey, ciphertext);
|
|
335
|
+
},
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Check if acp is expired
|
|
339
|
+
*/
|
|
340
|
+
isExpired: (acp: ACP): boolean => {
|
|
341
|
+
return ValidationUtils.isExpired(acp);
|
|
342
|
+
},
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Check if acp is signed
|
|
346
|
+
*/
|
|
347
|
+
isSigned: (acp: ACP): boolean => {
|
|
348
|
+
return ValidationUtils.isSigned(acp);
|
|
349
|
+
},
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Check if acp is signed and not expired
|
|
353
|
+
*/
|
|
354
|
+
isSignedAndNotExpired: (acp: ACP) => {
|
|
355
|
+
return ValidationUtils.isSignedAndNotExpired(acp);
|
|
356
|
+
},
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Assert that acp is signed and not expired
|
|
360
|
+
*/
|
|
361
|
+
assertSignedAndNotExpired: (acp: ACP): void => {
|
|
362
|
+
return ValidationUtils.assertSignedAndNotExpired(acp);
|
|
363
|
+
},
|
|
364
|
+
|
|
365
|
+
isValid: (acp: ACP) => {
|
|
366
|
+
return ValidationUtils.isValid(acp);
|
|
367
|
+
},
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Update acp name (returns new acp instance)
|
|
371
|
+
*/
|
|
372
|
+
updateName: (acp: ACP, name: string): ACP => {
|
|
373
|
+
return { ...acp, name };
|
|
374
|
+
},
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Fetch EIP712 domain from the blockchain
|
|
378
|
+
*/
|
|
379
|
+
fetchEIP712Domain: async (publicClient: PublicClient): Promise<EIP712Domain> => {
|
|
380
|
+
return getAclEIP712Domain(publicClient);
|
|
381
|
+
},
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Check if acp's signed domain matches the provided domain
|
|
385
|
+
*/
|
|
386
|
+
matchesDomain: (acp: ACP, domain: EIP712Domain): boolean => {
|
|
387
|
+
return (
|
|
388
|
+
acp._signedDomain?.name === domain.name &&
|
|
389
|
+
acp._signedDomain?.version === domain.version &&
|
|
390
|
+
acp._signedDomain?.verifyingContract === domain.verifyingContract &&
|
|
391
|
+
acp._signedDomain?.chainId === domain.chainId
|
|
392
|
+
);
|
|
393
|
+
},
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Check if acp's signed domain is valid for the current chain
|
|
397
|
+
*/
|
|
398
|
+
checkSignedDomainValid: async (acp: ACP, publicClient: PublicClient): Promise<boolean> => {
|
|
399
|
+
if (acp._signedDomain == null) return false;
|
|
400
|
+
const domain = await getAclEIP712Domain(publicClient);
|
|
401
|
+
return ACPUtils.matchesDomain(acp, domain);
|
|
402
|
+
},
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Check if acp passes the on-chain validation
|
|
406
|
+
*/
|
|
407
|
+
checkValidityOnChain: async (acp: ACP, publicClient: PublicClient): Promise<boolean> => {
|
|
408
|
+
const publicAcp = ACPUtils.getPublic(acp);
|
|
409
|
+
return checkACPValidityOnChain(publicAcp, publicClient);
|
|
410
|
+
},
|
|
411
|
+
};
|
package/acps/index.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// Core types
|
|
2
|
+
export type {
|
|
3
|
+
ACP,
|
|
4
|
+
CreateSelfACPOptions as SelfACPOptions,
|
|
5
|
+
CreateSharingACPOptions as SharingACPOptions,
|
|
6
|
+
ImportSharedACPOptions as ImportACPOptions,
|
|
7
|
+
SerializedACP,
|
|
8
|
+
ACPMetadata,
|
|
9
|
+
ACPPublic,
|
|
10
|
+
SharedACP,
|
|
11
|
+
IncomingShare,
|
|
12
|
+
EIP712Domain,
|
|
13
|
+
EIP712Types,
|
|
14
|
+
EIP712Message,
|
|
15
|
+
ValidationResult,
|
|
16
|
+
ACPSignaturePrimaryType,
|
|
17
|
+
} from './types.js';
|
|
18
|
+
|
|
19
|
+
// Main utilities
|
|
20
|
+
export { ACPUtils } from './acp.js';
|
|
21
|
+
|
|
22
|
+
// Validation utilities
|
|
23
|
+
export {
|
|
24
|
+
// Self ACP validators
|
|
25
|
+
SelfACPOptionsValidator,
|
|
26
|
+
SelfACPValidator,
|
|
27
|
+
validateSelfACPOptions,
|
|
28
|
+
validateSelfACP,
|
|
29
|
+
// Sharing ACP validators
|
|
30
|
+
SharingACPOptionsValidator,
|
|
31
|
+
SharingACPValidator,
|
|
32
|
+
validateSharingACPOptions,
|
|
33
|
+
validateSharingACP,
|
|
34
|
+
// Import ACP validators
|
|
35
|
+
ImportACPOptionsValidator,
|
|
36
|
+
ImportACPValidator,
|
|
37
|
+
validateImportACPOptions,
|
|
38
|
+
validateImportACP,
|
|
39
|
+
// Common utilities
|
|
40
|
+
ValidationUtils,
|
|
41
|
+
} from './validation.js';
|
|
42
|
+
|
|
43
|
+
// Signature utilities
|
|
44
|
+
export { SignatureUtils, getSignatureTypesAndMessage, SignatureTypes } from './signature.js';
|
|
45
|
+
|
|
46
|
+
// Storage utilities
|
|
47
|
+
export {
|
|
48
|
+
acpStore,
|
|
49
|
+
getACP,
|
|
50
|
+
getActiveACP,
|
|
51
|
+
getACPs,
|
|
52
|
+
setACP,
|
|
53
|
+
removeACP,
|
|
54
|
+
getActiveACPHash,
|
|
55
|
+
setActiveACPHash,
|
|
56
|
+
removeActiveACPHash,
|
|
57
|
+
clearStaleStore,
|
|
58
|
+
ACP_STORE_DEFAULTS,
|
|
59
|
+
} from './store.js';
|
|
60
|
+
|
|
61
|
+
// Sealing utilities
|
|
62
|
+
export { GenerateSealingKey, seal, unsealWithPrivateKey, type SealingKeyPair } from './sealing.js';
|
|
63
|
+
export type { EthEncryptedData } from './sealing.js';
|
|
64
|
+
|
|
65
|
+
// Re-export everything for convenience
|
|
66
|
+
export * from './types.js';
|
|
67
|
+
export * from './acp.js';
|
|
68
|
+
export * from './validation.js';
|
|
69
|
+
export * from './signature.js';
|
|
70
|
+
export * from './store.js';
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
decodeErrorResult,
|
|
7
7
|
parseAbi,
|
|
8
8
|
} from 'viem';
|
|
9
|
-
import type { EIP712Domain,
|
|
9
|
+
import type { EIP712Domain, ACPPublic } from './types';
|
|
10
10
|
import { TASK_MANAGER_ADDRESS } from '../core/consts.js';
|
|
11
11
|
|
|
12
12
|
export const getAclAddress = async (publicClient: PublicClient): Promise<Hex> => {
|
|
@@ -23,6 +23,10 @@ export const getAclAddress = async (publicClient: PublicClient): Promise<Hex> =>
|
|
|
23
23
|
})) as `0x${string}`;
|
|
24
24
|
};
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* ACP (ACP V3) signing domain — fetched from the ACL contract, which since V3
|
|
28
|
+
* carries the ACP verification logic and signs as name "ACL", version "2".
|
|
29
|
+
*/
|
|
26
30
|
export const getAclEIP712Domain = async (publicClient: PublicClient): Promise<EIP712Domain> => {
|
|
27
31
|
const aclAddress = await getAclAddress(publicClient);
|
|
28
32
|
const EIP712_DOMAIN_IFACE =
|
|
@@ -41,6 +45,12 @@ export const getAclEIP712Domain = async (publicClient: PublicClient): Promise<EI
|
|
|
41
45
|
// eslint-disable-next-line no-unused-vars
|
|
42
46
|
const [_fields, name, version, chainId, verifyingContract, _salt, _extensions] = domain;
|
|
43
47
|
|
|
48
|
+
if (version !== '2') {
|
|
49
|
+
throw new Error(
|
|
50
|
+
`Chain ${chainId}'s ACL serves EIP-712 domain version "${version}" — this SDK requires the upgraded (ACP-era) ACL, which signs as version "2". Pre-upgrade (V2 Permission) chains are not supported.`
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
44
54
|
return {
|
|
45
55
|
name,
|
|
46
56
|
version,
|
|
@@ -49,28 +59,28 @@ export const getAclEIP712Domain = async (publicClient: PublicClient): Promise<EI
|
|
|
49
59
|
};
|
|
50
60
|
};
|
|
51
61
|
|
|
52
|
-
export const
|
|
53
|
-
permission: Permission,
|
|
54
|
-
publicClient: PublicClient
|
|
55
|
-
): Promise<boolean> => {
|
|
62
|
+
export const checkACPValidityOnChain = async (acp: ACPPublic, publicClient: PublicClient): Promise<boolean> => {
|
|
56
63
|
const aclAddress = await getAclAddress(publicClient);
|
|
57
64
|
|
|
58
|
-
// Check if the
|
|
65
|
+
// Check if the acp is valid (structure: expiration / signatures / revocation)
|
|
59
66
|
try {
|
|
60
67
|
await publicClient.simulateContract({
|
|
61
68
|
address: aclAddress,
|
|
62
|
-
abi:
|
|
63
|
-
functionName: '
|
|
69
|
+
abi: checkACPValidityAbi,
|
|
70
|
+
functionName: 'checkPermissionValidity',
|
|
64
71
|
args: [
|
|
65
72
|
{
|
|
66
|
-
issuer:
|
|
67
|
-
expiration: BigInt(
|
|
68
|
-
recipient:
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
73
|
+
issuer: acp.issuer,
|
|
74
|
+
expiration: BigInt(acp.expiration),
|
|
75
|
+
recipient: acp.recipient,
|
|
76
|
+
revokerData: BigInt(acp.revokerData),
|
|
77
|
+
revokerContract: acp.revokerContract,
|
|
78
|
+
scope: acp.scope,
|
|
79
|
+
contracts: acp.contracts,
|
|
80
|
+
handles: acp.handles,
|
|
81
|
+
sealingKey: acp.sealingKey,
|
|
82
|
+
issuerSignature: acp.issuerSignature,
|
|
83
|
+
recipientSignature: acp.recipientSignature,
|
|
74
84
|
},
|
|
75
85
|
],
|
|
76
86
|
});
|
|
@@ -86,7 +96,7 @@ export const checkPermitValidityOnChain = async (
|
|
|
86
96
|
}
|
|
87
97
|
|
|
88
98
|
// Check details field for custom error names (e.g., from Hardhat test nodes)
|
|
89
|
-
const customErrorName = extractCustomErrorFromDetails(err,
|
|
99
|
+
const customErrorName = extractCustomErrorFromDetails(err, checkACPValidityAbi);
|
|
90
100
|
if (customErrorName) {
|
|
91
101
|
throw new Error(customErrorName);
|
|
92
102
|
}
|
|
@@ -95,7 +105,7 @@ export const checkPermitValidityOnChain = async (
|
|
|
95
105
|
const hhDetailsData = extractReturnData(err);
|
|
96
106
|
if (hhDetailsData != null) {
|
|
97
107
|
const decoded = decodeErrorResult({
|
|
98
|
-
abi:
|
|
108
|
+
abi: checkACPValidityAbi,
|
|
99
109
|
data: hhDetailsData,
|
|
100
110
|
});
|
|
101
111
|
|
|
@@ -136,15 +146,15 @@ function extractReturnData(err: unknown): `0x${string}` | undefined {
|
|
|
136
146
|
return s.match(/return data:\s*(0x[a-fA-F0-9]+)/)?.[1] as `0x${string}` | undefined;
|
|
137
147
|
}
|
|
138
148
|
|
|
139
|
-
const
|
|
149
|
+
const checkACPValidityAbi = [
|
|
140
150
|
{
|
|
141
151
|
type: 'function',
|
|
142
|
-
name: '
|
|
152
|
+
name: 'checkPermissionValidity',
|
|
143
153
|
inputs: [
|
|
144
154
|
{
|
|
145
|
-
name: '
|
|
155
|
+
name: 'acp',
|
|
146
156
|
type: 'tuple',
|
|
147
|
-
internalType: 'struct
|
|
157
|
+
internalType: 'struct ACP',
|
|
148
158
|
components: [
|
|
149
159
|
{
|
|
150
160
|
name: 'issuer',
|
|
@@ -162,15 +172,30 @@ const checkPermitValidityAbi = [
|
|
|
162
172
|
internalType: 'address',
|
|
163
173
|
},
|
|
164
174
|
{
|
|
165
|
-
name: '
|
|
175
|
+
name: 'revokerData',
|
|
166
176
|
type: 'uint256',
|
|
167
177
|
internalType: 'uint256',
|
|
168
178
|
},
|
|
169
179
|
{
|
|
170
|
-
name: '
|
|
180
|
+
name: 'revokerContract',
|
|
171
181
|
type: 'address',
|
|
172
182
|
internalType: 'address',
|
|
173
183
|
},
|
|
184
|
+
{
|
|
185
|
+
name: 'scope',
|
|
186
|
+
type: 'uint8',
|
|
187
|
+
internalType: 'uint8',
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
name: 'contracts',
|
|
191
|
+
type: 'address[]',
|
|
192
|
+
internalType: 'address[]',
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
name: 'handles',
|
|
196
|
+
type: 'bytes32[]',
|
|
197
|
+
internalType: 'bytes32[]',
|
|
198
|
+
},
|
|
174
199
|
{
|
|
175
200
|
name: 'sealingKey',
|
|
176
201
|
type: 'bytes32',
|