@obolnetwork/obol-sdk 2.8.1 → 2.9.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@obolnetwork/obol-sdk",
3
- "version": "2.8.1",
3
+ "version": "2.9.0",
4
4
  "description": "A package for creating Distributed Validators using the Obol API.",
5
5
  "bugs": {
6
6
  "url": "https://github.com/obolnetwork/obol-sdk/issues"
@@ -67,6 +67,25 @@ const validateOVMTotalSplitRecipients = (_, data) => {
67
67
  const splitPercentage = calculateTotalPercentage(data.principalSplitRecipients);
68
68
  return validateTotalPercentage(splitPercentage);
69
69
  };
70
+ const validateOVMRequestWithdrawalPayload = (_, data) => {
71
+ console.log(data, 'hanaaan dataaaa');
72
+ if (!data.pubKeys || !data.amounts) {
73
+ return false;
74
+ }
75
+ if (data.pubKeys.length !== data.amounts.length) {
76
+ return false;
77
+ }
78
+ // // Validate that all amounts are at least 1,000,000 gwei
79
+ // const minAmount = BigInt(1000000);
80
+ // for (const amountStr of data.amounts) {
81
+ // const minAmount = BigInt(1000000);
82
+ // const amount = BigInt(amountStr);
83
+ // if (amount < minAmount) {
84
+ // return false;
85
+ // }
86
+ // }
87
+ return true;
88
+ };
70
89
  const ajv = new ajv_1.default({
71
90
  allErrors: true,
72
91
  useDefaults: true,
@@ -100,6 +119,11 @@ ajv.addKeyword({
100
119
  validate: validateOVMTotalSplitRecipients,
101
120
  schemaType: 'boolean',
102
121
  });
122
+ ajv.addKeyword({
123
+ keyword: 'validateOVMRequestWithdrawalPayload',
124
+ validate: validateOVMRequestWithdrawalPayload,
125
+ schemaType: 'boolean',
126
+ });
103
127
  function validatePayload(data, schema) {
104
128
  var _a;
105
129
  const validate = ajv.compile(schema);
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ovmTotalSplitPayloadSchema = exports.ovmRewardsSplitPayloadSchema = exports.ovmBaseSplitPayload = exports.rewardsSplitterPayloadSchema = exports.totalSplitterPayloadSchema = exports.definitionSchema = exports.operatorPayloadSchema = void 0;
3
+ exports.ovmRequestWithdrawalPayloadSchema = exports.ovmTotalSplitPayloadSchema = exports.ovmRewardsSplitPayloadSchema = exports.ovmBaseSplitPayload = exports.rewardsSplitterPayloadSchema = exports.totalSplitterPayloadSchema = exports.definitionSchema = exports.operatorPayloadSchema = void 0;
4
4
  const ethers_1 = require("ethers");
5
5
  const constants_1 = require("./constants");
6
6
  const ajv_1 = require("./ajv");
@@ -209,3 +209,30 @@ exports.ovmTotalSplitPayloadSchema = {
209
209
  'OVMOwnerAddress',
210
210
  ],
211
211
  };
212
+ exports.ovmRequestWithdrawalPayloadSchema = {
213
+ type: 'object',
214
+ properties: {
215
+ ovmAddress: {
216
+ type: 'string',
217
+ pattern: '^0x[a-fA-F0-9]{40}$',
218
+ },
219
+ pubKeys: {
220
+ type: 'array',
221
+ minItems: 1,
222
+ items: {
223
+ type: 'string',
224
+ pattern: '^0x[a-fA-F0-9]{96}$', // 48 bytes = 96 hex chars + 0x prefix
225
+ },
226
+ },
227
+ amounts: {
228
+ type: 'array',
229
+ minItems: 1,
230
+ items: {
231
+ type: 'string',
232
+ pattern: '^[0-9]+$',
233
+ },
234
+ },
235
+ },
236
+ validateOVMRequestWithdrawalPayload: true,
237
+ required: ['ovmAddress', 'pubKeys', 'amounts'],
238
+ };
@@ -9,7 +9,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
9
9
  });
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.deployOVMAndSplitV2 = exports.deployOVMContract = exports.isSplitV2Deployed = exports.predictSplitV2Address = exports.formatRecipientsForSplitV2 = exports.multicall = exports.getOWRTranches = exports.deploySplitterAndOWRContracts = exports.deploySplitterContract = exports.handleDeployOWRAndSplitter = exports.predictSplitterAddress = exports.formatSplitRecipients = void 0;
12
+ exports.requestWithdrawalFromOVM = exports.deployOVMAndSplitV2 = exports.deployOVMContract = exports.isSplitV2Deployed = exports.predictSplitV2Address = exports.formatRecipientsForSplitV2 = exports.multicall = exports.getOWRTranches = exports.deploySplitterAndOWRContracts = exports.deploySplitterContract = exports.handleDeployOWRAndSplitter = exports.predictSplitterAddress = exports.formatSplitRecipients = void 0;
13
13
  const ethers_1 = require("ethers");
14
14
  const OWR_1 = require("../abi/OWR");
15
15
  const OVMFactory_1 = require("../abi/OVMFactory");
@@ -499,3 +499,26 @@ const getChainConfig = (chainId) => {
499
499
  }
500
500
  return config;
501
501
  };
502
+ /**
503
+ * Requests withdrawal from an OVM contract
504
+ * @param ovmAddress - The address of the OVM contract
505
+ * @param pubKeys - Array of validator public keys in bytes format
506
+ * @param amounts - Array of withdrawal amounts in wei (uint64)
507
+ * @param signer - The signer to use for the transaction
508
+ * @returns Promise that resolves to the transaction hash
509
+ */
510
+ const requestWithdrawalFromOVM = ({ ovmAddress, pubKeys, amounts, signer, }) => __awaiter(void 0, void 0, void 0, function* () {
511
+ var _15;
512
+ try {
513
+ // Convert string amounts to bigint
514
+ const bigintAmounts = amounts.map(amount => BigInt(amount));
515
+ const ovmContract = new ethers_1.Contract(ovmAddress, OVMFactory_1.OVMContract.abi, signer);
516
+ const tx = yield ovmContract.requestWithdrawal(pubKeys, bigintAmounts);
517
+ const receipt = yield tx.wait();
518
+ return { txHash: receipt.hash };
519
+ }
520
+ catch (error) {
521
+ throw new Error(`Failed to request withdrawal from OVM: ${(_15 = error.message) !== null && _15 !== void 0 ? _15 : 'Request withdrawal failed'}`);
522
+ }
523
+ });
524
+ exports.requestWithdrawalFromOVM = requestWithdrawalFromOVM;
@@ -279,5 +279,44 @@ class ObolSplits {
279
279
  }
280
280
  });
281
281
  }
282
+ /**
283
+ * Requests withdrawal from an OVM contract.
284
+ *
285
+ * This method allows requesting withdrawal of validator funds from an OVM contract.
286
+ * The withdrawal request includes OVM address, validator public keys and corresponding withdrawal amounts.
287
+ *
288
+ * @remarks
289
+ * **⚠️ Important:** If you're storing the private key in an `.env` file, ensure it is securely managed
290
+ * and not pushed to version control.
291
+ *
292
+ * @param {OVMRequestWithdrawalPayload} payload - Data needed to request withdrawal
293
+ * @returns {Promise<{txHash: string}>} Transaction hash of the withdrawal request
294
+ * @throws Will throw an error if the signer is not provided, OVM address is invalid, or the request fails
295
+ *
296
+ * An example of how to use requestWithdrawal:
297
+ * ```typescript
298
+ * const result = await client.splits.requestWithdrawal({
299
+ * ovmAddress: '0x1234567890123456789012345678901234567890',
300
+ * pubKeys: ['0x123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456'],
301
+ * amounts: ['32000000000'] // 32 ETH in gwei
302
+ * });
303
+ * console.log('Withdrawal requested:', result.txHash);
304
+ * ```
305
+ */
306
+ requestWithdrawal(payload) {
307
+ return __awaiter(this, void 0, void 0, function* () {
308
+ if (!this.signer) {
309
+ throw new Error('Signer is required in requestWithdrawal');
310
+ }
311
+ // [TBD] need to move ovm verification to sdk method and use it here
312
+ const validatedPayload = (0, ajv_1.validatePayload)(payload, schema_1.ovmRequestWithdrawalPayloadSchema);
313
+ return yield (0, splitHelpers_1.requestWithdrawalFromOVM)({
314
+ ovmAddress: validatedPayload.ovmAddress,
315
+ pubKeys: validatedPayload.pubKeys,
316
+ amounts: validatedPayload.amounts,
317
+ signer: this.signer,
318
+ });
319
+ });
320
+ }
282
321
  }
283
322
  exports.ObolSplits = ObolSplits;
@@ -267,3 +267,35 @@ describe('validatePayload - OVM Schemas', () => {
267
267
  });
268
268
  });
269
269
  });
270
+ describe('ovmRequestWithdrawalPayloadSchema', () => {
271
+ const validPayload = {
272
+ ovmAddress: '0x1234567890123456789012345678901234567890',
273
+ pubKeys: [
274
+ '0x123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456',
275
+ ],
276
+ amounts: ['32000000000'],
277
+ };
278
+ it('should throw error when OVM address is missing', () => {
279
+ const payload = {
280
+ pubKeys: validPayload.pubKeys,
281
+ amounts: validPayload.amounts,
282
+ };
283
+ expect(() => (0, ajv_1.validatePayload)(payload, schema_1.ovmRequestWithdrawalPayloadSchema)).toThrow("Validation failed: must have required property 'ovmAddress'");
284
+ });
285
+ it('should throw error when OVM address is invalid', () => {
286
+ const payload = {
287
+ ovmAddress: '0x123', // Too short
288
+ pubKeys: validPayload.pubKeys,
289
+ amounts: validPayload.amounts,
290
+ };
291
+ expect(() => (0, ajv_1.validatePayload)(payload, schema_1.ovmRequestWithdrawalPayloadSchema)).toThrow('Validation failed: /ovmAddress must match pattern');
292
+ });
293
+ it('should throw error when number of public keys does not match number of amounts', () => {
294
+ const payload = {
295
+ ovmAddress: validPayload.ovmAddress,
296
+ pubKeys: validPayload.pubKeys,
297
+ amounts: ['32000000000', '16000000000'], // 2 amounts, 1 pubKey
298
+ };
299
+ expect(() => (0, ajv_1.validatePayload)(payload, schema_1.ovmRequestWithdrawalPayloadSchema)).toThrow('Validation failed: must pass "validateOVMRequestWithdrawalPayload" keyword validation');
300
+ });
301
+ });
@@ -21,6 +21,7 @@ jest.mock('../../src/splits/splitHelpers', () => ({
21
21
  isSplitV2Deployed: jest.fn(),
22
22
  deployOVMContract: jest.fn(),
23
23
  deployOVMAndSplitV2: jest.fn(),
24
+ requestWithdrawalFromOVM: jest.fn(),
24
25
  }));
25
26
  // Mock the utils
26
27
  jest.mock('../../src/utils', () => ({
@@ -36,6 +37,7 @@ const mockPredictSplitV2Address = splitHelpers_1.predictSplitV2Address;
36
37
  const mockIsSplitV2Deployed = splitHelpers_1.isSplitV2Deployed;
37
38
  const mockDeployOVMAndSplitV2 = splitHelpers_1.deployOVMAndSplitV2;
38
39
  const mockDeployOVMContract = splitHelpers_1.deployOVMContract;
40
+ const mockRequestWithdrawalFromOVM = splitHelpers_1.requestWithdrawalFromOVM;
39
41
  const mockIsContractAvailable = utils_1.isContractAvailable;
40
42
  describe('ObolSplits', () => {
41
43
  let client;
@@ -44,6 +46,8 @@ describe('ObolSplits', () => {
44
46
  beforeEach(() => {
45
47
  // Clear all mocks before each test
46
48
  jest.clearAllMocks();
49
+ // Reset the mock to not be called by default
50
+ mockRequestWithdrawalFromOVM.mockReset();
47
51
  mockSigner = {
48
52
  getAddress: jest
49
53
  .fn()
@@ -228,6 +232,76 @@ describe('ObolSplits', () => {
228
232
  yield expect(clientUnsupportedChain.splits.createValidatorManagerAndTotalSplit(mockTotalSplitPayload)).rejects.toThrow('Splitter configuration is not supported on 999 chain');
229
233
  }));
230
234
  });
235
+ describe('requestWithdrawal', () => {
236
+ const mockOVMAddress = '0x1234567890123456789012345678901234567890';
237
+ const mockPubKeys = [
238
+ '0x123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456',
239
+ ];
240
+ const mockAmounts = ['32000000000']; // 32 ETH in gwei
241
+ it('should request withdrawal successfully', () => __awaiter(void 0, void 0, void 0, function* () {
242
+ mockRequestWithdrawalFromOVM.mockResolvedValue({
243
+ txHash: '0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890',
244
+ });
245
+ const result = yield client.splits.requestWithdrawal({
246
+ ovmAddress: mockOVMAddress,
247
+ pubKeys: mockPubKeys,
248
+ amounts: mockAmounts,
249
+ });
250
+ expect(result).toEqual({
251
+ txHash: '0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890',
252
+ });
253
+ expect(mockRequestWithdrawalFromOVM).toHaveBeenCalledWith({
254
+ ovmAddress: mockOVMAddress,
255
+ pubKeys: mockPubKeys,
256
+ amounts: mockAmounts,
257
+ signer: mockSigner,
258
+ });
259
+ }));
260
+ it('should throw error when signer is not provided', () => __awaiter(void 0, void 0, void 0, function* () {
261
+ const clientWithoutSigner = new src_1.Client({ chainId: 1 }, undefined, mockProvider);
262
+ yield expect(clientWithoutSigner.splits.requestWithdrawal({
263
+ ovmAddress: mockOVMAddress,
264
+ pubKeys: mockPubKeys,
265
+ amounts: mockAmounts,
266
+ })).rejects.toThrow('Signer is required in requestWithdrawal');
267
+ }));
268
+ // it('should throw error when amount is below minimum (1,000,000 gwei)', async () => {
269
+ // await expect(
270
+ // client.splits.requestWithdrawal({
271
+ // ovmAddress: mockOVMAddress,
272
+ // pubKeys: mockPubKeys,
273
+ // amounts: ['500000'], // Below minimum of 1,000,000 gwei
274
+ // }),
275
+ // ).rejects.toThrow('Validation failed');
276
+ // });
277
+ it('should handle multiple validators withdrawal request', () => __awaiter(void 0, void 0, void 0, function* () {
278
+ const multiplePubKeys = [
279
+ '0x123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456',
280
+ '0xabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcd',
281
+ ];
282
+ const multipleAmounts = [
283
+ '32000000000', // 32 ETH
284
+ '16000000000', // 16 ETH
285
+ ];
286
+ mockRequestWithdrawalFromOVM.mockResolvedValue({
287
+ txHash: '0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890',
288
+ });
289
+ const result = yield client.splits.requestWithdrawal({
290
+ ovmAddress: mockOVMAddress,
291
+ pubKeys: multiplePubKeys,
292
+ amounts: multipleAmounts,
293
+ });
294
+ expect(result).toEqual({
295
+ txHash: '0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890',
296
+ });
297
+ expect(mockRequestWithdrawalFromOVM).toHaveBeenCalledWith({
298
+ ovmAddress: mockOVMAddress,
299
+ pubKeys: multiplePubKeys,
300
+ amounts: multipleAmounts,
301
+ signer: mockSigner,
302
+ });
303
+ }));
304
+ });
231
305
  describe('constructor', () => {
232
306
  it('should create Client instance with splits property', () => {
233
307
  const testClient = new src_1.Client({ chainId: 1 }, mockSigner, mockProvider);
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@obolnetwork/obol-sdk",
3
- "version": "2.8.1",
3
+ "version": "2.9.0",
4
4
  "description": "A package for creating Distributed Validators using the Obol API.",
5
5
  "bugs": {
6
6
  "url": "https://github.com/obolnetwork/obol-sdk/issues"
@@ -61,6 +61,25 @@ const validateOVMTotalSplitRecipients = (_, data) => {
61
61
  const splitPercentage = calculateTotalPercentage(data.principalSplitRecipients);
62
62
  return validateTotalPercentage(splitPercentage);
63
63
  };
64
+ const validateOVMRequestWithdrawalPayload = (_, data) => {
65
+ console.log(data, 'hanaaan dataaaa');
66
+ if (!data.pubKeys || !data.amounts) {
67
+ return false;
68
+ }
69
+ if (data.pubKeys.length !== data.amounts.length) {
70
+ return false;
71
+ }
72
+ // // Validate that all amounts are at least 1,000,000 gwei
73
+ // const minAmount = BigInt(1000000);
74
+ // for (const amountStr of data.amounts) {
75
+ // const minAmount = BigInt(1000000);
76
+ // const amount = BigInt(amountStr);
77
+ // if (amount < minAmount) {
78
+ // return false;
79
+ // }
80
+ // }
81
+ return true;
82
+ };
64
83
  const ajv = new Ajv({
65
84
  allErrors: true,
66
85
  useDefaults: true,
@@ -94,6 +113,11 @@ ajv.addKeyword({
94
113
  validate: validateOVMTotalSplitRecipients,
95
114
  schemaType: 'boolean',
96
115
  });
116
+ ajv.addKeyword({
117
+ keyword: 'validateOVMRequestWithdrawalPayload',
118
+ validate: validateOVMRequestWithdrawalPayload,
119
+ schemaType: 'boolean',
120
+ });
97
121
  export function validatePayload(data, schema) {
98
122
  var _a;
99
123
  const validate = ajv.compile(schema);
@@ -206,3 +206,30 @@ export const ovmTotalSplitPayloadSchema = {
206
206
  'OVMOwnerAddress',
207
207
  ],
208
208
  };
209
+ export const ovmRequestWithdrawalPayloadSchema = {
210
+ type: 'object',
211
+ properties: {
212
+ ovmAddress: {
213
+ type: 'string',
214
+ pattern: '^0x[a-fA-F0-9]{40}$',
215
+ },
216
+ pubKeys: {
217
+ type: 'array',
218
+ minItems: 1,
219
+ items: {
220
+ type: 'string',
221
+ pattern: '^0x[a-fA-F0-9]{96}$', // 48 bytes = 96 hex chars + 0x prefix
222
+ },
223
+ },
224
+ amounts: {
225
+ type: 'array',
226
+ minItems: 1,
227
+ items: {
228
+ type: 'string',
229
+ pattern: '^[0-9]+$',
230
+ },
231
+ },
232
+ },
233
+ validateOVMRequestWithdrawalPayload: true,
234
+ required: ['ovmAddress', 'pubKeys', 'amounts'],
235
+ };
@@ -9,7 +9,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
9
9
  };
10
10
  import { Contract, Interface, parseEther, ZeroAddress } from 'ethers';
11
11
  import { OWRContract, OWRFactoryContract } from '../abi/OWR';
12
- import { OVMFactoryContract } from '../abi/OVMFactory';
12
+ import { OVMFactoryContract, OVMContract } from '../abi/OVMFactory';
13
13
  import { splitMainEthereumAbi } from '../abi/SplitMain';
14
14
  import { MultiCallContract } from '../abi/Multicall';
15
15
  import { CHAIN_CONFIGURATION } from '../constants';
@@ -484,3 +484,25 @@ const getChainConfig = (chainId) => {
484
484
  }
485
485
  return config;
486
486
  };
487
+ /**
488
+ * Requests withdrawal from an OVM contract
489
+ * @param ovmAddress - The address of the OVM contract
490
+ * @param pubKeys - Array of validator public keys in bytes format
491
+ * @param amounts - Array of withdrawal amounts in wei (uint64)
492
+ * @param signer - The signer to use for the transaction
493
+ * @returns Promise that resolves to the transaction hash
494
+ */
495
+ export const requestWithdrawalFromOVM = ({ ovmAddress, pubKeys, amounts, signer, }) => __awaiter(void 0, void 0, void 0, function* () {
496
+ var _15;
497
+ try {
498
+ // Convert string amounts to bigint
499
+ const bigintAmounts = amounts.map(amount => BigInt(amount));
500
+ const ovmContract = new Contract(ovmAddress, OVMContract.abi, signer);
501
+ const tx = yield ovmContract.requestWithdrawal(pubKeys, bigintAmounts);
502
+ const receipt = yield tx.wait();
503
+ return { txHash: receipt.hash };
504
+ }
505
+ catch (error) {
506
+ throw new Error(`Failed to request withdrawal from OVM: ${(_15 = error.message) !== null && _15 !== void 0 ? _15 : 'Request withdrawal failed'}`);
507
+ }
508
+ });
@@ -7,9 +7,9 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
7
7
  step((generator = generator.apply(thisArg, _arguments || [])).next());
8
8
  });
9
9
  };
10
- import { formatRecipientsForSplitV2, predictSplitV2Address, isSplitV2Deployed, deployOVMContract, deployOVMAndSplitV2, } from './splitHelpers';
10
+ import { formatRecipientsForSplitV2, predictSplitV2Address, isSplitV2Deployed, deployOVMContract, deployOVMAndSplitV2, requestWithdrawalFromOVM, } from './splitHelpers';
11
11
  import { CHAIN_CONFIGURATION, SPLITS_V2_SALT, DEFAULT_RETROACTIVE_FUNDING_REWARDS_ONLY_SPLIT, isChainSupportedForSplitters, } from '../constants';
12
- import { ovmRewardsSplitPayloadSchema, ovmTotalSplitPayloadSchema, } from '../schema';
12
+ import { ovmRewardsSplitPayloadSchema, ovmTotalSplitPayloadSchema, ovmRequestWithdrawalPayloadSchema, } from '../schema';
13
13
  import { validatePayload } from '../ajv';
14
14
  import { isContractAvailable } from '../utils';
15
15
  /**
@@ -276,4 +276,43 @@ export class ObolSplits {
276
276
  }
277
277
  });
278
278
  }
279
+ /**
280
+ * Requests withdrawal from an OVM contract.
281
+ *
282
+ * This method allows requesting withdrawal of validator funds from an OVM contract.
283
+ * The withdrawal request includes OVM address, validator public keys and corresponding withdrawal amounts.
284
+ *
285
+ * @remarks
286
+ * **⚠️ Important:** If you're storing the private key in an `.env` file, ensure it is securely managed
287
+ * and not pushed to version control.
288
+ *
289
+ * @param {OVMRequestWithdrawalPayload} payload - Data needed to request withdrawal
290
+ * @returns {Promise<{txHash: string}>} Transaction hash of the withdrawal request
291
+ * @throws Will throw an error if the signer is not provided, OVM address is invalid, or the request fails
292
+ *
293
+ * An example of how to use requestWithdrawal:
294
+ * ```typescript
295
+ * const result = await client.splits.requestWithdrawal({
296
+ * ovmAddress: '0x1234567890123456789012345678901234567890',
297
+ * pubKeys: ['0x123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456'],
298
+ * amounts: ['32000000000'] // 32 ETH in gwei
299
+ * });
300
+ * console.log('Withdrawal requested:', result.txHash);
301
+ * ```
302
+ */
303
+ requestWithdrawal(payload) {
304
+ return __awaiter(this, void 0, void 0, function* () {
305
+ if (!this.signer) {
306
+ throw new Error('Signer is required in requestWithdrawal');
307
+ }
308
+ // [TBD] need to move ovm verification to sdk method and use it here
309
+ const validatedPayload = validatePayload(payload, ovmRequestWithdrawalPayloadSchema);
310
+ return yield requestWithdrawalFromOVM({
311
+ ovmAddress: validatedPayload.ovmAddress,
312
+ pubKeys: validatedPayload.pubKeys,
313
+ amounts: validatedPayload.amounts,
314
+ signer: this.signer,
315
+ });
316
+ });
317
+ }
279
318
  }
@@ -1,5 +1,5 @@
1
1
  import { validatePayload } from '../../src/ajv';
2
- import { ovmRewardsSplitPayloadSchema, ovmTotalSplitPayloadSchema, } from '../../src/schema';
2
+ import { ovmRewardsSplitPayloadSchema, ovmTotalSplitPayloadSchema, ovmRequestWithdrawalPayloadSchema, } from '../../src/schema';
3
3
  import { TEST_ADDRESSES } from '../fixtures';
4
4
  describe('validatePayload - OVM Schemas', () => {
5
5
  describe('ovmRewardsSplitPayloadSchema', () => {
@@ -265,3 +265,35 @@ describe('validatePayload - OVM Schemas', () => {
265
265
  });
266
266
  });
267
267
  });
268
+ describe('ovmRequestWithdrawalPayloadSchema', () => {
269
+ const validPayload = {
270
+ ovmAddress: '0x1234567890123456789012345678901234567890',
271
+ pubKeys: [
272
+ '0x123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456',
273
+ ],
274
+ amounts: ['32000000000'],
275
+ };
276
+ it('should throw error when OVM address is missing', () => {
277
+ const payload = {
278
+ pubKeys: validPayload.pubKeys,
279
+ amounts: validPayload.amounts,
280
+ };
281
+ expect(() => validatePayload(payload, ovmRequestWithdrawalPayloadSchema)).toThrow("Validation failed: must have required property 'ovmAddress'");
282
+ });
283
+ it('should throw error when OVM address is invalid', () => {
284
+ const payload = {
285
+ ovmAddress: '0x123', // Too short
286
+ pubKeys: validPayload.pubKeys,
287
+ amounts: validPayload.amounts,
288
+ };
289
+ expect(() => validatePayload(payload, ovmRequestWithdrawalPayloadSchema)).toThrow('Validation failed: /ovmAddress must match pattern');
290
+ });
291
+ it('should throw error when number of public keys does not match number of amounts', () => {
292
+ const payload = {
293
+ ovmAddress: validPayload.ovmAddress,
294
+ pubKeys: validPayload.pubKeys,
295
+ amounts: ['32000000000', '16000000000'], // 2 amounts, 1 pubKey
296
+ };
297
+ expect(() => validatePayload(payload, ovmRequestWithdrawalPayloadSchema)).toThrow('Validation failed: must pass "validateOVMRequestWithdrawalPayload" keyword validation');
298
+ });
299
+ });
@@ -9,7 +9,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
9
9
  };
10
10
  import { Client } from '../../src';
11
11
  import { CHAIN_CONFIGURATION } from '../../src/constants';
12
- import { formatRecipientsForSplitV2, predictSplitV2Address, isSplitV2Deployed, deployOVMAndSplitV2, deployOVMContract, } from '../../src/splits/splitHelpers';
12
+ import { formatRecipientsForSplitV2, predictSplitV2Address, isSplitV2Deployed, deployOVMAndSplitV2, deployOVMContract, requestWithdrawalFromOVM, } from '../../src/splits/splitHelpers';
13
13
  import { isContractAvailable } from '../../src/utils';
14
14
  import { TEST_ADDRESSES } from '../fixtures';
15
15
  // Mock the split helpers
@@ -19,6 +19,7 @@ jest.mock('../../src/splits/splitHelpers', () => ({
19
19
  isSplitV2Deployed: jest.fn(),
20
20
  deployOVMContract: jest.fn(),
21
21
  deployOVMAndSplitV2: jest.fn(),
22
+ requestWithdrawalFromOVM: jest.fn(),
22
23
  }));
23
24
  // Mock the utils
24
25
  jest.mock('../../src/utils', () => ({
@@ -34,6 +35,7 @@ const mockPredictSplitV2Address = predictSplitV2Address;
34
35
  const mockIsSplitV2Deployed = isSplitV2Deployed;
35
36
  const mockDeployOVMAndSplitV2 = deployOVMAndSplitV2;
36
37
  const mockDeployOVMContract = deployOVMContract;
38
+ const mockRequestWithdrawalFromOVM = requestWithdrawalFromOVM;
37
39
  const mockIsContractAvailable = isContractAvailable;
38
40
  describe('ObolSplits', () => {
39
41
  let client;
@@ -42,6 +44,8 @@ describe('ObolSplits', () => {
42
44
  beforeEach(() => {
43
45
  // Clear all mocks before each test
44
46
  jest.clearAllMocks();
47
+ // Reset the mock to not be called by default
48
+ mockRequestWithdrawalFromOVM.mockReset();
45
49
  mockSigner = {
46
50
  getAddress: jest
47
51
  .fn()
@@ -226,6 +230,76 @@ describe('ObolSplits', () => {
226
230
  yield expect(clientUnsupportedChain.splits.createValidatorManagerAndTotalSplit(mockTotalSplitPayload)).rejects.toThrow('Splitter configuration is not supported on 999 chain');
227
231
  }));
228
232
  });
233
+ describe('requestWithdrawal', () => {
234
+ const mockOVMAddress = '0x1234567890123456789012345678901234567890';
235
+ const mockPubKeys = [
236
+ '0x123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456',
237
+ ];
238
+ const mockAmounts = ['32000000000']; // 32 ETH in gwei
239
+ it('should request withdrawal successfully', () => __awaiter(void 0, void 0, void 0, function* () {
240
+ mockRequestWithdrawalFromOVM.mockResolvedValue({
241
+ txHash: '0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890',
242
+ });
243
+ const result = yield client.splits.requestWithdrawal({
244
+ ovmAddress: mockOVMAddress,
245
+ pubKeys: mockPubKeys,
246
+ amounts: mockAmounts,
247
+ });
248
+ expect(result).toEqual({
249
+ txHash: '0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890',
250
+ });
251
+ expect(mockRequestWithdrawalFromOVM).toHaveBeenCalledWith({
252
+ ovmAddress: mockOVMAddress,
253
+ pubKeys: mockPubKeys,
254
+ amounts: mockAmounts,
255
+ signer: mockSigner,
256
+ });
257
+ }));
258
+ it('should throw error when signer is not provided', () => __awaiter(void 0, void 0, void 0, function* () {
259
+ const clientWithoutSigner = new Client({ chainId: 1 }, undefined, mockProvider);
260
+ yield expect(clientWithoutSigner.splits.requestWithdrawal({
261
+ ovmAddress: mockOVMAddress,
262
+ pubKeys: mockPubKeys,
263
+ amounts: mockAmounts,
264
+ })).rejects.toThrow('Signer is required in requestWithdrawal');
265
+ }));
266
+ // it('should throw error when amount is below minimum (1,000,000 gwei)', async () => {
267
+ // await expect(
268
+ // client.splits.requestWithdrawal({
269
+ // ovmAddress: mockOVMAddress,
270
+ // pubKeys: mockPubKeys,
271
+ // amounts: ['500000'], // Below minimum of 1,000,000 gwei
272
+ // }),
273
+ // ).rejects.toThrow('Validation failed');
274
+ // });
275
+ it('should handle multiple validators withdrawal request', () => __awaiter(void 0, void 0, void 0, function* () {
276
+ const multiplePubKeys = [
277
+ '0x123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456',
278
+ '0xabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcd',
279
+ ];
280
+ const multipleAmounts = [
281
+ '32000000000', // 32 ETH
282
+ '16000000000', // 16 ETH
283
+ ];
284
+ mockRequestWithdrawalFromOVM.mockResolvedValue({
285
+ txHash: '0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890',
286
+ });
287
+ const result = yield client.splits.requestWithdrawal({
288
+ ovmAddress: mockOVMAddress,
289
+ pubKeys: multiplePubKeys,
290
+ amounts: multipleAmounts,
291
+ });
292
+ expect(result).toEqual({
293
+ txHash: '0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890',
294
+ });
295
+ expect(mockRequestWithdrawalFromOVM).toHaveBeenCalledWith({
296
+ ovmAddress: mockOVMAddress,
297
+ pubKeys: multiplePubKeys,
298
+ amounts: multipleAmounts,
299
+ signer: mockSigner,
300
+ });
301
+ }));
302
+ });
229
303
  describe('constructor', () => {
230
304
  it('should create Client instance with splits property', () => {
231
305
  const testClient = new Client({ chainId: 1 }, mockSigner, mockProvider);
@@ -321,3 +321,30 @@ export declare const ovmTotalSplitPayloadSchema: {
321
321
  validateOVMTotalSplitRecipients: boolean;
322
322
  required: string[];
323
323
  };
324
+ export declare const ovmRequestWithdrawalPayloadSchema: {
325
+ type: string;
326
+ properties: {
327
+ ovmAddress: {
328
+ type: string;
329
+ pattern: string;
330
+ };
331
+ pubKeys: {
332
+ type: string;
333
+ minItems: number;
334
+ items: {
335
+ type: string;
336
+ pattern: string;
337
+ };
338
+ };
339
+ amounts: {
340
+ type: string;
341
+ minItems: number;
342
+ items: {
343
+ type: string;
344
+ pattern: string;
345
+ };
346
+ };
347
+ };
348
+ validateOVMRequestWithdrawalPayload: boolean;
349
+ required: string[];
350
+ };
@@ -99,4 +99,20 @@ export declare const deployOVMAndSplitV2: ({ ovmArgs, rewardRecipients, isReward
99
99
  isPrincipalSplitDeployed?: boolean | undefined;
100
100
  splitOwnerAddress: string;
101
101
  }) => Promise<string>;
102
+ /**
103
+ * Requests withdrawal from an OVM contract
104
+ * @param ovmAddress - The address of the OVM contract
105
+ * @param pubKeys - Array of validator public keys in bytes format
106
+ * @param amounts - Array of withdrawal amounts in wei (uint64)
107
+ * @param signer - The signer to use for the transaction
108
+ * @returns Promise that resolves to the transaction hash
109
+ */
110
+ export declare const requestWithdrawalFromOVM: ({ ovmAddress, pubKeys, amounts, signer, }: {
111
+ ovmAddress: string;
112
+ pubKeys: string[];
113
+ amounts: string[];
114
+ signer: SignerType;
115
+ }) => Promise<{
116
+ txHash: string;
117
+ }>;
102
118
  export {};
@@ -1,4 +1,4 @@
1
- import { type ClusterValidator, type ProviderType, type SignerType, type OVMRewardsSplitPayload, type OVMTotalSplitPayload } from '../types';
1
+ import { type ClusterValidator, type ProviderType, type SignerType, type OVMRewardsSplitPayload, type OVMTotalSplitPayload, type OVMRequestWithdrawalPayload } from '../types';
2
2
  /**
3
3
  * ObolSplits can be used for creating and managing Obol splits.
4
4
  * @class
@@ -48,4 +48,31 @@ export declare class ObolSplits {
48
48
  * [createValidatorManagerAndTotalSplit](https://github.com/ObolNetwork/obol-sdk-examples/blob/main/TS-Example/index.ts#340)
49
49
  */
50
50
  createValidatorManagerAndTotalSplit(payload: OVMTotalSplitPayload): Promise<ClusterValidator>;
51
+ /**
52
+ * Requests withdrawal from an OVM contract.
53
+ *
54
+ * This method allows requesting withdrawal of validator funds from an OVM contract.
55
+ * The withdrawal request includes OVM address, validator public keys and corresponding withdrawal amounts.
56
+ *
57
+ * @remarks
58
+ * **⚠️ Important:** If you're storing the private key in an `.env` file, ensure it is securely managed
59
+ * and not pushed to version control.
60
+ *
61
+ * @param {OVMRequestWithdrawalPayload} payload - Data needed to request withdrawal
62
+ * @returns {Promise<{txHash: string}>} Transaction hash of the withdrawal request
63
+ * @throws Will throw an error if the signer is not provided, OVM address is invalid, or the request fails
64
+ *
65
+ * An example of how to use requestWithdrawal:
66
+ * ```typescript
67
+ * const result = await client.splits.requestWithdrawal({
68
+ * ovmAddress: '0x1234567890123456789012345678901234567890',
69
+ * pubKeys: ['0x123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456'],
70
+ * amounts: ['32000000000'] // 32 ETH in gwei
71
+ * });
72
+ * console.log('Withdrawal requested:', result.txHash);
73
+ * ```
74
+ */
75
+ requestWithdrawal(payload: OVMRequestWithdrawalPayload): Promise<{
76
+ txHash: string;
77
+ }>;
51
78
  }
@@ -472,3 +472,14 @@ export type ChainConfig = {
472
472
  bytecode: string;
473
473
  };
474
474
  };
475
+ /**
476
+ * Payload for requesting withdrawal from OVM contract
477
+ */
478
+ export type OVMRequestWithdrawalPayload = {
479
+ /** OVM contract address */
480
+ ovmAddress: string;
481
+ /** Array of validator public keys in bytes format */
482
+ pubKeys: string[];
483
+ /** Array of withdrawal amounts in gwei (uint64) as strings */
484
+ amounts: string[];
485
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@obolnetwork/obol-sdk",
3
- "version": "2.8.1",
3
+ "version": "2.9.0",
4
4
  "description": "A package for creating Distributed Validators using the Obol API.",
5
5
  "bugs": {
6
6
  "url": "https://github.com/obolnetwork/obol-sdk/issues"
package/src/ajv.ts CHANGED
@@ -6,6 +6,7 @@ import {
6
6
  type OVMTotalSplitPayload,
7
7
  type RewardsSplitPayload,
8
8
  type TotalSplitPayload,
9
+ type OVMRequestWithdrawalPayload,
9
10
  } from './types';
10
11
  import Ajv from 'ajv';
11
12
  import {
@@ -107,6 +108,32 @@ const validateOVMTotalSplitRecipients = (
107
108
  return validateTotalPercentage(splitPercentage);
108
109
  };
109
110
 
111
+ const validateOVMRequestWithdrawalPayload = (
112
+ _: boolean,
113
+ data: OVMRequestWithdrawalPayload,
114
+ ): boolean => {
115
+ console.log(data, 'hanaaan dataaaa');
116
+ if (!data.pubKeys || !data.amounts) {
117
+ return false;
118
+ }
119
+
120
+ if (data.pubKeys.length !== data.amounts.length) {
121
+ return false;
122
+ }
123
+
124
+ // // Validate that all amounts are at least 1,000,000 gwei
125
+ // const minAmount = BigInt(1000000);
126
+ // for (const amountStr of data.amounts) {
127
+ // const minAmount = BigInt(1000000);
128
+ // const amount = BigInt(amountStr);
129
+ // if (amount < minAmount) {
130
+ // return false;
131
+ // }
132
+ // }
133
+
134
+ return true;
135
+ };
136
+
110
137
  const ajv = new Ajv({
111
138
  allErrors: true,
112
139
  useDefaults: true,
@@ -146,6 +173,12 @@ ajv.addKeyword({
146
173
  schemaType: 'boolean',
147
174
  });
148
175
 
176
+ ajv.addKeyword({
177
+ keyword: 'validateOVMRequestWithdrawalPayload',
178
+ validate: validateOVMRequestWithdrawalPayload,
179
+ schemaType: 'boolean',
180
+ });
181
+
149
182
  export function validatePayload<T>(data: unknown, schema: object): T {
150
183
  const validate = ajv.compile<T>(schema);
151
184
  const valid = validate(data);
package/src/schema.ts CHANGED
@@ -229,3 +229,31 @@ export const ovmTotalSplitPayloadSchema = {
229
229
  'OVMOwnerAddress',
230
230
  ],
231
231
  };
232
+
233
+ export const ovmRequestWithdrawalPayloadSchema = {
234
+ type: 'object',
235
+ properties: {
236
+ ovmAddress: {
237
+ type: 'string',
238
+ pattern: '^0x[a-fA-F0-9]{40}$',
239
+ },
240
+ pubKeys: {
241
+ type: 'array',
242
+ minItems: 1,
243
+ items: {
244
+ type: 'string',
245
+ pattern: '^0x[a-fA-F0-9]{96}$', // 48 bytes = 96 hex chars + 0x prefix
246
+ },
247
+ },
248
+ amounts: {
249
+ type: 'array',
250
+ minItems: 1,
251
+ items: {
252
+ type: 'string',
253
+ pattern: '^[0-9]+$',
254
+ },
255
+ },
256
+ },
257
+ validateOVMRequestWithdrawalPayload: true,
258
+ required: ['ovmAddress', 'pubKeys', 'amounts'],
259
+ };
@@ -10,7 +10,7 @@ import {
10
10
  } from '../types';
11
11
  import { Contract, Interface, parseEther, ZeroAddress } from 'ethers';
12
12
  import { OWRContract, OWRFactoryContract } from '../abi/OWR';
13
- import { OVMFactoryContract } from '../abi/OVMFactory';
13
+ import { OVMFactoryContract, OVMContract } from '../abi/OVMFactory';
14
14
  import { splitMainEthereumAbi } from '../abi/SplitMain';
15
15
  import { MultiCallContract } from '../abi/Multicall';
16
16
  import { CHAIN_CONFIGURATION } from '../constants';
@@ -910,3 +910,39 @@ const getChainConfig = (chainId: number): ChainConfig => {
910
910
  }
911
911
  return config;
912
912
  };
913
+
914
+ /**
915
+ * Requests withdrawal from an OVM contract
916
+ * @param ovmAddress - The address of the OVM contract
917
+ * @param pubKeys - Array of validator public keys in bytes format
918
+ * @param amounts - Array of withdrawal amounts in wei (uint64)
919
+ * @param signer - The signer to use for the transaction
920
+ * @returns Promise that resolves to the transaction hash
921
+ */
922
+ export const requestWithdrawalFromOVM = async ({
923
+ ovmAddress,
924
+ pubKeys,
925
+ amounts,
926
+ signer,
927
+ }: {
928
+ ovmAddress: string;
929
+ pubKeys: string[];
930
+ amounts: string[];
931
+ signer: SignerType;
932
+ }): Promise<{ txHash: string }> => {
933
+ try {
934
+ // Convert string amounts to bigint
935
+ const bigintAmounts = amounts.map(amount => BigInt(amount));
936
+
937
+ const ovmContract = new Contract(ovmAddress, OVMContract.abi, signer);
938
+
939
+ const tx = await ovmContract.requestWithdrawal(pubKeys, bigintAmounts);
940
+ const receipt = await tx.wait();
941
+
942
+ return { txHash: receipt.hash };
943
+ } catch (error: any) {
944
+ throw new Error(
945
+ `Failed to request withdrawal from OVM: ${error.message ?? 'Request withdrawal failed'}`,
946
+ );
947
+ }
948
+ };
@@ -4,6 +4,7 @@ import {
4
4
  isSplitV2Deployed,
5
5
  deployOVMContract,
6
6
  deployOVMAndSplitV2,
7
+ requestWithdrawalFromOVM,
7
8
  } from './splitHelpers';
8
9
  import {
9
10
  CHAIN_CONFIGURATION,
@@ -14,6 +15,7 @@ import {
14
15
  import {
15
16
  ovmRewardsSplitPayloadSchema,
16
17
  ovmTotalSplitPayloadSchema,
18
+ ovmRequestWithdrawalPayloadSchema,
17
19
  } from '../schema';
18
20
  import { validatePayload } from '../ajv';
19
21
  import { isContractAvailable } from '../utils';
@@ -23,6 +25,7 @@ import {
23
25
  type SignerType,
24
26
  type OVMRewardsSplitPayload,
25
27
  type OVMTotalSplitPayload,
28
+ type OVMRequestWithdrawalPayload,
26
29
  } from '../types';
27
30
 
28
31
  /**
@@ -403,4 +406,48 @@ export class ObolSplits {
403
406
  };
404
407
  }
405
408
  }
409
+
410
+ /**
411
+ * Requests withdrawal from an OVM contract.
412
+ *
413
+ * This method allows requesting withdrawal of validator funds from an OVM contract.
414
+ * The withdrawal request includes OVM address, validator public keys and corresponding withdrawal amounts.
415
+ *
416
+ * @remarks
417
+ * **⚠️ Important:** If you're storing the private key in an `.env` file, ensure it is securely managed
418
+ * and not pushed to version control.
419
+ *
420
+ * @param {OVMRequestWithdrawalPayload} payload - Data needed to request withdrawal
421
+ * @returns {Promise<{txHash: string}>} Transaction hash of the withdrawal request
422
+ * @throws Will throw an error if the signer is not provided, OVM address is invalid, or the request fails
423
+ *
424
+ * An example of how to use requestWithdrawal:
425
+ * ```typescript
426
+ * const result = await client.splits.requestWithdrawal({
427
+ * ovmAddress: '0x1234567890123456789012345678901234567890',
428
+ * pubKeys: ['0x123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456'],
429
+ * amounts: ['32000000000'] // 32 ETH in gwei
430
+ * });
431
+ * console.log('Withdrawal requested:', result.txHash);
432
+ * ```
433
+ */
434
+ async requestWithdrawal(
435
+ payload: OVMRequestWithdrawalPayload,
436
+ ): Promise<{ txHash: string }> {
437
+ if (!this.signer) {
438
+ throw new Error('Signer is required in requestWithdrawal');
439
+ }
440
+ // [TBD] need to move ovm verification to sdk method and use it here
441
+ const validatedPayload = validatePayload<OVMRequestWithdrawalPayload>(
442
+ payload,
443
+ ovmRequestWithdrawalPayloadSchema,
444
+ );
445
+
446
+ return await requestWithdrawalFromOVM({
447
+ ovmAddress: validatedPayload.ovmAddress,
448
+ pubKeys: validatedPayload.pubKeys,
449
+ amounts: validatedPayload.amounts,
450
+ signer: this.signer,
451
+ });
452
+ }
406
453
  }
package/src/types.ts CHANGED
@@ -607,3 +607,17 @@ export type ChainConfig = {
607
607
  bytecode: string;
608
608
  };
609
609
  };
610
+
611
+ /**
612
+ * Payload for requesting withdrawal from OVM contract
613
+ */
614
+ export type OVMRequestWithdrawalPayload = {
615
+ /** OVM contract address */
616
+ ovmAddress: string;
617
+
618
+ /** Array of validator public keys in bytes format */
619
+ pubKeys: string[];
620
+
621
+ /** Array of withdrawal amounts in gwei (uint64) as strings */
622
+ amounts: string[];
623
+ };