@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/acps/types.ts
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { type EthEncryptedData } from './sealing.js';
|
|
2
|
+
import { type Hex } from 'viem';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* EIP712 related types
|
|
6
|
+
*/
|
|
7
|
+
export type EIP712Type = { name: string; type: string };
|
|
8
|
+
export type EIP712Types = Record<string, EIP712Type[]>;
|
|
9
|
+
export type EIP712Message = Record<string, string>;
|
|
10
|
+
export type EIP712Domain = {
|
|
11
|
+
chainId: number;
|
|
12
|
+
name: string;
|
|
13
|
+
verifyingContract: Hex;
|
|
14
|
+
version: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Re-export EthEncryptedData from sealing module
|
|
19
|
+
*/
|
|
20
|
+
export type { EthEncryptedData };
|
|
21
|
+
|
|
22
|
+
// Viem client types will be imported from viem package
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The client-side-only component of an ACP — never leaves the client.
|
|
26
|
+
*/
|
|
27
|
+
export interface ACPPrivate {
|
|
28
|
+
/**
|
|
29
|
+
* Stable hash of relevant acp data, used as key in storage
|
|
30
|
+
*/
|
|
31
|
+
hash: string;
|
|
32
|
+
/**
|
|
33
|
+
* Name for this acp, for organization and UI usage, not included in signature.
|
|
34
|
+
*/
|
|
35
|
+
name: string;
|
|
36
|
+
/**
|
|
37
|
+
* The type of the ACP (self / sharing / recipient)
|
|
38
|
+
* (self) ACP that will be signed and used by the issuer
|
|
39
|
+
* (sharing) ACP that is signed by the issuer, but intended to be shared with recipient
|
|
40
|
+
* (recipient) ACP that has been received, and signed by the recipient
|
|
41
|
+
*/
|
|
42
|
+
type: 'self' | 'sharing' | 'recipient';
|
|
43
|
+
/**
|
|
44
|
+
* The private half of the sealing keypair — used to unseal returned data.
|
|
45
|
+
* `sealingKey` (in the public component) is the corresponding public key.
|
|
46
|
+
* Never leaves the client.
|
|
47
|
+
*/
|
|
48
|
+
sealingPrivateKey: Hex;
|
|
49
|
+
/**
|
|
50
|
+
* EIP712 domain used to sign this acp.
|
|
51
|
+
* Should not be set manually, included in metadata as part of serialization flows.
|
|
52
|
+
*/
|
|
53
|
+
_signedDomain?: EIP712Domain;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The public component of an ACP — the signed struct passed on-chain / to the
|
|
58
|
+
* decryption backend to grant encrypted data access. Produced by `getPublic()`.
|
|
59
|
+
*/
|
|
60
|
+
export interface ACPPublic {
|
|
61
|
+
/**
|
|
62
|
+
* (base) User that initially created the permission, target of data fetching
|
|
63
|
+
*/
|
|
64
|
+
issuer: Hex;
|
|
65
|
+
/**
|
|
66
|
+
* (base) Expiration timestamp
|
|
67
|
+
*/
|
|
68
|
+
expiration: number;
|
|
69
|
+
/**
|
|
70
|
+
* (sharing) The user that this permission will be shared with
|
|
71
|
+
* ** optional, use `address(0)` to disable **
|
|
72
|
+
*/
|
|
73
|
+
recipient: Hex;
|
|
74
|
+
/**
|
|
75
|
+
* (revocation) Opaque data interpreted by `revokerContract` — the default
|
|
76
|
+
* revoker interprets it as the acp's creation timestamp.
|
|
77
|
+
* ** optional, use `0` to disable **
|
|
78
|
+
*/
|
|
79
|
+
revokerData: number;
|
|
80
|
+
/**
|
|
81
|
+
* (revocation) The contract to query to determine permission validity
|
|
82
|
+
* ** optional, use `address(0)` to disable **
|
|
83
|
+
*/
|
|
84
|
+
revokerContract: Hex;
|
|
85
|
+
/**
|
|
86
|
+
* (scope) Scope discriminator — exactly one scope mode per ACP (see ACPScope).
|
|
87
|
+
* Global: all of `issuer`s values (arrays empty). Contract: values readable by
|
|
88
|
+
* `contracts`. Handles: the listed `handles` only.
|
|
89
|
+
*/
|
|
90
|
+
scope: number;
|
|
91
|
+
/**
|
|
92
|
+
* (scope) Grants access to `issuer`s values readable by any of these contracts.
|
|
93
|
+
* Checked on-chain as an intersection with the ACL's persisted allowances.
|
|
94
|
+
*/
|
|
95
|
+
contracts: Hex[];
|
|
96
|
+
/**
|
|
97
|
+
* (scope) Grants access to these specific ciphertext handles (bytes32 hex)
|
|
98
|
+
*/
|
|
99
|
+
handles: Hex[];
|
|
100
|
+
/**
|
|
101
|
+
* (base) The public half of the sealing keypair — used to re-encrypt `issuer`s confidential data
|
|
102
|
+
* (non-sharing) Populated by `issuer`
|
|
103
|
+
* (sharing) Populated by `recipient`
|
|
104
|
+
*/
|
|
105
|
+
sealingKey: Hex;
|
|
106
|
+
/**
|
|
107
|
+
* (base) `signTypedData` signature created by `issuer`.
|
|
108
|
+
* (base) Shared- and Self- permissions differ in signature format: (`sealingKey` absent in shared signature)
|
|
109
|
+
*/
|
|
110
|
+
issuerSignature: Hex;
|
|
111
|
+
/**
|
|
112
|
+
* (sharing) `signTypedData` signature created by `recipient`
|
|
113
|
+
* ** required for shared acps **
|
|
114
|
+
*/
|
|
115
|
+
recipientSignature: Hex;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Core ACP (Access Control Permission) type — immutable design for React compatibility.
|
|
120
|
+
* The union of the private (client-only) and public (signed) components.
|
|
121
|
+
*/
|
|
122
|
+
export type ACP = Expand<ACPPrivate & ACPPublic>;
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* ACP discriminant helpers
|
|
126
|
+
*/
|
|
127
|
+
export type ACPType = ACP['type'];
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Utility type to narrow an ACP to a specific discriminant.
|
|
131
|
+
*
|
|
132
|
+
* Note: this only narrows the `type` field. Runtime/validation constraints
|
|
133
|
+
* (e.g. recipient == zeroAddress for self acps) are enforced elsewhere.
|
|
134
|
+
*/
|
|
135
|
+
export type ACPOf<T extends ACPType> = Expand<Omit<ACP, 'type'> & { type: T }>;
|
|
136
|
+
|
|
137
|
+
export type SelfACP = ACPOf<'self'>;
|
|
138
|
+
export type SharingACP = ACPOf<'sharing'>;
|
|
139
|
+
export type RecipientACP = ACPOf<'recipient'>;
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Optional additional metadata of an ACP
|
|
143
|
+
* Can be passed into the constructor, but not necessary
|
|
144
|
+
* Useful for deserialization
|
|
145
|
+
*/
|
|
146
|
+
export interface ACPMetadata {
|
|
147
|
+
/**
|
|
148
|
+
* EIP712 domain used to sign this acp.
|
|
149
|
+
* Should not be set manually, included in metadata as part of serialization flows.
|
|
150
|
+
*/
|
|
151
|
+
_signedDomain?: EIP712Domain;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Utility types for acp creation
|
|
156
|
+
*/
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Scope fields shared by all acp creation options.
|
|
160
|
+
* When any scope array is populated and `global` is not explicitly set,
|
|
161
|
+
* `global` defaults to false (narrowest matching scope); with no scope
|
|
162
|
+
* arrays it defaults to true (V2 behavior).
|
|
163
|
+
*/
|
|
164
|
+
export const ACPScope = {
|
|
165
|
+
Global: 0,
|
|
166
|
+
Contract: 1,
|
|
167
|
+
Handles: 2,
|
|
168
|
+
} as const;
|
|
169
|
+
|
|
170
|
+
export type ACPScopeOptions = {
|
|
171
|
+
scope?: number;
|
|
172
|
+
contracts?: Hex[];
|
|
173
|
+
handles?: Hex[];
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
// Specific option types for each acp creation method
|
|
177
|
+
export type CreateSelfACPOptions = ACPScopeOptions & {
|
|
178
|
+
type?: 'self';
|
|
179
|
+
issuer: string;
|
|
180
|
+
name?: string;
|
|
181
|
+
expiration?: number;
|
|
182
|
+
revokerData?: number;
|
|
183
|
+
revokerContract?: string;
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
export type CreateSharingACPOptions = ACPScopeOptions & {
|
|
187
|
+
type?: 'sharing';
|
|
188
|
+
issuer: string;
|
|
189
|
+
recipient: string;
|
|
190
|
+
name?: string;
|
|
191
|
+
expiration?: number;
|
|
192
|
+
revokerData?: number;
|
|
193
|
+
revokerContract?: string;
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
export type ImportSharedACPOptions = ACPScopeOptions & {
|
|
197
|
+
type?: 'sharing';
|
|
198
|
+
issuer: string;
|
|
199
|
+
recipient: string;
|
|
200
|
+
issuerSignature: string;
|
|
201
|
+
name?: string;
|
|
202
|
+
expiration: number;
|
|
203
|
+
revokerData?: number;
|
|
204
|
+
revokerContract?: string;
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
/** An ACP is plain JSON-serializable data — the serialized form is the ACP itself. */
|
|
208
|
+
export type SerializedACP = ACP;
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* The share payload produced by `ACPUtils.export()` — the full public component
|
|
212
|
+
* with `sealingKey`/`recipientSignature` left for the recipient to fill, plus
|
|
213
|
+
* the display name and acp type. Fixed shape: every field is always present
|
|
214
|
+
* (zero-values instead of omissions), so importers can parse a single schema.
|
|
215
|
+
* Mirrors the on-chain sharing payload struct field-for-field.
|
|
216
|
+
*/
|
|
217
|
+
export type SharedACP = Expand<
|
|
218
|
+
Omit<ACPPublic, 'sealingKey' | 'recipientSignature'> & {
|
|
219
|
+
name: string;
|
|
220
|
+
type: 'sharing';
|
|
221
|
+
}
|
|
222
|
+
>;
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* A share read back from the on-chain ACPShareRegistry: the posted payload
|
|
226
|
+
* (SharedACP minus the client-side name/type) plus its registry id.
|
|
227
|
+
*/
|
|
228
|
+
export type IncomingShare = Expand<Omit<SharedACP, 'name' | 'type'> & { shareId: Hex }>;
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* A type representing the acp fields that are used to generate the hash
|
|
232
|
+
*/
|
|
233
|
+
export type ACPHashFields = Pick<
|
|
234
|
+
ACP,
|
|
235
|
+
'type' | 'issuer' | 'expiration' | 'recipient' | 'revokerData' | 'revokerContract' | 'scope' | 'contracts' | 'handles'
|
|
236
|
+
>;
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Validation result type
|
|
240
|
+
*/
|
|
241
|
+
export interface ValidationResult {
|
|
242
|
+
valid: boolean;
|
|
243
|
+
error: 'invalid-schema' | 'expired' | 'not-signed' | null;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Signature types for EIP712 signing
|
|
248
|
+
*/
|
|
249
|
+
export type ACPSignaturePrimaryType = 'ACPIssuerSelf' | 'ACPIssuerShared' | 'ACPRecipient';
|
|
250
|
+
|
|
251
|
+
// Utils
|
|
252
|
+
export type Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never;
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { getAddress, isAddress, isHex, zeroAddress, type Hex } from 'viem';
|
|
3
|
+
import type { ACP, ValidationResult } from './types.js';
|
|
4
|
+
|
|
5
|
+
export const addressSchema = z
|
|
6
|
+
.string()
|
|
7
|
+
.refine((val) => isAddress(val), {
|
|
8
|
+
error: 'Invalid address',
|
|
9
|
+
})
|
|
10
|
+
.transform((val): Hex => getAddress(val));
|
|
11
|
+
|
|
12
|
+
export const addressNotZeroSchema = addressSchema.refine((val) => val !== zeroAddress, {
|
|
13
|
+
error: 'Must not be zeroAddress',
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export const bytesSchema = z.custom<Hex>(
|
|
17
|
+
(val) => {
|
|
18
|
+
return typeof val === 'string' && isHex(val);
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
message: 'Invalid hex value',
|
|
22
|
+
}
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
export const bytesNotEmptySchema = bytesSchema.refine((val) => val !== '0x', {
|
|
26
|
+
error: 'Must not be empty',
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const DEFAULT_EXPIRATION_FN = () => Math.round(Date.now() / 1000) + 7 * 24 * 60 * 60; // 7 days from now
|
|
30
|
+
|
|
31
|
+
/** (scope) ciphertext handles — bytes32 hex strings */
|
|
32
|
+
export const handlesSchema = z
|
|
33
|
+
.array(z.string().regex(/^0x[0-9a-fA-F]{64}$/, 'Invalid handle: expected 32-byte hex') as z.ZodType<Hex>)
|
|
34
|
+
.optional()
|
|
35
|
+
.default([]);
|
|
36
|
+
|
|
37
|
+
/** (scope) contract addresses */
|
|
38
|
+
export const contractsSchema = z.array(addressSchema).optional().default([]);
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Context-sensitive `global` default: with no scope arrays it defaults to true
|
|
42
|
+
* (V2 behavior); when contracts/handles are provided it defaults to false
|
|
43
|
+
* (narrowest matching scope).
|
|
44
|
+
*/
|
|
45
|
+
/**
|
|
46
|
+
* Scope derivation + per-scope array rules (client-side validation, per review):
|
|
47
|
+
* - explicit `scope` wins; otherwise derived: contracts -> Contract, handles -> Handles, else Global
|
|
48
|
+
* - Global => contracts and handles must be empty
|
|
49
|
+
* - Contract => contracts non-empty, handles empty
|
|
50
|
+
* - Handles => handles non-empty, contracts empty
|
|
51
|
+
* Exactly one scope mode per ACP — no overlapping scopes.
|
|
52
|
+
*/
|
|
53
|
+
const SCOPE_GLOBAL = 0;
|
|
54
|
+
const SCOPE_CONTRACT = 1;
|
|
55
|
+
const SCOPE_HANDLES = 2;
|
|
56
|
+
|
|
57
|
+
const ScopeConsistencyRefinement = [
|
|
58
|
+
(data: { scope: number; contracts: Hex[]; handles: Hex[] }) =>
|
|
59
|
+
(data.scope === SCOPE_GLOBAL && data.contracts.length === 0 && data.handles.length === 0) ||
|
|
60
|
+
(data.scope === SCOPE_CONTRACT && data.contracts.length > 0 && data.handles.length === 0) ||
|
|
61
|
+
(data.scope === SCOPE_HANDLES && data.handles.length > 0 && data.contracts.length === 0),
|
|
62
|
+
{
|
|
63
|
+
error:
|
|
64
|
+
'ACP scope :: arrays must match the scope mode (Global: both empty; Contract: contracts only; Handles: handles only)',
|
|
65
|
+
path: ['scope'] as string[],
|
|
66
|
+
},
|
|
67
|
+
] as const;
|
|
68
|
+
|
|
69
|
+
const withDerivedScope = <T extends { scope?: number; contracts: Hex[]; handles: Hex[] }>(data: T) => ({
|
|
70
|
+
...data,
|
|
71
|
+
scope:
|
|
72
|
+
data.scope ?? (data.contracts.length > 0 ? SCOPE_CONTRACT : data.handles.length > 0 ? SCOPE_HANDLES : SCOPE_GLOBAL),
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const zACPWithDefaults = z.object({
|
|
76
|
+
name: z.string().optional().default('Unnamed ACP'),
|
|
77
|
+
type: z.enum(['self', 'sharing', 'recipient']),
|
|
78
|
+
issuer: addressNotZeroSchema,
|
|
79
|
+
expiration: z.int().optional().default(DEFAULT_EXPIRATION_FN),
|
|
80
|
+
recipient: addressSchema.optional().default(zeroAddress),
|
|
81
|
+
revokerData: z.int().optional().default(0),
|
|
82
|
+
revokerContract: addressSchema.optional().default(zeroAddress),
|
|
83
|
+
scope: z.int().min(0).max(2).optional().default(0),
|
|
84
|
+
contracts: contractsSchema,
|
|
85
|
+
handles: handlesSchema,
|
|
86
|
+
issuerSignature: bytesSchema.optional().default('0x'),
|
|
87
|
+
recipientSignature: bytesSchema.optional().default('0x'),
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const zACPWithSealingKeys = zACPWithDefaults.extend({
|
|
91
|
+
/** X25519 private key, 0x-prefixed 32-byte hex; never leaves the client */
|
|
92
|
+
sealingPrivateKey: z
|
|
93
|
+
.string()
|
|
94
|
+
.regex(/^0x[0-9a-fA-F]{64}$/, 'Invalid sealing private key')
|
|
95
|
+
.optional(),
|
|
96
|
+
sealingKey: z
|
|
97
|
+
.string()
|
|
98
|
+
.regex(/^0x[0-9a-fA-F]{64}$/, 'Invalid sealing key')
|
|
99
|
+
.optional(),
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
type zACPType = z.infer<typeof zACPWithDefaults>;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* ACPs allow a hook into an optional external revoker contract,
|
|
106
|
+
* this check ensures that IF an external revoker is applied, that both `revokerData` and `revokerContract` are populated,
|
|
107
|
+
* ELSE ensures that both `revokerData` and `revokerContract` are empty
|
|
108
|
+
*/
|
|
109
|
+
const ExternalValidatorRefinement = [
|
|
110
|
+
(data: Pick<zACPType, 'revokerData' | 'revokerContract'>) =>
|
|
111
|
+
(data.revokerData !== 0 && data.revokerContract !== zeroAddress) ||
|
|
112
|
+
(data.revokerData === 0 && data.revokerContract === zeroAddress),
|
|
113
|
+
{
|
|
114
|
+
error: 'ACP external revoker :: revokerData and revokerContract must either both be set or both be unset.',
|
|
115
|
+
path: ['revokerData', 'revokerContract'] as string[],
|
|
116
|
+
},
|
|
117
|
+
] as const;
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Prevents sharable acp from having the same issuer and recipient
|
|
121
|
+
*/
|
|
122
|
+
const RecipientRefinement = [
|
|
123
|
+
(data: Pick<zACPType, 'issuer' | 'recipient'>) => data.issuer !== data.recipient,
|
|
124
|
+
{
|
|
125
|
+
error: 'Sharing acp :: issuer and recipient must not be the same',
|
|
126
|
+
path: ['issuer', 'recipient'] as string[],
|
|
127
|
+
},
|
|
128
|
+
] as const;
|
|
129
|
+
|
|
130
|
+
// ============================================================================
|
|
131
|
+
// SELF ACP VALIDATORS
|
|
132
|
+
// ============================================================================
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Validator for self acp creation options
|
|
136
|
+
*/
|
|
137
|
+
export const SelfACPOptionsValidator = z
|
|
138
|
+
.object({
|
|
139
|
+
type: z.literal('self').optional().default('self'),
|
|
140
|
+
issuer: addressNotZeroSchema,
|
|
141
|
+
name: z.string().optional().default('Unnamed ACP'),
|
|
142
|
+
expiration: z.int().optional().default(DEFAULT_EXPIRATION_FN),
|
|
143
|
+
recipient: addressSchema.optional().default(zeroAddress),
|
|
144
|
+
revokerData: z.int().optional().default(0),
|
|
145
|
+
revokerContract: addressSchema.optional().default(zeroAddress),
|
|
146
|
+
scope: z.int().min(0).max(2).optional(),
|
|
147
|
+
contracts: contractsSchema,
|
|
148
|
+
handles: handlesSchema,
|
|
149
|
+
issuerSignature: bytesSchema.optional().default('0x'),
|
|
150
|
+
recipientSignature: bytesSchema.optional().default('0x'),
|
|
151
|
+
})
|
|
152
|
+
.refine(...ExternalValidatorRefinement)
|
|
153
|
+
.transform(withDerivedScope)
|
|
154
|
+
.refine(...ScopeConsistencyRefinement);
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Validator for fully formed self acps
|
|
158
|
+
*/
|
|
159
|
+
export const SelfACPValidator = zACPWithSealingKeys
|
|
160
|
+
.refine((data) => data.type === 'self', {
|
|
161
|
+
error: "Type must be 'self'",
|
|
162
|
+
})
|
|
163
|
+
.refine((data) => data.recipient === zeroAddress, {
|
|
164
|
+
error: 'Recipient must be zeroAddress',
|
|
165
|
+
})
|
|
166
|
+
.refine((data) => data.issuerSignature !== '0x', {
|
|
167
|
+
error: 'IssuerSignature must be populated',
|
|
168
|
+
})
|
|
169
|
+
.refine((data) => data.recipientSignature === '0x', {
|
|
170
|
+
error: 'RecipientSignature must be empty',
|
|
171
|
+
})
|
|
172
|
+
.refine(...ExternalValidatorRefinement);
|
|
173
|
+
|
|
174
|
+
// ============================================================================
|
|
175
|
+
// SHARING ACP VALIDATORS
|
|
176
|
+
// ============================================================================
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Validator for sharing acp creation options
|
|
180
|
+
*/
|
|
181
|
+
export const SharingACPOptionsValidator = z
|
|
182
|
+
.object({
|
|
183
|
+
type: z.literal('sharing').optional().default('sharing'),
|
|
184
|
+
issuer: addressNotZeroSchema,
|
|
185
|
+
recipient: addressNotZeroSchema,
|
|
186
|
+
name: z.string().optional().default('Unnamed ACP'),
|
|
187
|
+
expiration: z.int().optional().default(DEFAULT_EXPIRATION_FN),
|
|
188
|
+
revokerData: z.int().optional().default(0),
|
|
189
|
+
revokerContract: addressSchema.optional().default(zeroAddress),
|
|
190
|
+
scope: z.int().min(0).max(2).optional(),
|
|
191
|
+
contracts: contractsSchema,
|
|
192
|
+
handles: handlesSchema,
|
|
193
|
+
issuerSignature: bytesSchema.optional().default('0x'),
|
|
194
|
+
recipientSignature: bytesSchema.optional().default('0x'),
|
|
195
|
+
})
|
|
196
|
+
.refine(...RecipientRefinement)
|
|
197
|
+
.refine(...ExternalValidatorRefinement)
|
|
198
|
+
.transform(withDerivedScope)
|
|
199
|
+
.refine(...ScopeConsistencyRefinement);
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Validator for fully formed sharing acps
|
|
203
|
+
*/
|
|
204
|
+
export const SharingACPValidator = zACPWithSealingKeys
|
|
205
|
+
.refine((data) => data.type === 'sharing', {
|
|
206
|
+
error: "Type must be 'sharing'",
|
|
207
|
+
})
|
|
208
|
+
.refine((data) => data.recipient !== zeroAddress, {
|
|
209
|
+
error: 'Recipient must not be zeroAddress',
|
|
210
|
+
})
|
|
211
|
+
.refine((data) => data.issuerSignature !== '0x', {
|
|
212
|
+
error: 'IssuerSignature must be populated',
|
|
213
|
+
})
|
|
214
|
+
.refine((data) => data.recipientSignature === '0x', {
|
|
215
|
+
error: 'RecipientSignature must be empty',
|
|
216
|
+
})
|
|
217
|
+
.refine(...ExternalValidatorRefinement);
|
|
218
|
+
|
|
219
|
+
// ============================================================================
|
|
220
|
+
// IMPORT/RECIPIENT ACP VALIDATORS
|
|
221
|
+
// ============================================================================
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Validator for import acp creation options (recipient receiving shared acp)
|
|
225
|
+
*/
|
|
226
|
+
export const ImportACPOptionsValidator = z
|
|
227
|
+
.object({
|
|
228
|
+
type: z.literal('recipient').optional().default('recipient'),
|
|
229
|
+
issuer: addressNotZeroSchema,
|
|
230
|
+
recipient: addressNotZeroSchema,
|
|
231
|
+
name: z.string().optional().default('Unnamed ACP'),
|
|
232
|
+
expiration: z.int(),
|
|
233
|
+
revokerData: z.int().optional().default(0),
|
|
234
|
+
revokerContract: addressSchema.optional().default(zeroAddress),
|
|
235
|
+
scope: z.int().min(0).max(2).optional(),
|
|
236
|
+
contracts: contractsSchema,
|
|
237
|
+
handles: handlesSchema,
|
|
238
|
+
issuerSignature: bytesNotEmptySchema,
|
|
239
|
+
recipientSignature: bytesSchema.optional().default('0x'),
|
|
240
|
+
})
|
|
241
|
+
.refine(...ExternalValidatorRefinement)
|
|
242
|
+
.transform(withDerivedScope)
|
|
243
|
+
.refine(...ScopeConsistencyRefinement);
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Validator for fully formed import/recipient acps
|
|
247
|
+
*/
|
|
248
|
+
export const ImportACPValidator = zACPWithSealingKeys
|
|
249
|
+
.refine((data) => data.type === 'recipient', {
|
|
250
|
+
error: "Type must be 'recipient'",
|
|
251
|
+
})
|
|
252
|
+
.refine((data) => data.recipient !== zeroAddress, {
|
|
253
|
+
error: 'Recipient must not be zeroAddress',
|
|
254
|
+
})
|
|
255
|
+
.refine((data) => data.issuerSignature !== '0x', {
|
|
256
|
+
error: 'IssuerSignature must be populated',
|
|
257
|
+
})
|
|
258
|
+
.refine((data) => data.recipientSignature !== '0x', {
|
|
259
|
+
error: 'RecipientSignature must be populated',
|
|
260
|
+
})
|
|
261
|
+
.refine(...ExternalValidatorRefinement);
|
|
262
|
+
|
|
263
|
+
// ============================================================================
|
|
264
|
+
// VALIDATION FUNCTIONS
|
|
265
|
+
// ============================================================================
|
|
266
|
+
|
|
267
|
+
const safeParseAndThrowFormatted = <T extends z.ZodTypeAny>(schema: T, data: unknown, message: string): z.output<T> => {
|
|
268
|
+
const result = schema.safeParse(data);
|
|
269
|
+
if (!result.success) {
|
|
270
|
+
throw new Error(`${message}: ${z.prettifyError(result.error)}`, { cause: result.error });
|
|
271
|
+
}
|
|
272
|
+
return result.data;
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Validates self acp creation options
|
|
277
|
+
*/
|
|
278
|
+
export const validateSelfACPOptions = (options: any) => {
|
|
279
|
+
return safeParseAndThrowFormatted(SelfACPOptionsValidator, options, 'Invalid self acp options');
|
|
280
|
+
};
|
|
281
|
+
/**
|
|
282
|
+
* Validates sharing acp creation options
|
|
283
|
+
*/
|
|
284
|
+
export const validateSharingACPOptions = (options: any) => {
|
|
285
|
+
return safeParseAndThrowFormatted(SharingACPOptionsValidator, options, 'Invalid sharing acp options');
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Validates import acp creation options
|
|
290
|
+
*/
|
|
291
|
+
export const validateImportACPOptions = (options: any) => {
|
|
292
|
+
return safeParseAndThrowFormatted(ImportACPOptionsValidator, options, 'Invalid import acp options');
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Validates a fully formed self acp
|
|
297
|
+
*/
|
|
298
|
+
export const validateSelfACP = (acp: any) => {
|
|
299
|
+
return safeParseAndThrowFormatted(SelfACPValidator, acp, 'Invalid self acp');
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Validates a fully formed sharing acp
|
|
304
|
+
*/
|
|
305
|
+
export const validateSharingACP = (acp: any) => {
|
|
306
|
+
return safeParseAndThrowFormatted(SharingACPValidator, acp, 'Invalid sharing acp');
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Validates a fully formed import/recipient acp
|
|
311
|
+
*/
|
|
312
|
+
export const validateImportACP = (acp: any) => {
|
|
313
|
+
return safeParseAndThrowFormatted(ImportACPValidator, acp, 'Invalid import acp');
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Simple validation functions for common checks
|
|
318
|
+
*/
|
|
319
|
+
export const ValidationUtils = {
|
|
320
|
+
/**
|
|
321
|
+
* Check if acp is expired
|
|
322
|
+
*/
|
|
323
|
+
isExpired: (acp: ACP): boolean => {
|
|
324
|
+
return acp.expiration < Math.floor(Date.now() / 1000);
|
|
325
|
+
},
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Check if acp is signed by the active party
|
|
329
|
+
*/
|
|
330
|
+
isSigned: (acp: ACP): boolean => {
|
|
331
|
+
if (acp.type === 'self' || acp.type === 'sharing') {
|
|
332
|
+
return acp.issuerSignature !== '0x';
|
|
333
|
+
}
|
|
334
|
+
if (acp.type === 'recipient') {
|
|
335
|
+
return acp.recipientSignature !== '0x';
|
|
336
|
+
}
|
|
337
|
+
return false;
|
|
338
|
+
},
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Checks that an ACP is signed and not expired.
|
|
342
|
+
*/
|
|
343
|
+
isSignedAndNotExpired: (acp: ACP): ValidationResult => {
|
|
344
|
+
if (ValidationUtils.isExpired(acp)) {
|
|
345
|
+
return { valid: false, error: 'expired' };
|
|
346
|
+
}
|
|
347
|
+
if (!ValidationUtils.isSigned(acp)) {
|
|
348
|
+
return { valid: false, error: 'not-signed' };
|
|
349
|
+
}
|
|
350
|
+
return { valid: true, error: null };
|
|
351
|
+
},
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Asserts that an ACP is signed and not expired.
|
|
355
|
+
*
|
|
356
|
+
* Throws `Error` with message:
|
|
357
|
+
* - `ACP is expired`
|
|
358
|
+
* - `ACP is not signed`
|
|
359
|
+
*/
|
|
360
|
+
assertSignedAndNotExpired: (acp: ACP): void => {
|
|
361
|
+
const result = ValidationUtils.isSignedAndNotExpired(acp);
|
|
362
|
+
if (result.valid) return;
|
|
363
|
+
|
|
364
|
+
if (result.error === 'expired') {
|
|
365
|
+
throw new Error('ACP is expired');
|
|
366
|
+
}
|
|
367
|
+
if (result.error === 'not-signed') {
|
|
368
|
+
throw new Error('ACP is not signed');
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Should be unreachable, but keeps this future-proof.
|
|
372
|
+
throw new Error('ACP is invalid');
|
|
373
|
+
},
|
|
374
|
+
|
|
375
|
+
isValid: (acp: ACP): ValidationResult => {
|
|
376
|
+
const schema =
|
|
377
|
+
acp.type === 'self'
|
|
378
|
+
? SelfACPValidator
|
|
379
|
+
: acp.type === 'sharing'
|
|
380
|
+
? SharingACPValidator
|
|
381
|
+
: acp.type === 'recipient'
|
|
382
|
+
? ImportACPValidator
|
|
383
|
+
: null;
|
|
384
|
+
|
|
385
|
+
if (schema == null) return { valid: false, error: 'invalid-schema' };
|
|
386
|
+
|
|
387
|
+
const schemaResult = schema.safeParse(acp);
|
|
388
|
+
if (!schemaResult.success) return { valid: false, error: 'invalid-schema' };
|
|
389
|
+
|
|
390
|
+
return ValidationUtils.isSignedAndNotExpired(acp);
|
|
391
|
+
},
|
|
392
|
+
};
|
|
@@ -13,7 +13,10 @@ describe('Ethers5Adapter', () => {
|
|
|
13
13
|
beforeEach(() => {
|
|
14
14
|
// Create common setup for all tests
|
|
15
15
|
provider = new ethers5.providers.JsonRpcProvider(testRpcUrl);
|
|
16
|
-
|
|
16
|
+
// Fresh random key each run → a guaranteed-empty account (0 balance, nonce 0), so the
|
|
17
|
+
// sendTransaction test deterministically fails with "insufficient funds". The old shared
|
|
18
|
+
// 0x11…11 key is funded and heavily used on Sepolia, so its tx failed with other errors.
|
|
19
|
+
wallet = new ethers5.Wallet(ethers5.Wallet.createRandom().privateKey, provider);
|
|
17
20
|
});
|
|
18
21
|
|
|
19
22
|
it('should work with real JsonRpcProvider and Wallet', async () => {
|
|
@@ -13,7 +13,10 @@ describe('Ethers6Adapter', () => {
|
|
|
13
13
|
beforeEach(() => {
|
|
14
14
|
// Create common setup for all tests
|
|
15
15
|
provider = new ethers6.JsonRpcProvider(testRpcUrl);
|
|
16
|
-
|
|
16
|
+
// Fresh random key each run → a guaranteed-empty account (0 balance, nonce 0), so the
|
|
17
|
+
// sendTransaction test deterministically fails with "insufficient funds". The old shared
|
|
18
|
+
// 0x11…11 key is funded and heavily used on Sepolia, so its tx failed with other errors.
|
|
19
|
+
wallet = new ethers6.Wallet(ethers6.Wallet.createRandom().privateKey, provider);
|
|
17
20
|
});
|
|
18
21
|
|
|
19
22
|
it('should work with real JsonRpcProvider and Wallet', async () => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
2
|
import { parseEther, createPublicClient, createWalletClient, http, type PublicClient, type WalletClient } from 'viem';
|
|
3
|
-
import { privateKeyToAccount } from 'viem/accounts';
|
|
3
|
+
import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts';
|
|
4
4
|
import { WagmiAdapter } from '../wagmi.js';
|
|
5
5
|
|
|
6
6
|
describe('WagmiAdapter', () => {
|
|
@@ -12,7 +12,10 @@ describe('WagmiAdapter', () => {
|
|
|
12
12
|
|
|
13
13
|
beforeEach(() => {
|
|
14
14
|
// Create common setup for all tests - no chain needed
|
|
15
|
-
account
|
|
15
|
+
// Fresh random key each run → a guaranteed-empty account (0 balance, nonce 0), so the
|
|
16
|
+
// sendTransaction test deterministically fails with "insufficient funds". The old shared
|
|
17
|
+
// 0x11…11 key is funded and heavily used on Sepolia, so its tx failed with other errors.
|
|
18
|
+
account = privateKeyToAccount(generatePrivateKey());
|
|
16
19
|
|
|
17
20
|
publicClient = createPublicClient({
|
|
18
21
|
transport: http(testRpcUrl),
|