@cofhe/sdk 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/acps/acp.ts +411 -0
  3. package/acps/index.ts +70 -0
  4. package/{permits → acps}/onchain-utils.ts +49 -24
  5. package/acps/sealing.ts +90 -0
  6. package/acps/signature.ts +89 -0
  7. package/acps/store.ts +172 -0
  8. package/acps/test/acp.test.ts +615 -0
  9. package/acps/test/localstorage.test.ts +105 -0
  10. package/acps/test/sealing.test.ts +77 -0
  11. package/acps/test/store.test.ts +88 -0
  12. package/acps/test/validation.test.ts +361 -0
  13. package/acps/test-utils.ts +32 -0
  14. package/acps/types.ts +252 -0
  15. package/acps/validation.ts +392 -0
  16. package/adapters/test/ethers5.test.ts +4 -1
  17. package/adapters/test/ethers6.test.ts +4 -1
  18. package/adapters/test/wagmi.test.ts +5 -2
  19. package/chains/chains/stagingCofhe.ts +21 -0
  20. package/chains/index.ts +3 -1
  21. package/chains/test/chains.test.ts +2 -1
  22. package/core/acps.ts +625 -0
  23. package/core/client.ts +136 -39
  24. package/core/clientTypes.ts +52 -41
  25. package/core/config.ts +66 -5
  26. package/core/debug.ts +72 -0
  27. package/core/decrypt/MockThresholdNetworkAbi.ts +20 -11
  28. package/core/decrypt/apiError.ts +104 -0
  29. package/core/decrypt/cofheMocksDecryptForTx.ts +11 -11
  30. package/core/decrypt/cofheMocksDecryptForView.ts +7 -7
  31. package/core/decrypt/decryptForTxBuilder.ts +102 -102
  32. package/core/decrypt/decryptForViewBuilder.ts +90 -90
  33. package/core/decrypt/submitRetry.ts +38 -30
  34. package/core/decrypt/tnDecryptV1.ts +5 -5
  35. package/core/decrypt/tnDecryptV2.ts +45 -34
  36. package/core/decrypt/tnSealOutputV1.ts +4 -3
  37. package/core/decrypt/tnSealOutputV2.ts +44 -31
  38. package/core/encrypt/cofheMocksZkVerifySign.ts +59 -74
  39. package/core/encrypt/encryptInputsBuilder.ts +86 -52
  40. package/core/encrypt/zkPackProveVerify.ts +25 -18
  41. package/core/error.ts +34 -6
  42. package/core/index.ts +14 -14
  43. package/core/test/acpDefaults.test.ts +52 -0
  44. package/core/test/acps.test.ts +596 -0
  45. package/core/test/apiError.test.ts +130 -0
  46. package/core/test/client.test.ts +22 -19
  47. package/core/test/config.test.ts +25 -5
  48. package/core/test/decrypt.test.ts +40 -35
  49. package/core/test/decryptBuilders.test.ts +68 -68
  50. package/core/test/decryptErrorCodes.test.ts +217 -0
  51. package/core/test/encryptInputsBuilder.test.ts +72 -41
  52. package/core/test/pollCallbacks.test.ts +89 -18
  53. package/core/test/stagingRedirect.ts +18 -0
  54. package/core/test/submitRetry.test.ts +182 -0
  55. package/core/types.ts +9 -69
  56. package/dist/acp-Wi6isVQI.d.cts +407 -0
  57. package/dist/acp-Wi6isVQI.d.ts +407 -0
  58. package/dist/acps.cjs +1081 -0
  59. package/dist/acps.d.cts +480 -0
  60. package/dist/acps.d.ts +480 -0
  61. package/dist/acps.js +2 -0
  62. package/dist/chains.cjs +14 -1
  63. package/dist/chains.d.cts +30 -1
  64. package/dist/chains.d.ts +30 -1
  65. package/dist/chains.js +1 -1
  66. package/dist/{chunk-PE5V5CCV.js → chunk-43USWPEH.js} +1000 -604
  67. package/dist/{chunk-MTRAXQXC.js → chunk-N6IDQRRU.js} +14 -2
  68. package/dist/chunk-Q7CBWGQX.js +1029 -0
  69. package/dist/{clientTypes-CyUvRRzA.d.ts → clientTypes-BN3nbzYM.d.ts} +342 -165
  70. package/dist/{clientTypes-BDy1qIBu.d.cts → clientTypes-CYZjFznO.d.cts} +342 -165
  71. package/dist/core.cjs +1431 -1026
  72. package/dist/core.d.cts +55 -10
  73. package/dist/core.d.ts +55 -10
  74. package/dist/core.js +3 -3
  75. package/dist/node.cjs +1356 -976
  76. package/dist/node.d.cts +2 -2
  77. package/dist/node.d.ts +2 -2
  78. package/dist/node.js +3 -3
  79. package/dist/web.cjs +1356 -976
  80. package/dist/web.d.cts +2 -2
  81. package/dist/web.d.ts +2 -2
  82. package/dist/web.js +3 -3
  83. package/node/test/inherited.test.ts +75 -65
  84. package/node/test/tfheinit.test.ts +23 -8
  85. package/package.json +6 -6
  86. package/web/test/client.web.test.ts +5 -1
  87. package/web/test/inherited.web.test.ts +75 -65
  88. package/web/test/tfheinit.web.test.ts +14 -5
  89. package/web/test/worker.config.web.test.ts +38 -23
  90. package/web/test/worker.output.web.test.ts +25 -24
  91. package/core/encrypt/encryptUtils.ts +0 -67
  92. package/core/permits.ts +0 -206
  93. package/core/test/permits.test.ts +0 -534
  94. package/dist/chunk-VB62WYPL.js +0 -978
  95. package/dist/permit-DnVMDT5h.d.cts +0 -376
  96. package/dist/permit-DnVMDT5h.d.ts +0 -376
  97. package/dist/permits.cjs +0 -1026
  98. package/dist/permits.d.cts +0 -353
  99. package/dist/permits.d.ts +0 -353
  100. package/dist/permits.js +0 -2
  101. package/permits/index.ts +0 -68
  102. package/permits/permit.ts +0 -385
  103. package/permits/sealing.ts +0 -131
  104. package/permits/signature.ts +0 -79
  105. package/permits/store.ts +0 -157
  106. package/permits/test/localstorage.test.ts +0 -113
  107. package/permits/test/permit.test.ts +0 -557
  108. package/permits/test/sealing.test.ts +0 -84
  109. package/permits/test/store.test.ts +0 -88
  110. package/permits/test/validation.test.ts +0 -361
  111. package/permits/test-utils.ts +0 -28
  112. package/permits/types.ts +0 -204
  113. package/permits/validation.ts +0 -327
  114. /package/{permits → acps}/utils.ts +0 -0
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
- wallet = new ethers5.Wallet('0x' + '1'.repeat(64), provider);
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
- wallet = new ethers6.Wallet('0x' + '1'.repeat(64), provider);
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 = privateKeyToAccount(('0x' + '1'.repeat(64)) as `0x${string}`);
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),