@cofhe/sdk 0.1.0 → 0.1.1
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 +48 -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 +37 -0
- package/adapters/index.test.ts +25 -0
- package/adapters/index.ts +5 -0
- package/adapters/smartWallet.ts +91 -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/sepolia.ts +14 -0
- package/chains/chains.test.ts +49 -0
- package/chains/defineChain.ts +18 -0
- package/chains/index.ts +33 -0
- package/chains/types.ts +32 -0
- package/core/baseBuilder.ts +138 -0
- package/core/client.test.ts +298 -0
- package/core/client.ts +308 -0
- package/core/config.test.ts +224 -0
- package/core/config.ts +213 -0
- package/core/decrypt/MockQueryDecrypterAbi.ts +129 -0
- package/core/decrypt/cofheMocksSealOutput.ts +57 -0
- package/core/decrypt/decryptHandleBuilder.ts +281 -0
- package/core/decrypt/decryptUtils.ts +28 -0
- package/core/decrypt/tnSealOutput.ts +59 -0
- package/core/encrypt/MockZkVerifierAbi.ts +106 -0
- package/core/encrypt/cofheMocksZkVerifySign.ts +278 -0
- package/core/encrypt/encryptInputsBuilder.test.ts +735 -0
- package/core/encrypt/encryptInputsBuilder.ts +512 -0
- package/core/encrypt/encryptUtils.ts +64 -0
- package/core/encrypt/zkPackProveVerify.ts +273 -0
- package/core/error.ts +170 -0
- package/core/fetchKeys.test.ts +212 -0
- package/core/fetchKeys.ts +170 -0
- package/core/index.ts +77 -0
- package/core/keyStore.test.ts +226 -0
- package/core/keyStore.ts +127 -0
- package/core/permits.test.ts +242 -0
- package/core/permits.ts +136 -0
- package/core/result.test.ts +180 -0
- package/core/result.ts +67 -0
- package/core/test-utils.ts +45 -0
- package/core/types.ts +352 -0
- package/core/utils.ts +88 -0
- package/dist/adapters.cjs +88 -0
- package/dist/adapters.d.cts +14558 -0
- package/dist/adapters.d.ts +14558 -0
- package/dist/adapters.js +83 -0
- package/dist/chains.cjs +101 -0
- package/dist/chains.d.cts +99 -0
- package/dist/chains.d.ts +99 -0
- package/dist/chains.js +1 -0
- package/dist/chunk-GZCQQYVI.js +93 -0
- package/dist/chunk-KFGPTJ6X.js +2295 -0
- package/dist/chunk-LU7BMUUT.js +804 -0
- package/dist/core.cjs +3174 -0
- package/dist/core.d.cts +16 -0
- package/dist/core.d.ts +16 -0
- package/dist/core.js +3 -0
- package/dist/node.cjs +3090 -0
- package/dist/node.d.cts +22 -0
- package/dist/node.d.ts +22 -0
- package/dist/node.js +90 -0
- package/dist/permit-S9CnI6MF.d.cts +333 -0
- package/dist/permit-S9CnI6MF.d.ts +333 -0
- package/dist/permits.cjs +856 -0
- package/dist/permits.d.cts +1056 -0
- package/dist/permits.d.ts +1056 -0
- package/dist/permits.js +1 -0
- package/dist/types-KImPrEIe.d.cts +48 -0
- package/dist/types-KImPrEIe.d.ts +48 -0
- package/dist/types-PhwGgQvs.d.ts +953 -0
- package/dist/types-bB7wLj0q.d.cts +953 -0
- package/dist/web.cjs +3067 -0
- package/dist/web.d.cts +22 -0
- package/dist/web.d.ts +22 -0
- package/dist/web.js +64 -0
- package/node/client.test.ts +152 -0
- package/node/config.test.ts +68 -0
- package/node/encryptInputs.test.ts +175 -0
- package/node/index.ts +96 -0
- package/node/storage.ts +51 -0
- package/package.json +15 -3
- package/permits/index.ts +67 -0
- package/permits/localstorage.test.ts +118 -0
- package/permits/permit.test.ts +474 -0
- package/permits/permit.ts +396 -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 +128 -0
- package/permits/store.ts +168 -0
- package/permits/test-utils.ts +20 -0
- package/permits/types.ts +174 -0
- package/permits/utils.ts +63 -0
- package/permits/validation.test.ts +288 -0
- package/permits/validation.ts +349 -0
- package/web/client.web.test.ts +152 -0
- package/web/config.web.test.ts +71 -0
- package/web/encryptInputs.web.test.ts +195 -0
- package/web/index.ts +97 -0
- package/web/storage.ts +20 -0
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/* eslint-disable no-unused-vars */
|
|
2
|
+
|
|
3
|
+
import { CofhesdkError, CofhesdkErrorCode } from '../error.js';
|
|
4
|
+
import { type EncryptableItem, FheTypes } from '../types.js';
|
|
5
|
+
import { toBigIntOrThrow, validateBigIntInRange, toHexString, hexToBytes } from '../utils.js';
|
|
6
|
+
|
|
7
|
+
// ===== TYPES =====
|
|
8
|
+
|
|
9
|
+
export type VerifyResultRaw = {
|
|
10
|
+
ct_hash: string;
|
|
11
|
+
signature: string;
|
|
12
|
+
recid: number;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type VerifyResult = {
|
|
16
|
+
ct_hash: string;
|
|
17
|
+
signature: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type ZkProvenCiphertextList = {
|
|
21
|
+
serialize(): Uint8Array;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type ZkCompactPkeCrs = {
|
|
25
|
+
free(): void;
|
|
26
|
+
serialize(compress: boolean): Uint8Array;
|
|
27
|
+
safe_serialize(serialized_size_limit: bigint): Uint8Array;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type ZkCompactPkeCrsConstructor = {
|
|
31
|
+
deserialize(buffer: Uint8Array): ZkCompactPkeCrs;
|
|
32
|
+
safe_deserialize(buffer: Uint8Array, serialized_size_limit: bigint): ZkCompactPkeCrs;
|
|
33
|
+
from_config(config: unknown, max_num_bits: number): ZkCompactPkeCrs;
|
|
34
|
+
deserialize_from_public_params(buffer: Uint8Array): ZkCompactPkeCrs;
|
|
35
|
+
safe_deserialize_from_public_params(buffer: Uint8Array, serialized_size_limit: bigint): ZkCompactPkeCrs;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export type ZkCiphertextListBuilder = {
|
|
39
|
+
push_boolean(data: boolean): void;
|
|
40
|
+
push_u8(data: number): void;
|
|
41
|
+
push_u16(data: number): void;
|
|
42
|
+
push_u32(data: number): void;
|
|
43
|
+
push_u64(data: bigint): void;
|
|
44
|
+
push_u128(data: bigint): void;
|
|
45
|
+
push_u160(data: bigint): void;
|
|
46
|
+
build_with_proof_packed(
|
|
47
|
+
crs: ZkCompactPkeCrs,
|
|
48
|
+
metadata: Uint8Array,
|
|
49
|
+
computeLoad: 1 // ZkComputeLoad.Verify
|
|
50
|
+
): ZkProvenCiphertextList;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export type ZkBuilderAndCrsGenerator = (
|
|
54
|
+
fhe: string,
|
|
55
|
+
crs: string
|
|
56
|
+
) => { zkBuilder: ZkCiphertextListBuilder; zkCrs: ZkCompactPkeCrs };
|
|
57
|
+
|
|
58
|
+
// ===== CONSTANTS =====
|
|
59
|
+
|
|
60
|
+
export const MAX_UINT8: bigint = 255n;
|
|
61
|
+
export const MAX_UINT16: bigint = 65535n;
|
|
62
|
+
export const MAX_UINT32: bigint = 4294967295n;
|
|
63
|
+
export const MAX_UINT64: bigint = 18446744073709551615n; // 2^64 - 1
|
|
64
|
+
export const MAX_UINT128: bigint = 340282366920938463463374607431768211455n; // 2^128 - 1
|
|
65
|
+
export const MAX_UINT256: bigint = 115792089237316195423570985008687907853269984665640564039457584007913129640319n; // 2^256 - 1
|
|
66
|
+
export const MAX_UINT160: bigint = 1461501637330902918203684832716283019655932542975n; // 2^160 - 1
|
|
67
|
+
export const MAX_ENCRYPTABLE_BITS: number = 2048;
|
|
68
|
+
|
|
69
|
+
// ===== CORE FUNCTIONS =====
|
|
70
|
+
|
|
71
|
+
export const zkPack = (items: EncryptableItem[], builder: ZkCiphertextListBuilder): ZkCiphertextListBuilder => {
|
|
72
|
+
let totalBits = 0;
|
|
73
|
+
for (const item of items) {
|
|
74
|
+
switch (item.utype) {
|
|
75
|
+
case FheTypes.Bool: {
|
|
76
|
+
builder.push_boolean(item.data);
|
|
77
|
+
totalBits += 1;
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
case FheTypes.Uint8: {
|
|
81
|
+
const bint = toBigIntOrThrow(item.data);
|
|
82
|
+
validateBigIntInRange(bint, MAX_UINT8);
|
|
83
|
+
builder.push_u8(parseInt(bint.toString()));
|
|
84
|
+
totalBits += 8;
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
case FheTypes.Uint16: {
|
|
88
|
+
const bint = toBigIntOrThrow(item.data);
|
|
89
|
+
validateBigIntInRange(bint, MAX_UINT16);
|
|
90
|
+
builder.push_u16(parseInt(bint.toString()));
|
|
91
|
+
totalBits += 16;
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
case FheTypes.Uint32: {
|
|
95
|
+
const bint = toBigIntOrThrow(item.data);
|
|
96
|
+
validateBigIntInRange(bint, MAX_UINT32);
|
|
97
|
+
builder.push_u32(parseInt(bint.toString()));
|
|
98
|
+
totalBits += 32;
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
case FheTypes.Uint64: {
|
|
102
|
+
const bint = toBigIntOrThrow(item.data);
|
|
103
|
+
validateBigIntInRange(bint, MAX_UINT64);
|
|
104
|
+
builder.push_u64(bint);
|
|
105
|
+
totalBits += 64;
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
case FheTypes.Uint128: {
|
|
109
|
+
const bint = toBigIntOrThrow(item.data);
|
|
110
|
+
validateBigIntInRange(bint, MAX_UINT128);
|
|
111
|
+
builder.push_u128(bint);
|
|
112
|
+
totalBits += 128;
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
// [U256-DISABLED]
|
|
116
|
+
// case FheTypes.Uint256: {
|
|
117
|
+
// const bint = toBigIntOrThrow(item.data);
|
|
118
|
+
// validateBigIntInRange(bint, MAX_UINT256);
|
|
119
|
+
// builder.push_u256(bint);
|
|
120
|
+
// totalBits += 256;
|
|
121
|
+
// break;
|
|
122
|
+
// }
|
|
123
|
+
case FheTypes.Uint160: {
|
|
124
|
+
const bint = toBigIntOrThrow(item.data);
|
|
125
|
+
validateBigIntInRange(bint, MAX_UINT160);
|
|
126
|
+
builder.push_u160(bint);
|
|
127
|
+
totalBits += 160;
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
default: {
|
|
131
|
+
throw new CofhesdkError({
|
|
132
|
+
code: CofhesdkErrorCode.ZkPackFailed,
|
|
133
|
+
message: `Invalid utype: ${(item as any).utype}`,
|
|
134
|
+
hint: `Ensure that the utype is valid, using the Encryptable type, for example: Encryptable.uint128(100n)`,
|
|
135
|
+
context: {
|
|
136
|
+
item,
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (totalBits > MAX_ENCRYPTABLE_BITS) {
|
|
144
|
+
throw new CofhesdkError({
|
|
145
|
+
code: CofhesdkErrorCode.ZkPackFailed,
|
|
146
|
+
message: `Total bits ${totalBits} exceeds ${MAX_ENCRYPTABLE_BITS}`,
|
|
147
|
+
hint: `Ensure that the total bits of the items to encrypt does not exceed ${MAX_ENCRYPTABLE_BITS}`,
|
|
148
|
+
context: {
|
|
149
|
+
totalBits,
|
|
150
|
+
maxBits: MAX_ENCRYPTABLE_BITS,
|
|
151
|
+
items,
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return builder;
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
export const zkProve = async (
|
|
160
|
+
builder: ZkCiphertextListBuilder,
|
|
161
|
+
crs: ZkCompactPkeCrs,
|
|
162
|
+
address: string,
|
|
163
|
+
securityZone: number,
|
|
164
|
+
chainId: number
|
|
165
|
+
): Promise<Uint8Array> => {
|
|
166
|
+
const metadata = constructZkPoKMetadata(address, securityZone, chainId);
|
|
167
|
+
|
|
168
|
+
return new Promise((resolve) => {
|
|
169
|
+
setTimeout(() => {
|
|
170
|
+
const compactList = builder.build_with_proof_packed(
|
|
171
|
+
crs,
|
|
172
|
+
metadata,
|
|
173
|
+
1 // ZkComputeLoad.Verify
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
resolve(compactList.serialize());
|
|
177
|
+
}, 0);
|
|
178
|
+
});
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const constructZkPoKMetadata = (accountAddr: string, securityZone: number, chainId: number): Uint8Array => {
|
|
182
|
+
// Decode the account address from hex
|
|
183
|
+
const accountAddrNoPrefix = accountAddr.startsWith('0x') ? accountAddr.slice(2) : accountAddr;
|
|
184
|
+
const accountBytes = hexToBytes(accountAddrNoPrefix);
|
|
185
|
+
|
|
186
|
+
// Encode chainId as 32 bytes (u256) in big-endian format
|
|
187
|
+
const chainIdBytes = new Uint8Array(32);
|
|
188
|
+
|
|
189
|
+
// Since chain IDs are typically small numbers, we can just encode them
|
|
190
|
+
// directly without BigInt operations, filling only the necessary bytes
|
|
191
|
+
// from the right (least significant)
|
|
192
|
+
let value = chainId;
|
|
193
|
+
for (let i = 31; i >= 0 && value > 0; i--) {
|
|
194
|
+
chainIdBytes[i] = value & 0xff;
|
|
195
|
+
value = value >>> 8;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const metadata = new Uint8Array(1 + accountBytes.length + 32);
|
|
199
|
+
metadata[0] = securityZone;
|
|
200
|
+
metadata.set(accountBytes, 1);
|
|
201
|
+
metadata.set(chainIdBytes, 1 + accountBytes.length);
|
|
202
|
+
|
|
203
|
+
return metadata;
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
export const zkVerify = async (
|
|
207
|
+
verifierUrl: string,
|
|
208
|
+
serializedBytes: Uint8Array,
|
|
209
|
+
address: string,
|
|
210
|
+
securityZone: number,
|
|
211
|
+
chainId: number
|
|
212
|
+
): Promise<VerifyResult[]> => {
|
|
213
|
+
// Convert bytearray to hex string
|
|
214
|
+
const packed_list = toHexString(serializedBytes);
|
|
215
|
+
|
|
216
|
+
const sz_byte = new Uint8Array([securityZone]);
|
|
217
|
+
|
|
218
|
+
// Construct request payload
|
|
219
|
+
const payload = {
|
|
220
|
+
packed_list,
|
|
221
|
+
account_addr: address,
|
|
222
|
+
security_zone: sz_byte[0],
|
|
223
|
+
chain_id: chainId,
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const body = JSON.stringify(payload);
|
|
227
|
+
|
|
228
|
+
// Send request to verification server
|
|
229
|
+
try {
|
|
230
|
+
const response = await fetch(`${verifierUrl}/verify`, {
|
|
231
|
+
method: 'POST',
|
|
232
|
+
headers: {
|
|
233
|
+
'Content-Type': 'application/json',
|
|
234
|
+
},
|
|
235
|
+
body,
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
if (!response.ok) {
|
|
239
|
+
// Get the response body as text for better error details
|
|
240
|
+
const errorBody = await response.text();
|
|
241
|
+
throw new CofhesdkError({
|
|
242
|
+
code: CofhesdkErrorCode.ZkVerifyFailed,
|
|
243
|
+
message: `HTTP error! ZK proof verification failed - ${errorBody}`,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const json = (await response.json()) as { status: string; data: VerifyResultRaw[]; error: string };
|
|
248
|
+
|
|
249
|
+
if (json.status !== 'success') {
|
|
250
|
+
throw new CofhesdkError({
|
|
251
|
+
code: CofhesdkErrorCode.ZkVerifyFailed,
|
|
252
|
+
message: `ZK proof verification response malformed - ${json.error}`,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return json.data.map(({ ct_hash, signature, recid }) => {
|
|
257
|
+
return {
|
|
258
|
+
ct_hash,
|
|
259
|
+
signature: concatSigRecid(signature, recid),
|
|
260
|
+
};
|
|
261
|
+
});
|
|
262
|
+
} catch (e) {
|
|
263
|
+
throw new CofhesdkError({
|
|
264
|
+
code: CofhesdkErrorCode.ZkVerifyFailed,
|
|
265
|
+
message: `ZK proof verification failed`,
|
|
266
|
+
cause: e instanceof Error ? e : undefined,
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
const concatSigRecid = (signature: string, recid: number): string => {
|
|
272
|
+
return signature + (recid + 27).toString(16).padStart(2, '0');
|
|
273
|
+
};
|
package/core/error.ts
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/* eslint-disable no-unused-vars */
|
|
2
|
+
|
|
3
|
+
export enum CofhesdkErrorCode {
|
|
4
|
+
InternalError = 'INTERNAL_ERROR',
|
|
5
|
+
UnknownEnvironment = 'UNKNOWN_ENVIRONMENT',
|
|
6
|
+
InitTfheFailed = 'INIT_TFHE_FAILED',
|
|
7
|
+
InitViemFailed = 'INIT_VIEM_FAILED',
|
|
8
|
+
InitEthersFailed = 'INIT_ETHERS_FAILED',
|
|
9
|
+
NotConnected = 'NOT_CONNECTED',
|
|
10
|
+
MissingPublicClient = 'MISSING_PUBLIC_CLIENT',
|
|
11
|
+
MissingWalletClient = 'MISSING_WALLET_CLIENT',
|
|
12
|
+
MissingProviderParam = 'MISSING_PROVIDER_PARAM',
|
|
13
|
+
EmptySecurityZonesParam = 'EMPTY_SECURITY_ZONES_PARAM',
|
|
14
|
+
InvalidPermitData = 'INVALID_PERMIT_DATA',
|
|
15
|
+
InvalidPermitDomain = 'INVALID_PERMIT_DOMAIN',
|
|
16
|
+
PermitNotFound = 'PERMIT_NOT_FOUND',
|
|
17
|
+
CannotRemoveLastPermit = 'CANNOT_REMOVE_LAST_PERMIT',
|
|
18
|
+
AccountUninitialized = 'ACCOUNT_UNINITIALIZED',
|
|
19
|
+
ChainIdUninitialized = 'CHAIN_ID_UNINITIALIZED',
|
|
20
|
+
SealOutputFailed = 'SEAL_OUTPUT_FAILED',
|
|
21
|
+
SealOutputReturnedNull = 'SEAL_OUTPUT_RETURNED_NULL',
|
|
22
|
+
InvalidUtype = 'INVALID_UTYPE',
|
|
23
|
+
DecryptFailed = 'DECRYPT_FAILED',
|
|
24
|
+
DecryptReturnedNull = 'DECRYPT_RETURNED_NULL',
|
|
25
|
+
ZkMocksInsertCtHashesFailed = 'ZK_MOCKS_INSERT_CT_HASHES_FAILED',
|
|
26
|
+
ZkMocksCalcCtHashesFailed = 'ZK_MOCKS_CALC_CT_HASHES_FAILED',
|
|
27
|
+
ZkMocksVerifySignFailed = 'ZK_MOCKS_VERIFY_SIGN_FAILED',
|
|
28
|
+
ZkMocksCreateProofSignatureFailed = 'ZK_MOCKS_CREATE_PROOF_SIGNATURE_FAILED',
|
|
29
|
+
ZkVerifyFailed = 'ZK_VERIFY_FAILED',
|
|
30
|
+
ZkPackFailed = 'ZK_PACK_FAILED',
|
|
31
|
+
ZkProveFailed = 'ZK_PROVE_FAILED',
|
|
32
|
+
EncryptRemainingInItems = 'ENCRYPT_REMAINING_IN_ITEMS',
|
|
33
|
+
ZkUninitialized = 'ZK_UNINITIALIZED',
|
|
34
|
+
ZkVerifierUrlUninitialized = 'ZK_VERIFIER_URL_UNINITIALIZED',
|
|
35
|
+
ThresholdNetworkUrlUninitialized = 'THRESHOLD_NETWORK_URL_UNINITIALIZED',
|
|
36
|
+
MissingConfig = 'MISSING_CONFIG',
|
|
37
|
+
UnsupportedChain = 'UNSUPPORTED_CHAIN',
|
|
38
|
+
MissingZkBuilderAndCrsGenerator = 'MISSING_ZK_BUILDER_AND_CRS_GENERATOR',
|
|
39
|
+
MissingTfhePublicKeyDeserializer = 'MISSING_TFHE_PUBLIC_KEY_DESERIALIZER',
|
|
40
|
+
MissingCompactPkeCrsDeserializer = 'MISSING_COMPACT_PKE_CRS_DESERIALIZER',
|
|
41
|
+
MissingFheKey = 'MISSING_FHE_KEY',
|
|
42
|
+
MissingCrs = 'MISSING_CRS',
|
|
43
|
+
FetchKeysFailed = 'FETCH_KEYS_FAILED',
|
|
44
|
+
PublicWalletGetChainIdFailed = 'PUBLIC_WALLET_GET_CHAIN_ID_FAILED',
|
|
45
|
+
PublicWalletGetAddressesFailed = 'PUBLIC_WALLET_GET_ADDRESSES_FAILED',
|
|
46
|
+
RehydrateKeysStoreFailed = 'REHYDRATE_KEYS_STORE_FAILED',
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type CofhesdkErrorParams = {
|
|
50
|
+
code: CofhesdkErrorCode;
|
|
51
|
+
message: string;
|
|
52
|
+
cause?: Error;
|
|
53
|
+
hint?: string;
|
|
54
|
+
context?: Record<string, unknown>;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* CofhesdkError class
|
|
59
|
+
* This class is used to create errors that are specific to the CoFHE SDK
|
|
60
|
+
* It extends the Error class and adds a code, cause, hint, and context
|
|
61
|
+
* The code is used to identify the type of error
|
|
62
|
+
* The cause is used to indicate the inner error that caused the CofhesdkError
|
|
63
|
+
* The hint is used to provide a hint about how to fix the error
|
|
64
|
+
* The context is used to provide additional context about the state that caused the error
|
|
65
|
+
* The serialize method is used to serialize the error to a JSON string
|
|
66
|
+
* The toString method is used to provide a human-readable string representation of the error
|
|
67
|
+
*/
|
|
68
|
+
export class CofhesdkError extends Error {
|
|
69
|
+
public readonly code: CofhesdkErrorCode;
|
|
70
|
+
public readonly cause?: Error;
|
|
71
|
+
public readonly hint?: string;
|
|
72
|
+
public readonly context?: Record<string, unknown>;
|
|
73
|
+
|
|
74
|
+
constructor({ code, message, cause, hint, context }: CofhesdkErrorParams) {
|
|
75
|
+
// If there's a cause, append its message to provide full context
|
|
76
|
+
const fullMessage = cause ? `${message} | Caused by: ${cause.message}` : message;
|
|
77
|
+
|
|
78
|
+
super(fullMessage);
|
|
79
|
+
this.name = 'CofhesdkError';
|
|
80
|
+
this.code = code;
|
|
81
|
+
this.cause = cause;
|
|
82
|
+
this.hint = hint;
|
|
83
|
+
this.context = context;
|
|
84
|
+
|
|
85
|
+
// Maintains proper stack trace for where our error was thrown (only available on V8)
|
|
86
|
+
if (Error.captureStackTrace) {
|
|
87
|
+
Error.captureStackTrace(this, CofhesdkError);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Creates a CofhesdkError from an unknown error
|
|
93
|
+
* If the error is a CofhesdkError, it is returned unchanged, else a new CofhesdkError is created
|
|
94
|
+
* If a wrapperError is provided, it is used to create the new CofhesdkError, else a default is used
|
|
95
|
+
*/
|
|
96
|
+
static fromError(error: unknown, wrapperError?: CofhesdkErrorParams): CofhesdkError {
|
|
97
|
+
if (isCofhesdkError(error)) return error;
|
|
98
|
+
|
|
99
|
+
const cause = error instanceof Error ? error : new Error(`${error}`);
|
|
100
|
+
|
|
101
|
+
return new CofhesdkError({
|
|
102
|
+
code: wrapperError?.code ?? CofhesdkErrorCode.InternalError,
|
|
103
|
+
message: wrapperError?.message ?? 'An internal error occurred',
|
|
104
|
+
hint: wrapperError?.hint,
|
|
105
|
+
context: wrapperError?.context,
|
|
106
|
+
cause: cause,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Serializes the error to JSON string with proper handling of Error objects
|
|
112
|
+
*/
|
|
113
|
+
serialize(): string {
|
|
114
|
+
return bigintSafeJsonStringify({
|
|
115
|
+
name: this.name,
|
|
116
|
+
code: this.code,
|
|
117
|
+
message: this.message,
|
|
118
|
+
hint: this.hint,
|
|
119
|
+
context: this.context,
|
|
120
|
+
cause: this.cause
|
|
121
|
+
? {
|
|
122
|
+
name: this.cause.name,
|
|
123
|
+
message: this.cause.message,
|
|
124
|
+
stack: this.cause.stack,
|
|
125
|
+
}
|
|
126
|
+
: undefined,
|
|
127
|
+
stack: this.stack,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Returns a human-readable string representation of the error
|
|
133
|
+
*/
|
|
134
|
+
toString(): string {
|
|
135
|
+
const parts = [`${this.name} [${this.code}]: ${this.message}`];
|
|
136
|
+
|
|
137
|
+
if (this.hint) {
|
|
138
|
+
parts.push(`Hint: ${this.hint}`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (this.context && Object.keys(this.context).length > 0) {
|
|
142
|
+
parts.push(`Context: ${bigintSafeJsonStringify(this.context)}`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (this.stack) {
|
|
146
|
+
parts.push(`\nStack trace:`);
|
|
147
|
+
parts.push(this.stack);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (this.cause) {
|
|
151
|
+
parts.push(`\nCaused by: ${this.cause.name}: ${this.cause.message}`);
|
|
152
|
+
if (this.cause.stack) {
|
|
153
|
+
parts.push(this.cause.stack);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return parts.join('\n');
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const bigintSafeJsonStringify = (value: unknown): string => {
|
|
162
|
+
return JSON.stringify(value, (key, value) => {
|
|
163
|
+
if (typeof value === 'bigint') {
|
|
164
|
+
return `${value}n`;
|
|
165
|
+
}
|
|
166
|
+
return value;
|
|
167
|
+
});
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
export const isCofhesdkError = (error: unknown): error is CofhesdkError => error instanceof CofhesdkError;
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/* eslint-disable turbo/no-undeclared-env-vars */
|
|
2
|
+
/* eslint-disable no-undef */
|
|
3
|
+
|
|
4
|
+
import { sepolia, arbSepolia } from '@/chains';
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
7
|
+
import { fetchKeys, fetchMultichainKeys } from './fetchKeys.js';
|
|
8
|
+
import { type CofhesdkConfig, createCofhesdkConfigBase } from './config.js';
|
|
9
|
+
import { createKeysStore, type KeysStorage } from './keyStore.js';
|
|
10
|
+
|
|
11
|
+
describe('fetchKeys', () => {
|
|
12
|
+
let config: CofhesdkConfig;
|
|
13
|
+
let mockTfhePublicKeyDeserializer: any;
|
|
14
|
+
let mockCompactPkeCrsDeserializer: any;
|
|
15
|
+
let keysStorage: KeysStorage;
|
|
16
|
+
|
|
17
|
+
beforeEach(async () => {
|
|
18
|
+
// Reset all mocks
|
|
19
|
+
vi.clearAllMocks();
|
|
20
|
+
|
|
21
|
+
// Setup config with real chains
|
|
22
|
+
config = createCofhesdkConfigBase({
|
|
23
|
+
supportedChains: [sepolia, arbSepolia],
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// Setup mock serializers
|
|
27
|
+
mockTfhePublicKeyDeserializer = vi.fn();
|
|
28
|
+
mockCompactPkeCrsDeserializer = vi.fn();
|
|
29
|
+
|
|
30
|
+
// Create a fresh keysStorage instance for each test (non-persisted)
|
|
31
|
+
keysStorage = createKeysStore(null);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
afterEach(async () => {
|
|
35
|
+
vi.restoreAllMocks();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should fetch and store FHE public key and CRS for Sepolia when not cached', async () => {
|
|
39
|
+
const [[fheKey, fheKeyFetchedFromCoFHE], [crs, crsFetchedFromCoFHE]] = await fetchKeys(
|
|
40
|
+
config,
|
|
41
|
+
sepolia.id,
|
|
42
|
+
0,
|
|
43
|
+
mockTfhePublicKeyDeserializer,
|
|
44
|
+
mockCompactPkeCrsDeserializer,
|
|
45
|
+
keysStorage
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
expect(fheKeyFetchedFromCoFHE).toBe(true);
|
|
49
|
+
expect(crsFetchedFromCoFHE).toBe(true);
|
|
50
|
+
|
|
51
|
+
// Verify keys were stored
|
|
52
|
+
const storedFheKey = keysStorage.getFheKey(sepolia.id, 0);
|
|
53
|
+
const storedCrs = keysStorage.getCrs(sepolia.id);
|
|
54
|
+
|
|
55
|
+
expect(storedFheKey).toBeDefined();
|
|
56
|
+
expect(storedCrs).toBeDefined();
|
|
57
|
+
expect(mockTfhePublicKeyDeserializer).toHaveBeenCalled();
|
|
58
|
+
expect(mockCompactPkeCrsDeserializer).toHaveBeenCalled();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should fetch and store FHE public key and CRS for Arbitrum Sepolia when not cached', async () => {
|
|
62
|
+
const [[fheKey, fheKeyFetchedFromCoFHE], [crs, crsFetchedFromCoFHE]] = await fetchKeys(
|
|
63
|
+
config,
|
|
64
|
+
arbSepolia.id,
|
|
65
|
+
0,
|
|
66
|
+
mockTfhePublicKeyDeserializer,
|
|
67
|
+
mockCompactPkeCrsDeserializer,
|
|
68
|
+
keysStorage
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
expect(fheKeyFetchedFromCoFHE).toBe(true);
|
|
72
|
+
expect(crsFetchedFromCoFHE).toBe(true);
|
|
73
|
+
|
|
74
|
+
// Verify keys were stored
|
|
75
|
+
const storedFheKey = keysStorage.getFheKey(arbSepolia.id, 0);
|
|
76
|
+
const storedCrs = keysStorage.getCrs(arbSepolia.id);
|
|
77
|
+
|
|
78
|
+
expect(storedFheKey).toBeDefined();
|
|
79
|
+
expect(storedCrs).toBeDefined();
|
|
80
|
+
expect(mockTfhePublicKeyDeserializer).toHaveBeenCalled();
|
|
81
|
+
expect(mockCompactPkeCrsDeserializer).toHaveBeenCalled();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should not fetch FHE key if already cached', async () => {
|
|
85
|
+
// Pre-populate with a cached key
|
|
86
|
+
const mockCachedKey = '0x1234567890';
|
|
87
|
+
keysStorage.setFheKey(sepolia.id, 0, mockCachedKey);
|
|
88
|
+
|
|
89
|
+
const [[fheKey, fheKeyFetchedFromCoFHE], [crs, crsFetchedFromCoFHE]] = await fetchKeys(
|
|
90
|
+
config,
|
|
91
|
+
sepolia.id,
|
|
92
|
+
0,
|
|
93
|
+
mockTfhePublicKeyDeserializer,
|
|
94
|
+
mockCompactPkeCrsDeserializer,
|
|
95
|
+
keysStorage
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
expect(fheKeyFetchedFromCoFHE).toBe(false);
|
|
99
|
+
expect(crsFetchedFromCoFHE).toBe(true);
|
|
100
|
+
|
|
101
|
+
// Verify the cached key wasn't overwritten
|
|
102
|
+
const retrievedKey = keysStorage.getFheKey(sepolia.id, 0);
|
|
103
|
+
expect(retrievedKey).toEqual(mockCachedKey);
|
|
104
|
+
|
|
105
|
+
// Verify CRS was still fetched
|
|
106
|
+
const retrievedCrs = keysStorage.getCrs(sepolia.id);
|
|
107
|
+
expect(retrievedCrs).toBeDefined();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should not fetch CRS if already cached', async () => {
|
|
111
|
+
// Pre-populate with a cached CRS
|
|
112
|
+
const mockCachedCrs = '0x2345678901';
|
|
113
|
+
keysStorage.setCrs(sepolia.id, mockCachedCrs);
|
|
114
|
+
|
|
115
|
+
const [[fheKey, fheKeyFetchedFromCoFHE], [crs, crsFetchedFromCoFHE]] = await fetchKeys(
|
|
116
|
+
config,
|
|
117
|
+
sepolia.id,
|
|
118
|
+
0,
|
|
119
|
+
mockTfhePublicKeyDeserializer,
|
|
120
|
+
mockCompactPkeCrsDeserializer,
|
|
121
|
+
keysStorage
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
expect(fheKeyFetchedFromCoFHE).toBe(true);
|
|
125
|
+
expect(crsFetchedFromCoFHE).toBe(false);
|
|
126
|
+
|
|
127
|
+
// Verify the cached CRS wasn't overwritten
|
|
128
|
+
const retrievedCrs = keysStorage.getCrs(sepolia.id);
|
|
129
|
+
expect(retrievedCrs).toEqual(mockCachedCrs);
|
|
130
|
+
|
|
131
|
+
// Verify FHE key was still fetched
|
|
132
|
+
const retrievedKey = keysStorage.getFheKey(sepolia.id, 0);
|
|
133
|
+
expect(retrievedKey).toBeDefined();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('should not make any network calls if both keys are cached', async () => {
|
|
137
|
+
// Pre-populate both keys
|
|
138
|
+
const mockCachedKey = '0x1234567890';
|
|
139
|
+
const mockCachedCrs = '0x2345678901';
|
|
140
|
+
keysStorage.setFheKey(sepolia.id, 0, mockCachedKey);
|
|
141
|
+
keysStorage.setCrs(sepolia.id, mockCachedCrs);
|
|
142
|
+
|
|
143
|
+
const [[fheKey, fheKeyFetchedFromCoFHE], [crs, crsFetchedFromCoFHE]] = await fetchKeys(
|
|
144
|
+
config,
|
|
145
|
+
sepolia.id,
|
|
146
|
+
0,
|
|
147
|
+
mockTfhePublicKeyDeserializer,
|
|
148
|
+
mockCompactPkeCrsDeserializer,
|
|
149
|
+
keysStorage
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
expect(fheKeyFetchedFromCoFHE).toBe(false);
|
|
153
|
+
expect(crsFetchedFromCoFHE).toBe(false);
|
|
154
|
+
|
|
155
|
+
// Verify both keys remain unchanged
|
|
156
|
+
const retrievedKey = keysStorage.getFheKey(sepolia.id, 0);
|
|
157
|
+
const retrievedCrs = keysStorage.getCrs(sepolia.id);
|
|
158
|
+
|
|
159
|
+
expect(retrievedKey).toEqual(mockCachedKey);
|
|
160
|
+
expect(retrievedCrs).toEqual(mockCachedCrs);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('should throw error for unsupported chain ID', async () => {
|
|
164
|
+
await expect(
|
|
165
|
+
fetchKeys(
|
|
166
|
+
config,
|
|
167
|
+
999, // Non-existent chain
|
|
168
|
+
0,
|
|
169
|
+
mockTfhePublicKeyDeserializer,
|
|
170
|
+
mockCompactPkeCrsDeserializer,
|
|
171
|
+
keysStorage
|
|
172
|
+
)
|
|
173
|
+
).rejects.toThrow('Config does not support chain <999>');
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('should throw error when FHE public key serialization fails', async () => {
|
|
177
|
+
mockTfhePublicKeyDeserializer.mockImplementation(() => {
|
|
178
|
+
throw new Error('Serialization failed');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
await expect(
|
|
182
|
+
fetchKeys(config, sepolia.id, 0, mockTfhePublicKeyDeserializer, mockCompactPkeCrsDeserializer, keysStorage)
|
|
183
|
+
).rejects.toThrow('Error serializing FHE publicKey; Error: Serialization failed');
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('should throw error when CRS serialization fails', async () => {
|
|
187
|
+
mockCompactPkeCrsDeserializer.mockImplementation(() => {
|
|
188
|
+
throw new Error('Serialization failed');
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
await expect(
|
|
192
|
+
fetchKeys(config, sepolia.id, 0, mockTfhePublicKeyDeserializer, mockCompactPkeCrsDeserializer, keysStorage)
|
|
193
|
+
).rejects.toThrow('Error serializing CRS; Error: Serialization failed');
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('should fetch and store FHE public key and CRS for all chains in the config', async () => {
|
|
197
|
+
await fetchMultichainKeys(config, 0, mockTfhePublicKeyDeserializer, mockCompactPkeCrsDeserializer, keysStorage);
|
|
198
|
+
|
|
199
|
+
// Verify keys were stored
|
|
200
|
+
const storedFheKey = keysStorage.getFheKey(sepolia.id, 0);
|
|
201
|
+
const storedCrs = keysStorage.getCrs(sepolia.id);
|
|
202
|
+
const storedFheKeyArb = keysStorage.getFheKey(arbSepolia.id, 0);
|
|
203
|
+
const storedCrsArb = keysStorage.getCrs(arbSepolia.id);
|
|
204
|
+
|
|
205
|
+
expect(storedFheKey).toBeDefined();
|
|
206
|
+
expect(storedCrs).toBeDefined();
|
|
207
|
+
expect(storedFheKeyArb).toBeDefined();
|
|
208
|
+
expect(storedCrsArb).toBeDefined();
|
|
209
|
+
expect(mockTfhePublicKeyDeserializer).toHaveBeenCalled();
|
|
210
|
+
expect(mockCompactPkeCrsDeserializer).toHaveBeenCalled();
|
|
211
|
+
});
|
|
212
|
+
});
|