@obolnetwork/obol-sdk 2.8.1 → 2.9.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@obolnetwork/obol-sdk",
3
- "version": "2.8.1",
3
+ "version": "2.9.1",
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,24 @@ const validateOVMTotalSplitRecipients = (_, data) => {
67
67
  const splitPercentage = calculateTotalPercentage(data.principalSplitRecipients);
68
68
  return validateTotalPercentage(splitPercentage);
69
69
  };
70
+ const validateOVMRequestWithdrawalPayload = (_, data) => {
71
+ if (!data.pubKeys || !data.amounts) {
72
+ return false;
73
+ }
74
+ if (data.pubKeys.length !== data.amounts.length) {
75
+ return false;
76
+ }
77
+ // // Validate that all amounts are at least 1,000,000 gwei
78
+ // const minAmount = BigInt(1000000);
79
+ // for (const amountStr of data.amounts) {
80
+ // const minAmount = BigInt(1000000);
81
+ // const amount = BigInt(amountStr);
82
+ // if (amount < minAmount) {
83
+ // return false;
84
+ // }
85
+ // }
86
+ return true;
87
+ };
70
88
  const ajv = new ajv_1.default({
71
89
  allErrors: true,
72
90
  useDefaults: true,
@@ -100,6 +118,11 @@ ajv.addKeyword({
100
118
  validate: validateOVMTotalSplitRecipients,
101
119
  schemaType: 'boolean',
102
120
  });
121
+ ajv.addKeyword({
122
+ keyword: 'validateOVMRequestWithdrawalPayload',
123
+ validate: validateOVMRequestWithdrawalPayload,
124
+ schemaType: 'boolean',
125
+ });
103
126
  function validatePayload(data, schema) {
104
127
  var _a;
105
128
  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,34 @@ exports.ovmTotalSplitPayloadSchema = {
209
209
  'OVMOwnerAddress',
210
210
  ],
211
211
  };
212
+ exports.ovmRequestWithdrawalPayloadSchema = {
213
+ type: 'object',
214
+ properties: {
215
+ withdrawalFees: {
216
+ type: 'string',
217
+ pattern: '^[0-9]+$',
218
+ },
219
+ ovmAddress: {
220
+ type: 'string',
221
+ pattern: '^0x[a-fA-F0-9]{40}$',
222
+ },
223
+ pubKeys: {
224
+ type: 'array',
225
+ minItems: 1,
226
+ items: {
227
+ type: 'string',
228
+ pattern: '^0x[a-fA-F0-9]{96}$', // 48 bytes = 96 hex chars + 0x prefix
229
+ },
230
+ },
231
+ amounts: {
232
+ type: 'array',
233
+ minItems: 1,
234
+ items: {
235
+ type: 'string',
236
+ pattern: '^[0-9]+$',
237
+ },
238
+ },
239
+ },
240
+ validateOVMRequestWithdrawalPayload: true,
241
+ required: ['ovmAddress', 'pubKeys', 'amounts', 'withdrawalFees'],
242
+ };
@@ -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,28 @@ 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, withdrawalFees, 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
+ value: BigInt(withdrawalFees),
518
+ });
519
+ const receipt = yield tx.wait();
520
+ return { txHash: receipt.hash };
521
+ }
522
+ catch (error) {
523
+ throw new Error(`Failed to request withdrawal from OVM: ${(_15 = error.message) !== null && _15 !== void 0 ? _15 : 'Request withdrawal failed'}`);
524
+ }
525
+ });
526
+ exports.requestWithdrawalFromOVM = requestWithdrawalFromOVM;
@@ -279,5 +279,45 @@ 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
+ withdrawalFees: validatedPayload.withdrawalFees,
318
+ signer: this.signer,
319
+ });
320
+ });
321
+ }
282
322
  }
283
323
  exports.ObolSplits = ObolSplits;
@@ -267,3 +267,54 @@ 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
+ withdrawalFees: '1',
278
+ };
279
+ it('should throw error when OVM address is missing', () => {
280
+ const payload = {
281
+ pubKeys: validPayload.pubKeys,
282
+ amounts: validPayload.amounts,
283
+ };
284
+ expect(() => (0, ajv_1.validatePayload)(payload, schema_1.ovmRequestWithdrawalPayloadSchema)).toThrow("Validation failed: must have required property 'ovmAddress'");
285
+ });
286
+ it('should throw error when OVM address is invalid', () => {
287
+ const payload = {
288
+ ovmAddress: '0x123', // Too short
289
+ pubKeys: validPayload.pubKeys,
290
+ amounts: validPayload.amounts,
291
+ };
292
+ expect(() => (0, ajv_1.validatePayload)(payload, schema_1.ovmRequestWithdrawalPayloadSchema)).toThrow('Validation failed: must have required property \'withdrawalFees\', /ovmAddress must match pattern "^0x[a-fA-F0-9]{40}$"');
293
+ });
294
+ it('should throw error when number of public keys does not match number of amounts', () => {
295
+ const payload = {
296
+ ovmAddress: validPayload.ovmAddress,
297
+ pubKeys: validPayload.pubKeys,
298
+ amounts: ['32000000000', '16000000000'], // 2 amounts, 1 pubKey
299
+ withdrawalFees: validPayload.withdrawalFees,
300
+ };
301
+ expect(() => (0, ajv_1.validatePayload)(payload, schema_1.ovmRequestWithdrawalPayloadSchema)).toThrow('Validation failed: must pass "validateOVMRequestWithdrawalPayload" keyword validation');
302
+ });
303
+ it('should throw error when withdrawalFees is missing', () => {
304
+ const payload = {
305
+ ovmAddress: validPayload.ovmAddress,
306
+ pubKeys: validPayload.pubKeys,
307
+ amounts: validPayload.amounts,
308
+ };
309
+ expect(() => (0, ajv_1.validatePayload)(payload, schema_1.ovmRequestWithdrawalPayloadSchema)).toThrow("Validation failed: must have required property 'withdrawalFees'");
310
+ });
311
+ it('should throw error when withdrawalFees is invalid', () => {
312
+ const payload = {
313
+ ovmAddress: validPayload.ovmAddress,
314
+ pubKeys: validPayload.pubKeys,
315
+ amounts: validPayload.amounts,
316
+ withdrawalFees: 'invalid',
317
+ };
318
+ expect(() => (0, ajv_1.validatePayload)(payload, schema_1.ovmRequestWithdrawalPayloadSchema)).toThrow('Validation failed: /withdrawalFees must match pattern "^[0-9]+$"');
319
+ });
320
+ });
@@ -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,81 @@ 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
+ withdrawalFees: '1',
250
+ });
251
+ expect(result).toEqual({
252
+ txHash: '0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890',
253
+ });
254
+ expect(mockRequestWithdrawalFromOVM).toHaveBeenCalledWith({
255
+ ovmAddress: mockOVMAddress,
256
+ pubKeys: mockPubKeys,
257
+ amounts: mockAmounts,
258
+ withdrawalFees: '1',
259
+ signer: mockSigner,
260
+ });
261
+ }));
262
+ it('should throw error when signer is not provided', () => __awaiter(void 0, void 0, void 0, function* () {
263
+ const clientWithoutSigner = new src_1.Client({ chainId: 1 }, undefined, mockProvider);
264
+ yield expect(clientWithoutSigner.splits.requestWithdrawal({
265
+ ovmAddress: mockOVMAddress,
266
+ pubKeys: mockPubKeys,
267
+ amounts: mockAmounts,
268
+ withdrawalFees: '1',
269
+ })).rejects.toThrow('Signer is required in requestWithdrawal');
270
+ }));
271
+ // it('should throw error when amount is below minimum (1,000,000 gwei)', async () => {
272
+ // await expect(
273
+ // client.splits.requestWithdrawal({
274
+ // ovmAddress: mockOVMAddress,
275
+ // pubKeys: mockPubKeys,
276
+ // amounts: ['500000'], // Below minimum of 1,000,000 gwei
277
+ // }),
278
+ // ).rejects.toThrow('Validation failed');
279
+ // });
280
+ it('should handle multiple validators withdrawal request', () => __awaiter(void 0, void 0, void 0, function* () {
281
+ const multiplePubKeys = [
282
+ '0x123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456',
283
+ '0xabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcd',
284
+ ];
285
+ const multipleAmounts = [
286
+ '32000000000', // 32 ETH
287
+ '16000000000', // 16 ETH
288
+ ];
289
+ mockRequestWithdrawalFromOVM.mockResolvedValue({
290
+ txHash: '0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890',
291
+ });
292
+ const result = yield client.splits.requestWithdrawal({
293
+ ovmAddress: mockOVMAddress,
294
+ pubKeys: multiplePubKeys,
295
+ amounts: multipleAmounts,
296
+ withdrawalFees: '1',
297
+ });
298
+ expect(result).toEqual({
299
+ txHash: '0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890',
300
+ });
301
+ expect(mockRequestWithdrawalFromOVM).toHaveBeenCalledWith({
302
+ ovmAddress: mockOVMAddress,
303
+ pubKeys: multiplePubKeys,
304
+ amounts: multipleAmounts,
305
+ withdrawalFees: '1',
306
+ signer: mockSigner,
307
+ });
308
+ }));
309
+ });
231
310
  describe('constructor', () => {
232
311
  it('should create Client instance with splits property', () => {
233
312
  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.1",
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,24 @@ const validateOVMTotalSplitRecipients = (_, data) => {
61
61
  const splitPercentage = calculateTotalPercentage(data.principalSplitRecipients);
62
62
  return validateTotalPercentage(splitPercentage);
63
63
  };
64
+ const validateOVMRequestWithdrawalPayload = (_, data) => {
65
+ if (!data.pubKeys || !data.amounts) {
66
+ return false;
67
+ }
68
+ if (data.pubKeys.length !== data.amounts.length) {
69
+ return false;
70
+ }
71
+ // // Validate that all amounts are at least 1,000,000 gwei
72
+ // const minAmount = BigInt(1000000);
73
+ // for (const amountStr of data.amounts) {
74
+ // const minAmount = BigInt(1000000);
75
+ // const amount = BigInt(amountStr);
76
+ // if (amount < minAmount) {
77
+ // return false;
78
+ // }
79
+ // }
80
+ return true;
81
+ };
64
82
  const ajv = new Ajv({
65
83
  allErrors: true,
66
84
  useDefaults: true,
@@ -94,6 +112,11 @@ ajv.addKeyword({
94
112
  validate: validateOVMTotalSplitRecipients,
95
113
  schemaType: 'boolean',
96
114
  });
115
+ ajv.addKeyword({
116
+ keyword: 'validateOVMRequestWithdrawalPayload',
117
+ validate: validateOVMRequestWithdrawalPayload,
118
+ schemaType: 'boolean',
119
+ });
97
120
  export function validatePayload(data, schema) {
98
121
  var _a;
99
122
  const validate = ajv.compile(schema);
@@ -206,3 +206,34 @@ export const ovmTotalSplitPayloadSchema = {
206
206
  'OVMOwnerAddress',
207
207
  ],
208
208
  };
209
+ export const ovmRequestWithdrawalPayloadSchema = {
210
+ type: 'object',
211
+ properties: {
212
+ withdrawalFees: {
213
+ type: 'string',
214
+ pattern: '^[0-9]+$',
215
+ },
216
+ ovmAddress: {
217
+ type: 'string',
218
+ pattern: '^0x[a-fA-F0-9]{40}$',
219
+ },
220
+ pubKeys: {
221
+ type: 'array',
222
+ minItems: 1,
223
+ items: {
224
+ type: 'string',
225
+ pattern: '^0x[a-fA-F0-9]{96}$', // 48 bytes = 96 hex chars + 0x prefix
226
+ },
227
+ },
228
+ amounts: {
229
+ type: 'array',
230
+ minItems: 1,
231
+ items: {
232
+ type: 'string',
233
+ pattern: '^[0-9]+$',
234
+ },
235
+ },
236
+ },
237
+ validateOVMRequestWithdrawalPayload: true,
238
+ required: ['ovmAddress', 'pubKeys', 'amounts', 'withdrawalFees'],
239
+ };
@@ -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,27 @@ 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, withdrawalFees, 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
+ value: BigInt(withdrawalFees),
503
+ });
504
+ const receipt = yield tx.wait();
505
+ return { txHash: receipt.hash };
506
+ }
507
+ catch (error) {
508
+ throw new Error(`Failed to request withdrawal from OVM: ${(_15 = error.message) !== null && _15 !== void 0 ? _15 : 'Request withdrawal failed'}`);
509
+ }
510
+ });
@@ -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,44 @@ 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
+ withdrawalFees: validatedPayload.withdrawalFees,
315
+ signer: this.signer,
316
+ });
317
+ });
318
+ }
279
319
  }
@@ -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,54 @@ 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
+ withdrawalFees: '1',
276
+ };
277
+ it('should throw error when OVM address is missing', () => {
278
+ const payload = {
279
+ pubKeys: validPayload.pubKeys,
280
+ amounts: validPayload.amounts,
281
+ };
282
+ expect(() => validatePayload(payload, ovmRequestWithdrawalPayloadSchema)).toThrow("Validation failed: must have required property 'ovmAddress'");
283
+ });
284
+ it('should throw error when OVM address is invalid', () => {
285
+ const payload = {
286
+ ovmAddress: '0x123', // Too short
287
+ pubKeys: validPayload.pubKeys,
288
+ amounts: validPayload.amounts,
289
+ };
290
+ expect(() => validatePayload(payload, ovmRequestWithdrawalPayloadSchema)).toThrow('Validation failed: must have required property \'withdrawalFees\', /ovmAddress must match pattern "^0x[a-fA-F0-9]{40}$"');
291
+ });
292
+ it('should throw error when number of public keys does not match number of amounts', () => {
293
+ const payload = {
294
+ ovmAddress: validPayload.ovmAddress,
295
+ pubKeys: validPayload.pubKeys,
296
+ amounts: ['32000000000', '16000000000'], // 2 amounts, 1 pubKey
297
+ withdrawalFees: validPayload.withdrawalFees,
298
+ };
299
+ expect(() => validatePayload(payload, ovmRequestWithdrawalPayloadSchema)).toThrow('Validation failed: must pass "validateOVMRequestWithdrawalPayload" keyword validation');
300
+ });
301
+ it('should throw error when withdrawalFees is missing', () => {
302
+ const payload = {
303
+ ovmAddress: validPayload.ovmAddress,
304
+ pubKeys: validPayload.pubKeys,
305
+ amounts: validPayload.amounts,
306
+ };
307
+ expect(() => validatePayload(payload, ovmRequestWithdrawalPayloadSchema)).toThrow("Validation failed: must have required property 'withdrawalFees'");
308
+ });
309
+ it('should throw error when withdrawalFees is invalid', () => {
310
+ const payload = {
311
+ ovmAddress: validPayload.ovmAddress,
312
+ pubKeys: validPayload.pubKeys,
313
+ amounts: validPayload.amounts,
314
+ withdrawalFees: 'invalid',
315
+ };
316
+ expect(() => validatePayload(payload, ovmRequestWithdrawalPayloadSchema)).toThrow('Validation failed: /withdrawalFees must match pattern "^[0-9]+$"');
317
+ });
318
+ });
@@ -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,81 @@ 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
+ withdrawalFees: '1',
248
+ });
249
+ expect(result).toEqual({
250
+ txHash: '0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890',
251
+ });
252
+ expect(mockRequestWithdrawalFromOVM).toHaveBeenCalledWith({
253
+ ovmAddress: mockOVMAddress,
254
+ pubKeys: mockPubKeys,
255
+ amounts: mockAmounts,
256
+ withdrawalFees: '1',
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 Client({ chainId: 1 }, undefined, mockProvider);
262
+ yield expect(clientWithoutSigner.splits.requestWithdrawal({
263
+ ovmAddress: mockOVMAddress,
264
+ pubKeys: mockPubKeys,
265
+ amounts: mockAmounts,
266
+ withdrawalFees: '1',
267
+ })).rejects.toThrow('Signer is required in requestWithdrawal');
268
+ }));
269
+ // it('should throw error when amount is below minimum (1,000,000 gwei)', async () => {
270
+ // await expect(
271
+ // client.splits.requestWithdrawal({
272
+ // ovmAddress: mockOVMAddress,
273
+ // pubKeys: mockPubKeys,
274
+ // amounts: ['500000'], // Below minimum of 1,000,000 gwei
275
+ // }),
276
+ // ).rejects.toThrow('Validation failed');
277
+ // });
278
+ it('should handle multiple validators withdrawal request', () => __awaiter(void 0, void 0, void 0, function* () {
279
+ const multiplePubKeys = [
280
+ '0x123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456',
281
+ '0xabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcd',
282
+ ];
283
+ const multipleAmounts = [
284
+ '32000000000', // 32 ETH
285
+ '16000000000', // 16 ETH
286
+ ];
287
+ mockRequestWithdrawalFromOVM.mockResolvedValue({
288
+ txHash: '0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890',
289
+ });
290
+ const result = yield client.splits.requestWithdrawal({
291
+ ovmAddress: mockOVMAddress,
292
+ pubKeys: multiplePubKeys,
293
+ amounts: multipleAmounts,
294
+ withdrawalFees: '1',
295
+ });
296
+ expect(result).toEqual({
297
+ txHash: '0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890',
298
+ });
299
+ expect(mockRequestWithdrawalFromOVM).toHaveBeenCalledWith({
300
+ ovmAddress: mockOVMAddress,
301
+ pubKeys: multiplePubKeys,
302
+ amounts: multipleAmounts,
303
+ withdrawalFees: '1',
304
+ signer: mockSigner,
305
+ });
306
+ }));
307
+ });
229
308
  describe('constructor', () => {
230
309
  it('should create Client instance with splits property', () => {
231
310
  const testClient = new Client({ chainId: 1 }, mockSigner, mockProvider);
@@ -321,3 +321,34 @@ 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
+ withdrawalFees: {
328
+ type: string;
329
+ pattern: string;
330
+ };
331
+ ovmAddress: {
332
+ type: string;
333
+ pattern: string;
334
+ };
335
+ pubKeys: {
336
+ type: string;
337
+ minItems: number;
338
+ items: {
339
+ type: string;
340
+ pattern: string;
341
+ };
342
+ };
343
+ amounts: {
344
+ type: string;
345
+ minItems: number;
346
+ items: {
347
+ type: string;
348
+ pattern: string;
349
+ };
350
+ };
351
+ };
352
+ validateOVMRequestWithdrawalPayload: boolean;
353
+ required: string[];
354
+ };
@@ -99,4 +99,21 @@ 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, withdrawalFees, signer, }: {
111
+ ovmAddress: string;
112
+ pubKeys: string[];
113
+ amounts: string[];
114
+ withdrawalFees: string;
115
+ signer: SignerType;
116
+ }) => Promise<{
117
+ txHash: string;
118
+ }>;
102
119
  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,16 @@ 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
+ /** request withdrawal fees in wei */
480
+ withdrawalFees: string;
481
+ /** OVM contract address */
482
+ ovmAddress: string;
483
+ /** Array of validator public keys in bytes format */
484
+ pubKeys: string[];
485
+ /** Array of withdrawal amounts in gwei (uint64) as strings */
486
+ amounts: string[];
487
+ };
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.1",
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,31 @@ const validateOVMTotalSplitRecipients = (
107
108
  return validateTotalPercentage(splitPercentage);
108
109
  };
109
110
 
111
+ const validateOVMRequestWithdrawalPayload = (
112
+ _: boolean,
113
+ data: OVMRequestWithdrawalPayload,
114
+ ): boolean => {
115
+ if (!data.pubKeys || !data.amounts) {
116
+ return false;
117
+ }
118
+
119
+ if (data.pubKeys.length !== data.amounts.length) {
120
+ return false;
121
+ }
122
+
123
+ // // Validate that all amounts are at least 1,000,000 gwei
124
+ // const minAmount = BigInt(1000000);
125
+ // for (const amountStr of data.amounts) {
126
+ // const minAmount = BigInt(1000000);
127
+ // const amount = BigInt(amountStr);
128
+ // if (amount < minAmount) {
129
+ // return false;
130
+ // }
131
+ // }
132
+
133
+ return true;
134
+ };
135
+
110
136
  const ajv = new Ajv({
111
137
  allErrors: true,
112
138
  useDefaults: true,
@@ -146,6 +172,12 @@ ajv.addKeyword({
146
172
  schemaType: 'boolean',
147
173
  });
148
174
 
175
+ ajv.addKeyword({
176
+ keyword: 'validateOVMRequestWithdrawalPayload',
177
+ validate: validateOVMRequestWithdrawalPayload,
178
+ schemaType: 'boolean',
179
+ });
180
+
149
181
  export function validatePayload<T>(data: unknown, schema: object): T {
150
182
  const validate = ajv.compile<T>(schema);
151
183
  const valid = validate(data);
package/src/schema.ts CHANGED
@@ -229,3 +229,35 @@ export const ovmTotalSplitPayloadSchema = {
229
229
  'OVMOwnerAddress',
230
230
  ],
231
231
  };
232
+
233
+ export const ovmRequestWithdrawalPayloadSchema = {
234
+ type: 'object',
235
+ properties: {
236
+ withdrawalFees: {
237
+ type: 'string',
238
+ pattern: '^[0-9]+$',
239
+ },
240
+ ovmAddress: {
241
+ type: 'string',
242
+ pattern: '^0x[a-fA-F0-9]{40}$',
243
+ },
244
+ pubKeys: {
245
+ type: 'array',
246
+ minItems: 1,
247
+ items: {
248
+ type: 'string',
249
+ pattern: '^0x[a-fA-F0-9]{96}$', // 48 bytes = 96 hex chars + 0x prefix
250
+ },
251
+ },
252
+ amounts: {
253
+ type: 'array',
254
+ minItems: 1,
255
+ items: {
256
+ type: 'string',
257
+ pattern: '^[0-9]+$',
258
+ },
259
+ },
260
+ },
261
+ validateOVMRequestWithdrawalPayload: true,
262
+ required: ['ovmAddress', 'pubKeys', 'amounts', 'withdrawalFees'],
263
+ };
@@ -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,43 @@ 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
+ withdrawalFees,
927
+ signer,
928
+ }: {
929
+ ovmAddress: string;
930
+ pubKeys: string[];
931
+ amounts: string[];
932
+ withdrawalFees: string;
933
+ signer: SignerType;
934
+ }): Promise<{ txHash: string }> => {
935
+ try {
936
+ // Convert string amounts to bigint
937
+ const bigintAmounts = amounts.map(amount => BigInt(amount));
938
+
939
+ const ovmContract = new Contract(ovmAddress, OVMContract.abi, signer);
940
+
941
+ const tx = await ovmContract.requestWithdrawal(pubKeys, bigintAmounts, {
942
+ value: BigInt(withdrawalFees),
943
+ });
944
+ const receipt = await tx.wait();
945
+
946
+ return { txHash: receipt.hash };
947
+ } catch (error: any) {
948
+ throw new Error(
949
+ `Failed to request withdrawal from OVM: ${error.message ?? 'Request withdrawal failed'}`,
950
+ );
951
+ }
952
+ };
@@ -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,49 @@ 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
+ withdrawalFees: validatedPayload.withdrawalFees,
451
+ signer: this.signer,
452
+ });
453
+ }
406
454
  }
package/src/types.ts CHANGED
@@ -607,3 +607,20 @@ 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
+ /** request withdrawal fees in wei */
616
+ withdrawalFees: string;
617
+
618
+ /** OVM contract address */
619
+ ovmAddress: string;
620
+
621
+ /** Array of validator public keys in bytes format */
622
+ pubKeys: string[];
623
+
624
+ /** Array of withdrawal amounts in gwei (uint64) as strings */
625
+ amounts: string[];
626
+ };