@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.
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/src/ajv.js +23 -0
- package/dist/cjs/src/schema.js +32 -1
- package/dist/cjs/src/splits/splitHelpers.js +26 -1
- package/dist/cjs/src/splits/splits.js +40 -0
- package/dist/cjs/test/client/ajv.spec.js +51 -0
- package/dist/cjs/test/splits/splits.spec.js +79 -0
- package/dist/esm/package.json +1 -1
- package/dist/esm/src/ajv.js +23 -0
- package/dist/esm/src/schema.js +31 -0
- package/dist/esm/src/splits/splitHelpers.js +25 -1
- package/dist/esm/src/splits/splits.js +42 -2
- package/dist/esm/test/client/ajv.spec.js +52 -1
- package/dist/esm/test/splits/splits.spec.js +80 -1
- package/dist/types/src/schema.d.ts +31 -0
- package/dist/types/src/splits/splitHelpers.d.ts +17 -0
- package/dist/types/src/splits/splits.d.ts +28 -1
- package/dist/types/src/types.d.ts +13 -0
- package/package.json +1 -1
- package/src/ajv.ts +32 -0
- package/src/schema.ts +32 -0
- package/src/splits/splitHelpers.ts +41 -1
- package/src/splits/splits.ts +48 -0
- package/src/types.ts +17 -0
package/dist/cjs/package.json
CHANGED
package/dist/cjs/src/ajv.js
CHANGED
|
@@ -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);
|
package/dist/cjs/src/schema.js
CHANGED
|
@@ -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);
|
package/dist/esm/package.json
CHANGED
package/dist/esm/src/ajv.js
CHANGED
|
@@ -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);
|
package/dist/esm/src/schema.js
CHANGED
|
@@ -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
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
|
+
};
|
package/src/splits/splits.ts
CHANGED
|
@@ -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
|
+
};
|