@obolnetwork/obol-sdk 2.4.6 → 2.5.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/ajv.ts CHANGED
@@ -1,78 +1,117 @@
1
- import Ajv, { type ErrorObject } from 'ajv';
1
+ import addFormats from 'ajv-formats';
2
+ import addKeywords from 'ajv-keywords';
2
3
  import { parseUnits } from 'ethers';
3
4
  import {
4
5
  type RewardsSplitPayload,
5
6
  type SplitRecipient,
6
7
  type TotalSplitPayload,
7
8
  } from './types';
9
+ import Ajv from 'ajv';
8
10
  import {
9
11
  DEFAULT_RETROACTIVE_FUNDING_REWARDS_ONLY_SPLIT,
10
12
  DEFAULT_RETROACTIVE_FUNDING_TOTAL_SPLIT,
11
13
  } from './constants';
12
14
 
13
- const validDepositAmounts = (data: boolean, deposits: string[]): boolean => {
14
- let sum = 0;
15
- // from ether togwei is same as from gwei to wei
16
- const maxDeposit = Number(parseUnits('32', 'gwei'));
17
- const minDeposit = Number(parseUnits('1', 'gwei'));
15
+ export const VALID_DEPOSIT_AMOUNTS = [
16
+ parseUnits('1', 'gwei').toString(),
17
+ parseUnits('32', 'gwei').toString(),
18
+ parseUnits('8', 'gwei').toString(),
19
+ parseUnits('256', 'gwei').toString(),
20
+ ];
18
21
 
19
- for (const element of deposits) {
20
- const amountInGWei = Number(element);
22
+ export const VALID_NON_COMPOUNDING_AMOUNTS = [
23
+ parseUnits('1', 'gwei').toString(),
24
+ parseUnits('32', 'gwei').toString(),
25
+ ];
21
26
 
22
- if (
23
- !Number.isInteger(amountInGWei) ||
24
- amountInGWei > maxDeposit ||
25
- amountInGWei < minDeposit
26
- ) {
27
- return false;
28
- }
29
- sum += amountInGWei;
30
- }
31
- if (sum / minDeposit !== 32) {
32
- return false;
33
- } else {
34
- return true;
35
- }
27
+ // They dont see defaults set in schema
28
+ const validateRewardsSplitRecipients = (
29
+ _: boolean,
30
+ data: RewardsSplitPayload,
31
+ ): boolean => {
32
+ const obolRAFSplit =
33
+ data.ObolRAFSplit ?? DEFAULT_RETROACTIVE_FUNDING_REWARDS_ONLY_SPLIT;
34
+ const splitPercentage = data.splitRecipients.reduce(
35
+ (acc: number, curr: SplitRecipient) => acc + curr.percentAllocation,
36
+ 0,
37
+ );
38
+ return splitPercentage + obolRAFSplit === 100;
36
39
  };
37
40
 
38
- const validateSplitRecipients = (
41
+ const validateTotalSplitRecipients = (
39
42
  _: boolean,
40
- data: RewardsSplitPayload | TotalSplitPayload,
43
+ data: TotalSplitPayload,
41
44
  ): boolean => {
45
+ const obolRAFSplit =
46
+ data.ObolRAFSplit ?? DEFAULT_RETROACTIVE_FUNDING_TOTAL_SPLIT;
42
47
  const splitPercentage = data.splitRecipients.reduce(
43
48
  (acc: number, curr: SplitRecipient) => acc + curr.percentAllocation,
44
49
  0,
45
50
  );
46
- const ObolRAFSplitParam = data.ObolRAFSplit
47
- ? data.ObolRAFSplit
48
- : 'principalRecipient' in data
49
- ? DEFAULT_RETROACTIVE_FUNDING_REWARDS_ONLY_SPLIT
50
- : DEFAULT_RETROACTIVE_FUNDING_TOTAL_SPLIT;
51
- return splitPercentage + ObolRAFSplitParam === 100;
51
+ return splitPercentage + obolRAFSplit === 100;
52
52
  };
53
53
 
54
- export function validatePayload(
55
- data: any,
56
- schema: any,
57
- ): ErrorObject[] | undefined | null | boolean {
58
- const ajv = new Ajv();
59
- ajv.addKeyword({
60
- keyword: 'validDepositAmounts',
61
- validate: validDepositAmounts,
62
- errors: true,
63
- });
54
+ const validateUniqueAddresses = (
55
+ _: boolean,
56
+ operators: Array<{ address: string }>,
57
+ ): boolean => {
58
+ if (!operators) {
59
+ return false;
60
+ }
61
+
62
+ if (operators.length < 4) {
63
+ return false;
64
+ }
65
+
66
+ if (operators.every(op => op.address === '')) {
67
+ return true;
68
+ }
69
+
70
+ if (operators.some(op => op.address.length !== 42)) {
71
+ return false;
72
+ }
73
+
74
+ const addresses = operators.map(op => op.address);
75
+ const uniqueAddresses = new Set(addresses);
76
+ const isUnique = uniqueAddresses.size === addresses.length;
77
+ return isUnique;
78
+ };
79
+
80
+ const ajv = new Ajv({
81
+ allErrors: true,
82
+ useDefaults: true,
83
+ strict: false,
84
+ $data: true,
85
+ });
86
+ addFormats(ajv);
87
+ addKeywords(ajv, ['patternRequired']);
88
+
89
+ ajv.addKeyword({
90
+ keyword: 'validateRewardsSplitRecipients',
91
+ validate: validateRewardsSplitRecipients,
92
+ schemaType: 'boolean',
93
+ });
94
+
95
+ ajv.addKeyword({
96
+ keyword: 'validateTotalSplitRecipients',
97
+ validate: validateTotalSplitRecipients,
98
+ schemaType: 'boolean',
99
+ });
100
+
101
+ ajv.addKeyword({
102
+ keyword: 'validateUniqueAddresses',
103
+ validate: validateUniqueAddresses,
104
+ schemaType: 'boolean',
105
+ });
64
106
 
65
- ajv.addKeyword({
66
- keyword: 'validateSplitRecipients',
67
- validate: validateSplitRecipients,
68
- errors: true,
69
- });
70
- const validate = ajv.compile(schema);
71
- const isValid = validate(data);
72
- if (!isValid) {
73
- throw new Error(
74
- `Schema compilation errors', ${validate.errors?.[0].message}`,
75
- );
107
+ export function validatePayload<T>(data: unknown, schema: object): T {
108
+ const validate = ajv.compile<T>(schema);
109
+ const valid = validate(data);
110
+ if (!valid) {
111
+ const errors = validate.errors
112
+ ?.map(e => `${e.instancePath} ${e.message}`)
113
+ .join(', ');
114
+ throw new Error(`Validation failed: ${errors}`);
76
115
  }
77
- return isValid;
116
+ return data;
78
117
  }
package/src/constants.ts CHANGED
@@ -119,7 +119,7 @@ export const signEnrPayload = (
119
119
 
120
120
  export const DKG_ALGORITHM = 'default';
121
121
 
122
- export const CONFIG_VERSION = 'v1.8.0';
122
+ export const CONFIG_VERSION = 'v1.10.0';
123
123
 
124
124
  export const SDK_VERSION = pjson.version;
125
125
 
package/src/index.ts CHANGED
@@ -1,4 +1,3 @@
1
- import { ZeroAddress } from 'ethers';
2
1
  import { v4 as uuidv4 } from 'uuid';
3
2
  import { Base } from './base.js';
4
3
  import {
@@ -16,7 +15,6 @@ import {
16
15
  AVAILABLE_SPLITTER_CHAINS,
17
16
  CHAIN_CONFIGURATION,
18
17
  DEFAULT_RETROACTIVE_FUNDING_REWARDS_ONLY_SPLIT,
19
- DEFAULT_RETROACTIVE_FUNDING_TOTAL_SPLIT,
20
18
  OBOL_SDK_EMAIL,
21
19
  } from './constants.js';
22
20
  import { ConflictError } from './errors.js';
@@ -54,6 +52,7 @@ export * from './types.js';
54
52
  export * from './services.js';
55
53
  export * from './verification/signature-validator.js';
56
54
  export * from './verification/common.js';
55
+ export * from './constants.js';
57
56
  export { Incentives } from './incentives.js';
58
57
 
59
58
  /**
@@ -174,16 +173,16 @@ export class Client extends Base {
174
173
  principalRecipient,
175
174
  etherAmount,
176
175
  ObolRAFSplit = DEFAULT_RETROACTIVE_FUNDING_REWARDS_ONLY_SPLIT,
177
- distributorFee = 0,
178
- controllerAddress = ZeroAddress,
179
- recoveryAddress = ZeroAddress,
176
+ distributorFee,
177
+ controllerAddress,
178
+ recoveryAddress,
180
179
  }: RewardsSplitPayload): Promise<ClusterValidator> {
181
180
  // This method doesnt require T&C signature
182
181
  if (!this.signer) {
183
182
  throw new Error('Signer is required in createObolRewardsSplit');
184
183
  }
185
184
 
186
- validatePayload(
185
+ const validatedPayload = validatePayload<Required<RewardsSplitPayload>>(
187
186
  {
188
187
  splitRecipients,
189
188
  principalRecipient,
@@ -234,10 +233,10 @@ export class Client extends Base {
234
233
  const retroActiveFundingRecipient = {
235
234
  account:
236
235
  CHAIN_CONFIGURATION[this.chainId].RETROACTIVE_FUNDING_ADDRESS.address,
237
- percentAllocation: ObolRAFSplit,
236
+ percentAllocation: validatedPayload.ObolRAFSplit,
238
237
  };
239
238
 
240
- const copiedSplitRecipients = [...splitRecipients];
239
+ const copiedSplitRecipients = [...validatedPayload.splitRecipients];
241
240
  copiedSplitRecipients.push(retroActiveFundingRecipient);
242
241
 
243
242
  const { accounts, percentAllocations } = formatSplitRecipients(
@@ -249,8 +248,8 @@ export class Client extends Base {
249
248
  accounts,
250
249
  percentAllocations,
251
250
  chainId: this.chainId,
252
- distributorFee,
253
- controllerAddress,
251
+ distributorFee: validatedPayload.distributorFee,
252
+ controllerAddress: validatedPayload.controllerAddress,
254
253
  });
255
254
 
256
255
  const isSplitterDeployed = await isContractAvailable(
@@ -265,12 +264,12 @@ export class Client extends Base {
265
264
  predictedSplitterAddress,
266
265
  accounts,
267
266
  percentAllocations,
268
- principalRecipient,
269
- etherAmount,
267
+ principalRecipient: validatedPayload.principalRecipient,
268
+ etherAmount: validatedPayload.etherAmount,
270
269
  chainId: this.chainId,
271
- distributorFee,
272
- controllerAddress,
273
- recoveryAddress,
270
+ distributorFee: validatedPayload.distributorFee,
271
+ controllerAddress: validatedPayload.controllerAddress,
272
+ recoveryAddress: validatedPayload.recoveryAddress,
274
273
  });
275
274
 
276
275
  return { withdrawal_address, fee_recipient_address };
@@ -292,16 +291,16 @@ export class Client extends Base {
292
291
  // add the example reference
293
292
  async createObolTotalSplit({
294
293
  splitRecipients,
295
- ObolRAFSplit = DEFAULT_RETROACTIVE_FUNDING_TOTAL_SPLIT,
296
- distributorFee = 0,
297
- controllerAddress = ZeroAddress,
294
+ ObolRAFSplit,
295
+ distributorFee,
296
+ controllerAddress,
298
297
  }: TotalSplitPayload): Promise<ClusterValidator> {
299
298
  // This method doesnt require T&C signature
300
299
  if (!this.signer) {
301
300
  throw new Error('Signer is required in createObolTotalSplit');
302
301
  }
303
302
 
304
- validatePayload(
303
+ const validatedPayload = validatePayload<Required<TotalSplitPayload>>(
305
304
  {
306
305
  splitRecipients,
307
306
  ObolRAFSplit,
@@ -333,10 +332,10 @@ export class Client extends Base {
333
332
  const retroActiveFundingRecipient = {
334
333
  account:
335
334
  CHAIN_CONFIGURATION[this.chainId].RETROACTIVE_FUNDING_ADDRESS.address,
336
- percentAllocation: ObolRAFSplit,
335
+ percentAllocation: validatedPayload.ObolRAFSplit,
337
336
  };
338
337
 
339
- const copiedSplitRecipients = [...splitRecipients];
338
+ const copiedSplitRecipients = [...validatedPayload.splitRecipients];
340
339
  copiedSplitRecipients.push(retroActiveFundingRecipient);
341
340
 
342
341
  const { accounts, percentAllocations } = formatSplitRecipients(
@@ -347,8 +346,8 @@ export class Client extends Base {
347
346
  accounts,
348
347
  percentAllocations,
349
348
  chainId: this.chainId,
350
- distributorFee,
351
- controllerAddress,
349
+ distributorFee: validatedPayload.distributorFee,
350
+ controllerAddress: validatedPayload.controllerAddress,
352
351
  });
353
352
 
354
353
  const isSplitterDeployed = await isContractAvailable(
@@ -362,8 +361,8 @@ export class Client extends Base {
362
361
  accounts,
363
362
  percentAllocations,
364
363
  chainId: this.chainId,
365
- distributorFee,
366
- controllerAddress,
364
+ distributorFee: validatedPayload.distributorFee,
365
+ controllerAddress: validatedPayload.controllerAddress,
367
366
  });
368
367
  return {
369
368
  withdrawal_address: splitterAddress,
@@ -411,20 +410,20 @@ export class Client extends Base {
411
410
  throw new Error('Signer is required in createClusterDefinition');
412
411
  }
413
412
 
414
- validatePayload(newCluster, definitionSchema);
413
+ const validatedCluster = validatePayload<ClusterPayload>(
414
+ newCluster,
415
+ definitionSchema,
416
+ );
415
417
 
416
418
  const clusterConfig: Partial<ClusterDefinition> = {
417
- ...newCluster,
419
+ ...validatedCluster,
418
420
  fork_version: this.fork_version,
419
421
  dkg_algorithm: DKG_ALGORITHM,
420
422
  version: CONFIG_VERSION,
421
423
  uuid: uuidv4(),
422
424
  timestamp: new Date().toISOString(),
423
- threshold: Math.ceil((2 * newCluster.operators.length) / 3),
424
- num_validators: newCluster.validators.length,
425
- deposit_amounts: newCluster.deposit_amounts
426
- ? newCluster.deposit_amounts
427
- : ['32000000000'],
425
+ threshold: Math.ceil((2 * validatedCluster.operators.length) / 3),
426
+ num_validators: validatedCluster.validators.length,
428
427
  };
429
428
  try {
430
429
  const address = await this.signer.getAddress();
@@ -479,7 +478,10 @@ export class Client extends Base {
479
478
  throw new Error('Signer is required in acceptClusterDefinition');
480
479
  }
481
480
 
482
- validatePayload(operatorPayload, operatorPayloadSchema);
481
+ const validatedPayload = validatePayload<OperatorPayload>(
482
+ operatorPayload,
483
+ operatorPayloadSchema,
484
+ );
483
485
 
484
486
  try {
485
487
  const address = await this.signer.getAddress();
@@ -492,11 +494,11 @@ export class Client extends Base {
492
494
  const operatorENRSignature = await this.signer.signTypedData(
493
495
  Domain(this.chainId),
494
496
  EnrSigningTypes,
495
- { enr: operatorPayload.enr },
497
+ { enr: validatedPayload.enr },
496
498
  );
497
499
 
498
500
  const operatorData: OperatorPayload = {
499
- ...operatorPayload,
501
+ ...validatedPayload,
500
502
  address,
501
503
  enr_signature: operatorENRSignature,
502
504
  fork_version: this.fork_version,
package/src/schema.ts CHANGED
@@ -1,17 +1,15 @@
1
+ import { ZeroAddress } from 'ethers';
1
2
  import {
2
3
  DEFAULT_RETROACTIVE_FUNDING_REWARDS_ONLY_SPLIT,
3
4
  DEFAULT_RETROACTIVE_FUNDING_TOTAL_SPLIT,
4
5
  } from './constants';
6
+ import { VALID_DEPOSIT_AMOUNTS, VALID_NON_COMPOUNDING_AMOUNTS } from './ajv';
5
7
 
6
8
  export const operatorPayloadSchema = {
7
9
  type: 'object',
8
10
  properties: {
9
- version: {
10
- type: 'string',
11
- },
12
- enr: {
13
- type: 'string',
14
- },
11
+ version: { type: 'string' },
12
+ enr: { type: 'string' },
15
13
  },
16
14
  required: ['version', 'enr'],
17
15
  };
@@ -19,24 +17,20 @@ export const operatorPayloadSchema = {
19
17
  export const definitionSchema = {
20
18
  type: 'object',
21
19
  properties: {
22
- name: {
23
- type: 'string',
24
- },
20
+ name: { type: 'string' },
25
21
  operators: {
26
22
  type: 'array',
27
23
  minItems: 4,
28
- uniqueItems: true,
29
24
  items: {
30
25
  type: 'object',
31
26
  properties: {
32
27
  address: {
33
28
  type: 'string',
34
- minLength: 42,
35
- maxLength: 42,
36
29
  },
37
30
  },
38
31
  required: ['address'],
39
32
  },
33
+ validateUniqueAddresses: true,
40
34
  },
41
35
  validators: {
42
36
  type: 'array',
@@ -57,12 +51,39 @@ export const definitionSchema = {
57
51
  },
58
52
  },
59
53
  deposit_amounts: {
60
- type: 'array',
54
+ type: ['array', 'null'],
61
55
  items: {
62
56
  type: 'string',
63
57
  pattern: '^[0-9]+$',
64
58
  },
65
- validDepositAmounts: true,
59
+ if: {
60
+ $data: '1/compounding',
61
+ },
62
+ then: {
63
+ items: {
64
+ enum: VALID_DEPOSIT_AMOUNTS,
65
+ },
66
+ },
67
+ else: {
68
+ items: {
69
+ enum: VALID_NON_COMPOUNDING_AMOUNTS,
70
+ },
71
+ },
72
+ default: null,
73
+ },
74
+ compounding: {
75
+ type: 'boolean',
76
+ default: false,
77
+ },
78
+ target_gas_limit: {
79
+ type: 'number',
80
+ minimum: 1,
81
+ default: 36000000,
82
+ },
83
+ consensus_protocol: {
84
+ type: 'string',
85
+ enum: ['qbft', ''],
86
+ default: '',
66
87
  },
67
88
  },
68
89
  required: ['name', 'operators', 'validators'],
@@ -80,9 +101,7 @@ export const totalSplitterPayloadSchema = {
80
101
  type: 'string',
81
102
  pattern: '^0x[a-fA-F0-9]{40}$',
82
103
  },
83
- percentAllocation: {
84
- type: 'number',
85
- },
104
+ percentAllocation: { type: 'number' },
86
105
  },
87
106
  required: ['account', 'percentAllocation'],
88
107
  },
@@ -90,40 +109,44 @@ export const totalSplitterPayloadSchema = {
90
109
  ObolRAFSplit: {
91
110
  type: 'number',
92
111
  minimum: DEFAULT_RETROACTIVE_FUNDING_TOTAL_SPLIT,
112
+ default: DEFAULT_RETROACTIVE_FUNDING_TOTAL_SPLIT,
93
113
  },
94
114
  distributorFee: {
95
115
  type: 'number',
96
116
  maximum: 10,
97
117
  multipleOf: 0.01,
118
+ default: 0,
98
119
  },
99
120
  controllerAddress: {
100
121
  type: 'string',
101
122
  pattern: '^0x[a-fA-F0-9]{40}$',
123
+ default: ZeroAddress,
102
124
  },
103
- validateSplitRecipients: true,
104
125
  },
126
+ validateTotalSplitRecipients: true,
105
127
  required: ['splitRecipients'],
106
128
  };
107
129
 
108
130
  export const rewardsSplitterPayloadSchema = {
109
- ...totalSplitterPayloadSchema,
131
+ type: 'object',
110
132
  properties: {
111
133
  ...totalSplitterPayloadSchema.properties,
112
134
  ObolRAFSplit: {
113
135
  type: 'number',
114
136
  minimum: DEFAULT_RETROACTIVE_FUNDING_REWARDS_ONLY_SPLIT,
137
+ default: DEFAULT_RETROACTIVE_FUNDING_REWARDS_ONLY_SPLIT,
115
138
  },
116
139
  recoveryAddress: {
117
140
  type: 'string',
118
141
  pattern: '^0x[a-fA-F0-9]{40}$',
142
+ default: ZeroAddress,
119
143
  },
120
- etherAmount: {
121
- type: 'number',
122
- },
144
+ etherAmount: { type: 'number' },
123
145
  principalRecipient: {
124
146
  type: 'string',
125
147
  pattern: '^0x[a-fA-F0-9]{40}$',
126
148
  },
127
149
  },
150
+ validateRewardsSplitRecipients: true,
128
151
  required: ['splitRecipients', 'principalRecipient', 'etherAmount'],
129
152
  };
package/src/types.ts CHANGED
@@ -105,6 +105,15 @@ export type ClusterPayload = {
105
105
 
106
106
  /** The cluster partial deposits in gwei or 32000000000. */
107
107
  deposit_amounts?: string[] | null;
108
+
109
+ /** A withdrawal mechanism with 0x02 withdrawal credentials. */
110
+ compounding?: boolean;
111
+
112
+ /** The target gas limit where default is 36M. */
113
+ target_gas_limit?: number;
114
+
115
+ /** The consensus protocol e.g qbft. */
116
+ consensus_protocol?: string;
108
117
  };
109
118
 
110
119
  /**
@@ -144,7 +153,7 @@ export interface ClusterDefinition extends ClusterPayload {
144
153
  /** The consensus protocol e.g qbft. */
145
154
  consensus_protocol?: string;
146
155
 
147
- /** The target gas limit where default is 30M. */
156
+ /** The target gas limit where default is 36M. */
148
157
  target_gas_limit?: number;
149
158
 
150
159
  /** A withdrawal mechanism with 0x02 withdrawal credentials. */
@@ -305,7 +314,7 @@ export type ClaimableIncentives = {
305
314
  /** The amount the recipient is entitled to. */
306
315
  amount: string;
307
316
 
308
- /** The recipients index in the Merkle tree. */
317
+ /** The recipient's index in the Merkle tree. */
309
318
  index: number;
310
319
 
311
320
  /** The Merkle proof (an array of hashes) generated for the recipient. */
@@ -251,6 +251,30 @@ export const verifyDVV1X8 = (clusterLock: ClusterLock): boolean => {
251
251
  pubShares.push(fromHexString(element));
252
252
  }
253
253
 
254
+ // Check deposit amounts match exactly if they are defined
255
+ const depositAmounts = clusterLock.cluster_definition.deposit_amounts;
256
+ if (depositAmounts !== null) {
257
+ const partialDepositAmounts = (
258
+ validator.partial_deposit_data as DepositData[]
259
+ ).map(d => d.amount);
260
+ if (depositAmounts?.length !== partialDepositAmounts.length) {
261
+ return false;
262
+ }
263
+ // Check that both arrays contain exactly the same elements
264
+ const sortedDepositAmounts = [...depositAmounts]
265
+ .map(Number)
266
+ .sort((a, b) => a - b);
267
+ const sortedPartialAmounts = [...partialDepositAmounts]
268
+ .map(Number)
269
+ .sort((a, b) => a - b);
270
+ if (
271
+ !sortedDepositAmounts.every(
272
+ (amount, index) => amount === sortedPartialAmounts[index],
273
+ )
274
+ ) {
275
+ return false;
276
+ }
277
+ }
254
278
  // Deposit Data Verification
255
279
  for (const element of validator.partial_deposit_data as DepositData[]) {
256
280
  const depositData = element;