@obolnetwork/obol-sdk 2.0.0 → 2.1.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/README.md +6 -0
  2. package/dist/cjs/package.json +4 -7
  3. package/dist/cjs/src/abi/Multicall.js +148 -0
  4. package/dist/cjs/src/abi/OWR.js +133 -0
  5. package/dist/cjs/src/abi/SplitMain.js +929 -0
  6. package/dist/cjs/src/ajv.js +17 -2
  7. package/dist/cjs/src/bytecodes.js +9 -0
  8. package/dist/cjs/src/constants.js +48 -1
  9. package/dist/cjs/src/index.js +146 -0
  10. package/dist/cjs/src/schema.js +52 -5
  11. package/dist/cjs/src/splitHelpers.js +177 -0
  12. package/dist/cjs/src/utils.js +22 -1
  13. package/dist/cjs/src/verification/common.js +8 -0
  14. package/dist/cjs/src/verification/v1.8.0.js +5 -3
  15. package/dist/cjs/test/fixtures.js +119 -2
  16. package/dist/cjs/test/methods.test.js +227 -11
  17. package/dist/esm/package.json +4 -7
  18. package/dist/esm/src/abi/Multicall.js +145 -0
  19. package/dist/esm/src/abi/OWR.js +130 -0
  20. package/dist/esm/src/abi/SplitMain.js +926 -0
  21. package/dist/esm/src/ajv.js +17 -2
  22. package/dist/esm/src/bytecodes.js +6 -0
  23. package/dist/esm/src/constants.js +47 -0
  24. package/dist/esm/src/index.js +148 -2
  25. package/dist/esm/src/schema.js +51 -4
  26. package/dist/esm/src/splitHelpers.js +168 -0
  27. package/dist/esm/src/utils.js +19 -0
  28. package/dist/esm/src/verification/common.js +8 -0
  29. package/dist/esm/src/verification/v1.8.0.js +5 -3
  30. package/dist/esm/test/fixtures.js +118 -1
  31. package/dist/esm/test/methods.test.js +206 -13
  32. package/dist/types/src/abi/Multicall.d.ts +35 -0
  33. package/dist/types/src/abi/OWR.d.ts +52 -0
  34. package/dist/types/src/abi/SplitMain.d.ts +1159 -0
  35. package/dist/types/src/bytecodes.d.ts +6 -0
  36. package/dist/types/src/constants.d.ts +25 -0
  37. package/dist/types/src/index.d.ts +29 -1
  38. package/dist/types/src/schema.d.ts +85 -4
  39. package/dist/types/src/splitHelpers.d.ts +62 -0
  40. package/dist/types/src/types.d.ts +40 -5
  41. package/dist/types/src/utils.d.ts +3 -0
  42. package/dist/types/test/fixtures.d.ts +51 -0
  43. package/package.json +4 -7
  44. package/src/abi/Multicall.ts +145 -0
  45. package/src/abi/OWR.ts +130 -0
  46. package/src/abi/SplitMain.ts +927 -0
  47. package/src/ajv.ts +33 -2
  48. package/src/bytecodes.ts +12 -0
  49. package/src/constants.ts +59 -0
  50. package/src/index.ts +244 -2
  51. package/src/schema.ts +67 -4
  52. package/src/splitHelpers.ts +341 -0
  53. package/src/types.ts +50 -6
  54. package/src/utils.ts +21 -0
  55. package/src/verification/common.ts +12 -0
  56. package/src/verification/v1.8.0.ts +4 -4
@@ -0,0 +1,341 @@
1
+ import {
2
+ type ClusterValidator,
3
+ type ETH_ADDRESS,
4
+ type SplitRecipient,
5
+ } from './types';
6
+ import {
7
+ Contract,
8
+ Interface,
9
+ parseEther,
10
+ ZeroAddress,
11
+ type Signer,
12
+ } from 'ethers';
13
+ import { OWRFactoryContract } from './abi/OWR';
14
+ import { splitMainEthereumAbi } from './abi/SplitMain';
15
+ import { MultiCallContract } from './abi/Multicall';
16
+ import { CHAIN_CONFIGURATION } from './constants';
17
+
18
+ const splitMainContractInterface = new Interface(splitMainEthereumAbi);
19
+ const owrFactoryContractInterface = new Interface(OWRFactoryContract.abi);
20
+
21
+ type Call = {
22
+ target: ETH_ADDRESS;
23
+ callData: string;
24
+ };
25
+
26
+ type OWRArgs = {
27
+ recoveryAddress: ETH_ADDRESS;
28
+ principalRecipient: ETH_ADDRESS;
29
+ amountOfPrincipalStake: number;
30
+ predictedSplitterAddress: ETH_ADDRESS;
31
+ };
32
+
33
+ type SplitArgs = {
34
+ accounts: ETH_ADDRESS[];
35
+ percentAllocations: number[];
36
+ distributorFee: number;
37
+ controllerAddress: ETH_ADDRESS;
38
+ };
39
+
40
+ export const formatSplitRecipients = (
41
+ recipients: SplitRecipient[],
42
+ ): { accounts: ETH_ADDRESS[]; percentAllocations: number[] } => {
43
+ // Has to be sorted when passed
44
+ recipients.sort((a, b) => a.account.localeCompare(b.account));
45
+ const accounts = recipients.map(item => item.account);
46
+ const percentAllocations = recipients.map(recipient => {
47
+ const splitTostring = (recipient.percentAllocation * 1e4).toFixed(0);
48
+ return parseInt(splitTostring);
49
+ });
50
+ return { accounts, percentAllocations };
51
+ };
52
+
53
+ export const predictSplitterAddress = async ({
54
+ signer,
55
+ accounts,
56
+ percentAllocations,
57
+ chainId,
58
+ distributorFee,
59
+ controllerAddress,
60
+ }: {
61
+ signer: Signer;
62
+ accounts: ETH_ADDRESS[];
63
+ percentAllocations: number[];
64
+ chainId: number;
65
+ distributorFee: number;
66
+ controllerAddress: ETH_ADDRESS;
67
+ }): Promise<ETH_ADDRESS> => {
68
+ try {
69
+ let predictedSplitterAddress: string;
70
+ const splitMainContractInstance = new Contract(
71
+ CHAIN_CONFIGURATION[chainId].SPLITMAIN_ADDRESS.address,
72
+ splitMainEthereumAbi,
73
+ signer,
74
+ );
75
+
76
+ if (controllerAddress === ZeroAddress) {
77
+ predictedSplitterAddress =
78
+ await splitMainContractInstance.predictImmutableSplitAddress(
79
+ accounts,
80
+ percentAllocations,
81
+ distributorFee,
82
+ );
83
+ } else {
84
+ // It throws on deployed Immutable splitter
85
+ predictedSplitterAddress =
86
+ await splitMainContractInstance.createSplit.staticCall(
87
+ accounts,
88
+ percentAllocations,
89
+ distributorFee,
90
+ controllerAddress,
91
+ );
92
+ }
93
+
94
+ return predictedSplitterAddress;
95
+ } catch (e) {
96
+ throw e;
97
+ }
98
+ };
99
+
100
+ export const handleDeployOWRAndSplitter = async ({
101
+ signer,
102
+ isSplitterDeployed,
103
+ predictedSplitterAddress,
104
+ accounts,
105
+ percentAllocations,
106
+ etherAmount,
107
+ principalRecipient,
108
+ chainId,
109
+ distributorFee,
110
+ controllerAddress,
111
+ recoveryAddress,
112
+ }: {
113
+ signer: Signer;
114
+ isSplitterDeployed: boolean;
115
+ predictedSplitterAddress: ETH_ADDRESS;
116
+ accounts: ETH_ADDRESS[];
117
+ percentAllocations: number[];
118
+ etherAmount: number;
119
+ principalRecipient: ETH_ADDRESS;
120
+ chainId: number;
121
+ distributorFee: number;
122
+ controllerAddress: ETH_ADDRESS;
123
+ recoveryAddress: ETH_ADDRESS;
124
+ }): Promise<ClusterValidator> => {
125
+ try {
126
+ if (isSplitterDeployed) {
127
+ const owrAddress = await createOWRContract({
128
+ owrArgs: {
129
+ principalRecipient,
130
+ amountOfPrincipalStake: etherAmount,
131
+ predictedSplitterAddress,
132
+ recoveryAddress,
133
+ },
134
+ signer,
135
+ chainId,
136
+ });
137
+ return {
138
+ withdrawal_address: owrAddress,
139
+ fee_recipient_address: predictedSplitterAddress,
140
+ };
141
+ } else {
142
+ const { owrAddress, splitterAddress } =
143
+ await deploySplitterAndOWRContracts({
144
+ owrArgs: {
145
+ principalRecipient,
146
+ amountOfPrincipalStake: etherAmount,
147
+ predictedSplitterAddress,
148
+ recoveryAddress,
149
+ },
150
+ splitterArgs: {
151
+ accounts,
152
+ percentAllocations,
153
+ distributorFee,
154
+ controllerAddress,
155
+ },
156
+ signer,
157
+ chainId,
158
+ });
159
+
160
+ return {
161
+ withdrawal_address: owrAddress,
162
+ fee_recipient_address: splitterAddress,
163
+ };
164
+ }
165
+ } catch (e) {
166
+ throw e;
167
+ }
168
+ };
169
+
170
+ const createOWRContract = async ({
171
+ owrArgs,
172
+ signer,
173
+ chainId,
174
+ }: {
175
+ owrArgs: OWRArgs;
176
+ signer: Signer;
177
+ chainId: number;
178
+ }): Promise<ETH_ADDRESS> => {
179
+ try {
180
+ const OWRFactoryInstance = new Contract(
181
+ CHAIN_CONFIGURATION[chainId].OWR_FACTORY_ADDRESS.address,
182
+ OWRFactoryContract.abi,
183
+ signer,
184
+ );
185
+
186
+ const tx = await OWRFactoryInstance.createOWRecipient(
187
+ owrArgs.recoveryAddress,
188
+ owrArgs.principalRecipient,
189
+ owrArgs.predictedSplitterAddress,
190
+ parseEther(owrArgs.amountOfPrincipalStake.toString()),
191
+ );
192
+
193
+ const receipt = await tx.wait();
194
+ const OWRAddressData = receipt?.logs[0]?.topics[1];
195
+ const formattedOWRAddress = '0x' + OWRAddressData?.slice(26, 66);
196
+
197
+ return formattedOWRAddress;
198
+ } catch (e) {
199
+ throw e;
200
+ }
201
+ };
202
+
203
+ export const deploySplitterContract = async ({
204
+ signer,
205
+ accounts,
206
+ percentAllocations,
207
+ chainId,
208
+ distributorFee,
209
+ controllerAddress,
210
+ }: {
211
+ signer: Signer;
212
+ accounts: ETH_ADDRESS[];
213
+ percentAllocations: number[];
214
+ chainId: number;
215
+ distributorFee: number;
216
+ controllerAddress: ETH_ADDRESS;
217
+ }): Promise<ETH_ADDRESS> => {
218
+ try {
219
+ const splitMainContractInstance = new Contract(
220
+ CHAIN_CONFIGURATION[chainId].SPLITMAIN_ADDRESS.address,
221
+ splitMainEthereumAbi,
222
+ signer,
223
+ );
224
+ const tx = await splitMainContractInstance.createSplit(
225
+ accounts,
226
+ percentAllocations,
227
+ distributorFee,
228
+ controllerAddress,
229
+ );
230
+
231
+ const receipt = await tx.wait();
232
+ const splitterAddressData = receipt?.logs[0]?.topics[1];
233
+ const formattedSplitterAddress = '0x' + splitterAddressData?.slice(26, 66);
234
+
235
+ return formattedSplitterAddress;
236
+ } catch (e) {
237
+ throw e;
238
+ }
239
+ };
240
+ export const deploySplitterAndOWRContracts = async ({
241
+ owrArgs,
242
+ splitterArgs,
243
+ signer,
244
+ chainId,
245
+ }: {
246
+ owrArgs: OWRArgs;
247
+ splitterArgs: SplitArgs;
248
+ signer: Signer;
249
+ chainId: number;
250
+ }): Promise<{ owrAddress: ETH_ADDRESS; splitterAddress: ETH_ADDRESS }> => {
251
+ const executeCalls: Call[] = [];
252
+ try {
253
+ const splitTxData = encodeCreateSplitTxData(
254
+ splitterArgs.accounts,
255
+ splitterArgs.percentAllocations,
256
+ splitterArgs.distributorFee,
257
+ splitterArgs.controllerAddress,
258
+ );
259
+
260
+ const owrTxData = encodeCreateOWRecipientTxData(
261
+ owrArgs.recoveryAddress,
262
+ owrArgs.principalRecipient,
263
+ owrArgs.predictedSplitterAddress,
264
+ owrArgs.amountOfPrincipalStake,
265
+ );
266
+
267
+ executeCalls.push(
268
+ {
269
+ target: CHAIN_CONFIGURATION[chainId].SPLITMAIN_ADDRESS.address,
270
+ callData: splitTxData,
271
+ },
272
+ {
273
+ target: CHAIN_CONFIGURATION[chainId].OWR_FACTORY_ADDRESS.address,
274
+ callData: owrTxData,
275
+ },
276
+ );
277
+ const multicallAddess =
278
+ CHAIN_CONFIGURATION[chainId].MULTICALL_ADDRESS.address;
279
+
280
+ const executeMultiCalls = await multicall(
281
+ executeCalls,
282
+ signer,
283
+ multicallAddess,
284
+ );
285
+
286
+ const splitAddressData = executeMultiCalls?.logs[0]?.topics[1];
287
+ const formattedSplitterAddress = '0x' + splitAddressData?.slice(26, 66);
288
+ const owrAddressData = executeMultiCalls?.logs[1]?.topics[1];
289
+ const formattedOwrAddress = '0x' + owrAddressData?.slice(26, 66);
290
+
291
+ return {
292
+ owrAddress: formattedOwrAddress,
293
+ splitterAddress: formattedSplitterAddress,
294
+ };
295
+ } catch (e) {
296
+ throw e;
297
+ }
298
+ };
299
+
300
+ export const multicall = async (
301
+ calls: Call[],
302
+ signer: Signer,
303
+ multicallAddress: string,
304
+ ): Promise<any> => {
305
+ const multiCallContractInstance = new Contract(
306
+ multicallAddress,
307
+ MultiCallContract.abi,
308
+ signer,
309
+ );
310
+ const tx = await multiCallContractInstance.aggregate(calls);
311
+ const receipt = await tx.wait();
312
+ return receipt;
313
+ };
314
+
315
+ const encodeCreateSplitTxData = (
316
+ accounts: ETH_ADDRESS[],
317
+ percentAllocations: number[],
318
+ distributorFee: number,
319
+ controller: ETH_ADDRESS,
320
+ ): ETH_ADDRESS => {
321
+ return splitMainContractInterface.encodeFunctionData('createSplit', [
322
+ accounts,
323
+ percentAllocations,
324
+ distributorFee,
325
+ controller,
326
+ ]);
327
+ };
328
+
329
+ const encodeCreateOWRecipientTxData = (
330
+ recoveryAddress: ETH_ADDRESS,
331
+ principalRecipient: ETH_ADDRESS,
332
+ rewardRecipient: ETH_ADDRESS,
333
+ amountOfPrincipalStake: number,
334
+ ): ETH_ADDRESS => {
335
+ return owrFactoryContractInterface.encodeFunctionData('createOWRecipient', [
336
+ recoveryAddress,
337
+ principalRecipient,
338
+ rewardRecipient,
339
+ parseEther(amountOfPrincipalStake.toString()),
340
+ ]);
341
+ };
package/src/types.ts CHANGED
@@ -58,10 +58,10 @@ export type ClusterCreator = {
58
58
  * Validator withdrawal configuration
59
59
  */
60
60
  export type ClusterValidator = {
61
- /** The validator fee recipient address. */
61
+ /** Address to receive MEV rewards (if enabled), block proposal and priority fees. */
62
62
  fee_recipient_address: string;
63
63
 
64
- /** The validator reward address. */
64
+ /** Address to receive skimming rewards and validator principal at exit. */
65
65
  withdrawal_address: string;
66
66
  };
67
67
 
@@ -79,7 +79,7 @@ export type ClusterPayload = {
79
79
  validators: ClusterValidator[];
80
80
 
81
81
  /** The cluster partial deposits in gwei or 32000000000. */
82
- deposit_amounts?: string[];
82
+ deposit_amounts?: string[] | null;
83
83
  };
84
84
 
85
85
  /**
@@ -113,13 +113,52 @@ export interface ClusterDefinition extends ClusterPayload {
113
113
  /** The number of distributed validators in the cluster. */
114
114
  num_validators: number;
115
115
 
116
- /** The cluster partial deposits in gwei or 32000000000. */
117
- deposit_amounts?: string[];
118
-
119
116
  /** The hash of the cluster definition. */
120
117
  definition_hash?: string;
121
118
  }
122
119
 
120
+ /**
121
+ * Split Recipient Keys
122
+ */
123
+ export type SplitRecipient = {
124
+ /** The split recipient address. */
125
+ account: string;
126
+
127
+ /** The recipient split. */
128
+ percentAllocation: number;
129
+ };
130
+
131
+ /**
132
+ * Split Proxy Params
133
+ */
134
+ export type TotalSplitPayload = {
135
+ /** The split recipients addresses and splits. */
136
+ splitRecipients: SplitRecipient[];
137
+
138
+ /** Split percentageNumber allocated for obol retroactive funding, minimum is 1%. */
139
+ ObolRAFSplit?: number;
140
+
141
+ /** The percentageNumber of accrued rewards that is paid to the caller of the distribution function to compensate them for the gas costs of doing so. Cannot be greater than 10%. For example, 5 represents 5%. */
142
+ distributorFee?: number;
143
+
144
+ /** Address that can mutate the split, should be ZeroAddress for immutable split. */
145
+ controllerAddress?: string;
146
+ };
147
+
148
+ /**
149
+ * OWR and Split Proxy Params
150
+ */
151
+ export interface RewardsSplitPayload extends TotalSplitPayload {
152
+ /** Address that will reclaim validator principal after exit. */
153
+ principalRecipient: string;
154
+
155
+ /** Amount needed to deploy all validators expected for the OWR/Splitter configuration. */
156
+ etherAmount: number;
157
+
158
+ /** Address that can control where the owr erc-20 tokens can be pushed, if set to zero it goes to splitter or principal address. */
159
+ recoveryAddress?: string;
160
+ }
161
+
123
162
  /**
124
163
  * Unsigned DV Builder Registration Message
125
164
  */
@@ -207,3 +246,8 @@ export type ClusterLock = {
207
246
  /** Node Signature for the lock hash by the node secp256k1 key. */
208
247
  node_signatures?: string[];
209
248
  };
249
+
250
+ /**
251
+ * String expected to be Ethereum Address
252
+ */
253
+ export type ETH_ADDRESS = string;
package/src/utils.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { type Provider } from 'ethers';
1
2
  import { DefinitionFlow } from './constants';
2
3
  import { type ClusterDefinition } from './types';
3
4
 
@@ -54,3 +55,23 @@ export const definitionFlow = (
54
55
  }
55
56
  return null;
56
57
  };
58
+
59
+ export const findDeployedBytecode = async (
60
+ contractAddress: string,
61
+ provider: Provider,
62
+ ): Promise<string> => {
63
+ return await provider?.getCode(contractAddress);
64
+ };
65
+
66
+ export const isContractAvailable = async (
67
+ contractAddress: string,
68
+ provider: Provider,
69
+ bytecode?: string,
70
+ ): Promise<boolean> => {
71
+ const code = await findDeployedBytecode(contractAddress, provider);
72
+
73
+ if (bytecode) {
74
+ return !!code && code === bytecode;
75
+ }
76
+ return !!code && code !== '0x' && code !== '0x0';
77
+ };
@@ -109,6 +109,18 @@ export const clusterLockHash = (clusterLock: ClusterLock): string => {
109
109
  }
110
110
 
111
111
  if (semver.eq(clusterLock.cluster_definition.version, 'v1.8.0')) {
112
+ if (
113
+ clusterLock.cluster_definition.deposit_amounts === null &&
114
+ clusterLock.distributed_validators.some(
115
+ distributedValidator =>
116
+ distributedValidator.partial_deposit_data?.length !== 1 ||
117
+ distributedValidator.partial_deposit_data[0].amount !== '32000000000',
118
+ )
119
+ ) {
120
+ throw new Error(
121
+ 'mismatch between deposit_amounts and partial_deposit_data fields',
122
+ );
123
+ }
112
124
  return hashClusterLockV1X8(clusterLock);
113
125
  }
114
126
 
@@ -137,11 +137,11 @@ export const hashClusterDefinitionV1X8 = (
137
137
  withdrawal_address: fromHexString(validator.withdrawal_address),
138
138
  };
139
139
  });
140
- val.deposit_amounts = (cluster.deposit_amounts as string[]).map(
141
- (amount: string) => {
140
+ if (cluster.deposit_amounts) {
141
+ val.deposit_amounts = cluster.deposit_amounts.map((amount: string) => {
142
142
  return parseInt(amount);
143
- },
144
- );
143
+ });
144
+ }
145
145
 
146
146
  if (!configOnly) {
147
147
  val.config_hash = fromHexString(cluster.config_hash);