@obolnetwork/obol-sdk 2.1.2 → 2.2.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/src/types.ts CHANGED
@@ -13,8 +13,31 @@ export enum FORK_MAPPING {
13
13
 
14
14
  /** Holesky. */
15
15
  '0x01017000' = 17000,
16
+
17
+ /** Sepolia. */
18
+ '0x90000069' = 11155111,
16
19
  }
17
20
 
21
+ /**
22
+ * Permitted Chain Names
23
+ */
24
+ export const FORK_NAMES: Record<number, string> = {
25
+ /** Mainnet. */
26
+ [FORK_MAPPING['0x00000000']]: 'mainnet',
27
+
28
+ /** Goerli/Prater. */
29
+ [FORK_MAPPING['0x00001020']]: 'goerli',
30
+
31
+ /** Gnosis Chain. */
32
+ [FORK_MAPPING['0x00000064']]: 'gnosis',
33
+
34
+ /** Holesky. */
35
+ [FORK_MAPPING['0x01017000']]: 'holesky',
36
+
37
+ /** Sepolia. */
38
+ [FORK_MAPPING['0x90000069']]: 'sepolia',
39
+ };
40
+
18
41
  /**
19
42
  * Node operator data
20
43
  */
package/src/utils.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { type Provider } from 'ethers';
2
- import { DefinitionFlow } from './constants';
3
- import { type ClusterDefinition } from './types';
1
+ import { ethers, type Provider } from 'ethers';
2
+ import { DefinitionFlow, PROVIDER_MAP } from './constants';
3
+ import { FORK_NAMES, type ClusterDefinition } from './types';
4
4
 
5
5
  export const hexWithout0x = (hex: string): string => {
6
6
  return hex.slice(2, hex.length);
@@ -75,3 +75,11 @@ export const isContractAvailable = async (
75
75
  }
76
76
  return !!code && code !== '0x' && code !== '0x0';
77
77
  };
78
+
79
+ export const getProvider = (chainId: number): ethers.Provider => {
80
+ const rpcUrl = PROVIDER_MAP[chainId];
81
+ if (!rpcUrl) {
82
+ throw new Error(`No provider configured for ${FORK_NAMES[chainId]}`);
83
+ }
84
+ return new ethers.JsonRpcProvider(rpcUrl);
85
+ };
@@ -23,7 +23,6 @@ import {
23
23
  hashClusterLockV1X7,
24
24
  verifyDVV1X7,
25
25
  } from './v1.7.0.js';
26
- import { ethers } from 'ethers';
27
26
  import {
28
27
  DOMAIN_APPLICATION_BUILDER,
29
28
  DOMAIN_DEPOSIT,
@@ -33,7 +32,6 @@ import {
33
32
  signEnrPayload,
34
33
  signOperatorConfigHashPayload,
35
34
  } from '../constants.js';
36
- import { SignTypedDataVersion, TypedDataUtils } from '@metamask/eth-sig-util';
37
35
  import {
38
36
  builderRegistrationMessageType,
39
37
  depositMessageType,
@@ -48,6 +46,7 @@ import {
48
46
  hashClusterLockV1X8,
49
47
  verifyDVV1X8,
50
48
  } from './v1.8.0.js';
49
+ import { validateAddressSignature } from './signature-validator.js';
51
50
 
52
51
  // cluster-definition hash
53
52
 
@@ -132,82 +131,95 @@ export const clusterLockHash = (clusterLock: ClusterLock): string => {
132
131
 
133
132
  // cluster-definition signatures verification
134
133
 
135
- const getPOSTConfigHashSigner = (
134
+ const validatePOSTConfigHashSigner = async (
135
+ address: string,
136
136
  signature: string,
137
137
  configHash: string,
138
138
  chainId: FORK_MAPPING,
139
- ): string => {
139
+ ): Promise<boolean> => {
140
140
  try {
141
- const sig = ethers.Signature.from(signature);
142
141
  const data = signCreatorConfigHashPayload(
143
142
  { creator_config_hash: configHash },
144
143
  chainId,
145
144
  );
146
- const digest = TypedDataUtils.eip712Hash(data, SignTypedDataVersion.V4);
147
145
 
148
- return ethers.recoverAddress(digest, sig).toLowerCase();
146
+ return await validateAddressSignature({
147
+ address,
148
+ token: signature,
149
+ data,
150
+ chainId,
151
+ });
149
152
  } catch (err) {
150
153
  throw err;
151
154
  }
152
155
  };
153
156
 
154
- const getPUTConfigHashSigner = (
157
+ const validatePUTConfigHashSigner = async (
158
+ address: string,
155
159
  signature: string,
156
160
  configHash: string,
157
161
  chainId: number,
158
- ): string => {
162
+ ): Promise<boolean> => {
159
163
  try {
160
- const sig = ethers.Signature.from(signature);
161
164
  const data = signOperatorConfigHashPayload(
162
165
  { operator_config_hash: configHash },
163
166
  chainId,
164
167
  );
165
- const digest = TypedDataUtils.eip712Hash(data, SignTypedDataVersion.V4);
166
-
167
- return ethers.recoverAddress(digest, sig).toLowerCase();
168
+ return await validateAddressSignature({
169
+ address,
170
+ token: signature,
171
+ data,
172
+ chainId,
173
+ });
168
174
  } catch (err) {
169
175
  throw err;
170
176
  }
171
177
  };
172
178
 
173
- const getEnrSigner = (
179
+ const validateEnrSigner = async (
180
+ address: string,
174
181
  signature: string,
175
182
  payload: string,
176
183
  chainId: number,
177
- ): string => {
184
+ ): Promise<boolean> => {
178
185
  try {
179
- const sig = ethers.Signature.from(signature);
180
-
181
186
  const data = signEnrPayload({ enr: payload }, chainId);
182
- const digest = TypedDataUtils.eip712Hash(data, SignTypedDataVersion.V4);
183
187
 
184
- return ethers.recoverAddress(digest, sig).toLowerCase();
188
+ return await validateAddressSignature({
189
+ address,
190
+ token: signature,
191
+ data,
192
+ chainId,
193
+ });
185
194
  } catch (err) {
186
195
  throw err;
187
196
  }
188
197
  };
189
198
 
190
- const verifyDefinitionSignatures = (
199
+ const verifyDefinitionSignatures = async (
191
200
  clusterDefinition: ClusterDefinition,
192
201
  definitionType: DefinitionFlow,
193
- ): boolean => {
202
+ ): Promise<boolean> => {
194
203
  if (definitionType === DefinitionFlow.Charon) {
195
204
  return true;
196
205
  } else {
197
- const configSigner = getPOSTConfigHashSigner(
206
+ const isPOSTConfigHashSignerValid = await validatePOSTConfigHashSigner(
207
+ clusterDefinition.creator.address,
198
208
  clusterDefinition.creator.config_signature as string,
199
209
  clusterDefinition.config_hash,
200
210
  FORK_MAPPING[clusterDefinition.fork_version as keyof typeof FORK_MAPPING],
201
211
  );
202
212
 
203
- if (configSigner !== clusterDefinition.creator.address.toLowerCase()) {
213
+ if (!isPOSTConfigHashSignerValid) {
204
214
  return false;
205
215
  }
206
216
  if (definitionType === DefinitionFlow.Solo) {
207
217
  return true;
208
218
  }
209
- return clusterDefinition.operators.every(operator => {
210
- const configSigner = getPUTConfigHashSigner(
219
+
220
+ for (const operator of clusterDefinition.operators) {
221
+ const isPUTConfigHashSignerValid = await validatePUTConfigHashSigner(
222
+ operator.address,
211
223
  operator.config_signature as string,
212
224
  clusterDefinition.config_hash,
213
225
  FORK_MAPPING[
@@ -215,7 +227,8 @@ const verifyDefinitionSignatures = (
215
227
  ],
216
228
  );
217
229
 
218
- const enrSigner = getEnrSigner(
230
+ const isENRSignerValid = await validateEnrSigner(
231
+ operator.address,
219
232
  operator.enr_signature as string,
220
233
  operator.enr as string,
221
234
  FORK_MAPPING[
@@ -223,14 +236,12 @@ const verifyDefinitionSignatures = (
223
236
  ],
224
237
  );
225
238
 
226
- if (
227
- configSigner !== operator.address.toLowerCase() ||
228
- enrSigner !== operator.address.toLowerCase()
229
- ) {
239
+ if (!isPUTConfigHashSignerValid || !isENRSignerValid) {
230
240
  return false;
231
241
  }
232
- return true;
233
- });
242
+ }
243
+
244
+ return true;
234
245
  }
235
246
  };
236
247
 
@@ -444,7 +455,7 @@ export const isValidClusterLock = async (
444
455
  if (definitionType == null) {
445
456
  return false;
446
457
  }
447
- const isValidDefinitionData = verifyDefinitionSignatures(
458
+ const isValidDefinitionData = await verifyDefinitionSignatures(
448
459
  clusterLock.cluster_definition,
449
460
  definitionType,
450
461
  );
@@ -0,0 +1,96 @@
1
+ /* eslint-disable @typescript-eslint/restrict-template-expressions */
2
+ import { ethers } from 'ethers';
3
+ import {
4
+ SignTypedDataVersion,
5
+ TypedDataUtils,
6
+ type TypedMessage,
7
+ } from '@metamask/eth-sig-util';
8
+ import Safe from '@safe-global/protocol-kit';
9
+ import { PROVIDER_MAP } from '../constants';
10
+ import { hashTypedData } from '@safe-global/protocol-kit/dist/src/utils';
11
+ import { type EIP712TypedData } from '@safe-global/safe-core-sdk-types';
12
+ import { isContractAvailable, getProvider } from '../utils';
13
+
14
+ export const validateAddressSignature = async ({
15
+ address,
16
+ token,
17
+ data,
18
+ chainId,
19
+ }: {
20
+ address: string;
21
+ token: string;
22
+ data: TypedMessage<any>;
23
+ chainId: number;
24
+ }): Promise<boolean> => {
25
+ try {
26
+ const provider = getProvider(chainId);
27
+ if (provider) {
28
+ const contractAddress = await isContractAvailable(address, provider);
29
+ if (contractAddress) {
30
+ return await validateSmartContractSignature({
31
+ token,
32
+ data: data as unknown as EIP712TypedData,
33
+ address,
34
+ chainId,
35
+ });
36
+ }
37
+ }
38
+ return validateEOASignature({ token, data, address });
39
+ } catch (error) {
40
+ return validateEOASignature({ token, data, address });
41
+ }
42
+ };
43
+
44
+ export const validateEOASignature = ({
45
+ token,
46
+ data,
47
+ address,
48
+ }: {
49
+ token: string;
50
+ data: TypedMessage<any>;
51
+ address: string;
52
+ }): boolean => {
53
+ try {
54
+ const sig = ethers.Signature.from(token);
55
+ const digest = TypedDataUtils.eip712Hash(data, SignTypedDataVersion.V4);
56
+
57
+ return (
58
+ ethers.recoverAddress(digest, sig).toLowerCase() ===
59
+ address.toLocaleLowerCase()
60
+ );
61
+ } catch (err) {
62
+ console.error(`validate EOA Signature error: ${err}`);
63
+ throw err;
64
+ }
65
+ };
66
+
67
+ export const validateSmartContractSignature = async ({
68
+ token,
69
+ data,
70
+ address,
71
+ chainId,
72
+ }: {
73
+ token: string;
74
+ data: EIP712TypedData;
75
+ address: string;
76
+ chainId: number;
77
+ }): Promise<boolean> => {
78
+ try {
79
+ const provider = PROVIDER_MAP[chainId];
80
+ const protocolKit = await Safe.init({
81
+ provider,
82
+ safeAddress: address,
83
+ });
84
+ const messageHash = hashTypedData(data);
85
+ const isValidSignature = await protocolKit.isValidSignature(
86
+ messageHash,
87
+ token,
88
+ );
89
+
90
+ return isValidSignature;
91
+ } catch (err: any) {
92
+ throw new Error(
93
+ `Error validating smart contract signature: ${err.message}`,
94
+ );
95
+ }
96
+ };