@cofhe/sdk 0.0.0-alpha-20260409113701
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 +146 -0
- package/adapters/ethers5.test.ts +174 -0
- package/adapters/ethers5.ts +36 -0
- package/adapters/ethers6.test.ts +169 -0
- package/adapters/ethers6.ts +36 -0
- package/adapters/hardhat-node.ts +167 -0
- package/adapters/hardhat.hh2.test.ts +159 -0
- package/adapters/hardhat.ts +36 -0
- package/adapters/index.test.ts +20 -0
- package/adapters/index.ts +5 -0
- package/adapters/smartWallet.ts +99 -0
- package/adapters/test-utils.ts +53 -0
- package/adapters/types.ts +6 -0
- package/adapters/wagmi.test.ts +156 -0
- package/adapters/wagmi.ts +17 -0
- package/chains/chains/arbSepolia.ts +14 -0
- package/chains/chains/baseSepolia.ts +14 -0
- package/chains/chains/hardhat.ts +15 -0
- package/chains/chains/localcofhe.ts +14 -0
- package/chains/chains/sepolia.ts +14 -0
- package/chains/chains.test.ts +50 -0
- package/chains/defineChain.ts +18 -0
- package/chains/index.ts +35 -0
- package/chains/types.ts +32 -0
- package/core/baseBuilder.ts +119 -0
- package/core/client.test.ts +429 -0
- package/core/client.ts +341 -0
- package/core/clientTypes.ts +119 -0
- package/core/config.test.ts +242 -0
- package/core/config.ts +225 -0
- package/core/consts.ts +22 -0
- package/core/decrypt/MockThresholdNetworkAbi.ts +179 -0
- package/core/decrypt/cofheMocksDecryptForTx.ts +84 -0
- package/core/decrypt/cofheMocksDecryptForView.ts +48 -0
- package/core/decrypt/decryptForTxBuilder.ts +359 -0
- package/core/decrypt/decryptForViewBuilder.ts +332 -0
- package/core/decrypt/decryptUtils.ts +28 -0
- package/core/decrypt/pollCallbacks.test.ts +194 -0
- package/core/decrypt/polling.ts +14 -0
- package/core/decrypt/tnDecryptUtils.ts +65 -0
- package/core/decrypt/tnDecryptV1.ts +171 -0
- package/core/decrypt/tnDecryptV2.ts +365 -0
- package/core/decrypt/tnSealOutputV1.ts +59 -0
- package/core/decrypt/tnSealOutputV2.ts +324 -0
- package/core/decrypt/verifyDecryptResult.ts +52 -0
- package/core/encrypt/MockZkVerifierAbi.ts +106 -0
- package/core/encrypt/cofheMocksZkVerifySign.ts +281 -0
- package/core/encrypt/encryptInputsBuilder.test.ts +747 -0
- package/core/encrypt/encryptInputsBuilder.ts +583 -0
- package/core/encrypt/encryptUtils.ts +67 -0
- package/core/encrypt/zkPackProveVerify.ts +335 -0
- package/core/error.ts +168 -0
- package/core/fetchKeys.test.ts +195 -0
- package/core/fetchKeys.ts +144 -0
- package/core/index.ts +106 -0
- package/core/keyStore.test.ts +226 -0
- package/core/keyStore.ts +154 -0
- package/core/permits.test.ts +493 -0
- package/core/permits.ts +201 -0
- package/core/types.ts +419 -0
- package/core/utils.ts +130 -0
- package/dist/adapters.cjs +88 -0
- package/dist/adapters.d.cts +14576 -0
- package/dist/adapters.d.ts +14576 -0
- package/dist/adapters.js +83 -0
- package/dist/chains.cjs +111 -0
- package/dist/chains.d.cts +121 -0
- package/dist/chains.d.ts +121 -0
- package/dist/chains.js +1 -0
- package/dist/chunk-36FBWLUS.js +3310 -0
- package/dist/chunk-7HLGHV67.js +990 -0
- package/dist/chunk-TBLR7NNE.js +102 -0
- package/dist/clientTypes-AVSCBet7.d.cts +998 -0
- package/dist/clientTypes-flH1ju82.d.ts +998 -0
- package/dist/core.cjs +4362 -0
- package/dist/core.d.cts +138 -0
- package/dist/core.d.ts +138 -0
- package/dist/core.js +3 -0
- package/dist/node.cjs +4225 -0
- package/dist/node.d.cts +22 -0
- package/dist/node.d.ts +22 -0
- package/dist/node.js +91 -0
- package/dist/permit-jRirYqFt.d.cts +376 -0
- package/dist/permit-jRirYqFt.d.ts +376 -0
- package/dist/permits.cjs +1025 -0
- package/dist/permits.d.cts +353 -0
- package/dist/permits.d.ts +353 -0
- package/dist/permits.js +1 -0
- package/dist/types-YiAC4gig.d.cts +33 -0
- package/dist/types-YiAC4gig.d.ts +33 -0
- package/dist/web.cjs +4434 -0
- package/dist/web.d.cts +42 -0
- package/dist/web.d.ts +42 -0
- package/dist/web.js +256 -0
- package/dist/zkProve.worker.cjs +93 -0
- package/dist/zkProve.worker.d.cts +2 -0
- package/dist/zkProve.worker.d.ts +2 -0
- package/dist/zkProve.worker.js +91 -0
- package/node/client.test.ts +159 -0
- package/node/config.test.ts +68 -0
- package/node/encryptInputs.test.ts +155 -0
- package/node/index.ts +97 -0
- package/node/storage.ts +51 -0
- package/package.json +121 -0
- package/permits/index.ts +68 -0
- package/permits/localstorage.test.ts +113 -0
- package/permits/onchain-utils.ts +221 -0
- package/permits/permit.test.ts +534 -0
- package/permits/permit.ts +386 -0
- package/permits/sealing.test.ts +84 -0
- package/permits/sealing.ts +131 -0
- package/permits/signature.ts +79 -0
- package/permits/store.test.ts +88 -0
- package/permits/store.ts +156 -0
- package/permits/test-utils.ts +28 -0
- package/permits/types.ts +204 -0
- package/permits/utils.ts +58 -0
- package/permits/validation.test.ts +361 -0
- package/permits/validation.ts +327 -0
- package/web/client.web.test.ts +159 -0
- package/web/config.web.test.ts +69 -0
- package/web/const.ts +2 -0
- package/web/encryptInputs.web.test.ts +172 -0
- package/web/index.ts +166 -0
- package/web/storage.ts +49 -0
- package/web/worker.builder.web.test.ts +148 -0
- package/web/worker.config.web.test.ts +329 -0
- package/web/worker.output.web.test.ts +84 -0
- package/web/workerManager.test.ts +80 -0
- package/web/workerManager.ts +214 -0
- package/web/workerManager.web.test.ts +114 -0
- package/web/zkProve.worker.ts +133 -0
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
ValidationUtils,
|
|
4
|
+
validateSelfPermitOptions,
|
|
5
|
+
validateSharingPermitOptions,
|
|
6
|
+
validateImportPermitOptions,
|
|
7
|
+
validateSelfPermit,
|
|
8
|
+
validateSharingPermit,
|
|
9
|
+
validateImportPermit,
|
|
10
|
+
type Permit,
|
|
11
|
+
type CreateSelfPermitOptions,
|
|
12
|
+
type CreateSharingPermitOptions,
|
|
13
|
+
type ImportSharedPermitOptions,
|
|
14
|
+
} from './index.js';
|
|
15
|
+
import { createMockPermit } from './test-utils.js';
|
|
16
|
+
|
|
17
|
+
describe('Validation Tests', () => {
|
|
18
|
+
describe('validateSelfPermitOptions', () => {
|
|
19
|
+
it('should validate valid self permit options', () => {
|
|
20
|
+
const options: CreateSelfPermitOptions = {
|
|
21
|
+
type: 'self',
|
|
22
|
+
issuer: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', // Bob's address
|
|
23
|
+
name: 'Test Permit',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
expect(() => validateSelfPermitOptions(options)).not.toThrow();
|
|
27
|
+
const result = validateSelfPermitOptions(options);
|
|
28
|
+
expect(result).toBeDefined();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should reject invalid address', () => {
|
|
32
|
+
const options: CreateSelfPermitOptions = {
|
|
33
|
+
type: 'self',
|
|
34
|
+
issuer: 'invalid-address',
|
|
35
|
+
name: 'Test Permit',
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
expect(() => validateSelfPermitOptions(options)).toThrow();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should reject zero address', () => {
|
|
42
|
+
const options: CreateSelfPermitOptions = {
|
|
43
|
+
type: 'self',
|
|
44
|
+
issuer: '0x0000000000000000000000000000000000000000',
|
|
45
|
+
name: 'Test Permit',
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
expect(() => validateSelfPermitOptions(options)).toThrow();
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe('validateSharingPermitOptions', () => {
|
|
53
|
+
it('should validate valid sharing permit options', () => {
|
|
54
|
+
const options: CreateSharingPermitOptions = {
|
|
55
|
+
type: 'sharing',
|
|
56
|
+
issuer: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', // Bob's address
|
|
57
|
+
recipient: '0x70997970C51812dc3A010C7d01b50e0d17dc79C8', // Alice's address
|
|
58
|
+
name: 'Sharing Permit',
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
expect(() => validateSharingPermitOptions(options)).not.toThrow();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should reject sharing permit with zero recipient', () => {
|
|
65
|
+
const options: CreateSharingPermitOptions = {
|
|
66
|
+
type: 'sharing',
|
|
67
|
+
issuer: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', // Bob's address
|
|
68
|
+
recipient: '0x0000000000000000000000000000000000000000',
|
|
69
|
+
name: 'Sharing Permit',
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
expect(() => validateSharingPermitOptions(options)).toThrow();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should reject sharing permit with invalid recipient', () => {
|
|
76
|
+
const options: CreateSharingPermitOptions = {
|
|
77
|
+
type: 'sharing',
|
|
78
|
+
issuer: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', // Bob's address
|
|
79
|
+
recipient: 'invalid-address',
|
|
80
|
+
name: 'Sharing Permit',
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
expect(() => validateSharingPermitOptions(options)).toThrow();
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe('validateImportPermitOptions', () => {
|
|
88
|
+
it('should validate valid import permit options', () => {
|
|
89
|
+
const options: ImportSharedPermitOptions = {
|
|
90
|
+
issuer: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', // Bob's address
|
|
91
|
+
expiration: Math.floor(Date.now() / 1000) + 3600, // 1 hour from now
|
|
92
|
+
recipient: '0x70997970C51812dc3A010C7d01b50e0d17dc79C8', // Alice's address
|
|
93
|
+
issuerSignature: '0x1234567890abcdef',
|
|
94
|
+
name: 'Import Permit',
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
expect(() => validateImportPermitOptions(options)).not.toThrow();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should reject import permit with missing expiration', () => {
|
|
101
|
+
const options = {
|
|
102
|
+
issuer: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', // Bob's address
|
|
103
|
+
recipient: '0x70997970C51812dc3A010C7d01b50e0d17dc79C8', // Alice's address
|
|
104
|
+
issuerSignature: '0x1234567890abcdef',
|
|
105
|
+
name: 'Import Permit',
|
|
106
|
+
};
|
|
107
|
+
expect(() => validateImportPermitOptions(options)).toThrow();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should reject import permit with empty signature', () => {
|
|
111
|
+
const options: ImportSharedPermitOptions = {
|
|
112
|
+
issuer: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', // Bob's address
|
|
113
|
+
expiration: Math.floor(Date.now() / 1000) + 3600, // 1 hour from now
|
|
114
|
+
recipient: '0x70997970C51812dc3A010C7d01b50e0d17dc79C8', // Alice's address
|
|
115
|
+
issuerSignature: '0x',
|
|
116
|
+
name: 'Import Permit',
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
expect(() => validateImportPermitOptions(options)).toThrow();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('should reject import permit with invalid signature', () => {
|
|
123
|
+
const options: ImportSharedPermitOptions = {
|
|
124
|
+
issuer: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', // Bob's address
|
|
125
|
+
expiration: Math.floor(Date.now() / 1000) + 3600, // 1 hour from now
|
|
126
|
+
recipient: '0x70997970C51812dc3A010C7d01b50e0d17dc79C8', // Alice's address
|
|
127
|
+
issuerSignature: '0x',
|
|
128
|
+
name: 'Import Permit',
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
expect(() => validateImportPermitOptions(options)).toThrow();
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe('validateSelfPermit', () => {
|
|
136
|
+
it('should validate valid self permit', async () => {
|
|
137
|
+
const permit = await createMockPermit();
|
|
138
|
+
permit.type = 'self';
|
|
139
|
+
permit.issuerSignature = '0x1234567890abcdef';
|
|
140
|
+
|
|
141
|
+
expect(() => validateSelfPermit(permit)).not.toThrow();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('should reject self permit with missing sealing pair', async () => {
|
|
145
|
+
const permit = { ...(await createMockPermit()), sealingPair: undefined };
|
|
146
|
+
permit.type = 'self';
|
|
147
|
+
expect(() => validateSelfPermit(permit as unknown as Permit)).toThrow();
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe('validateSharingPermit', () => {
|
|
152
|
+
it('should validate valid sharing permit', async () => {
|
|
153
|
+
const permit = await createMockPermit();
|
|
154
|
+
permit.type = 'sharing';
|
|
155
|
+
permit.issuerSignature = '0x1234567890abcdef';
|
|
156
|
+
permit.recipient = '0x70997970C51812dc3A010C7d01b50e0d17dc79C8'; // Alice's address
|
|
157
|
+
|
|
158
|
+
expect(() => validateSharingPermit(permit)).not.toThrow();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('should reject sharing permit with zero recipient', async () => {
|
|
162
|
+
const permit = await createMockPermit();
|
|
163
|
+
permit.type = 'sharing';
|
|
164
|
+
permit.issuerSignature = '0x1234567890abcdef';
|
|
165
|
+
permit.recipient = '0x0000000000000000000000000000000000000000';
|
|
166
|
+
|
|
167
|
+
expect(() => validateSharingPermit(permit)).toThrow();
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe('validateImportPermit', () => {
|
|
172
|
+
it('should validate valid import permit', async () => {
|
|
173
|
+
const permit = await createMockPermit();
|
|
174
|
+
permit.type = 'recipient';
|
|
175
|
+
permit.issuerSignature = '0x1234567890abcdef';
|
|
176
|
+
permit.recipient = '0x70997970C51812dc3A010C7d01b50e0d17dc79C8'; // Alice's address
|
|
177
|
+
permit.recipientSignature = '0xabcdef1234567890';
|
|
178
|
+
|
|
179
|
+
expect(() => validateImportPermit(permit)).not.toThrow();
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('should reject import permit with empty recipient signature', async () => {
|
|
183
|
+
const permit = await createMockPermit();
|
|
184
|
+
permit.type = 'recipient';
|
|
185
|
+
permit.issuerSignature = '0x1234567890abcdef';
|
|
186
|
+
permit.recipient = '0x70997970C51812dc3A010C7d01b50e0d17dc79C8'; // Alice's address
|
|
187
|
+
permit.recipientSignature = '0x';
|
|
188
|
+
|
|
189
|
+
expect(() => validateImportPermit(permit)).toThrow();
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
describe('ValidationUtils', () => {
|
|
194
|
+
describe('isExpired', () => {
|
|
195
|
+
it('should return true for expired permit', async () => {
|
|
196
|
+
const permit = {
|
|
197
|
+
...(await createMockPermit()),
|
|
198
|
+
expiration: Math.floor(Date.now() / 1000) - 3600, // 1 hour ago
|
|
199
|
+
};
|
|
200
|
+
expect(ValidationUtils.isExpired(permit)).toBe(true);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('should return false for non-expired permit', async () => {
|
|
204
|
+
const permit = {
|
|
205
|
+
...(await createMockPermit()),
|
|
206
|
+
expiration: Math.floor(Date.now() / 1000) + 3600, // 1 hour from now
|
|
207
|
+
};
|
|
208
|
+
expect(ValidationUtils.isExpired(permit)).toBe(false);
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
describe('isSigned', () => {
|
|
213
|
+
it('should return true for signed self permit', async () => {
|
|
214
|
+
const permit = {
|
|
215
|
+
...(await createMockPermit()),
|
|
216
|
+
type: 'self' as const,
|
|
217
|
+
issuerSignature: '0x1234567890abcdef' as `0x${string}`,
|
|
218
|
+
};
|
|
219
|
+
expect(ValidationUtils.isSigned(permit)).toBe(true);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('should return false for unsigned self permit', async () => {
|
|
223
|
+
const permit = {
|
|
224
|
+
...(await createMockPermit()),
|
|
225
|
+
type: 'self' as const,
|
|
226
|
+
issuerSignature: '0x' as `0x${string}`,
|
|
227
|
+
};
|
|
228
|
+
expect(ValidationUtils.isSigned(permit)).toBe(false);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('should return true for signed recipient permit', async () => {
|
|
232
|
+
const permit = {
|
|
233
|
+
...(await createMockPermit()),
|
|
234
|
+
type: 'recipient' as const,
|
|
235
|
+
recipientSignature: '0x1234567890abcdef' as `0x${string}`,
|
|
236
|
+
};
|
|
237
|
+
expect(ValidationUtils.isSigned(permit)).toBe(true);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('should return false for unsigned recipient permit', async () => {
|
|
241
|
+
const permit = {
|
|
242
|
+
...(await createMockPermit()),
|
|
243
|
+
type: 'recipient' as const,
|
|
244
|
+
recipientSignature: '0x' as `0x${string}`,
|
|
245
|
+
};
|
|
246
|
+
expect(ValidationUtils.isSigned(permit)).toBe(false);
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
describe('isSignedAndNotExpired', () => {
|
|
251
|
+
it('should return valid for valid permit', async () => {
|
|
252
|
+
const permit = {
|
|
253
|
+
...(await createMockPermit()),
|
|
254
|
+
expiration: Math.floor(Date.now() / 1000) + 3600,
|
|
255
|
+
issuerSignature: '0x1234567890abcdef' as `0x${string}`,
|
|
256
|
+
};
|
|
257
|
+
const result = ValidationUtils.isSignedAndNotExpired(permit);
|
|
258
|
+
expect(result.valid).toBe(true);
|
|
259
|
+
expect(result.error).toBeNull();
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it('should return invalid for expired permit', async () => {
|
|
263
|
+
const permit = {
|
|
264
|
+
...(await createMockPermit()),
|
|
265
|
+
expiration: Math.floor(Date.now() / 1000) - 3600,
|
|
266
|
+
issuerSignature: '0x1234567890abcdef' as `0x${string}`,
|
|
267
|
+
};
|
|
268
|
+
const result = ValidationUtils.isSignedAndNotExpired(permit);
|
|
269
|
+
expect(result.valid).toBe(false);
|
|
270
|
+
expect(result.error).toBe('expired');
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it('should return invalid for unsigned permit', async () => {
|
|
274
|
+
const permit = {
|
|
275
|
+
...(await createMockPermit()),
|
|
276
|
+
expiration: Math.floor(Date.now() / 1000) + 3600,
|
|
277
|
+
issuerSignature: '0x' as `0x${string}`,
|
|
278
|
+
};
|
|
279
|
+
const result = ValidationUtils.isSignedAndNotExpired(permit);
|
|
280
|
+
expect(result.valid).toBe(false);
|
|
281
|
+
expect(result.error).toBe('not-signed');
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
describe('assertSignedAndNotExpired', () => {
|
|
286
|
+
it('should not throw for valid permit', async () => {
|
|
287
|
+
const permit = {
|
|
288
|
+
...(await createMockPermit()),
|
|
289
|
+
expiration: Math.floor(Date.now() / 1000) + 3600,
|
|
290
|
+
issuerSignature: '0x1234567890abcdef' as `0x${string}`,
|
|
291
|
+
};
|
|
292
|
+
expect(() => ValidationUtils.assertSignedAndNotExpired(permit)).not.toThrow();
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('should throw for expired permit', async () => {
|
|
296
|
+
const permit = {
|
|
297
|
+
...(await createMockPermit()),
|
|
298
|
+
expiration: Math.floor(Date.now() / 1000) - 3600,
|
|
299
|
+
issuerSignature: '0x1234567890abcdef' as `0x${string}`,
|
|
300
|
+
};
|
|
301
|
+
expect(() => ValidationUtils.assertSignedAndNotExpired(permit)).toThrow('Permit is expired');
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it('should throw for unsigned permit', async () => {
|
|
305
|
+
const permit = {
|
|
306
|
+
...(await createMockPermit()),
|
|
307
|
+
expiration: Math.floor(Date.now() / 1000) + 3600,
|
|
308
|
+
issuerSignature: '0x' as `0x${string}`,
|
|
309
|
+
};
|
|
310
|
+
expect(() => ValidationUtils.assertSignedAndNotExpired(permit)).toThrow('Permit is not signed');
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
describe('isValid', () => {
|
|
315
|
+
it('should return invalid-schema for schema-invalid permit', async () => {
|
|
316
|
+
const permit = {
|
|
317
|
+
...(await createMockPermit()),
|
|
318
|
+
type: 'self' as const,
|
|
319
|
+
expiration: Math.floor(Date.now() / 1000) + 3600,
|
|
320
|
+
issuerSignature: '0x1234567890abcdef' as `0x${string}`,
|
|
321
|
+
// Self permits must have recipient == zeroAddress per schema.
|
|
322
|
+
recipient: '0x70997970C51812dc3A010C7d01b50e0d17dc79C8' as `0x${string}`,
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
const result = ValidationUtils.isValid(permit as unknown as Permit);
|
|
326
|
+
expect(result.valid).toBe(false);
|
|
327
|
+
expect(result.error).toBe('invalid-schema');
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it('should return expired for expired but otherwise schema-valid permit', async () => {
|
|
331
|
+
const permit = {
|
|
332
|
+
...(await createMockPermit()),
|
|
333
|
+
type: 'self' as const,
|
|
334
|
+
expiration: Math.floor(Date.now() / 1000) - 3600,
|
|
335
|
+
issuerSignature: '0x1234567890abcdef' as `0x${string}`,
|
|
336
|
+
recipient: '0x0000000000000000000000000000000000000000' as `0x${string}`,
|
|
337
|
+
recipientSignature: '0x' as `0x${string}`,
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
const result = ValidationUtils.isValid(permit as unknown as Permit);
|
|
341
|
+
expect(result.valid).toBe(false);
|
|
342
|
+
expect(result.error).toBe('expired');
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it('should return valid for schema-valid, signed, non-expired permit', async () => {
|
|
346
|
+
const permit = {
|
|
347
|
+
...(await createMockPermit()),
|
|
348
|
+
type: 'self' as const,
|
|
349
|
+
expiration: Math.floor(Date.now() / 1000) + 3600,
|
|
350
|
+
issuerSignature: '0x1234567890abcdef' as `0x${string}`,
|
|
351
|
+
recipient: '0x0000000000000000000000000000000000000000' as `0x${string}`,
|
|
352
|
+
recipientSignature: '0x' as `0x${string}`,
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
const result = ValidationUtils.isValid(permit as unknown as Permit);
|
|
356
|
+
expect(result.valid).toBe(true);
|
|
357
|
+
expect(result.error).toBeNull();
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
});
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { getAddress, isAddress, isHex, zeroAddress, type Hex } from 'viem';
|
|
3
|
+
import type { Permit, ValidationResult } from './types.js';
|
|
4
|
+
|
|
5
|
+
const SerializedSealingPair = z.object({
|
|
6
|
+
privateKey: z.string(),
|
|
7
|
+
publicKey: z.string(),
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
export const addressSchema = z
|
|
11
|
+
.string()
|
|
12
|
+
.refine((val) => isAddress(val), {
|
|
13
|
+
error: 'Invalid address',
|
|
14
|
+
})
|
|
15
|
+
.transform((val): Hex => getAddress(val));
|
|
16
|
+
|
|
17
|
+
export const addressNotZeroSchema = addressSchema.refine((val) => val !== zeroAddress, {
|
|
18
|
+
error: 'Must not be zeroAddress',
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export const bytesSchema = z.custom<Hex>(
|
|
22
|
+
(val) => {
|
|
23
|
+
return typeof val === 'string' && isHex(val);
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
message: 'Invalid hex value',
|
|
27
|
+
}
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
export const bytesNotEmptySchema = bytesSchema.refine((val) => val !== '0x', {
|
|
31
|
+
error: 'Must not be empty',
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const DEFAULT_EXPIRATION_FN = () => Math.round(Date.now() / 1000) + 7 * 24 * 60 * 60; // 7 days from now
|
|
35
|
+
|
|
36
|
+
const zPermitWithDefaults = z.object({
|
|
37
|
+
name: z.string().optional().default('Unnamed Permit'),
|
|
38
|
+
type: z.enum(['self', 'sharing', 'recipient']),
|
|
39
|
+
issuer: addressNotZeroSchema,
|
|
40
|
+
expiration: z.int().optional().default(DEFAULT_EXPIRATION_FN),
|
|
41
|
+
recipient: addressSchema.optional().default(zeroAddress),
|
|
42
|
+
validatorId: z.int().optional().default(0),
|
|
43
|
+
validatorContract: addressSchema.optional().default(zeroAddress),
|
|
44
|
+
issuerSignature: bytesSchema.optional().default('0x'),
|
|
45
|
+
recipientSignature: bytesSchema.optional().default('0x'),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const zPermitWithSealingPair = zPermitWithDefaults.extend({
|
|
49
|
+
sealingPair: SerializedSealingPair.optional(),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
type zPermitType = z.infer<typeof zPermitWithDefaults>;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Permits allow a hook into an optional external validator contract,
|
|
56
|
+
* this check ensures that IF an external validator is applied, that both `validatorId` and `validatorContract` are populated,
|
|
57
|
+
* ELSE ensures that both `validatorId` and `validatorContract` are empty
|
|
58
|
+
*/
|
|
59
|
+
const ExternalValidatorRefinement = [
|
|
60
|
+
(data: zPermitType) =>
|
|
61
|
+
(data.validatorId !== 0 && data.validatorContract !== zeroAddress) ||
|
|
62
|
+
(data.validatorId === 0 && data.validatorContract === zeroAddress),
|
|
63
|
+
{
|
|
64
|
+
error: 'Permit external validator :: validatorId and validatorContract must either both be set or both be unset.',
|
|
65
|
+
path: ['validatorId', 'validatorContract'] as string[],
|
|
66
|
+
},
|
|
67
|
+
] as const;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Prevents sharable permit from having the same issuer and recipient
|
|
71
|
+
*/
|
|
72
|
+
const RecipientRefinement = [
|
|
73
|
+
(data: zPermitType) => data.issuer !== data.recipient,
|
|
74
|
+
{
|
|
75
|
+
error: 'Sharing permit :: issuer and recipient must not be the same',
|
|
76
|
+
path: ['issuer', 'recipient'] as string[],
|
|
77
|
+
},
|
|
78
|
+
] as const;
|
|
79
|
+
|
|
80
|
+
// ============================================================================
|
|
81
|
+
// SELF PERMIT VALIDATORS
|
|
82
|
+
// ============================================================================
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Validator for self permit creation options
|
|
86
|
+
*/
|
|
87
|
+
export const SelfPermitOptionsValidator = z
|
|
88
|
+
.object({
|
|
89
|
+
type: z.literal('self').optional().default('self'),
|
|
90
|
+
issuer: addressNotZeroSchema,
|
|
91
|
+
name: z.string().optional().default('Unnamed Permit'),
|
|
92
|
+
expiration: z.int().optional().default(DEFAULT_EXPIRATION_FN),
|
|
93
|
+
recipient: addressSchema.optional().default(zeroAddress),
|
|
94
|
+
validatorId: z.int().optional().default(0),
|
|
95
|
+
validatorContract: addressSchema.optional().default(zeroAddress),
|
|
96
|
+
issuerSignature: bytesSchema.optional().default('0x'),
|
|
97
|
+
recipientSignature: bytesSchema.optional().default('0x'),
|
|
98
|
+
})
|
|
99
|
+
.refine(...ExternalValidatorRefinement);
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Validator for fully formed self permits
|
|
103
|
+
*/
|
|
104
|
+
export const SelfPermitValidator = zPermitWithSealingPair
|
|
105
|
+
.refine((data) => data.type === 'self', {
|
|
106
|
+
error: "Type must be 'self'",
|
|
107
|
+
})
|
|
108
|
+
.refine((data) => data.recipient === zeroAddress, {
|
|
109
|
+
error: 'Recipient must be zeroAddress',
|
|
110
|
+
})
|
|
111
|
+
.refine((data) => data.issuerSignature !== '0x', {
|
|
112
|
+
error: 'IssuerSignature must be populated',
|
|
113
|
+
})
|
|
114
|
+
.refine((data) => data.recipientSignature === '0x', {
|
|
115
|
+
error: 'RecipientSignature must be empty',
|
|
116
|
+
})
|
|
117
|
+
.refine(...ExternalValidatorRefinement);
|
|
118
|
+
|
|
119
|
+
// ============================================================================
|
|
120
|
+
// SHARING PERMIT VALIDATORS
|
|
121
|
+
// ============================================================================
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Validator for sharing permit creation options
|
|
125
|
+
*/
|
|
126
|
+
export const SharingPermitOptionsValidator = z
|
|
127
|
+
.object({
|
|
128
|
+
type: z.literal('sharing').optional().default('sharing'),
|
|
129
|
+
issuer: addressNotZeroSchema,
|
|
130
|
+
recipient: addressNotZeroSchema,
|
|
131
|
+
name: z.string().optional().default('Unnamed Permit'),
|
|
132
|
+
expiration: z.int().optional().default(DEFAULT_EXPIRATION_FN),
|
|
133
|
+
validatorId: z.int().optional().default(0),
|
|
134
|
+
validatorContract: addressSchema.optional().default(zeroAddress),
|
|
135
|
+
issuerSignature: bytesSchema.optional().default('0x'),
|
|
136
|
+
recipientSignature: bytesSchema.optional().default('0x'),
|
|
137
|
+
})
|
|
138
|
+
.refine(...RecipientRefinement)
|
|
139
|
+
.refine(...ExternalValidatorRefinement);
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Validator for fully formed sharing permits
|
|
143
|
+
*/
|
|
144
|
+
export const SharingPermitValidator = zPermitWithSealingPair
|
|
145
|
+
.refine((data) => data.type === 'sharing', {
|
|
146
|
+
error: "Type must be 'sharing'",
|
|
147
|
+
})
|
|
148
|
+
.refine((data) => data.recipient !== zeroAddress, {
|
|
149
|
+
error: 'Recipient must not be zeroAddress',
|
|
150
|
+
})
|
|
151
|
+
.refine((data) => data.issuerSignature !== '0x', {
|
|
152
|
+
error: 'IssuerSignature must be populated',
|
|
153
|
+
})
|
|
154
|
+
.refine((data) => data.recipientSignature === '0x', {
|
|
155
|
+
error: 'RecipientSignature must be empty',
|
|
156
|
+
})
|
|
157
|
+
.refine(...ExternalValidatorRefinement);
|
|
158
|
+
|
|
159
|
+
// ============================================================================
|
|
160
|
+
// IMPORT/RECIPIENT PERMIT VALIDATORS
|
|
161
|
+
// ============================================================================
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Validator for import permit creation options (recipient receiving shared permit)
|
|
165
|
+
*/
|
|
166
|
+
export const ImportPermitOptionsValidator = z
|
|
167
|
+
.object({
|
|
168
|
+
type: z.literal('recipient').optional().default('recipient'),
|
|
169
|
+
issuer: addressNotZeroSchema,
|
|
170
|
+
recipient: addressNotZeroSchema,
|
|
171
|
+
name: z.string().optional().default('Unnamed Permit'),
|
|
172
|
+
expiration: z.int(),
|
|
173
|
+
validatorId: z.int().optional().default(0),
|
|
174
|
+
validatorContract: addressSchema.optional().default(zeroAddress),
|
|
175
|
+
issuerSignature: bytesNotEmptySchema,
|
|
176
|
+
recipientSignature: bytesSchema.optional().default('0x'),
|
|
177
|
+
})
|
|
178
|
+
.refine(...ExternalValidatorRefinement);
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Validator for fully formed import/recipient permits
|
|
182
|
+
*/
|
|
183
|
+
export const ImportPermitValidator = zPermitWithSealingPair
|
|
184
|
+
.refine((data) => data.type === 'recipient', {
|
|
185
|
+
error: "Type must be 'recipient'",
|
|
186
|
+
})
|
|
187
|
+
.refine((data) => data.recipient !== zeroAddress, {
|
|
188
|
+
error: 'Recipient must not be zeroAddress',
|
|
189
|
+
})
|
|
190
|
+
.refine((data) => data.issuerSignature !== '0x', {
|
|
191
|
+
error: 'IssuerSignature must be populated',
|
|
192
|
+
})
|
|
193
|
+
.refine((data) => data.recipientSignature !== '0x', {
|
|
194
|
+
error: 'RecipientSignature must be populated',
|
|
195
|
+
})
|
|
196
|
+
.refine(...ExternalValidatorRefinement);
|
|
197
|
+
|
|
198
|
+
// ============================================================================
|
|
199
|
+
// VALIDATION FUNCTIONS
|
|
200
|
+
// ============================================================================
|
|
201
|
+
|
|
202
|
+
const safeParseAndThrowFormatted = <T extends z.ZodTypeAny>(schema: T, data: unknown, message: string): z.output<T> => {
|
|
203
|
+
const result = schema.safeParse(data);
|
|
204
|
+
if (!result.success) {
|
|
205
|
+
throw new Error(`${message}: ${z.prettifyError(result.error)}`, { cause: result.error });
|
|
206
|
+
}
|
|
207
|
+
return result.data;
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Validates self permit creation options
|
|
212
|
+
*/
|
|
213
|
+
export const validateSelfPermitOptions = (options: any) => {
|
|
214
|
+
return safeParseAndThrowFormatted(SelfPermitOptionsValidator, options, 'Invalid self permit options');
|
|
215
|
+
};
|
|
216
|
+
/**
|
|
217
|
+
* Validates sharing permit creation options
|
|
218
|
+
*/
|
|
219
|
+
export const validateSharingPermitOptions = (options: any) => {
|
|
220
|
+
return safeParseAndThrowFormatted(SharingPermitOptionsValidator, options, 'Invalid sharing permit options');
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Validates import permit creation options
|
|
225
|
+
*/
|
|
226
|
+
export const validateImportPermitOptions = (options: any) => {
|
|
227
|
+
return safeParseAndThrowFormatted(ImportPermitOptionsValidator, options, 'Invalid import permit options');
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Validates a fully formed self permit
|
|
232
|
+
*/
|
|
233
|
+
export const validateSelfPermit = (permit: any) => {
|
|
234
|
+
return safeParseAndThrowFormatted(SelfPermitValidator, permit, 'Invalid self permit');
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Validates a fully formed sharing permit
|
|
239
|
+
*/
|
|
240
|
+
export const validateSharingPermit = (permit: any) => {
|
|
241
|
+
return safeParseAndThrowFormatted(SharingPermitValidator, permit, 'Invalid sharing permit');
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Validates a fully formed import/recipient permit
|
|
246
|
+
*/
|
|
247
|
+
export const validateImportPermit = (permit: any) => {
|
|
248
|
+
return safeParseAndThrowFormatted(ImportPermitValidator, permit, 'Invalid import permit');
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Simple validation functions for common checks
|
|
253
|
+
*/
|
|
254
|
+
export const ValidationUtils = {
|
|
255
|
+
/**
|
|
256
|
+
* Check if permit is expired
|
|
257
|
+
*/
|
|
258
|
+
isExpired: (permit: Permit): boolean => {
|
|
259
|
+
return permit.expiration < Math.floor(Date.now() / 1000);
|
|
260
|
+
},
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Check if permit is signed by the active party
|
|
264
|
+
*/
|
|
265
|
+
isSigned: (permit: Permit): boolean => {
|
|
266
|
+
if (permit.type === 'self' || permit.type === 'sharing') {
|
|
267
|
+
return permit.issuerSignature !== '0x';
|
|
268
|
+
}
|
|
269
|
+
if (permit.type === 'recipient') {
|
|
270
|
+
return permit.recipientSignature !== '0x';
|
|
271
|
+
}
|
|
272
|
+
return false;
|
|
273
|
+
},
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Checks that a permit is signed and not expired.
|
|
277
|
+
*/
|
|
278
|
+
isSignedAndNotExpired: (permit: Permit): ValidationResult => {
|
|
279
|
+
if (ValidationUtils.isExpired(permit)) {
|
|
280
|
+
return { valid: false, error: 'expired' };
|
|
281
|
+
}
|
|
282
|
+
if (!ValidationUtils.isSigned(permit)) {
|
|
283
|
+
return { valid: false, error: 'not-signed' };
|
|
284
|
+
}
|
|
285
|
+
return { valid: true, error: null };
|
|
286
|
+
},
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Asserts that a permit is signed and not expired.
|
|
290
|
+
*
|
|
291
|
+
* Throws `Error` with message:
|
|
292
|
+
* - `Permit is expired`
|
|
293
|
+
* - `Permit is not signed`
|
|
294
|
+
*/
|
|
295
|
+
assertSignedAndNotExpired: (permit: Permit): void => {
|
|
296
|
+
const result = ValidationUtils.isSignedAndNotExpired(permit);
|
|
297
|
+
if (result.valid) return;
|
|
298
|
+
|
|
299
|
+
if (result.error === 'expired') {
|
|
300
|
+
throw new Error('Permit is expired');
|
|
301
|
+
}
|
|
302
|
+
if (result.error === 'not-signed') {
|
|
303
|
+
throw new Error('Permit is not signed');
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Should be unreachable, but keeps this future-proof.
|
|
307
|
+
throw new Error('Permit is invalid');
|
|
308
|
+
},
|
|
309
|
+
|
|
310
|
+
isValid: (permit: Permit): ValidationResult => {
|
|
311
|
+
const schema =
|
|
312
|
+
permit.type === 'self'
|
|
313
|
+
? SelfPermitValidator
|
|
314
|
+
: permit.type === 'sharing'
|
|
315
|
+
? SharingPermitValidator
|
|
316
|
+
: permit.type === 'recipient'
|
|
317
|
+
? ImportPermitValidator
|
|
318
|
+
: null;
|
|
319
|
+
|
|
320
|
+
if (schema == null) return { valid: false, error: 'invalid-schema' };
|
|
321
|
+
|
|
322
|
+
const schemaResult = schema.safeParse(permit);
|
|
323
|
+
if (!schemaResult.success) return { valid: false, error: 'invalid-schema' };
|
|
324
|
+
|
|
325
|
+
return ValidationUtils.isSignedAndNotExpired(permit);
|
|
326
|
+
},
|
|
327
|
+
};
|