diff --git a/README.md b/README.md index ab5b88e1..457b0ef5 100644 --- a/README.md +++ b/README.md @@ -2,861 +2,13 @@ Simple Lido keys and validators HTTP API. -## How it works - -Service is based on modular architecture. We will have separate library for each type of `Staking Router` module: `Curated`, `Community`, `DVT`. These modules data will be stored in separate tables too. For example, we will have `CuratedKey` and `CommunityKey` tables. API will run cron job to update keys in db for all modules to `latest EL block`. At the moment API support only `NodeOperatorRegistry` contract keys. - ## Glossary - SR - Staking Router contract ## API -### /keys - -**GET** `/v1/keys` - -Endpoint returns list of keys for all modules. - -Query: - -- `used` - filter for used/unused keys. Possible values: true/false; -- `operatorIndex` - filter for keys of operator with index `operatorIndex`; - -```typescript -interface ELBlockSnapshot { - blockNumber: number; - blockHash: string; - blockTimestamp: number; -} - -interface Key { - key: string; - depositSignature: string; - used: boolean; - operatorIndex: number; -} - -interface ModuleKey extends Key { - moduleAddress: string; -} - -class Response { - data: ModuleKey[]; - meta: { - elBlockSnapshot: ELBlockSnapshot; - }; -} -interface HttpException { - statusCode: number; - message: string; -} - -class TooEarlyResponse implements HttpException { - statusCode: number = 425; - message: string = 'Too early response'; -} -``` - -Example: - -``` -curl -X 'GET' \ - 'http://localhost:3000/v1/keys' \ - -H 'accept: application/json' -``` - -:::warning -Response of this endpoint could be very large but we can’t have a pagination here since data could be updated in the process. -::: - -:::warning -If API returns 425 code, it means database is not ready for work -::: - -**GET** `/v1/keys/{pubkey}` - -Return key by public key with basic fields. `pubkey` should be in lowercase. - -```typescript -class Response { - data: ModuleKey[]; - meta: { - elBlockSnapshot: ELBlockSnapshot; - }; -} - -interface HttpException { - statusCode: number; - message: string; -} - -class TooEarlyResponse implements HttpException { - statusCode: number = 425; - message: string = 'Too early response'; -} - -class NotFoundException implements HttpException { - statusCode: number = 404; -} -``` - -Example: - -``` -curl -X 'GET' \ - 'http://localhost:3005/v1/keys/pubkey' \ - -H 'accept: application/json' -``` - -**POST** `/v1/keys/find` - -Returns all keys found in db. - -Request body: - -```typescript -interface RequestBody { - // public keys in lowercase - pubkeys: string[]; -} -``` - -Response: - -```typescript -class Response { - data: ModuleKey[]; - meta: { - elBlockSnapshot: ElBlockSnapshot; - }; -} - -interface HttpException { - statusCode: number; - message: string; -} - -class TooEarlyResponse implements HttpException { - statusCode: number = 425; - message: string = 'Too early response'; -} -``` - -Example: - -``` -curl -X 'POST' \ - 'http://localhost:3000/v1/keys/find' \ - -H 'accept: application/json' \ - -H 'Content-Type: application/json' \ - -d '{"pubkeys": ["pubkey0 "]}' -``` - -:::warning -If API returns 425 code, it means database is not ready for work -::: - -### /modules - -**GET** `/v1/modules` - -Endpoint returns list of staking router modules. - -```typescript -interface Module { - nonce: number; - type: string; - // unique id of the module - id: number; - // address of module - stakingModuleAddress: string; - // rewarf fee of the module - moduleFee: number; - // treasury fee - treasuryFee: number; - // target percent of total keys in protocol, in BP - targetShare: number; - // module status if module can not accept the deposits or can participate in further reward distribution - status: number; - // name of module - name: string; - // block.timestamp of the last deposit of the module - lastDepositAt: number; - // block.number of the last deposit of the module - lastDepositBlock: number; -} - -interface ELBlockSnapshot { - blockNumber: number; - blockHash: string; - blockTimestamp: number; -} - -class Reponse { - data: Module[]; - elBlockSnapshot: ElBlockSnapshot; -} - -interface HttpException { - statusCode: number; - message: string; -} - -class TooEarlyResponse implements HttpException { - statusCode: number = 425; - message: string = 'Too early response'; -} -``` - -Example: - -``` -curl -X 'GET' \ - 'http://localhost:3000/v1/modules' \ - -H 'accept: application/json' -``` - -:::warning -If API returns 425 code, it means database is not ready for work -::: - -**GET** `/v1/modules/{module_id}` - -`module_id` - staking router module contact address or id; - -Endpoint return information about staking router module; - -```typescript -interface Module { - nonce: number; - type: string; - /// @notice unique id of the module - id: number; - /// @notice address of module - stakingModuleAddress: string; - /// @notice rewarf fee of the module - moduleFee: number; - /// @notice treasury fee - treasuryFee: number; - /// @notice target percent of total keys in protocol, in BP - targetShare: number; - /// @notice module status if module can not accept the deposits or can participate in further reward distribution - status: number; - /// @notice name of module - name: string; - /// @notice block.timestamp of the last deposit of the module - lastDepositAt: number; - /// @notice block.number of the last deposit of the module - lastDepositBlock: number; -} - -interface ELBlockSnapshot { - blockNumber: number; - blockHash: string; - blockTimestamp: number; -} - -class Reponse { - data: Module; - elBlockSnapshot: ElBlockSnapshot; -} - -interface HttpException { - statusCode: number; - message: string; -} - -class TooEarlyResponse implements HttpException { - statusCode: number = 425; - message: string = 'Too early response'; -} - -class NotFoundResponse implements HttpException { - statusCode: number = 404; -} -``` - -Example: - -``` -curl -X 'GET' \ - 'http://localhost:3000/v1/modules/1' \ - -H 'accept: application/json' -``` - -:::warning -If API returns 425 code, it means database is not ready for work -::: - -### /modules/keys/ - -**GET** `/v1/modules/keys/` - -Return keys for all modules grouped by staking router module. - -Query: - -- `used` - filter for used/unused keys. Possible values: true/false; -- `operatorIndex` - filter for keys of operator with index `operatorIndex`; - -```typescript -interface Module { - // current KeyOpIndex - nonce: number; - // type of module - type: string; - /// @notice unique id of the module - id: number; - /// @notice address of module - stakingModuleAddress: string; - /// @notice rewarf fee of the module - moduleFee: number; - /// @notice treasury fee - treasuryFee: number; - /// @notice target percent of total keys in protocol, in BP - targetShare: number; - /// @notice module status if module can not accept the deposits or can participate in further reward distribution - status: number; - /// @notice name of module - name: string; - /// @notice block.timestamp of the last deposit of the module - lastDepositAt: number; - /// @notice block.number of the last deposit of the module - lastDepositBlock: number; -} - -class Response { - data: { - keys: Key[]; - module: Module; - }[]; - meta: { - elBlockSnapshot: ELBlockSnapshot; - }; -} - -interface HttpException { - statusCode: number; - message: string; -} - -class TooEarlyResponse implements HttpException { - statusCode: number = 425; - message: string = 'Too early response'; -} - -class NotFoundException implements HttpException { - statusCode: number = 404; -} -``` - -Example: - -``` -curl -X 'GET' \ - 'http://localhost:3000/v1/modules/keys?used=true&operatorIndex=1' \ - -H 'accept: application/json' -``` - -:::warning -If API returns 425 code, it means database is not ready for work -::: - -**GET** `/v1/modules/{module_id}/keys` - -`module_id` - staking router module contact address or id; - -Endpoint returns list of keys for module. - -Query: - -- `used` - filter for used/unused keys. Possible values: true/false; -- `operatorIndex` - filter for keys of operator with index `operatorIndex`; - -Response: - -Response depends on `module type` - -```typescript -interface Key { - key: string; - depositSignature: string; - used: boolean; - operatorIndex: number; -} - -interface RegistryKey extends Key { - index: number; -} - -interface CommunityKey extends Key {} - -class Response { - data: { - keys: RegistryKey[] | CommunityKey[]; - module: Module; - }; - meta: { - elBlockSnapshot: ELBlockSnapshot; - }; -} - -interface HttpException { - statusCode: number; - message: string; -} - -class TooEarlyResponse implements HttpException { - statusCode: number = 425; - message: string = 'Too early response'; -} - -class NotFoundException implements HttpException { - statusCode: number = 404; -} -``` - -Example: - -``` -curl -X 'GET' \ - 'http://localhost:3000/v1/modules/1/keys?used=true&operatorIndex=1' \ - -H 'accept: application/json' -``` - -:::warning -If API returns 425 code, it means database is not ready for work -::: - -**POST** `/v1/modules/{module_id}/keys/find` - -`module_id` - staking router module contact address or id; - -Returns all keys found in db. - -Request body: - -```typescript -interface RequestBody { - // public keys in lowercase - pubkeys: string[]; -} -``` - -Response: - -```typescript -interface Key { - key: string; - depositSignature: string; - used: boolean; - operatorIndex: number; -} - -interface RegistryKey extends Key { - index: number; -} - -interface CommunityKey extends Key {} - -class Response { - data: { - keys: RegistryKey[] | CommunityKey[]; - module: Module; - }; - meta: { - elBlockSnapshot: ElBlockSnapshot; - }; -} - -interface HttpException { - statusCode: number; - message: string; -} - -class TooEarlyResponse implements HttpException { - statusCode: number = 425; - message: string = 'Too early response'; -} - -class NotFoundException implements HttpException { - statusCode: number = 404; -} -``` - -Example: - -``` -curl -X 'POST' \ - 'http://localhost:3000/v1/modules/1/keys/find' \ - -H 'accept: application/json' \ - -H 'Content-Type: application/json' \ - -d '{ - "pubkeys": [ - "pubkey" - ] -}' -``` - -:::warning -If API returns 425 code, it means database is not ready for work -::: - -### /validators - -**GET** `/v1/modules/{module_id}/validators/validator-exits-to-prepare/{operator_id}` - -`module_id` - staking router module contact address or id; - -This endpoint will return N of oldest lido validators for earliest epoch when voluntary exit can be processed for specific node operator and specific `StakingRouter` module. Node operator will use this validators list for preparing pre-sign exit messages. API will find `used` keys of node operator and find for these public keys N oldest validators in `Validator` table. We consider active validators (`active_ongoing` status) or validators in `pending_initialized`, `pending_queued` statuses. Module tables state fetched from `EL` should be newer than `Validator` table state fetched from `CL`. Otherwise API will return error on request. - -Query: - -Only one filter is available. If both parameters are provided, `percent` has a high priority. - -- `percent` - Percent of validators to exit. Default value is 10. -- `max_amount` - Number of validators to exit. If validators number less than amount, return all validators. - -```typescript -interface Validator { - validatorIndex: number; - key: string; -} - -interface CLBlockSnapshot { - epoch: number; - root: number; - slot: number; - blockNumber: number; - timestamp: number; - blockHash: string; -} - -class Response { - data: Validator[]; - meta: { - clBlockSnapshot: CLBlockSnapshot; - }; -} - -interface HttpException { - statusCode: number; - message: string; -} - -class TooEarlyResponse implements HttpException { - statusCode: number = 425; - message: string = 'Too early response'; -} - -class NotFoundException implements HttpException { - statusCode: number = 404; -} - -class InternalServerErrorExceptionNotActualData implements HttpException { - statusCode: number = 500; - message: string = 'Last Execution Layer block number in our database older than last Consensus Layer'; -} - -class InternalServerErrorExceptionDisable implements HttpException { - statusCode: number = 500; - message: string = 'Validators Registry is disabled. Check environment variables'; -} -``` - -Example: - -``` -curl -X 'GET' \ - 'http://localhost:3000/v1/modules/1/validators/validator-exits-to-prepare/1?percent=10' \ - -H 'accept: application/json' -``` - -:::warning -If API returns 425 code, it means database is not ready for work -::: - -**GET** `/v1/modules/{module_id}/validators/generate-unsigned-exit-messages/{operator_id}` - -`module_id` - staking router module contact address or id; - -Return unsigned exit messages for N oldest validators for earliest epoch when voluntary exit can be processed. - -Query: - -Only one filter is available. If both parameters are provided, `percent` has a high priority. - -- `percent` - Percent of validators to exit. Default value is 10. -- `max_amount` - Number of validators to exit. If validators number less than amount, return all validators. - -```typescript -interface ExitPresignMessage { - validator_index: string; - epoch: string; -} - -interface CLBlockSnapshot { - epoch: number; - root: string; - slot: number; - blockNumber: number; - timestamp: number; - blockHash: string; -} - -class Response { - data: ExitPresignMessage[]; - meta: { - clBlockSnapshot: CLBlockSnapshot; - }; -} - -interface HttpException { - statusCode: number; - message: string; -} - -class TooEarlyResponse implements HttpException { - statusCode: number = 425; - message: string = 'Too early response'; -} - -class NotFoundException implements HttpException { - statusCode: number = 404; -} - -class InternalServerErrorExceptionNotActualData implements HttpException { - statusCode: number = 500; - message: string = 'Last Execution Layer block number in our database older than last Consensus Layer'; -} - -class InternalServerErrorExceptionDisable implements HttpException { - statusCode: number = 500; - message: string = 'Validators Registry is disabled. Check environment variables'; -} -``` - -Example: - -``` -curl -X 'GET' \ - 'http://localhost:3000/v1/modules/1/validators/generate-unsigned-exit-messages/1?percent=10' \ - -H 'accept: application/json' -``` - -:::warning -If API returns 425 code, it means database is not ready for work -::: - -### /operators - -**GET** `/v1/operators` - -List of operators grouped by staking router module - -Query - -```typescript -interface Operator { - index: number; - active: boolean; -} - -interface CuratedOperator extends Operator { - name: string; - rewardAddress: string; - stakingLimit: number; - stoppedValidators: number; - totalSigningKeys: number; - usedSigningKeys: number; -} - -interface CommunityOperator extends Operator {} - -class Response { - data: { - operators: CuratedOperator[] | CommunityOperator[]; - module: Module; - }[]; - meta: { - elBlockSnapshot: ELBlockSnapshot; - }; -} - -interface HttpException { - statusCode: number; - message: string; -} - -class TooEarlyResponse implements HttpException { - statusCode: number = 425; - message: string = 'Too early response'; -} -``` - -Example: - -``` -curl -X 'GET' \ - 'http://localhost:3000/v1/operators' \ - -H 'accept: application/json' -``` - -:::warning -If API returns 425 code, it means database is not ready for work -::: - -**GET** `/v1/modules/{module_id}/operators/` - -`module_id` - staking router module contact address or id; - -List of SR module operators - -```typescript -class Response { - data: { - operators: CuratedOperator[] | CommunityOperator[]; - module: Module; - }; - meta: { - elBlockSnapshot: ELBlockSnapshot; - }; -} - -interface HttpException { - statusCode: number; - message: string; -} - -class TooEarlyResponse implements HttpException { - statusCode: number = 425; - message: string = 'Too early response'; -} - -class NotFoundException implements HttpException { - statusCode: number = 404; -} -``` - -Example: - -``` -curl -X 'GET' \ - 'http://localhost:3000/v1/modules/1/operators' \ - -H 'accept: application/json' -``` - -:::warning -If API returns 425 code, it means database is not ready for work -::: - -**GET** `/v1/modules/{module_id}/operators/{operator_id}` - -`module_id` - staking router module contact address or id; -`operator_id` - operator index; - -List of SR module operators - -```typescript -class Response { - data: { - operators: CuratedOperator | CommunityOperator; - module: Module; - }; - meta: { - elBlockSnapshot: ELBlockSnapshot; - }; -} - -interface HttpException { - statusCode: number; - message: string; -} - -class TooEarlyResponse implements HttpException { - statusCode: number = 425; - message: string = 'Too early response'; -} - -class NotFoundException implements HttpException { - statusCode: number = 404; -} -``` - -Example: - -``` -curl -X 'GET' \ - 'http://localhost:3005/v1/modules/1/operators/1' \ - -H 'accept: application/json' -``` - -:::warning -If API returns 425 code, it means database is not ready for work -::: - -**GET** `/v1/modules/{module_id}/operators/keys` - -`module_id` - staking router module contact address or id; - -Query: - -- `used` - filter for used/unused keys. Possible values: true/false; -- `operatorIndex` - filter for keys of operator with index `operatorIndex`; - -```typescript -class Response { - data: { - keys: RegistryKey[] | CommunityKey[]; - operators: CuratedOperator[] | CommunityOperator[]; - module: Module; - }; - meta: { - elBlockSnapshot: ELBlockSnapshot; - }; -} - -interface HttpException { - statusCode: number; - message: string; -} - -class TooEarlyResponse implements HttpException { - statusCode: number = 425; - message: string = 'Too early response'; -} - -class NotFoundException implements HttpException { - statusCode: number = 404; -} -``` - -Example: - -``` -curl -X 'GET' \ - 'http://localhost:3005/v1/modules/1/operators/1' \ - -H 'accept: application/json' -``` - -:::warning -If API returns 425 code, it means database is not ready for work -::: - -### /status - -**GET** /v1/status - -```typescript -class Response { - // keys api version - appVersion: string; - chainId: number; - elBlockSnapshot: ELBlockSnapshot; - clBlockSnapshot: CLBlockSnapshot; -} -``` +You can familiarize yourself with the REST API by accessing it [here](rest-api.md). ## Requirements @@ -883,13 +35,11 @@ For running locally in container run To configure grafana go to `http://localhost:8000/dashboards` and dashboards from `./grafana` folder. -For running KAPI you can also use image from this page https://docs.lido.fi/guides/tooling#keys-api. Please always use the SHA256 hash of the Docker image for the latest release: lidofinance/lido-keys-api@. - -## Tests +For running KAPI, one can also use the image from this page https://docs.lido.fi/guides/tooling#keys-api. Please always use the SHA256 hash of the Docker image for the latest release: lidofinance/lido-keys-api@. -Unit tests +## E2E tests -`$ yarn test` +`$ yarn test:e2e` ## Environment variable diff --git a/jest-e2e.json b/jest-e2e.json index c9155380..98dd1ffd 100644 --- a/jest-e2e.json +++ b/jest-e2e.json @@ -1,6 +1,7 @@ { "moduleFileExtensions": ["js", "json", "ts"], - "rootDir": "src", + "rootDir": ".", + "modulePaths": ["/src"], "testEnvironment": "node", "testRegex": ".e2e-spec.ts$", "transform": { diff --git a/rest-api.md b/rest-api.md new file mode 100644 index 00000000..ef8eeeda --- /dev/null +++ b/rest-api.md @@ -0,0 +1,751 @@ +## API + +### Keys of staking modules + +> :warning: If API returns 425 code, it means database is not ready for work :warning: + +#### List keys + +Path: `/v1/keys` + +Returns list of keys for all modules. + +> :warning: Response of this endpoint could be very large. However, due to the possibility of updates occurring during processing, pagination is not supported. :warning: + +Query: + +- `used` - used/unused keys. Possible values: true/false; +- `operatorIndex` - operator index; + +Request example: + +`curl http://localhost:3000/v1/keys` + +Response: + +```typescript +interface ELBlockSnapshot { + blockNumber: number; + blockHash: string; + blockTimestamp: number; +} + +interface Key { + index: number; + key: string; + depositSignature: string; + used: boolean; + operatorIndex: number; + moduleAddress: string; +} + +class Response { + data: Key[]; + meta: { + elBlockSnapshot: ELBlockSnapshot; + }; +} +interface HttpException { + statusCode: number; + message: string; +} + +class TooEarlyResponse implements HttpException { + statusCode: number = 425; + message: string = 'Too early response'; +} +``` + +#### Find key with duplicates + +Path: `/v1/keys/{pubkey}` + +Returns keys associated with a given public key. `pubkey` should be in lowercase. + +Parameters: + +- `pubkey` - public key + +Request example: + +`curl http://localhost:3005/v1/keys/pubkey` + +Response: + +```typescript +class Response { + data: Key[]; + meta: { + elBlockSnapshot: ELBlockSnapshot; + }; +} + +interface HttpException { + statusCode: number; + message: string; +} + +class TooEarlyResponse implements HttpException { + statusCode: number = 425; + message: string = 'Too early response'; +} + +class NotFoundException implements HttpException { + statusCode: number = 404; +} +``` + +#### Find keys by list of public keys + +Path: `/v1/keys/find` + +Returns all keys associated with provided list of keys. + +Request body: + +```typescript +interface RequestBody { + // public keys in lowercase + pubkeys: string[]; +} +``` + +Request example: + +`curl -X POST http://localhost:3000/v1/keys/find -H 'Content-Type: application/json' -d '{"pubkeys":["pubkey0 "]}'` + +Response: + +```typescript +class Response { + data: Key[]; + meta: { + elBlockSnapshot: ElBlockSnapshot; + }; +} + +interface HttpException { + statusCode: number; + message: string; +} + +class TooEarlyResponse implements HttpException { + statusCode: number = 425; + message: string = 'Too early response'; +} +``` + +### Staking modules + +#### List of staking modules + +Path: `/v1/modules` + +Returns list of staking modules. + +Request example: + +`curl http://localhost:3000/v1/modules` + +Response: + +```typescript +interface Module { + nonce: number; + type: string; + // unique id of the module + id: number; + // address of module + stakingModuleAddress: string; + // rewarf fee of the module + moduleFee: number; + // treasury fee + treasuryFee: number; + // target percent of total keys in protocol, in BP + targetShare: number; + // module status if module can not accept the deposits or can participate in further reward distribution + status: number; + // name of module + name: string; + // block.timestamp of the last deposit of the module + lastDepositAt: number; + // block.number of the last deposit of the module + lastDepositBlock: number; +} + +interface ELBlockSnapshot { + blockNumber: number; + blockHash: string; + blockTimestamp: number; +} + +class Reponse { + data: Module[]; + elBlockSnapshot: ElBlockSnapshot; +} + +interface HttpException { + statusCode: number; + message: string; +} + +class TooEarlyResponse implements HttpException { + statusCode: number = 425; + message: string = 'Too early response'; +} +``` + +#### Find staking module + +Path: `/v1/modules/{module_id}` + +Returns staking module details. + +Parameters: + +- `module_id` - staking module contact address or id; + +Request example: + +`curl http://localhost:3000/v1/modules/1` + +Response: + +```typescript +interface Module { + nonce: number; + type: string; + /// @notice unique id of the module + id: number; + /// @notice address of module + stakingModuleAddress: string; + /// @notice reward fee of the module + moduleFee: number; + /// @notice treasury fee + treasuryFee: number; + /// @notice target percent of total keys in protocol, in BP + targetShare: number; + /// @notice module status if module can not accept the deposits or can participate in further reward distribution + status: number; + /// @notice name of module + name: string; + /// @notice block.timestamp of the last deposit of the module + lastDepositAt: number; + /// @notice block.number of the last deposit of the module + lastDepositBlock: number; +} + +interface ELBlockSnapshot { + blockNumber: number; + blockHash: string; + blockTimestamp: number; +} + +class Reponse { + data: Module; + elBlockSnapshot: ElBlockSnapshot; +} + +interface HttpException { + statusCode: number; + message: string; +} + +class TooEarlyResponse implements HttpException { + statusCode: number = 425; + message: string = 'Too early response'; +} + +class NotFoundResponse implements HttpException { + statusCode: number = 404; +} +``` + +### Keys of staking module + +#### List keys grouped by modules + +Path: `/v1/modules/keys/` + +Returns keys for all modules grouped by staking module. + +Query: + +- `used` - used/unused keys. Possible values: true/false; +- `operatorIndex` - operator index; + +Request example: + +`curl http://localhost:3000/v1/modules/keys?used=true&operatorIndex=1` + +Response: + +```typescript +class Response { + data: { + keys: Key[]; + module: Module; + }[]; + meta: { + elBlockSnapshot: ELBlockSnapshot; + }; +} + +interface HttpException { + statusCode: number; + message: string; +} + +class TooEarlyResponse implements HttpException { + statusCode: number = 425; + message: string = 'Too early response'; +} + +class NotFoundException implements HttpException { + statusCode: number = 404; +} +``` + +#### List keys of staking module + +The endpoint returns a list of staking module keys. + +Path: `/v1/modules/{module_id}/keys` + +Parameters: + +- `module_id` - staking module contact address or id; + +Query: + +- `used` - used/unused keys. Possible values: true/false; +- `operatorIndex` - operator index; + +Request example: + +`curl http://localhost:3000/v1/modules/1/keys?used=true&operatorIndex=1` + +Response: + +```typescript +class Response { + data: { + keys: Key[]; + module: Module; + }; + meta: { + elBlockSnapshot: ELBlockSnapshot; + }; +} + +interface HttpException { + statusCode: number; + message: string; +} + +class TooEarlyResponse implements HttpException { + statusCode: number = 425; + message: string = 'Too early response'; +} + +class NotFoundException implements HttpException { + statusCode: number = 404; +} +``` + +#### Find keys of staking module by list of public keys + +Path: `/v1/modules/{module_id}/keys/find` + +Returns all keys found in db. + +Parameters: + +`module_id` - staking module contact address or id; + +Request body: + +```typescript +interface RequestBody { + // public keys in lowercase + pubkeys: string[]; +} +``` + +Request example: + +`curl -X POST http://localhost:3000/v1/modules/1/keys/find -d '{"pubkeys": ["pubkey"]}'` + +Response: + +```typescript +class Response { + data: { + keys: Key[]; + module: Module; + }; + meta: { + elBlockSnapshot: ElBlockSnapshot; + }; +} + +interface HttpException { + statusCode: number; + message: string; +} + +class TooEarlyResponse implements HttpException { + statusCode: number = 425; + message: string = 'Too early response'; +} + +class NotFoundException implements HttpException { + statusCode: number = 404; +} +``` + +### Validators + +#### Lists N oldest lido validators + +Path: `/v1/modules/{module_id}/validators/validator-exits-to-prepare/{operator_id}` + +Parameters: + +- `module_id` - staking module contact address or id; +- `operator_id` - operator index; + +This endpoint will return N of oldest lido validators for earliest epoch when voluntary exit can be processed for specific node operator and specific `StakingRouter` module. Node operator will use this validators list for preparing pre-sign exit messages. API will find `used` keys of node operator and find for these public keys N oldest validators. We consider active validators (`active_ongoing` status) or validators in `pending_initialized`, `pending_queued` statuses. `block_number` fetched from `EL` should be newer or equal to `slot` fetched from `CL`. Otherwise API will return error on request. + +Query: + +Only one filter is available. If both parameters are provided, `percent` has a high priority. + +- `percent` - Percent of validators to exit. Default value is 10. +- `max_amount` - Number of validators to exit. If validators number less than amount, return all validators. + +Request Example: + +`curl http://localhost:3000/v1/modules/1/validators/validator-exits-to-prepare/1?percent=10` + +Response: + +```typescript +interface Validator { + validatorIndex: number; + key: string; +} + +interface CLBlockSnapshot { + epoch: number; + root: number; + slot: number; + blockNumber: number; + timestamp: number; + blockHash: string; +} + +class Response { + data: Validator[]; + meta: { + clBlockSnapshot: CLBlockSnapshot; + }; +} + +interface HttpException { + statusCode: number; + message: string; +} + +class TooEarlyResponse implements HttpException { + statusCode: number = 425; + message: string = 'Too early response'; +} + +class NotFoundException implements HttpException { + statusCode: number = 404; +} + +class InternalServerErrorExceptionNotActualData implements HttpException { + statusCode: number = 500; + message: string = 'Last Execution Layer block number in our database older than last Consensus Layer'; +} + +class InternalServerErrorExceptionDisable implements HttpException { + statusCode: number = 500; + message: string = 'Validators Registry is disabled. Check environment variables'; +} +``` + +#### Returns unsigned exit messages for N oldest validators + +Path: `/v1/modules/{module_id}/validators/generate-unsigned-exit-messages/{operator_id}` + +Parameters: + +- `module_id` - staking module contact address or id; + +Returns unsigned exit messages for N oldest validators for earliest epoch when voluntary exit can be processed. + +Query: + +Only one filter is available. If both parameters are provided, `percent` has a high priority. + +- `percent` - Percent of validators to exit. Default value is 10. +- `max_amount` - Number of validators to exit. If validators number less than amount, return all validators. + +Request example: + +`curl http://localhost:3000/v1/modules/1/validators/generate-unsigned-exit-messages/1?percent=10` + +Response: + +```typescript +interface ExitPresignMessage { + validator_index: string; + epoch: string; +} + +interface CLBlockSnapshot { + epoch: number; + root: string; + slot: number; + blockNumber: number; + timestamp: number; + blockHash: string; +} + +class Response { + data: ExitPresignMessage[]; + meta: { + clBlockSnapshot: CLBlockSnapshot; + }; +} + +interface HttpException { + statusCode: number; + message: string; +} + +class TooEarlyResponse implements HttpException { + statusCode: number = 425; + message: string = 'Too early response'; +} + +class NotFoundException implements HttpException { + statusCode: number = 404; +} + +class InternalServerErrorExceptionNotActualData implements HttpException { + statusCode: number = 500; + message: string = 'Last Execution Layer block number in our database older than last Consensus Layer'; +} + +class InternalServerErrorExceptionDisable implements HttpException { + statusCode: number = 500; + message: string = 'Validators Registry is disabled. Check environment variables'; +} +``` + +### Operators + +#### List all operators for all staking modules + +Path: `/v1/operators` + +Returns operators grouped by staking module + +Request example: + +`curl http://localhost:3000/v1/operators` + +Response: + +```typescript +interface Operator { + index: number; + active: boolean; + name: string; + rewardAddress: string; + stakingLimit: number; + stoppedValidators: number; + totalSigningKeys: number; + usedSigningKeys: number; + moduleAddress: string; +} + +class Response { + data: { + operators: Operator[]; + module: Module; + }[]; + meta: { + elBlockSnapshot: ELBlockSnapshot; + }; +} + +interface HttpException { + statusCode: number; + message: string; +} + +class TooEarlyResponse implements HttpException { + statusCode: number = 425; + message: string = 'Too early response'; +} +``` + +#### List operators of staking module + +Path: `/v1/modules/{module_id}/operators/` + +Returns list of staking module operators. + +Query: + +- `module_id` - staking module contact address or id; + +Request example: + +`curl http://localhost:3000/v1/modules/1/operators` + +Response: + +```typescript +class Response { + data: { + operators: Operator[]; + module: Module; + }; + meta: { + elBlockSnapshot: ELBlockSnapshot; + }; +} + +interface HttpException { + statusCode: number; + message: string; +} + +class TooEarlyResponse implements HttpException { + statusCode: number = 425; + message: string = 'Too early response'; +} + +class NotFoundException implements HttpException { + statusCode: number = 404; +} +``` + +#### Find staking module operator + +Path: `/v1/modules/{module_id}/operators/{operator_id}` + +Returns staking module operator by operator index. + +Query: + +- `module_id` - staking module contact address or id; +- `operator_id` - operator index; + +Request example: + +`curl http://localhost:3005/v1/modules/1/operators/1` + +Response: + +```typescript +class Response { + data: { + operators: Operator; + module: Module; + }; + meta: { + elBlockSnapshot: ELBlockSnapshot; + }; +} + +interface HttpException { + statusCode: number; + message: string; +} + +class TooEarlyResponse implements HttpException { + statusCode: number = 425; + message: string = 'Too early response'; +} + +class NotFoundException implements HttpException { + statusCode: number = 404; +} +``` + +### Staking module operators and keys + +#### List staking module operators and keys + +Path: `/v1/modules/{module_id}/operators/keys` + +Parameters: + +- `module_id` - staking module contact address or id; + +Query: + +- `used` - used/unused keys. Possible values: true/false; +- `operatorIndex` - operator index; + +Request example: + +`curl 'http://localhost:3000/v1/modules/1/operators/1` + +Response: + +```typescript +class Response { + data: { + keys: Key[]; + operators: Operator[]; + module: Module; + }; + meta: { + elBlockSnapshot: ELBlockSnapshot; + }; +} + +interface HttpException { + statusCode: number; + message: string; +} + +class TooEarlyResponse implements HttpException { + statusCode: number = 425; + message: string = 'Too early response'; +} + +class NotFoundException implements HttpException { + statusCode: number = 404; +} +``` + +### KAPI status + +#### Return KAPI status + +Path: /v1/status + +Request example: + +`curl https://keys-api.infra-staging.org/v1/status` + +Response: + +```typescript +class Response { + // keys api version + appVersion: string; + chainId: number; + elBlockSnapshot: ELBlockSnapshot; + clBlockSnapshot: CLBlockSnapshot; +} +``` diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 9efe2c5a..3ef024a2 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -11,12 +11,12 @@ import { ConsensusProviderModule } from '../common/consensus-provider'; import { MikroOrmModule } from '@mikro-orm/nestjs'; import { JobsModule } from '../jobs'; import { ScheduleModule } from '@nestjs/schedule'; -import config from 'mikro-orm.config'; +import config from '../mikro-orm.config'; import { ValidatorsModule } from '../validators'; import { LoggerModule } from '@lido-nestjs/logger'; import { SimpleFallbackJsonRpcBatchProvider } from '@lido-nestjs/execution'; -import { KeyRegistryModule } from 'common/registry'; -import { StakingRouterModule } from 'staking-router-modules'; +import { KeyRegistryModule } from '../common/registry'; +import { StakingRouterModule } from '../staking-router-modules'; @Module({ imports: [ @@ -46,9 +46,9 @@ import { StakingRouterModule } from 'staking-router-modules'; }), ScheduleModule.forRoot(), KeyRegistryModule.forRootAsync({ - inject: [SimpleFallbackJsonRpcBatchProvider], - async useFactory(provider) { - return { provider }; + inject: [SimpleFallbackJsonRpcBatchProvider, ConfigService], + async useFactory(provider, configService) { + return { provider, keysBatchSize: configService?.get('KEYS_FETCH_BATCH_SIZE') }; }, }), StakingRouterModule, diff --git a/src/common/config/env.validation.ts b/src/common/config/env.validation.ts index 7f224643..5ae3a860 100644 --- a/src/common/config/env.validation.ts +++ b/src/common/config/env.validation.ts @@ -11,6 +11,7 @@ import { IsBoolean, ValidateIf, IsNotEmpty, + IsPositive, } from 'class-validator'; import { Environment, LogLevel, LogFormat } from './interfaces'; import { NonEmptyArray } from '@lido-nestjs/execution/dist/interfaces/non-empty-array'; @@ -169,6 +170,11 @@ export class EnvironmentVariables { @IsOptional() @IsString() LIDO_LOCATOR_ADDRESS = ''; + + @IsOptional() + @IsPositive() + @Transform(({ value }) => parseInt(value, 10)) + KEYS_FETCH_BATCH_SIZE = 1100; } export function validate(config: Record) { diff --git a/src/common/registry/fetch/interfaces/module.interface.ts b/src/common/registry/fetch/interfaces/module.interface.ts index c6b4c5db..aa1c7776 100644 --- a/src/common/registry/fetch/interfaces/module.interface.ts +++ b/src/common/registry/fetch/interfaces/module.interface.ts @@ -3,10 +3,13 @@ import { ModuleMetadata } from '@nestjs/common'; import { Signer } from 'ethers'; import { Provider } from '@ethersproject/providers'; +export const REGISTRY_FETCH_OPTIONS_TOKEN = Symbol('registryFetchOptionsToken'); + export interface RegistryFetchOptions { registryAddress?: string; lidoAddress?: string; provider?: Provider | Signer; + keysBatchSize?: number; } export interface RegistryFetchModuleSyncOptions extends Pick, RegistryFetchOptions {} diff --git a/src/common/registry/fetch/key-batch.constants.ts b/src/common/registry/fetch/key-batch.constants.ts index b6d300f7..a300595b 100644 --- a/src/common/registry/fetch/key-batch.constants.ts +++ b/src/common/registry/fetch/key-batch.constants.ts @@ -5,3 +5,4 @@ */ export const KEYS_LENGTH = 96; export const SIGNATURE_LENGTH = 192; +export const KEYS_BATCH_SIZE = 1100; diff --git a/src/common/registry/fetch/key-batch.fetch.ts b/src/common/registry/fetch/key-batch.fetch.ts index 1d36d23f..9a0b9d19 100644 --- a/src/common/registry/fetch/key-batch.fetch.ts +++ b/src/common/registry/fetch/key-batch.fetch.ts @@ -3,13 +3,15 @@ import { REGISTRY_CONTRACT_TOKEN, Registry } from '@lido-nestjs/contracts'; import { CallOverrides } from './interfaces/overrides.interface'; import { KeyBatchRecord, RegistryKey } from './interfaces/key.interface'; import { RegistryOperatorFetchService } from './operator.fetch'; -import { KEYS_LENGTH, SIGNATURE_LENGTH } from './key-batch.constants'; +import { KEYS_BATCH_SIZE, KEYS_LENGTH, SIGNATURE_LENGTH } from './key-batch.constants'; +import { RegistryFetchOptions, REGISTRY_FETCH_OPTIONS_TOKEN } from './interfaces/module.interface'; @Injectable() export class RegistryKeyBatchFetchService { constructor( protected readonly operatorsService: RegistryOperatorFetchService, @Inject(REGISTRY_CONTRACT_TOKEN) private contract: Registry, + @Inject(REGISTRY_FETCH_OPTIONS_TOKEN) private options: RegistryFetchOptions, ) {} private getContract(moduleAddress: string) { @@ -107,8 +109,7 @@ export class RegistryKeyBatchFetchService { fromIndex: number, totalAmount: number, ) { - // TODO: move to constants/config cause this limit depends on eth node - const batchSize = 1100; + const batchSize = this.options.keysBatchSize || KEYS_BATCH_SIZE; const numberOfBatches = Math.ceil(totalAmount / batchSize); const promises: Promise[] = []; diff --git a/src/common/registry/fetch/meta.fetch.ts b/src/common/registry/fetch/meta.fetch.ts index dbaa99c7..bb6eaa54 100644 --- a/src/common/registry/fetch/meta.fetch.ts +++ b/src/common/registry/fetch/meta.fetch.ts @@ -1,6 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { Inject, Injectable } from '@nestjs/common'; -// import { Registry__factory } from '@lido-nestjs/contracts'; import { CallOverrides } from './interfaces/overrides.interface'; import { REGISTRY_CONTRACT_TOKEN, Registry } from '@lido-nestjs/contracts'; @@ -12,10 +11,9 @@ export class RegistryMetaFetchService { return this.contract.attach(moduleAddress); } - /** fetches keys operation index */ - public async fetchKeysOpIndex(moduleAddress: string, overrides: CallOverrides = {}): Promise { - // TODO: read data from all contract that implement curated-v1-onchain type - const bigNumber = await this.getContract(moduleAddress).getKeysOpIndex(overrides as any); + /** Fetches nonce from staking module contract */ + public async fetchStakingModuleNonce(moduleAddress: string, overrides: CallOverrides = {}): Promise { + const bigNumber = await this.getContract(moduleAddress).getNonce(overrides as any); return bigNumber.toNumber(); } } diff --git a/src/common/registry/fetch/registry-fetch.module.ts b/src/common/registry/fetch/registry-fetch.module.ts index ba8e69a6..4949f32b 100644 --- a/src/common/registry/fetch/registry-fetch.module.ts +++ b/src/common/registry/fetch/registry-fetch.module.ts @@ -1,6 +1,10 @@ import { DynamicModule, Module } from '@nestjs/common'; import { LidoContractModule, RegistryContractModule } from '@lido-nestjs/contracts'; -import { RegistryFetchModuleSyncOptions, RegistryFetchModuleAsyncOptions } from './interfaces/module.interface'; +import { + RegistryFetchModuleSyncOptions, + RegistryFetchModuleAsyncOptions, + REGISTRY_FETCH_OPTIONS_TOKEN, +} from './interfaces/module.interface'; import { RegistryOperatorFetchService } from './operator.fetch'; import { RegistryMetaFetchService } from './meta.fetch'; import { RegistryKeyFetchService } from './key.fetch'; @@ -52,6 +56,12 @@ export class RegistryFetchModule { provider: options?.provider, }), ], + providers: [ + { + provide: REGISTRY_FETCH_OPTIONS_TOKEN, + useValue: options?.keysBatchSize ? { keysBatchSize: options.keysBatchSize } : {}, + }, + ], exports: [LidoContractModule, RegistryContractModule], }; } @@ -61,7 +71,6 @@ export class RegistryFetchModule { module: RegistryFetchModule, imports: [ ...(options.imports || []), - // TODO: why we need it here? LidoContractModule.forFeatureAsync({ async useFactory(...args) { const config = await options.useFactory(...args); @@ -81,6 +90,13 @@ export class RegistryFetchModule { inject: options.inject, }), ], + providers: [ + { + provide: REGISTRY_FETCH_OPTIONS_TOKEN, + useFactory: options.useFactory, + inject: options.inject, + }, + ], exports: [LidoContractModule, RegistryContractModule], }; } diff --git a/src/common/registry/main/abstract-registry.ts b/src/common/registry/main/abstract-registry.ts index 3dd80fff..77ff6e64 100644 --- a/src/common/registry/main/abstract-registry.ts +++ b/src/common/registry/main/abstract-registry.ts @@ -110,7 +110,6 @@ export abstract class AbstractRegistryService { // TODO: use feature flag const result = await this.keyBatchFetch.fetch(moduleAddress, operatorIndex, fromIndex, toIndex, overrides); - // add moduleAddress const operatorKeys = result.filter((key) => key); this.logger.log('Keys fetched', { @@ -133,12 +132,10 @@ export abstract class AbstractRegistryService { /** returns the latest operators data from the db */ public async getOperatorsFromStorage(moduleAddress: string) { - // TODO: find for module return await this.operatorStorage.findAll(moduleAddress); } /** returns all the keys from storage */ - // the same public async getOperatorsKeysFromStorage(moduleAddress: string) { return await this.keyStorage.findAll(moduleAddress); } @@ -161,12 +158,12 @@ export abstract class AbstractRegistryService { /** contract */ /** returns the meta data from the contract */ - public async getNonceFromContract(moduleAddress: string, blockHash: string): Promise { - const keysOpIndex = await this.metaFetch.fetchKeysOpIndex(moduleAddress, { blockTag: { blockHash } }); + public async getStakingModuleNonce(moduleAddress: string, blockHash: string): Promise { + const keysOpIndex = await this.metaFetch.fetchStakingModuleNonce(moduleAddress, { blockTag: { blockHash } }); return keysOpIndex; } - /** saves all the data to the db */ + /** saves all data to the db for staking module*/ public async saveOperators(moduleAddress: string, currentOperators: RegistryOperator[]) { // save all data in a transaction await this.entityManager.transactional(async (entityManager) => { @@ -189,7 +186,6 @@ export abstract class AbstractRegistryService { await entityManager .createQueryBuilder(RegistryOperator) .insert(operatorsChunk) - // TODO: module_address or moduleAddress ? .onConflict(['index', 'module_address']) .merge() .execute(); @@ -197,12 +193,4 @@ export abstract class AbstractRegistryService { ); }); } - - /** clears the db */ - public async clear() { - await this.entityManager.transactional(async (entityManager) => { - entityManager.nativeDelete(RegistryKey, {}); - entityManager.nativeDelete(RegistryOperator, {}); - }); - } } diff --git a/src/common/registry/main/key-registry/key-registry.service.ts b/src/common/registry/main/key-registry/key-registry.service.ts index 80e5ec6f..41b6b245 100644 --- a/src/common/registry/main/key-registry/key-registry.service.ts +++ b/src/common/registry/main/key-registry/key-registry.service.ts @@ -7,7 +7,7 @@ export class KeyRegistryService extends AbstractRegistryService { return currOperator.totalSigningKeys; } /** returns all operators keys from the db */ - public async getAllKeysFromStorage(moduleAddress: string) { + public async getModuleKeysFromStorage(moduleAddress: string) { return await this.keyStorage.findAll(moduleAddress); } /** returns used keys from the db */ diff --git a/src/common/registry/storage/constants.ts b/src/common/registry/storage/constants.ts index c521d0b9..9544f1d1 100644 --- a/src/common/registry/storage/constants.ts +++ b/src/common/registry/storage/constants.ts @@ -1,4 +1,3 @@ export const KEY_LEN = 98; export const DEPOSIT_SIGNATURE_LEN = 194; -export const MODULE_ADDRESS_LEN = 42; -export const REWARD_ADDRESS_LEN = 42; +export const ADDRESS_LEN = 42; diff --git a/src/common/registry/storage/key.entity.ts b/src/common/registry/storage/key.entity.ts index 6486f120..9b3ef39b 100644 --- a/src/common/registry/storage/key.entity.ts +++ b/src/common/registry/storage/key.entity.ts @@ -1,5 +1,5 @@ import { Entity, EntityRepositoryType, PrimaryKey, PrimaryKeyType, Property } from '@mikro-orm/core'; -import { DEPOSIT_SIGNATURE_LEN, MODULE_ADDRESS_LEN, KEY_LEN } from './constants'; +import { DEPOSIT_SIGNATURE_LEN, ADDRESS_LEN, KEY_LEN } from './constants'; import { RegistryKeyRepository } from './key.repository'; @Entity({ customRepository: () => RegistryKeyRepository }) @@ -32,6 +32,6 @@ export class RegistryKey { used!: boolean; @PrimaryKey() - @Property({ length: MODULE_ADDRESS_LEN }) + @Property({ length: ADDRESS_LEN }) moduleAddress!: string; } diff --git a/src/common/registry/storage/operator.entity.ts b/src/common/registry/storage/operator.entity.ts index 63943b85..2538a588 100644 --- a/src/common/registry/storage/operator.entity.ts +++ b/src/common/registry/storage/operator.entity.ts @@ -1,5 +1,5 @@ import { Entity, EntityRepositoryType, PrimaryKey, PrimaryKeyType, Property } from '@mikro-orm/core'; -import { MODULE_ADDRESS_LEN, REWARD_ADDRESS_LEN } from './constants'; +import { ADDRESS_LEN } from './constants'; import { RegistryOperatorRepository } from './operator.repository'; @Entity({ customRepository: () => RegistryOperatorRepository }) @@ -28,7 +28,7 @@ export class RegistryOperator { @Property({ length: 256 }) name!: string; - @Property({ length: REWARD_ADDRESS_LEN }) + @Property({ length: ADDRESS_LEN }) rewardAddress!: string; @Property() @@ -44,6 +44,6 @@ export class RegistryOperator { usedSigningKeys!: number; @PrimaryKey() - @Property({ length: MODULE_ADDRESS_LEN }) + @Property({ length: ADDRESS_LEN }) moduleAddress!: string; } diff --git a/src/common/registry/test/fetch/meta.fetch.e2e-spec.ts b/src/common/registry/test/fetch/meta.fetch.e2e-spec.ts index 480708c5..839bf408 100644 --- a/src/common/registry/test/fetch/meta.fetch.e2e-spec.ts +++ b/src/common/registry/test/fetch/meta.fetch.e2e-spec.ts @@ -23,7 +23,7 @@ describe('Operators', () => { }); test('fetch keysOpIndex', async () => { - const keysOpIndex = await fetchService.fetchKeysOpIndex(address); + const keysOpIndex = await fetchService.fetchStakingModuleNonce(address); expect(typeof keysOpIndex).toBe('number'); expect(keysOpIndex).toBeGreaterThan(0); }); diff --git a/src/common/registry/test/fetch/meta.fetch.spec.ts b/src/common/registry/test/fetch/meta.fetch.spec.ts index cd3c6159..d9ee6420 100644 --- a/src/common/registry/test/fetch/meta.fetch.spec.ts +++ b/src/common/registry/test/fetch/meta.fetch.spec.ts @@ -36,7 +36,7 @@ describe('Meta', () => { const iface = new Interface(Registry__factory.abi); return iface.encodeFunctionResult('getKeysOpIndex', [expected]); }); - const result = await fetchService.fetchKeysOpIndex(address); + const result = await fetchService.fetchStakingModuleNonce(address); expect(result).toEqual(expected); expect(mockCall).toBeCalledTimes(1); diff --git a/src/common/registry/test/fetch/operator.fetch.spec.ts b/src/common/registry/test/fetch/operator.fetch.spec.ts index 8597430d..7eb4da6b 100644 --- a/src/common/registry/test/fetch/operator.fetch.spec.ts +++ b/src/common/registry/test/fetch/operator.fetch.spec.ts @@ -43,7 +43,6 @@ describe('Operators', () => { mockCall.mockImplementation(async () => { const iface = new Interface(Registry__factory.abi); - // TODO: moduleAddress depends on chain id operator['moduleAddress'] = address; return iface.encodeFunctionResult('getNodeOperator', operatorFields(operator)); }); diff --git a/src/common/registry/test/key-registry/async.spec.ts b/src/common/registry/test/key-registry/async.spec.ts index f6dd59fa..90fb4355 100644 --- a/src/common/registry/test/key-registry/async.spec.ts +++ b/src/common/registry/test/key-registry/async.spec.ts @@ -6,6 +6,7 @@ import { getNetwork } from '@ethersproject/networks'; import { getDefaultProvider } from '@ethersproject/providers'; import { KeyRegistryModule, KeyRegistryService, RegistryStorageService } from '../../'; import { MikroORM } from '@mikro-orm/core'; +import { mikroORMConfig } from '../testing.utils'; describe('Async module initializing', () => { const provider = getDefaultProvider('mainnet'); @@ -26,12 +27,7 @@ describe('Async module initializing', () => { test('forRootAsync', async () => { await testModules([ - MikroOrmModule.forRoot({ - dbName: ':memory:', - type: 'sqlite', - allowGlobalContext: true, - entities: ['./**/*.entity.ts'], - }), + MikroOrmModule.forRoot(mikroORMConfig), LoggerModule.forRoot({ transports: [nullTransport()] }), KeyRegistryModule.forRootAsync({ async useFactory() { diff --git a/src/common/registry/test/key-registry/connect.e2e-spec.ts b/src/common/registry/test/key-registry/connect.e2e-spec.ts index e0cfdd69..ec6e1f7e 100644 --- a/src/common/registry/test/key-registry/connect.e2e-spec.ts +++ b/src/common/registry/test/key-registry/connect.e2e-spec.ts @@ -5,7 +5,7 @@ import { BatchProviderModule, ExtendedJsonRpcBatchProvider } from '@lido-nestjs/ import { KeyRegistryModule, KeyRegistryService, RegistryStorageService } from '../../'; -import { compareTestMetaOperators } from '../testing.utils'; +import { clearDb, compareTestOperators, mikroORMConfig } from '../testing.utils'; import { operators } from '../fixtures/connect.fixture'; import { MikroORM } from '@mikro-orm/core'; @@ -17,6 +17,7 @@ dotenv.config(); describe('Registry', () => { let registryService: KeyRegistryService; let storageService: RegistryStorageService; + let mikroOrm: MikroORM; if (!process.env.CHAIN_ID) { console.error("CHAIN_ID wasn't provides"); process.exit(1); @@ -28,12 +29,7 @@ describe('Registry', () => { beforeEach(async () => { const imports = [ - MikroOrmModule.forRoot({ - dbName: ':memory:', - type: 'sqlite', - allowGlobalContext: true, - entities: ['./**/*.entity.ts'], - }), + MikroOrmModule.forRoot(mikroORMConfig), BatchProviderModule.forRoot({ url: process.env.PROVIDERS_URLS as string, requestPolicy: { @@ -53,13 +49,13 @@ describe('Registry', () => { const moduleRef = await Test.createTestingModule({ imports }).compile(); registryService = moduleRef.get(KeyRegistryService); storageService = moduleRef.get(RegistryStorageService); - - const generator = moduleRef.get(MikroORM).getSchemaGenerator(); + mikroOrm = moduleRef.get(MikroORM); + const generator = mikroOrm.getSchemaGenerator(); await generator.updateSchema(); }); afterEach(async () => { - await registryService.clear(); + await clearDb(mikroOrm); await storageService.onModuleDestroy(); }); @@ -68,7 +64,7 @@ describe('Registry', () => { await registryService.update(address, blockHash); - await compareTestMetaOperators(address, registryService, { + await compareTestOperators(address, registryService, { operators: operatorsWithModuleAddress, }); diff --git a/src/common/registry/test/key-registry/registry-db.spec.ts b/src/common/registry/test/key-registry/registry-db.e2e-spec.ts similarity index 84% rename from src/common/registry/test/key-registry/registry-db.spec.ts rename to src/common/registry/test/key-registry/registry-db.e2e-spec.ts index 41645c82..a104a51a 100644 --- a/src/common/registry/test/key-registry/registry-db.spec.ts +++ b/src/common/registry/test/key-registry/registry-db.e2e-spec.ts @@ -9,9 +9,9 @@ import { RegistryStorageService, RegistryKeyStorageService, RegistryOperatorStorageService, -} from '../../'; +} from '../..'; import { keys, operators } from '../fixtures/db.fixture'; -import { compareTestMeta } from '../testing.utils'; +import { clearDb, compareTestMeta, mikroORMConfig } from '../testing.utils'; import { MikroORM } from '@mikro-orm/core'; import { REGISTRY_CONTRACT_ADDRESSES } from '@lido-nestjs/contracts'; @@ -33,20 +33,15 @@ describe('Registry', () => { let keyStorageService: RegistryKeyStorageService; let operatorStorageService: RegistryOperatorStorageService; + let mikroOrm: MikroORM; const mockCall = jest.spyOn(provider, 'call').mockImplementation(async () => ''); - // TODO: why we fix here mainnet jest.spyOn(provider, 'detectNetwork').mockImplementation(async () => getNetwork('mainnet')); beforeEach(async () => { const imports = [ - MikroOrmModule.forRoot({ - dbName: ':memory:', - type: 'sqlite', - allowGlobalContext: true, - entities: ['./**/*.entity.ts'], - }), + MikroOrmModule.forRoot(mikroORMConfig), LoggerModule.forRoot({ transports: [nullTransport()] }), KeyRegistryModule.forFeature({ provider }), ]; @@ -57,11 +52,10 @@ describe('Registry', () => { keyStorageService = moduleRef.get(RegistryKeyStorageService); operatorStorageService = moduleRef.get(RegistryOperatorStorageService); - const generator = moduleRef.get(MikroORM).getSchemaGenerator(); + mikroOrm = moduleRef.get(MikroORM); + const generator = mikroOrm.getSchemaGenerator(); await generator.updateSchema(); - // TODO: should we change name of this test from spec -> e2e-spec - await keyStorageService.save(keysWithModuleAddress); await operatorStorageService.save(operatorsWithModuleAddress); @@ -69,7 +63,7 @@ describe('Registry', () => { afterEach(async () => { mockCall.mockReset(); - await registryService.clear(); + await clearDb(mikroOrm); await registryStorageService.onModuleDestroy(); }); diff --git a/src/common/registry/test/key-registry/registry-update.spec.ts b/src/common/registry/test/key-registry/registry-update.spec.ts index ce6e2e19..f47765fd 100644 --- a/src/common/registry/test/key-registry/registry-update.spec.ts +++ b/src/common/registry/test/key-registry/registry-update.spec.ts @@ -11,7 +11,14 @@ import { RegistryOperatorStorageService, } from '../../'; import { keys, newKey, newOperator, operators, operatorWithDefaultsRecords } from '../fixtures/db.fixture'; -import { clone, compareTestMeta, compareTestMetaKeys, compareTestMetaOperators } from '../testing.utils'; +import { + clone, + compareTestMeta, + compareTestKeys, + compareTestOperators, + mikroORMConfig, + clearDb, +} from '../testing.utils'; import { registryServiceMock } from '../mock-utils'; import { MikroORM } from '@mikro-orm/core'; import { REGISTRY_CONTRACT_ADDRESSES } from '@lido-nestjs/contracts'; @@ -37,6 +44,7 @@ describe('Registry', () => { let keyStorageService: RegistryKeyStorageService; let operatorStorageService: RegistryOperatorStorageService; + let mikroOrm: MikroORM; let moduleRef: TestingModule; @@ -46,12 +54,7 @@ describe('Registry', () => { beforeEach(async () => { const imports = [ - MikroOrmModule.forRoot({ - dbName: ':memory:', - type: 'sqlite', - allowGlobalContext: true, - entities: ['./**/*.entity.ts'], - }), + MikroOrmModule.forRoot(mikroORMConfig), LoggerModule.forRoot({ transports: [nullTransport()] }), KeyRegistryModule.forFeature({ provider }), ]; @@ -63,7 +66,8 @@ describe('Registry', () => { keyStorageService = moduleRef.get(RegistryKeyStorageService); operatorStorageService = moduleRef.get(RegistryOperatorStorageService); - const generator = moduleRef.get(MikroORM).getSchemaGenerator(); + mikroOrm = moduleRef.get(MikroORM); + const generator = mikroOrm.getSchemaGenerator(); await generator.updateSchema(); await keyStorageService.save(keysWithModuleAddress); @@ -72,7 +76,7 @@ describe('Registry', () => { afterEach(async () => { mockCall.mockReset(); - await registryService.clear(); + await clearDb(mikroOrm); await registryStorageService.onModuleDestroy(); }); @@ -86,7 +90,9 @@ describe('Registry', () => { operators: operatorsWithModuleAddress, }); - await registryService.update(address, 'latest'); + const blockHash = '0x4ef0f15a8a04a97f60a9f76ba83d27bcf98dac9635685cd05fe1d78bd6e93418'; + + await registryService.update(address, blockHash); expect(saveRegistryMock).toBeCalledTimes(1); // 2 - number of operators expect(saveKeyRegistryMock).toBeCalledTimes(2); @@ -106,9 +112,11 @@ describe('Registry', () => { operators: operatorsWithModuleAddress, }); + const blockHash = '0x4ef0f15a8a04a97f60a9f76ba83d27bcf98dac9635685cd05fe1d78bd6e93418'; + // update function doesn't make a decision about update no more // so here would happen update if list of keys was changed - await registryService.update(address, 'latest'); + await registryService.update(address, blockHash); expect(saveRegistryMock).toBeCalledTimes(1); expect(saveKeyRegistryMock).toBeCalledTimes(2); }); @@ -125,11 +133,13 @@ describe('Registry', () => { operators: operatorsWithModuleAddress, }); - await registryService.update(address, 'latest'); + const blockHash = '0x4ef0f15a8a04a97f60a9f76ba83d27bcf98dac9635685cd05fe1d78bd6e93418'; + + await registryService.update(address, blockHash); expect(saveRegistryMock).toBeCalledTimes(1); expect(saveKeyRegistryMock.mock.calls.length).toBeGreaterThanOrEqual(1); - await compareTestMetaKeys(address, registryService, { keys: keysWithModuleAddress }); - await compareTestMetaOperators(address, registryService, { operators: operatorsWithModuleAddress }); + await compareTestKeys(address, registryService, { keys: keysWithModuleAddress }); + await compareTestOperators(address, registryService, { operators: operatorsWithModuleAddress }); }); test('looking for totalSigningKeys', async () => { @@ -146,12 +156,14 @@ describe('Registry', () => { operators: newOperators, }); - await registryService.update(address, 'latest'); + const blockHash = '0x4ef0f15a8a04a97f60a9f76ba83d27bcf98dac9635685cd05fe1d78bd6e93418'; + + await registryService.update(address, blockHash); expect(saveRegistryMock).toBeCalledTimes(1); expect(saveKeyRegistryMock.mock.calls.length).toBeGreaterThanOrEqual(1); - await compareTestMetaKeys(address, registryService, { keys: newKeys }); - await compareTestMetaOperators(address, registryService, { + await compareTestKeys(address, registryService, { keys: newKeys }); + await compareTestOperators(address, registryService, { operators: newOperators, }); }); @@ -167,12 +179,14 @@ describe('Registry', () => { operators: newOperators, }); - await registryService.update(address, 'latest'); + const blockHash = '0x4ef0f15a8a04a97f60a9f76ba83d27bcf98dac9635685cd05fe1d78bd6e93418'; + + await registryService.update(address, blockHash); expect(saveOperatorRegistryMock).toBeCalledTimes(1); expect(saveKeyRegistryMock.mock.calls.length).toBeGreaterThanOrEqual(1); - await compareTestMetaKeys(address, registryService, { keys: keysWithModuleAddress }); - await compareTestMetaOperators(address, registryService, { + await compareTestKeys(address, registryService, { keys: keysWithModuleAddress }); + await compareTestOperators(address, registryService, { operators: newOperators, }); }); @@ -192,11 +206,13 @@ describe('Registry', () => { operators: newOperators, }); - await registryService.update(address, 'latest'); + const blockHash = '0x4ef0f15a8a04a97f60a9f76ba83d27bcf98dac9635685cd05fe1d78bd6e93418'; + + await registryService.update(address, blockHash); expect(saveOperatorsRegistryMock).toBeCalledTimes(1); expect(saveKeyRegistryMock.mock.calls.length).toBeGreaterThanOrEqual(1); - await compareTestMetaKeys(address, registryService, { keys: keysWithModuleAddress }); - await compareTestMetaOperators(address, registryService, { + await compareTestKeys(address, registryService, { keys: keysWithModuleAddress }); + await compareTestOperators(address, registryService, { operators: newOperators, }); }); @@ -213,11 +229,13 @@ describe('Registry', () => { operators: newOperators, }); - await registryService.update(address, 'latest'); + const blockHash = '0x4ef0f15a8a04a97f60a9f76ba83d27bcf98dac9635685cd05fe1d78bd6e93418'; + + await registryService.update(address, blockHash); expect(saveRegistryMock).toBeCalledTimes(1); expect(saveKeyRegistryMock.mock.calls.length).toBeGreaterThanOrEqual(1); - await compareTestMetaKeys(address, registryService, { keys: keysWithModuleAddress }); - await compareTestMetaOperators(address, registryService, { + await compareTestKeys(address, registryService, { keys: keysWithModuleAddress }); + await compareTestOperators(address, registryService, { operators: newOperators, }); }); @@ -233,15 +251,17 @@ describe('Registry', () => { keys: keysWithModuleAddress, operators: newOperators, }); - await registryService.update(address, 'latest'); + const blockHash = '0x4ef0f15a8a04a97f60a9f76ba83d27bcf98dac9635685cd05fe1d78bd6e93418'; + + await registryService.update(address, blockHash); expect(saveRegistryMock).toBeCalledTimes(1); expect(saveKeyRegistryMock.mock.calls.length).toBeGreaterThanOrEqual(1); - await compareTestMetaOperators(address, registryService, { + await compareTestOperators(address, registryService, { operators: newOperators, }); const firstOperatorKeys = await ( - await registryService.getAllKeysFromStorage(address) + await registryService.getModuleKeysFromStorage(address) ).filter(({ operatorIndex }) => operatorIndex === 0); expect(firstOperatorKeys.length).toBe(newOperators[0].totalSigningKeys); }); diff --git a/src/common/registry/test/key-registry/registry.spec.ts b/src/common/registry/test/key-registry/registry.spec.ts index 941779f7..9b24949f 100644 --- a/src/common/registry/test/key-registry/registry.spec.ts +++ b/src/common/registry/test/key-registry/registry.spec.ts @@ -8,6 +8,7 @@ import { key } from '../fixtures/key.fixture'; import { RegistryKeyStorageService, KeyRegistryModule, KeyRegistryService, RegistryStorageService } from '../../'; import { MikroORM } from '@mikro-orm/core'; import { REGISTRY_CONTRACT_ADDRESSES } from '@lido-nestjs/contracts'; +import { mikroORMConfig } from '../testing.utils'; describe('Key', () => { const CHAIN_ID = process.env.CHAIN_ID || 1; @@ -23,12 +24,7 @@ describe('Key', () => { beforeEach(async () => { const imports = [ - MikroOrmModule.forRoot({ - dbName: ':memory:', - type: 'sqlite', - allowGlobalContext: true, - entities: ['./**/*.entity.ts'], - }), + MikroOrmModule.forRoot(mikroORMConfig), LoggerModule.forRoot({ transports: [nullTransport()] }), KeyRegistryModule.forFeature({ provider }), ]; @@ -52,11 +48,11 @@ describe('Key', () => { expect(validatorService.getToIndex({ totalSigningKeys: expected } as any)).toBe(expected); }); - test('getAllKeysFromStorage', async () => { + test('getModuleKeysFromStorage', async () => { const expected = [{ index: 0, operatorIndex: 0, moduleAddress: address, ...key, used: false }]; jest.spyOn(keyStorage, 'findAll').mockImplementation(async () => expected); - await expect(validatorService.getAllKeysFromStorage(address)).resolves.toBe(expected); + await expect(validatorService.getModuleKeysFromStorage(address)).resolves.toBe(expected); }); test('getUsedKeysFromStorage', async () => { diff --git a/src/common/registry/test/key-registry/sync.spec.ts b/src/common/registry/test/key-registry/sync.spec.ts index 51729337..dd101cd0 100644 --- a/src/common/registry/test/key-registry/sync.spec.ts +++ b/src/common/registry/test/key-registry/sync.spec.ts @@ -6,6 +6,7 @@ import { getDefaultProvider, Provider } from '@ethersproject/providers'; import { Test } from '@nestjs/testing'; import { KeyRegistryModule, KeyRegistryService, RegistryStorageService } from '../../index'; import { MikroORM } from '@mikro-orm/core'; +import { mikroORMConfig } from '../testing.utils'; describe('Sync module initializing', () => { const provider = getDefaultProvider('mainnet'); @@ -26,12 +27,7 @@ describe('Sync module initializing', () => { test('forRoot', async () => { const imports = [ - MikroOrmModule.forRoot({ - dbName: ':memory:', - type: 'sqlite', - allowGlobalContext: true, - entities: ['./**/*.entity.ts'], - }), + MikroOrmModule.forRoot(mikroORMConfig), LoggerModule.forRoot({ transports: [nullTransport()] }), KeyRegistryModule.forRoot({ provider, diff --git a/src/common/registry/test/storage/async.spec.ts b/src/common/registry/test/storage/async.spec.ts index 0e71d3bc..c84e5615 100644 --- a/src/common/registry/test/storage/async.spec.ts +++ b/src/common/registry/test/storage/async.spec.ts @@ -4,6 +4,7 @@ import { ModuleMetadata } from '@nestjs/common'; import { Test } from '@nestjs/testing'; import { RegistryStorageModule, RegistryStorageService } from '../../'; import { MikroORM } from '@mikro-orm/core'; +import { mikroORMConfig } from '../testing.utils'; @Injectable() class TestService {} @@ -35,12 +36,7 @@ describe('Async module initializing', () => { test('forRootAsync', async () => { await testModules([ TestModule.forRoot(), - MikroOrmModule.forRoot({ - dbName: ':memory:', - type: 'sqlite', - allowGlobalContext: true, - entities: ['./**/*.entity.ts'], - }), + MikroOrmModule.forRoot(mikroORMConfig), RegistryStorageModule.forRootAsync({ async useFactory() { return {}; @@ -53,12 +49,7 @@ describe('Async module initializing', () => { test('forFeatureAsync', async () => { await testModules([ TestModule.forRoot(), - MikroOrmModule.forRoot({ - dbName: ':memory:', - type: 'sqlite', - allowGlobalContext: true, - entities: ['./**/*.entity.ts'], - }), + MikroOrmModule.forRoot(mikroORMConfig), RegistryStorageModule.forFeatureAsync({ async useFactory() { return {}; diff --git a/src/common/registry/test/storage/key.storage.e2e-spec.ts b/src/common/registry/test/storage/key.storage.e2e-spec.ts index cd7daf9a..d90bcc9f 100644 --- a/src/common/registry/test/storage/key.storage.e2e-spec.ts +++ b/src/common/registry/test/storage/key.storage.e2e-spec.ts @@ -4,29 +4,21 @@ import { MikroOrmModule } from '@mikro-orm/nestjs'; import { key } from '../fixtures/key.fixture'; import { RegistryStorageModule, RegistryStorageService, RegistryKeyStorageService } from '../../'; import { REGISTRY_CONTRACT_ADDRESSES } from '@lido-nestjs/contracts'; -import * as dotenv from 'dotenv'; - -dotenv.config(); +import { ConfigService } from '../../../config'; +import { mikroORMConfig } from '../testing.utils'; describe('Keys', () => { let storageService: RegistryKeyStorageService; let registryService: RegistryStorageService; - if (!process.env.CHAIN_ID) { + const configService: ConfigService = new ConfigService(); + if (!configService.get('CHAIN_ID')) { console.error("CHAIN_ID wasn't provides"); process.exit(1); } - const address = REGISTRY_CONTRACT_ADDRESSES[process.env.CHAIN_ID]; + const address = REGISTRY_CONTRACT_ADDRESSES[configService.get('CHAIN_ID')]; beforeEach(async () => { - const imports = [ - MikroOrmModule.forRoot({ - dbName: ':memory:', - type: 'sqlite', - allowGlobalContext: true, - entities: ['./**/*.entity.ts'], - }), - RegistryStorageModule.forFeature(), - ]; + const imports = [MikroOrmModule.forRoot(mikroORMConfig), RegistryStorageModule.forFeature()]; const moduleRef = await Test.createTestingModule({ imports }).compile(); storageService = moduleRef.get(RegistryKeyStorageService); diff --git a/src/common/registry/test/storage/operator.storage.e2e-spec.ts b/src/common/registry/test/storage/operator.storage.e2e-spec.ts index 6eea2587..f3eaecdc 100644 --- a/src/common/registry/test/storage/operator.storage.e2e-spec.ts +++ b/src/common/registry/test/storage/operator.storage.e2e-spec.ts @@ -4,29 +4,21 @@ import { MikroOrmModule } from '@mikro-orm/nestjs'; import { operator } from '../fixtures/operator.fixture'; import { RegistryStorageModule, RegistryStorageService, RegistryOperatorStorageService } from '../../'; import { REGISTRY_CONTRACT_ADDRESSES } from '@lido-nestjs/contracts'; -import * as dotenv from 'dotenv'; - -dotenv.config(); +import { ConfigService } from '../../../config'; +import { mikroORMConfig } from '../testing.utils'; describe('Operators', () => { let storageService: RegistryOperatorStorageService; let registryService: RegistryStorageService; - if (!process.env.CHAIN_ID) { + const configService: ConfigService = new ConfigService(); + if (!configService.get('CHAIN_ID')) { console.error("CHAIN_ID wasn't provides"); process.exit(1); } - const address = REGISTRY_CONTRACT_ADDRESSES[process.env.CHAIN_ID]; + const address = REGISTRY_CONTRACT_ADDRESSES[configService.get('CHAIN_ID')]; beforeEach(async () => { - const imports = [ - MikroOrmModule.forRoot({ - dbName: ':memory:', - type: 'sqlite', - allowGlobalContext: true, - entities: ['./**/*.entity.ts'], - }), - RegistryStorageModule.forFeature(), - ]; + const imports = [MikroOrmModule.forRoot(mikroORMConfig), RegistryStorageModule.forFeature()]; const moduleRef = await Test.createTestingModule({ imports }).compile(); storageService = moduleRef.get(RegistryOperatorStorageService); diff --git a/src/common/registry/test/storage/sync.spec.ts b/src/common/registry/test/storage/sync.spec.ts index c6e80b15..c6792174 100644 --- a/src/common/registry/test/storage/sync.spec.ts +++ b/src/common/registry/test/storage/sync.spec.ts @@ -3,6 +3,7 @@ import { ModuleMetadata } from '@nestjs/common'; import { Test } from '@nestjs/testing'; import { RegistryStorageModule, RegistryStorageService } from '../../'; import { MikroORM } from '@mikro-orm/core'; +import { mikroORMConfig } from '../testing.utils'; describe('Sync module initializing', () => { const testModules = async (imports: ModuleMetadata['imports']) => { @@ -17,26 +18,10 @@ describe('Sync module initializing', () => { }; test('forRoot', async () => { - await testModules([ - MikroOrmModule.forRoot({ - dbName: ':memory:', - type: 'sqlite', - allowGlobalContext: true, - entities: ['./**/*.entity.ts'], - }), - RegistryStorageModule.forRoot({}), - ]); + await testModules([MikroOrmModule.forRoot(mikroORMConfig), RegistryStorageModule.forRoot({})]); }); test('forFeature', async () => { - await testModules([ - MikroOrmModule.forRoot({ - dbName: ':memory:', - type: 'sqlite', - allowGlobalContext: true, - entities: ['./**/*.entity.ts'], - }), - RegistryStorageModule.forFeature(), - ]); + await testModules([MikroOrmModule.forRoot(mikroORMConfig), RegistryStorageModule.forFeature()]); }); }); diff --git a/src/common/registry/test/testing.utils.ts b/src/common/registry/test/testing.utils.ts index 91fa659a..f528e2e0 100644 --- a/src/common/registry/test/testing.utils.ts +++ b/src/common/registry/test/testing.utils.ts @@ -7,8 +7,7 @@ type Expected = { operators: RegistryOperator[]; }; -// TODO: why meta? if we compare keys -export const compareTestMetaKeys = async ( +export const compareTestKeys = async ( address: string, registryService: AbstractRegistryService, { keys }: Pick, @@ -21,8 +20,7 @@ export const compareTestMetaKeys = async ( expect(fetchedAndSorted).toEqual(sorted); }; -// TODO: why meta? if we compare operators -export const compareTestMetaOperators = async ( +export const compareTestOperators = async ( address: string, registryService: AbstractRegistryService, { operators }: Pick, @@ -35,13 +33,29 @@ export const compareTestMeta = async ( registryService: AbstractRegistryService, { keys, operators }: Expected, ) => { - await compareTestMetaKeys(address, registryService, { keys }); - await compareTestMetaOperators(address, registryService, { operators }); + await compareTestKeys(address, registryService, { keys }); + await compareTestOperators(address, registryService, { operators }); }; -// TODO: maybe add address as argument -export const fetchKeyMock = (fromIndex = 0, toIndex = 1, expected: Array) => { - return expected.splice(fromIndex, toIndex); +export const clone = (obj: T) => JSON.parse(JSON.stringify(obj)) as T; + +/** clears the db */ +// can we get rid of it? +export const clearDb = async (orm) => { + const em = orm.em; + + await em.transactional(async (em) => { + const keyRepository = em.getRepository(RegistryKey); + await keyRepository.nativeDelete({}); + + const operatorRepository = em.getRepository(RegistryKey); + await operatorRepository.nativeDelete({}); + }); }; -export const clone = (obj: T) => JSON.parse(JSON.stringify(obj)) as T; +export const mikroORMConfig = { + dbName: ':memory:', + type: 'sqlite' as const, + allowGlobalContext: true, + entities: ['./**/*.entity.ts'], +}; diff --git a/src/common/registry/test/validator-registry/async.spec.ts b/src/common/registry/test/validator-registry/async.spec.ts index db5ba5d1..5225f72c 100644 --- a/src/common/registry/test/validator-registry/async.spec.ts +++ b/src/common/registry/test/validator-registry/async.spec.ts @@ -6,6 +6,7 @@ import { getNetwork } from '@ethersproject/networks'; import { getDefaultProvider } from '@ethersproject/providers'; import { ValidatorRegistryModule, ValidatorRegistryService, RegistryStorageService } from '../../'; import { MikroORM } from '@mikro-orm/core'; +import { mikroORMConfig } from '../testing.utils'; describe('Async module initializing', () => { const provider = getDefaultProvider('mainnet'); @@ -26,12 +27,7 @@ describe('Async module initializing', () => { test('forRootAsync', async () => { await testModules([ - MikroOrmModule.forRoot({ - dbName: ':memory:', - type: 'sqlite', - allowGlobalContext: true, - entities: ['./**/*.entity.ts'], - }), + MikroOrmModule.forRoot(mikroORMConfig), LoggerModule.forRoot({ transports: [nullTransport()] }), ValidatorRegistryModule.forRootAsync({ async useFactory() { @@ -43,12 +39,7 @@ describe('Async module initializing', () => { test('forFeatureAsync', async () => { await testModules([ - MikroOrmModule.forRoot({ - dbName: ':memory:', - type: 'sqlite', - allowGlobalContext: true, - entities: ['./**/*.entity.ts'], - }), + MikroOrmModule.forRoot(mikroORMConfig), LoggerModule.forRoot({ transports: [nullTransport()] }), ValidatorRegistryModule.forFeatureAsync({ async useFactory() { diff --git a/src/common/registry/test/validator-registry/connect.e2e-spec.ts b/src/common/registry/test/validator-registry/connect.e2e-spec.ts index aff867ad..c4ae0b7d 100644 --- a/src/common/registry/test/validator-registry/connect.e2e-spec.ts +++ b/src/common/registry/test/validator-registry/connect.e2e-spec.ts @@ -5,7 +5,7 @@ import { BatchProviderModule, ExtendedJsonRpcBatchProvider } from '@lido-nestjs/ import { ValidatorRegistryModule, ValidatorRegistryService, RegistryStorageService } from '../../'; -import { compareTestMetaOperators } from '../testing.utils'; +import { clearDb, compareTestOperators } from '../testing.utils'; import { operators } from '../fixtures/connect.fixture'; import { MikroORM } from '@mikro-orm/core'; @@ -16,6 +16,7 @@ dotenv.config(); describe('Registry', () => { let registryService: ValidatorRegistryService; + let mikroOrm: MikroORM; let storageService: RegistryStorageService; if (!process.env.CHAIN_ID) { @@ -56,12 +57,13 @@ describe('Registry', () => { registryService = moduleRef.get(ValidatorRegistryService); storageService = moduleRef.get(RegistryStorageService); - const generator = moduleRef.get(MikroORM).getSchemaGenerator(); + mikroOrm = moduleRef.get(MikroORM); + const generator = mikroOrm.getSchemaGenerator(); await generator.updateSchema(); }); afterEach(async () => { - await registryService.clear(); + await clearDb(mikroOrm); await storageService.onModuleDestroy(); }); @@ -71,7 +73,7 @@ describe('Registry', () => { await registryService.update(address, blockHash); - await compareTestMetaOperators(address, registryService, { + await compareTestOperators(address, registryService, { operators: operatorsWithModuleAddress, }); const keys = await registryService.getOperatorsKeysFromStorage(address); diff --git a/src/common/registry/test/validator-registry/registry-db.spec.ts b/src/common/registry/test/validator-registry/registry-db.spec.ts index 2080219c..baa3302f 100644 --- a/src/common/registry/test/validator-registry/registry-db.spec.ts +++ b/src/common/registry/test/validator-registry/registry-db.spec.ts @@ -11,7 +11,7 @@ import { RegistryOperatorStorageService, } from '../../'; import { keys, operators } from '../fixtures/db.fixture'; -import { compareTestMeta } from '../testing.utils'; +import { clearDb, compareTestMeta, mikroORMConfig } from '../testing.utils'; import { MikroORM } from '@mikro-orm/core'; import { REGISTRY_CONTRACT_ADDRESSES } from '@lido-nestjs/contracts'; @@ -33,6 +33,7 @@ describe('Registry', () => { let keyStorageService: RegistryKeyStorageService; let operatorStorageService: RegistryOperatorStorageService; + let mikroOrm: MikroORM; const mockCall = jest.spyOn(provider, 'call').mockImplementation(async () => ''); @@ -40,12 +41,7 @@ describe('Registry', () => { beforeEach(async () => { const imports = [ - MikroOrmModule.forRoot({ - dbName: ':memory:', - type: 'sqlite', - allowGlobalContext: true, - entities: ['./**/*.entity.ts'], - }), + MikroOrmModule.forRoot(mikroORMConfig), LoggerModule.forRoot({ transports: [nullTransport()] }), ValidatorRegistryModule.forFeature({ provider }), ]; @@ -56,7 +52,8 @@ describe('Registry', () => { keyStorageService = moduleRef.get(RegistryKeyStorageService); operatorStorageService = moduleRef.get(RegistryOperatorStorageService); - const generator = moduleRef.get(MikroORM).getSchemaGenerator(); + mikroOrm = moduleRef.get(MikroORM); + const generator = mikroOrm.getSchemaGenerator(); await generator.updateSchema(); await keyStorageService.save(keysWithModuleAddress); @@ -65,7 +62,7 @@ describe('Registry', () => { afterEach(async () => { mockCall.mockReset(); - await registryService.clear(); + await clearDb(mikroOrm); await registryStorageService.onModuleDestroy(); }); diff --git a/src/common/registry/test/validator-registry/registry-update.spec.ts b/src/common/registry/test/validator-registry/registry-update.spec.ts index 58397802..76f996cd 100644 --- a/src/common/registry/test/validator-registry/registry-update.spec.ts +++ b/src/common/registry/test/validator-registry/registry-update.spec.ts @@ -11,7 +11,14 @@ import { RegistryOperatorStorageService, } from '../../'; import { keys, newKey, newOperator, operators, operatorWithDefaultsRecords } from '../fixtures/db.fixture'; -import { clone, compareTestMeta, compareTestMetaKeys, compareTestMetaOperators } from '../testing.utils'; +import { + clone, + compareTestMeta, + compareTestKeys, + compareTestOperators, + clearDb, + mikroORMConfig, +} from '../testing.utils'; import { registryServiceMock } from '../mock-utils'; import { MikroORM } from '@mikro-orm/core'; import { REGISTRY_CONTRACT_ADDRESSES } from '@lido-nestjs/contracts'; @@ -34,6 +41,7 @@ describe('Validator registry', () => { let keyStorageService: RegistryKeyStorageService; let operatorStorageService: RegistryOperatorStorageService; + let mikroOrm: MikroORM; let moduleRef: TestingModule; @@ -43,12 +51,7 @@ describe('Validator registry', () => { beforeEach(async () => { const imports = [ - MikroOrmModule.forRoot({ - dbName: ':memory:', - type: 'sqlite', - allowGlobalContext: true, - entities: ['./**/*.entity.ts'], - }), + MikroOrmModule.forRoot(mikroORMConfig), LoggerModule.forRoot({ transports: [nullTransport()] }), ValidatorRegistryModule.forFeature({ provider }), ]; @@ -60,7 +63,8 @@ describe('Validator registry', () => { keyStorageService = moduleRef.get(RegistryKeyStorageService); operatorStorageService = moduleRef.get(RegistryOperatorStorageService); - const generator = moduleRef.get(MikroORM).getSchemaGenerator(); + mikroOrm = moduleRef.get(MikroORM); + const generator = mikroOrm.getSchemaGenerator(); await generator.updateSchema(); await keyStorageService.save(keysWithModuleAddress); @@ -69,7 +73,7 @@ describe('Validator registry', () => { afterEach(async () => { mockCall.mockReset(); - await registryService.clear(); + await clearDb(mikroOrm); await registryStorageService.onModuleDestroy(); }); @@ -83,7 +87,9 @@ describe('Validator registry', () => { operators: operatorsWithModuleAddress, }); - await registryService.update(address, 'latest'); + const blockHash = '0x4ef0f15a8a04a97f60a9f76ba83d27bcf98dac9635685cd05fe1d78bd6e93418'; + + await registryService.update(address, blockHash); // update function doesn't make a decision about update no more // so here would happen update if list of keys was changed expect(saveOperatorRegistryMock).toBeCalledTimes(1); @@ -104,7 +110,9 @@ describe('Validator registry', () => { operators: operatorsWithModuleAddress, }); - await registryService.update(address, 'latest'); + const blockHash = '0x4ef0f15a8a04a97f60a9f76ba83d27bcf98dac9635685cd05fe1d78bd6e93418'; + + await registryService.update(address, blockHash); expect(saveRegistryMock).toBeCalledTimes(1); expect(saveKeyRegistryMock).toBeCalledTimes(2); }); @@ -121,11 +129,13 @@ describe('Validator registry', () => { operators: operatorsWithModuleAddress, }); - await registryService.update(address, 'latest'); + const blockHash = '0x4ef0f15a8a04a97f60a9f76ba83d27bcf98dac9635685cd05fe1d78bd6e93418'; + + await registryService.update(address, blockHash); expect(saveRegistryMock).toBeCalledTimes(1); expect(saveKeyRegistryMock.mock.calls.length).toBeGreaterThanOrEqual(1); - await compareTestMetaKeys(address, registryService, { keys: keysWithModuleAddress }); - await compareTestMetaOperators(address, registryService, { operators: operatorsWithModuleAddress }); + await compareTestKeys(address, registryService, { keys: keysWithModuleAddress }); + await compareTestOperators(address, registryService, { operators: operatorsWithModuleAddress }); }); test('looking only for used keys', async () => { @@ -141,11 +151,13 @@ describe('Validator registry', () => { operators: newOperators, }); - await registryService.update(address, 'latest'); + const blockHash = '0x4ef0f15a8a04a97f60a9f76ba83d27bcf98dac9635685cd05fe1d78bd6e93418'; + + await registryService.update(address, blockHash); expect(saveRegistryMock).toBeCalledTimes(1); expect(saveKeyRegistryMock.mock.calls.length).toBeGreaterThanOrEqual(1); - await compareTestMetaKeys(address, registryService, { keys: keysWithModuleAddress }); - await compareTestMetaOperators(address, registryService, { + await compareTestKeys(address, registryService, { keys: keysWithModuleAddress }); + await compareTestOperators(address, registryService, { operators: newOperators, }); }); @@ -160,11 +172,13 @@ describe('Validator registry', () => { operators: newOperators, }); - await registryService.update(address, 'latest'); + const blockHash = '0x4ef0f15a8a04a97f60a9f76ba83d27bcf98dac9635685cd05fe1d78bd6e93418'; + + await registryService.update(address, blockHash); expect(saveRegistryMock).toBeCalledTimes(1); expect(saveKeyRegistryMock.mock.calls.length).toBeGreaterThanOrEqual(1); - await compareTestMetaKeys(address, registryService, { keys: keysWithModuleAddress }); - await compareTestMetaOperators(address, registryService, { + await compareTestKeys(address, registryService, { keys: keysWithModuleAddress }); + await compareTestOperators(address, registryService, { operators: newOperators, }); }); @@ -183,11 +197,13 @@ describe('Validator registry', () => { operators: newOperators, }); - await registryService.update(address, 'latest'); + const blockHash = '0x4ef0f15a8a04a97f60a9f76ba83d27bcf98dac9635685cd05fe1d78bd6e93418'; + + await registryService.update(address, blockHash); expect(saveOperatorRegistryMock).toBeCalledTimes(1); expect(saveKeyRegistryMock.mock.calls.length).toBeGreaterThanOrEqual(1); - await compareTestMetaKeys(address, registryService, { keys: keysWithModuleAddress }); - await compareTestMetaOperators(address, registryService, { + await compareTestKeys(address, registryService, { keys: keysWithModuleAddress }); + await compareTestOperators(address, registryService, { operators: newOperators, }); }); @@ -208,11 +224,13 @@ describe('Validator registry', () => { operators: newOperators, }); - await registryService.update(address, 'latest'); + const blockHash = '0x4ef0f15a8a04a97f60a9f76ba83d27bcf98dac9635685cd05fe1d78bd6e93418'; + + await registryService.update(address, blockHash); expect(saveRegistryMock).toBeCalledTimes(1); expect(saveKeyRegistryMock.mock.calls.length).toBeGreaterThanOrEqual(1); - await compareTestMetaKeys(address, registryService, { keys: keysWithModuleAddress }); - await compareTestMetaOperators(address, registryService, { + await compareTestKeys(address, registryService, { keys: keysWithModuleAddress }); + await compareTestOperators(address, registryService, { operators: newOperators, }); }); @@ -228,10 +246,12 @@ describe('Validator registry', () => { operators: newOperators, }); - await registryService.update(address, 'latest'); + const blockHash = '0x4ef0f15a8a04a97f60a9f76ba83d27bcf98dac9635685cd05fe1d78bd6e93418'; + + await registryService.update(address, blockHash); expect(saveOperatorRegistryMock).toBeCalledTimes(1); expect(saveKeyRegistryMock.mock.calls.length).toBeGreaterThanOrEqual(1); - await compareTestMetaOperators(address, registryService, { + await compareTestOperators(address, registryService, { operators: newOperators, }); @@ -252,6 +272,7 @@ describe('Empty registry', () => { const mockCall = jest.spyOn(provider, 'call').mockImplementation(async () => ''); const CHAIN_ID = process.env.CHAIN_ID || 1; const address = REGISTRY_CONTRACT_ADDRESSES[CHAIN_ID]; + let mikroOrm: MikroORM; const keysWithModuleAddress = keys.map((key) => { return { ...key, moduleAddress: address }; @@ -265,12 +286,7 @@ describe('Empty registry', () => { beforeEach(async () => { const imports = [ - MikroOrmModule.forRoot({ - dbName: ':memory:', - type: 'sqlite', - allowGlobalContext: true, - entities: ['./**/*.entity.ts'], - }), + MikroOrmModule.forRoot(mikroORMConfig), MockLoggerModule.forRoot({ log: jest.fn(), error: jest.fn(), @@ -284,12 +300,13 @@ describe('Empty registry', () => { }).compile(); registryService = moduleRef.get(ValidatorRegistryService); registryStorageService = moduleRef.get(RegistryStorageService); - const generator = moduleRef.get(MikroORM).getSchemaGenerator(); + mikroOrm = moduleRef.get(MikroORM); + const generator = mikroOrm.getSchemaGenerator(); await generator.updateSchema(); }); afterEach(async () => { mockCall.mockReset(); - await registryService.clear(); + await clearDb(mikroOrm); await registryStorageService.onModuleDestroy(); }); test('init on update', async () => { @@ -299,13 +316,15 @@ describe('Empty registry', () => { keys: keysWithModuleAddress, operators: operatorsWithModuleAddress, }); - await registryService.update(address, 'latest'); + const blockHash = '0x4ef0f15a8a04a97f60a9f76ba83d27bcf98dac9635685cd05fe1d78bd6e93418'; + + await registryService.update(address, blockHash); expect(saveRegistryMock).toBeCalledTimes(1); expect(saveKeyRegistryMock.mock.calls.length).toBeGreaterThanOrEqual(1); await compareTestMeta(address, registryService, { keys: keysWithModuleAddress, operators: operatorsWithModuleAddress, }); - await registryService.update(address, 'latest'); + await registryService.update(address, blockHash); }); }); diff --git a/src/common/registry/test/validator-registry/registry.spec.ts b/src/common/registry/test/validator-registry/registry.spec.ts index dc1293a1..a81f09b2 100644 --- a/src/common/registry/test/validator-registry/registry.spec.ts +++ b/src/common/registry/test/validator-registry/registry.spec.ts @@ -13,6 +13,7 @@ import { } from '../..'; import { MikroORM } from '@mikro-orm/core'; import { REGISTRY_CONTRACT_ADDRESSES } from '@lido-nestjs/contracts'; +import { mikroORMConfig } from '../testing.utils'; describe('Validator', () => { const provider = new JsonRpcBatchProvider(process.env.PROVIDERS_URLS); @@ -29,12 +30,7 @@ describe('Validator', () => { beforeEach(async () => { const imports = [ - MikroOrmModule.forRoot({ - dbName: ':memory:', - type: 'sqlite', - allowGlobalContext: true, - entities: ['./**/*.entity.ts'], - }), + MikroOrmModule.forRoot(mikroORMConfig), LoggerModule.forRoot({ transports: [nullTransport()] }), ValidatorRegistryModule.forFeature({ provider }), ]; diff --git a/src/common/registry/test/validator-registry/sync.spec.ts b/src/common/registry/test/validator-registry/sync.spec.ts index 41cfbee1..e172adfe 100644 --- a/src/common/registry/test/validator-registry/sync.spec.ts +++ b/src/common/registry/test/validator-registry/sync.spec.ts @@ -6,6 +6,7 @@ import { getDefaultProvider, Provider } from '@ethersproject/providers'; import { Test } from '@nestjs/testing'; import { ValidatorRegistryModule, ValidatorRegistryService, RegistryStorageService } from '../../'; import { MikroORM } from '@mikro-orm/core'; +import { mikroORMConfig } from '../testing.utils'; describe('Sync module initializing', () => { const provider = getDefaultProvider('mainnet'); @@ -26,12 +27,7 @@ describe('Sync module initializing', () => { test('forRoot', async () => { const imports = [ - MikroOrmModule.forRoot({ - dbName: ':memory:', - type: 'sqlite', - allowGlobalContext: true, - entities: ['./**/*.entity.ts'], - }), + MikroOrmModule.forRoot(mikroORMConfig), LoggerModule.forRoot({ transports: [nullTransport()] }), ValidatorRegistryModule.forRoot({ provider, @@ -42,12 +38,7 @@ describe('Sync module initializing', () => { test('forFeature', async () => { const imports = [ - MikroOrmModule.forRoot({ - dbName: ':memory:', - type: 'sqlite', - allowGlobalContext: true, - entities: ['./**/*.entity.ts'], - }), + MikroOrmModule.forRoot(mikroORMConfig), LoggerModule.forRoot({ transports: [nullTransport()] }), ValidatorRegistryModule.forFeature({ provider, @@ -58,12 +49,7 @@ describe('Sync module initializing', () => { test('forFeature + global provider', async () => { const imports = [ - MikroOrmModule.forRoot({ - dbName: ':memory:', - type: 'sqlite', - allowGlobalContext: true, - entities: ['./**/*.entity.ts'], - }), + MikroOrmModule.forRoot(mikroORMConfig), LoggerModule.forRoot({ transports: [nullTransport()] }), ValidatorRegistryModule.forFeature(), ]; diff --git a/src/http/common/entities/index.ts b/src/http/common/entities/index.ts index 0b0e3fd8..8b2b4839 100644 --- a/src/http/common/entities/index.ts +++ b/src/http/common/entities/index.ts @@ -15,3 +15,4 @@ export * from './operator'; // query export * from './module-id'; export * from './key-query'; +export * from './operator-id'; diff --git a/src/http/common/entities/module-id.ts b/src/http/common/entities/module-id.ts index 8be67864..bebf96f6 100644 --- a/src/http/common/entities/module-id.ts +++ b/src/http/common/entities/module-id.ts @@ -1,2 +1,29 @@ -//TODO: add validation in endpoint -export type ModuleId = string; // | number; +import { BadRequestException } from '@nestjs/common'; +import { ApiProperty } from '@nestjs/swagger'; +import { Transform } from 'class-transformer'; + +function toModuleId(moduleId: string): string | number { + if (isContractAddress(moduleId)) { + return moduleId; + } + + if (Number(moduleId)) { + return Number(moduleId); + } + + throw new BadRequestException([`module_id must be a contract address or numeric value`]); +} + +export function isContractAddress(address: string): boolean { + const contractAddressRegex = /^0x[0-9a-fA-F]{40}$/; + return contractAddressRegex.test(address); +} + +export class ModuleId { + @ApiProperty({ + name: 'module_id', + description: "Staking modules' numeric id or contract module address", + }) + @Transform(({ value }) => toModuleId(value)) + module_id!: string | number; +} diff --git a/src/http/common/entities/operator-id-param.ts b/src/http/common/entities/operator-id.ts similarity index 90% rename from src/http/common/entities/operator-id-param.ts rename to src/http/common/entities/operator-id.ts index 62cffd26..819f2bf5 100644 --- a/src/http/common/entities/operator-id-param.ts +++ b/src/http/common/entities/operator-id.ts @@ -2,7 +2,7 @@ import { ApiProperty } from '@nestjs/swagger'; import { Type } from 'class-transformer'; import { IsInt, Min } from 'class-validator'; -export class OperatorIdParam { +export class OperatorId { @ApiProperty({ name: 'operator_id', description: 'Operator index', diff --git a/src/http/common/entities/operator.ts b/src/http/common/entities/operator.ts index b01a184d..43a293d1 100644 --- a/src/http/common/entities/operator.ts +++ b/src/http/common/entities/operator.ts @@ -31,7 +31,7 @@ export class Operator implements RegistryOperator { @ApiProperty({ description: 'Ethereum 1 address which receives stETH rewards for this operator' }) rewardAddress: string; - @ApiProperty({ description: 'The maximum number of validators to stake for this operator' }) + @ApiProperty({ description: 'The number of keys vetted by the DAO and that can be used for the deposit' }) stakingLimit: number; @ApiProperty({ description: 'Amount of stopped validators' }) diff --git a/src/http/consensus.fixtures.ts b/src/http/consensus.fixtures.ts new file mode 100644 index 00000000..0ddf2cd4 --- /dev/null +++ b/src/http/consensus.fixtures.ts @@ -0,0 +1,489 @@ +import { CLBlockSnapshot } from './common/entities'; + +export const header = { + execution_optimistic: false, + data: { + root: '0xe38dd3eba0199ae8a22407a2c485fd00ce0c5522d8c7da0cc491caea732807f4', + canonical: true, + header: { + message: { + slot: '4774624', + proposer_index: '130400', + parent_root: '0x6998730c2a5fe8dc03e60e9ffff0f554d5f14d43384dfa9697107bb513765660', + state_root: '0xb841bbb0ebc4b078d5efde852ff95c7253c2843e9f5364187aa14c782de13231', + body_root: '0x2cb6e3df1fd314899a06822238939b0faca3c29659a50c9c8fddcdd71691356f', + }, + signature: + '0xb4d8b266a7c6bf3cc1efebe6fa85dea914428928e7414f29db6f17b71925a1bbb7213410160717d199b9c287aba64fe4102804f28bfa0c313b1dcf7e687dacde8e7fa70482d8e05f2d4f981bc71b52aff72ade601e107a0fd89db7a0dcbe3c06', + }, + }, +}; + +export const slot = header.data.header.message.slot; + +export const block = { + version: 'bellatrix', + execution_optimistic: false, + data: { + message: { + slot: '4774624', + proposer_index: '130400', + parent_root: '0x6998730c2a5fe8dc03e60e9ffff0f554d5f14d43384dfa9697107bb513765660', + state_root: '0xb841bbb0ebc4b078d5efde852ff95c7253c2843e9f5364187aa14c782de13231', + body: { + randao_reveal: + '0x979d121e562f2e5a91a8b4c3fa09c3100b082a846921504b2e161cb329789deb57e7064ed4c52c903ba08621a32c1e0303839b901fac75cef7ff5eaec5226f0c8d98c8de9fb3ee2860593dd9ea4e9b7171bd3d7cce9bb53891528c8abf84a293', + eth1_data: { + deposit_root: '0xd4e95fd34dec8960ce6973c762ebf1f80a6577310d98a8004307302f5f4dbf5e', + deposit_count: '195216', + block_hash: '0xf7a79965a9caf9a89d6b36a349ec599df48bb3d75dba6a51f8c7015fe1bab2c6', + }, + graffiti: '0x74656b752f7632322e31322e302b3132372d6733616339323536000000000000', + proposer_slashings: [], + attester_slashings: [], + attestations: [ + /* fake empty */ + ], + deposits: [], + voluntary_exits: [], + sync_aggregate: { + sync_committee_bits: + '0xfcdfffdffdfdffbf3ff99fefe5fb95b3fffff7f71e77bf3ff7bf1fd8fdffbf7dffe7ffbfff9acef7ed9ffbbffbf7fdf7faf0bffedfedffbd3ffa3f947ffbd7ff', + sync_committee_signature: + '0xa7d49f496e1c8db9bd3292a55e1451175a3cd4adc6f1246b015427fcffbd4eecaf0eead1e96bfa7842bdf662e5082dc31099fedbb21f00f674e9beda8fa08d26826c28121b212a06f33d1d328faff8d95c068fad52078e0e858931205a9e45b9', + }, + execution_payload: { + parent_hash: '0x1447e721f3f56cc65899023f67876e86560bf172a3d9913b358f76a3bdd66f41', + fee_recipient: '0x000095e79eac4d76aab57cb2c1f091d553b36ca0', + state_root: '0xabc57973b76308d7843ab9dc29967a5c2308034ceb093a1fbc72a86522785da2', + receipts_root: '0xd1dabf348b90517787732552669d290328a593a7e1bac5a5f648995498bc7fd3', + logs_bloom: + '0x040400f11440100000401040001118200200424000000812008b000018890513200000090200421010020030a800200080009424640281488822c2a4042004002240a20c847800c908400028090c90000002800802d2014102012000c002008000441010020210c00100069000000a140d000240206040200022081004080080288080040406680201081001a00801200820158428108000900120021c2000440a090400420002106000293400020040800104002004002020210088802029004922d00240048940142000000803240002210049020200020111090c09002062b09b8002000008300828006b00000027424800d000a84544004a000000801304', + prev_randao: '0x2409abe67204d5fb2e241c134dd1488f87f2f0d6fb690b8efb6c8248c389f998', + block_number: '8316773', + gas_limit: '30000000', + gas_used: '7972101', + timestamp: '1673803488', + extra_data: '0x', + base_fee_per_gas: '3778', + block_hash: '0x769ff0798b912b17b5b7ecb32c6110df055c85f2b3e6ae3260c93d7e15cfd2c3', + transactions: [ + /* fake empty */ + ], + }, + }, + }, + signature: + '0xb4d8b266a7c6bf3cc1efebe6fa85dea914428928e7414f29db6f17b71925a1bbb7213410160717d199b9c287aba64fe4102804f28bfa0c313b1dcf7e687dacde8e7fa70482d8e05f2d4f981bc71b52aff72ade601e107a0fd89db7a0dcbe3c06', + }, +}; + +const epoch = Math.floor(Number(block.data.message.slot) / 32); + +export const consensusMetaResp: CLBlockSnapshot = { + epoch, + slot: Number(block.data.message.slot), + root: block.data.message.state_root, + timestamp: Number(block.data.message.body.execution_payload.timestamp), + blockNumber: Number(block.data.message.body.execution_payload.block_number), + blockHash: block.data.message.body.execution_payload.block_hash, +}; + +export const dvtOpOneReadyForExitValidators = [ + { + index: '1', + balance: '34006594880', + status: 'active_ongoing', + validator: { + pubkey: '0xa544bc44d9eacbf4dd6a2d6087b43f4c67fd5618651b97effcb30997bf49e5d7acf0100ef14e5d087cc228bc78d498e6', + withdrawal_credentials: '0x00fc40352b0a186d83267fc1342ec5da49dbb78e1099a4bd8db16d2c0d223594', + effective_balance: '32000000000', + slashed: false, + activation_eligibility_epoch: '0', + activation_epoch: '0', + exit_epoch: '18446744073709551615', + withdrawable_epoch: '18446744073709551615', + }, + }, + { + index: '5', + balance: '34006594880', + status: 'active_ongoing', + validator: { + pubkey: '0xb3b9f4e915f9fb9ef9c55da1815071f3f728cc6fc434fba2c11e08db5b5fa22b71d5975cec30ef97e7fc901e5a04ee5b', + withdrawal_credentials: '0x00fc40352b0a186d83267fc1342ec5da49dbb78e1099a4bd8db16d2c0d223594', + effective_balance: '32000000000', + slashed: false, + activation_eligibility_epoch: '0', + activation_epoch: '0', + exit_epoch: '18446744073709551615', + withdrawable_epoch: '18446744073709551615', + }, + }, + { + index: '6', + balance: '34006594880', + status: 'active_ongoing', + validator: { + pubkey: '0xc3e9f4e915f9fb9ef9c55da1815071f3f728cc6fc434fba2c11e08db5b5fa22b71d5975cec30ef97e7fc901e5a04ee5b', + withdrawal_credentials: '0x00fc40352b0a186d83267fc1342ec5da49dbb78e1099a4bd8db16d2c0d223594', + effective_balance: '32000000000', + slashed: false, + activation_eligibility_epoch: '0', + activation_epoch: '0', + exit_epoch: '18446744073709551615', + withdrawable_epoch: '18446744073709551615', + }, + }, + { + index: '7', + balance: '34006594880', + status: 'active_ongoing', + validator: { + pubkey: '0xd3e9f4e915f9fb9ef9c55da1815071f3f728cc6fc434fba2c11e08db5b5fa22b71d5975cec30ef97e7fc901e5a04ee5b', + withdrawal_credentials: '0x00fc40352b0a186d83267fc1342ec5da49dbb78e1099a4bd8db16d2c0d223594', + effective_balance: '32000000000', + slashed: false, + activation_eligibility_epoch: '0', + activation_epoch: '0', + exit_epoch: '18446744073709551615', + withdrawable_epoch: '18446744073709551615', + }, + }, + { + index: '8', + balance: '34006594880', + status: 'active_ongoing', + validator: { + pubkey: '0xe3e9f4e915f9fb9ef9c55da1815071f3f728cc6fc434fba2c11e08db5b5fa22b71d5975cec30ef97e7fc901e5a04ee5b', + withdrawal_credentials: '0x00fc40352b0a186d83267fc1342ec5da49dbb78e1099a4bd8db16d2c0d223594', + effective_balance: '32000000000', + slashed: false, + activation_eligibility_epoch: '0', + activation_epoch: '0', + exit_epoch: '18446744073709551615', + withdrawable_epoch: '18446744073709551615', + }, + }, + { + index: '9', + balance: '34006594880', + status: 'active_ongoing', + validator: { + pubkey: '0xf3e9f4e915f9fb9ef9c55da1815071f3f728cc6fc434fba2c11e08db5b5fa22b71d5975cec30ef97e7fc901e5a04ee5b', + withdrawal_credentials: '0x00fc40352b0a186d83267fc1342ec5da49dbb78e1099a4bd8db16d2c0d223594', + effective_balance: '32000000000', + slashed: false, + activation_eligibility_epoch: '0', + activation_epoch: '0', + exit_epoch: '18446744073709551615', + withdrawable_epoch: '18446744073709551615', + }, + }, + { + index: '10', + balance: '34006594880', + status: 'active_ongoing', + validator: { + pubkey: '0xa5e9f4e915f9fb9ef9c55da1815071f3f728cc6fc434fba2c11e08db5b5fa22b71d5975cec30ef97e7fc901e5a04ee5b', + withdrawal_credentials: '0x00fc40352b0a186d83267fc1342ec5da49dbb78e1099a4bd8db16d2c0d223594', + effective_balance: '32000000000', + slashed: false, + activation_eligibility_epoch: '0', + activation_epoch: '0', + exit_epoch: '18446744073709551615', + withdrawable_epoch: '18446744073709551615', + }, + }, + { + index: '11', + balance: '34006594880', + status: 'active_ongoing', + validator: { + pubkey: '0xb6e9f4e915f9fb9ef9c55da1815071f3f728cc6fc434fba2c11e08db5b5fa22b71d5975cec30ef97e7fc901e5a04ee5b', + withdrawal_credentials: '0x00fc40352b0a186d83267fc1342ec5da49dbb78e1099a4bd8db16d2c0d223594', + effective_balance: '32000000000', + slashed: false, + activation_eligibility_epoch: '0', + activation_epoch: '0', + exit_epoch: '18446744073709551615', + withdrawable_epoch: '18446744073709551615', + }, + }, + { + index: '12', + balance: '34006594880', + status: 'pending_queued', + validator: { + pubkey: '0xc7e9f4e915f9fb9ef9c55da1815071f3f728cc6fc434fba2c11e08db5b5fa22b71d5975cec30ef97e7fc901e5a04ee5b', + withdrawal_credentials: '0x00fc40352b0a186d83267fc1342ec5da49dbb78e1099a4bd8db16d2c0d223594', + effective_balance: '32000000000', + slashed: false, + activation_eligibility_epoch: '0', + activation_epoch: '0', + exit_epoch: '18446744073709551615', + withdrawable_epoch: '18446744073709551615', + }, + }, + { + index: '13', + balance: '34006594880', + status: 'pending_initialized', + validator: { + pubkey: '0xd8e9f4e915f9fb9ef9c55da1815071f3f728cc6fc434fba2c11e08db5b5fa22b71d5975cec30ef97e7fc901e5a04ee5b', + withdrawal_credentials: '0x00fc40352b0a186d83267fc1342ec5da49dbb78e1099a4bd8db16d2c0d223594', + effective_balance: '32000000000', + slashed: false, + activation_eligibility_epoch: '0', + activation_epoch: '0', + exit_epoch: '18446744073709551615', + withdrawable_epoch: '18446744073709551615', + }, + }, + { + index: '14', + balance: '34006594880', + status: 'exited_unslashed', + validator: { + pubkey: '0xe9e9f4e915f9fb9ef9c55da1815071f3f728cc6fc434fba2c11e08db5b5fa22b71d5975cec30ef97e7fc901e5a04ee5b', + withdrawal_credentials: '0x00fc40352b0a186d83267fc1342ec5da49dbb78e1099a4bd8db16d2c0d223594', + effective_balance: '32000000000', + slashed: false, + activation_eligibility_epoch: '0', + activation_epoch: '0', + exit_epoch: '18446744073709551615', + withdrawable_epoch: '18446744073709551615', + }, + }, +]; + +export const curatedOpOneReadyForExitValidators = [ + { + index: '3', + balance: '34006594880', + status: 'active_ongoing', + validator: { + pubkey: '0xa554bc44d9eacbf4dd6a2d6087b43f4c67fd5618651b97effcb30997bf49e5d7acf0100ef14e5d087cc228bc78d498e6', + withdrawal_credentials: '0x00fc40352b0a186d83267fc1342ec5da49dbb78e1099a4bd8db16d2c0d223594', + effective_balance: '32000000000', + slashed: false, + activation_eligibility_epoch: '0', + activation_epoch: '0', + exit_epoch: '18446744073709551615', + withdrawable_epoch: '18446744073709551615', + }, + }, + { + index: '4', + balance: '34006594880', + status: 'exited_unslashed', + validator: { + pubkey: '0xb3a9f4e915f9fb9ef9c55da1815071f3f728cc6fc434fba2c11e08db5b5fa22b71d5975cec30ef97e7fc901e5a04ee5b', + withdrawal_credentials: '0x00fc40352b0a186d83267fc1342ec5da49dbb78e1099a4bd8db16d2c0d223594', + effective_balance: '32000000000', + slashed: false, + activation_eligibility_epoch: '0', + activation_epoch: '0', + exit_epoch: '18446744073709551615', + withdrawable_epoch: '18446744073709551615', + }, + }, +]; + +export const dvtOpOneResp10percent = [ + { + validatorIndex: 1, + key: '0xa544bc44d9eacbf4dd6a2d6087b43f4c67fd5618651b97effcb30997bf49e5d7acf0100ef14e5d087cc228bc78d498e6', + }, +]; + +export const dvtOpOneRespExitMessages10percent = [ + { + validator_index: '1', + epoch: String(epoch), + }, +]; + +export const dvtOpOneResp20percent = [ + { + validatorIndex: 1, + key: '0xa544bc44d9eacbf4dd6a2d6087b43f4c67fd5618651b97effcb30997bf49e5d7acf0100ef14e5d087cc228bc78d498e6', + }, + { + validatorIndex: 5, + key: '0xb3b9f4e915f9fb9ef9c55da1815071f3f728cc6fc434fba2c11e08db5b5fa22b71d5975cec30ef97e7fc901e5a04ee5b', + }, +]; + +export const dvtOpOneRespExitMessages20percent = [ + { + validator_index: '1', + epoch: String(epoch), + }, + { + validator_index: '5', + epoch: String(epoch), + }, +]; + +export const dvtOpOneResp5maxAmount = [ + { + validatorIndex: 1, + key: '0xa544bc44d9eacbf4dd6a2d6087b43f4c67fd5618651b97effcb30997bf49e5d7acf0100ef14e5d087cc228bc78d498e6', + }, + { + validatorIndex: 5, + key: '0xb3b9f4e915f9fb9ef9c55da1815071f3f728cc6fc434fba2c11e08db5b5fa22b71d5975cec30ef97e7fc901e5a04ee5b', + }, + { + validatorIndex: 6, + key: '0xc3e9f4e915f9fb9ef9c55da1815071f3f728cc6fc434fba2c11e08db5b5fa22b71d5975cec30ef97e7fc901e5a04ee5b', + }, + { + validatorIndex: 7, + key: '0xd3e9f4e915f9fb9ef9c55da1815071f3f728cc6fc434fba2c11e08db5b5fa22b71d5975cec30ef97e7fc901e5a04ee5b', + }, + { + validatorIndex: 8, + key: '0xe3e9f4e915f9fb9ef9c55da1815071f3f728cc6fc434fba2c11e08db5b5fa22b71d5975cec30ef97e7fc901e5a04ee5b', + }, +]; + +export const dvtOpOneRespExitMessages5maxAmount = [ + { + validator_index: '1', + epoch: String(epoch), + }, + { + validator_index: '5', + epoch: String(epoch), + }, + { + validator_index: '6', + epoch: String(epoch), + }, + { + validator_index: '7', + epoch: String(epoch), + }, + { + validator_index: '8', + epoch: String(epoch), + }, +]; + +export const dvtOpOneResp100percent = [ + { + validatorIndex: 1, + key: '0xa544bc44d9eacbf4dd6a2d6087b43f4c67fd5618651b97effcb30997bf49e5d7acf0100ef14e5d087cc228bc78d498e6', + }, + { + validatorIndex: 5, + key: '0xb3b9f4e915f9fb9ef9c55da1815071f3f728cc6fc434fba2c11e08db5b5fa22b71d5975cec30ef97e7fc901e5a04ee5b', + }, + { + validatorIndex: 6, + key: '0xc3e9f4e915f9fb9ef9c55da1815071f3f728cc6fc434fba2c11e08db5b5fa22b71d5975cec30ef97e7fc901e5a04ee5b', + }, + { + validatorIndex: 7, + key: '0xd3e9f4e915f9fb9ef9c55da1815071f3f728cc6fc434fba2c11e08db5b5fa22b71d5975cec30ef97e7fc901e5a04ee5b', + }, + { + validatorIndex: 8, + key: '0xe3e9f4e915f9fb9ef9c55da1815071f3f728cc6fc434fba2c11e08db5b5fa22b71d5975cec30ef97e7fc901e5a04ee5b', + }, + { + validatorIndex: 9, + key: '0xf3e9f4e915f9fb9ef9c55da1815071f3f728cc6fc434fba2c11e08db5b5fa22b71d5975cec30ef97e7fc901e5a04ee5b', + }, + { + validatorIndex: 10, + key: '0xa5e9f4e915f9fb9ef9c55da1815071f3f728cc6fc434fba2c11e08db5b5fa22b71d5975cec30ef97e7fc901e5a04ee5b', + }, + { + validatorIndex: 11, + key: '0xb6e9f4e915f9fb9ef9c55da1815071f3f728cc6fc434fba2c11e08db5b5fa22b71d5975cec30ef97e7fc901e5a04ee5b', + }, + { + validatorIndex: 12, + key: '0xc7e9f4e915f9fb9ef9c55da1815071f3f728cc6fc434fba2c11e08db5b5fa22b71d5975cec30ef97e7fc901e5a04ee5b', + }, + { + validatorIndex: 13, + key: '0xd8e9f4e915f9fb9ef9c55da1815071f3f728cc6fc434fba2c11e08db5b5fa22b71d5975cec30ef97e7fc901e5a04ee5b', + }, +]; + +export const dvtOpOneRespExitMessages100percent = [ + { + validator_index: '1', + epoch: String(epoch), + }, + { + validator_index: '5', + epoch: String(epoch), + }, + { + validator_index: '6', + epoch: String(epoch), + }, + { + validator_index: '7', + epoch: String(epoch), + }, + { + validator_index: '8', + epoch: String(epoch), + }, + { + validator_index: '9', + epoch: String(epoch), + }, + { + validator_index: '10', + epoch: String(epoch), + }, + { + validator_index: '11', + epoch: String(epoch), + }, + { + validator_index: '12', + epoch: String(epoch), + }, + { + validator_index: '13', + epoch: String(epoch), + }, +]; + +export const curatedOpOneResp = [ + { + validatorIndex: 3, + key: '0xa554bc44d9eacbf4dd6a2d6087b43f4c67fd5618651b97effcb30997bf49e5d7acf0100ef14e5d087cc228bc78d498e6', + }, +]; + +export const validators = { + execution_optimistic: false, + data: [ + ...dvtOpOneReadyForExitValidators, + ...curatedOpOneReadyForExitValidators, + { + index: '2', + balance: '34274744045', + status: 'pending_queued', + validator: { + pubkey: '0xad9a0951d00c0988d3b8e719b9e65d6bc3501c9c35392fb6f050fcbbcdd316836a887acee989730bdf093629448bb731', + withdrawal_credentials: '0x00472bc262a89d741a00806182cf90466d92ed498bb04d6a07620ebf798747db', + effective_balance: '32000000000', + slashed: false, + activation_eligibility_epoch: '0', + activation_epoch: '0', + exit_epoch: '18446744073709551615', + withdrawable_epoch: '18446744073709551615', + }, + }, + ], +}; diff --git a/src/http/el-meta.fixture.ts b/src/http/el-meta.fixture.ts new file mode 100644 index 00000000..8d8109e7 --- /dev/null +++ b/src/http/el-meta.fixture.ts @@ -0,0 +1,5 @@ +export const elMeta = { + number: 74, + hash: '0x662e3e713207240b25d01324b6eccdc91493249a5048881544254994694530a5', + timestamp: 1691500803, +}; diff --git a/src/http/key.fixtures.ts b/src/http/key.fixtures.ts new file mode 100644 index 00000000..92aa7eac --- /dev/null +++ b/src/http/key.fixtures.ts @@ -0,0 +1,167 @@ +import { dvtModule, curatedModule } from './module.fixture'; + +export const dvtModuleKeys = [ + { + operatorIndex: 1, + index: 1, + moduleAddress: dvtModule.stakingModuleAddress, + key: '0xa544bc44d9eacbf4dd6a2d6087b43f4c67fd5618651b97effcb30997bf49e5d7acf0100ef14e5d087cc228bc78d498e6', + depositSignature: + '0x967875a0104d9f674538e2ec0df4be0a61ef08061cdcfa83e5a63a43dadb772d29053368224e5d8e046ba1a78490f5fc0f0186f23af0465d0a82b2db2e7535782fe12e1fd1cd4f6eb77d8dc7a4f7ab0fde31435d5fa98a013e0a716c5e1ef6a2', + used: true, + }, + { + operatorIndex: 1, + index: 2, + moduleAddress: dvtModule.stakingModuleAddress, + key: '0xb3e9f4e915f9fb9ef9c55da1815071f3f728cc6fc434fba2c11e08db5b5fa22b71d5975cec30ef97e7fc901e5a04ee5b', + depositSignature: + '0xb048f4a409d5a0aa638e5ec65c21e936ffde9a8d848e74e6b2f6972a4145620dc78c79db5425ea1a5c6b1dd8d50fc77f0bcec894c0a9446776936f2adf4f1dc7056fb3c4bdf9dbd00981288d4e582875d10b13d780dddc642496e97826abd3c7', + used: false, + }, + { + operatorIndex: 1, + index: 7, + moduleAddress: dvtModule.stakingModuleAddress, + key: '0xb3b9f4e915f9fb9ef9c55da1815071f3f728cc6fc434fba2c11e08db5b5fa22b71d5975cec30ef97e7fc901e5a04ee5b', + depositSignature: + '0xb048f4a409d5a0aa638e5ec65c21e936ffde9a8d848e74e6b2f6972a4145620dc78c79db5425ea1a5c6b1dd8d50fc77f0bcec894c0a9446776936f2adf4f1dc7056fb3c4bdf9dbd00981288d4e582875d10b13d780dddc642496e97826abd3c7', + used: true, + }, + { + operatorIndex: 1, + index: 8, + moduleAddress: dvtModule.stakingModuleAddress, + key: '0xc3e9f4e915f9fb9ef9c55da1815071f3f728cc6fc434fba2c11e08db5b5fa22b71d5975cec30ef97e7fc901e5a04ee5b', + depositSignature: + '0xb048f4a409d5a0aa638e5ec65c21e936ffde9a8d848e74e6b2f6972a4145620dc78c79db5425ea1a5c6b1dd8d50fc77f0bcec894c0a9446776936f2adf4f1dc7056fb3c4bdf9dbd00981288d4e582875d10b13d780dddc642496e97826abd3c7', + used: true, + }, + { + operatorIndex: 1, + index: 9, + moduleAddress: dvtModule.stakingModuleAddress, + key: '0xd3e9f4e915f9fb9ef9c55da1815071f3f728cc6fc434fba2c11e08db5b5fa22b71d5975cec30ef97e7fc901e5a04ee5b', + depositSignature: + '0xb048f4a409d5a0aa638e5ec65c21e936ffde9a8d848e74e6b2f6972a4145620dc78c79db5425ea1a5c6b1dd8d50fc77f0bcec894c0a9446776936f2adf4f1dc7056fb3c4bdf9dbd00981288d4e582875d10b13d780dddc642496e97826abd3c7', + used: true, + }, + { + operatorIndex: 1, + index: 10, + moduleAddress: dvtModule.stakingModuleAddress, + key: '0xe3e9f4e915f9fb9ef9c55da1815071f3f728cc6fc434fba2c11e08db5b5fa22b71d5975cec30ef97e7fc901e5a04ee5b', + depositSignature: + '0xb048f4a409d5a0aa638e5ec65c21e936ffde9a8d848e74e6b2f6972a4145620dc78c79db5425ea1a5c6b1dd8d50fc77f0bcec894c0a9446776936f2adf4f1dc7056fb3c4bdf9dbd00981288d4e582875d10b13d780dddc642496e97826abd3c7', + used: true, + }, + { + operatorIndex: 1, + index: 11, + moduleAddress: dvtModule.stakingModuleAddress, + key: '0xf3e9f4e915f9fb9ef9c55da1815071f3f728cc6fc434fba2c11e08db5b5fa22b71d5975cec30ef97e7fc901e5a04ee5b', + depositSignature: + '0xb048f4a409d5a0aa638e5ec65c21e936ffde9a8d848e74e6b2f6972a4145620dc78c79db5425ea1a5c6b1dd8d50fc77f0bcec894c0a9446776936f2adf4f1dc7056fb3c4bdf9dbd00981288d4e582875d10b13d780dddc642496e97826abd3c7', + used: true, + }, + { + operatorIndex: 1, + index: 12, + moduleAddress: dvtModule.stakingModuleAddress, + key: '0xa5e9f4e915f9fb9ef9c55da1815071f3f728cc6fc434fba2c11e08db5b5fa22b71d5975cec30ef97e7fc901e5a04ee5b', + depositSignature: + '0xb048f4a409d5a0aa638e5ec65c21e936ffde9a8d848e74e6b2f6972a4145620dc78c79db5425ea1a5c6b1dd8d50fc77f0bcec894c0a9446776936f2adf4f1dc7056fb3c4bdf9dbd00981288d4e582875d10b13d780dddc642496e97826abd3c7', + used: true, + }, + { + operatorIndex: 1, + index: 13, + moduleAddress: dvtModule.stakingModuleAddress, + key: '0xb6e9f4e915f9fb9ef9c55da1815071f3f728cc6fc434fba2c11e08db5b5fa22b71d5975cec30ef97e7fc901e5a04ee5b', + depositSignature: + '0xb048f4a409d5a0aa638e5ec65c21e936ffde9a8d848e74e6b2f6972a4145620dc78c79db5425ea1a5c6b1dd8d50fc77f0bcec894c0a9446776936f2adf4f1dc7056fb3c4bdf9dbd00981288d4e582875d10b13d780dddc642496e97826abd3c7', + used: true, + }, + { + operatorIndex: 1, + index: 14, + moduleAddress: dvtModule.stakingModuleAddress, + key: '0xc7e9f4e915f9fb9ef9c55da1815071f3f728cc6fc434fba2c11e08db5b5fa22b71d5975cec30ef97e7fc901e5a04ee5b', + depositSignature: + '0xb048f4a409d5a0aa638e5ec65c21e936ffde9a8d848e74e6b2f6972a4145620dc78c79db5425ea1a5c6b1dd8d50fc77f0bcec894c0a9446776936f2adf4f1dc7056fb3c4bdf9dbd00981288d4e582875d10b13d780dddc642496e97826abd3c7', + used: true, + }, + { + operatorIndex: 1, + index: 15, + moduleAddress: dvtModule.stakingModuleAddress, + key: '0xd8e9f4e915f9fb9ef9c55da1815071f3f728cc6fc434fba2c11e08db5b5fa22b71d5975cec30ef97e7fc901e5a04ee5b', + depositSignature: + '0xb048f4a409d5a0aa638e5ec65c21e936ffde9a8d848e74e6b2f6972a4145620dc78c79db5425ea1a5c6b1dd8d50fc77f0bcec894c0a9446776936f2adf4f1dc7056fb3c4bdf9dbd00981288d4e582875d10b13d780dddc642496e97826abd3c7', + used: true, + }, + { + operatorIndex: 1, + index: 16, + moduleAddress: dvtModule.stakingModuleAddress, + key: '0xe9e9f4e915f9fb9ef9c55da1815071f3f728cc6fc434fba2c11e08db5b5fa22b71d5975cec30ef97e7fc901e5a04ee5b', + depositSignature: + '0xb048f4a409d5a0aa638e5ec65c21e936ffde9a8d848e74e6b2f6972a4145620dc78c79db5425ea1a5c6b1dd8d50fc77f0bcec894c0a9446776936f2adf4f1dc7056fb3c4bdf9dbd00981288d4e582875d10b13d780dddc642496e97826abd3c7', + used: true, + }, +]; + +export const keyForOperatorTwo = { + operatorIndex: 2, + index: 5, + moduleAddress: curatedModule.stakingModuleAddress, + key: '0x91024d603575605569c212b00f375c8bad733a697b453fbe054bb996bd24c7d1a5b6034cc58943aeddab05cbdfd40632', + depositSignature: + '0x9990450099816e066c20b5947be6bf089b57fcfacfb2c8285ddfd6c678a44198bf7c013a0d1a6353ed19dd94423eef7b010d25aaa2c3093760c79bf247f5350120e8a74e4586eeba0f1e2bcf17806f705007d7b5862039da5cd93ee659280d77', + used: true, +}; + +export const keyForOperatorTwoDuplicate = { ...keyForOperatorTwo, index: 6 }; + +export const curatedModuleKeys = [ + { + operatorIndex: 1, + index: 1, + moduleAddress: curatedModule.stakingModuleAddress, + key: '0xa554bc44d9eacbf4dd6a2d6087b43f4c67fd5618651b97effcb30997bf49e5d7acf0100ef14e5d087cc228bc78d498e6', + depositSignature: + '0x967875a0104d9f674538e2ec0df4be0a61ef08061cdcfa83e5a63a43dadb772d29053368224e5d8e046ba1a78490f5fc0f0186f23af0465d0a82b2db2e7535782fe12e1fd1cd4f6eb77d8dc7a4f7ab0fde31435d5fa98a013e0a716c5e1ef6a2', + used: true, + }, + { + operatorIndex: 1, + index: 2, + moduleAddress: curatedModule.stakingModuleAddress, + key: '0xb3a9f4e915f9fb9ef9c55da1815071f3f728cc6fc434fba2c11e08db5b5fa22b71d5975cec30ef97e7fc901e5a04ee5b', + depositSignature: + '0xb048f4a409d5a0aa638e5ec65c21e936ffde9a8d848e74e6b2f6972a4145620dc78c79db5425ea1a5c6b1dd8d50fc77f0bcec894c0a9446776936f2adf4f1dc7056fb3c4bdf9dbd00981288d4e582875d10b13d780dddc642496e97826abd3c7', + used: true, + }, + { + operatorIndex: 1, + index: 3, + moduleAddress: curatedModule.stakingModuleAddress, + key: '0x91524d603575605569c212b00f375c8bad733a697b453fbe054bb996bd24c7d1a5b6034cc58943aeddab05cbdfd40632', + depositSignature: + '0x9990450099816e066c20b5947be6bf089b57fcfacfb2c8285ddfd6c678a44198bf7c013a0d1a6353ed19dd94423eef7b010d25aaa2c3093760c79bf247f5350120e8a74e4586eeba0f1e2bcf17806f705007d7b5862039da5cd93ee659280d77', + used: false, + }, + { + operatorIndex: 2, + index: 4, + moduleAddress: curatedModule.stakingModuleAddress, + key: '0xa544bc44d8eacbf4dd6a2d6087b43f4c67fd5618651b97effcb30997bf49e5d7acf0100ef14e5d087cc228bc78d498e6', + depositSignature: + '0x967875a0104d1f674538e2ec0df4be0a61ef08061cdcfa83e5a63a43dadb772d29053368224e5d8e046ba1a78490f5fc0f0186f23af0465d0a82b2db2e7535782fe12e1fd1cd4f6eb77d8dc7a4f7ab0fde31435d5fa98a013e0a716c5e1ef6a2', + used: true, + }, + keyForOperatorTwo, + keyForOperatorTwoDuplicate, +]; + +export const keys = [...dvtModuleKeys, ...curatedModuleKeys]; diff --git a/src/http/keys/entities/index.ts b/src/http/keys/entities/index.ts index 3d002682..dbc1ea0f 100644 --- a/src/http/keys/entities/index.ts +++ b/src/http/keys/entities/index.ts @@ -1,2 +1 @@ export * from './response'; -// export * from './key-with-module-address'; diff --git a/src/http/keys/keys.e2e-spec.ts b/src/http/keys/keys.e2e-spec.ts index 9cc733fe..1622fc78 100644 --- a/src/http/keys/keys.e2e-spec.ts +++ b/src/http/keys/keys.e2e-spec.ts @@ -11,7 +11,7 @@ import { MikroORM } from '@mikro-orm/core'; import { MikroOrmModule } from '@mikro-orm/nestjs'; import { KeysController } from './keys.controller'; import { StakingRouterModule } from '../../staking-router-modules/staking-router.module'; -import { dvtModule, curatedModule } from '../../storage/module.fixture'; + import { SRModuleStorageService } from '../../storage/sr-module.storage'; import { ElMetaStorageService } from '../../storage/el-meta.storage'; import { KeysService } from './keys.service'; @@ -19,7 +19,10 @@ import { nullTransport, LoggerModule } from '@lido-nestjs/logger'; import * as request from 'supertest'; import { FastifyAdapter, NestFastifyApplication } from '@nestjs/platform-fastify'; -// import { validationOpt } from '../../main'; + +import { dvtModule, curatedModule } from '../module.fixture'; +import { elMeta } from '../el-meta.fixture'; +import { keys, keyForOperatorTwo, keyForOperatorTwoDuplicate } from '../key.fixtures'; describe('KeyController (e2e)', () => { let app: INestApplication; @@ -29,88 +32,6 @@ describe('KeyController (e2e)', () => { let elMetaStorageService: ElMetaStorageService; let registryStorage: RegistryStorageService; - const elMeta = { - number: 74, - hash: '0x662e3e713207240b25d01324b6eccdc91493249a5048881544254994694530a5', - timestamp: 1691500803, - }; - - const dvtModuleKeys = [ - { - operatorIndex: 1, - index: 1, - moduleAddress: dvtModule.stakingModuleAddress, - key: '0xa544bc44d9eacbf4dd6a2d6087b43f4c67fd5618651b97effcb30997bf49e5d7acf0100ef14e5d087cc228bc78d498e6', - depositSignature: - '0x967875a0104d9f674538e2ec0df4be0a61ef08061cdcfa83e5a63a43dadb772d29053368224e5d8e046ba1a78490f5fc0f0186f23af0465d0a82b2db2e7535782fe12e1fd1cd4f6eb77d8dc7a4f7ab0fde31435d5fa98a013e0a716c5e1ef6a2', - used: true, - }, - { - operatorIndex: 1, - index: 2, - moduleAddress: dvtModule.stakingModuleAddress, - key: '0xb3e9f4e915f9fb9ef9c55da1815071f3f728cc6fc434fba2c11e08db5b5fa22b71d5975cec30ef97e7fc901e5a04ee5b', - depositSignature: - '0xb048f4a409d5a0aa638e5ec65c21e936ffde9a8d848e74e6b2f6972a4145620dc78c79db5425ea1a5c6b1dd8d50fc77f0bcec894c0a9446776936f2adf4f1dc7056fb3c4bdf9dbd00981288d4e582875d10b13d780dddc642496e97826abd3c7', - used: false, - }, - ]; - - const keyForOperatorTwo = { - operatorIndex: 2, - index: 5, - moduleAddress: curatedModule.stakingModuleAddress, - key: '0x91024d603575605569c212b00f375c8bad733a697b453fbe054bb996bd24c7d1a5b6034cc58943aeddab05cbdfd40632', - depositSignature: - '0x9990450099816e066c20b5947be6bf089b57fcfacfb2c8285ddfd6c678a44198bf7c013a0d1a6353ed19dd94423eef7b010d25aaa2c3093760c79bf247f5350120e8a74e4586eeba0f1e2bcf17806f705007d7b5862039da5cd93ee659280d77', - used: true, - }; - - const keyForOperatorTwoDuplicate = { ...keyForOperatorTwo, index: 6 }; - - const curatedModuleKeys = [ - { - operatorIndex: 1, - index: 1, - moduleAddress: curatedModule.stakingModuleAddress, - key: '0xa554bc44d9eacbf4dd6a2d6087b43f4c67fd5618651b97effcb30997bf49e5d7acf0100ef14e5d087cc228bc78d498e6', - depositSignature: - '0x967875a0104d9f674538e2ec0df4be0a61ef08061cdcfa83e5a63a43dadb772d29053368224e5d8e046ba1a78490f5fc0f0186f23af0465d0a82b2db2e7535782fe12e1fd1cd4f6eb77d8dc7a4f7ab0fde31435d5fa98a013e0a716c5e1ef6a2', - used: true, - }, - { - operatorIndex: 1, - index: 2, - moduleAddress: curatedModule.stakingModuleAddress, - key: '0xb3a9f4e915f9fb9ef9c55da1815071f3f728cc6fc434fba2c11e08db5b5fa22b71d5975cec30ef97e7fc901e5a04ee5b', - depositSignature: - '0xb048f4a409d5a0aa638e5ec65c21e936ffde9a8d848e74e6b2f6972a4145620dc78c79db5425ea1a5c6b1dd8d50fc77f0bcec894c0a9446776936f2adf4f1dc7056fb3c4bdf9dbd00981288d4e582875d10b13d780dddc642496e97826abd3c7', - used: true, - }, - { - operatorIndex: 1, - index: 3, - moduleAddress: curatedModule.stakingModuleAddress, - key: '0x91524d603575605569c212b00f375c8bad733a697b453fbe054bb996bd24c7d1a5b6034cc58943aeddab05cbdfd40632', - depositSignature: - '0x9990450099816e066c20b5947be6bf089b57fcfacfb2c8285ddfd6c678a44198bf7c013a0d1a6353ed19dd94423eef7b010d25aaa2c3093760c79bf247f5350120e8a74e4586eeba0f1e2bcf17806f705007d7b5862039da5cd93ee659280d77', - used: false, - }, - { - operatorIndex: 2, - index: 4, - moduleAddress: curatedModule.stakingModuleAddress, - key: '0xa544bc44d8eacbf4dd6a2d6087b43f4c67fd5618651b97effcb30997bf49e5d7acf0100ef14e5d087cc228bc78d498e6', - depositSignature: - '0x967875a0104d1f674538e2ec0df4be0a61ef08061cdcfa83e5a63a43dadb772d29053368224e5d8e046ba1a78490f5fc0f0186f23af0465d0a82b2db2e7535782fe12e1fd1cd4f6eb77d8dc7a4f7ab0fde31435d5fa98a013e0a716c5e1ef6a2', - used: true, - }, - keyForOperatorTwo, - keyForOperatorTwoDuplicate, - ]; - - const keys = [...dvtModuleKeys, ...curatedModuleKeys]; - async function cleanDB() { await keysStorageService.removeAll(); await moduleStorageService.removeAll(); @@ -448,7 +369,6 @@ describe('KeyController (e2e)', () => { .send({ pubkeys }); expect(resp.status).toEqual(200); - // as pubkeys contains 3 elements and keyForOperatorTwo has a duplicate expect(resp.body.data.length).toEqual(0); expect(resp.body.meta).toEqual({ elBlockSnapshot: { diff --git a/src/storage/module.fixture.ts b/src/http/module.fixture.ts similarity index 95% rename from src/storage/module.fixture.ts rename to src/http/module.fixture.ts index 94f767d9..fd7e4424 100644 --- a/src/storage/module.fixture.ts +++ b/src/http/module.fixture.ts @@ -1,4 +1,4 @@ -import { STAKING_MODULE_TYPE } from 'staking-router-modules/constants'; +import { STAKING_MODULE_TYPE } from '../staking-router-modules/constants'; export const curatedModule = { id: 1, diff --git a/src/http/operator.fixtures.ts b/src/http/operator.fixtures.ts new file mode 100644 index 00000000..691ab25e --- /dev/null +++ b/src/http/operator.fixtures.ts @@ -0,0 +1,53 @@ +import { AddressZero } from '@ethersproject/constants'; +import { RegistryOperator } from '../common/registry'; +import { curatedModule, dvtModule } from './module.fixture'; + +export const operatorOneCurated: RegistryOperator = { + index: 1, + active: true, + name: 'test', + rewardAddress: AddressZero, + stoppedValidators: 0, + stakingLimit: 1, + usedSigningKeys: 2, + totalSigningKeys: 3, + moduleAddress: curatedModule.stakingModuleAddress, +}; + +export const operatorTwoCurated: RegistryOperator = { + index: 2, + active: true, + name: 'test', + rewardAddress: AddressZero, + stoppedValidators: 0, + stakingLimit: 1, + usedSigningKeys: 2, + totalSigningKeys: 3, + moduleAddress: curatedModule.stakingModuleAddress, +}; + +export const operatorOneDvt: RegistryOperator = { + index: 1, + active: true, + name: 'test', + rewardAddress: AddressZero, + stoppedValidators: 0, + stakingLimit: 1, + usedSigningKeys: 2, + totalSigningKeys: 3, + moduleAddress: dvtModule.stakingModuleAddress, +}; + +export const operatorTwoDvt: RegistryOperator = { + index: 2, + active: true, + name: 'test', + rewardAddress: AddressZero, + stoppedValidators: 0, + stakingLimit: 1, + usedSigningKeys: 2, + totalSigningKeys: 3, + moduleAddress: dvtModule.stakingModuleAddress, +}; + +export const operators = [operatorOneCurated, operatorTwoCurated, operatorOneDvt, operatorTwoDvt]; diff --git a/src/http/sr-modules-keys/entities/grouped-by-module-keys.response.ts b/src/http/sr-modules-keys/entities/grouped-by-module-keys.response.ts index 83905fd5..b45a8284 100644 --- a/src/http/sr-modules-keys/entities/grouped-by-module-keys.response.ts +++ b/src/http/sr-modules-keys/entities/grouped-by-module-keys.response.ts @@ -1,5 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; -import { Key, SRModule, ELMeta } from 'http/common/entities/'; +import { Key, SRModule, ELMeta } from '../../common/entities/'; export class KeyListWithModule { @ApiProperty({ diff --git a/src/http/sr-modules-keys/sr-modules-keys.controller.ts b/src/http/sr-modules-keys/sr-modules-keys.controller.ts index d916319c..e2d9a806 100644 --- a/src/http/sr-modules-keys/sr-modules-keys.controller.ts +++ b/src/http/sr-modules-keys/sr-modules-keys.controller.ts @@ -1,10 +1,22 @@ -import { Controller, Get, Version, Param, Query, Body, Post, NotFoundException, HttpStatus, Res } from '@nestjs/common'; +import { + Controller, + Get, + Version, + Param, + Query, + Body, + Post, + NotFoundException, + HttpStatus, + Res, + HttpCode, +} from '@nestjs/common'; import { ApiNotFoundResponse, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger'; import { SRModuleKeyListResponse, GroupedByModuleKeyListResponse } from './entities'; import { SRModulesKeysService } from './sr-modules-keys.service'; -import { ModuleId, KeyQuery } from 'http/common/entities/'; -import { KeysFindBody } from 'http/common/entities/pubkeys'; -import { TooEarlyResponse } from 'http/common/entities/http-exceptions'; +import { ModuleId, KeyQuery } from '../common/entities/'; +import { KeysFindBody } from '../common/entities/pubkeys'; +import { TooEarlyResponse } from '../common/entities/http-exceptions'; import { IsolationLevel } from '@mikro-orm/core'; import { EntityManager } from '@mikro-orm/knex'; import * as JSONStream from 'jsonstream'; @@ -32,7 +44,7 @@ export class SRModulesKeysController { type: TooEarlyResponse, }) @Get('keys') - async getGroupedByModuleKeys(@Query() filters: KeyQuery) { + getGroupedByModuleKeys(@Query() filters: KeyQuery) { return this.srModulesKeysService.getGroupedByModuleKeys(filters); } @@ -58,12 +70,16 @@ export class SRModulesKeysController { description: 'Staking router module_id or contract address.', }) @Get(':module_id/keys') - async getModuleKeys(@Param('module_id') moduleId: ModuleId, @Query() filters: KeyQuery, @Res() reply: FastifyReply) { + async getModuleKeys(@Param() module: ModuleId, @Query() filters: KeyQuery, @Res() reply: FastifyReply) { await this.entityManager.transactional( async () => { - const { keysGenerator, module, meta } = await this.srModulesKeysService.getModuleKeys(moduleId, filters); + const { + keysGenerator, + module: srModule, + meta, + } = await this.srModulesKeysService.getModuleKeys(module.module_id, filters); const jsonStream = JSONStream.stringify( - '{ "meta": ' + JSON.stringify(meta) + ', "data": { "module": ' + JSON.stringify(module) + ', "keys": [', + '{ "meta": ' + JSON.stringify(meta) + ', "data": { "module": ' + JSON.stringify(srModule) + ', "keys": [', ',', ']}}', ); @@ -82,6 +98,7 @@ export class SRModulesKeysController { @Version('1') @Post(':module_id/keys/find') + @HttpCode(HttpStatus.OK) @ApiOperation({ summary: 'Get list of found staking router module keys in db from pubkey list.' }) @ApiResponse({ status: 200, @@ -102,7 +119,7 @@ export class SRModulesKeysController { name: 'module_id', description: 'Staking router module_id or contract address.', }) - getModuleKeysByPubkeys(@Param('module_id') moduleId: ModuleId, @Body() keys: KeysFindBody) { - return this.srModulesKeysService.getModuleKeysByPubKeys(moduleId, keys.pubkeys); + getModuleKeysByPubkeys(@Param() module: ModuleId, @Body() keys: KeysFindBody) { + return this.srModulesKeysService.getModuleKeysByPubKeys(module.module_id, keys.pubkeys); } } diff --git a/src/http/sr-modules-keys/sr-modules-keys.e2e-spec.ts b/src/http/sr-modules-keys/sr-modules-keys.e2e-spec.ts new file mode 100644 index 00000000..0fb537e8 --- /dev/null +++ b/src/http/sr-modules-keys/sr-modules-keys.e2e-spec.ts @@ -0,0 +1,480 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { Test } from '@nestjs/testing'; +import { Global, INestApplication, Module, ValidationPipe, VersioningType } from '@nestjs/common'; +import { + KeyRegistryService, + RegistryKeyStorageService, + RegistryStorageModule, + RegistryStorageService, +} from '../../common/registry'; +import { MikroORM } from '@mikro-orm/core'; +import { MikroOrmModule } from '@mikro-orm/nestjs'; +import { StakingRouterModule } from '../../staking-router-modules/staking-router.module'; + +import { SRModuleStorageService } from '../../storage/sr-module.storage'; +import { ElMetaStorageService } from '../../storage/el-meta.storage'; +import { nullTransport, LoggerModule } from '@lido-nestjs/logger'; + +import * as request from 'supertest'; +import { FastifyAdapter, NestFastifyApplication } from '@nestjs/platform-fastify'; + +import { dvtModule, curatedModule, dvtModuleResp, curatedModuleResp } from '../module.fixture'; +import { elMeta } from '../el-meta.fixture'; +import { keys, dvtModuleKeys, curatedModuleKeys } from '../key.fixtures'; +import { SRModulesKeysController } from './sr-modules-keys.controller'; +import { SRModulesKeysService } from './sr-modules-keys.service'; + +describe('SRModulesKeysController (e2e)', () => { + let app: INestApplication; + + let keysStorageService: RegistryKeyStorageService; + let moduleStorageService: SRModuleStorageService; + let elMetaStorageService: ElMetaStorageService; + let registryStorage: RegistryStorageService; + + async function cleanDB() { + await keysStorageService.removeAll(); + await moduleStorageService.removeAll(); + await elMetaStorageService.removeAll(); + } + + const keysByModules = [ + { + keys: dvtModuleKeys, + module: dvtModuleResp, + }, + { + keys: curatedModuleKeys, + module: curatedModuleResp, + }, + ]; + + @Global() + @Module({ + imports: [RegistryStorageModule], + providers: [KeyRegistryService], + exports: [KeyRegistryService, RegistryStorageModule], + }) + class KeyRegistryModule {} + + class KeysRegistryServiceMock { + async update(moduleAddress, blockHash) { + return; + } + } + + beforeAll(async () => { + const imports = [ + // sqlite3 only supports serializable transactions, ignoring the isolation level param + // TODO: use postgres + MikroOrmModule.forRoot({ + dbName: ':memory:', + type: 'sqlite', + allowGlobalContext: true, + entities: ['./**/*.entity.ts'], + }), + LoggerModule.forRoot({ transports: [nullTransport()] }), + KeyRegistryModule, + StakingRouterModule, + ]; + + const controllers = [SRModulesKeysController]; + const providers = [SRModulesKeysService]; + const moduleRef = await Test.createTestingModule({ imports, controllers, providers }) + .overrideProvider(KeyRegistryService) + .useClass(KeysRegistryServiceMock) + .compile(); + + elMetaStorageService = moduleRef.get(ElMetaStorageService); + keysStorageService = moduleRef.get(RegistryKeyStorageService); + moduleStorageService = moduleRef.get(SRModuleStorageService); + registryStorage = moduleRef.get(RegistryStorageService); + + const generator = moduleRef.get(MikroORM).getSchemaGenerator(); + await generator.updateSchema(); + + app = moduleRef.createNestApplication(new FastifyAdapter()); + app.enableVersioning({ type: VersioningType.URI }); + app.useGlobalPipes(new ValidationPipe({ transform: true })); + + await app.init(); + await app.getHttpAdapter().getInstance().ready(); + }); + + afterAll(async () => { + await registryStorage.onModuleDestroy(); + await app.getHttpAdapter().close(); + await app.close(); + }); + + describe('The /modules/{module_id}/keys request', () => { + describe('api ready to work', () => { + beforeAll(async () => { + // lets save meta + await elMetaStorageService.update(elMeta); + // lets save keys + await keysStorageService.save(keys); + // lets save modules + await moduleStorageService.upsert(dvtModule, 1); + await moduleStorageService.upsert(curatedModule, 1); + }); + + afterAll(async () => { + await cleanDB(); + }); + + it('Should return all keys for request without filters', async () => { + const resp = await request(app.getHttpServer()).get(`/v1/modules/${dvtModule.id}/keys`); + + expect(resp.status).toEqual(200); + expect(resp.body.data.keys).toEqual(expect.arrayContaining(dvtModuleKeys)); + expect(resp.body.data.module).toEqual(dvtModuleResp); + expect(resp.body.meta).toEqual({ + elBlockSnapshot: { + blockNumber: elMeta.number, + blockHash: elMeta.hash, + timestamp: elMeta.timestamp, + }, + }); + }); + + it('Should return 400 error if operatorIndex is not a number', async () => { + const resp = await request(app.getHttpServer()) + .get(`/v1/modules/${dvtModule.id}/keys`) + .query({ used: false, operatorIndex: 'one' }); + expect(resp.status).toEqual(400); + expect(resp.body).toEqual({ + error: 'Bad Request', + message: ['operatorIndex must not be less than 0', 'operatorIndex must be an integer number'], + statusCode: 400, + }); + }); + + it('Should return 400 error if used is not a boolean value', async () => { + const resp = await request(app.getHttpServer()) + .get(`/v1/modules/${dvtModule.id}/keys`) + .query({ used: 0, operatorIndex: 2 }); + expect(resp.status).toEqual(400); + expect(resp.body).toEqual({ error: 'Bad Request', message: ['used must be a boolean value'], statusCode: 400 }); + }); + + it('Should return used keys for operator one', async () => { + const resp = await request(app.getHttpServer()) + .get(`/v1/modules/${dvtModule.id}/keys`) + .query({ used: true, operatorIndex: 1 }); + + const expectedKeys = dvtModuleKeys.filter((key) => key.used && key.operatorIndex == 1); + + expect(resp.status).toEqual(200); + expect(resp.body.data.keys).toEqual(expect.arrayContaining(expectedKeys)); + expect(resp.body.data.module).toEqual(dvtModuleResp); + expect(resp.body.meta).toEqual({ + elBlockSnapshot: { + blockNumber: elMeta.number, + blockHash: elMeta.hash, + timestamp: elMeta.timestamp, + }, + }); + }); + + it('Should return unused keys for operator one', async () => { + const resp = await request(app.getHttpServer()) + .get(`/v1/modules/${dvtModule.id}/keys`) + .query({ used: false, operatorIndex: 1 }); + + const expectedKeys = dvtModuleKeys.filter((key) => !key.used && key.operatorIndex == 1); + + expect(resp.status).toEqual(200); + expect(resp.body.data.keys).toEqual(expect.arrayContaining(expectedKeys)); + expect(resp.body.data.module).toEqual(dvtModuleResp); + expect(resp.body.meta).toEqual({ + elBlockSnapshot: { + blockNumber: elMeta.number, + blockHash: elMeta.hash, + timestamp: elMeta.timestamp, + }, + }); + }); + + it('Should return empty keys list for non-existent operator', async () => { + const resp = await request(app.getHttpServer()) + .get(`/v1/modules/${dvtModule.id}/keys`) + .query({ operatorIndex: 777 }); + + expect(resp.status).toEqual(200); + expect(resp.body.data.keys).toEqual([]); + expect(resp.body.data.module).toEqual(dvtModuleResp); + expect(resp.body.meta).toEqual({ + elBlockSnapshot: { + blockNumber: elMeta.number, + blockHash: elMeta.hash, + timestamp: elMeta.timestamp, + }, + }); + }); + + it("Should return 404 if module doesn't exist", async () => { + const resp = await request(app.getHttpServer()).get('/v1/modules/777/keys'); + expect(resp.status).toEqual(404); + expect(resp.body).toEqual({ + error: 'Not Found', + message: 'Module with moduleId 777 is not supported', + statusCode: 404, + }); + }); + + it('Should return 400 error if module_id is not a contract address or number', async () => { + const resp = await request(app.getHttpServer()).get(`/v1/modules/sjdnsjkfsjkbfsjdfbdjfb/keys`); + expect(resp.status).toEqual(400); + expect(resp.body).toEqual({ + error: 'Bad Request', + message: ['module_id must be a contract address or numeric value'], + statusCode: 400, + }); + }); + }); + + describe('too early response case', () => { + beforeEach(async () => { + await cleanDB(); + }); + afterEach(async () => { + await cleanDB(); + }); + + it('Should return too early response if there are no meta', async () => { + await moduleStorageService.upsert(dvtModule, 1); + const resp = await request(app.getHttpServer()).get(`/v1/modules/${dvtModule.id}/keys`); + expect(resp.status).toEqual(425); + expect(resp.body).toEqual({ message: 'Too early response', statusCode: 425 }); + }); + }); + }); + + describe('The /modules/{module_id}/keys/find', () => { + describe('api ready to work', () => { + beforeAll(async () => { + // lets save meta + await elMetaStorageService.update(elMeta); + // lets save keys + await keysStorageService.save(keys); + // lets save modules + await moduleStorageService.upsert(dvtModule, 1); + await moduleStorageService.upsert(curatedModule, 1); + }); + + afterAll(async () => { + await cleanDB(); + }); + + it('Should return all keys that satisfy the request', async () => { + const pubkeys = [dvtModuleKeys[0].key, dvtModuleKeys[1].key]; + + const resp = await request(app.getHttpServer()) + .post(`/v1/modules/${dvtModule.id}/keys/find`) + .set('Content-Type', 'application/json') + .send({ pubkeys }); + + expect(resp.status).toEqual(200); + expect(resp.body.data.keys).toEqual(expect.arrayContaining([dvtModuleKeys[0], dvtModuleKeys[1]])); + expect(resp.body.data.module).toEqual(dvtModuleResp); + expect(resp.body.meta).toEqual({ + elBlockSnapshot: { + blockNumber: elMeta.number, + blockHash: elMeta.hash, + timestamp: elMeta.timestamp, + }, + }); + }); + + it('Should return an empty list if no keys satisfy the request', async () => { + const pubkeys = ['somerandomkey']; + + const resp = await request(app.getHttpServer()) + .post(`/v1/modules/${dvtModule.id}/keys/find`) + .set('Content-Type', 'application/json') + .send({ pubkeys }); + + expect(resp.status).toEqual(200); + expect(resp.body.data.keys).toEqual([]); + expect(resp.body.data.module).toEqual(dvtModuleResp); + expect(resp.body.meta).toEqual({ + elBlockSnapshot: { + blockNumber: elMeta.number, + blockHash: elMeta.hash, + timestamp: elMeta.timestamp, + }, + }); + }); + + it('Should return validation error if pubkeys list was not provided', async () => { + const resp = await request(app.getHttpServer()) + .post(`/v1/modules/${dvtModule.id}/keys/find`) + .set('Content-Type', 'application/json') + .send({ pubkeys: [] }); + + expect(resp.status).toEqual(400); + expect(resp.body).toEqual({ + error: 'Bad Request', + message: ['pubkeys must contain at least 1 elements'], + statusCode: 400, + }); + }); + }); + + describe('too early response case', () => { + beforeEach(async () => { + await cleanDB(); + }); + afterEach(async () => { + await cleanDB(); + }); + + it('Should return too early response if there are no meta', async () => { + await moduleStorageService.upsert(dvtModule, 1); + const resp = await request(app.getHttpServer()) + .post(`/v1/modules/${dvtModule.id}/keys/find`) + .set('Content-Type', 'application/json') + .send({ pubkeys: ['somerandomkey'] }); + expect(resp.status).toEqual(425); + expect(resp.body).toEqual({ message: 'Too early response', statusCode: 425 }); + }); + }); + }); + + describe('The /modules/keys request', () => { + describe('api ready to work', () => { + beforeAll(async () => { + // lets save meta + await elMetaStorageService.update(elMeta); + // lets save keys + await keysStorageService.save(keys); + // lets save modules + await moduleStorageService.upsert(dvtModule, 1); + await moduleStorageService.upsert(curatedModule, 1); + }); + + afterAll(async () => { + await cleanDB(); + }); + + it('Should return all keys for request without filters', async () => { + const resp = await request(app.getHttpServer()).get(`/v1/modules/keys`); + + expect(resp.status).toEqual(200); + expect(resp.body.data).toEqual(expect.arrayContaining(keysByModules)); + expect(resp.body.meta).toEqual({ + elBlockSnapshot: { + blockNumber: elMeta.number, + blockHash: elMeta.hash, + timestamp: elMeta.timestamp, + }, + }); + }); + + it('Should return 400 error if operatorIndex is not a number', async () => { + const resp = await request(app.getHttpServer()) + .get(`/v1/modules/keys`) + .query({ used: false, operatorIndex: 'one' }); + expect(resp.status).toEqual(400); + expect(resp.body).toEqual({ + error: 'Bad Request', + message: ['operatorIndex must not be less than 0', 'operatorIndex must be an integer number'], + statusCode: 400, + }); + }); + + it('Should return 400 error if used is not a boolean value', async () => { + const resp = await request(app.getHttpServer()).get(`/v1/modules/keys`).query({ used: 0, operatorIndex: 2 }); + expect(resp.status).toEqual(400); + expect(resp.body).toEqual({ error: 'Bad Request', message: ['used must be a boolean value'], statusCode: 400 }); + }); + + it('Should return used keys for operator one', async () => { + const resp = await request(app.getHttpServer()).get(`/v1/modules/keys`).query({ used: true, operatorIndex: 1 }); + + const expectedKeysDvt = dvtModuleKeys.filter((key) => key.used && key.operatorIndex == 1); + const expectedKeysCurated = curatedModuleKeys.filter((key) => key.used && key.operatorIndex == 1); + + expect(resp.status).toEqual(200); + expect(resp.body.data).toEqual( + expect.arrayContaining([ + { keys: expectedKeysDvt, module: dvtModuleResp }, + { keys: expectedKeysCurated, module: curatedModuleResp }, + ]), + ); + expect(resp.body.meta).toEqual({ + elBlockSnapshot: { + blockNumber: elMeta.number, + blockHash: elMeta.hash, + timestamp: elMeta.timestamp, + }, + }); + }); + + it('Should return unused keys for operator one', async () => { + const resp = await request(app.getHttpServer()) + .get(`/v1/modules/keys`) + .query({ used: false, operatorIndex: 1 }); + + const expectedKeysDvt = dvtModuleKeys.filter((key) => !key.used && key.operatorIndex == 1); + const expectedKeysCurated = curatedModuleKeys.filter((key) => !key.used && key.operatorIndex == 1); + + expect(resp.status).toEqual(200); + expect(resp.body.data).toEqual( + expect.arrayContaining([ + { keys: expectedKeysDvt, module: dvtModuleResp }, + { keys: expectedKeysCurated, module: curatedModuleResp }, + ]), + ); + expect(resp.body.meta).toEqual({ + elBlockSnapshot: { + blockNumber: elMeta.number, + blockHash: elMeta.hash, + timestamp: elMeta.timestamp, + }, + }); + }); + + it('Should return empty keys lists for non-existent operator', async () => { + const resp = await request(app.getHttpServer()).get(`/v1/modules/keys`).query({ operatorIndex: 777 }); + + expect(resp.status).toEqual(200); + expect(resp.body.data).toEqual([ + { keys: [], module: dvtModuleResp }, + { keys: [], module: curatedModuleResp }, + ]); + expect(resp.body.meta).toEqual({ + elBlockSnapshot: { + blockNumber: elMeta.number, + blockHash: elMeta.hash, + timestamp: elMeta.timestamp, + }, + }); + }); + }); + + describe('too early response case', () => { + beforeEach(async () => { + await cleanDB(); + }); + afterEach(async () => { + await cleanDB(); + }); + + it('Should return too early response if there are no meta', async () => { + await moduleStorageService.upsert(dvtModule, 1); + const resp = await request(app.getHttpServer()).get(`/v1/modules/keys`); + expect(resp.status).toEqual(425); + expect(resp.body).toEqual({ message: 'Too early response', statusCode: 425 }); + }); + + it('Should return too early response if there are no modules', async () => { + await elMetaStorageService.update(elMeta); + const resp = await request(app.getHttpServer()).get(`/v1/modules/keys`); + expect(resp.status).toEqual(425); + expect(resp.body).toEqual({ message: 'Too early response', statusCode: 425 }); + }); + }); + }); +}); diff --git a/src/http/sr-modules-keys/sr-modules-keys.module.ts b/src/http/sr-modules-keys/sr-modules-keys.module.ts index 58561802..5977f47c 100644 --- a/src/http/sr-modules-keys/sr-modules-keys.module.ts +++ b/src/http/sr-modules-keys/sr-modules-keys.module.ts @@ -1,5 +1,5 @@ import { Module } from '@nestjs/common'; -import { LoggerModule } from 'common/logger'; +import { LoggerModule } from '../../common/logger'; import { SRModulesKeysController } from './sr-modules-keys.controller'; import { SRModulesKeysService } from './sr-modules-keys.service'; diff --git a/src/http/sr-modules-keys/sr-modules-keys.service.ts b/src/http/sr-modules-keys/sr-modules-keys.service.ts index 52207de9..ce56f194 100644 --- a/src/http/sr-modules-keys/sr-modules-keys.service.ts +++ b/src/http/sr-modules-keys/sr-modules-keys.service.ts @@ -1,9 +1,8 @@ import { Inject, Injectable, LoggerService } from '@nestjs/common'; -import { ConfigService } from 'common/config'; import { GroupedByModuleKeyListResponse, SRModuleKeyListResponse } from './entities'; -import { ModuleId, KeyQuery, Key, ELBlockSnapshot, SRModule } from 'http/common/entities'; +import { KeyQuery, Key, ELBlockSnapshot, SRModule } from '../common/entities'; import { LOGGER_PROVIDER } from '@lido-nestjs/logger'; -import { StakingRouterService } from 'staking-router-modules/staking-router.service'; +import { StakingRouterService } from '../../staking-router-modules/staking-router.service'; import { EntityManager } from '@mikro-orm/knex'; import { IsolationLevel } from '@mikro-orm/core'; @@ -11,7 +10,6 @@ import { IsolationLevel } from '@mikro-orm/core'; export class SRModulesKeysService { constructor( @Inject(LOGGER_PROVIDER) protected readonly logger: LoggerService, - protected configService: ConfigService, protected stakingRouterService: StakingRouterService, protected readonly entityManager: EntityManager, ) {} @@ -37,7 +35,7 @@ export class SRModulesKeysService { } async getModuleKeys( - moduleId: ModuleId, + moduleId: string | number, filters: KeyQuery, ): Promise<{ keysGenerator: AsyncGenerator; @@ -52,7 +50,7 @@ export class SRModulesKeysService { return { keysGenerator, module, meta: { elBlockSnapshot } }; } - async getModuleKeysByPubKeys(moduleId: ModuleId, pubKeys: string[]): Promise { + async getModuleKeysByPubKeys(moduleId: string | number, pubKeys: string[]): Promise { const { keys, module, elBlockSnapshot } = await this.entityManager.transactional( async () => { const { module, elBlockSnapshot } = await this.stakingRouterService.getStakingModuleAndMeta(moduleId); diff --git a/src/http/sr-modules-operators-keys/entities/sr-module-operators-keys.response.ts b/src/http/sr-modules-operators-keys/entities/sr-module-operators-keys.response.ts index 8325f3d3..b4a207a1 100644 --- a/src/http/sr-modules-operators-keys/entities/sr-module-operators-keys.response.ts +++ b/src/http/sr-modules-operators-keys/entities/sr-module-operators-keys.response.ts @@ -1,6 +1,5 @@ import { ApiProperty, ApiExtraModels } from '@nestjs/swagger'; -import { Key, Operator, SRModule } from 'http/common/entities/'; -import { ELMeta } from 'http/common/entities/'; +import { Key, Operator, SRModule, ELMeta } from '../../common/entities/'; @ApiExtraModels(Operator) @ApiExtraModels(Key) diff --git a/src/http/sr-modules-operators-keys/sr-modules-operators-keys.controller.ts b/src/http/sr-modules-operators-keys/sr-modules-operators-keys.controller.ts index c02f5c14..9eb0b602 100644 --- a/src/http/sr-modules-operators-keys/sr-modules-operators-keys.controller.ts +++ b/src/http/sr-modules-operators-keys/sr-modules-operators-keys.controller.ts @@ -1,9 +1,9 @@ import { Controller, Get, Version, Param, Query, NotFoundException, HttpStatus, Res } from '@nestjs/common'; import { ApiOperation, ApiResponse, ApiTags, ApiParam, ApiNotFoundResponse } from '@nestjs/swagger'; import { SRModuleOperatorsKeysResponse } from './entities'; -import { ModuleId, KeyQuery } from 'http/common/entities/'; +import { ModuleId, KeyQuery } from '../common/entities/'; import { SRModulesOperatorsKeysService } from './sr-modules-operators-keys.service'; -import { TooEarlyResponse } from 'http/common/entities/http-exceptions'; +import { TooEarlyResponse } from '../common/entities/http-exceptions'; import { EntityManager } from '@mikro-orm/knex'; import * as JSONStream from 'jsonstream'; import type { FastifyReply } from 'fastify'; @@ -39,20 +39,21 @@ export class SRModulesOperatorsKeysController { description: 'Staking router module_id or contract address.', }) @Get(':module_id/operators/keys') - async getOperatorsKeys( - @Param('module_id') moduleId: ModuleId, - @Query() filters: KeyQuery, - @Res() reply: FastifyReply, - ) { + async getOperatorsKeys(@Param() module: ModuleId, @Query() filters: KeyQuery, @Res() reply: FastifyReply) { await this.entityManager.transactional( async () => { - const { operators, keysGenerator, module, meta } = await this.srModulesOperatorsKeys.get(moduleId, filters); + const { + operators, + keysGenerator, + module: srModule, + meta, + } = await this.srModulesOperatorsKeys.get(module.module_id, filters); const jsonStream = JSONStream.stringify( '{ "meta": ' + JSON.stringify(meta) + ', "data": { "module": ' + - JSON.stringify(module) + + JSON.stringify(srModule) + ', "operators": ' + JSON.stringify(operators) + ', "keys": [', diff --git a/src/http/sr-modules-operators-keys/sr-modules-operators-keys.e2e-spec.ts b/src/http/sr-modules-operators-keys/sr-modules-operators-keys.e2e-spec.ts new file mode 100644 index 00000000..0e5ade08 --- /dev/null +++ b/src/http/sr-modules-operators-keys/sr-modules-operators-keys.e2e-spec.ts @@ -0,0 +1,258 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { Test } from '@nestjs/testing'; +import { Global, INestApplication, Module, ValidationPipe, VersioningType } from '@nestjs/common'; +import { + KeyRegistryService, + RegistryKeyStorageService, + RegistryOperatorStorageService, + RegistryStorageModule, + RegistryStorageService, +} from '../../common/registry'; +import { MikroORM } from '@mikro-orm/core'; +import { MikroOrmModule } from '@mikro-orm/nestjs'; +import { SRModulesOperatorsKeysController } from './sr-modules-operators-keys.controller'; +import { StakingRouterModule } from '../../staking-router-modules/staking-router.module'; + +import { SRModuleStorageService } from '../../storage/sr-module.storage'; +import { ElMetaStorageService } from '../../storage/el-meta.storage'; +import { SRModulesOperatorsKeysService } from './sr-modules-operators-keys.service'; +import { nullTransport, LoggerModule } from '@lido-nestjs/logger'; + +import * as request from 'supertest'; +import { FastifyAdapter, NestFastifyApplication } from '@nestjs/platform-fastify'; + +import { dvtModule, curatedModule, dvtModuleResp } from '../module.fixture'; +import { elMeta } from '../el-meta.fixture'; +import { keys, dvtModuleKeys } from '../key.fixtures'; +import { operators, operatorOneDvt, operatorTwoDvt } from '../operator.fixtures'; + +describe('SRModulesOperatorsKeysController (e2e)', () => { + let app: INestApplication; + + let keysStorageService: RegistryKeyStorageService; + let operatorsStorageService: RegistryOperatorStorageService; + let moduleStorageService: SRModuleStorageService; + let elMetaStorageService: ElMetaStorageService; + let registryStorage: RegistryStorageService; + + async function cleanDB() { + await keysStorageService.removeAll(); + await moduleStorageService.removeAll(); + await elMetaStorageService.removeAll(); + } + + @Global() + @Module({ + imports: [RegistryStorageModule], + providers: [KeyRegistryService], + exports: [KeyRegistryService, RegistryStorageModule], + }) + class KeyRegistryModule {} + + class KeysRegistryServiceMock { + async update(moduleAddress, blockHash) { + return; + } + } + + beforeAll(async () => { + const imports = [ + // sqlite3 only supports serializable transactions, ignoring the isolation level param + // TODO: use postgres + MikroOrmModule.forRoot({ + dbName: ':memory:', + type: 'sqlite', + allowGlobalContext: true, + entities: ['./**/*.entity.ts'], + }), + LoggerModule.forRoot({ transports: [nullTransport()] }), + KeyRegistryModule, + StakingRouterModule, + ]; + + const controllers = [SRModulesOperatorsKeysController]; + const providers = [SRModulesOperatorsKeysService]; + const moduleRef = await Test.createTestingModule({ imports, controllers, providers }) + .overrideProvider(KeyRegistryService) + .useClass(KeysRegistryServiceMock) + .compile(); + + elMetaStorageService = moduleRef.get(ElMetaStorageService); + keysStorageService = moduleRef.get(RegistryKeyStorageService); + moduleStorageService = moduleRef.get(SRModuleStorageService); + registryStorage = moduleRef.get(RegistryStorageService); + operatorsStorageService = moduleRef.get(RegistryOperatorStorageService); + + const generator = moduleRef.get(MikroORM).getSchemaGenerator(); + await generator.updateSchema(); + + app = moduleRef.createNestApplication(new FastifyAdapter()); + app.enableVersioning({ type: VersioningType.URI }); + app.useGlobalPipes(new ValidationPipe({ transform: true })); + + await app.init(); + await app.getHttpAdapter().getInstance().ready(); + }); + + afterAll(async () => { + await registryStorage.onModuleDestroy(); + await app.getHttpAdapter().close(); + await app.close(); + }); + + describe('The /operators request', () => { + describe('api ready to work', () => { + beforeAll(async () => { + // lets save meta + await elMetaStorageService.update(elMeta); + // lets save keys + await keysStorageService.save(keys); + // lets save operators + await operatorsStorageService.save(operators); + // lets save modules + await moduleStorageService.upsert(dvtModule, 1); + await moduleStorageService.upsert(curatedModule, 1); + }); + + afterAll(async () => { + await cleanDB(); + }); + + it('should return all keys for request without filters', async () => { + const resp = await request(app.getHttpServer()).get(`/v1/modules/${dvtModule.id}/operators/keys`); + + const respByContractAddress = await request(app.getHttpServer()).get( + `/v1/modules/${dvtModule.stakingModuleAddress}/operators/keys`, + ); + + expect(resp.body).toEqual(respByContractAddress.body); + + expect(resp.status).toEqual(200); + expect(resp.body.data.operators).toEqual(expect.arrayContaining([operatorOneDvt, operatorTwoDvt])); + expect(resp.body.data.keys).toEqual(expect.arrayContaining(dvtModuleKeys)); + expect(resp.body.data.module).toEqual(dvtModuleResp); + expect(resp.body.meta).toEqual({ + elBlockSnapshot: { + blockNumber: elMeta.number, + blockHash: elMeta.hash, + timestamp: elMeta.timestamp, + }, + }); + }); + + it('should return 400 error if operatorIndex is not a number', async () => { + const resp = await request(app.getHttpServer()) + .get(`/v1/modules/${dvtModule.id}/operators/keys`) + .query({ used: false, operatorIndex: 'one' }); + expect(resp.status).toEqual(400); + expect(resp.body).toEqual({ + error: 'Bad Request', + message: ['operatorIndex must not be less than 0', 'operatorIndex must be an integer number'], + statusCode: 400, + }); + }); + + it('should return 400 error if used is not a boolean value', async () => { + const resp = await request(app.getHttpServer()) + .get(`/v1/modules/${dvtModule.id}/operators/keys`) + .query({ used: 0, operatorIndex: 2 }); + expect(resp.status).toEqual(400); + expect(resp.body).toEqual({ error: 'Bad Request', message: ['used must be a boolean value'], statusCode: 400 }); + }); + + it('should return used keys and operator one', async () => { + const resp = await request(app.getHttpServer()) + .get(`/v1/modules/${dvtModule.id}/operators/keys`) + .query({ used: true, operatorIndex: 1 }); + + const expectedKeys = dvtModuleKeys.filter((key) => key.used && key.operatorIndex == 1); + + expect(resp.status).toEqual(200); + expect(resp.body.data.operators).toEqual(expect.arrayContaining([operatorOneDvt])); + expect(resp.body.data.keys).toEqual(expect.arrayContaining(expectedKeys)); + expect(resp.body.data.module).toEqual(dvtModuleResp); + expect(resp.body.meta).toEqual({ + elBlockSnapshot: { + blockNumber: elMeta.number, + blockHash: elMeta.hash, + timestamp: elMeta.timestamp, + }, + }); + }); + + it('should return unused keys and operator one', async () => { + const resp = await request(app.getHttpServer()) + .get(`/v1/modules/${dvtModule.id}/operators/keys`) + .query({ used: false, operatorIndex: 1 }); + + const expectedKeys = dvtModuleKeys.filter((key) => !key.used && key.operatorIndex == 1); + + expect(resp.status).toEqual(200); + expect(resp.body.data.operators).toEqual(expect.arrayContaining([operatorOneDvt])); + expect(resp.body.data.keys).toEqual(expect.arrayContaining(expectedKeys)); + expect(resp.body.data.module).toEqual(dvtModuleResp); + expect(resp.body.meta).toEqual({ + elBlockSnapshot: { + blockNumber: elMeta.number, + blockHash: elMeta.hash, + timestamp: elMeta.timestamp, + }, + }); + }); + + it('should return empty keys and operators lists for non-existent operator', async () => { + const resp = await request(app.getHttpServer()) + .get(`/v1/modules/${dvtModule.id}/operators/keys`) + .query({ operatorIndex: 0 }); + + expect(resp.status).toEqual(200); + expect(resp.body.data.operators).toEqual([]); + expect(resp.body.data.keys).toEqual([]); + expect(resp.body.data.module).toEqual(dvtModuleResp); + expect(resp.body.meta).toEqual({ + elBlockSnapshot: { + blockNumber: elMeta.number, + blockHash: elMeta.hash, + timestamp: elMeta.timestamp, + }, + }); + }); + + it("should return 404 if module doesn't exist", async () => { + const resp = await request(app.getHttpServer()).get('/v1/modules/777/operators/keys'); + expect(resp.status).toEqual(404); + expect(resp.body).toEqual({ + error: 'Not Found', + message: 'Module with moduleId 777 is not supported', + statusCode: 404, + }); + }); + + it('should return 400 error if module_id is not a contract address or number', async () => { + const resp = await request(app.getHttpServer()).get(`/v1/modules/sjdnsjkfsjkbfsjdfbdjfb/operators/keys`); + expect(resp.status).toEqual(400); + expect(resp.body).toEqual({ + error: 'Bad Request', + message: ['module_id must be a contract address or numeric value'], + statusCode: 400, + }); + }); + }); + + describe('too early response case', () => { + beforeEach(async () => { + await cleanDB(); + }); + afterEach(async () => { + await cleanDB(); + }); + + it('should return too early response if there are no meta', async () => { + await moduleStorageService.upsert(dvtModule, 1); + const resp = await request(app.getHttpServer()).get(`/v1/modules/${dvtModule.id}/operators/keys`); + expect(resp.status).toEqual(425); + expect(resp.body).toEqual({ message: 'Too early response', statusCode: 425 }); + }); + }); + }); +}); diff --git a/src/http/sr-modules-operators-keys/sr-modules-operators-keys.module.ts b/src/http/sr-modules-operators-keys/sr-modules-operators-keys.module.ts index 07bb88e9..4afde144 100644 --- a/src/http/sr-modules-operators-keys/sr-modules-operators-keys.module.ts +++ b/src/http/sr-modules-operators-keys/sr-modules-operators-keys.module.ts @@ -1,5 +1,5 @@ import { Module } from '@nestjs/common'; -import { LoggerModule } from 'common/logger'; +import { LoggerModule } from '../../common/logger'; import { SRModulesOperatorsKeysController } from './sr-modules-operators-keys.controller'; import { SRModulesOperatorsKeysService } from './sr-modules-operators-keys.service'; diff --git a/src/http/sr-modules-operators-keys/sr-modules-operators-keys.service.ts b/src/http/sr-modules-operators-keys/sr-modules-operators-keys.service.ts index cf2286d0..54cabb3e 100644 --- a/src/http/sr-modules-operators-keys/sr-modules-operators-keys.service.ts +++ b/src/http/sr-modules-operators-keys/sr-modules-operators-keys.service.ts @@ -1,22 +1,19 @@ import { Inject, Injectable, LoggerService } from '@nestjs/common'; -import { ELBlockSnapshot, Key, ModuleId, Operator, SRModule } from 'http/common/entities'; -import { KeyQuery } from 'http/common/entities'; -import { ConfigService } from 'common/config'; +import { ELBlockSnapshot, Key, Operator, SRModule } from '../common/entities'; +import { KeyQuery } from '../common/entities'; + import { LOGGER_PROVIDER } from '@lido-nestjs/logger'; -import { StakingRouterService } from 'staking-router-modules/staking-router.service'; -import { EntityManager } from '@mikro-orm/knex'; +import { StakingRouterService } from '../../staking-router-modules/staking-router.service'; @Injectable() export class SRModulesOperatorsKeysService { constructor( @Inject(LOGGER_PROVIDER) protected readonly logger: LoggerService, - protected readonly configService: ConfigService, protected stakingRouterService: StakingRouterService, - protected readonly entityManager: EntityManager, ) {} public async get( - moduleId: ModuleId, + moduleId: string | number, filters: KeyQuery, ): Promise<{ keysGenerator: AsyncGenerator; @@ -29,7 +26,11 @@ export class SRModulesOperatorsKeysService { const moduleInstance = this.stakingRouterService.getStakingRouterModuleImpl(module.type); const keysGenerator: AsyncGenerator = await moduleInstance.getKeysStream(module.stakingModuleAddress, filters); - const operatorsFilter = filters.operatorIndex ? { index: filters.operatorIndex } : {}; + const operatorsFilter = {}; + + if (filters.operatorIndex != undefined) { + operatorsFilter['index'] = filters.operatorIndex; + } const operators: Operator[] = await moduleInstance.getOperators(module.stakingModuleAddress, operatorsFilter); return { operators, keysGenerator, module, meta: { elBlockSnapshot } }; diff --git a/src/http/sr-modules-operators/sr-modules-operators.controller.ts b/src/http/sr-modules-operators/sr-modules-operators.controller.ts index 9de0184f..b0976c9c 100644 --- a/src/http/sr-modules-operators/sr-modules-operators.controller.ts +++ b/src/http/sr-modules-operators/sr-modules-operators.controller.ts @@ -7,7 +7,7 @@ import { } from './entities'; import { ModuleId } from '../common/entities/'; import { SRModulesOperatorsService } from './sr-modules-operators.service'; -import { OperatorIdParam } from '../common/entities/operator-id-param'; +import { OperatorId } from '../common/entities/operator-id'; import { TooEarlyResponse } from '../common/entities/http-exceptions'; @Controller('/') @@ -54,8 +54,8 @@ export class SRModulesOperatorsController { description: 'Staking router module_id or contract address.', }) @Get('modules/:module_id/operators') - getModuleOperators(@Param('module_id') moduleId: ModuleId) { - return this.srModulesOperators.getByModule(moduleId); + getModuleOperators(@Param() module: ModuleId) { + return this.srModulesOperators.getByModule(module.module_id); } @Version('1') @@ -80,8 +80,7 @@ export class SRModulesOperatorsController { description: 'Staking router module_id or contract address.', }) @Get('modules/:module_id/operators/:operator_id') - //TODO: here should be validation - getModuleOperator(@Param('module_id') moduleId: ModuleId, @Param() operator: OperatorIdParam) { - return this.srModulesOperators.getModuleOperator(moduleId, operator.operator_id); + getModuleOperator(@Param() module: ModuleId, @Param() operator: OperatorId) { + return this.srModulesOperators.getModuleOperator(module.module_id, operator.operator_id); } } diff --git a/src/http/sr-modules-operators/sr-modules-operators.e2e-spec.ts b/src/http/sr-modules-operators/sr-modules-operators.e2e-spec.ts new file mode 100644 index 00000000..bd3a8620 --- /dev/null +++ b/src/http/sr-modules-operators/sr-modules-operators.e2e-spec.ts @@ -0,0 +1,378 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { Test } from '@nestjs/testing'; +import { Global, INestApplication, Module, ValidationPipe, VersioningType } from '@nestjs/common'; +import { + KeyRegistryService, + RegistryOperatorStorageService, + RegistryStorageModule, + RegistryStorageService, +} from '../../common/registry'; +import { MikroORM } from '@mikro-orm/core'; +import { MikroOrmModule } from '@mikro-orm/nestjs'; +import { StakingRouterModule } from '../../staking-router-modules/staking-router.module'; + +import { SRModuleStorageService } from '../../storage/sr-module.storage'; +import { ElMetaStorageService } from '../../storage/el-meta.storage'; +import { nullTransport, LoggerModule } from '@lido-nestjs/logger'; + +import * as request from 'supertest'; +import { FastifyAdapter, NestFastifyApplication } from '@nestjs/platform-fastify'; + +import { dvtModule, curatedModule, srModules, dvtModuleResp, curatedModuleResp } from '../module.fixture'; +import { elMeta } from '../el-meta.fixture'; +import { SRModulesOperatorsController } from './sr-modules-operators.controller'; +import { SRModulesOperatorsService } from './sr-modules-operators.service'; +import { + operators, + operatorOneCurated, + operatorTwoCurated, + operatorOneDvt, + operatorTwoDvt, +} from '../operator.fixtures'; + +describe('SRModuleOperatorsController (e2e)', () => { + let app: INestApplication; + + let moduleStorageService: SRModuleStorageService; + let elMetaStorageService: ElMetaStorageService; + let registryStorage: RegistryStorageService; + let operatorsStorageService: RegistryOperatorStorageService; + + const operatorByModules = [ + { + operators: [operatorOneDvt, operatorTwoDvt], + module: dvtModuleResp, + }, + { + operators: [operatorOneCurated, operatorTwoCurated], + module: curatedModuleResp, + }, + ]; + + async function cleanDB() { + await operatorsStorageService.removeAll(); + await moduleStorageService.removeAll(); + await elMetaStorageService.removeAll(); + } + + @Global() + @Module({ + imports: [RegistryStorageModule], + providers: [KeyRegistryService], + exports: [KeyRegistryService, RegistryStorageModule], + }) + class KeyRegistryModule {} + + class KeysRegistryServiceMock { + async update(moduleAddress, blockHash) { + return; + } + } + + beforeAll(async () => { + const imports = [ + // sqlite3 only supports serializable transactions, ignoring the isolation level param + // TODO: use postgres + MikroOrmModule.forRoot({ + dbName: ':memory:', + type: 'sqlite', + allowGlobalContext: true, + entities: ['./**/*.entity.ts'], + }), + LoggerModule.forRoot({ transports: [nullTransport()] }), + KeyRegistryModule, + StakingRouterModule, + ]; + + const controllers = [SRModulesOperatorsController]; + const providers = [SRModulesOperatorsService]; + const moduleRef = await Test.createTestingModule({ imports, controllers, providers }) + .overrideProvider(KeyRegistryService) + .useClass(KeysRegistryServiceMock) + .compile(); + + elMetaStorageService = moduleRef.get(ElMetaStorageService); + operatorsStorageService = moduleRef.get(RegistryOperatorStorageService); + moduleStorageService = moduleRef.get(SRModuleStorageService); + registryStorage = moduleRef.get(RegistryStorageService); + + const generator = moduleRef.get(MikroORM).getSchemaGenerator(); + await generator.updateSchema(); + + app = moduleRef.createNestApplication(new FastifyAdapter()); + app.enableVersioning({ type: VersioningType.URI }); + app.useGlobalPipes(new ValidationPipe({ transform: true })); + + await app.init(); + await app.getHttpAdapter().getInstance().ready(); + }); + + afterAll(async () => { + await registryStorage.onModuleDestroy(); + await app.getHttpAdapter().close(); + await app.close(); + }); + + describe('The /operators request', () => { + describe('api ready to work', () => { + beforeAll(async () => { + // lets save meta + await elMetaStorageService.update(elMeta); + // lets save operators + await operatorsStorageService.save(operators); + + // lets save modules + await moduleStorageService.upsert(dvtModule, 1); + await moduleStorageService.upsert(curatedModule, 1); + }); + + afterAll(async () => { + await cleanDB(); + }); + + it('should return all operators for request without filters', async () => { + // Get all operators without filters + const resp = await request(app.getHttpServer()).get('/v1/operators'); + + expect(resp.status).toEqual(200); + expect(resp.body.data.length).toEqual(srModules.length); + expect(resp.body.data).toEqual(expect.arrayContaining(operatorByModules)); + + expect(resp.body.meta).toEqual({ + elBlockSnapshot: { + blockNumber: elMeta.number, + blockHash: elMeta.hash, + timestamp: elMeta.timestamp, + }, + }); + }); + }); + + describe('too early response case', () => { + beforeEach(async () => { + await cleanDB(); + }); + + afterEach(async () => { + await cleanDB(); + }); + + it('should return too early response if there are no modules in database', async () => { + // lets save meta + await elMetaStorageService.update(elMeta); + // lets save operators + await operatorsStorageService.save(operators); + + const resp = await request(app.getHttpServer()).get('/v1/operators'); + expect(resp.status).toEqual(425); + expect(resp.body).toEqual({ message: 'Too early response', statusCode: 425 }); + }); + + it('should return too early response if there are no meta', async () => { + // lets save operators + await operatorsStorageService.save(operators); + await moduleStorageService.upsert(curatedModule, 1); + + const resp = await request(app.getHttpServer()).get('/v1/operators'); + expect(resp.status).toEqual(425); + expect(resp.body).toEqual({ message: 'Too early response', statusCode: 425 }); + }); + }); + }); + + describe('The /modules/:module_id/operators request', () => { + describe('api ready to work', () => { + beforeAll(async () => { + // lets save meta + await elMetaStorageService.update(elMeta); + // lets save operators + await operatorsStorageService.save(operators); + + // lets save modules + await moduleStorageService.upsert(dvtModule, 1); + await moduleStorageService.upsert(curatedModule, 1); + }); + + afterAll(async () => { + await cleanDB(); + }); + + it('should return all operators that satisfy the request', async () => { + // Get all operators without filters + const resp = await request(app.getHttpServer()).get(`/v1/modules/${dvtModule.id}/operators`); + + const respByContractAddress = await request(app.getHttpServer()).get( + `/v1/modules/${dvtModule.stakingModuleAddress}/operators`, + ); + + expect(resp.body).toEqual(respByContractAddress.body); + + expect(resp.status).toEqual(200); + expect(resp.body.data.operators).toBeDefined(); + expect(resp.body.data.operators).toEqual(expect.arrayContaining([operatorOneDvt, operatorTwoDvt])); + + expect(resp.body.meta).toEqual({ + elBlockSnapshot: { + blockNumber: elMeta.number, + blockHash: elMeta.hash, + timestamp: elMeta.timestamp, + }, + }); + + const resp2 = await request(app.getHttpServer()).get(`/v1/modules/${curatedModule.id}/operators`); + + expect(resp2.status).toEqual(200); + expect(resp2.body.data.operators).toEqual(expect.arrayContaining([operatorOneCurated, operatorTwoCurated])); + + expect(resp2.body.meta).toEqual({ + elBlockSnapshot: { + blockNumber: elMeta.number, + blockHash: elMeta.hash, + timestamp: elMeta.timestamp, + }, + }); + }); + + it('should return 404 if module was not found', async () => { + // Get all operators without filters + const resp = await request(app.getHttpServer()).get('/v1/modules/777/operators'); + + expect(resp.status).toEqual(404); + expect(resp.body).toEqual({ + error: 'Not Found', + message: 'Module with moduleId 777 is not supported', + statusCode: 404, + }); + }); + + it('should return 400 error if module_id is not a contract address or number', async () => { + const resp = await request(app.getHttpServer()).get(`/v1/modules/sjdnsjkfsjkbfsjdfbdjfb/operators`); + expect(resp.status).toEqual(400); + expect(resp.body).toEqual({ + error: 'Bad Request', + message: ['module_id must be a contract address or numeric value'], + statusCode: 400, + }); + }); + }); + + describe('too early response case', () => { + beforeEach(async () => { + await cleanDB(); + }); + + afterEach(async () => { + await cleanDB(); + }); + + it('should return too early response if there are no meta', async () => { + // lets save operators + await operatorsStorageService.save(operators); + await moduleStorageService.upsert(curatedModule, 1); + + const resp = await request(app.getHttpServer()).get(`/v1/modules/${curatedModule.id}/operators`); + expect(resp.status).toEqual(425); + expect(resp.body).toEqual({ message: 'Too early response', statusCode: 425 }); + }); + }); + }); + + describe('The /modules/:module_id/operators/:operator_id', () => { + describe('api ready to work', () => { + beforeAll(async () => { + // lets save meta + await elMetaStorageService.update(elMeta); + // lets save operators + await operatorsStorageService.save(operators); + + // lets save modules + await moduleStorageService.upsert(dvtModule, 1); + await moduleStorageService.upsert(curatedModule, 1); + }); + + afterAll(async () => { + await cleanDB(); + }); + + it('should return operator and module', async () => { + const resp = await request(app.getHttpServer()).get( + `/v1/modules/${dvtModule.id}/operators/${operatorOneDvt.index}`, + ); + + expect(resp.status).toEqual(200); + expect(resp.body.data).toEqual({ operator: operatorOneDvt, module: dvtModuleResp }); + expect(resp.body.meta).toEqual({ + elBlockSnapshot: { + blockNumber: elMeta.number, + blockHash: elMeta.hash, + timestamp: elMeta.timestamp, + }, + }); + }); + + it('should return 404 if operator was not found', async () => { + const resp = await request(app.getHttpServer()).get(`/v1/modules/${dvtModule.id}/operators/777`); + expect(resp.status).toEqual(404); + expect(resp.body).toEqual({ + error: 'Not Found', + message: 'Operator with index 777 is not found for module with moduleId 2', + statusCode: 404, + }); + }); + + it('should return 404 if module was not found', async () => { + const resp = await request(app.getHttpServer()).get(`/v1/modules/777/operators/${operatorOneDvt.index}`); + expect(resp.status).toEqual(404); + expect(resp.body).toEqual({ + error: 'Not Found', + message: 'Module with moduleId 777 is not supported', + statusCode: 404, + }); + }); + + it('should return 400 error if operator_id is not a number', async () => { + const resp = await request(app.getHttpServer()).get(`/v1/modules/${dvtModule.id}/operators/somenumber`); + expect(resp.status).toEqual(400); + expect(resp.body).toEqual({ + error: 'Bad Request', + message: ['operator_id must not be less than 0', 'operator_id must be an integer number'], + statusCode: 400, + }); + }); + + it('should return 400 error if module_id is not a contract address or number', async () => { + const resp = await request(app.getHttpServer()).get( + `/v1/modules/sjdnsjkfsjkbfsjdfbdjfb/operators/${operatorOneCurated.index}`, + ); + expect(resp.status).toEqual(400); + expect(resp.body).toEqual({ + error: 'Bad Request', + message: ['module_id must be a contract address or numeric value'], + statusCode: 400, + }); + }); + }); + + describe('too early response case', () => { + beforeEach(async () => { + await cleanDB(); + }); + + afterEach(async () => { + await cleanDB(); + }); + + it('should return too early response if there are no meta', async () => { + // lets save operators + await operatorsStorageService.save(operators); + await moduleStorageService.upsert(curatedModule, 1); + + const resp = await request(app.getHttpServer()).get( + `/v1/modules/${curatedModule.id}/operators/${operatorOneCurated.index}`, + ); + expect(resp.status).toEqual(425); + expect(resp.body).toEqual({ message: 'Too early response', statusCode: 425 }); + }); + }); + }); +}); diff --git a/src/http/sr-modules-operators/sr-modules-operators.service.ts b/src/http/sr-modules-operators/sr-modules-operators.service.ts index acc1ebe4..94bf0d76 100644 --- a/src/http/sr-modules-operators/sr-modules-operators.service.ts +++ b/src/http/sr-modules-operators/sr-modules-operators.service.ts @@ -1,6 +1,5 @@ import { Inject, Injectable, LoggerService, NotFoundException } from '@nestjs/common'; -import { ConfigService } from '../../common/config'; -import { ModuleId, Operator, SRModule } from '../common/entities/'; +import { Operator, SRModule } from '../common/entities/'; import { GroupedByModuleOperatorListResponse, SRModuleOperatorListResponse, @@ -15,7 +14,6 @@ import { IsolationLevel } from '@mikro-orm/core'; export class SRModulesOperatorsService { constructor( @Inject(LOGGER_PROVIDER) protected readonly logger: LoggerService, - protected configService: ConfigService, protected stakingRouterService: StakingRouterService, protected readonly entityManager: EntityManager, ) {} @@ -41,14 +39,13 @@ export class SRModulesOperatorsService { return { data: operatorsByModules, meta: { elBlockSnapshot } }; } - public async getByModule(moduleId: ModuleId): Promise { + public async getByModule(moduleId: string | number): Promise { const { operators, module, elBlockSnapshot } = await this.entityManager.transactional( async () => { const { module, elBlockSnapshot } = await this.stakingRouterService.getStakingModuleAndMeta(moduleId); const moduleInstance = this.stakingRouterService.getStakingRouterModuleImpl(module.type); - // /v1/operators return these common fields for all modules const operators: Operator[] = await moduleInstance.getOperators(module.stakingModuleAddress, {}); return { operators, module, elBlockSnapshot }; @@ -64,7 +61,7 @@ export class SRModulesOperatorsService { }; } - public async getModuleOperator(moduleId: ModuleId, operatorIndex: number): Promise { + public async getModuleOperator(moduleId: string | number, operatorIndex: number): Promise { const { operator, module, elBlockSnapshot } = await this.entityManager.transactional( async () => { const { module, elBlockSnapshot } = await this.stakingRouterService.getStakingModuleAndMeta(moduleId); diff --git a/src/http/sr-modules-validators/entities/exit-validators.response.ts b/src/http/sr-modules-validators/entities/exit-validators.response.ts index 212ec42b..9924b089 100644 --- a/src/http/sr-modules-validators/entities/exit-validators.response.ts +++ b/src/http/sr-modules-validators/entities/exit-validators.response.ts @@ -1,5 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; -import { CLMeta } from 'http/common/entities'; +import { CLMeta } from '../../common/entities'; import { ExitValidator } from './exit-validator'; export class ExitValidatorListResponse { diff --git a/src/http/sr-modules-validators/entities/exits-presign-message.reponse.ts b/src/http/sr-modules-validators/entities/exits-presign-message.reponse.ts index e41b9f9c..57c6d545 100644 --- a/src/http/sr-modules-validators/entities/exits-presign-message.reponse.ts +++ b/src/http/sr-modules-validators/entities/exits-presign-message.reponse.ts @@ -1,5 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; -import { CLMeta } from 'http/common/entities'; +import { CLMeta } from '../../common/entities'; import { ExitPresignMessage } from './exits-presign-message'; export class ExitPresignMessageListResponse { diff --git a/src/http/sr-modules-validators/sr-modules-validators.controller.ts b/src/http/sr-modules-validators/sr-modules-validators.controller.ts index e5756d0e..348a1a00 100644 --- a/src/http/sr-modules-validators/sr-modules-validators.controller.ts +++ b/src/http/sr-modules-validators/sr-modules-validators.controller.ts @@ -17,11 +17,11 @@ import { ApiTags, } from '@nestjs/swagger'; import { SRModulesValidatorsService } from './sr-modules-validators.service'; -import { ModuleId } from 'http/common/entities/'; +import { ModuleId } from '../common/entities/'; import { ValidatorsQuery } from './entities/query'; import { ExitPresignMessageListResponse, ExitValidatorListResponse } from './entities'; -import { OperatorIdParam } from 'http/common/entities/operator-id-param'; -import { TooEarlyResponse } from 'http/common/entities/http-exceptions'; +import { OperatorId } from '../common/entities/operator-id'; +import { TooEarlyResponse } from '../common/entities/http-exceptions'; @Controller('modules') @ApiTags('validators') @@ -56,12 +56,8 @@ export class SRModulesValidatorsController { example: '0x55032650b14df07b85bF18A3a3eC8E0Af2e028d5', description: 'Staking router module_id or contract address.', }) - getOldestValidators( - @Param('module_id') moduleId: ModuleId, - @Param() operator: OperatorIdParam, - @Query() query: ValidatorsQuery, - ) { - return this.validatorsService.getOldestLidoValidators(moduleId, operator.operator_id, query); + getOldestValidators(@Param() module: ModuleId, @Param() operator: OperatorId, @Query() query: ValidatorsQuery) { + return this.validatorsService.getOldestLidoValidators(module.module_id, operator.operator_id, query); } @Version('1') @@ -93,10 +89,10 @@ export class SRModulesValidatorsController { description: 'Staking router module_id or contract address.', }) getMessagesForOldestValidators( - @Param('module_id') moduleId: ModuleId, - @Param() operator: OperatorIdParam, + @Param() module: ModuleId, + @Param() operator: OperatorId, @Query() query: ValidatorsQuery, ) { - return this.validatorsService.getVoluntaryExitMessages(moduleId, operator.operator_id, query); + return this.validatorsService.getVoluntaryExitMessages(module.module_id, operator.operator_id, query); } } diff --git a/src/http/sr-modules-validators/sr-modules-validators.e2e-spec.ts b/src/http/sr-modules-validators/sr-modules-validators.e2e-spec.ts new file mode 100644 index 00000000..00f386df --- /dev/null +++ b/src/http/sr-modules-validators/sr-modules-validators.e2e-spec.ts @@ -0,0 +1,597 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { Test } from '@nestjs/testing'; +import { Global, INestApplication, Module, ValidationPipe, VersioningType } from '@nestjs/common'; +import { + KeyRegistryService, + RegistryKey, + RegistryKeyStorageService, + RegistryOperator, + RegistryStorageModule, + RegistryStorageService, +} from '../../common/registry'; +import { MikroORM } from '@mikro-orm/core'; +import { MikroOrmModule } from '@mikro-orm/nestjs'; +import { SRModulesValidatorsController } from './sr-modules-validators.controller'; +import { StakingRouterModule } from '../../staking-router-modules/staking-router.module'; + +import { SRModuleStorageService } from '../../storage/sr-module.storage'; +import { ElMetaStorageService } from '../../storage/el-meta.storage'; +import { SRModulesValidatorsService } from './sr-modules-validators.service'; +import { nullTransport, LoggerModule } from '@lido-nestjs/logger'; + +import * as request from 'supertest'; +import { FastifyAdapter, NestFastifyApplication } from '@nestjs/platform-fastify'; + +import { dvtModule, curatedModule } from '../module.fixture'; +import { elMeta } from '../el-meta.fixture'; +import { keys } from '../key.fixtures'; +import { ConfigService } from '../../common/config'; +import { + ConsensusMetaEntity, + ConsensusValidatorEntity, + ValidatorsRegistryInterface, +} from '@lido-nestjs/validators-registry'; +import { ConsensusModule, ConsensusService } from '@lido-nestjs/consensus'; +import { FetchModule } from '@lido-nestjs/fetch'; +import { ConfigModule } from '../../common/config'; +import { ValidatorsModule } from '../../validators'; +import { SrModuleEntity } from '../../storage/sr-module.entity'; +import { ElMetaEntity } from '../../storage/el-meta.entity'; +import { + block, + header, + slot, + validators, + consensusMetaResp, + dvtOpOneResp100percent, + dvtOpOneResp10percent, + dvtOpOneResp20percent, + dvtOpOneResp5maxAmount, + dvtOpOneRespExitMessages100percent, + dvtOpOneRespExitMessages10percent, + dvtOpOneRespExitMessages20percent, + dvtOpOneRespExitMessages5maxAmount, +} from '../consensus.fixtures'; + +describe('SRModulesValidatorsController (e2e)', () => { + let app: INestApplication; + + let keysStorageService: RegistryKeyStorageService; + let moduleStorageService: SRModuleStorageService; + let elMetaStorageService: ElMetaStorageService; + let registryStorage: RegistryStorageService; + let validatorsRegistry: ValidatorsRegistryInterface; + + async function cleanDB() { + await keysStorageService.removeAll(); + await moduleStorageService.removeAll(); + await elMetaStorageService.removeAll(); + } + + @Global() + @Module({ + imports: [RegistryStorageModule], + providers: [KeyRegistryService], + exports: [KeyRegistryService, RegistryStorageModule], + }) + class KeyRegistryModule {} + + class KeysRegistryServiceMock { + async update(moduleAddress, blockHash) { + return; + } + } + + const consensusServiceMock = { + getBlockV2: (args: { blockId: string | number }) => { + return block; + }, + getBlockHeader: (args: { blockId: string | number }) => { + return header; + }, + getStateValidators: (args: { stateId: string }) => { + return validators; + }, + }; + + beforeAll(async () => { + const imports = [ + // sqlite3 only supports serializable transactions, ignoring the isolation level param + // TODO: use postgres + MikroOrmModule.forRoot({ + dbName: ':memory:', + type: 'sqlite', + allowGlobalContext: true, + entities: [ + RegistryKey, + RegistryOperator, + ConsensusValidatorEntity, + ConsensusMetaEntity, + SrModuleEntity, + ElMetaEntity, + ], + }), + LoggerModule.forRoot({ transports: [nullTransport()] }), + KeyRegistryModule, + StakingRouterModule, + ConfigModule, + ConsensusModule.forRoot({ + imports: [FetchModule], + }), + ValidatorsModule, + ]; + + const controllers = [SRModulesValidatorsController]; + const providers = [SRModulesValidatorsService]; + const moduleRef = await Test.createTestingModule({ imports, controllers, providers }) + .overrideProvider(KeyRegistryService) + .useClass(KeysRegistryServiceMock) + .overrideProvider(ConfigService) + .useValue({ + get(path) { + const conf = { VALIDATOR_REGISTRY_ENABLE: true }; + return conf[path]; + }, + }) + .overrideProvider(ConsensusService) + .useValue(consensusServiceMock) + .compile(); + + elMetaStorageService = moduleRef.get(ElMetaStorageService); + keysStorageService = moduleRef.get(RegistryKeyStorageService); + moduleStorageService = moduleRef.get(SRModuleStorageService); + registryStorage = moduleRef.get(RegistryStorageService); + // validatorsStorage = moduleRef.get(StorageService); + validatorsRegistry = moduleRef.get(ValidatorsRegistryInterface); + + const generator = moduleRef.get(MikroORM).getSchemaGenerator(); + await generator.updateSchema(); + + app = moduleRef.createNestApplication(new FastifyAdapter()); + app.enableVersioning({ type: VersioningType.URI }); + app.useGlobalPipes(new ValidationPipe({ transform: true })); + + await app.init(); + await app.getHttpAdapter().getInstance().ready(); + }); + + afterAll(async () => { + await registryStorage.onModuleDestroy(); + await app.getHttpAdapter().close(); + await app.close(); + }); + + describe('The /v1/modules/{:module_id}/validators/validator-exits-to-prepare/{:operator_id} request', () => { + describe('api ready to work', () => { + beforeAll(async () => { + // lets save keys + await keysStorageService.save(keys); + // lets save modules + await moduleStorageService.upsert(dvtModule, 1); + await moduleStorageService.upsert(curatedModule, 1); + await validatorsRegistry.update(slot); + }); + + afterAll(async () => { + await cleanDB(); + }); + + it("Should return 10% validators by default for 'dvt' module when 'operator' is set to 'one'", async () => { + await elMetaStorageService.update({ ...elMeta, number: consensusMetaResp.blockNumber }); + const resp = await request(app.getHttpServer()).get( + `/v1/modules/${dvtModule.id}/validators/validator-exits-to-prepare/1`, + ); + + expect(resp.status).toEqual(200); + expect(resp.body.data.length).toEqual(1); + expect(resp.body.data).toEqual(expect.arrayContaining(dvtOpOneResp10percent)); + expect(resp.body.meta).toEqual({ + clBlockSnapshot: consensusMetaResp, + }); + }); + + it("Should return 100% validators for 'dvt' module when 'operator' is set to 'one'", async () => { + await elMetaStorageService.update({ ...elMeta, number: consensusMetaResp.blockNumber }); + const resp = await request(app.getHttpServer()) + .get(`/v1/modules/${dvtModule.id}/validators/validator-exits-to-prepare/1`) + .query({ percent: 100 }); + + expect(resp.status).toEqual(200); + expect(resp.body.data.length).toEqual(10); + expect(resp.body.data).toEqual(expect.arrayContaining(dvtOpOneResp100percent)); + expect(resp.body.meta).toEqual({ + clBlockSnapshot: consensusMetaResp, + }); + }); + + it("Should prioritize 'percent' over 'max_amount' when both are provided in the query", async () => { + await elMetaStorageService.update({ ...elMeta, number: consensusMetaResp.blockNumber }); + const resp = await request(app.getHttpServer()) + .get(`/v1/modules/${dvtModule.id}/validators/validator-exits-to-prepare/1`) + .query({ percent: 20, max_amount: 5 }); + + expect(resp.status).toEqual(200); + expect(resp.body.data.length).toEqual(2); + expect(resp.body.data).toEqual(expect.arrayContaining(dvtOpOneResp20percent)); + expect(resp.body.meta).toEqual({ + clBlockSnapshot: consensusMetaResp, + }); + }); + + it('Should return 5 validators when max_amount is 5', async () => { + await elMetaStorageService.update({ ...elMeta, number: consensusMetaResp.blockNumber }); + const resp = await request(app.getHttpServer()) + .get(`/v1/modules/${dvtModule.id}/validators/validator-exits-to-prepare/1`) + .query({ max_amount: 5 }); + + expect(resp.status).toEqual(200); + expect(resp.body.data.length).toEqual(5); + expect(resp.body.data).toEqual(expect.arrayContaining(dvtOpOneResp5maxAmount)); + expect(resp.body.meta).toEqual({ + clBlockSnapshot: consensusMetaResp, + }); + }); + + it('Should return 100% validators when max_amount exceeds the total validator amount', async () => { + await elMetaStorageService.update({ ...elMeta, number: consensusMetaResp.blockNumber }); + const resp = await request(app.getHttpServer()) + .get(`/v1/modules/${dvtModule.id}/validators/validator-exits-to-prepare/1`) + .query({ max_amount: 100 }); + + expect(resp.status).toEqual(200); + expect(resp.body.data.length).toEqual(10); + expect(resp.body.data).toEqual(expect.arrayContaining(dvtOpOneResp100percent)); + expect(resp.body.meta).toEqual({ + clBlockSnapshot: consensusMetaResp, + }); + }); + + it('Should return 100% validators when percent exceeds the total validator amount', async () => { + await elMetaStorageService.update({ ...elMeta, number: consensusMetaResp.blockNumber }); + const resp = await request(app.getHttpServer()) + .get(`/v1/modules/${dvtModule.id}/validators/validator-exits-to-prepare/1`) + .query({ percent: 200 }); + + expect(resp.status).toEqual(200); + expect(resp.body.data.length).toEqual(10); + expect(resp.body.data).toEqual(expect.arrayContaining(dvtOpOneResp100percent)); + expect(resp.body.meta).toEqual({ + clBlockSnapshot: consensusMetaResp, + }); + }); + + it('Should return empty list of validators when percent equal to 0', async () => { + await elMetaStorageService.update({ ...elMeta, number: consensusMetaResp.blockNumber }); + const resp = await request(app.getHttpServer()) + .get(`/v1/modules/${dvtModule.id}/validators/validator-exits-to-prepare/1`) + .query({ percent: 0 }); + + expect(resp.status).toEqual(200); + expect(resp.body.data.length).toEqual(0); + expect(resp.body.data).toEqual([]); + expect(resp.body.meta).toEqual({ + clBlockSnapshot: consensusMetaResp, + }); + }); + + it('Should return empty list of validators when max_amount equal to 0', async () => { + await elMetaStorageService.update({ ...elMeta, number: consensusMetaResp.blockNumber }); + const resp = await request(app.getHttpServer()) + .get(`/v1/modules/${dvtModule.id}/validators/validator-exits-to-prepare/1`) + .query({ percent: 0 }); + + expect(resp.status).toEqual(200); + expect(resp.body.data.length).toEqual(0); + expect(resp.body.data).toEqual([]); + expect(resp.body.meta).toEqual({ + clBlockSnapshot: consensusMetaResp, + }); + }); + + it('Should return 400 error if percent or max_amount are negative', async () => { + await elMetaStorageService.update({ ...elMeta, number: consensusMetaResp.blockNumber }); + const resp = await request(app.getHttpServer()) + .get(`/v1/modules/${dvtModule.id}/validators/validator-exits-to-prepare/1`) + .query({ percent: -1, max_amount: -1 }); + + expect(resp.status).toEqual(400); + expect(resp.body.message).toEqual( + expect.arrayContaining(['max_amount must not be less than 0', 'percent must not be less than 0']), + ); + }); + + it('Should return 400 error if percent or max_amount is not number', async () => { + await elMetaStorageService.update({ ...elMeta, number: consensusMetaResp.blockNumber }); + const resp = await request(app.getHttpServer()) + .get(`/v1/modules/${dvtModule.id}/validators/validator-exits-to-prepare/1`) + .query({ max_amount: 'dfsdfds', percent: 'sdsdfsf' }); + + expect(resp.status).toEqual(400); + expect(resp.body.message).toEqual( + expect.arrayContaining([ + 'max_amount must not be less than 0', + 'max_amount must be an integer number', + 'percent must not be less than 0', + 'percent must be an integer number', + ]), + ); + }); + + it("Should return a 500 error if 'el_meta' is older than 'cl_meta'", async () => { + await elMetaStorageService.update({ ...elMeta, number: consensusMetaResp.blockNumber - 1 }); + const resp = await request(app.getHttpServer()).get( + `/v1/modules/${dvtModule.id}/validators/validator-exits-to-prepare/1`, + ); + + expect(resp.status).toEqual(500); + expect(resp.body).toEqual({ + error: 'Internal Server Error', + message: + 'The Execution Layer node is behind the Consensus Layer node, check that the EL node is synced and running.', + statusCode: 500, + }); + }); + + it('Should return a 404 error if the requested module does not exist', async () => { + await elMetaStorageService.update({ ...elMeta, number: consensusMetaResp.blockNumber }); + const resp = await request(app.getHttpServer()).get(`/v1/modules/777/validators/validator-exits-to-prepare/1`); + + expect(resp.status).toEqual(404); + expect(resp.body).toEqual({ + error: 'Not Found', + message: 'Module with moduleId 777 is not supported', + statusCode: 404, + }); + }); + + it("Should return empty list if operator doesn't exist", async () => { + await elMetaStorageService.update({ ...elMeta, number: consensusMetaResp.blockNumber }); + const resp = await request(app.getHttpServer()).get( + `/v1/modules/${dvtModule.id}/validators/validator-exits-to-prepare/777`, + ); + + expect(resp.status).toEqual(200); + expect(resp.body.data).toEqual([]); + expect(resp.body.meta).toEqual({ + clBlockSnapshot: consensusMetaResp, + }); + }); + }); + + describe('too early response case', () => { + beforeEach(async () => { + await cleanDB(); + }); + afterEach(async () => { + await cleanDB(); + }); + + it('Should return too early response if there are no meta', async () => { + await moduleStorageService.upsert(dvtModule, 1); + const resp = await request(app.getHttpServer()).get( + `/v1/modules/${dvtModule.id}/validators/validator-exits-to-prepare/1`, + ); + expect(resp.status).toEqual(425); + expect(resp.body).toEqual({ message: 'Too early response', statusCode: 425 }); + }); + }); + }); + + describe('The /v1/modules/{:module_id}/validators/generate-unsigned-exit-messages/{:operator_id} request', () => { + describe('api ready to work', () => { + beforeAll(async () => { + // lets save keys + await keysStorageService.save(keys); + // lets save modules + await moduleStorageService.upsert(dvtModule, 1); + await moduleStorageService.upsert(curatedModule, 1); + await validatorsRegistry.update(slot); + }); + + afterAll(async () => { + await cleanDB(); + }); + + it("Should return 10% validators by default for 'dvt' module when 'operator' is set to 'one'", async () => { + await elMetaStorageService.update({ ...elMeta, number: consensusMetaResp.blockNumber }); + const resp = await request(app.getHttpServer()).get( + `/v1/modules/${dvtModule.id}/validators/generate-unsigned-exit-messages/1`, + ); + + expect(resp.status).toEqual(200); + expect(resp.body.data.length).toEqual(1); + expect(resp.body.data).toEqual(expect.arrayContaining(dvtOpOneRespExitMessages10percent)); + expect(resp.body.meta).toEqual({ + clBlockSnapshot: consensusMetaResp, + }); + }); + + it("Should return 100% validators for 'dvt' module when 'operator' is set to 'one'", async () => { + await elMetaStorageService.update({ ...elMeta, number: consensusMetaResp.blockNumber }); + const resp = await request(app.getHttpServer()) + .get(`/v1/modules/${dvtModule.id}/validators/generate-unsigned-exit-messages/1`) + .query({ percent: 100 }); + + expect(resp.status).toEqual(200); + expect(resp.body.data.length).toEqual(10); + expect(resp.body.data).toEqual(expect.arrayContaining(dvtOpOneRespExitMessages100percent)); + expect(resp.body.meta).toEqual({ + clBlockSnapshot: consensusMetaResp, + }); + }); + + it("Should prioritize 'percent' over 'max_amount' when both are provided in the query", async () => { + await elMetaStorageService.update({ ...elMeta, number: consensusMetaResp.blockNumber }); + const resp = await request(app.getHttpServer()) + .get(`/v1/modules/${dvtModule.id}/validators/generate-unsigned-exit-messages/1`) + .query({ percent: 20, max_amount: 5 }); + + expect(resp.status).toEqual(200); + expect(resp.body.data.length).toEqual(2); + expect(resp.body.data).toEqual(expect.arrayContaining(dvtOpOneRespExitMessages20percent)); + expect(resp.body.meta).toEqual({ + clBlockSnapshot: consensusMetaResp, + }); + }); + + it('Should return 5 validators when max_amount is 5', async () => { + await elMetaStorageService.update({ ...elMeta, number: consensusMetaResp.blockNumber }); + const resp = await request(app.getHttpServer()) + .get(`/v1/modules/${dvtModule.id}/validators/generate-unsigned-exit-messages/1`) + .query({ max_amount: 5 }); + + expect(resp.status).toEqual(200); + expect(resp.body.data.length).toEqual(5); + expect(resp.body.data).toEqual(expect.arrayContaining(dvtOpOneRespExitMessages5maxAmount)); + expect(resp.body.meta).toEqual({ + clBlockSnapshot: consensusMetaResp, + }); + }); + + it('Should return 100% validators when max_amount exceeds the total validator amount', async () => { + await elMetaStorageService.update({ ...elMeta, number: consensusMetaResp.blockNumber }); + const resp = await request(app.getHttpServer()) + .get(`/v1/modules/${dvtModule.id}/validators/generate-unsigned-exit-messages/1`) + .query({ max_amount: 100 }); + + expect(resp.status).toEqual(200); + expect(resp.body.data.length).toEqual(10); + expect(resp.body.data).toEqual(expect.arrayContaining(dvtOpOneRespExitMessages100percent)); + expect(resp.body.meta).toEqual({ + clBlockSnapshot: consensusMetaResp, + }); + }); + + it('Should return 100% validators when percent exceeds the total validator amount', async () => { + await elMetaStorageService.update({ ...elMeta, number: consensusMetaResp.blockNumber }); + const resp = await request(app.getHttpServer()) + .get(`/v1/modules/${dvtModule.id}/validators/generate-unsigned-exit-messages/1`) + .query({ percent: 200 }); + + expect(resp.status).toEqual(200); + expect(resp.body.data.length).toEqual(10); + expect(resp.body.data).toEqual(expect.arrayContaining(dvtOpOneRespExitMessages100percent)); + expect(resp.body.meta).toEqual({ + clBlockSnapshot: consensusMetaResp, + }); + }); + + it('Should return empty list of validators when percent equal to 0', async () => { + await elMetaStorageService.update({ ...elMeta, number: consensusMetaResp.blockNumber }); + const resp = await request(app.getHttpServer()) + .get(`/v1/modules/${dvtModule.id}/validators/generate-unsigned-exit-messages/1`) + .query({ percent: 0 }); + + expect(resp.status).toEqual(200); + expect(resp.body.data.length).toEqual(0); + expect(resp.body.data).toEqual([]); + expect(resp.body.meta).toEqual({ + clBlockSnapshot: consensusMetaResp, + }); + }); + + it('Should return empty list of validators when max_amount equal to 0', async () => { + await elMetaStorageService.update({ ...elMeta, number: consensusMetaResp.blockNumber }); + const resp = await request(app.getHttpServer()) + .get(`/v1/modules/${dvtModule.id}/validators/generate-unsigned-exit-messages/1`) + .query({ percent: 0 }); + + expect(resp.status).toEqual(200); + expect(resp.body.data.length).toEqual(0); + expect(resp.body.data).toEqual([]); + expect(resp.body.meta).toEqual({ + clBlockSnapshot: consensusMetaResp, + }); + }); + + it('Should return 400 error if percent or max_amount are negative', async () => { + await elMetaStorageService.update({ ...elMeta, number: consensusMetaResp.blockNumber }); + const resp = await request(app.getHttpServer()) + .get(`/v1/modules/${dvtModule.id}/validators/generate-unsigned-exit-messages/1`) + .query({ percent: -1, max_amount: -1 }); + + expect(resp.status).toEqual(400); + expect(resp.body.message).toEqual( + expect.arrayContaining(['max_amount must not be less than 0', 'percent must not be less than 0']), + ); + }); + + it('Should return 400 error if percent or max_amount is not number', async () => { + await elMetaStorageService.update({ ...elMeta, number: consensusMetaResp.blockNumber }); + const resp = await request(app.getHttpServer()) + .get(`/v1/modules/${dvtModule.id}/validators/generate-unsigned-exit-messages/1`) + .query({ max_amount: 'dfsdfds', percent: 'sdsdfsf' }); + + expect(resp.status).toEqual(400); + expect(resp.body.message).toEqual( + expect.arrayContaining([ + 'max_amount must not be less than 0', + 'max_amount must be an integer number', + 'percent must not be less than 0', + 'percent must be an integer number', + ]), + ); + }); + + it("Should return a 500 error if 'el_meta' is older than 'cl_meta'", async () => { + await elMetaStorageService.update({ ...elMeta, number: consensusMetaResp.blockNumber - 1 }); + const resp = await request(app.getHttpServer()).get( + `/v1/modules/${dvtModule.id}/validators/generate-unsigned-exit-messages/1`, + ); + + expect(resp.status).toEqual(500); + expect(resp.body).toEqual({ + error: 'Internal Server Error', + message: + 'The Execution Layer node is behind the Consensus Layer node, check that the EL node is synced and running.', + statusCode: 500, + }); + }); + + it('Should return a 404 error if the requested module does not exist', async () => { + await elMetaStorageService.update({ ...elMeta, number: consensusMetaResp.blockNumber }); + const resp = await request(app.getHttpServer()).get( + `/v1/modules/777/validators/generate-unsigned-exit-messages/1`, + ); + + expect(resp.status).toEqual(404); + expect(resp.body).toEqual({ + error: 'Not Found', + message: 'Module with moduleId 777 is not supported', + statusCode: 404, + }); + }); + + it("Should return empty list if operator doesn't exist", async () => { + await elMetaStorageService.update({ ...elMeta, number: consensusMetaResp.blockNumber }); + const resp = await request(app.getHttpServer()).get( + `/v1/modules/${dvtModule.id}/validators/generate-unsigned-exit-messages/777`, + ); + + expect(resp.status).toEqual(200); + expect(resp.body.data).toEqual([]); + expect(resp.body.meta).toEqual({ + clBlockSnapshot: consensusMetaResp, + }); + }); + }); + + describe('too early response case', () => { + beforeEach(async () => { + await cleanDB(); + }); + afterEach(async () => { + await cleanDB(); + }); + + it('Should return too early response if there are no meta', async () => { + await moduleStorageService.upsert(dvtModule, 1); + const resp = await request(app.getHttpServer()).get( + `/v1/modules/${dvtModule.id}/validators/generate-unsigned-exit-messages/1`, + ); + expect(resp.status).toEqual(425); + expect(resp.body).toEqual({ message: 'Too early response', statusCode: 425 }); + }); + }); + }); +}); diff --git a/src/http/sr-modules-validators/sr-modules-validators.module.ts b/src/http/sr-modules-validators/sr-modules-validators.module.ts index 43df82f3..c441bf50 100644 --- a/src/http/sr-modules-validators/sr-modules-validators.module.ts +++ b/src/http/sr-modules-validators/sr-modules-validators.module.ts @@ -1,6 +1,6 @@ import { Module } from '@nestjs/common'; -import { LoggerModule } from 'common/logger'; -import { ValidatorsModule } from 'validators'; +import { LoggerModule } from '../../common/logger'; +import { ValidatorsModule } from '../../validators'; import { SRModulesValidatorsController } from './sr-modules-validators.controller'; import { SRModulesValidatorsService } from './sr-modules-validators.service'; diff --git a/src/http/sr-modules-validators/sr-modules-validators.service.ts b/src/http/sr-modules-validators/sr-modules-validators.service.ts index 17a618f6..1d096067 100644 --- a/src/http/sr-modules-validators/sr-modules-validators.service.ts +++ b/src/http/sr-modules-validators/sr-modules-validators.service.ts @@ -1,5 +1,5 @@ import { Injectable, Inject, InternalServerErrorException, LoggerService } from '@nestjs/common'; -import { ConfigService } from 'common/config'; +import { ConfigService } from '../../common/config'; import { ExitValidatorListResponse, ExitValidator, @@ -7,18 +7,18 @@ import { ExitPresignMessage, ValidatorsQuery, } from './entities'; -import { CLBlockSnapshot, ModuleId } from 'http/common/entities/'; +import { CLBlockSnapshot } from '../common/entities/'; import { LOGGER_PROVIDER } from '@lido-nestjs/logger'; import { Validator } from '@lido-nestjs/validators-registry'; -import { ValidatorsService } from 'validators'; -import { StakingRouterService } from 'staking-router-modules/staking-router.service'; +import { ValidatorsService } from '../../validators'; +import { StakingRouterService } from '../../staking-router-modules/staking-router.service'; import { EntityManager } from '@mikro-orm/knex'; import { DEFAULT_EXIT_PERCENT, VALIDATORS_STATUSES_FOR_EXIT, VALIDATORS_REGISTRY_DISABLED_ERROR, -} from 'validators/validators.constants'; -import { httpExceptionTooEarlyResp } from 'http/common/entities/http-exceptions'; +} from '../../validators/validators.constants'; +import { httpExceptionTooEarlyResp } from '../common/entities/http-exceptions'; import { IsolationLevel } from '@mikro-orm/core'; @Injectable() @@ -32,7 +32,7 @@ export class SRModulesValidatorsService { ) {} async getOldestLidoValidators( - moduleId: ModuleId, + moduleId: string | number, operatorId: number, filters: ValidatorsQuery, ): Promise { @@ -53,7 +53,7 @@ export class SRModulesValidatorsService { } async getVoluntaryExitMessages( - moduleId: ModuleId, + moduleId: string | number, operatorId: number, filters: ValidatorsQuery, ): Promise { @@ -86,7 +86,7 @@ export class SRModulesValidatorsService { } private async getOperatorOldestValidators( - moduleId: string, + moduleId: string | number, operatorIndex: number, filters: ValidatorsQuery, ): Promise<{ validators: Validator[]; clBlockSnapshot: CLBlockSnapshot }> { @@ -126,12 +126,12 @@ export class SRModulesValidatorsService { // We need EL meta always be actual if (elBlockSnapshot.blockNumber < clMeta.blockNumber) { - this.logger.warn('Last Execution Layer block number in our database older than last Consensus Layer'); - // add metric or alert on breaking el > cl condition - // TODO: what answer will be better here? - // TODO: describe in doc + this.logger.warn( + 'The Execution Layer node is behind the Consensus Layer node, check that the EL node is synced and running.', + ); + // TODO: add metric or alert on breaking el > cl condition throw new InternalServerErrorException( - 'Last Execution Layer block number in our database older than last Consensus Layer', + 'The Execution Layer node is behind the Consensus Layer node, check that the EL node is synced and running.', ); } diff --git a/src/http/sr-modules/sr-modules.controller.ts b/src/http/sr-modules/sr-modules.controller.ts index 120e1978..fc832517 100644 --- a/src/http/sr-modules/sr-modules.controller.ts +++ b/src/http/sr-modules/sr-modules.controller.ts @@ -49,8 +49,7 @@ export class SRModulesController { name: 'module_id', description: 'Staking router module_id or contract address.', }) - // TODO: add pattern check Module Id - getModule(@Param('module_id') moduleId: ModuleId) { - return this.srModulesService.getModule(moduleId); + getModule(@Param() module: ModuleId) { + return this.srModulesService.getModule(module.module_id); } } diff --git a/src/http/sr-modules/sr-modules.e2e-spec.ts b/src/http/sr-modules/sr-modules.e2e-spec.ts index 7fb3bb86..4d4b75a5 100644 --- a/src/http/sr-modules/sr-modules.e2e-spec.ts +++ b/src/http/sr-modules/sr-modules.e2e-spec.ts @@ -6,7 +6,7 @@ import { MikroOrmModule } from '@mikro-orm/nestjs'; import { KeyRegistryService, RegistryStorageModule, RegistryStorageService } from '../../common/registry'; import { StakingRouterModule } from '../../staking-router-modules/staking-router.module'; -import { dvtModule, curatedModule, dvtModuleResp, curatedModuleResp } from '../../storage/module.fixture'; +import { dvtModule, curatedModule, dvtModuleResp, curatedModuleResp } from '../module.fixture'; import { SRModuleStorageService } from '../../storage/sr-module.storage'; import { ElMetaStorageService } from '../../storage/el-meta.storage'; @@ -17,7 +17,7 @@ import { FastifyAdapter, NestFastifyApplication } from '@nestjs/platform-fastify import { SRModulesController } from './sr-modules.controller'; import { SRModulesService } from './sr-modules.service'; -// import { validationOpt } from '../../main'; +import { elMeta } from '../el-meta.fixture'; describe('SRModulesController (e2e)', () => { let app: INestApplication; @@ -26,12 +26,6 @@ describe('SRModulesController (e2e)', () => { let elMetaStorageService: ElMetaStorageService; let registryStorage: RegistryStorageService; - const elMeta = { - number: 74, - hash: '0x662e3e713207240b25d01324b6eccdc91493249a5048881544254994694530a5', - timestamp: 1691500803, - }; - async function cleanDB() { await moduleStorageService.removeAll(); await elMetaStorageService.removeAll(); @@ -173,6 +167,17 @@ describe('SRModulesController (e2e)', () => { }); }); + it('should return module by contract address', async () => { + const resp = await request(app.getHttpServer()).get(`/v1/modules/${dvtModule.stakingModuleAddress}`); + expect(resp.status).toEqual(200); + expect(resp.body.data).toEqual(dvtModuleResp); + expect(resp.body.elBlockSnapshot).toEqual({ + blockNumber: elMeta.number, + blockHash: elMeta.hash, + timestamp: elMeta.timestamp, + }); + }); + it("should return 404 if module doesn't exist", async () => { const resp = await request(app.getHttpServer()).get(`/v1/modules/77`); expect(resp.status).toEqual(404); @@ -182,7 +187,18 @@ describe('SRModulesController (e2e)', () => { statusCode: 404, }); }); + + it('should return 400 error if module_id is not a contract address or number', async () => { + const resp = await request(app.getHttpServer()).get(`/v1/modules/sjdnsjkfsjkbfsjdfbdjfb`); + expect(resp.status).toEqual(400); + expect(resp.body).toEqual({ + error: 'Bad Request', + message: ['module_id must be a contract address or numeric value'], + statusCode: 400, + }); + }); }); + describe('too early response case', () => { beforeEach(async () => { await cleanDB(); diff --git a/src/http/sr-modules/sr-modules.service.ts b/src/http/sr-modules/sr-modules.service.ts index 05e3723a..e9f4e1d9 100644 --- a/src/http/sr-modules/sr-modules.service.ts +++ b/src/http/sr-modules/sr-modules.service.ts @@ -2,7 +2,6 @@ import { Inject, Injectable, LoggerService } from '@nestjs/common'; import { LOGGER_PROVIDER } from '@lido-nestjs/logger'; import { SRModuleResponse, SRModuleListResponse } from './entities'; import { ELBlockSnapshot, SRModule } from '../common/entities'; -import { ModuleId } from '../common/entities/'; import { StakingRouterService } from '../../staking-router-modules/staking-router.service'; @Injectable() @@ -23,7 +22,7 @@ export class SRModulesService { }; } - async getModule(moduleId: ModuleId): Promise { + async getModule(moduleId: string | number): Promise { const { module, elBlockSnapshot } = await this.stakingRouterService.getStakingModuleAndMeta(moduleId); return { diff --git a/src/http/status/status.service.ts b/src/http/status/status.service.ts index db07e81e..a12f783c 100644 --- a/src/http/status/status.service.ts +++ b/src/http/status/status.service.ts @@ -19,8 +19,6 @@ export class StatusService { public async get(): Promise { const chainId = this.configService.get('CHAIN_ID'); - - // TODO: maybe move this code to sr-modules-service const { elMeta, clMeta } = await this.entityManager.transactional( async () => { const elMeta = await this.stakingRouterService.getElBlockSnapshot(); diff --git a/src/jobs/keys-update/keys-update.service.ts b/src/jobs/keys-update/keys-update.service.ts index 342efb05..349dc343 100644 --- a/src/jobs/keys-update/keys-update.service.ts +++ b/src/jobs/keys-update/keys-update.service.ts @@ -119,15 +119,10 @@ export class KeysUpdateService { this.logger.warn('Previous data is newer than current data', prevElMeta); return; } - - // TODO: еcли была реорганизация, может ли currElMeta.number быть меньше и нам надо обновиться ? - const storageModules = await this.srModulesStorage.findAll(); // get staking router modules from SR contract const modules = await this.stakingRouterFetchService.getStakingModules({ blockHash: currElMeta.hash }); - // TODO: is it correct that i use here modules from blockchain instead of storage - if (this.modulesWereDeleted(modules, storageModules)) { const error = new Error('Modules list is wrong'); this.logger.error(error); @@ -141,12 +136,12 @@ export class KeysUpdateService { for (const module of modules) { const moduleInstance = this.stakingRouterService.getStakingRouterModuleImpl(module.type); - - // At the moment lets think that for all modules it is possible to make decision base on nonce value const currNonce = await moduleInstance.getCurrentNonce(module.stakingModuleAddress, currElMeta.hash); const moduleInStorage = await this.srModulesStorage.findOneById(module.id); + // update staking module information + await this.srModulesStorage.upsert(module, currNonce); - // now updating decision should be here moduleInstance.updateKeys + // now updating decision should be here moduleInstance.update // TODO: operators list also the same ? if (moduleInStorage && moduleInStorage.nonce === currNonce) { // nothing changed, don't need to update @@ -156,7 +151,6 @@ export class KeysUpdateService { return; } - await this.srModulesStorage.upsert(module, currNonce); await moduleInstance.update(module.stakingModuleAddress, currElMeta.hash); } }, @@ -176,6 +170,7 @@ export class KeysUpdateService { const elMeta = await this.stakingRouterService.getElBlockSnapshot(); if (!elMeta) { + this.logger.warn("Meta is null, maybe data hasn't been written in db yet"); return; } diff --git a/src/migrations/Migration20230903183749.ts b/src/migrations/Migration20230903183749.ts index 33fe6234..7aca5ebc 100644 --- a/src/migrations/Migration20230903183749.ts +++ b/src/migrations/Migration20230903183749.ts @@ -38,9 +38,9 @@ export class Migration20230903183749 extends Migration { } async down(): Promise { - // TODO: I think it will not work for keys of different modules with the same index and operatorIndex + // it will not work for keys of different modules with the same index and operatorIndex // also we will get column "module_address" of relation "registry_key" contains null values during up of this transaction - // it the reason why I add here truncate + // it is the reason why truncate was added here this.addSql('TRUNCATE registry_key'); this.addSql('TRUNCATE registry_operator'); diff --git a/src/mikro-orm.config.ts b/src/mikro-orm.config.ts index e3e9dae9..a5d8b098 100644 --- a/src/mikro-orm.config.ts +++ b/src/mikro-orm.config.ts @@ -62,8 +62,6 @@ const findMigrations = (mainFolder: string, npmPackageNames: string[]): Migratio // TODO think about Nest.js logger console.log(`Found [${migrations.length}] DB migration files.`); - // console.log(migrations); - return migrations; }; diff --git a/src/staking-router-modules/contracts/staking-router/staking-router-fetch.service.ts b/src/staking-router-modules/contracts/staking-router/staking-router-fetch.service.ts index b9a6f61f..f325373b 100644 --- a/src/staking-router-modules/contracts/staking-router/staking-router-fetch.service.ts +++ b/src/staking-router-modules/contracts/staking-router/staking-router-fetch.service.ts @@ -54,8 +54,7 @@ export class StakingRouterFetchService { blockTag, )) as STAKING_MODULE_TYPE; - // TODO: reconsider way of checking this module type without - // TODO: how to handle this case? + // TODO: reconsider way of checking this module type if (!Object.values(STAKING_MODULE_TYPE).includes(stakingModuleType)) { this.logger.error(new Error(`Staking Module type ${STAKING_MODULE_TYPE} is unknown`)); process.exit(1); diff --git a/src/staking-router-modules/curated-module.service.ts b/src/staking-router-modules/curated-module.service.ts index d7960bd0..a080cb01 100644 --- a/src/staking-router-modules/curated-module.service.ts +++ b/src/staking-router-modules/curated-module.service.ts @@ -25,7 +25,7 @@ export class CuratedModuleService implements StakingModuleInterface { } public async getCurrentNonce(moduleAddress: string, blockHash: string): Promise { - const nonce = await this.keyRegistryService.getNonceFromContract(moduleAddress, blockHash); + const nonce = await this.keyRegistryService.getStakingModuleNonce(moduleAddress, blockHash); return nonce; } diff --git a/src/staking-router-modules/interfaces/filters.ts b/src/staking-router-modules/interfaces/filters.ts index f3129f88..94f52d11 100644 --- a/src/staking-router-modules/interfaces/filters.ts +++ b/src/staking-router-modules/interfaces/filters.ts @@ -1,4 +1,3 @@ -//TODO: KeysFilter HTTP query // should staking router service import this type from http/entities export interface KeysFilter { used?: boolean; diff --git a/src/staking-router-modules/staking-router.service.ts b/src/staking-router-modules/staking-router.service.ts index dca2094a..53a4c1d4 100644 --- a/src/staking-router-modules/staking-router.service.ts +++ b/src/staking-router-modules/staking-router.service.ts @@ -4,14 +4,13 @@ import { LOGGER_PROVIDER } from '@lido-nestjs/logger'; import { ModuleRef } from '@nestjs/core'; import { StakingModuleInterface } from './interfaces/staking-module.interface'; import { httpExceptionTooEarlyResp } from '../http/common/entities/http-exceptions'; -import { ELBlockSnapshot, ModuleId, SRModule } from '../http/common/entities'; +import { ELBlockSnapshot, SRModule } from '../http/common/entities'; import { config } from './staking-module-impl-config'; import { IsolationLevel } from '@mikro-orm/core'; import { SrModuleEntity } from 'storage/sr-module.entity'; import { SRModuleStorageService } from '../storage/sr-module.storage'; import { ElMetaStorageService } from '../storage/el-meta.storage'; import { ElMetaEntity } from 'storage/el-meta.entity'; -import { isValidContractAddress } from './utils'; @Injectable() export class StakingRouterService { @@ -37,16 +36,12 @@ export class StakingRouterService { * @param moduleId - id or address of staking module * @returns Staking module from database */ - public async getStakingModule(moduleId: ModuleId): Promise { - if (isValidContractAddress(moduleId)) { + public async getStakingModule(moduleId: string | number): Promise { + if (typeof moduleId == 'string') { return await this.srModulesStorage.findOneByContractAddress(moduleId); } - if (Number(moduleId)) { - return await this.srModulesStorage.findOneById(Number(moduleId)); - } - - return null; + return await this.srModulesStorage.findOneById(moduleId); } public getStakingRouterModuleImpl(moduleType: string): StakingModuleInterface { @@ -102,7 +97,7 @@ export class StakingRouterService { * @returns staking module from database and execution layer meta */ public async getStakingModuleAndMeta( - moduleId: ModuleId, + moduleId: string | number, ): Promise<{ module: SRModule; elBlockSnapshot: ELBlockSnapshot }> { const { stakingModule, elBlockSnapshot } = await this.entityManager.transactional( async () => { diff --git a/src/staking-router-modules/utils.ts b/src/staking-router-modules/utils.ts deleted file mode 100644 index d2de6850..00000000 --- a/src/staking-router-modules/utils.ts +++ /dev/null @@ -1,4 +0,0 @@ -export function isValidContractAddress(address: string): boolean { - const contractAddressRegex = /^0x[0-9a-fA-F]{40}$/; - return contractAddressRegex.test(address); -} diff --git a/src/storage/constants.ts b/src/storage/constants.ts index 55b91e01..68d010c9 100644 --- a/src/storage/constants.ts +++ b/src/storage/constants.ts @@ -1 +1,2 @@ export const MODULE_ADDRESS_LEN = 42; +export const BLOCK_HASH_LEN = 66; diff --git a/src/storage/el-meta.entity.ts b/src/storage/el-meta.entity.ts index c5267bb1..693e1c5c 100644 --- a/src/storage/el-meta.entity.ts +++ b/src/storage/el-meta.entity.ts @@ -1,4 +1,5 @@ import { Entity, EntityRepositoryType, PrimaryKey, PrimaryKeyType, Property } from '@mikro-orm/core'; +import { BLOCK_HASH_LEN } from './constants'; import { ElMetaRepository } from './el-meta.repository'; @Entity({ customRepository: () => ElMetaRepository }) @@ -16,7 +17,7 @@ export class ElMetaEntity { blockNumber: number; @PrimaryKey() - @Property({ length: 66 }) + @Property({ length: BLOCK_HASH_LEN }) blockHash: string; @Property() diff --git a/src/validators/validators.module.ts b/src/validators/validators.module.ts index 8fe9cb7f..1ce33630 100644 --- a/src/validators/validators.module.ts +++ b/src/validators/validators.module.ts @@ -1,12 +1,11 @@ import { Global, Module } from '@nestjs/common'; import { ValidatorsService } from './validators.service'; -import { LoggerModule } from 'common/logger'; -import { JobModule } from 'common/job'; +import { LoggerModule } from '../common/logger'; import { ValidatorsRegistryModule } from '@lido-nestjs/validators-registry'; @Global() @Module({ - imports: [LoggerModule, JobModule, ValidatorsRegistryModule], + imports: [LoggerModule, ValidatorsRegistryModule], providers: [ValidatorsService], exports: [ValidatorsService], }) diff --git a/src/validators/validators.service.ts b/src/validators/validators.service.ts index de6f0aa7..d945d33a 100644 --- a/src/validators/validators.service.ts +++ b/src/validators/validators.service.ts @@ -5,8 +5,8 @@ import { Validator, ConsensusMeta, } from '@lido-nestjs/validators-registry'; -import { LOGGER_PROVIDER, LoggerService } from 'common/logger'; -import { ConfigService } from 'common/config'; +import { LOGGER_PROVIDER, LoggerService } from '../common/logger'; +import { ConfigService } from '../common/config'; import { QueryOrder } from '@mikro-orm/core'; export interface ValidatorsFilter { @@ -23,6 +23,7 @@ export class ValidatorsService { protected readonly validatorsRegistry: ValidatorsRegistryInterface, protected readonly configService: ConfigService, ) {} + public isDisabledRegistry() { return !this.configService.get('VALIDATOR_REGISTRY_ENABLE'); } @@ -70,7 +71,6 @@ export class ValidatorsService { return { validators: nextValidatorsToExit, meta }; } - // TODO: if provided percent is 0 what should we do ? // return default value in this case is unpredictable. so lets return [] if (filter.percent == 0) { return { validators: [], meta }; @@ -89,7 +89,6 @@ export class ValidatorsService { } private getPercentOfValidators(validators: Validator[], percent: number): Validator[] { - // TODO: Does this ceil method suit to our purposes? const amount = (validators.length * percent) / 100; // or const roundedAmount = amount < 1 ? 1 : Math.round(amount); const ceilAmount = Math.ceil(amount);