@obolnetwork/obol-sdk 2.7.0 → 2.8.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.
Files changed (56) hide show
  1. package/dist/cjs/package.json +1 -1
  2. package/dist/cjs/src/abi/OVMFactory.js +620 -0
  3. package/dist/cjs/src/abi/splitV2FactoryAbi.js +420 -0
  4. package/dist/cjs/src/ajv.js +33 -6
  5. package/dist/cjs/src/bytecodes.js +10 -1
  6. package/dist/cjs/src/constants.js +71 -15
  7. package/dist/cjs/src/index.js +21 -9
  8. package/dist/cjs/src/schema.js +70 -1
  9. package/dist/cjs/src/splits/splitHelpers.js +188 -14
  10. package/dist/cjs/src/splits/splits.js +283 -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/fixtures.js +13 -1
  15. package/dist/cjs/test/splits/splits.spec.js +239 -0
  16. package/dist/esm/package.json +1 -1
  17. package/dist/esm/src/abi/OVMFactory.js +617 -0
  18. package/dist/esm/src/abi/splitV2FactoryAbi.js +417 -0
  19. package/dist/esm/src/ajv.js +33 -6
  20. package/dist/esm/src/bytecodes.js +9 -0
  21. package/dist/esm/src/constants.js +70 -15
  22. package/dist/esm/src/index.js +20 -9
  23. package/dist/esm/src/schema.js +70 -1
  24. package/dist/esm/src/splits/splitHelpers.js +182 -13
  25. package/dist/esm/src/splits/splits.js +279 -0
  26. package/dist/esm/src/types.js +0 -1
  27. package/dist/esm/test/client/ajv.spec.js +267 -0
  28. package/dist/esm/test/client/methods.spec.js +393 -0
  29. package/dist/esm/test/fixtures.js +12 -0
  30. package/dist/esm/test/splits/splits.spec.js +237 -0
  31. package/dist/types/src/abi/OVMFactory.d.ts +662 -0
  32. package/dist/types/src/abi/splitV2FactoryAbi.d.ts +60 -0
  33. package/dist/types/src/bytecodes.d.ts +9 -0
  34. package/dist/types/src/constants.d.ts +10 -21
  35. package/dist/types/src/exits/verificationHelpers.d.ts +1 -0
  36. package/dist/types/src/index.d.ts +7 -0
  37. package/dist/types/src/schema.d.ts +145 -0
  38. package/dist/types/src/splits/splitHelpers.d.ts +39 -2
  39. package/dist/types/src/splits/splits.d.ts +51 -0
  40. package/dist/types/src/types.d.ts +87 -2
  41. package/dist/types/src/verification/common.d.ts +1 -0
  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 +1 -1
  47. package/src/abi/OVMFactory.ts +617 -0
  48. package/src/abi/splitV2FactoryAbi.ts +417 -0
  49. package/src/ajv.ts +55 -13
  50. package/src/bytecodes.ts +19 -0
  51. package/src/constants.ts +84 -16
  52. package/src/index.ts +36 -15
  53. package/src/schema.ts +79 -0
  54. package/src/splits/splitHelpers.ts +373 -18
  55. package/src/splits/splits.ts +406 -0
  56. package/src/types.ts +100 -3
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_CONTRACT) {
225
+ throw new Error(
226
+ `SPLITMAIN_CONTRACT 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_CONTRACT.address,
216
232
  this.signer.provider as ProviderType,
217
- CHAIN_CONFIGURATION[this.chainId].SPLITMAIN_ADDRESS.bytecode,
233
+ chainConfig.SPLITMAIN_CONTRACT.bytecode,
218
234
  );
219
235
 
220
236
  const checkMulticallAddress = await isContractAvailable(
221
- CHAIN_CONFIGURATION[this.chainId].MULTICALL_ADDRESS.address,
237
+ chainConfig.MULTICALL_CONTRACT.address,
222
238
  this.signer.provider as ProviderType,
223
- CHAIN_CONFIGURATION[this.chainId].MULTICALL_ADDRESS.bytecode,
239
+ chainConfig.MULTICALL_CONTRACT.bytecode,
224
240
  );
225
241
 
226
242
  const checkOWRFactoryAddress = await isContractAvailable(
227
- CHAIN_CONFIGURATION[this.chainId].OWR_FACTORY_ADDRESS.address,
243
+ chainConfig.OWR_FACTORY_CONTRACT.address,
228
244
  this.signer.provider as ProviderType,
229
- CHAIN_CONFIGURATION[this.chainId].OWR_FACTORY_ADDRESS.bytecode,
245
+ chainConfig.OWR_FACTORY_CONTRACT.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_CONTRACT.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_CONTRACT) {
346
+ throw new Error(
347
+ `SPLITMAIN_CONTRACT 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_CONTRACT.address,
331
353
  this.signer.provider as ProviderType,
332
- CHAIN_CONFIGURATION[this.chainId].SPLITMAIN_ADDRESS.bytecode,
354
+ chainConfig.SPLITMAIN_CONTRACT.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_CONTRACT.address,
344
365
  percentAllocation: validatedPayload.ObolRAFSplit,
345
366
  };
346
367
 
package/src/schema.ts CHANGED
@@ -2,6 +2,7 @@ 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';
7
8
 
@@ -150,3 +151,81 @@ export const rewardsSplitterPayloadSchema = {
150
151
  validateRewardsSplitRecipients: true,
151
152
  required: ['splitRecipients', 'principalRecipient', 'etherAmount'],
152
153
  };
154
+
155
+ export const ovmBaseSplitPayload = {
156
+ rewardSplitRecipients: {
157
+ type: 'array',
158
+ items: {
159
+ type: 'object',
160
+ properties: {
161
+ address: {
162
+ type: 'string',
163
+ pattern: '^0x[a-fA-F0-9]{40}$',
164
+ },
165
+ percentAllocation: { type: 'number' },
166
+ },
167
+ required: ['address', 'percentAllocation'],
168
+ },
169
+ },
170
+ OVMOwnerAddress: {
171
+ type: 'string',
172
+ pattern: '^0x[a-fA-F0-9]{40}$',
173
+ },
174
+ splitOwnerAddress: {
175
+ type: 'string',
176
+ pattern: '^0x[a-fA-F0-9]{40}$',
177
+ default: ZeroAddress,
178
+ },
179
+ principalThreshold: {
180
+ type: 'number',
181
+ minimum: 16,
182
+ default: PRINCIPAL_THRESHOLD,
183
+ },
184
+ distributorFeePercent: {
185
+ type: 'number',
186
+ minimum: 0,
187
+ maximum: 10,
188
+ default: 0,
189
+ },
190
+ };
191
+
192
+ export const ovmRewardsSplitPayloadSchema = {
193
+ type: 'object',
194
+ properties: {
195
+ ...ovmBaseSplitPayload,
196
+ principalRecipient: {
197
+ type: 'string',
198
+ pattern: '^0x[a-fA-F0-9]{40}$',
199
+ },
200
+ },
201
+ validateOVMRewardsSplitRecipients: true,
202
+ required: ['rewardSplitRecipients', 'OVMOwnerAddress', 'principalRecipient'],
203
+ };
204
+
205
+ export const ovmTotalSplitPayloadSchema = {
206
+ type: 'object',
207
+ properties: {
208
+ ...ovmBaseSplitPayload,
209
+ principalSplitRecipients: {
210
+ type: 'array',
211
+ items: {
212
+ type: 'object',
213
+ properties: {
214
+ address: {
215
+ type: 'string',
216
+ pattern: '^0x[a-fA-F0-9]{40}$',
217
+ },
218
+ percentAllocation: { type: 'number' },
219
+ },
220
+ required: ['address', 'percentAllocation'],
221
+ },
222
+ },
223
+ },
224
+ validateOVMRewardsSplitRecipients: true,
225
+ validateOVMTotalSplitRecipients: true,
226
+ required: [
227
+ 'rewardSplitRecipients',
228
+ 'principalSplitRecipients',
229
+ 'OVMOwnerAddress',
230
+ ],
231
+ };
@@ -4,15 +4,22 @@ import {
4
4
  type ETH_ADDRESS,
5
5
  type SplitRecipient,
6
6
  type SignerType,
7
+ type SplitV2Recipient,
8
+ type OVMArgs,
9
+ type ChainConfig,
7
10
  } from '../types';
8
11
  import { Contract, Interface, parseEther, ZeroAddress } from 'ethers';
9
12
  import { OWRContract, OWRFactoryContract } from '../abi/OWR';
13
+ import { OVMFactoryContract } from '../abi/OVMFactory';
10
14
  import { splitMainEthereumAbi } from '../abi/SplitMain';
11
15
  import { MultiCallContract } from '../abi/Multicall';
12
16
  import { CHAIN_CONFIGURATION } from '../constants';
17
+ import { splitV2FactoryAbi } from '../abi/splitV2FactoryAbi';
13
18
 
14
19
  const splitMainContractInterface = new Interface(splitMainEthereumAbi);
15
20
  const owrFactoryContractInterface = new Interface(OWRFactoryContract.abi);
21
+ const ovmFactoryContractInterface = new Interface(OVMFactoryContract.abi);
22
+ const splitV2FactoryInterface = new Interface(splitV2FactoryAbi);
16
23
 
17
24
  type Call = {
18
25
  target: ETH_ADDRESS;
@@ -33,14 +40,39 @@ type SplitArgs = {
33
40
  controllerAddress: ETH_ADDRESS;
34
41
  };
35
42
 
43
+ // Helper function to extract common recipient formatting logic
44
+ const formatRecipientsCommon = (
45
+ recipients: SplitRecipient[] | SplitV2Recipient[],
46
+ ): {
47
+ sortedRecipients: any[];
48
+ getAddress: (item: any) => string;
49
+ getPercentAllocation: (item: any) => number;
50
+ } => {
51
+ const copiedRecipients = [...recipients];
52
+
53
+ // Handle both SplitRecipient and SplitV2Recipient types
54
+ const getAddress = (item: any): string => item.account || item.address;
55
+ const getPercentAllocation = (item: any): number => item.percentAllocation;
56
+
57
+ // Has to be sorted when passed
58
+ copiedRecipients.sort((a, b) => getAddress(a).localeCompare(getAddress(b)));
59
+
60
+ return {
61
+ sortedRecipients: copiedRecipients,
62
+ getAddress,
63
+ getPercentAllocation,
64
+ };
65
+ };
66
+
36
67
  export const formatSplitRecipients = (
37
68
  recipients: SplitRecipient[],
38
69
  ): { accounts: ETH_ADDRESS[]; percentAllocations: number[] } => {
39
- // Has to be sorted when passed
40
- recipients.sort((a, b) => a.account.localeCompare(b.account));
41
- const accounts = recipients.map(item => item.account);
42
- const percentAllocations = recipients.map(recipient => {
43
- const splitTostring = (recipient.percentAllocation * 1e4).toFixed(0);
70
+ const { sortedRecipients, getAddress, getPercentAllocation } =
71
+ formatRecipientsCommon(recipients);
72
+
73
+ const accounts = sortedRecipients.map(item => getAddress(item));
74
+ const percentAllocations = sortedRecipients.map(recipient => {
75
+ const splitTostring = (getPercentAllocation(recipient) * 1e4).toFixed(0);
44
76
  return parseInt(splitTostring);
45
77
  });
46
78
  return { accounts, percentAllocations };
@@ -63,7 +95,7 @@ export const predictSplitterAddress = async ({
63
95
  }): Promise<ETH_ADDRESS> => {
64
96
  try {
65
97
  const splitMainContractInstance = new Contract(
66
- CHAIN_CONFIGURATION[chainId].SPLITMAIN_ADDRESS.address,
98
+ getChainConfig(chainId).SPLITMAIN_CONTRACT.address,
67
99
  splitMainEthereumAbi,
68
100
  signer,
69
101
  );
@@ -217,7 +249,7 @@ const createOWRContract = async ({
217
249
  }): Promise<ETH_ADDRESS> => {
218
250
  try {
219
251
  const OWRFactoryInstance = new Contract(
220
- CHAIN_CONFIGURATION[chainId].OWR_FACTORY_ADDRESS.address,
252
+ getChainConfig(chainId).OWR_FACTORY_CONTRACT.address,
221
253
  OWRFactoryContract.abi,
222
254
  signer,
223
255
  );
@@ -304,7 +336,7 @@ export const deploySplitterContract = async ({
304
336
  }): Promise<ETH_ADDRESS> => {
305
337
  try {
306
338
  const splitMainContractInstance = new Contract(
307
- CHAIN_CONFIGURATION[chainId].SPLITMAIN_ADDRESS.address,
339
+ getChainConfig(chainId).SPLITMAIN_CONTRACT.address,
308
340
  splitMainEthereumAbi,
309
341
  signer,
310
342
  );
@@ -403,22 +435,16 @@ export const deploySplitterAndOWRContracts = async ({
403
435
 
404
436
  executeCalls.push(
405
437
  {
406
- target: CHAIN_CONFIGURATION[chainId].SPLITMAIN_ADDRESS.address,
438
+ target: getChainConfig(chainId).SPLITMAIN_CONTRACT.address,
407
439
  callData: splitTxData,
408
440
  },
409
441
  {
410
- target: CHAIN_CONFIGURATION[chainId].OWR_FACTORY_ADDRESS.address,
442
+ target: getChainConfig(chainId).OWR_FACTORY_CONTRACT.address,
411
443
  callData: owrTxData,
412
444
  },
413
445
  );
414
- const multicallAddess =
415
- CHAIN_CONFIGURATION[chainId].MULTICALL_ADDRESS.address;
416
446
 
417
- const executeMultiCalls = await multicall(
418
- executeCalls,
419
- signer,
420
- multicallAddess,
421
- );
447
+ const executeMultiCalls = await multicall(executeCalls, signer, chainId);
422
448
 
423
449
  const splitAddressData = executeMultiCalls?.logs[0]?.topics[1];
424
450
  const formattedSplitterAddress = '0x' + splitAddressData?.slice(26, 66);
@@ -479,9 +505,11 @@ export const getOWRTranches = async ({
479
505
  export const multicall = async (
480
506
  calls: Call[],
481
507
  signer: SignerType,
482
- multicallAddress: string,
508
+ chainId: number,
483
509
  ): Promise<any> => {
484
510
  try {
511
+ const chainConfig = getChainConfig(chainId);
512
+ const multicallAddress = chainConfig.MULTICALL_CONTRACT.address;
485
513
  const multiCallContractInstance = new Contract(
486
514
  multicallAddress,
487
515
  MultiCallContract.abi,
@@ -555,3 +583,330 @@ const encodeCreateOWRecipientTxData = (
555
583
  parseEther(amountOfPrincipalStake.toString()),
556
584
  ]);
557
585
  };
586
+
587
+ // OVM and SplitV2 Helper Functions
588
+
589
+ // Helper function to format recipients specifically for SplitV2 (returns SplitV2Recipient[])
590
+ export const formatRecipientsForSplitV2 = (
591
+ splitRecipients: SplitRecipient[] | SplitV2Recipient[],
592
+ ): SplitV2Recipient[] => {
593
+ const { sortedRecipients, getAddress, getPercentAllocation } =
594
+ formatRecipientsCommon(splitRecipients);
595
+
596
+ return sortedRecipients
597
+ .filter(item => getAddress(item) !== '')
598
+ .map(item => ({
599
+ address: getAddress(item),
600
+ percentAllocation: parseFloat(getPercentAllocation(item).toString()),
601
+ }));
602
+ };
603
+
604
+ // Helper function to create SplitV2 parameters
605
+ const createSplitV2Params = (
606
+ recipients: SplitV2Recipient[],
607
+ distributorFeePercent: number,
608
+ ): {
609
+ recipients: string[];
610
+ allocations: number[];
611
+ totalAllocation: number;
612
+ distributionIncentive: number;
613
+ } => {
614
+ const addresses = recipients.map(r => r.address);
615
+ const allocations = recipients.map(r =>
616
+ Math.floor(r.percentAllocation * 1e4),
617
+ ); // Convert to basis points
618
+ const totalAllocation = allocations.reduce(
619
+ (sum, allocation) => sum + allocation,
620
+ 0,
621
+ );
622
+
623
+ return {
624
+ recipients: addresses,
625
+ allocations,
626
+ totalAllocation,
627
+ distributionIncentive: distributorFeePercent,
628
+ };
629
+ };
630
+
631
+ export const predictSplitV2Address = async ({
632
+ splitOwnerAddress,
633
+ recipients,
634
+ distributorFeePercent,
635
+ salt,
636
+ signer,
637
+ chainId,
638
+ }: {
639
+ splitOwnerAddress: string;
640
+ recipients: SplitV2Recipient[];
641
+ distributorFeePercent: number;
642
+ salt: `0x${string}`;
643
+ signer: SignerType;
644
+ chainId: number;
645
+ }): Promise<string> => {
646
+ try {
647
+ const chainConfig = getChainConfig(chainId);
648
+ if (!chainConfig?.SPLIT_V2_FACTORY_CONTRACT?.address) {
649
+ throw new Error(`SplitV2 Factory not configured for chain ${chainId}`);
650
+ }
651
+
652
+ const splitV2FactoryContract = new Contract(
653
+ chainConfig.SPLIT_V2_FACTORY_CONTRACT.address,
654
+ splitV2FactoryAbi,
655
+ signer,
656
+ );
657
+
658
+ const splitParams = createSplitV2Params(recipients, distributorFeePercent);
659
+
660
+ const predictedAddress = await splitV2FactoryContract[
661
+ 'predictDeterministicAddress((address[],uint256[],uint256,uint16),address,bytes32)'
662
+ ](splitParams, splitOwnerAddress, salt);
663
+
664
+ return predictedAddress;
665
+ } catch (error: any) {
666
+ throw new Error(
667
+ `Failed to predict SplitV2 address: ${error.message ?? 'SplitV2 contract call failed'}`,
668
+ );
669
+ }
670
+ };
671
+
672
+ export const isSplitV2Deployed = async ({
673
+ splitOwnerAddress,
674
+ recipients,
675
+ distributorFeePercent,
676
+ salt,
677
+ signer,
678
+ chainId,
679
+ }: {
680
+ splitOwnerAddress: string;
681
+ recipients: SplitV2Recipient[];
682
+ distributorFeePercent: number;
683
+ salt: `0x${string}`;
684
+ signer: SignerType;
685
+ chainId: number;
686
+ }): Promise<boolean> => {
687
+ try {
688
+ const chainConfig = getChainConfig(chainId);
689
+ if (!chainConfig?.SPLIT_V2_FACTORY_CONTRACT?.address) {
690
+ throw new Error(`SplitV2 Factory not configured for chain ${chainId}`);
691
+ }
692
+
693
+ const splitV2FactoryContract = new Contract(
694
+ chainConfig.SPLIT_V2_FACTORY_CONTRACT.address,
695
+ splitV2FactoryAbi,
696
+ signer,
697
+ );
698
+
699
+ const splitParams = createSplitV2Params(recipients, distributorFeePercent);
700
+
701
+ const [, exists] = await splitV2FactoryContract.isDeployed(
702
+ splitParams,
703
+ splitOwnerAddress,
704
+ salt,
705
+ );
706
+
707
+ return exists;
708
+ } catch (error: any) {
709
+ // If the check fails, assume it's not deployed
710
+ return false;
711
+ }
712
+ };
713
+
714
+ export const deployOVMContract = async ({
715
+ OVMOwnerAddress,
716
+ principalRecipient,
717
+ rewardRecipient,
718
+ principalThreshold,
719
+ signer,
720
+ chainId,
721
+ }: {
722
+ OVMOwnerAddress: string;
723
+ principalRecipient: string;
724
+ rewardRecipient: string;
725
+ principalThreshold: number;
726
+ signer: SignerType;
727
+ chainId: number;
728
+ }): Promise<string> => {
729
+ try {
730
+ const chainConfig = getChainConfig(chainId);
731
+ if (!chainConfig?.OVM_FACTORY_CONTRACT?.address) {
732
+ throw new Error(`OVM Factory not configured for chain ${chainId}`);
733
+ }
734
+
735
+ const ovmFactoryContract = new Contract(
736
+ chainConfig.OVM_FACTORY_CONTRACT.address,
737
+ OVMFactoryContract.abi,
738
+ signer,
739
+ );
740
+
741
+ const tx = await ovmFactoryContract.createObolValidatorManager(
742
+ OVMOwnerAddress,
743
+ principalRecipient,
744
+ rewardRecipient,
745
+ principalThreshold,
746
+ );
747
+
748
+ const receipt = await tx.wait();
749
+
750
+ // Extract OVM address from logs
751
+ const ovmAddressLog = receipt?.logs[1]?.topics[1];
752
+ if (!ovmAddressLog) {
753
+ throw new Error('Failed to extract OVM address from transaction logs');
754
+ }
755
+
756
+ const ovmAddress = '0x' + ovmAddressLog.slice(26, 66);
757
+ return ovmAddress;
758
+ } catch (error: any) {
759
+ throw new Error(
760
+ `Failed to deploy OVM contract: ${error.message ?? 'OVM deployment failed'}`,
761
+ );
762
+ }
763
+ };
764
+
765
+ export const deployOVMAndSplitV2 = async ({
766
+ ovmArgs,
767
+ rewardRecipients,
768
+ isRewardsSplitterDeployed,
769
+ distributorFeePercent,
770
+ salt,
771
+ signer,
772
+ chainId,
773
+ principalSplitRecipients,
774
+ isPrincipalSplitDeployed,
775
+ splitOwnerAddress,
776
+ }: {
777
+ ovmArgs: OVMArgs;
778
+ rewardRecipients: SplitV2Recipient[];
779
+ isRewardsSplitterDeployed?: boolean;
780
+ distributorFeePercent: number;
781
+ salt: `0x${string}`;
782
+ signer: SignerType;
783
+ chainId: number;
784
+ principalSplitRecipients?: SplitV2Recipient[];
785
+ isPrincipalSplitDeployed?: boolean;
786
+ splitOwnerAddress: string;
787
+ }): Promise<string> => {
788
+ try {
789
+ const chainConfig = getChainConfig(chainId);
790
+ if (!chainConfig?.OVM_FACTORY_CONTRACT?.address) {
791
+ throw new Error(`OVM Factory not configured for chain ${chainId}`);
792
+ }
793
+
794
+ if (!chainConfig?.SPLIT_V2_FACTORY_CONTRACT?.address) {
795
+ throw new Error(`SplitV2 Factory not configured for chain ${chainId}`);
796
+ }
797
+
798
+ const executeCalls: Call[] = [];
799
+
800
+ if (rewardRecipients && !isRewardsSplitterDeployed) {
801
+ // Create rewards split call data
802
+ const splitParams = createSplitV2Params(
803
+ rewardRecipients,
804
+ distributorFeePercent,
805
+ );
806
+ const rewardsSplitTxData = encodeCreateSplitV2DeterministicTxData(
807
+ splitParams,
808
+ splitOwnerAddress,
809
+ salt,
810
+ );
811
+
812
+ executeCalls.push({
813
+ target: chainConfig.SPLIT_V2_FACTORY_CONTRACT.address,
814
+ callData: rewardsSplitTxData,
815
+ });
816
+ }
817
+
818
+ // Create principal split call data if needed (for total split scenario)
819
+ if (principalSplitRecipients && !isPrincipalSplitDeployed) {
820
+ const principalSplitParams = createSplitV2Params(
821
+ principalSplitRecipients,
822
+ distributorFeePercent,
823
+ );
824
+ const principalSplitTxData = encodeCreateSplitV2DeterministicTxData(
825
+ principalSplitParams,
826
+ splitOwnerAddress,
827
+ salt,
828
+ );
829
+ executeCalls.push({
830
+ target: chainConfig.SPLIT_V2_FACTORY_CONTRACT.address,
831
+ callData: principalSplitTxData,
832
+ });
833
+ }
834
+
835
+ // Create OVM call data
836
+ const ovmTxData = encodeCreateOVMTxData(
837
+ ovmArgs.OVMOwnerAddress,
838
+ ovmArgs.principalRecipient,
839
+ ovmArgs.rewardRecipient,
840
+ ovmArgs.principalThreshold,
841
+ );
842
+
843
+ executeCalls.push({
844
+ target: chainConfig.OVM_FACTORY_CONTRACT.address,
845
+ callData: ovmTxData,
846
+ });
847
+
848
+ // Execute multicall
849
+ const executeMultiCalls = await multicall(executeCalls, signer, chainId);
850
+
851
+ // Extract addresses from events
852
+ let ovmAddress: string | undefined;
853
+ const logsCount = executeMultiCalls?.logs?.length || 0;
854
+
855
+ if (logsCount === 2) {
856
+ ovmAddress = '0x' + executeMultiCalls?.logs[1]?.topics[1]?.slice(26, 66);
857
+ } else if (logsCount === 5) {
858
+ ovmAddress = '0x' + executeMultiCalls?.logs[4]?.topics[1]?.slice(26, 66);
859
+ } else {
860
+ ovmAddress = '0x' + executeMultiCalls?.logs[7]?.topics[1]?.slice(26, 66);
861
+ }
862
+ if (!ovmAddress) {
863
+ throw new Error(
864
+ 'Failed to extract contract addresses from multicall events',
865
+ );
866
+ }
867
+
868
+ return ovmAddress;
869
+ } catch (error: any) {
870
+ throw new Error(
871
+ `Failed to deploy OVM and SplitV2: ${error.message ?? 'Deployment failed'}`,
872
+ );
873
+ }
874
+ };
875
+
876
+ const encodeCreateOVMTxData = (
877
+ OVMOwnerAddress: string,
878
+ principalRecipient: string,
879
+ rewardRecipient: string,
880
+ principalThreshold: number,
881
+ ): string => {
882
+ return ovmFactoryContractInterface.encodeFunctionData(
883
+ 'createObolValidatorManager',
884
+ [OVMOwnerAddress, principalRecipient, rewardRecipient, principalThreshold],
885
+ );
886
+ };
887
+
888
+ const encodeCreateSplitV2DeterministicTxData = (
889
+ splitParams: {
890
+ recipients: string[];
891
+ allocations: number[];
892
+ totalAllocation: number;
893
+ distributionIncentive: number;
894
+ },
895
+ splitOwnerAddress: string,
896
+ salt: `0x${string}`,
897
+ ): string => {
898
+ // creatorAddress can be kept as default https://docs.splits.org/sdk/splits-v2#createsplit
899
+ return splitV2FactoryInterface.encodeFunctionData(
900
+ 'createSplitDeterministic',
901
+ [splitParams, splitOwnerAddress, ZeroAddress, salt],
902
+ );
903
+ };
904
+
905
+ // Helper function to safely get chain configuration
906
+ const getChainConfig = (chainId: number): ChainConfig => {
907
+ const config = CHAIN_CONFIGURATION[chainId];
908
+ if (!config) {
909
+ throw new Error(`Chain configuration not found for chain ID ${chainId}`);
910
+ }
911
+ return config;
912
+ };