@obolnetwork/obol-sdk 2.9.1 → 2.10.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.9.1",
3
+ "version": "2.10.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"
@@ -171,6 +171,9 @@ exports.CHAIN_CONFIGURATION = {
171
171
  address: '0x5cbA88D55Cec83caD5A105Ad40C8c9aF20bE21d1',
172
172
  bytecode: bytecodes_1.MAINNET_SPLIT_V2_FACTORY_BYTECODE,
173
173
  },
174
+ EOA_WITHDRAWAL_CONTRACT: {
175
+ address: '0x00000961Ef480Eb55e80D19ad83579A64c007002',
176
+ },
174
177
  },
175
178
  [types_1.FORK_MAPPING['0x01017000']]: {
176
179
  SPLITMAIN_CONTRACT: {
@@ -189,6 +192,9 @@ exports.CHAIN_CONFIGURATION = {
189
192
  address: '0x43F641fA70e09f0326ac66b4Ef0C416EaEcBC6f5',
190
193
  bytecode: '',
191
194
  },
195
+ EOA_WITHDRAWAL_CONTRACT: {
196
+ address: '0x00000961Ef480Eb55e80D19ad83579A64c007002',
197
+ },
192
198
  },
193
199
  [types_1.FORK_MAPPING['0x10000910']]: {
194
200
  SPLITMAIN_CONTRACT: {
@@ -220,6 +226,9 @@ exports.CHAIN_CONFIGURATION = {
220
226
  address: '0x5cbA88D55Cec83caD5A105Ad40C8c9aF20bE21d1',
221
227
  bytecode: bytecodes_1.HOODI_SPLIT_V2_FACTORY_BYTECODE,
222
228
  },
229
+ EOA_WITHDRAWAL_CONTRACT: {
230
+ address: '0x00000961Ef480Eb55e80D19ad83579A64c007002',
231
+ },
223
232
  },
224
233
  };
225
234
  exports.DEFAULT_RETROACTIVE_FUNDING_REWARDS_ONLY_SPLIT = 1;
@@ -0,0 +1,83 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.EOA = void 0;
13
+ const eoaHelpers_1 = require("./eoaHelpers");
14
+ const ajv_1 = require("../ajv");
15
+ const schema_1 = require("../schema");
16
+ const constants_1 = require("../constants");
17
+ /**
18
+ * EOA can be used for managing EOA (Externally Owned Account) operations like withdrawals.
19
+ * @class
20
+ * @internal Access it through Client.eoa.
21
+ * @example
22
+ * const obolClient = new Client(config);
23
+ * await obolClient.eoa.requestWithdrawal(EOAWithdrawalPayload);
24
+ */
25
+ class EOA {
26
+ constructor(signer, chainId, provider) {
27
+ this.signer = signer;
28
+ this.chainId = chainId;
29
+ this.provider = provider;
30
+ }
31
+ /**
32
+ * Requests withdrawal from an EOA contract.
33
+ *
34
+ * This method allows requesting withdrawal of validator funds.
35
+ * The withdrawal request includes validator public key and corresponding withdrawal amount.
36
+ *
37
+ * @remarks
38
+ * **⚠️ Important:** If you're storing the private key in an `.env` file, ensure it is securely managed
39
+ * and not pushed to version control.
40
+ *
41
+ * @param {EOAWithdrawalPayload} payload - Data needed to request withdrawal
42
+ * @returns {Promise<{txHash: string}>} Transaction hash of the withdrawal request
43
+ * @throws Will throw an error if the signer is not provided or the request fails
44
+ *
45
+ * An example of how to use requestWithdrawal:
46
+ * ```typescript
47
+ * const result = await client.eoa.requestWithdrawal({
48
+ * pubkey: '0x123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456',
49
+ * allocation: 32, // 32 ETH
50
+ * requiredFee: '1' // in wei
51
+ * });
52
+ * console.log('Withdrawal requested:', result.txHash);
53
+ * ```
54
+ */
55
+ requestWithdrawal(payload) {
56
+ var _a;
57
+ return __awaiter(this, void 0, void 0, function* () {
58
+ if (!this.signer) {
59
+ throw new Error('Signer is required in requestWithdrawal');
60
+ }
61
+ if (!this.provider) {
62
+ throw new Error('Provider is required in requestWithdrawal');
63
+ }
64
+ const chainConfig = constants_1.CHAIN_CONFIGURATION[this.chainId];
65
+ if (!((_a = chainConfig === null || chainConfig === void 0 ? void 0 : chainConfig.EOA_WITHDRAWAL_CONTRACT) === null || _a === void 0 ? void 0 : _a.address)) {
66
+ throw new Error(`EOA withdrawal contract is not configured for chain ${this.chainId}`);
67
+ }
68
+ const validatedPayload = (0, ajv_1.validatePayload)(payload, schema_1.eoaWithdrawalPayloadSchema);
69
+ const withdrawalAddress = yield this.signer.getAddress();
70
+ return yield (0, eoaHelpers_1.submitEOAWithdrawalRequest)({
71
+ pubkey: validatedPayload.pubkey,
72
+ allocation: validatedPayload.allocation,
73
+ withdrawalAddress,
74
+ withdrawalContractAddress: chainConfig.EOA_WITHDRAWAL_CONTRACT.address,
75
+ requiredFee: validatedPayload.requiredFee,
76
+ chainId: this.chainId,
77
+ signer: this.signer,
78
+ provider: this.provider,
79
+ });
80
+ });
81
+ }
82
+ }
83
+ exports.EOA = EOA;
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.submitEOAWithdrawalRequest = void 0;
13
+ const constants_1 = require("../constants");
14
+ /**
15
+ * Helper function to submit withdrawal request for EOA
16
+ */
17
+ function submitEOAWithdrawalRequest({ pubkey, allocation, withdrawalAddress, withdrawalContractAddress, requiredFee, chainId, signer, provider, }) {
18
+ return __awaiter(this, void 0, void 0, function* () {
19
+ if (!withdrawalAddress)
20
+ throw new Error('No withdrawal address provided');
21
+ if (!allocation)
22
+ throw new Error('No allocation provided');
23
+ const amountInGwei = BigInt(Math.floor(Number(allocation) * constants_1.ETHER_TO_GWEI));
24
+ const data = `0x${pubkey.slice(2)}${amountInGwei.toString(16).padStart(16, '0')}`;
25
+ const { hash } = yield signer.sendTransaction({
26
+ to: withdrawalContractAddress,
27
+ chainId,
28
+ value: BigInt(requiredFee),
29
+ data: data,
30
+ });
31
+ const txResult = yield provider.getTransactionReceipt(hash);
32
+ if (!txResult)
33
+ return { txHash: null };
34
+ return { txHash: txResult === null || txResult === void 0 ? void 0 : txResult.hash };
35
+ });
36
+ }
37
+ exports.submitEOAWithdrawalRequest = submitEOAWithdrawalRequest;
@@ -23,7 +23,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
23
23
  });
24
24
  };
25
25
  Object.defineProperty(exports, "__esModule", { value: true });
26
- exports.Client = exports.ObolSplits = exports.Exit = exports.Incentives = void 0;
26
+ exports.Client = exports.EOA = exports.ObolSplits = exports.Exit = exports.Incentives = void 0;
27
27
  const uuid_1 = require("uuid");
28
28
  const base_js_1 = require("./base.js");
29
29
  const constants_js_1 = require("./constants.js");
@@ -36,6 +36,7 @@ const utils_js_1 = require("./utils.js");
36
36
  const incentives_js_1 = require("./incentives/incentives.js");
37
37
  const exit_js_1 = require("./exits/exit.js");
38
38
  const splits_js_1 = require("./splits/splits.js");
39
+ const eoa_js_1 = require("./eoa/eoa.js");
39
40
  __exportStar(require("./types.js"), exports);
40
41
  __exportStar(require("./services.js"), exports);
41
42
  __exportStar(require("./verification/signature-validator.js"), exports);
@@ -47,6 +48,8 @@ var exit_js_2 = require("./exits/exit.js");
47
48
  Object.defineProperty(exports, "Exit", { enumerable: true, get: function () { return exit_js_2.Exit; } });
48
49
  var splits_js_2 = require("./splits/splits.js");
49
50
  Object.defineProperty(exports, "ObolSplits", { enumerable: true, get: function () { return splits_js_2.ObolSplits; } });
51
+ var eoa_js_2 = require("./eoa/eoa.js");
52
+ Object.defineProperty(exports, "EOA", { enumerable: true, get: function () { return eoa_js_2.EOA; } });
50
53
  /**
51
54
  * Obol sdk Client can be used for creating, managing and activating distributed validators.
52
55
  */
@@ -70,6 +73,7 @@ class Client extends base_js_1.Base {
70
73
  this.incentives = new incentives_js_1.Incentives(this.signer, this.chainId, this.request.bind(this), this.provider);
71
74
  this.exit = new exit_js_1.Exit(this.chainId, this.provider);
72
75
  this.splits = new splits_js_1.ObolSplits(this.signer, this.chainId, this.provider);
76
+ this.eoa = new eoa_js_1.EOA(this.signer, this.chainId, this.provider);
73
77
  }
74
78
  /**
75
79
  * Accepts Obol terms and conditions to be able to create or update data.
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ovmRequestWithdrawalPayloadSchema = exports.ovmTotalSplitPayloadSchema = exports.ovmRewardsSplitPayloadSchema = exports.ovmBaseSplitPayload = exports.rewardsSplitterPayloadSchema = exports.totalSplitterPayloadSchema = exports.definitionSchema = exports.operatorPayloadSchema = void 0;
3
+ exports.eoaWithdrawalPayloadSchema = 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");
@@ -240,3 +240,21 @@ exports.ovmRequestWithdrawalPayloadSchema = {
240
240
  validateOVMRequestWithdrawalPayload: true,
241
241
  required: ['ovmAddress', 'pubKeys', 'amounts', 'withdrawalFees'],
242
242
  };
243
+ exports.eoaWithdrawalPayloadSchema = {
244
+ type: 'object',
245
+ properties: {
246
+ pubkey: {
247
+ type: 'string',
248
+ pattern: '^0x[a-fA-F0-9]{96}$', // 48 bytes = 96 hex chars + 0x prefix
249
+ },
250
+ allocation: {
251
+ type: 'number',
252
+ minimum: 0,
253
+ },
254
+ requiredFee: {
255
+ type: 'string',
256
+ pattern: '^[0-9]+$',
257
+ },
258
+ },
259
+ required: ['pubkey', 'allocation', 'requiredFee'],
260
+ };
@@ -410,7 +410,7 @@ const deployOVMContract = ({ OVMOwnerAddress, principalRecipient, rewardRecipien
410
410
  throw new Error(`OVM Factory not configured for chain ${chainId}`);
411
411
  }
412
412
  const ovmFactoryContract = new ethers_1.Contract(chainConfig.OVM_FACTORY_CONTRACT.address, OVMFactory_1.OVMFactoryContract.abi, signer);
413
- const tx = yield ovmFactoryContract.createObolValidatorManager(OVMOwnerAddress, principalRecipient, rewardRecipient, principalThreshold);
413
+ const tx = yield ovmFactoryContract.createObolValidatorManager(OVMOwnerAddress, principalRecipient, rewardRecipient, principalThreshold * constants_1.ETHER_TO_GWEI);
414
414
  const receipt = yield tx.wait();
415
415
  // Extract OVM address from logs
416
416
  const ovmAddressLog = (_3 = receipt === null || receipt === void 0 ? void 0 : receipt.logs[1]) === null || _3 === void 0 ? void 0 : _3.topics[1];
@@ -455,7 +455,7 @@ const deployOVMAndSplitV2 = ({ ovmArgs, rewardRecipients, isRewardsSplitterDeplo
455
455
  });
456
456
  }
457
457
  // Create OVM call data
458
- const ovmTxData = encodeCreateOVMTxData(ovmArgs.OVMOwnerAddress, ovmArgs.principalRecipient, ovmArgs.rewardRecipient, ovmArgs.principalThreshold);
458
+ const ovmTxData = encodeCreateOVMTxData(ovmArgs.OVMOwnerAddress, ovmArgs.principalRecipient, ovmArgs.rewardRecipient, ovmArgs.principalThreshold * constants_1.ETHER_TO_GWEI);
459
459
  executeCalls.push({
460
460
  target: chainConfig.OVM_FACTORY_CONTRACT.address,
461
461
  callData: ovmTxData,
@@ -0,0 +1,87 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ const eoa_1 = require("../../src/eoa/eoa");
13
+ const eoaHelpers_1 = require("../../src/eoa/eoaHelpers");
14
+ // Mock the helper function
15
+ jest.mock('../../src/eoa/eoaHelpers', () => ({
16
+ submitEOAWithdrawalRequest: jest.fn(),
17
+ }));
18
+ describe('EOA', () => {
19
+ let eoa;
20
+ let mockSigner;
21
+ let mockProvider;
22
+ beforeEach(() => {
23
+ // Clear all mocks before each test
24
+ jest.clearAllMocks();
25
+ mockSigner = {
26
+ getAddress: jest
27
+ .fn()
28
+ .mockResolvedValue('0x1234567890123456789012345678901234567890'),
29
+ };
30
+ mockProvider = {
31
+ getNetwork: jest.fn().mockResolvedValue({ chainId: 1 }),
32
+ };
33
+ eoa = new eoa_1.EOA(mockSigner, 1, mockProvider);
34
+ });
35
+ describe('requestWithdrawal', () => {
36
+ it('should successfully request withdrawal', () => __awaiter(void 0, void 0, void 0, function* () {
37
+ const mockPayload = {
38
+ pubkey: '0x123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456',
39
+ allocation: 32,
40
+ requiredFee: '1',
41
+ };
42
+ const mockResult = {
43
+ txHash: '0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890',
44
+ };
45
+ eoaHelpers_1.submitEOAWithdrawalRequest.mockResolvedValue(mockResult);
46
+ const result = yield eoa.requestWithdrawal(mockPayload);
47
+ expect(eoaHelpers_1.submitEOAWithdrawalRequest).toHaveBeenCalledWith({
48
+ pubkey: mockPayload.pubkey,
49
+ allocation: mockPayload.allocation,
50
+ withdrawalAddress: '0x1234567890123456789012345678901234567890',
51
+ withdrawalContractAddress: '0x00000961Ef480Eb55e80D19ad83579A64c007002',
52
+ requiredFee: mockPayload.requiredFee,
53
+ chainId: 1,
54
+ signer: mockSigner,
55
+ provider: mockProvider,
56
+ });
57
+ expect(result).toEqual(mockResult);
58
+ }));
59
+ it('should throw error when signer is not provided', () => __awaiter(void 0, void 0, void 0, function* () {
60
+ const eoaWithoutSigner = new eoa_1.EOA(undefined, 1, mockProvider);
61
+ const mockPayload = {
62
+ pubkey: '0x123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456',
63
+ allocation: 32,
64
+ requiredFee: '1',
65
+ };
66
+ yield expect(eoaWithoutSigner.requestWithdrawal(mockPayload)).rejects.toThrow('Signer is required in requestWithdrawal');
67
+ }));
68
+ it('should throw error when provider is not provided', () => __awaiter(void 0, void 0, void 0, function* () {
69
+ const eoaWithoutProvider = new eoa_1.EOA(mockSigner, 1, null);
70
+ const mockPayload = {
71
+ pubkey: '0x123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456',
72
+ allocation: 32,
73
+ requiredFee: '1',
74
+ };
75
+ yield expect(eoaWithoutProvider.requestWithdrawal(mockPayload)).rejects.toThrow('Provider is required in requestWithdrawal');
76
+ }));
77
+ it('should throw error when EOA withdrawal contract is not configured for chain', () => __awaiter(void 0, void 0, void 0, function* () {
78
+ const eoaUnsupportedChain = new eoa_1.EOA(mockSigner, 999, mockProvider);
79
+ const mockPayload = {
80
+ pubkey: '0x123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456',
81
+ allocation: 32,
82
+ requiredFee: '1',
83
+ };
84
+ yield expect(eoaUnsupportedChain.requestWithdrawal(mockPayload)).rejects.toThrow('EOA withdrawal contract is not configured for chain 999');
85
+ }));
86
+ });
87
+ });
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@obolnetwork/obol-sdk",
3
- "version": "2.9.1",
3
+ "version": "2.10.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"
@@ -137,6 +137,9 @@ export const CHAIN_CONFIGURATION = {
137
137
  address: '0x5cbA88D55Cec83caD5A105Ad40C8c9aF20bE21d1',
138
138
  bytecode: MAINNET_SPLIT_V2_FACTORY_BYTECODE,
139
139
  },
140
+ EOA_WITHDRAWAL_CONTRACT: {
141
+ address: '0x00000961Ef480Eb55e80D19ad83579A64c007002',
142
+ },
140
143
  },
141
144
  [FORK_MAPPING['0x01017000']]: {
142
145
  SPLITMAIN_CONTRACT: {
@@ -155,6 +158,9 @@ export const CHAIN_CONFIGURATION = {
155
158
  address: '0x43F641fA70e09f0326ac66b4Ef0C416EaEcBC6f5',
156
159
  bytecode: '',
157
160
  },
161
+ EOA_WITHDRAWAL_CONTRACT: {
162
+ address: '0x00000961Ef480Eb55e80D19ad83579A64c007002',
163
+ },
158
164
  },
159
165
  [FORK_MAPPING['0x10000910']]: {
160
166
  SPLITMAIN_CONTRACT: {
@@ -186,6 +192,9 @@ export const CHAIN_CONFIGURATION = {
186
192
  address: '0x5cbA88D55Cec83caD5A105Ad40C8c9aF20bE21d1',
187
193
  bytecode: HOODI_SPLIT_V2_FACTORY_BYTECODE,
188
194
  },
195
+ EOA_WITHDRAWAL_CONTRACT: {
196
+ address: '0x00000961Ef480Eb55e80D19ad83579A64c007002',
197
+ },
189
198
  },
190
199
  };
191
200
  export const DEFAULT_RETROACTIVE_FUNDING_REWARDS_ONLY_SPLIT = 1;
@@ -0,0 +1,79 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ import { submitEOAWithdrawalRequest } from './eoaHelpers';
11
+ import { validatePayload } from '../ajv';
12
+ import { eoaWithdrawalPayloadSchema } from '../schema';
13
+ import { CHAIN_CONFIGURATION } from '../constants';
14
+ /**
15
+ * EOA can be used for managing EOA (Externally Owned Account) operations like withdrawals.
16
+ * @class
17
+ * @internal Access it through Client.eoa.
18
+ * @example
19
+ * const obolClient = new Client(config);
20
+ * await obolClient.eoa.requestWithdrawal(EOAWithdrawalPayload);
21
+ */
22
+ export class EOA {
23
+ constructor(signer, chainId, provider) {
24
+ this.signer = signer;
25
+ this.chainId = chainId;
26
+ this.provider = provider;
27
+ }
28
+ /**
29
+ * Requests withdrawal from an EOA contract.
30
+ *
31
+ * This method allows requesting withdrawal of validator funds.
32
+ * The withdrawal request includes validator public key and corresponding withdrawal amount.
33
+ *
34
+ * @remarks
35
+ * **⚠️ Important:** If you're storing the private key in an `.env` file, ensure it is securely managed
36
+ * and not pushed to version control.
37
+ *
38
+ * @param {EOAWithdrawalPayload} payload - Data needed to request withdrawal
39
+ * @returns {Promise<{txHash: string}>} Transaction hash of the withdrawal request
40
+ * @throws Will throw an error if the signer is not provided or the request fails
41
+ *
42
+ * An example of how to use requestWithdrawal:
43
+ * ```typescript
44
+ * const result = await client.eoa.requestWithdrawal({
45
+ * pubkey: '0x123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456',
46
+ * allocation: 32, // 32 ETH
47
+ * requiredFee: '1' // in wei
48
+ * });
49
+ * console.log('Withdrawal requested:', result.txHash);
50
+ * ```
51
+ */
52
+ requestWithdrawal(payload) {
53
+ var _a;
54
+ return __awaiter(this, void 0, void 0, function* () {
55
+ if (!this.signer) {
56
+ throw new Error('Signer is required in requestWithdrawal');
57
+ }
58
+ if (!this.provider) {
59
+ throw new Error('Provider is required in requestWithdrawal');
60
+ }
61
+ const chainConfig = CHAIN_CONFIGURATION[this.chainId];
62
+ if (!((_a = chainConfig === null || chainConfig === void 0 ? void 0 : chainConfig.EOA_WITHDRAWAL_CONTRACT) === null || _a === void 0 ? void 0 : _a.address)) {
63
+ throw new Error(`EOA withdrawal contract is not configured for chain ${this.chainId}`);
64
+ }
65
+ const validatedPayload = validatePayload(payload, eoaWithdrawalPayloadSchema);
66
+ const withdrawalAddress = yield this.signer.getAddress();
67
+ return yield submitEOAWithdrawalRequest({
68
+ pubkey: validatedPayload.pubkey,
69
+ allocation: validatedPayload.allocation,
70
+ withdrawalAddress,
71
+ withdrawalContractAddress: chainConfig.EOA_WITHDRAWAL_CONTRACT.address,
72
+ requiredFee: validatedPayload.requiredFee,
73
+ chainId: this.chainId,
74
+ signer: this.signer,
75
+ provider: this.provider,
76
+ });
77
+ });
78
+ }
79
+ }
@@ -0,0 +1,33 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ import { ETHER_TO_GWEI } from '../constants';
11
+ /**
12
+ * Helper function to submit withdrawal request for EOA
13
+ */
14
+ export function submitEOAWithdrawalRequest({ pubkey, allocation, withdrawalAddress, withdrawalContractAddress, requiredFee, chainId, signer, provider, }) {
15
+ return __awaiter(this, void 0, void 0, function* () {
16
+ if (!withdrawalAddress)
17
+ throw new Error('No withdrawal address provided');
18
+ if (!allocation)
19
+ throw new Error('No allocation provided');
20
+ const amountInGwei = BigInt(Math.floor(Number(allocation) * ETHER_TO_GWEI));
21
+ const data = `0x${pubkey.slice(2)}${amountInGwei.toString(16).padStart(16, '0')}`;
22
+ const { hash } = yield signer.sendTransaction({
23
+ to: withdrawalContractAddress,
24
+ chainId,
25
+ value: BigInt(requiredFee),
26
+ data: data,
27
+ });
28
+ const txResult = yield provider.getTransactionReceipt(hash);
29
+ if (!txResult)
30
+ return { txHash: null };
31
+ return { txHash: txResult === null || txResult === void 0 ? void 0 : txResult.hash };
32
+ });
33
+ }
@@ -19,6 +19,7 @@ import { isContractAvailable } from './utils.js';
19
19
  import { Incentives } from './incentives/incentives.js';
20
20
  import { Exit } from './exits/exit.js';
21
21
  import { ObolSplits } from './splits/splits.js';
22
+ import { EOA } from './eoa/eoa.js';
22
23
  export * from './types.js';
23
24
  export * from './services.js';
24
25
  export * from './verification/signature-validator.js';
@@ -27,6 +28,7 @@ export * from './constants.js';
27
28
  export { Incentives } from './incentives/incentives.js';
28
29
  export { Exit } from './exits/exit.js';
29
30
  export { ObolSplits } from './splits/splits.js';
31
+ export { EOA } from './eoa/eoa.js';
30
32
  /**
31
33
  * Obol sdk Client can be used for creating, managing and activating distributed validators.
32
34
  */
@@ -50,6 +52,7 @@ export class Client extends Base {
50
52
  this.incentives = new Incentives(this.signer, this.chainId, this.request.bind(this), this.provider);
51
53
  this.exit = new Exit(this.chainId, this.provider);
52
54
  this.splits = new ObolSplits(this.signer, this.chainId, this.provider);
55
+ this.eoa = new EOA(this.signer, this.chainId, this.provider);
53
56
  }
54
57
  /**
55
58
  * Accepts Obol terms and conditions to be able to create or update data.
@@ -237,3 +237,21 @@ export const ovmRequestWithdrawalPayloadSchema = {
237
237
  validateOVMRequestWithdrawalPayload: true,
238
238
  required: ['ovmAddress', 'pubKeys', 'amounts', 'withdrawalFees'],
239
239
  };
240
+ export const eoaWithdrawalPayloadSchema = {
241
+ type: 'object',
242
+ properties: {
243
+ pubkey: {
244
+ type: 'string',
245
+ pattern: '^0x[a-fA-F0-9]{96}$', // 48 bytes = 96 hex chars + 0x prefix
246
+ },
247
+ allocation: {
248
+ type: 'number',
249
+ minimum: 0,
250
+ },
251
+ requiredFee: {
252
+ type: 'string',
253
+ pattern: '^[0-9]+$',
254
+ },
255
+ },
256
+ required: ['pubkey', 'allocation', 'requiredFee'],
257
+ };
@@ -12,7 +12,7 @@ import { OWRContract, OWRFactoryContract } from '../abi/OWR';
12
12
  import { OVMFactoryContract, OVMContract } from '../abi/OVMFactory';
13
13
  import { splitMainEthereumAbi } from '../abi/SplitMain';
14
14
  import { MultiCallContract } from '../abi/Multicall';
15
- import { CHAIN_CONFIGURATION } from '../constants';
15
+ import { CHAIN_CONFIGURATION, ETHER_TO_GWEI } from '../constants';
16
16
  import { splitV2FactoryAbi } from '../abi/splitV2FactoryAbi';
17
17
  const splitMainContractInterface = new Interface(splitMainEthereumAbi);
18
18
  const owrFactoryContractInterface = new Interface(OWRFactoryContract.abi);
@@ -397,7 +397,7 @@ export const deployOVMContract = ({ OVMOwnerAddress, principalRecipient, rewardR
397
397
  throw new Error(`OVM Factory not configured for chain ${chainId}`);
398
398
  }
399
399
  const ovmFactoryContract = new Contract(chainConfig.OVM_FACTORY_CONTRACT.address, OVMFactoryContract.abi, signer);
400
- const tx = yield ovmFactoryContract.createObolValidatorManager(OVMOwnerAddress, principalRecipient, rewardRecipient, principalThreshold);
400
+ const tx = yield ovmFactoryContract.createObolValidatorManager(OVMOwnerAddress, principalRecipient, rewardRecipient, principalThreshold * ETHER_TO_GWEI);
401
401
  const receipt = yield tx.wait();
402
402
  // Extract OVM address from logs
403
403
  const ovmAddressLog = (_3 = receipt === null || receipt === void 0 ? void 0 : receipt.logs[1]) === null || _3 === void 0 ? void 0 : _3.topics[1];
@@ -441,7 +441,7 @@ export const deployOVMAndSplitV2 = ({ ovmArgs, rewardRecipients, isRewardsSplitt
441
441
  });
442
442
  }
443
443
  // Create OVM call data
444
- const ovmTxData = encodeCreateOVMTxData(ovmArgs.OVMOwnerAddress, ovmArgs.principalRecipient, ovmArgs.rewardRecipient, ovmArgs.principalThreshold);
444
+ const ovmTxData = encodeCreateOVMTxData(ovmArgs.OVMOwnerAddress, ovmArgs.principalRecipient, ovmArgs.rewardRecipient, ovmArgs.principalThreshold * ETHER_TO_GWEI);
445
445
  executeCalls.push({
446
446
  target: chainConfig.OVM_FACTORY_CONTRACT.address,
447
447
  callData: ovmTxData,
@@ -0,0 +1,85 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ import { EOA } from '../../src/eoa/eoa';
11
+ import { submitEOAWithdrawalRequest } from '../../src/eoa/eoaHelpers';
12
+ // Mock the helper function
13
+ jest.mock('../../src/eoa/eoaHelpers', () => ({
14
+ submitEOAWithdrawalRequest: jest.fn(),
15
+ }));
16
+ describe('EOA', () => {
17
+ let eoa;
18
+ let mockSigner;
19
+ let mockProvider;
20
+ beforeEach(() => {
21
+ // Clear all mocks before each test
22
+ jest.clearAllMocks();
23
+ mockSigner = {
24
+ getAddress: jest
25
+ .fn()
26
+ .mockResolvedValue('0x1234567890123456789012345678901234567890'),
27
+ };
28
+ mockProvider = {
29
+ getNetwork: jest.fn().mockResolvedValue({ chainId: 1 }),
30
+ };
31
+ eoa = new EOA(mockSigner, 1, mockProvider);
32
+ });
33
+ describe('requestWithdrawal', () => {
34
+ it('should successfully request withdrawal', () => __awaiter(void 0, void 0, void 0, function* () {
35
+ const mockPayload = {
36
+ pubkey: '0x123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456',
37
+ allocation: 32,
38
+ requiredFee: '1',
39
+ };
40
+ const mockResult = {
41
+ txHash: '0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890',
42
+ };
43
+ submitEOAWithdrawalRequest.mockResolvedValue(mockResult);
44
+ const result = yield eoa.requestWithdrawal(mockPayload);
45
+ expect(submitEOAWithdrawalRequest).toHaveBeenCalledWith({
46
+ pubkey: mockPayload.pubkey,
47
+ allocation: mockPayload.allocation,
48
+ withdrawalAddress: '0x1234567890123456789012345678901234567890',
49
+ withdrawalContractAddress: '0x00000961Ef480Eb55e80D19ad83579A64c007002',
50
+ requiredFee: mockPayload.requiredFee,
51
+ chainId: 1,
52
+ signer: mockSigner,
53
+ provider: mockProvider,
54
+ });
55
+ expect(result).toEqual(mockResult);
56
+ }));
57
+ it('should throw error when signer is not provided', () => __awaiter(void 0, void 0, void 0, function* () {
58
+ const eoaWithoutSigner = new EOA(undefined, 1, mockProvider);
59
+ const mockPayload = {
60
+ pubkey: '0x123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456',
61
+ allocation: 32,
62
+ requiredFee: '1',
63
+ };
64
+ yield expect(eoaWithoutSigner.requestWithdrawal(mockPayload)).rejects.toThrow('Signer is required in requestWithdrawal');
65
+ }));
66
+ it('should throw error when provider is not provided', () => __awaiter(void 0, void 0, void 0, function* () {
67
+ const eoaWithoutProvider = new EOA(mockSigner, 1, null);
68
+ const mockPayload = {
69
+ pubkey: '0x123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456',
70
+ allocation: 32,
71
+ requiredFee: '1',
72
+ };
73
+ yield expect(eoaWithoutProvider.requestWithdrawal(mockPayload)).rejects.toThrow('Provider is required in requestWithdrawal');
74
+ }));
75
+ it('should throw error when EOA withdrawal contract is not configured for chain', () => __awaiter(void 0, void 0, void 0, function* () {
76
+ const eoaUnsupportedChain = new EOA(mockSigner, 999, mockProvider);
77
+ const mockPayload = {
78
+ pubkey: '0x123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456',
79
+ allocation: 32,
80
+ requiredFee: '1',
81
+ };
82
+ yield expect(eoaUnsupportedChain.requestWithdrawal(mockPayload)).rejects.toThrow('EOA withdrawal contract is not configured for chain 999');
83
+ }));
84
+ });
85
+ });
@@ -0,0 +1,42 @@
1
+ import { type ProviderType, type SignerType, type EOAWithdrawalPayload } from '../types';
2
+ /**
3
+ * EOA can be used for managing EOA (Externally Owned Account) operations like withdrawals.
4
+ * @class
5
+ * @internal Access it through Client.eoa.
6
+ * @example
7
+ * const obolClient = new Client(config);
8
+ * await obolClient.eoa.requestWithdrawal(EOAWithdrawalPayload);
9
+ */
10
+ export declare class EOA {
11
+ private readonly signer;
12
+ readonly chainId: number;
13
+ readonly provider: ProviderType | undefined | null;
14
+ constructor(signer: SignerType | undefined, chainId: number, provider: ProviderType | undefined | null);
15
+ /**
16
+ * Requests withdrawal from an EOA contract.
17
+ *
18
+ * This method allows requesting withdrawal of validator funds.
19
+ * The withdrawal request includes validator public key and corresponding withdrawal amount.
20
+ *
21
+ * @remarks
22
+ * **⚠️ Important:** If you're storing the private key in an `.env` file, ensure it is securely managed
23
+ * and not pushed to version control.
24
+ *
25
+ * @param {EOAWithdrawalPayload} payload - Data needed to request withdrawal
26
+ * @returns {Promise<{txHash: string}>} Transaction hash of the withdrawal request
27
+ * @throws Will throw an error if the signer is not provided or the request fails
28
+ *
29
+ * An example of how to use requestWithdrawal:
30
+ * ```typescript
31
+ * const result = await client.eoa.requestWithdrawal({
32
+ * pubkey: '0x123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456',
33
+ * allocation: 32, // 32 ETH
34
+ * requiredFee: '1' // in wei
35
+ * });
36
+ * console.log('Withdrawal requested:', result.txHash);
37
+ * ```
38
+ */
39
+ requestWithdrawal(payload: EOAWithdrawalPayload): Promise<{
40
+ txHash: string | null;
41
+ }>;
42
+ }
@@ -0,0 +1,16 @@
1
+ import { type SignerType, type ProviderType } from '../types';
2
+ /**
3
+ * Helper function to submit withdrawal request for EOA
4
+ */
5
+ export declare function submitEOAWithdrawalRequest({ pubkey, allocation, withdrawalAddress, withdrawalContractAddress, requiredFee, chainId, signer, provider, }: {
6
+ pubkey: string;
7
+ allocation: number;
8
+ withdrawalAddress: string;
9
+ withdrawalContractAddress: string;
10
+ requiredFee: string;
11
+ chainId: number;
12
+ signer: SignerType;
13
+ provider: ProviderType;
14
+ }): Promise<{
15
+ txHash: string | null;
16
+ }>;
@@ -3,6 +3,7 @@ import { type RewardsSplitPayload, type ClusterDefinition, type ClusterLock, typ
3
3
  import { Incentives } from './incentives/incentives.js';
4
4
  import { Exit } from './exits/exit.js';
5
5
  import { ObolSplits } from './splits/splits.js';
6
+ import { EOA } from './eoa/eoa.js';
6
7
  export * from './types.js';
7
8
  export * from './services.js';
8
9
  export * from './verification/signature-validator.js';
@@ -11,6 +12,7 @@ export * from './constants.js';
11
12
  export { Incentives } from './incentives/incentives.js';
12
13
  export { Exit } from './exits/exit.js';
13
14
  export { ObolSplits } from './splits/splits.js';
15
+ export { EOA } from './eoa/eoa.js';
14
16
  /**
15
17
  * Obol sdk Client can be used for creating, managing and activating distributed validators.
16
18
  */
@@ -34,6 +36,11 @@ export declare class Client extends Base {
34
36
  * @type {ObolSplits}
35
37
  */
36
38
  splits: ObolSplits;
39
+ /**
40
+ * The eoa module, responsible for managing EOA operations.
41
+ * @type {EOA}
42
+ */
43
+ eoa: EOA;
37
44
  /**
38
45
  * The blockchain provider, used to interact with the network.
39
46
  * It can be null, undefined, or a valid provider instance and defaults to the Signer provider if Signer is passed.
@@ -352,3 +352,21 @@ export declare const ovmRequestWithdrawalPayloadSchema: {
352
352
  validateOVMRequestWithdrawalPayload: boolean;
353
353
  required: string[];
354
354
  };
355
+ export declare const eoaWithdrawalPayloadSchema: {
356
+ type: string;
357
+ properties: {
358
+ pubkey: {
359
+ type: string;
360
+ pattern: string;
361
+ };
362
+ allocation: {
363
+ type: string;
364
+ minimum: number;
365
+ };
366
+ requiredFee: {
367
+ type: string;
368
+ pattern: string;
369
+ };
370
+ };
371
+ required: string[];
372
+ };
@@ -149,7 +149,7 @@ export type OVMBaseSplitPayload = {
149
149
  OVMOwnerAddress: string;
150
150
  /** Owner address for the splitter contracts. */
151
151
  splitOwnerAddress?: string;
152
- /** Principal threshold for OVM contract. */
152
+ /** Principal threshold in ETH for OVM contract. */
153
153
  principalThreshold?: number;
154
154
  /** Distributor fee percentage (0-10). */
155
155
  distributorFeePercent?: number;
@@ -439,7 +439,7 @@ export type OVMArgs = {
439
439
  principalRecipient: string;
440
440
  /** Rewards recipient of the cluster. */
441
441
  rewardRecipient: string;
442
- /** Principal threshold for OVM contract. */
442
+ /** Principal threshold in ETH for OVM contract. */
443
443
  principalThreshold: number;
444
444
  };
445
445
  export type ChainConfig = {
@@ -471,6 +471,9 @@ export type ChainConfig = {
471
471
  address: string;
472
472
  bytecode: string;
473
473
  };
474
+ EOA_WITHDRAWAL_CONTRACT?: {
475
+ address: string;
476
+ };
474
477
  };
475
478
  /**
476
479
  * Payload for requesting withdrawal from OVM contract
@@ -485,3 +488,14 @@ export type OVMRequestWithdrawalPayload = {
485
488
  /** Array of withdrawal amounts in gwei (uint64) as strings */
486
489
  amounts: string[];
487
490
  };
491
+ /**
492
+ * Payload for requesting withdrawal from EOA contract
493
+ */
494
+ export type EOAWithdrawalPayload = {
495
+ /** Validator public key in hex format */
496
+ pubkey: string;
497
+ /** Withdrawal amount in ETH */
498
+ allocation: number;
499
+ /** Required fee in wei */
500
+ requiredFee: string;
501
+ };
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@obolnetwork/obol-sdk",
3
- "version": "2.9.1",
3
+ "version": "2.10.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/constants.ts CHANGED
@@ -199,6 +199,9 @@ export const CHAIN_CONFIGURATION: Record<number, ChainConfig> = {
199
199
  address: '0x5cbA88D55Cec83caD5A105Ad40C8c9aF20bE21d1',
200
200
  bytecode: MAINNET_SPLIT_V2_FACTORY_BYTECODE,
201
201
  },
202
+ EOA_WITHDRAWAL_CONTRACT: {
203
+ address: '0x00000961Ef480Eb55e80D19ad83579A64c007002',
204
+ },
202
205
  },
203
206
  [FORK_MAPPING['0x01017000']]: {
204
207
  SPLITMAIN_CONTRACT: {
@@ -217,6 +220,9 @@ export const CHAIN_CONFIGURATION: Record<number, ChainConfig> = {
217
220
  address: '0x43F641fA70e09f0326ac66b4Ef0C416EaEcBC6f5',
218
221
  bytecode: '',
219
222
  },
223
+ EOA_WITHDRAWAL_CONTRACT: {
224
+ address: '0x00000961Ef480Eb55e80D19ad83579A64c007002',
225
+ },
220
226
  },
221
227
  [FORK_MAPPING['0x10000910']]: {
222
228
  SPLITMAIN_CONTRACT: {
@@ -248,6 +254,9 @@ export const CHAIN_CONFIGURATION: Record<number, ChainConfig> = {
248
254
  address: '0x5cbA88D55Cec83caD5A105Ad40C8c9aF20bE21d1',
249
255
  bytecode: HOODI_SPLIT_V2_FACTORY_BYTECODE,
250
256
  },
257
+ EOA_WITHDRAWAL_CONTRACT: {
258
+ address: '0x00000961Ef480Eb55e80D19ad83579A64c007002',
259
+ },
251
260
  },
252
261
  };
253
262
 
package/src/eoa/eoa.ts ADDED
@@ -0,0 +1,94 @@
1
+ import { submitEOAWithdrawalRequest } from './eoaHelpers';
2
+ import { validatePayload } from '../ajv';
3
+ import { eoaWithdrawalPayloadSchema } from '../schema';
4
+ import { CHAIN_CONFIGURATION } from '../constants';
5
+ import {
6
+ type ProviderType,
7
+ type SignerType,
8
+ type EOAWithdrawalPayload,
9
+ } from '../types';
10
+
11
+ /**
12
+ * EOA can be used for managing EOA (Externally Owned Account) operations like withdrawals.
13
+ * @class
14
+ * @internal Access it through Client.eoa.
15
+ * @example
16
+ * const obolClient = new Client(config);
17
+ * await obolClient.eoa.requestWithdrawal(EOAWithdrawalPayload);
18
+ */
19
+ export class EOA {
20
+ private readonly signer: SignerType | undefined;
21
+ public readonly chainId: number;
22
+ public readonly provider: ProviderType | undefined | null;
23
+
24
+ constructor(
25
+ signer: SignerType | undefined,
26
+ chainId: number,
27
+ provider: ProviderType | undefined | null,
28
+ ) {
29
+ this.signer = signer;
30
+ this.chainId = chainId;
31
+ this.provider = provider;
32
+ }
33
+
34
+ /**
35
+ * Requests withdrawal from an EOA contract.
36
+ *
37
+ * This method allows requesting withdrawal of validator funds.
38
+ * The withdrawal request includes validator public key and corresponding withdrawal amount.
39
+ *
40
+ * @remarks
41
+ * **⚠️ Important:** If you're storing the private key in an `.env` file, ensure it is securely managed
42
+ * and not pushed to version control.
43
+ *
44
+ * @param {EOAWithdrawalPayload} payload - Data needed to request withdrawal
45
+ * @returns {Promise<{txHash: string}>} Transaction hash of the withdrawal request
46
+ * @throws Will throw an error if the signer is not provided or the request fails
47
+ *
48
+ * An example of how to use requestWithdrawal:
49
+ * ```typescript
50
+ * const result = await client.eoa.requestWithdrawal({
51
+ * pubkey: '0x123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456',
52
+ * allocation: 32, // 32 ETH
53
+ * requiredFee: '1' // in wei
54
+ * });
55
+ * console.log('Withdrawal requested:', result.txHash);
56
+ * ```
57
+ */
58
+ async requestWithdrawal(
59
+ payload: EOAWithdrawalPayload,
60
+ ): Promise<{ txHash: string | null }> {
61
+ if (!this.signer) {
62
+ throw new Error('Signer is required in requestWithdrawal');
63
+ }
64
+
65
+ if (!this.provider) {
66
+ throw new Error('Provider is required in requestWithdrawal');
67
+ }
68
+
69
+ const chainConfig = CHAIN_CONFIGURATION[this.chainId];
70
+ if (!chainConfig?.EOA_WITHDRAWAL_CONTRACT?.address) {
71
+ throw new Error(
72
+ `EOA withdrawal contract is not configured for chain ${this.chainId}`,
73
+ );
74
+ }
75
+
76
+ const validatedPayload = validatePayload<EOAWithdrawalPayload>(
77
+ payload,
78
+ eoaWithdrawalPayloadSchema,
79
+ );
80
+
81
+ const withdrawalAddress = await this.signer.getAddress();
82
+
83
+ return await submitEOAWithdrawalRequest({
84
+ pubkey: validatedPayload.pubkey,
85
+ allocation: validatedPayload.allocation,
86
+ withdrawalAddress,
87
+ withdrawalContractAddress: chainConfig.EOA_WITHDRAWAL_CONTRACT.address,
88
+ requiredFee: validatedPayload.requiredFee,
89
+ chainId: this.chainId,
90
+ signer: this.signer,
91
+ provider: this.provider,
92
+ });
93
+ }
94
+ }
@@ -0,0 +1,43 @@
1
+ import { ETHER_TO_GWEI } from '../constants';
2
+ import { type SignerType, type ProviderType } from '../types';
3
+
4
+ /**
5
+ * Helper function to submit withdrawal request for EOA
6
+ */
7
+ export async function submitEOAWithdrawalRequest({
8
+ pubkey,
9
+ allocation,
10
+ withdrawalAddress,
11
+ withdrawalContractAddress,
12
+ requiredFee,
13
+ chainId,
14
+ signer,
15
+ provider,
16
+ }: {
17
+ pubkey: string;
18
+ allocation: number;
19
+ withdrawalAddress: string;
20
+ withdrawalContractAddress: string;
21
+ requiredFee: string;
22
+ chainId: number;
23
+ signer: SignerType;
24
+ provider: ProviderType;
25
+ }): Promise<{ txHash: string | null }> {
26
+ if (!withdrawalAddress) throw new Error('No withdrawal address provided');
27
+ if (!allocation) throw new Error('No allocation provided');
28
+
29
+ const amountInGwei = BigInt(Math.floor(Number(allocation) * ETHER_TO_GWEI));
30
+ const data = `0x${pubkey.slice(2)}${amountInGwei.toString(16).padStart(16, '0')}`;
31
+
32
+ const { hash } = await signer.sendTransaction({
33
+ to: withdrawalContractAddress,
34
+ chainId,
35
+ value: BigInt(requiredFee),
36
+ data: data as `0x${string}`,
37
+ });
38
+
39
+ const txResult = await provider.getTransactionReceipt(hash);
40
+ if (!txResult) return { txHash: null };
41
+
42
+ return { txHash: txResult?.hash };
43
+ }
package/src/index.ts CHANGED
@@ -50,6 +50,7 @@ import { isContractAvailable } from './utils.js';
50
50
  import { Incentives } from './incentives/incentives.js';
51
51
  import { Exit } from './exits/exit.js';
52
52
  import { ObolSplits } from './splits/splits.js';
53
+ import { EOA } from './eoa/eoa.js';
53
54
  export * from './types.js';
54
55
  export * from './services.js';
55
56
  export * from './verification/signature-validator.js';
@@ -58,6 +59,7 @@ export * from './constants.js';
58
59
  export { Incentives } from './incentives/incentives.js';
59
60
  export { Exit } from './exits/exit.js';
60
61
  export { ObolSplits } from './splits/splits.js';
62
+ export { EOA } from './eoa/eoa.js';
61
63
 
62
64
  /**
63
65
  * Obol sdk Client can be used for creating, managing and activating distributed validators.
@@ -86,6 +88,12 @@ export class Client extends Base {
86
88
  */
87
89
  public splits: ObolSplits;
88
90
 
91
+ /**
92
+ * The eoa module, responsible for managing EOA operations.
93
+ * @type {EOA}
94
+ */
95
+ public eoa: EOA;
96
+
89
97
  /**
90
98
  * The blockchain provider, used to interact with the network.
91
99
  * It can be null, undefined, or a valid provider instance and defaults to the Signer provider if Signer is passed.
@@ -121,6 +129,7 @@ export class Client extends Base {
121
129
  );
122
130
  this.exit = new Exit(this.chainId, this.provider);
123
131
  this.splits = new ObolSplits(this.signer, this.chainId, this.provider);
132
+ this.eoa = new EOA(this.signer, this.chainId, this.provider);
124
133
  }
125
134
 
126
135
  /**
package/src/schema.ts CHANGED
@@ -261,3 +261,22 @@ export const ovmRequestWithdrawalPayloadSchema = {
261
261
  validateOVMRequestWithdrawalPayload: true,
262
262
  required: ['ovmAddress', 'pubKeys', 'amounts', 'withdrawalFees'],
263
263
  };
264
+
265
+ export const eoaWithdrawalPayloadSchema = {
266
+ type: 'object',
267
+ properties: {
268
+ pubkey: {
269
+ type: 'string',
270
+ pattern: '^0x[a-fA-F0-9]{96}$', // 48 bytes = 96 hex chars + 0x prefix
271
+ },
272
+ allocation: {
273
+ type: 'number',
274
+ minimum: 0,
275
+ },
276
+ requiredFee: {
277
+ type: 'string',
278
+ pattern: '^[0-9]+$',
279
+ },
280
+ },
281
+ required: ['pubkey', 'allocation', 'requiredFee'],
282
+ };
@@ -13,7 +13,7 @@ import { OWRContract, OWRFactoryContract } from '../abi/OWR';
13
13
  import { OVMFactoryContract, OVMContract } from '../abi/OVMFactory';
14
14
  import { splitMainEthereumAbi } from '../abi/SplitMain';
15
15
  import { MultiCallContract } from '../abi/Multicall';
16
- import { CHAIN_CONFIGURATION } from '../constants';
16
+ import { CHAIN_CONFIGURATION, ETHER_TO_GWEI } from '../constants';
17
17
  import { splitV2FactoryAbi } from '../abi/splitV2FactoryAbi';
18
18
 
19
19
  const splitMainContractInterface = new Interface(splitMainEthereumAbi);
@@ -742,7 +742,7 @@ export const deployOVMContract = async ({
742
742
  OVMOwnerAddress,
743
743
  principalRecipient,
744
744
  rewardRecipient,
745
- principalThreshold,
745
+ principalThreshold * ETHER_TO_GWEI,
746
746
  );
747
747
 
748
748
  const receipt = await tx.wait();
@@ -837,7 +837,7 @@ export const deployOVMAndSplitV2 = async ({
837
837
  ovmArgs.OVMOwnerAddress,
838
838
  ovmArgs.principalRecipient,
839
839
  ovmArgs.rewardRecipient,
840
- ovmArgs.principalThreshold,
840
+ ovmArgs.principalThreshold * ETHER_TO_GWEI,
841
841
  );
842
842
 
843
843
  executeCalls.push({
package/src/types.ts CHANGED
@@ -214,7 +214,7 @@ export type OVMBaseSplitPayload = {
214
214
  /** Owner address for the splitter contracts. */
215
215
  splitOwnerAddress?: string;
216
216
 
217
- /** Principal threshold for OVM contract. */
217
+ /** Principal threshold in ETH for OVM contract. */
218
218
  principalThreshold?: number;
219
219
 
220
220
  /** Distributor fee percentage (0-10). */
@@ -573,7 +573,7 @@ export type OVMArgs = {
573
573
  /** Rewards recipient of the cluster. */
574
574
  rewardRecipient: string;
575
575
 
576
- /** Principal threshold for OVM contract. */
576
+ /** Principal threshold in ETH for OVM contract. */
577
577
  principalThreshold: number;
578
578
  };
579
579
 
@@ -606,6 +606,9 @@ export type ChainConfig = {
606
606
  address: string;
607
607
  bytecode: string;
608
608
  };
609
+ EOA_WITHDRAWAL_CONTRACT?: {
610
+ address: string;
611
+ };
609
612
  };
610
613
 
611
614
  /**
@@ -624,3 +627,17 @@ export type OVMRequestWithdrawalPayload = {
624
627
  /** Array of withdrawal amounts in gwei (uint64) as strings */
625
628
  amounts: string[];
626
629
  };
630
+
631
+ /**
632
+ * Payload for requesting withdrawal from EOA contract
633
+ */
634
+ export type EOAWithdrawalPayload = {
635
+ /** Validator public key in hex format */
636
+ pubkey: string;
637
+
638
+ /** Withdrawal amount in ETH */
639
+ allocation: number;
640
+
641
+ /** Required fee in wei */
642
+ requiredFee: string;
643
+ };