@obolnetwork/obol-sdk 2.6.0 → 2.8.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.
- package/dist/cjs/package.json +4 -2
- package/dist/cjs/src/abi/OVMFactory.js +620 -0
- package/dist/cjs/src/ajv.js +33 -6
- package/dist/cjs/src/bytecodes.js +8 -1
- package/dist/cjs/src/constants.js +55 -7
- package/dist/cjs/src/exits/exit.js +82 -0
- package/dist/cjs/src/index.js +21 -9
- package/dist/cjs/src/schema.js +71 -1
- package/dist/cjs/src/splits/splitHelpers.js +218 -12
- package/dist/cjs/src/splits/splits.js +261 -0
- package/dist/cjs/src/types.js +0 -1
- package/dist/cjs/test/client/ajv.spec.js +269 -0
- package/dist/cjs/test/client/methods.spec.js +418 -0
- package/dist/cjs/test/exit/exit.spec.js +74 -3
- package/dist/cjs/test/fixtures.js +13 -1
- package/dist/cjs/test/splits/splits.spec.js +239 -0
- package/dist/esm/package.json +4 -2
- package/dist/esm/src/abi/OVMFactory.js +617 -0
- package/dist/esm/src/ajv.js +33 -6
- package/dist/esm/src/bytecodes.js +7 -0
- package/dist/esm/src/constants.js +54 -7
- package/dist/esm/src/exits/exit.js +83 -1
- package/dist/esm/src/index.js +20 -9
- package/dist/esm/src/schema.js +71 -1
- package/dist/esm/src/splits/splitHelpers.js +213 -13
- package/dist/esm/src/splits/splits.js +257 -0
- package/dist/esm/src/types.js +0 -1
- package/dist/esm/test/client/ajv.spec.js +267 -0
- package/dist/esm/test/client/methods.spec.js +393 -0
- package/dist/esm/test/exit/exit.spec.js +74 -3
- package/dist/esm/test/fixtures.js +12 -0
- package/dist/esm/test/splits/splits.spec.js +237 -0
- package/dist/types/src/abi/OVMFactory.d.ts +662 -0
- package/dist/types/src/bytecodes.d.ts +7 -0
- package/dist/types/src/constants.d.ts +10 -21
- package/dist/types/src/exits/exit.d.ts +21 -1
- package/dist/types/src/index.d.ts +7 -0
- package/dist/types/src/schema.d.ts +145 -0
- package/dist/types/src/splits/splitHelpers.d.ts +40 -1
- package/dist/types/src/splits/splits.d.ts +51 -0
- package/dist/types/src/types.d.ts +122 -6
- package/dist/types/test/client/ajv.spec.d.ts +1 -0
- package/dist/types/test/client/methods.spec.d.ts +1 -0
- package/dist/types/test/fixtures.d.ts +10 -0
- package/dist/types/test/splits/splits.spec.d.ts +1 -0
- package/package.json +4 -2
- package/src/abi/OVMFactory.ts +617 -0
- package/src/ajv.ts +55 -13
- package/src/bytecodes.ts +15 -0
- package/src/constants.ts +66 -8
- package/src/exits/exit.ts +98 -1
- package/src/index.ts +36 -15
- package/src/schema.ts +80 -0
- package/src/splits/splitHelpers.ts +360 -14
- package/src/splits/splits.ts +355 -0
- package/src/types.ts +138 -10
package/src/exits/exit.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ENR } from '@chainsafe/discv5';
|
|
2
2
|
import * as elliptic from 'elliptic';
|
|
3
|
-
import { init, verify } from '@chainsafe/bls';
|
|
3
|
+
import { init, verify, aggregateSignatures } from '@chainsafe/bls';
|
|
4
4
|
import {
|
|
5
5
|
ByteVectorType,
|
|
6
6
|
ContainerType,
|
|
@@ -16,6 +16,7 @@ import type {
|
|
|
16
16
|
ExitValidationMessage,
|
|
17
17
|
SignedExitValidationMessage,
|
|
18
18
|
ExistingExitValidationBlobData,
|
|
19
|
+
FullExitBlob,
|
|
19
20
|
} from '../types';
|
|
20
21
|
import { getCapellaFork, getGenesisValidatorsRoot } from './ethUtils';
|
|
21
22
|
import { computeDomain, signingRoot } from './verificationHelpers';
|
|
@@ -561,4 +562,100 @@ export class Exit {
|
|
|
561
562
|
|
|
562
563
|
return validNonDuplicateBlobs;
|
|
563
564
|
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Recombines exit blobs into a single exit blob.
|
|
568
|
+
*
|
|
569
|
+
* This method aggregates partial exit signatures from multiple operators into a single exit blob.
|
|
570
|
+
* It ensures that the signatures are properly ordered and aggregated according to the operator indices.
|
|
571
|
+
*
|
|
572
|
+
* @param exitBlob - The existing exit blob data containing partial exit signatures
|
|
573
|
+
*
|
|
574
|
+
* @returns Promise resolving to a single exit blob with aggregated signatures
|
|
575
|
+
*
|
|
576
|
+
* @throws {Error} When no valid signatures are found for aggregation
|
|
577
|
+
* @throws {Error} When signature length is invalid
|
|
578
|
+
* @throws {Error} When signature parsing fails
|
|
579
|
+
*
|
|
580
|
+
* @example
|
|
581
|
+
* ```typescript
|
|
582
|
+
* const aggregatedExitBlob = await exit.recombineExitBlobs(existingBlobData);
|
|
583
|
+
* ```
|
|
584
|
+
*/
|
|
585
|
+
async recombineExitBlobs(
|
|
586
|
+
exitBlob: ExistingExitValidationBlobData,
|
|
587
|
+
): Promise<FullExitBlob> {
|
|
588
|
+
await init('herumi');
|
|
589
|
+
|
|
590
|
+
// Map to store signatures by their share index (matching Go's map[int]tbls.Signature)
|
|
591
|
+
const signaturesByIndex = new Map<number, Uint8Array>();
|
|
592
|
+
|
|
593
|
+
// Extract signatures from shares_exit_data (equivalent to er.Signatures in Go)
|
|
594
|
+
if (!exitBlob.shares_exit_data || exitBlob.shares_exit_data.length === 0) {
|
|
595
|
+
throw new Error('No shares exit data available for aggregation');
|
|
596
|
+
}
|
|
597
|
+
const signaturesMap = exitBlob.shares_exit_data[0] || {};
|
|
598
|
+
for (const [sigIdxStr, sigData] of Object.entries(signaturesMap)) {
|
|
599
|
+
const sigStr = sigData.partial_exit_signature;
|
|
600
|
+
|
|
601
|
+
if (!sigStr || sigStr.length === 0) {
|
|
602
|
+
// ignore, the associated share index didn't push a partial signature yet
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
if (sigStr.length < 2) {
|
|
607
|
+
throw new Error(`Signature string has invalid size: ${sigStr.length}`);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// Remove 0x prefix and ensure it's 96 bytes (192 hex chars)
|
|
611
|
+
const cleanSigStr = sigStr.startsWith('0x')
|
|
612
|
+
? sigStr.substring(2)
|
|
613
|
+
: sigStr;
|
|
614
|
+
if (cleanSigStr.length !== 192) {
|
|
615
|
+
throw new Error(
|
|
616
|
+
`Invalid signature length. Expected 192 hex chars (96 bytes), got ${cleanSigStr.length}`,
|
|
617
|
+
);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
try {
|
|
621
|
+
const sigBytes = fromHexString(cleanSigStr);
|
|
622
|
+
// Convert string index to number and add 1 (matching Go's sigIdx+1)
|
|
623
|
+
const sigIdx = parseInt(sigIdxStr, 10);
|
|
624
|
+
signaturesByIndex.set(sigIdx + 1, sigBytes);
|
|
625
|
+
} catch (err) {
|
|
626
|
+
throw new Error(`Invalid partial signature: ${String(err)}`);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
if (signaturesByIndex.size === 0) {
|
|
631
|
+
throw new Error('No valid signatures found for aggregation');
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// Sort by index and extract signatures in correct order
|
|
635
|
+
const sortedIndices = Array.from(signaturesByIndex.keys()).sort(
|
|
636
|
+
(a, b) => a - b,
|
|
637
|
+
);
|
|
638
|
+
const rawSignatures = sortedIndices.map(idx => {
|
|
639
|
+
const signature = signaturesByIndex.get(idx);
|
|
640
|
+
if (signature === undefined) {
|
|
641
|
+
throw new Error(`Missing signature for index ${idx}`);
|
|
642
|
+
}
|
|
643
|
+
return signature;
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
// Aggregate signatures (equivalent to tbls.ThresholdAggregate in Go)
|
|
647
|
+
// Note: @chainsafe/bls doesn't have explicit threshold aggregation, but ordering should be preserved
|
|
648
|
+
const fullSig = aggregateSignatures(rawSignatures);
|
|
649
|
+
|
|
650
|
+
return {
|
|
651
|
+
public_key: exitBlob.public_key,
|
|
652
|
+
signed_exit_message: {
|
|
653
|
+
message: {
|
|
654
|
+
epoch: exitBlob.epoch,
|
|
655
|
+
validator_index: exitBlob.validator_index,
|
|
656
|
+
},
|
|
657
|
+
signature: '0x' + Buffer.from(fullSig).toString('hex'),
|
|
658
|
+
},
|
|
659
|
+
};
|
|
660
|
+
}
|
|
564
661
|
}
|
package/src/index.ts
CHANGED
|
@@ -12,10 +12,10 @@ import {
|
|
|
12
12
|
TermsAndConditionsSigningTypes,
|
|
13
13
|
DEFAULT_BASE_VERSION,
|
|
14
14
|
TERMS_AND_CONDITIONS_HASH,
|
|
15
|
-
AVAILABLE_SPLITTER_CHAINS,
|
|
16
15
|
CHAIN_CONFIGURATION,
|
|
17
16
|
DEFAULT_RETROACTIVE_FUNDING_REWARDS_ONLY_SPLIT,
|
|
18
17
|
OBOL_SDK_EMAIL,
|
|
18
|
+
isChainSupportedForSplitters,
|
|
19
19
|
} from './constants.js';
|
|
20
20
|
import { ConflictError } from './errors.js';
|
|
21
21
|
import {
|
|
@@ -49,6 +49,7 @@ import {
|
|
|
49
49
|
import { isContractAvailable } from './utils.js';
|
|
50
50
|
import { Incentives } from './incentives/incentives.js';
|
|
51
51
|
import { Exit } from './exits/exit.js';
|
|
52
|
+
import { ObolSplits } from './splits/splits.js';
|
|
52
53
|
export * from './types.js';
|
|
53
54
|
export * from './services.js';
|
|
54
55
|
export * from './verification/signature-validator.js';
|
|
@@ -56,6 +57,7 @@ export * from './verification/common.js';
|
|
|
56
57
|
export * from './constants.js';
|
|
57
58
|
export { Incentives } from './incentives/incentives.js';
|
|
58
59
|
export { Exit } from './exits/exit.js';
|
|
60
|
+
export { ObolSplits } from './splits/splits.js';
|
|
59
61
|
|
|
60
62
|
/**
|
|
61
63
|
* Obol sdk Client can be used for creating, managing and activating distributed validators.
|
|
@@ -78,6 +80,12 @@ export class Client extends Base {
|
|
|
78
80
|
*/
|
|
79
81
|
public exit: Exit;
|
|
80
82
|
|
|
83
|
+
/**
|
|
84
|
+
* The splits module, responsible for managing splits.
|
|
85
|
+
* @type {ObolSplits}
|
|
86
|
+
*/
|
|
87
|
+
public splits: ObolSplits;
|
|
88
|
+
|
|
81
89
|
/**
|
|
82
90
|
* The blockchain provider, used to interact with the network.
|
|
83
91
|
* It can be null, undefined, or a valid provider instance and defaults to the Signer provider if Signer is passed.
|
|
@@ -112,6 +120,7 @@ export class Client extends Base {
|
|
|
112
120
|
this.provider,
|
|
113
121
|
);
|
|
114
122
|
this.exit = new Exit(this.chainId, this.provider);
|
|
123
|
+
this.splits = new ObolSplits(this.signer, this.chainId, this.provider);
|
|
115
124
|
}
|
|
116
125
|
|
|
117
126
|
/**
|
|
@@ -205,28 +214,35 @@ export class Client extends Base {
|
|
|
205
214
|
);
|
|
206
215
|
|
|
207
216
|
// Check if we allow splitters on this chainId
|
|
208
|
-
if (!
|
|
217
|
+
if (!isChainSupportedForSplitters(this.chainId)) {
|
|
209
218
|
throw new Error(
|
|
210
219
|
`Splitter configuration is not supported on ${this.chainId} chain`,
|
|
211
220
|
);
|
|
212
221
|
}
|
|
213
222
|
|
|
223
|
+
const chainConfig = CHAIN_CONFIGURATION[this.chainId];
|
|
224
|
+
if (!chainConfig?.SPLITMAIN_ADDRESS) {
|
|
225
|
+
throw new Error(
|
|
226
|
+
`SPLITMAIN_ADDRESS is not configured for chain ${this.chainId}`,
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
214
230
|
const checkSplitMainAddress = await isContractAvailable(
|
|
215
|
-
|
|
231
|
+
chainConfig.SPLITMAIN_ADDRESS.address,
|
|
216
232
|
this.signer.provider as ProviderType,
|
|
217
|
-
|
|
233
|
+
chainConfig.SPLITMAIN_ADDRESS.bytecode,
|
|
218
234
|
);
|
|
219
235
|
|
|
220
236
|
const checkMulticallAddress = await isContractAvailable(
|
|
221
|
-
|
|
237
|
+
chainConfig.MULTICALL_ADDRESS.address,
|
|
222
238
|
this.signer.provider as ProviderType,
|
|
223
|
-
|
|
239
|
+
chainConfig.MULTICALL_ADDRESS.bytecode,
|
|
224
240
|
);
|
|
225
241
|
|
|
226
242
|
const checkOWRFactoryAddress = await isContractAvailable(
|
|
227
|
-
|
|
243
|
+
chainConfig.OWR_FACTORY_ADDRESS.address,
|
|
228
244
|
this.signer.provider as ProviderType,
|
|
229
|
-
|
|
245
|
+
chainConfig.OWR_FACTORY_ADDRESS.bytecode,
|
|
230
246
|
);
|
|
231
247
|
|
|
232
248
|
if (
|
|
@@ -240,8 +256,7 @@ export class Client extends Base {
|
|
|
240
256
|
}
|
|
241
257
|
|
|
242
258
|
const retroActiveFundingRecipient = {
|
|
243
|
-
account:
|
|
244
|
-
CHAIN_CONFIGURATION[this.chainId].RETROACTIVE_FUNDING_ADDRESS.address,
|
|
259
|
+
account: chainConfig.RETROACTIVE_FUNDING_ADDRESS.address,
|
|
245
260
|
percentAllocation: validatedPayload.ObolRAFSplit,
|
|
246
261
|
};
|
|
247
262
|
|
|
@@ -320,16 +335,23 @@ export class Client extends Base {
|
|
|
320
335
|
);
|
|
321
336
|
|
|
322
337
|
// Check if we allow splitters on this chainId
|
|
323
|
-
if (!
|
|
338
|
+
if (!isChainSupportedForSplitters(this.chainId)) {
|
|
324
339
|
throw new Error(
|
|
325
340
|
`Splitter configuration is not supported on ${this.chainId} chain`,
|
|
326
341
|
);
|
|
327
342
|
}
|
|
328
343
|
|
|
344
|
+
const chainConfig = CHAIN_CONFIGURATION[this.chainId];
|
|
345
|
+
if (!chainConfig?.SPLITMAIN_ADDRESS) {
|
|
346
|
+
throw new Error(
|
|
347
|
+
`SPLITMAIN_ADDRESS is not configured for chain ${this.chainId}`,
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
|
|
329
351
|
const checkSplitMainAddress = await isContractAvailable(
|
|
330
|
-
|
|
352
|
+
chainConfig.SPLITMAIN_ADDRESS.address,
|
|
331
353
|
this.signer.provider as ProviderType,
|
|
332
|
-
|
|
354
|
+
chainConfig.SPLITMAIN_ADDRESS.bytecode,
|
|
333
355
|
);
|
|
334
356
|
|
|
335
357
|
if (!checkSplitMainAddress) {
|
|
@@ -339,8 +361,7 @@ export class Client extends Base {
|
|
|
339
361
|
}
|
|
340
362
|
|
|
341
363
|
const retroActiveFundingRecipient = {
|
|
342
|
-
account:
|
|
343
|
-
CHAIN_CONFIGURATION[this.chainId].RETROACTIVE_FUNDING_ADDRESS.address,
|
|
364
|
+
account: chainConfig.RETROACTIVE_FUNDING_ADDRESS.address,
|
|
344
365
|
percentAllocation: validatedPayload.ObolRAFSplit,
|
|
345
366
|
};
|
|
346
367
|
|
package/src/schema.ts
CHANGED
|
@@ -2,8 +2,10 @@ import { ZeroAddress } from 'ethers';
|
|
|
2
2
|
import {
|
|
3
3
|
DEFAULT_RETROACTIVE_FUNDING_REWARDS_ONLY_SPLIT,
|
|
4
4
|
DEFAULT_RETROACTIVE_FUNDING_TOTAL_SPLIT,
|
|
5
|
+
PRINCIPAL_THRESHOLD,
|
|
5
6
|
} from './constants';
|
|
6
7
|
import { VALID_DEPOSIT_AMOUNTS, VALID_NON_COMPOUNDING_AMOUNTS } from './ajv';
|
|
8
|
+
import { zeroAddress } from 'viem';
|
|
7
9
|
|
|
8
10
|
export const operatorPayloadSchema = {
|
|
9
11
|
type: 'object',
|
|
@@ -150,3 +152,81 @@ export const rewardsSplitterPayloadSchema = {
|
|
|
150
152
|
validateRewardsSplitRecipients: true,
|
|
151
153
|
required: ['splitRecipients', 'principalRecipient', 'etherAmount'],
|
|
152
154
|
};
|
|
155
|
+
|
|
156
|
+
export const ovmBaseSplitPayload = {
|
|
157
|
+
rewardSplitRecipients: {
|
|
158
|
+
type: 'array',
|
|
159
|
+
items: {
|
|
160
|
+
type: 'object',
|
|
161
|
+
properties: {
|
|
162
|
+
address: {
|
|
163
|
+
type: 'string',
|
|
164
|
+
pattern: '^0x[a-fA-F0-9]{40}$',
|
|
165
|
+
},
|
|
166
|
+
percentAllocation: { type: 'number' },
|
|
167
|
+
},
|
|
168
|
+
required: ['address', 'percentAllocation'],
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
OVMOwnerAddress: {
|
|
172
|
+
type: 'string',
|
|
173
|
+
pattern: '^0x[a-fA-F0-9]{40}$',
|
|
174
|
+
},
|
|
175
|
+
splitOwnerAddress: {
|
|
176
|
+
type: 'string',
|
|
177
|
+
pattern: '^0x[a-fA-F0-9]{40}$',
|
|
178
|
+
default: zeroAddress,
|
|
179
|
+
},
|
|
180
|
+
principalThreshold: {
|
|
181
|
+
type: 'number',
|
|
182
|
+
minimum: 16,
|
|
183
|
+
default: PRINCIPAL_THRESHOLD,
|
|
184
|
+
},
|
|
185
|
+
distributorFeePercent: {
|
|
186
|
+
type: 'number',
|
|
187
|
+
minimum: 0,
|
|
188
|
+
maximum: 10,
|
|
189
|
+
default: 0,
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
export const ovmRewardsSplitPayloadSchema = {
|
|
194
|
+
type: 'object',
|
|
195
|
+
properties: {
|
|
196
|
+
...ovmBaseSplitPayload,
|
|
197
|
+
principalRecipient: {
|
|
198
|
+
type: 'string',
|
|
199
|
+
pattern: '^0x[a-fA-F0-9]{40}$',
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
validateOVMRewardsSplitRecipients: true,
|
|
203
|
+
required: ['rewardSplitRecipients', 'OVMOwnerAddress', 'principalRecipient'],
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
export const ovmTotalSplitPayloadSchema = {
|
|
207
|
+
type: 'object',
|
|
208
|
+
properties: {
|
|
209
|
+
...ovmBaseSplitPayload,
|
|
210
|
+
principalSplitRecipients: {
|
|
211
|
+
type: 'array',
|
|
212
|
+
items: {
|
|
213
|
+
type: 'object',
|
|
214
|
+
properties: {
|
|
215
|
+
address: {
|
|
216
|
+
type: 'string',
|
|
217
|
+
pattern: '^0x[a-fA-F0-9]{40}$',
|
|
218
|
+
},
|
|
219
|
+
percentAllocation: { type: 'number' },
|
|
220
|
+
},
|
|
221
|
+
required: ['address', 'percentAllocation'],
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
validateOVMRewardsSplitRecipients: true,
|
|
226
|
+
validateOVMTotalSplitRecipients: true,
|
|
227
|
+
required: [
|
|
228
|
+
'rewardSplitRecipients',
|
|
229
|
+
'principalSplitRecipients',
|
|
230
|
+
'OVMOwnerAddress',
|
|
231
|
+
],
|
|
232
|
+
};
|