@obolnetwork/obol-sdk 2.6.0 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/dist/cjs/package.json +4 -2
  2. package/dist/cjs/src/abi/OVMFactory.js +620 -0
  3. package/dist/cjs/src/ajv.js +33 -6
  4. package/dist/cjs/src/bytecodes.js +8 -1
  5. package/dist/cjs/src/constants.js +55 -7
  6. package/dist/cjs/src/exits/exit.js +82 -0
  7. package/dist/cjs/src/index.js +21 -9
  8. package/dist/cjs/src/schema.js +71 -1
  9. package/dist/cjs/src/splits/splitHelpers.js +218 -12
  10. package/dist/cjs/src/splits/splits.js +261 -0
  11. package/dist/cjs/src/types.js +0 -1
  12. package/dist/cjs/test/client/ajv.spec.js +269 -0
  13. package/dist/cjs/test/client/methods.spec.js +418 -0
  14. package/dist/cjs/test/exit/exit.spec.js +74 -3
  15. package/dist/cjs/test/fixtures.js +13 -1
  16. package/dist/cjs/test/splits/splits.spec.js +239 -0
  17. package/dist/esm/package.json +4 -2
  18. package/dist/esm/src/abi/OVMFactory.js +617 -0
  19. package/dist/esm/src/ajv.js +33 -6
  20. package/dist/esm/src/bytecodes.js +7 -0
  21. package/dist/esm/src/constants.js +54 -7
  22. package/dist/esm/src/exits/exit.js +83 -1
  23. package/dist/esm/src/index.js +20 -9
  24. package/dist/esm/src/schema.js +71 -1
  25. package/dist/esm/src/splits/splitHelpers.js +213 -13
  26. package/dist/esm/src/splits/splits.js +257 -0
  27. package/dist/esm/src/types.js +0 -1
  28. package/dist/esm/test/client/ajv.spec.js +267 -0
  29. package/dist/esm/test/client/methods.spec.js +393 -0
  30. package/dist/esm/test/exit/exit.spec.js +74 -3
  31. package/dist/esm/test/fixtures.js +12 -0
  32. package/dist/esm/test/splits/splits.spec.js +237 -0
  33. package/dist/types/src/abi/OVMFactory.d.ts +662 -0
  34. package/dist/types/src/bytecodes.d.ts +7 -0
  35. package/dist/types/src/constants.d.ts +10 -21
  36. package/dist/types/src/exits/exit.d.ts +21 -1
  37. package/dist/types/src/index.d.ts +7 -0
  38. package/dist/types/src/schema.d.ts +145 -0
  39. package/dist/types/src/splits/splitHelpers.d.ts +40 -1
  40. package/dist/types/src/splits/splits.d.ts +51 -0
  41. package/dist/types/src/types.d.ts +122 -6
  42. package/dist/types/test/client/ajv.spec.d.ts +1 -0
  43. package/dist/types/test/client/methods.spec.d.ts +1 -0
  44. package/dist/types/test/fixtures.d.ts +10 -0
  45. package/dist/types/test/splits/splits.spec.d.ts +1 -0
  46. package/package.json +4 -2
  47. package/src/abi/OVMFactory.ts +617 -0
  48. package/src/ajv.ts +55 -13
  49. package/src/bytecodes.ts +15 -0
  50. package/src/constants.ts +66 -8
  51. package/src/exits/exit.ts +98 -1
  52. package/src/index.ts +36 -15
  53. package/src/schema.ts +80 -0
  54. package/src/splits/splitHelpers.ts +360 -14
  55. package/src/splits/splits.ts +355 -0
  56. package/src/types.ts +138 -10
package/src/exits/exit.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { ENR } from '@chainsafe/discv5';
2
2
  import * as elliptic from 'elliptic';
3
- import { init, verify } from '@chainsafe/bls';
3
+ import { init, verify, aggregateSignatures } from '@chainsafe/bls';
4
4
  import {
5
5
  ByteVectorType,
6
6
  ContainerType,
@@ -16,6 +16,7 @@ import type {
16
16
  ExitValidationMessage,
17
17
  SignedExitValidationMessage,
18
18
  ExistingExitValidationBlobData,
19
+ FullExitBlob,
19
20
  } from '../types';
20
21
  import { getCapellaFork, getGenesisValidatorsRoot } from './ethUtils';
21
22
  import { computeDomain, signingRoot } from './verificationHelpers';
@@ -561,4 +562,100 @@ export class Exit {
561
562
 
562
563
  return validNonDuplicateBlobs;
563
564
  }
565
+
566
+ /**
567
+ * Recombines exit blobs into a single exit blob.
568
+ *
569
+ * This method aggregates partial exit signatures from multiple operators into a single exit blob.
570
+ * It ensures that the signatures are properly ordered and aggregated according to the operator indices.
571
+ *
572
+ * @param exitBlob - The existing exit blob data containing partial exit signatures
573
+ *
574
+ * @returns Promise resolving to a single exit blob with aggregated signatures
575
+ *
576
+ * @throws {Error} When no valid signatures are found for aggregation
577
+ * @throws {Error} When signature length is invalid
578
+ * @throws {Error} When signature parsing fails
579
+ *
580
+ * @example
581
+ * ```typescript
582
+ * const aggregatedExitBlob = await exit.recombineExitBlobs(existingBlobData);
583
+ * ```
584
+ */
585
+ async recombineExitBlobs(
586
+ exitBlob: ExistingExitValidationBlobData,
587
+ ): Promise<FullExitBlob> {
588
+ await init('herumi');
589
+
590
+ // Map to store signatures by their share index (matching Go's map[int]tbls.Signature)
591
+ const signaturesByIndex = new Map<number, Uint8Array>();
592
+
593
+ // Extract signatures from shares_exit_data (equivalent to er.Signatures in Go)
594
+ if (!exitBlob.shares_exit_data || exitBlob.shares_exit_data.length === 0) {
595
+ throw new Error('No shares exit data available for aggregation');
596
+ }
597
+ const signaturesMap = exitBlob.shares_exit_data[0] || {};
598
+ for (const [sigIdxStr, sigData] of Object.entries(signaturesMap)) {
599
+ const sigStr = sigData.partial_exit_signature;
600
+
601
+ if (!sigStr || sigStr.length === 0) {
602
+ // ignore, the associated share index didn't push a partial signature yet
603
+ continue;
604
+ }
605
+
606
+ if (sigStr.length < 2) {
607
+ throw new Error(`Signature string has invalid size: ${sigStr.length}`);
608
+ }
609
+
610
+ // Remove 0x prefix and ensure it's 96 bytes (192 hex chars)
611
+ const cleanSigStr = sigStr.startsWith('0x')
612
+ ? sigStr.substring(2)
613
+ : sigStr;
614
+ if (cleanSigStr.length !== 192) {
615
+ throw new Error(
616
+ `Invalid signature length. Expected 192 hex chars (96 bytes), got ${cleanSigStr.length}`,
617
+ );
618
+ }
619
+
620
+ try {
621
+ const sigBytes = fromHexString(cleanSigStr);
622
+ // Convert string index to number and add 1 (matching Go's sigIdx+1)
623
+ const sigIdx = parseInt(sigIdxStr, 10);
624
+ signaturesByIndex.set(sigIdx + 1, sigBytes);
625
+ } catch (err) {
626
+ throw new Error(`Invalid partial signature: ${String(err)}`);
627
+ }
628
+ }
629
+
630
+ if (signaturesByIndex.size === 0) {
631
+ throw new Error('No valid signatures found for aggregation');
632
+ }
633
+
634
+ // Sort by index and extract signatures in correct order
635
+ const sortedIndices = Array.from(signaturesByIndex.keys()).sort(
636
+ (a, b) => a - b,
637
+ );
638
+ const rawSignatures = sortedIndices.map(idx => {
639
+ const signature = signaturesByIndex.get(idx);
640
+ if (signature === undefined) {
641
+ throw new Error(`Missing signature for index ${idx}`);
642
+ }
643
+ return signature;
644
+ });
645
+
646
+ // Aggregate signatures (equivalent to tbls.ThresholdAggregate in Go)
647
+ // Note: @chainsafe/bls doesn't have explicit threshold aggregation, but ordering should be preserved
648
+ const fullSig = aggregateSignatures(rawSignatures);
649
+
650
+ return {
651
+ public_key: exitBlob.public_key,
652
+ signed_exit_message: {
653
+ message: {
654
+ epoch: exitBlob.epoch,
655
+ validator_index: exitBlob.validator_index,
656
+ },
657
+ signature: '0x' + Buffer.from(fullSig).toString('hex'),
658
+ },
659
+ };
660
+ }
564
661
  }
package/src/index.ts CHANGED
@@ -12,10 +12,10 @@ import {
12
12
  TermsAndConditionsSigningTypes,
13
13
  DEFAULT_BASE_VERSION,
14
14
  TERMS_AND_CONDITIONS_HASH,
15
- AVAILABLE_SPLITTER_CHAINS,
16
15
  CHAIN_CONFIGURATION,
17
16
  DEFAULT_RETROACTIVE_FUNDING_REWARDS_ONLY_SPLIT,
18
17
  OBOL_SDK_EMAIL,
18
+ isChainSupportedForSplitters,
19
19
  } from './constants.js';
20
20
  import { ConflictError } from './errors.js';
21
21
  import {
@@ -49,6 +49,7 @@ import {
49
49
  import { isContractAvailable } from './utils.js';
50
50
  import { Incentives } from './incentives/incentives.js';
51
51
  import { Exit } from './exits/exit.js';
52
+ import { ObolSplits } from './splits/splits.js';
52
53
  export * from './types.js';
53
54
  export * from './services.js';
54
55
  export * from './verification/signature-validator.js';
@@ -56,6 +57,7 @@ export * from './verification/common.js';
56
57
  export * from './constants.js';
57
58
  export { Incentives } from './incentives/incentives.js';
58
59
  export { Exit } from './exits/exit.js';
60
+ export { ObolSplits } from './splits/splits.js';
59
61
 
60
62
  /**
61
63
  * Obol sdk Client can be used for creating, managing and activating distributed validators.
@@ -78,6 +80,12 @@ export class Client extends Base {
78
80
  */
79
81
  public exit: Exit;
80
82
 
83
+ /**
84
+ * The splits module, responsible for managing splits.
85
+ * @type {ObolSplits}
86
+ */
87
+ public splits: ObolSplits;
88
+
81
89
  /**
82
90
  * The blockchain provider, used to interact with the network.
83
91
  * It can be null, undefined, or a valid provider instance and defaults to the Signer provider if Signer is passed.
@@ -112,6 +120,7 @@ export class Client extends Base {
112
120
  this.provider,
113
121
  );
114
122
  this.exit = new Exit(this.chainId, this.provider);
123
+ this.splits = new ObolSplits(this.signer, this.chainId, this.provider);
115
124
  }
116
125
 
117
126
  /**
@@ -205,28 +214,35 @@ export class Client extends Base {
205
214
  );
206
215
 
207
216
  // Check if we allow splitters on this chainId
208
- if (!AVAILABLE_SPLITTER_CHAINS.includes(this.chainId)) {
217
+ if (!isChainSupportedForSplitters(this.chainId)) {
209
218
  throw new Error(
210
219
  `Splitter configuration is not supported on ${this.chainId} chain`,
211
220
  );
212
221
  }
213
222
 
223
+ const chainConfig = CHAIN_CONFIGURATION[this.chainId];
224
+ if (!chainConfig?.SPLITMAIN_ADDRESS) {
225
+ throw new Error(
226
+ `SPLITMAIN_ADDRESS is not configured for chain ${this.chainId}`,
227
+ );
228
+ }
229
+
214
230
  const checkSplitMainAddress = await isContractAvailable(
215
- CHAIN_CONFIGURATION[this.chainId].SPLITMAIN_ADDRESS.address,
231
+ chainConfig.SPLITMAIN_ADDRESS.address,
216
232
  this.signer.provider as ProviderType,
217
- CHAIN_CONFIGURATION[this.chainId].SPLITMAIN_ADDRESS.bytecode,
233
+ chainConfig.SPLITMAIN_ADDRESS.bytecode,
218
234
  );
219
235
 
220
236
  const checkMulticallAddress = await isContractAvailable(
221
- CHAIN_CONFIGURATION[this.chainId].MULTICALL_ADDRESS.address,
237
+ chainConfig.MULTICALL_ADDRESS.address,
222
238
  this.signer.provider as ProviderType,
223
- CHAIN_CONFIGURATION[this.chainId].MULTICALL_ADDRESS.bytecode,
239
+ chainConfig.MULTICALL_ADDRESS.bytecode,
224
240
  );
225
241
 
226
242
  const checkOWRFactoryAddress = await isContractAvailable(
227
- CHAIN_CONFIGURATION[this.chainId].OWR_FACTORY_ADDRESS.address,
243
+ chainConfig.OWR_FACTORY_ADDRESS.address,
228
244
  this.signer.provider as ProviderType,
229
- CHAIN_CONFIGURATION[this.chainId].OWR_FACTORY_ADDRESS.bytecode,
245
+ chainConfig.OWR_FACTORY_ADDRESS.bytecode,
230
246
  );
231
247
 
232
248
  if (
@@ -240,8 +256,7 @@ export class Client extends Base {
240
256
  }
241
257
 
242
258
  const retroActiveFundingRecipient = {
243
- account:
244
- CHAIN_CONFIGURATION[this.chainId].RETROACTIVE_FUNDING_ADDRESS.address,
259
+ account: chainConfig.RETROACTIVE_FUNDING_ADDRESS.address,
245
260
  percentAllocation: validatedPayload.ObolRAFSplit,
246
261
  };
247
262
 
@@ -320,16 +335,23 @@ export class Client extends Base {
320
335
  );
321
336
 
322
337
  // Check if we allow splitters on this chainId
323
- if (!AVAILABLE_SPLITTER_CHAINS.includes(this.chainId)) {
338
+ if (!isChainSupportedForSplitters(this.chainId)) {
324
339
  throw new Error(
325
340
  `Splitter configuration is not supported on ${this.chainId} chain`,
326
341
  );
327
342
  }
328
343
 
344
+ const chainConfig = CHAIN_CONFIGURATION[this.chainId];
345
+ if (!chainConfig?.SPLITMAIN_ADDRESS) {
346
+ throw new Error(
347
+ `SPLITMAIN_ADDRESS is not configured for chain ${this.chainId}`,
348
+ );
349
+ }
350
+
329
351
  const checkSplitMainAddress = await isContractAvailable(
330
- CHAIN_CONFIGURATION[this.chainId].SPLITMAIN_ADDRESS.address,
352
+ chainConfig.SPLITMAIN_ADDRESS.address,
331
353
  this.signer.provider as ProviderType,
332
- CHAIN_CONFIGURATION[this.chainId].SPLITMAIN_ADDRESS.bytecode,
354
+ chainConfig.SPLITMAIN_ADDRESS.bytecode,
333
355
  );
334
356
 
335
357
  if (!checkSplitMainAddress) {
@@ -339,8 +361,7 @@ export class Client extends Base {
339
361
  }
340
362
 
341
363
  const retroActiveFundingRecipient = {
342
- account:
343
- CHAIN_CONFIGURATION[this.chainId].RETROACTIVE_FUNDING_ADDRESS.address,
364
+ account: chainConfig.RETROACTIVE_FUNDING_ADDRESS.address,
344
365
  percentAllocation: validatedPayload.ObolRAFSplit,
345
366
  };
346
367
 
package/src/schema.ts CHANGED
@@ -2,8 +2,10 @@ import { ZeroAddress } from 'ethers';
2
2
  import {
3
3
  DEFAULT_RETROACTIVE_FUNDING_REWARDS_ONLY_SPLIT,
4
4
  DEFAULT_RETROACTIVE_FUNDING_TOTAL_SPLIT,
5
+ PRINCIPAL_THRESHOLD,
5
6
  } from './constants';
6
7
  import { VALID_DEPOSIT_AMOUNTS, VALID_NON_COMPOUNDING_AMOUNTS } from './ajv';
8
+ import { zeroAddress } from 'viem';
7
9
 
8
10
  export const operatorPayloadSchema = {
9
11
  type: 'object',
@@ -150,3 +152,81 @@ export const rewardsSplitterPayloadSchema = {
150
152
  validateRewardsSplitRecipients: true,
151
153
  required: ['splitRecipients', 'principalRecipient', 'etherAmount'],
152
154
  };
155
+
156
+ export const ovmBaseSplitPayload = {
157
+ rewardSplitRecipients: {
158
+ type: 'array',
159
+ items: {
160
+ type: 'object',
161
+ properties: {
162
+ address: {
163
+ type: 'string',
164
+ pattern: '^0x[a-fA-F0-9]{40}$',
165
+ },
166
+ percentAllocation: { type: 'number' },
167
+ },
168
+ required: ['address', 'percentAllocation'],
169
+ },
170
+ },
171
+ OVMOwnerAddress: {
172
+ type: 'string',
173
+ pattern: '^0x[a-fA-F0-9]{40}$',
174
+ },
175
+ splitOwnerAddress: {
176
+ type: 'string',
177
+ pattern: '^0x[a-fA-F0-9]{40}$',
178
+ default: zeroAddress,
179
+ },
180
+ principalThreshold: {
181
+ type: 'number',
182
+ minimum: 16,
183
+ default: PRINCIPAL_THRESHOLD,
184
+ },
185
+ distributorFeePercent: {
186
+ type: 'number',
187
+ minimum: 0,
188
+ maximum: 10,
189
+ default: 0,
190
+ },
191
+ };
192
+
193
+ export const ovmRewardsSplitPayloadSchema = {
194
+ type: 'object',
195
+ properties: {
196
+ ...ovmBaseSplitPayload,
197
+ principalRecipient: {
198
+ type: 'string',
199
+ pattern: '^0x[a-fA-F0-9]{40}$',
200
+ },
201
+ },
202
+ validateOVMRewardsSplitRecipients: true,
203
+ required: ['rewardSplitRecipients', 'OVMOwnerAddress', 'principalRecipient'],
204
+ };
205
+
206
+ export const ovmTotalSplitPayloadSchema = {
207
+ type: 'object',
208
+ properties: {
209
+ ...ovmBaseSplitPayload,
210
+ principalSplitRecipients: {
211
+ type: 'array',
212
+ items: {
213
+ type: 'object',
214
+ properties: {
215
+ address: {
216
+ type: 'string',
217
+ pattern: '^0x[a-fA-F0-9]{40}$',
218
+ },
219
+ percentAllocation: { type: 'number' },
220
+ },
221
+ required: ['address', 'percentAllocation'],
222
+ },
223
+ },
224
+ },
225
+ validateOVMRewardsSplitRecipients: true,
226
+ validateOVMTotalSplitRecipients: true,
227
+ required: [
228
+ 'rewardSplitRecipients',
229
+ 'principalSplitRecipients',
230
+ 'OVMOwnerAddress',
231
+ ],
232
+ };