diff --git a/src/contracts/deposit/deposit.service.ts b/src/contracts/deposit/deposit.service.ts index d6402b52..caf51273 100644 --- a/src/contracts/deposit/deposit.service.ts +++ b/src/contracts/deposit/deposit.service.ts @@ -36,6 +36,7 @@ export class DepositService { private blsService: BlsService, ) {} + // Why OneAtTime() ? this function is executed in guardian.service inside another function with OneAtTime() in cron job @OneAtTime() public async handleNewBlock(blockNumber: number): Promise { if (blockNumber % DEPOSIT_EVENTS_CACHE_UPDATE_BLOCK_RATE !== 0) return; @@ -336,6 +337,7 @@ export class DepositService { const mergedEvents = cachedEvents.data.concat(freshEvents); + // TODO: Why we don't cache freshEvents? return { events: mergedEvents, startBlock: cachedEvents.headers.startBlock, diff --git a/src/guardian/guardian.service.ts b/src/guardian/guardian.service.ts index 34d13b6e..fccafc7b 100644 --- a/src/guardian/guardian.service.ts +++ b/src/guardian/guardian.service.ts @@ -96,10 +96,9 @@ export class GuardianService implements OnModuleInit { this.logger.log('New staking router state cycle start'); try { - const { - elBlockSnapshot: { blockHash, blockNumber }, - data: stakingModules, - } = await this.stakingRouterService.getStakingModules(); + // TODO: rename + const { blockHash, blockNumber, vettedKeys, stakingModulesData } = + await this.stakingRouterService.getVettedAndUnusedKeys(); await this.repositoryService.initCachedContracts({ blockHash }); @@ -119,8 +118,10 @@ export class GuardianService implements OnModuleInit { return; } + const stakingModulesNumber = stakingModulesData.length; + this.logger.log('Staking modules loaded', { - modulesCount: stakingModules.length, + modulesCount: stakingModulesNumber, }); await this.depositService.handleNewBlock(blockNumber); @@ -136,14 +137,14 @@ export class GuardianService implements OnModuleInit { blockHash: blockData.blockHash, }); - await Promise.all( - stakingModules.map(async (stakingRouterModule) => { - const stakingModuleData = - await this.stakingModuleGuardService.getStakingRouterModuleData( - stakingRouterModule, - blockHash, - ); + // TODO: check only if one of nonce changed + await this.stakingModuleGuardService.checkVettedKeysDuplicates( + vettedKeys, + blockData, + ); + await Promise.all( + stakingModulesData.map(async (stakingModuleData) => { await this.stakingModuleGuardService.checkKeysIntersections( stakingModuleData, blockData, @@ -157,7 +158,7 @@ export class GuardianService implements OnModuleInit { ); await this.guardianMessageService.pingMessageBroker( - stakingModules.map(({ id }) => id), + stakingModulesData.map(({ stakingModuleId }) => stakingModuleId), blockData, ); diff --git a/src/guardian/interfaces/staking-module.interface.ts b/src/guardian/interfaces/staking-module.interface.ts index fe08e7bd..593a2c83 100644 --- a/src/guardian/interfaces/staking-module.interface.ts +++ b/src/guardian/interfaces/staking-module.interface.ts @@ -1,7 +1,9 @@ +import { RegistryKey } from 'keys-api/interfaces/RegistryKey'; + export interface StakingModuleData { blockHash: string; - isDepositsPaused: boolean; unusedKeys: string[]; + vettedKeys: RegistryKey[]; nonce: number; stakingModuleId: number; } diff --git a/src/guardian/staking-module-guard/staking-module-guard.service.ts b/src/guardian/staking-module-guard/staking-module-guard.service.ts index 6590df66..5333fb4e 100644 --- a/src/guardian/staking-module-guard/staking-module-guard.service.ts +++ b/src/guardian/staking-module-guard/staking-module-guard.service.ts @@ -12,6 +12,7 @@ import { GuardianMessageService } from '../guardian-message'; import { StakingRouterService } from 'staking-router'; import { SRModule } from 'keys-api/interfaces'; +import { RegistryKey } from 'keys-api/interfaces/RegistryKey'; @Injectable() export class StakingModuleGuardService { @@ -30,34 +31,55 @@ export class StakingModuleGuardService { private lastContractsStateByModuleId: Record = {}; - public async getStakingRouterModuleData( - stakingRouterModule: SRModule, - blockHash: string, - ): Promise { - const { - data: { - keys, - module: { nonce }, - }, - } = await this.stakingRouterService.getStakingModuleUnusedKeys( - blockHash, - stakingRouterModule, - ); + // public async getStakingRouterModuleData( + // stakingRouterModule: SRModule, + // blockHash: string, + // ): Promise { + // const { + // data: { + // keys, + // module: { nonce }, + // }, + // } = await this.stakingRouterService.getStakingModuleUnusedKeys( + // blockHash, + // stakingRouterModule, + // ); + + // const isDepositsPaused = await this.securityService.isDepositsPaused( + // stakingRouterModule.id, + // { + // blockHash, + // }, + // ); + + // return { + // nonce, + // unusedKeys: keys.map((srKey) => srKey.key), + // stakingModuleId: stakingRouterModule.id, + // blockHash, + // }; + // } - const isDepositsPaused = await this.securityService.isDepositsPaused( - stakingRouterModule.id, - { - blockHash, - }, + /** + * Check vetted among staking modules + */ + public async checkVettedKeysDuplicates( + vettedKeys: RegistryKey[], + blockData: BlockData, + ): Promise { + const uniqueKeys = new Set(); + const duplicatedKeys = vettedKeys.filter( + (vettedKey) => uniqueKeys.size === uniqueKeys.add(vettedKey.key).size, ); - return { - nonce, - unusedKeys: keys.map((srKey) => srKey.key), - isDepositsPaused, - stakingModuleId: stakingRouterModule.id, - blockHash, - }; + if (duplicatedKeys.length) { + this.logger.warn('Found duplicated vetted key', { + blockHash: blockData.blockHash, + duplicatedKeys, + }); + //TODO: set metric + throw Error('Found duplicated vetted key'); + } } /** @@ -88,7 +110,14 @@ export class StakingModuleGuardService { filteredIntersections, ); - if (stakingModuleData.isDepositsPaused) { + const isDepositsPaused = await this.securityService.isDepositsPaused( + stakingModuleData.stakingModuleId, + { + blockHash: stakingModuleData.blockHash, + }, + ); + + if (isDepositsPaused) { this.logger.warn('Deposits are paused', { blockHash, stakingModuleId }); return; } @@ -96,6 +125,17 @@ export class StakingModuleGuardService { if (isFilteredIntersectionsFound) { await this.handleKeysIntersections(stakingModuleData, blockData); } else { + const usedKeys = await this.handleIntersectionBetweenUsedAndUnusedKeys( + keysIntersections, + ); + + // if found used keys, Lido already made deposit on this keys + if (usedKeys.length) { + this.logger.log('Found that we already deposited on these keys'); + // set metric ccouncil_daemon_used_duplicate + return; + } + await this.handleCorrectKeys(stakingModuleData, blockData); } } @@ -156,6 +196,29 @@ export class StakingModuleGuardService { return attackIntersections; } + public async handleIntersectionBetweenUsedAndUnusedKeys( + intersectionsWithLidoWC: VerifiedDepositEvent[], + ) { + const depositedPubkeys = intersectionsWithLidoWC.map( + (deposit) => deposit.pubkey, + ); + + if (depositedPubkeys.length) { + console.log('deposited pubkeys', depositedPubkeys); + this.logger.log( + 'Found intersections with lido credentials, need to check duplicated keys', + ); + + const keys = await this.stakingRouterService.getKeysWithDuplicates( + depositedPubkeys, + ); + const usedKeys = keys.data.filter((key) => key.used); + return usedKeys; + } + + return []; + } + /** * Handles the situation when keys have previously deposited copies * @param blockData - collected data from the current block diff --git a/src/keys-api/interfaces/GroupedByModuleOperatorListResponse.ts b/src/keys-api/interfaces/GroupedByModuleOperatorListResponse.ts new file mode 100644 index 00000000..f2ae79ed --- /dev/null +++ b/src/keys-api/interfaces/GroupedByModuleOperatorListResponse.ts @@ -0,0 +1,10 @@ +import { Meta } from './Meta'; +import { SROperatorListWithModule } from './SROperatorListWithModule'; + +export type GroupedByModuleOperatorListResponse = { + /** + * Staking router module operators with module + */ + data: SROperatorListWithModule[]; + meta: Meta; +}; diff --git a/src/keys-api/interfaces/KeyListResponse.ts b/src/keys-api/interfaces/KeyListResponse.ts new file mode 100644 index 00000000..33db0f39 --- /dev/null +++ b/src/keys-api/interfaces/KeyListResponse.ts @@ -0,0 +1,7 @@ +import type { Meta } from './Meta'; +import { RegistryKey } from './RegistryKey'; + +export type KeyListResponse = { + data: Array; + meta: Meta; +}; diff --git a/src/keys-api/interfaces/RegistryKey.ts b/src/keys-api/interfaces/RegistryKey.ts index e7c331cc..4b133b3f 100644 --- a/src/keys-api/interfaces/RegistryKey.ts +++ b/src/keys-api/interfaces/RegistryKey.ts @@ -19,4 +19,9 @@ export type RegistryKey = { * Key index in contract */ index: number; + + /** + * Staking module address + */ + moduleAddress: string; }; diff --git a/src/keys-api/interfaces/RegistryOperator.ts b/src/keys-api/interfaces/RegistryOperator.ts new file mode 100644 index 00000000..f20d4cb6 --- /dev/null +++ b/src/keys-api/interfaces/RegistryOperator.ts @@ -0,0 +1,38 @@ +export type RegistryOperator = { + /** + * Index of Operator + */ + index: number; + /** + * This value shows if node operator active + */ + active: boolean; + /** + * Operator name + */ + name: string; + /** + * Ethereum 1 address which receives stETH rewards for this operator + */ + rewardAddress: string; + /** + * The number of keys vetted by the DAO and that can be used for the deposit + */ + stakingLimit: number; + /** + * Amount of stopped validators + */ + stoppedValidators: number; + /** + * Total signing keys amount + */ + totalSigningKeys: number; + /** + * Amount of used signing keys + */ + usedSigningKeys: number; + /** + * Staking module address + */ + moduleAddress: string; +}; diff --git a/src/keys-api/interfaces/SROperatorListWithModule.ts b/src/keys-api/interfaces/SROperatorListWithModule.ts new file mode 100644 index 00000000..b3cdfec0 --- /dev/null +++ b/src/keys-api/interfaces/SROperatorListWithModule.ts @@ -0,0 +1,13 @@ +import type { RegistryOperator } from './RegistryOperator'; +import type { SRModule } from './SRModule'; + +export type SROperatorListWithModule = { + /** + * Operators of staking router module + */ + operators: Array; + /** + * Detailed Staking Router information + */ + module: SRModule; +}; diff --git a/src/keys-api/interfaces/index.ts b/src/keys-api/interfaces/index.ts index 3eaafa78..da3cc9df 100644 --- a/src/keys-api/interfaces/index.ts +++ b/src/keys-api/interfaces/index.ts @@ -1,3 +1,4 @@ export type { SRModuleKeysResponse } from './SRModuleKeysResponse'; export type { SRModuleListResponse } from './SRModuleListResponse'; export type { SRModule } from './SRModule'; +export type { KeyListResponse } from './KeyListResponse'; diff --git a/src/keys-api/keys-api.service.ts b/src/keys-api/keys-api.service.ts index aaf84677..6cf28664 100644 --- a/src/keys-api/keys-api.service.ts +++ b/src/keys-api/keys-api.service.ts @@ -1,10 +1,15 @@ import { Injectable, LoggerService, Inject } from '@nestjs/common'; -import { FetchService } from '@lido-nestjs/fetch'; +import { FetchService, RequestInit } from '@lido-nestjs/fetch'; import { AbortController } from 'node-abort-controller'; import { FETCH_REQUEST_TIMEOUT } from './keys-api.constants'; -import { SRModuleKeysResponse, SRModuleListResponse } from './interfaces'; +import { + SRModuleKeysResponse, + SRModuleListResponse, + KeyListResponse, +} from './interfaces'; import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; import { Configuration } from 'common/config'; +import { GroupedByModuleOperatorListResponse } from './interfaces/GroupedByModuleOperatorListResponse'; @Injectable() export class KeysApiService { @@ -14,7 +19,7 @@ export class KeysApiService { protected readonly fetchService: FetchService, ) {} - protected async fetch(url: string) { + protected async fetch(url: string, requestInit?: RequestInit) { const controller = new AbortController(); const { signal } = controller; @@ -29,6 +34,7 @@ export class KeysApiService { `${baseUrl}${url}`, { signal, + ...requestInit, }, ); clearTimeout(timer); @@ -54,4 +60,33 @@ export class KeysApiService { throw Error('Keys API not synced, please wait'); return result; } + + /** + * + * @param The /v1/keys/find KAPI endpoint returns a key along with its duplicates + * @returns + */ + public async getKeysWithDuplicates(pubkeys: string[]) { + const result = await this.fetch(`/v1/keys/find`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ pubkeys }), + }); + + return result; + } + + public async getUnusedKeys() { + const result = await this.fetch(`/v1/keys?used=false`); + return result; + } + + public async getOperatorListWithModule() { + const result = await this.fetch( + `/v1/operators`, + ); + return result; + } } diff --git a/src/staking-router/keys.fixtures.ts b/src/staking-router/keys.fixtures.ts new file mode 100644 index 00000000..d6d63188 --- /dev/null +++ b/src/staking-router/keys.fixtures.ts @@ -0,0 +1,66 @@ +export const keysAllStakingModules = { + data: [ + { + key: '0x9948d2becf42e9f76922bc6f664545e6f50401050af95785a984802d32a95c4c61f8e3de312b78167f86e047f83a7796', + depositSignature: + '0x8bf4401a354de243a3716ee2efc0bde1ded56a40e2943ac7c50290bec37e935d6170b21e7c0872f203199386143ef12612a1488a8e9f1cdf1229c382f29c326bcbf6ed6a87d8fbfe0df87dacec6632fc4709d9d338f4cf81e861d942c23bba1e', + operatorIndex: 0, + used: false, + moduleAddress: '0x595F64Ddc3856a3b5Ff4f4CC1d1fb4B46cFd2bAC', + index: 100, + }, + { + key: '0x911dd3091cfb1b42c960e4f343ea98d9ee6a1dc8ef215afa976fb557bd627a901717c0008bc33a0bfea15f0dfe9c5d01', + depositSignature: + '0x898ac7072aa26d983f9ece384c4037966dde614b75dddf982f6a415f3107cb2569b96f6d1c44e608a250ac4bbe908df51473f0de2cf732d283b07d88f3786893124967b8697a8b93d31976e7ac49ab1e568f98db0bbb13384477e8357b6d7e9b', + operatorIndex: 0, + used: false, + moduleAddress: '0x595F64Ddc3856a3b5Ff4f4CC1d1fb4B46cFd2bAC', + index: 101, + }, + { + key: '0x8d12ec44816f108df84ef9b03e423a6d8fb0f0a1823c871b123ff41f893a7b372eb038a1ed1ff15083e07a777a5cba50', + depositSignature: + '0xb3a683ec2a71f4b24039ccd10905aee7c08bc542203f68208215853fcf300fde5c10aee40f060da2a35d57050116668511a4b9b1db97e1da33b7c1fcfc192588c7989b00ae3fb7fe697dab18656403fc3d196e6d3bec51bd877c6033653ff5be', + operatorIndex: 0, + used: false, + moduleAddress: '0x595F64Ddc3856a3b5Ff4f4CC1d1fb4B46cFd2bAC', + index: 102, + }, + { + key: '0x83fc58f68d913481e065c928b040ae8b157ef2b32371b7df93d40188077c619dc789d443c18ac4a9b7e76de5ed6c8247', + depositSignature: + '0xa13833d96f4b98291dbf428cb69e7a3bdce61c9d20efcdb276423c7d6199ebd10cf1728dbd418c592701a41983cb02330e736610be254f617140af48a9d20b31cdffdd1d4fc8c0776439fca3330337d33042768acf897000b9e5da386077be44', + operatorIndex: 28, + used: false, + moduleAddress: '0x11a93807078f8BB880c1BD0ee4C387537de4b4b6', + index: 4, + }, + { + key: '0x84e85db03bee714dbecf01914460d9576b7f7226030bdbeae9ee923bf5f8e01eec4f7dfe54aa7eca6f4bccce59a0bf42', + depositSignature: + '0xb024b67a2f6c579213529e143bd4ebb81c5a2dc385cb526de4a816c8fe0317ebfb38369b08622e9f27e62cce2811679a13a459d4e9a8d7bd00080c36b359c1ca03bdcf4a0fcbbc2e18fe9923d8c4edb503ade58bdefe690760611e3738d5e64f', + operatorIndex: 28, + used: false, + moduleAddress: '0x11a93807078f8BB880c1BD0ee4C387537de4b4b6', + index: 5, + }, + { + key: '0x84ff489c1e07c75ac635914d4fa20bb37b30f7cf37a8fb85298a88e6f45daab122b43a352abce2132bdde96fd4a01599', + depositSignature: + '0xb3967b288b566c72316bc9de9a208d56248df4d29b4666fbe0986f66b1ff64eb1d04dfc484af591084d9992b8dc1c1370f96cf974425b47b1f4315dd3b236005b90c8a88ceef78057bf7c54e84fdc0c3f6ed464257f0823111bac2b2ea1818e0', + operatorIndex: 28, + used: false, + moduleAddress: '0x11a93807078f8BB880c1BD0ee4C387537de4b4b6', + index: 6, + }, + ], + meta: { + elBlockSnapshot: { + blockNumber: 400153, + blockHash: + '0x40c697def4d4f7233b75149ab941462582bb5f035b5089f7c6a3d7849222f47c', + timestamp: 1701027516, + }, + }, +}; diff --git a/src/staking-router/operators.fixtures.ts b/src/staking-router/operators.fixtures.ts new file mode 100644 index 00000000..fcefbba9 --- /dev/null +++ b/src/staking-router/operators.fixtures.ts @@ -0,0 +1,83 @@ +export const groupedByModulesOperators = { + data: [ + { + operators: [ + { + name: 'Dev team', + rewardAddress: '0x6D725DAe055287f913661ee0b79dE6B21F12A459', + stakingLimit: 101, + stoppedValidators: 0, + totalSigningKeys: 103, + usedSigningKeys: 100, + index: 0, + active: true, + moduleAddress: '0x595F64Ddc3856a3b5Ff4f4CC1d1fb4B46cFd2bAC', + }, + { + name: 'DSRV', + rewardAddress: '0x39ceC2b3ba293CC15f15a3876dB8D356a1670789', + stakingLimit: 2, + stoppedValidators: 0, + totalSigningKeys: 2, + usedSigningKeys: 2, + index: 1, + active: true, + moduleAddress: '0x595F64Ddc3856a3b5Ff4f4CC1d1fb4B46cFd2bAC', + }, + ], + module: { + nonce: 364, + type: 'curated-onchain-v1', + id: 1, + stakingModuleAddress: '0x595F64Ddc3856a3b5Ff4f4CC1d1fb4B46cFd2bAC', + moduleFee: 500, + treasuryFee: 500, + targetShare: 10000, + status: 0, + name: 'curated-onchain-v1', + lastDepositAt: 1700841084, + lastDepositBlock: 385525, + exitedValidatorsCount: 2, + active: true, + }, + }, + { + operators: [ + { + name: 'Lido x Obol: Delightful Dragonfly', + rewardAddress: '0x142E4542865a638208c17fF288cdA8cC82ecD27a', + stakingLimit: 5, + stoppedValidators: 0, + totalSigningKeys: 7, + usedSigningKeys: 4, + index: 28, + active: true, + moduleAddress: '0x11a93807078f8BB880c1BD0ee4C387537de4b4b6', + }, + ], + module: { + nonce: 69, + type: 'curated-onchain-v1', + id: 2, + stakingModuleAddress: '0x11a93807078f8BB880c1BD0ee4C387537de4b4b6', + moduleFee: 800, + treasuryFee: 200, + targetShare: 500, + status: 0, + name: 'SimpleDVT', + lastDepositAt: 1700764452, + lastDepositBlock: 379465, + exitedValidatorsCount: 0, + active: true, + }, + }, + ], + meta: { + elBlockSnapshot: { + blockNumber: 400153, + blockHash: + '0x40c697def4d4f7233b75149ab941462582bb5f035b5089f7c6a3d7849222f47c', + timestamp: 1701027516, + }, + }, +}; diff --git a/src/staking-router/staking-router.service.ts b/src/staking-router/staking-router.service.ts index b769ded2..40614346 100644 --- a/src/staking-router/staking-router.service.ts +++ b/src/staking-router/staking-router.service.ts @@ -3,6 +3,8 @@ import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; import { Configuration } from 'common/config'; import { KeysApiService } from 'keys-api/keys-api.service'; import { SRModuleKeysResponse, SRModule } from 'keys-api/interfaces'; +import { RegistryKey } from 'keys-api/interfaces/RegistryKey'; +import { StakingModuleData } from 'guardian'; @Injectable() export class StakingRouterService { @@ -13,6 +15,77 @@ export class StakingRouterService { protected readonly keysApiService: KeysApiService, ) {} + public async getVettedAndUnusedKeys() { + // TODO: add cache by modules nonce + const operatorsByModules = + await this.keysApiService.getOperatorListWithModule(); + const operatorsBlockHash = + operatorsByModules.meta.elBlockSnapshot.blockHash; + const operatorsBlockNumber = + operatorsByModules.meta.elBlockSnapshot.blockNumber; + + const unusedKeys = await this.keysApiService.getUnusedKeys(); + const keysBlockHash = unusedKeys.meta.elBlockSnapshot.blockHash; + if (keysBlockHash != operatorsBlockHash) { + this.logger.log('Blockhash of the received keys and operators', { + keysBlockHash, + operatorsBlockHash, + }); + + throw Error( + 'Blockhash of the received keys does not match the blockhash of operators', + ); + } + + // found vetted keys + const vettedKeys: RegistryKey[] = []; + const stakingModulesData: StakingModuleData[] = []; + + operatorsByModules.data.forEach(({ operators, module: stakingModule }) => { + const moduleKeys: RegistryKey[] = []; + const moduleVettedKeys: RegistryKey[] = []; + operators.forEach((operator) => { + const operatorKeys = unusedKeys.data.filter( + (key) => + key.moduleAddress === operator.moduleAddress && + key.operatorIndex === operator.index, + ); + // Sort the filtered keys by index + operatorKeys.sort((a, b) => a.index - b.index); + + moduleKeys.push(...operatorKeys); + + const numberOfVettedUnusedKeys = + operator.stakingLimit - operator.usedSigningKeys; + const operatorVettedKeys = operatorKeys.slice( + 0, + numberOfVettedUnusedKeys, + ); + moduleVettedKeys.push(...operatorVettedKeys); + vettedKeys.push(...operatorVettedKeys); + }); + + stakingModulesData.push({ + unusedKeys: moduleKeys.map((srKey) => srKey.key), + nonce: stakingModule.nonce, + stakingModuleId: stakingModule.id, + blockHash: operatorsBlockHash, + vettedKeys: moduleVettedKeys, + }); + }); + + return { + stakingModulesData, + vettedKeys, + blockHash: operatorsBlockHash, + blockNumber: operatorsBlockNumber, + }; + } + + public async getKeysWithDuplicates(pubkeys: string[]) { + return await this.keysApiService.getKeysWithDuplicates(pubkeys); + } + public async getStakingModules() { return await this.keysApiService.getModulesList(); } diff --git a/src/staking-router/staking-router.spec.ts b/src/staking-router/staking-router.spec.ts new file mode 100644 index 00000000..4229e2e9 --- /dev/null +++ b/src/staking-router/staking-router.spec.ts @@ -0,0 +1,117 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { KeysApiService } from '../keys-api/keys-api.service'; +import { StakingRouterService } from './staking-router.service'; +import { groupedByModulesOperators } from './operators.fixtures'; +import { keysAllStakingModules } from './keys.fixtures'; +import { ConfigModule } from 'common/config'; +import { LoggerModule } from 'common/logger'; + +describe('YourService', () => { + let stakingRouterService: StakingRouterService; + let keysApiService: KeysApiService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [ConfigModule.forRoot(), LoggerModule], + providers: [ + StakingRouterService, + { + provide: KeysApiService, + useValue: { + getOperatorListWithModule: jest.fn(), + getUnusedKeys: jest.fn(), + }, + }, + ], + }).compile(); + + stakingRouterService = + module.get(StakingRouterService); + keysApiService = module.get(KeysApiService); + // keysApiService = module.get(KeysApiService) as jest.Mocked; + }); + + it('should return correct data when block hashes match', async () => { + (keysApiService.getOperatorListWithModule as jest.Mock).mockResolvedValue( + groupedByModulesOperators, + ); + (keysApiService.getUnusedKeys as jest.Mock).mockResolvedValue( + keysAllStakingModules, + ); + + const result = await stakingRouterService.getVettedAndUnusedKeys(); + + // Assertions + expect(result).toEqual({ + blockNumber: 400153, + blockHash: + '0x40c697def4d4f7233b75149ab941462582bb5f035b5089f7c6a3d7849222f47c', + stakingModulesData: [ + { + unusedKeys: [ + '0x9948d2becf42e9f76922bc6f664545e6f50401050af95785a984802d32a95c4c61f8e3de312b78167f86e047f83a7796', + '0x911dd3091cfb1b42c960e4f343ea98d9ee6a1dc8ef215afa976fb557bd627a901717c0008bc33a0bfea15f0dfe9c5d01', + '0x8d12ec44816f108df84ef9b03e423a6d8fb0f0a1823c871b123ff41f893a7b372eb038a1ed1ff15083e07a777a5cba50', + ], + vettedKeys: [ + { + key: '0x9948d2becf42e9f76922bc6f664545e6f50401050af95785a984802d32a95c4c61f8e3de312b78167f86e047f83a7796', + depositSignature: + '0x8bf4401a354de243a3716ee2efc0bde1ded56a40e2943ac7c50290bec37e935d6170b21e7c0872f203199386143ef12612a1488a8e9f1cdf1229c382f29c326bcbf6ed6a87d8fbfe0df87dacec6632fc4709d9d338f4cf81e861d942c23bba1e', + operatorIndex: 0, + used: false, + moduleAddress: '0x595F64Ddc3856a3b5Ff4f4CC1d1fb4B46cFd2bAC', + index: 100, + }, + ], + blockHash: + '0x40c697def4d4f7233b75149ab941462582bb5f035b5089f7c6a3d7849222f47c', + stakingModuleId: 1, + nonce: 364, + }, + { + unusedKeys: [ + '0x83fc58f68d913481e065c928b040ae8b157ef2b32371b7df93d40188077c619dc789d443c18ac4a9b7e76de5ed6c8247', + '0x84e85db03bee714dbecf01914460d9576b7f7226030bdbeae9ee923bf5f8e01eec4f7dfe54aa7eca6f4bccce59a0bf42', + '0x84ff489c1e07c75ac635914d4fa20bb37b30f7cf37a8fb85298a88e6f45daab122b43a352abce2132bdde96fd4a01599', + ], + vettedKeys: [ + { + key: '0x83fc58f68d913481e065c928b040ae8b157ef2b32371b7df93d40188077c619dc789d443c18ac4a9b7e76de5ed6c8247', + depositSignature: + '0xa13833d96f4b98291dbf428cb69e7a3bdce61c9d20efcdb276423c7d6199ebd10cf1728dbd418c592701a41983cb02330e736610be254f617140af48a9d20b31cdffdd1d4fc8c0776439fca3330337d33042768acf897000b9e5da386077be44', + operatorIndex: 28, + used: false, + moduleAddress: '0x11a93807078f8BB880c1BD0ee4C387537de4b4b6', + index: 4, + }, + ], + nonce: 69, + blockHash: + '0x40c697def4d4f7233b75149ab941462582bb5f035b5089f7c6a3d7849222f47c', + stakingModuleId: 2, + }, + ], + vettedKeys: [ + { + key: '0x9948d2becf42e9f76922bc6f664545e6f50401050af95785a984802d32a95c4c61f8e3de312b78167f86e047f83a7796', + depositSignature: + '0x8bf4401a354de243a3716ee2efc0bde1ded56a40e2943ac7c50290bec37e935d6170b21e7c0872f203199386143ef12612a1488a8e9f1cdf1229c382f29c326bcbf6ed6a87d8fbfe0df87dacec6632fc4709d9d338f4cf81e861d942c23bba1e', + operatorIndex: 0, + used: false, + moduleAddress: '0x595F64Ddc3856a3b5Ff4f4CC1d1fb4B46cFd2bAC', + index: 100, + }, + { + key: '0x83fc58f68d913481e065c928b040ae8b157ef2b32371b7df93d40188077c619dc789d443c18ac4a9b7e76de5ed6c8247', + depositSignature: + '0xa13833d96f4b98291dbf428cb69e7a3bdce61c9d20efcdb276423c7d6199ebd10cf1728dbd418c592701a41983cb02330e736610be254f617140af48a9d20b31cdffdd1d4fc8c0776439fca3330337d33042768acf897000b9e5da386077be44', + operatorIndex: 28, + used: false, + moduleAddress: '0x11a93807078f8BB880c1BD0ee4C387537de4b4b6', + index: 4, + }, + ], + }); + }); +});