@obolnetwork/obol-sdk 2.0.1 → 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 (49) hide show
  1. package/README.md +6 -0
  2. package/dist/cjs/package.json +1 -1
  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/test/fixtures.js +1 -1
  14. package/dist/cjs/test/methods.test.js +215 -11
  15. package/dist/esm/package.json +1 -1
  16. package/dist/esm/src/abi/Multicall.js +145 -0
  17. package/dist/esm/src/abi/OWR.js +130 -0
  18. package/dist/esm/src/abi/SplitMain.js +926 -0
  19. package/dist/esm/src/ajv.js +17 -2
  20. package/dist/esm/src/bytecodes.js +6 -0
  21. package/dist/esm/src/constants.js +47 -0
  22. package/dist/esm/src/index.js +148 -2
  23. package/dist/esm/src/schema.js +51 -4
  24. package/dist/esm/src/splitHelpers.js +168 -0
  25. package/dist/esm/src/utils.js +19 -0
  26. package/dist/esm/test/fixtures.js +1 -1
  27. package/dist/esm/test/methods.test.js +193 -12
  28. package/dist/types/src/abi/Multicall.d.ts +35 -0
  29. package/dist/types/src/abi/OWR.d.ts +52 -0
  30. package/dist/types/src/abi/SplitMain.d.ts +1159 -0
  31. package/dist/types/src/bytecodes.d.ts +6 -0
  32. package/dist/types/src/constants.d.ts +25 -0
  33. package/dist/types/src/index.d.ts +29 -1
  34. package/dist/types/src/schema.d.ts +85 -4
  35. package/dist/types/src/splitHelpers.d.ts +62 -0
  36. package/dist/types/src/types.d.ts +39 -2
  37. package/dist/types/src/utils.d.ts +3 -0
  38. package/package.json +1 -1
  39. package/src/abi/Multicall.ts +145 -0
  40. package/src/abi/OWR.ts +130 -0
  41. package/src/abi/SplitMain.ts +927 -0
  42. package/src/ajv.ts +33 -2
  43. package/src/bytecodes.ts +12 -0
  44. package/src/constants.ts +59 -0
  45. package/src/index.ts +244 -2
  46. package/src/schema.ts +67 -4
  47. package/src/splitHelpers.ts +341 -0
  48. package/src/types.ts +49 -2
  49. package/src/utils.ts +21 -0
@@ -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
 
@@ -117,6 +117,48 @@ export interface ClusterDefinition extends ClusterPayload {
117
117
  definition_hash?: string;
118
118
  }
119
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
+
120
162
  /**
121
163
  * Unsigned DV Builder Registration Message
122
164
  */
@@ -204,3 +246,8 @@ export type ClusterLock = {
204
246
  /** Node Signature for the lock hash by the node secp256k1 key. */
205
247
  node_signatures?: string[];
206
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
+ };