diff --git a/CHANGELOG.md b/CHANGELOG.md index 6846e0d..009612e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,13 +6,31 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [0.2.0] - 2023-16-22 +## [0.3.0] - 2024-01-02 + +### Added + +- e2e tests for async methods +- unit tests for async methods +- example scripts for async methods + +### Changed + +- Updated documentation +- Updated molecule test scenarios to support Prysm + +### Fixed + +- Missing class argument in asyncio methods that caused exceptions to be raised + + +## [0.2.0] - 2023-12-22 ### Changed - Updated documentation -## [0.1.0] - 2023-16-12 +## [0.1.0] - 2023-12-16 ### Added diff --git a/README.md b/README.md index b08476c..c57b05f 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,11 @@ Validator clients' native key management is CLI based which does not work well w The ETH 2 Key Manager API (also referred to as Validator API) enables users to use a single interface to manage keys for all clients. The keystore operations performed using the API do not require the client to be restarted. +## Asynchronous and synchronous methods + +The API client provides both asynchronous and synchronous methods. Both methods are implemented using the [httpx](https://www.python-httpx.org/) library, which uses AsyncIO under the hood. The asynchronous methods let you perform operation on multiple validators in non-blocking manner. This is significantly faster when managing large number of validators. Refer to [examples](https://eth-2-key-manager-api-client.slingnode.com/) for sample scripts. + + ## Resources The Full documentation is available at [https://eth-2-key-manager-api-client.slingnode.com/](https://eth-2-key-manager-api-client.slingnode.com/) @@ -66,7 +71,7 @@ eth_2_key_manager = eth_2_key_manager_api_client.Eth2KeyManager(base_url="http:/ For full list of examples refer to [examples](https://eth-2-key-manager-api-client.slingnode.com/). -## Import keystores +## Import keystores - synchronous ```python import eth_2_key_manager_api_client @@ -112,7 +117,7 @@ assert response.status_code == 200 ``` -## List keys +## List keys - synchronous ```python import eth_2_key_manager_api_client @@ -132,3 +137,100 @@ else: print(f"List keys failed with status code: {response.status_code}") ``` + +## Import remote keys - asynchronous + +NOTE: for legibility this example imports the same remote keys to multiple validators. We would never do that in a real world scenario. + +```python +import asyncio +import eth_2_key_manager_api_client + +validators = [ + ( + "LIGHTHOUSE", + "http://192.168.121.71:7500", + "api-token-0x024fa0a0c597e83a970e689866703a89a555c9ee602d08cf848ee7935509a62f33" + ), + ( + "TEKU", + "https://192.168.121.245:7500", + "5b874584cecab7eadfc3a224f177272f" + ), + ( + "NIMBUS", + "http://192.168.121.61:7500", + "fcb4869b587a71b6d7ff25c85ac312201e53" + ), + ( + "LODESTAR", + "http://192.168.121.118:7500", + "api-token-0xff6b738e030ee41e6bc317c204e2dae8965f6d666dc158e5690940936eeb35d9" + ) +] + +remote_keys = [ + { + "pubkey": "0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a", + "url": "https://remote.signer" + } +] + + +async def import_remote_keys_async(validator): + + eth_2_key_manager = eth_2_key_manager_api_client.Eth2KeyManager( + base_url=validator[1], + token=validator[2] + ) + + response = await eth_2_key_manager.import_remote_keys.asyncio_detailed(remote_keys=remote_keys) + + if response.status_code == 200: + print(f"{validator[0]} - {validator[1]} - Remote keys imported successfully") + else: + print(f"{validator[0]} - {validator[1]} - Remote keys import failed with status code: {response.status_code}") + + +async def main(): + await asyncio.gather(*(import_remote_keys_async(validator) for validator in validators)) + + +asyncio.run(main()) +``` + +## List keys - asynchronous + +```python +import asyncio +import eth_2_key_manager_api_client +from tests.conftest import parse_file + +validators = parse_file("../.env") + + +async def list_keys_async(validator): + + eth_2_key_manager = eth_2_key_manager_api_client.Eth2KeyManager( + base_url=validator[1], + token=validator[2] + ) + + response = await eth_2_key_manager.list_keys.asyncio_detailed() + + if response.status_code == 200: + print(f"{validator[0]} - {validator[1]} - Number of keys: {len(response.parsed.data)}") + if response.parsed.data: + print("List of keys:") + for key in response.parsed.data: + print(f" {key.validating_pubkey}") + else: + print(f"{validator[0]} - {validator[1]} - List keys failed with status code: {response.status_code}") + + +async def main(): + await asyncio.gather(*(list_keys_async(validator) for validator in validators)) + + +asyncio.run(main()) +``` diff --git a/docs/examples.md b/docs/examples.md index 2749ca3..4fafdda 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -5,15 +5,22 @@ The example scripts are located in the examples directory. # Local Key Manager -## Import keystores +## Import keystores - synchronous ```python --8<-- examples/import_keystores.py --8<-- ``` +## Import keystores - async -## List keys +```python +--8<-- +examples/import_keystores_async.py +--8<-- +``` + +## List keys - synchronous ```python --8<-- @@ -21,7 +28,15 @@ examples/list_keys.py --8<-- ``` -## Delete keys +## List keys - async + +```python +--8<-- +examples/list_keys_async.py +--8<-- +``` + +## Delete keys - synchronous ```python --8<-- @@ -29,6 +44,14 @@ examples/delete_keys.py --8<-- ``` +## Delete keys - async + +```python +--8<-- +examples/delete_keys_async.py +--8<-- +``` + # Gas limit ## Set gas limit @@ -83,7 +106,7 @@ examples/delete_fee_recipient.py # Remote Key Manager -## Import remote keys +## Import remote keys - synchronous ```python --8<-- @@ -91,7 +114,23 @@ examples/import_remote_keys.py --8<-- ``` -## List remote keys +## Import remote keys - async + +```python +--8<-- +examples/import_remote_keys_async.py +--8<-- +``` + +## List remote keys - synchronous + +```python +--8<-- +examples/list_remote_keys.py +--8<-- +``` + +## List remote keys - async ```python --8<-- @@ -99,10 +138,18 @@ examples/list_remote_keys.py --8<-- ``` -## Delete remote keys +## Delete remote keys - synchronous ```python --8<-- examples/delete_remote_keys.py --8<-- ``` + +## Delete remote keys - async + +```python +--8<-- +examples/delete_remote_keys_async.py +--8<-- +``` diff --git a/docs/index.md b/docs/index.md index cdef5c0..7175a24 100644 --- a/docs/index.md +++ b/docs/index.md @@ -6,6 +6,11 @@ Validator clients' native key management is CLI based which does not work well w The ETH 2 Key Manager API (also referred to as Validator API) enables users to use a single interface to manage keys for all clients. The keystore operations performed using the API do not require the client to be restarted. +## Asynchronous and synchronous methods + +The API client provides both asynchronous and synchronous methods. Both methods are implemented using the [httpx](https://www.python-httpx.org/) library, which uses AsyncIO under the hood. The asynchronous methods let you perform operation on multiple validators in non-blocking manner. This is significantly faster when managing large number of validators. Refer to [examples](https://eth-2-key-manager-api-client.slingnode.com/) for sample scripts. + + ## Resources The Full documentation is available at [https://eth-2-key-manager-api-client.slingnode.com/](https://eth-2-key-manager-api-client.slingnode.com/) @@ -64,11 +69,11 @@ eth_2_key_manager = eth_2_key_manager_api_client.Eth2KeyManager(base_url="http:/ # Examples + For full list of examples refer to [examples](examples.md) -This module provides synchronous and asynchronous methods for all API endpoints. The synchronous methods are blocking and will return the response object when the request is complete. The asynchronous methods are implemented using AsyncIO and will return a coroutine object that can be awaited. -## Import keystores +## Import keystores - synchronous ```python --8<-- @@ -76,10 +81,26 @@ examples/import_keystores.py --8<-- ``` -## List keys +## List keys - synchronous ```python --8<-- examples/list_keys.py --8<-- ``` + +## Import remote keys - asynchronous + +```python +--8<-- +examples/import_remote_keys_async.py +--8<-- +``` + +## List keys - asynchronous + +```python +--8<-- +examples/list_keys_async.py +--8<-- +``` diff --git a/docs/testing.md b/docs/testing.md index 1268aca..9ba910f 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -34,13 +34,49 @@ VALIDATOR_NAME BASE_URL API_TOKEN For example: ```bash -TEKU http://192.168.121.35:7500 410ef40da53b447f76ec52fa43092032 +TEKU https://192.168.121.35:7500 410ef40da53b447f76ec52fa43092032 LIGHTHOUSE http://192.168.121.189:7500 api-token-0x022b0c73c4757866df53b9b24a5b254222daca269e8be677c1efb15a93aba148a7 NIMBUS http://192.168.121.42:7500 06ab97c6170c1ae09bf3eb69a300d2795fb6 LODESTAR http://192.168.121.57:7500 api-token-0x9e75d2fb89d9a8230d027665ad0f67b777aaa2f1d23f282125001d0dea753901 PRYSM http://192.168.121.53:7500 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.orijiINlQIwSlB5ZxjmzAEqlYtI3SUNC6ennFnbcVCs ``` +Sample output: + +```bash +python -m pytest tests/e2e/test_e2e_api_fee_recipient_async.py -s +==================================================== test session starts ==================================================== +platform linux -- Python 3.10.12, pytest-7.4.3, pluggy-1.3.0 +rootdir: /home/kp/projects/slingnode/eth-2-key-manager-api-client +plugins: mock-3.12.0, cov-4.1.0, httpx-0.26.0, asyncio-0.21.1, anyio-4.1.0 +asyncio: mode=strict +collected 4 items + +tests/e2e/test_e2e_api_fee_recipient_async.py +Validator client -> LIGHTHOUSE +Fee Recipient set successfully +Fee recipient for pubkey 0x99c4c42fac7d1393956bd9e2785ed67cf5aaca4bf56d2fcda94c42d6042aebb1723ce6bac6f0216ff8c5d4f9f013008b is 0xabcf8e0d4e9587369b2301d0790347320302cc09 +Fee Recipient deleted successfully +. +Validator client -> TEKU +Fee Recipient set successfully +Fee recipient for pubkey 0x99c4c42fac7d1393956bd9e2785ed67cf5aaca4bf56d2fcda94c42d6042aebb1723ce6bac6f0216ff8c5d4f9f013008b is 0xAbcF8e0d4e9587369b2301D0790347320302cc09 +Fee Recipient deleted successfully +. +Validator client -> NIMBUS +Fee Recipient set successfully +Fee recipient for pubkey 0x99c4c42fac7d1393956bd9e2785ed67cf5aaca4bf56d2fcda94c42d6042aebb1723ce6bac6f0216ff8c5d4f9f013008b is 0xabcf8e0d4e9587369b2301d0790347320302cc09 +Fee Recipient deleted successfully +. +Validator client -> LODESTAR +Fee Recipient set successfully +Fee recipient for pubkey 0x99c4c42fac7d1393956bd9e2785ed67cf5aaca4bf56d2fcda94c42d6042aebb1723ce6bac6f0216ff8c5d4f9f013008b is 0xabcf8e0d4e9587369b2301d0790347320302cc09 +Fee Recipient deleted successfully +. + +==================================================== 4 passed in 13.87s ===================================================== +``` + # Testing using Ansible Molecule The project includes a Molecule test suite that can be used to test the API client against a real Ethereum Validator client. Molecule is a testing framework for Ansible roles. The test suite is located in the `molecule` directory. The test suite uses SlingNode Ethereum Ansible roles to set up the Ethereum Validator clients. The included test suite will automatically create the .env file and output the details required by the pytest e2e tests. diff --git a/eth_2_key_manager_api_client/api/fee_recipient.py b/eth_2_key_manager_api_client/api/fee_recipient.py index 03c56c9..807ca21 100644 --- a/eth_2_key_manager_api_client/api/fee_recipient.py +++ b/eth_2_key_manager_api_client/api/fee_recipient.py @@ -31,7 +31,7 @@ class DeleteFeeRecipient: The endpoint returns the following HTTP status code if successful: - 204: No Content - Typical usage example: + Typical usage example - synchronous: ```python import eth_2_key_manager_api_client @@ -44,6 +44,20 @@ class DeleteFeeRecipient: print(f"Fee Recipient deletion failed with status code: {response.status_code}") assert response.status_code == 204 ``` + + Typical usage example - asynchronous: + ```python + import eth_2_key_manager_api_client + + eth_2_key_manager = eth_2_key_manager_api_client.Eth2KeyManager() + pubkey = "0x99c4c42fac7d1393956bd9e2785ed67cf5aaca4bf56d2fcda94c42d6042aebb1723ce6bac6f0216ff8c5d4f9f013008b" + response = await eth_2_key_manager.delete_fee_recipient.asyncio_detailed(pubkey=pubkey) + if response.status_code == 204: + print("Fee Recipient deleted successfully") + else: + print(f"Fee Recipient deletion failed with status code: {response.status_code}") + assert response.status_code == 204 + ``` """ client: AuthenticatedClient @@ -167,7 +181,7 @@ class ListFeeRecipient: The endpoint returns the following HTTP status code if successful: - 200: OK - Typical usage example: + Typical usage example - synchronous: ```python import eth_2_key_manager_api_client @@ -180,6 +194,20 @@ class ListFeeRecipient: print(f"Fee Recipient listing failed with status code: {response.status_code}") assert response.status_code == 200 ``` + + Typical usage example - asynchronous: + ```python + import eth_2_key_manager_api_client + + eth_2_key_manager = eth_2_key_manager_api_client.Eth2KeyManager() + pubkey = "0x99c4c42fac7d1393956bd9e2785ed67cf5aaca4bf56d2fcda94c42d6042aebb1723ce6bac6f0216ff8c5d4f9f013008b" + response = await eth_2_key_manager.list_fee_recipient.asyncio_detailed(pubkey=pubkey) + if response.status_code == 200: + print(f"Fee recipient for pubkey {pubkey} is {response.parsed.data.ethaddress}") + else: + print(f"Fee Recipient listing failed with status code: {response.status_code}") + assert response.status_code == 200 + ``` """ client: AuthenticatedClient @@ -332,6 +360,20 @@ class SetFeeRecipient: print(f"Fee Recipient set failed with status code: {response.status_code}") assert response.status_code == 202 ``` + + Typical usage example - asynchronous: + ```python + import eth_2_key_manager_api_client + + eth_2_key_manager = eth_2_key_manager_api_client.Eth2KeyManager() + pubkey = "0x99c4c42fac7d1393956bd9e2785ed67cf5aaca4bf56d2fcda94c42d6042aebb1723ce6bac6f0216ff8c5d4f9f013008b" + response = await eth_2_key_manager.set_fee_recipient.asyncio_detailed(pubkey=pubkey, ethaddress="0xabcf8e0d4e9587369b2301d0790347320302cc09") + if response.status_code == 202: + print("Fee Recipient set successfully") + else: + print(f"Fee Recipient set failed with status code: {response.status_code}") + assert response.status_code == 202 + ``` """ client: AuthenticatedClient diff --git a/eth_2_key_manager_api_client/api/gas_limit.py b/eth_2_key_manager_api_client/api/gas_limit.py index 3b6a5af..bcd80a5 100644 --- a/eth_2_key_manager_api_client/api/gas_limit.py +++ b/eth_2_key_manager_api_client/api/gas_limit.py @@ -31,7 +31,7 @@ class DeleteGasLimit: The endpoint returns the following HTTP status code if successful: - 204: No Content - Typical usage example: + Typical usage example - synchronous: ```python import eth_2_key_manager_api_client @@ -44,6 +44,20 @@ class DeleteGasLimit: print(f"Gas limit deletion failed with status code: {response.status_code}") assert response.status_code == 204 ``` + + Typical usage example - asynchronous: + ```python + import eth_2_key_manager_api_client + + eth_2_key_manager = eth_2_key_manager_api_client.Eth2KeyManager() + pubkey = "0x99c4c42fac7d1393956bd9e2785ed67cf5aaca4bf56d2fcda94c42d6042aebb1723ce6bac6f0216ff8c5d4f9f013008b" + response = await eth_2_key_manager.delete_gas_limit.asyncio_detailed(pubkey=pubkey) + if response.status_code == 204: + print("Gas limit deleted successfully") + else: + print(f"Gas limit deletion failed with status code: {response.status_code}") + assert response.status_code == 204 + ``` """ client: AuthenticatedClient @@ -187,6 +201,20 @@ class GetGasLimit: print(f"Gas limit listing failed with status code: {response.status_code}") assert response.status_code == 200 ``` + + Typical usage example - asynchronous: + ```python + import eth_2_key_manager_api_client + + eth_2_key_manager = eth_2_key_manager_api_client.Eth2KeyManager() + pubkey = "0x99c4c42fac7d1393956bd9e2785ed67cf5aaca4bf56d2fcda94c42d6042aebb1723ce6bac6f0216ff8c5d4f9f013008b" + response = await eth_2_key_manager.get_gas_limit.asyncio_detailed(pubkey=pubkey) + if response.status_code == 200: + print(f"Gas limit for pubkey {pubkey} is {response.parsed.data.gas_limit}") + else: + print(f"Gas limit listing failed with status code: {response.status_code}") + assert response.status_code == 200 + ``` """ client: AuthenticatedClient @@ -334,7 +362,7 @@ class SetGasLimit: The endpoint returns the following HTTP status code if successful: - 202: Accepted - Typical usage example: + Typical usage example - synchronous: ```python import eth_2_key_manager_api_client @@ -347,6 +375,20 @@ class SetGasLimit: print(f"Gas limit set failed with status code: {response.status_code}") assert response.status_code == 202 ``` + + Typical usage example - asynchronous: + ```python + import eth_2_key_manager_api_client + + eth_2_key_manager = eth_2_key_manager_api_client.Eth2KeyManager() + pubkey = "0x99c4c42fac7d1393956bd9e2785ed67cf5aaca4bf56d2fcda94c42d6042aebb1723ce6bac6f0216ff8c5d4f9f013008b" + response = await eth_2_key_manager.set_gas_limit.asyncio_detailed(pubkey=pubkey, gas_limit="999999") + if response.status_code == 202: + print("Gas limit set successfully") + else: + print(f"Gas limit set failed with status code: {response.status_code}") + assert response.status_code == 202 + ``` """ client: AuthenticatedClient diff --git a/eth_2_key_manager_api_client/api/local_key_manager.py b/eth_2_key_manager_api_client/api/local_key_manager.py index 12a5f3b..1ac3d19 100644 --- a/eth_2_key_manager_api_client/api/local_key_manager.py +++ b/eth_2_key_manager_api_client/api/local_key_manager.py @@ -281,13 +281,27 @@ class ListKeys: The endpoint returns the following HTTP status code if successful: - 200: OK - Typical usage example: + Typical usage example - synchronous: ```python import eth_2_key_manager_api_client eth_2_key_manager = eth_2_key_manager_api_client.Eth2KeyManager() response = eth_2_key_manager.list_keys.sync_detailed() + if response.status_code == 200: + print(f"List of keys: {response.parsed.data}") + else: + print(f"List keys failed with status code: {response.status_code}") + assert response.status_code == 200 + ``` + + Typical usage example - asynchronous: + ```python + import eth_2_key_manager_api_client + + eth_2_key_manager = eth_2_key_manager_api_client.Eth2KeyManager() + response = await eth_2_key_manager.list_keys.asyncio_detailed() + if response.status_code == 200: print(f"List of keys: {response.parsed.data}") else: @@ -387,7 +401,7 @@ class DeleteKeys: The endpoint returns the following HTTP status code if successful: - 204: No Content - Typical usage example: + Typical usage example - synchronous: ```python import eth_2_key_manager_api_client @@ -401,6 +415,21 @@ class DeleteKeys: print(f"Keystores delete failed with status code: {response.status_code}") assert response.status_code == 200 ``` + + Typical usage example - synchronous: + ```python + import eth_2_key_manager_api_client + + eth_2_key_manager = eth_2_key_manager_api_client.Eth2KeyManager() + pubkey = "0x99c4c42fac7d1393956bd9e2785ed67cf5aaca4bf56d2fcda94c42d6042aebb1723ce6bac6f0216ff8c5d4f9f013008b" + + response = await eth_2_key_manager.delete_keys.asyncio_detailed(pubkeys=[pubkey]) + if response.status_code == 200: + print("Keystores deleted successfully") + else: + print(f"Keystores delete failed with status code: {response.status_code}") + assert response.status_code == 200 + ``` """ client: AuthenticatedClient diff --git a/eth_2_key_manager_api_client/api/remote_key_manager.py b/eth_2_key_manager_api_client/api/remote_key_manager.py index bbabbb9..c624c29 100644 --- a/eth_2_key_manager_api_client/api/remote_key_manager.py +++ b/eth_2_key_manager_api_client/api/remote_key_manager.py @@ -37,7 +37,7 @@ class DeleteRemoteKeys: The endpoint returns the following HTTP status code if successful: - 200: OK - Typical usage example: + Typical usage example - synchronous: ```python import eth_2_key_manager_api_client @@ -45,6 +45,20 @@ class DeleteRemoteKeys: pubkey = "0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a" response = eth_2_key_manager.delete_remote_keys.sync_detailed(pubkeys=[pubkey]) + if response.status_code == 200: + print("Remote keys deleted successfully") + else: + print(f"Remote keys delete failed with status code: {response.status_code}") + ``` + + Typical usage example - asynchronous: + ```python + import eth_2_key_manager_api_client + + eth_2_key_manager = eth_2_key_manager_api_client.Eth2KeyManager() + pubkey = "0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a" + response = await eth_2_key_manager.delete_remote_keys.asyncio_detailed(pubkeys=[pubkey]) + if response.status_code == 200: print("Remote keys deleted successfully") else: @@ -182,7 +196,7 @@ class ImportRemoteKeys: The endpoint returns the following HTTP status code if successful: - 200: OK - Typical usage example: + Typical usage example - synchronous: ```python import eth_2_key_manager_api_client @@ -196,6 +210,28 @@ class ImportRemoteKeys: eth_2_key_manager = eth_2_key_manager_api_client.Eth2KeyManager() response = eth_2_key_manager.import_remote_keys.sync_detailed(remote_keys=remote_keys) + if response.status_code == 200: + print("Remote keys imported successfully") + else: + print(f"Remote keys import failed with status code: {response.status_code}") + + assert response.status_code == 200 + ``` + + Typical usage example - asynchronous: + ```python + import eth_2_key_manager_api_client + + remote_keys = [ + { + "pubkey": "0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a", + "url": "https://remote.signer", + } + ] + + eth_2_key_manager = eth_2_key_manager_api_client.Eth2KeyManager() + response = await eth_2_key_manager.import_remote_keys.asyncio_detailed(remote_keys=remote_keys) + if response.status_code == 200: print("Remote keys imported successfully") else: @@ -358,13 +394,28 @@ class ListRemoteKeys: The endpoint returns the following HTTP status code if successful: - 200: OK - Typical usage example: + Typical usage example - synchronous: ```python import eth_2_key_manager_api_client eth_2_key_manager = eth_2_key_manager_api_client.Eth2KeyManager() response = eth_2_key_manager.list_remote_keys.sync_detailed() + if response.status_code == 200: + print(f"List of remote keys: {response.parsed.data}") + else: + print(f"List remote keys failed with status code: {response.status_code}") + + assert response.status_code == 200 + ``` + + Typical usage example - asynchronous: + ```python + import eth_2_key_manager_api_client + + eth_2_key_manager = eth_2_key_manager_api_client.Eth2KeyManager() + response = await eth_2_key_manager.list_remote_keys.asyncio_detailed() + if response.status_code == 200: print(f"List of remote keys: {response.parsed.data}") else: diff --git a/eth_2_key_manager_api_client/eth_2_keymanager.py b/eth_2_key_manager_api_client/eth_2_keymanager.py index 809327b..5dadfdb 100644 --- a/eth_2_key_manager_api_client/eth_2_keymanager.py +++ b/eth_2_key_manager_api_client/eth_2_keymanager.py @@ -33,14 +33,14 @@ class Eth2KeyManager: * Listing remote keys Args: - base_url (str | None): The base URL of the Eth2 Key Manager API. - token (str | None): The API token to authenticate with the Eth2 Key Manager API. - cookies (Dict[str, str]): Optional cookies to send with requests. - headers (Dict[str, str]): Optional headers to send with requests. - timeout (float): The timeout for requests. - verify_ssl (Union[str, bool, ssl.SSLContext]): Whether to verify SSL certificates. - raise_on_unexpected_status (bool): Whether to raise an exception if a request returns an unexpected status code. - follow_redirects (bool): Whether to follow redirects. + base_url: The base URL of the Eth2 Key Manager API. + token: The API token to authenticate with the Eth2 Key Manager API. + cookies: Optional cookies to send with requests. + headers: Optional headers to send with requests. + timeout: The timeout for requests. + verify_ssl: Whether to verify SSL certificates. + raise_on_unexpected_status: Whether to raise an exception if a request returns an unexpected status code. + follow_redirects: Whether to follow redirects. Raises: ConfigurationMissing: If the base_url or token is not provided. diff --git a/examples/delete_keys_async.py b/examples/delete_keys_async.py new file mode 100644 index 0000000..62b9634 --- /dev/null +++ b/examples/delete_keys_async.py @@ -0,0 +1,31 @@ +import asyncio + +import eth_2_key_manager_api_client +from tests.conftest import parse_file + +validators = parse_file("../.env") + +pubkeys = [ + "0x874bed7931ba14832198a4070b881f89e7ddf81898dd800446ef382344e9726a5e6265acb21f5c8ee2759c313ec6ca0d", + "0x88a471158d618a8f9997dcb2cc1921411392d82d00e339ccf912fd9335bd42f97c9de046280d9d5f681a8e73a7d3baad", + "0x99c4c42fac7d1393956bd9e2785ed67cf5aaca4bf56d2fcda94c42d6042aebb1723ce6bac6f0216ff8c5d4f9f013008b", +] + + +async def delete_keys_async(validator): + + eth_2_key_manager = eth_2_key_manager_api_client.Eth2KeyManager(base_url=validator[1], token=validator[2]) + + response = await eth_2_key_manager.delete_keys.asyncio_detailed(pubkeys=pubkeys) + + if response.status_code == 200: + print(f"{validator[0]} - {validator[1]} - Keystore deleted successfully") + else: + print(f"{validator[0]} - {validator[1]} - Keystore delete failed with status code: {response.status_code}") + + +async def main(): + await asyncio.gather(*(delete_keys_async(validator) for validator in validators)) + + +asyncio.run(main()) diff --git a/examples/delete_remote_keys_async.py b/examples/delete_remote_keys_async.py new file mode 100644 index 0000000..fd28e05 --- /dev/null +++ b/examples/delete_remote_keys_async.py @@ -0,0 +1,27 @@ +import asyncio + +import eth_2_key_manager_api_client +from tests.conftest import parse_file + +validators = parse_file("../.env") + +pubkey = "0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a" + + +async def delete_remote_keys_async(validator): + + eth_2_key_manager = eth_2_key_manager_api_client.Eth2KeyManager(base_url=validator[1], token=validator[2]) + + response = await eth_2_key_manager.delete_remote_keys.asyncio_detailed(pubkeys=[pubkey]) + + if response.status_code == 200: + print(f"{validator[0]} - {validator[1]} - Remote keys deleted successfully") + else: + print(f"{validator[0]} - {validator[1]} - Remote keys delete failed with status code: {response.status_code}") + + +async def main(): + await asyncio.gather(*(delete_remote_keys_async(validator) for validator in validators)) + + +asyncio.run(main()) diff --git a/examples/import_keystores_async.py b/examples/import_keystores_async.py new file mode 100644 index 0000000..80ce590 --- /dev/null +++ b/examples/import_keystores_async.py @@ -0,0 +1,54 @@ +import asyncio + +import eth_2_key_manager_api_client +from tests.conftest import parse_file + +validators = parse_file("../.env") + +# Create lists of keystore and password strings, note: the order of the lists must match `passwords[i]` must unlock `keystores[i]` +list_of_keystore_strs: list[str] = [] +list_of_keystore_password_strs: list[str] = [] + +keystore_files = [ + "mock_validator_keystores/keystore-m_12381_3600_0_0_0-1669980799.json", + "mock_validator_keystores/keystore-m_12381_3600_1_0_0-1680087924.json", + "mock_validator_keystores/keystore-m_12381_3600_2_0_0-1680087924.json", +] + +# Read the keystores as strings +for keystore_file in keystore_files: + with open(keystore_file, "r") as f: + list_of_keystore_strs.append(f.read()) + +# Read the passwords as strings +keystore_password_files = [ + "mock_validator_keystores/keystore-m_12381_3600_0_0_0-1669980799.txt", + "mock_validator_keystores/keystore-m_12381_3600_1_0_0-1680087924.txt", + "mock_validator_keystores/keystore-m_12381_3600_2_0_0-1680087924.txt", +] + +for keystore_password_file in keystore_password_files: + with open(keystore_password_file, "r") as f: + list_of_keystore_password_strs.append(f.read()) + +with open("mock_validator_keystores/slashing_protection_db.json", "r") as f: + slashing_protection_str = f.read() + + +async def import_keystores_async(validator): + + eth_2_key_manager = eth_2_key_manager_api_client.Eth2KeyManager(base_url=validator[1], token=validator[2]) + + response = await eth_2_key_manager.import_keystores.asyncio_detailed(list_of_keystore_strs, list_of_keystore_password_strs, slashing_protection_str) + + if response.status_code == 200: + print(f"{validator[0]} - {validator[1]} - Keystores imported successfully") + else: + print(f"{validator[0]} - {validator[1]} - Keystores import failed with status code: {response.status_code}") + + +async def main(): + await asyncio.gather(*(import_keystores_async(validator) for validator in validators)) + + +asyncio.run(main()) diff --git a/examples/import_remote_keys_async.py b/examples/import_remote_keys_async.py new file mode 100644 index 0000000..e8a446c --- /dev/null +++ b/examples/import_remote_keys_async.py @@ -0,0 +1,50 @@ +import asyncio + +import eth_2_key_manager_api_client +from tests.conftest import parse_file + +validators = parse_file("../.env") + +# validators = [ +# ( +# "LIGHTHOUSE", +# "http://192.168.121.71:7500", +# "api-token-0x024fa0a0c597e83a970e689866703a89a555c9ee602d08cf848ee7935509a62f33" +# ), +# ( +# "TEKU", +# "https://192.168.121.245:7500", +# "5b874584cecab7eadfc3a224f177272f" +# ), +# ( +# "NIMBUS", +# "http://192.168.121.61:7500", +# "fcb4869b587a71b6d7ff25c85ac312201e53" +# ), +# ( +# "LODESTAR", +# "http://192.168.121.118:7500", +# "api-token-0xff6b738e030ee41e6bc317c204e2dae8965f6d666dc158e5690940936eeb35d9" +# ) +# ] + +remote_keys = [{"pubkey": "0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a", "url": "https://remote.signer"}] + + +async def import_remote_keys_async(validator): + + eth_2_key_manager = eth_2_key_manager_api_client.Eth2KeyManager(base_url=validator[1], token=validator[2]) + + response = await eth_2_key_manager.import_remote_keys.asyncio_detailed(remote_keys=remote_keys) + + if response.status_code == 200: + print(f"{validator[0]} - {validator[1]} - Remote keys imported successfully") + else: + print(f"{validator[0]} - {validator[1]} - Remote keys import failed with status code: {response.status_code}") + + +async def main(): + await asyncio.gather(*(import_remote_keys_async(validator) for validator in validators)) + + +asyncio.run(main()) diff --git a/examples/list_fee_recipient_async.py b/examples/list_fee_recipient_async.py new file mode 100644 index 0000000..af972c9 --- /dev/null +++ b/examples/list_fee_recipient_async.py @@ -0,0 +1,28 @@ +import asyncio + +import eth_2_key_manager_api_client +from tests.conftest import parse_file + +validators = parse_file("../.env") + +pubkey = "0x99c4c42fac7d1393956bd9e2785ed67cf5aaca4bf56d2fcda94c42d6042aebb1723ce6bac6f0216ff8c5d4f9f013008b" + + +async def list_fee_recipient_async(validator): + + eth_2_key_manager = eth_2_key_manager_api_client.Eth2KeyManager(base_url=validator[1], token=validator[2]) + + response = await eth_2_key_manager.list_fee_recipient.asyncio_detailed(pubkey=pubkey) + + if response.status_code == 200: + assert isinstance(response.parsed, eth_2_key_manager_api_client.models.list_fee_recipient_response.ListFeeRecipientResponse) + print(f"{validator[0]} - {validator[1]} - Fee recipient for pubkey {pubkey} is {response.parsed.data.ethaddress}") + else: + print(f"{validator[0]} - {validator[1]} - Fee Recipient listing failed with status code: {response.status_code}") + + +async def main(): + await asyncio.gather(*(list_fee_recipient_async(validator) for validator in validators)) + + +asyncio.run(main()) diff --git a/examples/list_keys_async.py b/examples/list_keys_async.py new file mode 100644 index 0000000..233058b --- /dev/null +++ b/examples/list_keys_async.py @@ -0,0 +1,29 @@ +import asyncio + +import eth_2_key_manager_api_client +from tests.conftest import parse_file + +validators = parse_file("../.env") + + +async def list_keys_async(validator): + + eth_2_key_manager = eth_2_key_manager_api_client.Eth2KeyManager(base_url=validator[1], token=validator[2]) + + response = await eth_2_key_manager.list_keys.asyncio_detailed() + + if response.status_code == 200: + print(f"{validator[0]} - {validator[1]} - Number of keys: {len(response.parsed.data)}") + if response.parsed.data: + print("List of keys:") + for key in response.parsed.data: + print(f" {key.validating_pubkey}") + else: + print(f"{validator[0]} - {validator[1]} - List keys failed with status code: {response.status_code}") + + +async def main(): + await asyncio.gather(*(list_keys_async(validator) for validator in validators)) + + +asyncio.run(main()) diff --git a/examples/list_remote_keys_async.py b/examples/list_remote_keys_async.py new file mode 100644 index 0000000..74273e2 --- /dev/null +++ b/examples/list_remote_keys_async.py @@ -0,0 +1,34 @@ +import asyncio + +import eth_2_key_manager_api_client +from tests.conftest import parse_file + +validators = parse_file("../.env") + + +async def list_remote_keys_async(validator): + + eth_2_key_manager = eth_2_key_manager_api_client.Eth2KeyManager(base_url=validator[1], token=validator[2]) + + response = await eth_2_key_manager.list_remote_keys.asyncio_detailed() + + if response.status_code == 200: + print(f"\n\n{validator[0]} - {validator[1]}") + # Assert that the response is of the expected type to satisfy static type checking. + # Ref: https://mypy.readthedocs.io/en/stable/error_code_list.html#check-that-attribute-exists-in-each-union-item-union-attr + assert isinstance(response.parsed, eth_2_key_manager_api_client.models.list_remote_keys_response.ListRemoteKeysResponse) + + print(f"Number of remote keys: {len(response.parsed.data)}") + if response.parsed.data: + print("List of keys:") + for key in response.parsed.data: + print(f" {key.pubkey}") + else: + print(f"{validator[0]} - {validator[1]} - Remote keys listing failed with status code: {response.status_code}") + + +async def main(): + await asyncio.gather(*(list_remote_keys_async(validator) for validator in validators)) + + +asyncio.run(main()) diff --git a/molecule/all_validator_clients/molecule.yml b/molecule/all_validator_clients/molecule.yml index 7a0897e..fb5e3d6 100644 --- a/molecule/all_validator_clients/molecule.yml +++ b/molecule/all_validator_clients/molecule.yml @@ -34,19 +34,19 @@ platforms: cpu_mode: 'host-passthrough' video_vram: '16384' - # - name: prysm - # hostname: prysm - # config_options: - # ssh.keep_alive: true - # ssh.remote_user: 'vagrant' - # synced_folder: false - # memory: 5120 - # cpus: 4 - # provider_options: - # video_type: 'vga' - # driver: 'kvm' - # cpu_mode: 'host-passthrough' - # video_vram: '16384' + - name: prysm + hostname: prysm + config_options: + ssh.keep_alive: true + ssh.remote_user: 'vagrant' + synced_folder: false + memory: 5120 + cpus: 4 + provider_options: + video_type: 'vga' + driver: 'kvm' + cpu_mode: 'host-passthrough' + video_vram: '16384' - name: nimbus hostname: nimbus @@ -96,7 +96,7 @@ provisioner: env: ANSIBLE_PIPELINING: "True" playbooks: - converge: ../resources/playbooks/converge_multiple.yml + converge: ../resources/playbooks/converge.yml prepare: ../resources/playbooks/prepare.yml inventory: group_vars: diff --git a/molecule/resources/playbooks/converge.yml b/molecule/resources/playbooks/converge.yml index d354c3a..86538c4 100644 --- a/molecule/resources/playbooks/converge.yml +++ b/molecule/resources/playbooks/converge.yml @@ -19,6 +19,31 @@ ansible.builtin.include_role: name: slingnode.ethereum + + - name: Copy prysm_wallet_pass.txt to initilize a wallet + ansible.builtin.copy: + src: prysm_wallet_pass.txt + dest: /opt/blockchain/validator/prysm/prysm_wallet_pass.txt + when: clients.validator == "prysm" + + + - name: Create wallet + ansible.builtin.command: docker exec validator /app/cmd/validator/validator wallet create --wallet-dir=/var/lib/prysm --wallet-password-file=/var/lib/prysm/prysm_wallet_pass.txt --keymanager-kind=imported --accept-terms-of-use --goerli + when: clients.validator == "prysm" + ignore_errors: true + + + - name: Set correct permissions on prysm wallet + ansible.builtin.file: + path: /opt/blockchain/validator/prysm/direct/accounts/all-accounts.keystore.json + mode: 0600 + when: clients.validator == "prysm" + + - name: Restart prysm validator + ansible.builtin.command: docker restart validator + when: clients.validator == "prysm" + + - name: Get Lighthouse validator api token ansible.builtin.command: head -n 1 /opt/blockchain/validator/lighthouse/validators/api-token.txt register: validator_api_token_lighthouse @@ -35,20 +60,20 @@ when: clients.validator == "lighthouse" - # - name: Get Prysm validator api token - # ansible.builtin.command: sed -n '2p' /opt/blockchain/validator/prysm/auth-token - # register: validator_api_token_prysm - # when: clients.validator == "prysm" - # retries: 30 - # delay: 5 - # until: validator_api_token_prysm.rc == 0 + - name: Get Prysm validator api token + ansible.builtin.command: sed -n '2p' /opt/blockchain/validator/prysm/auth-token + register: validator_api_token_prysm + when: clients.validator == "prysm" + retries: 30 + delay: 5 + until: validator_api_token_prysm.rc == 0 - # - name: export the IP address of the Prysm instance - # local_action: - # shell printf "\nPRYSM http://{{ ansible_default_ipv4.address }}:7500 {{ validator_api_token_prysm.stdout }}" >> {{ dotenv_file_path }} - # become: false - # when: clients.validator == "prysm" + - name: export the IP address of the Prysm instance + local_action: + shell printf "\nPRYSM http://{{ ansible_default_ipv4.address }}:7500 {{ validator_api_token_prysm.stdout }}" >> {{ dotenv_file_path }} + become: false + when: clients.validator == "prysm" - name: Get Teku validator api token diff --git a/examples/mock_validator_keystores/prysm_wallet_pass.txt b/molecule/resources/playbooks/files/prysm_wallet_pass.txt similarity index 100% rename from examples/mock_validator_keystores/prysm_wallet_pass.txt rename to molecule/resources/playbooks/files/prysm_wallet_pass.txt diff --git a/pyproject.toml b/pyproject.toml index 48925a9..d302b7a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "eth-2-key-manager-api-client" -version = "0.2.0" +version = "0.3.0" description = "A client library for accessing ETH2 key manager API" authors = ["Karol Piwowarski "] diff --git a/tests/conftest.py b/tests/conftest.py index f335822..4a1c874 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -20,11 +20,17 @@ def parse_file(filename=".env") -> List[Tuple[str, str, str]]: return result +@pytest.fixture +def get_client_configs(): + return parse_file() + + @pytest.fixture def set_valid_client_config_env_vars(request): print(f"\nValidator client -> {request.param[0]}") os.environ["ETH_2_KEY_MANAGER_API_BASE_URL"] = request.param[1] os.environ["ETH_2_KEY_MANAGER_API_TOKEN"] = request.param[2] + return request.param[0] @pytest.fixture diff --git a/tests/e2e/test_e2e_api_fee_recipient_async.py b/tests/e2e/test_e2e_api_fee_recipient_async.py new file mode 100644 index 0000000..f0e6080 --- /dev/null +++ b/tests/e2e/test_e2e_api_fee_recipient_async.py @@ -0,0 +1,41 @@ +import pytest + +from ..conftest import parse_file + + +@pytest.mark.asyncio +@pytest.mark.parametrize("set_valid_client_config_env_vars", parse_file(), indirect=True) +async def test_e2e_fee_recipient_docstrings_code(setup_and_teardown_test, set_valid_client_config_env_vars): + + import eth_2_key_manager_api_client + + eth_2_key_manager = eth_2_key_manager_api_client.Eth2KeyManager() + pubkey = "0x99c4c42fac7d1393956bd9e2785ed67cf5aaca4bf56d2fcda94c42d6042aebb1723ce6bac6f0216ff8c5d4f9f013008b" + response = await eth_2_key_manager.set_fee_recipient.asyncio_detailed(pubkey=pubkey, ethaddress="0xabcf8e0d4e9587369b2301d0790347320302cc09") + if response.status_code == 202: + print("Fee Recipient set successfully") + else: + print(f"Fee Recipient set failed with status code: {response.status_code}") + assert response.status_code == 202 + + import eth_2_key_manager_api_client + + eth_2_key_manager = eth_2_key_manager_api_client.Eth2KeyManager() + pubkey = "0x99c4c42fac7d1393956bd9e2785ed67cf5aaca4bf56d2fcda94c42d6042aebb1723ce6bac6f0216ff8c5d4f9f013008b" + response = await eth_2_key_manager.list_fee_recipient.asyncio_detailed(pubkey=pubkey) + if response.status_code == 200: + print(f"Fee recipient for pubkey {pubkey} is {response.parsed.data.ethaddress}") + else: + print(f"Fee Recipient listing failed with status code: {response.status_code}") + assert response.status_code == 200 + + import eth_2_key_manager_api_client + + eth_2_key_manager = eth_2_key_manager_api_client.Eth2KeyManager() + pubkey = "0x99c4c42fac7d1393956bd9e2785ed67cf5aaca4bf56d2fcda94c42d6042aebb1723ce6bac6f0216ff8c5d4f9f013008b" + response = await eth_2_key_manager.delete_fee_recipient.asyncio_detailed(pubkey=pubkey) + if response.status_code == 204: + print("Fee Recipient deleted successfully") + else: + print(f"Fee Recipient deletion failed with status code: {response.status_code}") + assert response.status_code == 204 diff --git a/tests/e2e/test_e2e_api_gas_limit_async.py b/tests/e2e/test_e2e_api_gas_limit_async.py new file mode 100644 index 0000000..bafd6f8 --- /dev/null +++ b/tests/e2e/test_e2e_api_gas_limit_async.py @@ -0,0 +1,41 @@ +import pytest + +from ..conftest import parse_file + + +@pytest.mark.asyncio +@pytest.mark.parametrize("set_valid_client_config_env_vars", parse_file(), indirect=True) +async def test_e2e_gas_limit_docstrings_code(setup_and_teardown_test, set_valid_client_config_env_vars): + + import eth_2_key_manager_api_client + + eth_2_key_manager = eth_2_key_manager_api_client.Eth2KeyManager() + pubkey = "0x99c4c42fac7d1393956bd9e2785ed67cf5aaca4bf56d2fcda94c42d6042aebb1723ce6bac6f0216ff8c5d4f9f013008b" + response = await eth_2_key_manager.set_gas_limit.asyncio_detailed(pubkey=pubkey, gas_limit="999999") + if response.status_code == 202: + print("Gas limit set successfully") + else: + print(f"Gas limit set failed with status code: {response.status_code}") + assert response.status_code == 202 + + import eth_2_key_manager_api_client + + eth_2_key_manager = eth_2_key_manager_api_client.Eth2KeyManager() + pubkey = "0x99c4c42fac7d1393956bd9e2785ed67cf5aaca4bf56d2fcda94c42d6042aebb1723ce6bac6f0216ff8c5d4f9f013008b" + response = await eth_2_key_manager.get_gas_limit.asyncio_detailed(pubkey=pubkey) + if response.status_code == 200: + print(f"Gas limit for pubkey {pubkey} is {response.parsed.data.gas_limit}") + else: + print(f"Gas limit listing failed with status code: {response.status_code}") + assert response.status_code == 200 + + import eth_2_key_manager_api_client + + eth_2_key_manager = eth_2_key_manager_api_client.Eth2KeyManager() + pubkey = "0x99c4c42fac7d1393956bd9e2785ed67cf5aaca4bf56d2fcda94c42d6042aebb1723ce6bac6f0216ff8c5d4f9f013008b" + response = await eth_2_key_manager.delete_gas_limit.asyncio_detailed(pubkey=pubkey) + if response.status_code == 204: + print("Gas limit deleted successfully") + else: + print(f"Gas limit deletion failed with status code: {response.status_code}") + assert response.status_code == 204 diff --git a/tests/e2e/test_e2e_api_local_key_manager_async.py b/tests/e2e/test_e2e_api_local_key_manager_async.py new file mode 100644 index 0000000..d3a687f --- /dev/null +++ b/tests/e2e/test_e2e_api_local_key_manager_async.py @@ -0,0 +1,110 @@ +import pytest + +from ..conftest import parse_file + + +@pytest.mark.asyncio +@pytest.mark.parametrize("set_valid_client_config_env_vars", parse_file(), indirect=True) +async def test_e2e_api_local_key_manager_docstrings_code(set_valid_client_config_env_vars): + + import eth_2_key_manager_api_client + + keystore_str = """{ + "crypto": { + "kdf": { + "function": "scrypt", + "params": { + "dklen": 32, + "n": 262144, + "r": 8, + "p": 1, + "salt": "1c4a91c48175d4742b88c0c3cca7321ba8e3127906678a0f0195321234b2f61d" + }, + "message": "" + }, + "checksum": { + "function": "sha256", + "params": {}, + "message": "8f260d986ba4cf13dd75998d8b0f10bef6a5e60fdcec3fc0d606b082077c1b24" + }, + "cipher": { + "function": "aes-128-ctr", + "params": { + "iv": "77495a6ef36049d4edb83dd02bbe419b" + }, + "message": "d28fd393f4ee8b2516e4f8287245a2ccf1e42816b684becfca6c56c3b56d6ae5" + } + }, + "description": "", + "pubkey": "99c4c42fac7d1393956bd9e2785ed67cf5aaca4bf56d2fcda94c42d6042aebb1723ce6bac6f0216ff8c5d4f9f013008b", + "path": "m/12381/3600/2/0/0", + "uuid": "6d4913af-4cc2-457f-a962-39eca6d0dd37", + "version": 4 + }""" + + keystore_password_str = "validatorkey123" + + slashing_protection_str = """{ + "metadata": { + "interchange_format_version": "5", + "genesis_validators_root": "0x043db0d9a83813551ee2f33450d23797757d430911a9320530ad8a0eabc43efb" + }, + "data": [ + { + "pubkey": "0x876a9a7fadb5b9d2114a5180f9fe50b451cbab5f241b42e476b724a3575e5a8277767bc5a7c831c63f066a9a725c53d6", + "signed_blocks": [ + { + "slot": "4866645", + "signing_root": "0xc24c384a4b9ecef533b6d838691d83ac3e4b06c2903fb09200957583ea291c3d" + } + ], + "signed_attestations": [ + { + "source_epoch": "154315", + "target_epoch": "154316", + "signing_root": "0x04ec24fb31df03f65b7af9a74a62bf3c8e44817ae1f976b1d9c81af277f3ddfa" + }, + { + "source_epoch": "154316", + "target_epoch": "154317", + "signing_root": "0x9d731a700e06f0999b6964d65b6858022690387a47d25919f6e01daa6173dfc9" + } + ] + } + ] + }""" + + eth_2_key_manager = eth_2_key_manager_api_client.Eth2KeyManager() + response = await eth_2_key_manager.import_keystores.asyncio_detailed([keystore_str], [keystore_password_str], slashing_protection_str) + + if response.status_code == 200: + print("Keystores imported successfully") + else: + print(f"Keystores import failed with status code: {response.status_code}") + assert response.status_code == 200 + + # list keys + import eth_2_key_manager_api_client + + eth_2_key_manager = eth_2_key_manager_api_client.Eth2KeyManager() + response = await eth_2_key_manager.list_keys.asyncio_detailed() + + if response.status_code == 200: + print(f"List of keys: {response.parsed.data}") + else: + print(f"List keys failed with status code: {response.status_code}") + assert response.status_code == 200 + + # delete keys + + import eth_2_key_manager_api_client + + eth_2_key_manager = eth_2_key_manager_api_client.Eth2KeyManager() + pubkey = "0x99c4c42fac7d1393956bd9e2785ed67cf5aaca4bf56d2fcda94c42d6042aebb1723ce6bac6f0216ff8c5d4f9f013008b" + + response = await eth_2_key_manager.delete_keys.asyncio_detailed(pubkeys=[pubkey]) + if response.status_code == 200: + print("Keystores deleted successfully") + else: + print(f"Keystores delete failed with status code: {response.status_code}") + assert response.status_code == 200 diff --git a/tests/e2e/test_e2e_api_remote_keys.py b/tests/e2e/test_e2e_api_remote_keys.py index 9642d2c..2ee85ac 100644 --- a/tests/e2e/test_e2e_api_remote_keys.py +++ b/tests/e2e/test_e2e_api_remote_keys.py @@ -6,6 +6,9 @@ @pytest.mark.parametrize("set_valid_client_config_env_vars", parse_file(), indirect=True) def test_e2e_api_remote_keys_docstrings_code(set_valid_client_config_env_vars): + if set_valid_client_config_env_vars == "PRYSM": + pytest.skip("Skipping this test for Prysm - remote keys require different wallet type.") + import eth_2_key_manager_api_client remote_keys = [ diff --git a/tests/e2e/test_e2e_api_remote_keys_async.py b/tests/e2e/test_e2e_api_remote_keys_async.py new file mode 100644 index 0000000..0290d3b --- /dev/null +++ b/tests/e2e/test_e2e_api_remote_keys_async.py @@ -0,0 +1,55 @@ +import pytest + +from ..conftest import parse_file + + +@pytest.mark.asyncio +@pytest.mark.parametrize("set_valid_client_config_env_vars", parse_file(), indirect=True) +async def test_e2e_api_remote_keys_docstrings_code(set_valid_client_config_env_vars): + + if set_valid_client_config_env_vars == "PRYSM": + pytest.skip("Skipping this test for Prysm - remote keys require different wallet type.") + + import eth_2_key_manager_api_client + + remote_keys = [ + { + "pubkey": "0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a", + "url": "https://remote.signer", + } + ] + + eth_2_key_manager = eth_2_key_manager_api_client.Eth2KeyManager() + response = await eth_2_key_manager.import_remote_keys.asyncio_detailed(remote_keys=remote_keys) + + if response.status_code == 200: + print("Remote keys imported successfully") + else: + print(f"Remote keys import failed with status code: {response.status_code}") + + assert response.status_code == 200 + + # list remote keys + import eth_2_key_manager_api_client + + eth_2_key_manager = eth_2_key_manager_api_client.Eth2KeyManager() + response = await eth_2_key_manager.list_remote_keys.asyncio_detailed() + + if response.status_code == 200: + print(f"List of remote keys: {response.parsed.data}") + else: + print(f"List remote keys failed with status code: {response.status_code}") + + assert response.status_code == 200 + + # delete remote keys + import eth_2_key_manager_api_client + + eth_2_key_manager = eth_2_key_manager_api_client.Eth2KeyManager() + pubkey = "0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a" + response = await eth_2_key_manager.delete_remote_keys.asyncio_detailed(pubkeys=[pubkey]) + + if response.status_code == 200: + print("Remote keys deleted successfully") + else: + print(f"Remote keys delete failed with status code: {response.status_code}")