Repository: eth-educators/ethstaker-deposit-cli

Issue:
Enable every command to use a custom network
Similar to how we added `--devnet_chain_setting` flag to the `generate-bls-to-execution-change` command, we should also have that flag or a very similar feature to the other commands.

Related to #55

Discussion:
@remyroy: The idea is to either pass a chain as a flag, either as a known named chain or as a custom devnet setting (JSON string) and if no chain is passed, we prompt for one of the known named chain.

Solution patch:
diff --git a/docs/src/existing_mnemonic.md b/docs/src/existing_mnemonic.md
index 9efc6a98..50eaaa38 100644
--- a/docs/src/existing_mnemonic.md
+++ b/docs/src/existing_mnemonic.md
@@ -25,6 +25,8 @@ Uses an existing BIP-39 mnemonic phrase for key generation.
 
 - **`--folder`**: The folder where keystore and deposit data files will be saved.
 
+- **`--devnet_chain_setting`**: The custom chain setting of a devnet or testnet. Note that it will override your `--chain` choice. This should be a JSON string containing an object with the following keys: network_name, genesis_fork_version, exit_fork_version and genesis_validator_root.
+
 ## Output files
 A successful call to this command will result in one or many [keystore files](keystore_file.md) created, one per validator created, and one [deposit data file](deposit_data_file.md) created. The amount for each deposit in the deposit data file should always be 32 Ethers (`32000000000` in GWEI) with this command.
 
diff --git a/docs/src/exit_transaction_keystore.md b/docs/src/exit_transaction_keystore.md
index 9114cac1..9928c295 100644
--- a/docs/src/exit_transaction_keystore.md
+++ b/docs/src/exit_transaction_keystore.md
@@ -19,6 +19,8 @@ Creates an exit transaction using a keystore file.
 
 - **`--output_folder`**: The folder path for the `signed_exit_transaction-*` JSON file.
 
+- **`--devnet_chain_setting`**: The custom chain setting of a devnet or testnet. Note that it will override your `--chain` choice. This should be a JSON string containing an object with the following keys: network_name, genesis_fork_version, exit_fork_version and genesis_validator_root.
+
 
 ## Example Usage
 
diff --git a/docs/src/exit_transaction_mnemonic.md b/docs/src/exit_transaction_mnemonic.md
index f16a0cfc..6a05a711 100644
--- a/docs/src/exit_transaction_mnemonic.md
+++ b/docs/src/exit_transaction_mnemonic.md
@@ -21,6 +21,8 @@ Creates an exit transaction using a mnemonic phrase.
 
 - **`--output_folder`**: The folder path for the `signed_exit_transaction-*` JSON file.
 
+- **`--devnet_chain_setting`**: The custom chain setting of a devnet or testnet. Note that it will override your `--chain` choice. This should be a JSON string containing an object with the following keys: network_name, genesis_fork_version, exit_fork_version and genesis_validator_root.
+
 ## Example Usage
 
 ```sh
diff --git a/docs/src/generate_bls_to_execution_change.md b/docs/src/generate_bls_to_execution_change.md
index 4edbf7b9..0ba68492 100644
--- a/docs/src/generate_bls_to_execution_change.md
+++ b/docs/src/generate_bls_to_execution_change.md
@@ -23,7 +23,7 @@ Generates a BLS to execution address change message. This is used to add a withd
 
 - **`--withdrawal_address`**: If this field is set and valid, the given Ethereum execution address will be used to create the withdrawal credentials. Otherwise it will generate withdrawal credentials with the mnemonic-derived withdrawal public key.
 
-- **`--devnet_chain_setting`**: The custom chain setting of a devnet or testnet. Note that it will override your `--chain` choice.
+- **`--devnet_chain_setting`**: The custom chain setting of a devnet or testnet. Note that it will override your `--chain` choice. This should be a JSON string containing an object with the following keys: network_name, genesis_fork_version, exit_fork_version and genesis_validator_root.
 
 ## Example Usage
 
diff --git a/docs/src/generate_bls_to_execution_change_keystore.md b/docs/src/generate_bls_to_execution_change_keystore.md
index b31a3e4c..f7a1f7f6 100644
--- a/docs/src/generate_bls_to_execution_change_keystore.md
+++ b/docs/src/generate_bls_to_execution_change_keystore.md
@@ -23,7 +23,7 @@ Signs a withdrawal credential update message using the provided keystore. This s
 
 - **`--output_folder`**: The folder path for the `bls_to_execution_change_keystore_signature-*` JSON file.
 
-- **`--devnet_chain_setting`**: The custom chain setting of a devnet or testnet. Note that it will override your `--chain` choice.
+- **`--devnet_chain_setting`**: The custom chain setting of a devnet or testnet. Note that it will override your `--chain` choice. This should be a JSON string containing an object with the following keys: network_name, genesis_fork_version, exit_fork_version and genesis_validator_root.
 
 ## Example Usage
 
diff --git a/docs/src/new_mnemonic.md b/docs/src/new_mnemonic.md
index 2a6e772f..470ada2e 100644
--- a/docs/src/new_mnemonic.md
+++ b/docs/src/new_mnemonic.md
@@ -21,6 +21,8 @@ Generates a new BIP-39 mnemonic along with validator keystore and deposit files
 
 - **`--folder`**: The folder where keystore and deposit data files will be saved.
 
+- **`--devnet_chain_setting`**: The custom chain setting of a devnet or testnet. Note that it will override your `--chain` choice. This should be a JSON string containing an object with the following keys: network_name, genesis_fork_version, exit_fork_version and genesis_validator_root.
+
 ## Output files
 A successful call to this command will result in one or many [keystore files](keystore_file.md) created, one per validator created, and one [deposit data file](deposit_data_file.md) created. The amount for each deposit in the deposit data file should always be 32 Ethers (`32000000000` in GWEI) with this command.
 
diff --git a/docs/src/partial_deposit.md b/docs/src/partial_deposit.md
index f1110f54..3613b2ab 100644
--- a/docs/src/partial_deposit.md
+++ b/docs/src/partial_deposit.md
@@ -20,6 +20,8 @@ If you wish to create a validator with 0x00 credentials, you must use the **[new
 
 - **`--output_folder`**: The folder path for the `deposit-*` JSON file.
 
+- **`--devnet_chain_setting`**: The custom chain setting of a devnet or testnet. Note that it will override your `--chain` choice. This should be a JSON string containing an object with the following keys: network_name, genesis_fork_version, exit_fork_version and genesis_validator_root.
+
 ## Output file
 A successful call to this command will result in one [deposit data file](deposit_data_file.md) created.
 
diff --git a/ethstaker_deposit/bls_to_execution_change_keystore.py b/ethstaker_deposit/bls_to_execution_change_keystore.py
index 6a2b4a05..26f1ba43 100644
--- a/ethstaker_deposit/bls_to_execution_change_keystore.py
+++ b/ethstaker_deposit/bls_to_execution_change_keystore.py
@@ -16,13 +16,13 @@
 
 
 def bls_to_execution_change_keystore_generation(
-        chain_settings: BaseChainSetting,
+        chain_setting: BaseChainSetting,
         signing_key: int,
         execution_address: HexAddress,
         validator_index: int) -> SignedBLSToExecutionChangeKeystore:
     if execution_address is None:
         raise ValueError("The execution address should NOT be empty.")
-    if chain_settings.GENESIS_VALIDATORS_ROOT is None:
+    if chain_setting.GENESIS_VALIDATORS_ROOT is None:
         raise ValidationError("The genesis validators root should NOT be empty "
                               "for this chain to obtain the BLS to execution change.")
 
@@ -31,8 +31,8 @@ def bls_to_execution_change_keystore_generation(
         to_execution_address=to_canonical_address(execution_address),
     )
     domain = compute_bls_to_execution_change_keystore_domain(
-        fork_version=chain_settings.GENESIS_FORK_VERSION,
-        genesis_validators_root=chain_settings.GENESIS_VALIDATORS_ROOT,
+        fork_version=chain_setting.GENESIS_FORK_VERSION,
+        genesis_validators_root=chain_setting.GENESIS_VALIDATORS_ROOT,
     )
     signing_root = compute_signing_root(message, domain)
     signature = bls.Sign(signing_key, signing_root)
diff --git a/ethstaker_deposit/cli/exit_transaction_keystore.py b/ethstaker_deposit/cli/exit_transaction_keystore.py
index 7c9a7091..c8e97d6f 100644
--- a/ethstaker_deposit/cli/exit_transaction_keystore.py
+++ b/ethstaker_deposit/cli/exit_transaction_keystore.py
@@ -1,7 +1,7 @@
 import click
 import os
 import time
-from typing import Any
+from typing import Any, Optional
 
 from ethstaker_deposit.exceptions import ValidationError
 from ethstaker_deposit.utils.exit_transaction import exit_transaction_generation, export_exit_transaction_json
@@ -10,6 +10,7 @@
     MAINNET,
     ALL_CHAIN_KEYS,
     get_chain_setting,
+    BaseChainSetting,
 )
 from ethstaker_deposit.utils.click import (
     captive_prompt_callback,
@@ -21,7 +22,12 @@
     closest_match,
     load_text,
 )
-from ethstaker_deposit.utils.validation import validate_int_range, validate_keystore_file, verify_signed_exit_json
+from ethstaker_deposit.utils.validation import (
+    validate_int_range,
+    validate_keystore_file,
+    verify_signed_exit_json,
+    validate_devnet_chain_setting,
+)
 
 
 FUNC_NAME = 'exit_transaction_keystore'
@@ -37,14 +43,13 @@
             lambda: load_text(['arg_exit_transaction_keystore_chain', 'prompt'], func=FUNC_NAME),
             ALL_CHAIN_KEYS
         ),
+        prompt_if_other_is_none='devnet_chain_setting',
+        default=MAINNET,
     ),
     default=MAINNET,
     help=lambda: load_text(['arg_exit_transaction_keystore_chain', 'help'], func=FUNC_NAME),
     param_decls='--chain',
-    prompt=choice_prompt_func(
-        lambda: load_text(['arg_exit_transaction_keystore_chain', 'prompt'], func=FUNC_NAME),
-        ALL_CHAIN_KEYS
-    ),
+    prompt=False,  # the callback handles the prompt
 )
 @jit_option(
     callback=captive_prompt_callback(
@@ -89,6 +94,13 @@
     param_decls='--output_folder',
     type=click.Path(exists=True, file_okay=False, dir_okay=True),
 )
+@jit_option(
+    callback=validate_devnet_chain_setting,
+    default=None,
+    help=lambda: load_text(['arg_devnet_chain_setting', 'help'], func=FUNC_NAME),
+    param_decls='--devnet_chain_setting',
+    is_eager=True,
+)
 @click.pass_context
 def exit_transaction_keystore(
         ctx: click.Context,
@@ -98,6 +110,7 @@ def exit_transaction_keystore(
         validator_index: int,
         epoch: int,
         output_folder: str,
+        devnet_chain_setting: Optional[BaseChainSetting],
         **kwargs: Any) -> None:
     try:
         secret_bytes = keystore.decrypt(keystore_password)
@@ -106,10 +119,12 @@ def exit_transaction_keystore(
         exit(1)
 
     signing_key = int.from_bytes(secret_bytes, 'big')
-    chain_settings = get_chain_setting(chain)
+
+    # Get chain setting
+    chain_setting = devnet_chain_setting if devnet_chain_setting is not None else get_chain_setting(chain)
 
     signed_exit = exit_transaction_generation(
-        chain_settings=chain_settings,
+        chain_setting=chain_setting,
         signing_key=signing_key,
         validator_index=validator_index,
         epoch=epoch,
@@ -123,7 +138,7 @@ def exit_transaction_keystore(
     saved_folder = export_exit_transaction_json(folder=folder, signed_exit=signed_exit, timestamp=time.time())
 
     click.echo(load_text(['msg_verify_exit_transaction']))
-    if (not verify_signed_exit_json(saved_folder, keystore.pubkey, chain_settings)):
+    if (not verify_signed_exit_json(saved_folder, keystore.pubkey, chain_setting)):
         raise ValidationError(load_text(['err_verify_exit_transaction']))
 
     click.echo(load_text(['msg_creation_success']) + saved_folder)
diff --git a/ethstaker_deposit/cli/exit_transaction_mnemonic.py b/ethstaker_deposit/cli/exit_transaction_mnemonic.py
index 28434a13..5dc18157 100644
--- a/ethstaker_deposit/cli/exit_transaction_mnemonic.py
+++ b/ethstaker_deposit/cli/exit_transaction_mnemonic.py
@@ -3,7 +3,7 @@
 import os
 import time
 
-from typing import Any, Sequence, Dict
+from typing import Any, Sequence, Dict, Optional
 from ethstaker_deposit.cli.existing_mnemonic import load_mnemonic_arguments_decorator
 from ethstaker_deposit.credentials import Credential
 from ethstaker_deposit.exceptions import ValidationError
@@ -11,6 +11,7 @@
     MAINNET,
     ALL_CHAIN_KEYS,
     get_chain_setting,
+    BaseChainSetting,
 )
 from ethstaker_deposit.utils.click import (
     captive_prompt_callback,
@@ -22,7 +23,12 @@
     closest_match,
     load_text,
 )
-from ethstaker_deposit.utils.validation import validate_int_range, validate_validator_indices, verify_signed_exit_json
+from ethstaker_deposit.utils.validation import (
+    validate_int_range,
+    validate_validator_indices,
+    verify_signed_exit_json,
+    validate_devnet_chain_setting,
+)
 
 
 def _credential_builder(kwargs: Dict[str, Any]) -> Credential:
@@ -37,7 +43,7 @@ def _exit_exporter(kwargs: Dict[str, Any]) -> str:
 def _exit_verifier(kwargs: Dict[str, Any]) -> bool:
     credential: Credential = kwargs.pop('credential')
     kwargs['pubkey'] = credential.signing_pk.hex()
-    kwargs['chain_settings'] = credential.chain_setting
+    kwargs['chain_setting'] = credential.chain_setting
     return verify_signed_exit_json(**kwargs)
 
 
@@ -54,14 +60,13 @@ def _exit_verifier(kwargs: Dict[str, Any]) -> bool:
             lambda: load_text(['arg_exit_transaction_mnemonic_chain', 'prompt'], func=FUNC_NAME),
             ALL_CHAIN_KEYS
         ),
+        prompt_if_other_is_none='devnet_chain_setting',
+        default=MAINNET,
     ),
     default=MAINNET,
     help=lambda: load_text(['arg_exit_transaction_mnemonic_chain', 'help'], func=FUNC_NAME),
     param_decls='--chain',
-    prompt=choice_prompt_func(
-        lambda: load_text(['arg_exit_transaction_mnemonic_chain', 'prompt'], func=FUNC_NAME),
-        ALL_CHAIN_KEYS
-    ),
+    prompt=False,  # the callback handles the prompt
 )
 @load_mnemonic_arguments_decorator
 @jit_option(
@@ -94,6 +99,13 @@ def _exit_verifier(kwargs: Dict[str, Any]) -> bool:
     param_decls='--output_folder',
     type=click.Path(exists=True, file_okay=False, dir_okay=True),
 )
+@jit_option(
+    callback=validate_devnet_chain_setting,
+    default=None,
+    help=lambda: load_text(['arg_devnet_chain_setting', 'help'], func=FUNC_NAME),
+    param_decls='--devnet_chain_setting',
+    is_eager=True,
+)
 @click.pass_context
 def exit_transaction_mnemonic(
         ctx: click.Context,
@@ -104,10 +116,14 @@ def exit_transaction_mnemonic(
         validator_indices: Sequence[int],
         epoch: int,
         output_folder: str,
+        devnet_chain_setting: Optional[BaseChainSetting],
         **kwargs: Any) -> None:
 
     folder = os.path.join(output_folder, DEFAULT_EXIT_TRANSACTION_FOLDER_NAME)
-    chain_settings = get_chain_setting(chain)
+
+    # Get chain setting
+    chain_setting = devnet_chain_setting if devnet_chain_setting is not None else get_chain_setting(chain)
+
     num_keys = len(validator_indices)
     key_indices = range(validator_start_index, validator_start_index + num_keys)
 
@@ -121,7 +137,7 @@ def exit_transaction_mnemonic(
             'mnemonic_password': mnemonic_password,
             'index': index,
             'amount': 0,
-            'chain_setting': chain_settings,
+            'chain_setting': chain_setting,
             'hex_withdrawal_address': None,
         } for index in key_indices]
 
diff --git a/ethstaker_deposit/cli/generate_bls_to_execution_change.py b/ethstaker_deposit/cli/generate_bls_to_execution_change.py
index 3dce6b06..69f7e28a 100644
--- a/ethstaker_deposit/cli/generate_bls_to_execution_change.py
+++ b/ethstaker_deposit/cli/generate_bls_to_execution_change.py
@@ -1,6 +1,5 @@
 import os
 import click
-import json
 import concurrent.futures
 from typing import (
     Any,
@@ -23,6 +22,7 @@
     validate_int_range,
     verify_bls_to_execution_change_json,
     validate_validator_indices,
+    validate_devnet_chain_setting,
 )
 from ethstaker_deposit.utils.constants import (
     DEFAULT_BLS_TO_EXECUTION_CHANGES_FOLDER_NAME,
@@ -42,7 +42,7 @@
     MAINNET,
     ALL_CHAIN_KEYS,
     get_chain_setting,
-    get_devnet_chain_setting,
+    BaseChainSetting,
 )
 from .existing_mnemonic import (
     load_mnemonic_arguments_decorator,
@@ -79,14 +79,13 @@ def _validate_credentials_match(kwargs: Dict[str, Any]) -> Optional[ValidationEr
             lambda: load_text(['arg_chain', 'prompt'], func=FUNC_NAME),
             ALL_CHAIN_KEYS
         ),
+        prompt_if_other_is_none='devnet_chain_setting',
+        default=MAINNET,
     ),
     default=MAINNET,
     help=lambda: load_text(['arg_chain', 'help'], func=FUNC_NAME),
     param_decls='--chain',
-    prompt=choice_prompt_func(
-        lambda: load_text(['arg_chain', 'prompt'], func=FUNC_NAME),
-        ALL_CHAIN_KEYS
-    ),
+    prompt=False,  # the callback handles the prompt
 )
 @load_mnemonic_arguments_decorator
 @jit_option(
@@ -132,10 +131,11 @@ def _validate_credentials_match(kwargs: Dict[str, Any]) -> Optional[ValidationEr
     prompt=False,  # the callback handles the prompt
 )
 @jit_option(
-    # Only for devnet tests
+    callback=validate_devnet_chain_setting,
     default=None,
-    help="[DEVNET ONLY] Set specific GENESIS_FORK_VERSION value",
+    help=lambda: load_text(['arg_devnet_chain_setting', 'help'], func=FUNC_NAME),
     param_decls='--devnet_chain_setting',
+    is_eager=True,
 )
 @click.pass_context
 def generate_bls_to_execution_change(
@@ -148,7 +148,7 @@ def generate_bls_to_execution_change(
         validator_indices: Sequence[int],
         bls_withdrawal_credentials_list: Sequence[bytes],
         withdrawal_address: HexAddress,
-        devnet_chain_setting: str,
+        devnet_chain_setting: Optional[BaseChainSetting],
         **kwargs: Any) -> None:
     # Generate folder
     bls_to_execution_changes_folder = os.path.join(
@@ -159,17 +159,7 @@ def generate_bls_to_execution_change(
         os.mkdir(bls_to_execution_changes_folder)
 
     # Get chain setting
-    chain_setting = get_chain_setting(chain)
-
-    if devnet_chain_setting is not None:
-        click.echo('\n%s\n' % '**[Warning] Using devnet chain setting to generate the SignedBLSToExecutionChange.**\t')
-        devnet_chain_setting_dict = json.loads(devnet_chain_setting)
-        chain_setting = get_devnet_chain_setting(
-            network_name=devnet_chain_setting_dict['network_name'],
-            genesis_fork_version=devnet_chain_setting_dict['genesis_fork_version'],
-            exit_fork_version=devnet_chain_setting_dict['exit_fork_version'],
-            genesis_validator_root=devnet_chain_setting_dict['genesis_validator_root'],
-        )
+    chain_setting = devnet_chain_setting if devnet_chain_setting is not None else get_chain_setting(chain)
 
     if len(validator_indices) != len(bls_withdrawal_credentials_list):
         raise ValueError(
diff --git a/ethstaker_deposit/cli/generate_bls_to_execution_change_keystore.py b/ethstaker_deposit/cli/generate_bls_to_execution_change_keystore.py
index e3b6ce19..4de9611d 100644
--- a/ethstaker_deposit/cli/generate_bls_to_execution_change_keystore.py
+++ b/ethstaker_deposit/cli/generate_bls_to_execution_change_keystore.py
@@ -1,7 +1,7 @@
 import os
 import time
 import click
-from typing import Any
+from typing import Any, Optional
 
 from eth_typing import HexAddress
 
@@ -16,6 +16,7 @@
     validate_int_range,
     validate_keystore_file,
     verify_bls_to_execution_change_keystore_json,
+    validate_devnet_chain_setting,
 )
 from ethstaker_deposit.utils.constants import (
     DEFAULT_BLS_TO_EXECUTION_CHANGES_KEYSTORE_FOLDER_NAME,
@@ -33,6 +34,7 @@
     MAINNET,
     ALL_CHAIN_KEYS,
     get_chain_setting,
+    BaseChainSetting,
 )
 
 
@@ -49,14 +51,13 @@
             lambda: load_text(['arg_chain', 'prompt'], func=FUNC_NAME),
             ALL_CHAIN_KEYS
         ),
+        prompt_if_other_is_none='devnet_chain_setting',
+        default=MAINNET,
     ),
     default=MAINNET,
     help=lambda: load_text(['arg_chain', 'help'], func=FUNC_NAME),
     param_decls='--chain',
-    prompt=choice_prompt_func(
-        lambda: load_text(['arg_chain', 'prompt'], func=FUNC_NAME),
-        ALL_CHAIN_KEYS
-    ),
+    prompt=False,  # the callback handles the prompt
 )
 @jit_option(
     callback=captive_prompt_callback(
@@ -109,10 +110,11 @@
     type=click.Path(exists=True, file_okay=False, dir_okay=True),
 )
 @jit_option(
-    # Only for devnet tests
+    callback=validate_devnet_chain_setting,
     default=None,
-    help="[DEVNET ONLY] Set specific GENESIS_FORK_VERSION value",
+    help=lambda: load_text(['arg_devnet_chain_setting', 'help'], func=FUNC_NAME),
     param_decls='--devnet_chain_setting',
+    is_eager=True,
 )
 @click.pass_context
 def generate_bls_to_execution_change_keystore(
@@ -123,6 +125,7 @@ def generate_bls_to_execution_change_keystore(
         validator_index: int,
         withdrawal_address: HexAddress,
         output_folder: str,
+        devnet_chain_setting: Optional[BaseChainSetting],
         **kwargs: Any) -> None:
     try:
         secret_bytes = keystore.decrypt(keystore_password)
@@ -131,10 +134,12 @@ def generate_bls_to_execution_change_keystore(
         exit(1)
 
     signing_key = int.from_bytes(secret_bytes, 'big')
-    chain_settings = get_chain_setting(chain)
+
+    # Get chain setting
+    chain_setting = devnet_chain_setting if devnet_chain_setting is not None else get_chain_setting(chain)
 
     signed_btec = bls_to_execution_change_keystore_generation(
-        chain_settings=chain_settings,
+        chain_setting=chain_setting,
         signing_key=signing_key,
         validator_index=validator_index,
         execution_address=withdrawal_address,
@@ -150,7 +155,7 @@ def generate_bls_to_execution_change_keystore(
                                                                 timestamp=time.time())
 
     click.echo(load_text(['msg_verify_btec']))
-    if (not verify_bls_to_execution_change_keystore_json(saved_folder, keystore.pubkey, chain_settings)):
+    if (not verify_bls_to_execution_change_keystore_json(saved_folder, keystore.pubkey, chain_setting)):
         raise ValidationError(load_text(['err_verify_btec']))
 
     click.echo(load_text(['msg_creation_success']) + saved_folder)
diff --git a/ethstaker_deposit/cli/generate_keys.py b/ethstaker_deposit/cli/generate_keys.py
index a678cdb4..f14db47d 100644
--- a/ethstaker_deposit/cli/generate_keys.py
+++ b/ethstaker_deposit/cli/generate_keys.py
@@ -4,6 +4,7 @@
 from typing import (
     Any,
     Callable,
+    Optional,
 )
 
 from eth_typing import HexAddress
@@ -16,6 +17,7 @@
     validate_int_range,
     validate_password_strength,
     validate_withdrawal_address,
+    validate_devnet_chain_setting,
 )
 from ethstaker_deposit.utils.constants import (
     DEFAULT_VALIDATOR_KEYS_FOLDER_NAME,
@@ -35,6 +37,7 @@
     MAINNET,
     ALL_CHAIN_KEYS,
     get_chain_setting,
+    BaseChainSetting,
 )
 
 
@@ -66,15 +69,13 @@ def generate_keys_arguments_decorator(function: Callable[..., Any]) -> Callable[
                     lambda: load_text(['chain', 'prompt'], func='generate_keys_arguments_decorator'),
                     ALL_CHAIN_KEYS
                 ),
+                prompt_if_other_is_none='devnet_chain_setting',
                 default=MAINNET,
             ),
             default=MAINNET,
             help=lambda: load_text(['chain', 'help'], func='generate_keys_arguments_decorator'),
             param_decls='--chain',
-            prompt=choice_prompt_func(
-                lambda: load_text(['chain', 'prompt'], func='generate_keys_arguments_decorator'),
-                ALL_CHAIN_KEYS
-            ),
+            prompt=False,  # the callback handles the prompt
         ),
         jit_option(
             callback=captive_prompt_callback(
@@ -110,6 +111,13 @@ def generate_keys_arguments_decorator(function: Callable[..., Any]) -> Callable[
             param_decls='--pbkdf2',
             help=lambda: load_text(['arg_pbkdf2', 'help'], func='generate_keys_arguments_decorator'),
         ),
+        jit_option(
+            callback=validate_devnet_chain_setting,
+            default=None,
+            help=lambda: load_text(['arg_devnet_chain_setting', 'help'], func='generate_keys_arguments_decorator'),
+            param_decls='--devnet_chain_setting',
+            is_eager=True,
+        ),
     ]
     for decorator in reversed(decorators):
         function = decorator(function)
@@ -120,12 +128,16 @@ def generate_keys_arguments_decorator(function: Callable[..., Any]) -> Callable[
 @click.pass_context
 def generate_keys(ctx: click.Context, validator_start_index: int,
                   num_validators: int, folder: str, chain: str, keystore_password: str,
-                  withdrawal_address: HexAddress, pbkdf2: bool, **kwargs: Any) -> None:
+                  withdrawal_address: HexAddress, pbkdf2: bool,
+                  devnet_chain_setting: Optional[BaseChainSetting], **kwargs: Any) -> None:
     mnemonic = ctx.obj['mnemonic']
     mnemonic_password = ctx.obj['mnemonic_password']
     amounts = [MIN_ACTIVATION_AMOUNT] * num_validators
     folder = os.path.join(folder, DEFAULT_VALIDATOR_KEYS_FOLDER_NAME)
-    chain_setting = get_chain_setting(chain)
+
+    # Get chain setting
+    chain_setting = devnet_chain_setting if devnet_chain_setting is not None else get_chain_setting(chain)
+
     if not os.path.exists(folder):
         os.mkdir(folder)
     click.clear()
diff --git a/ethstaker_deposit/cli/partial_deposit.py b/ethstaker_deposit/cli/partial_deposit.py
index 7dd1dbfe..202cff62 100644
--- a/ethstaker_deposit/cli/partial_deposit.py
+++ b/ethstaker_deposit/cli/partial_deposit.py
@@ -6,7 +6,7 @@
 from eth_typing import HexAddress
 from eth_utils import to_canonical_address
 from py_ecc.bls import G2ProofOfPossession as bls
-from typing import Any
+from typing import Any, Optional
 
 from ethstaker_deposit.key_handling.keystore import Keystore
 from ethstaker_deposit.settings import (
@@ -14,6 +14,7 @@
     MAINNET,
     ALL_CHAIN_KEYS,
     get_chain_setting,
+    BaseChainSetting,
 )
 from ethstaker_deposit.utils.click import (
     captive_prompt_callback,
@@ -37,6 +38,7 @@
     validate_keystore_file,
     validate_partial_deposit_amount,
     validate_withdrawal_address,
+    validate_devnet_chain_setting,
 )
 
 
@@ -53,14 +55,13 @@
             lambda: load_text(['arg_partial_deposit_chain', 'prompt'], func=FUNC_NAME),
             ALL_CHAIN_KEYS
         ),
+        prompt_if_other_is_none='devnet_chain_setting',
+        default=MAINNET,
     ),
     default=MAINNET,
     help=lambda: load_text(['arg_partial_deposit_chain', 'help'], func=FUNC_NAME),
     param_decls='--chain',
-    prompt=choice_prompt_func(
-        lambda: load_text(['arg_partial_deposit_chain', 'prompt'], func=FUNC_NAME),
-        ALL_CHAIN_KEYS
-    ),
+    prompt=False,  # the callback handles the prompt
 )
 @jit_option(
     callback=captive_prompt_callback(
@@ -115,6 +116,13 @@
     param_decls='--output_folder',
     type=click.Path(exists=True, file_okay=False, dir_okay=True),
 )
+@jit_option(
+    callback=validate_devnet_chain_setting,
+    default=None,
+    help=lambda: load_text(['arg_devnet_chain_setting', 'help'], func=FUNC_NAME),
+    param_decls='--devnet_chain_setting',
+    is_eager=True,
+)
 @click.pass_context
 def partial_deposit(
         ctx: click.Context,
@@ -124,6 +132,7 @@ def partial_deposit(
         amount: int,
         withdrawal_address: HexAddress,
         output_folder: str,
+        devnet_chain_setting: Optional[BaseChainSetting],
         **kwargs: Any) -> None:
     try:
         secret_bytes = keystore.decrypt(keystore_password)
@@ -132,7 +141,9 @@ def partial_deposit(
         exit(1)
 
     signing_key = int.from_bytes(secret_bytes, 'big')
-    chain_settings = get_chain_setting(chain)
+
+    # Get chain setting
+    chain_setting = devnet_chain_setting if devnet_chain_setting is not None else get_chain_setting(chain)
 
     withdrawal_credentials = EXECUTION_ADDRESS_WITHDRAWAL_PREFIX
     withdrawal_credentials += b'\x00' * 11
@@ -144,7 +155,7 @@ def partial_deposit(
         amount=amount
     )
 
-    domain = compute_deposit_domain(fork_version=chain_settings.GENESIS_FORK_VERSION)
+    domain = compute_deposit_domain(fork_version=chain_setting.GENESIS_FORK_VERSION)
 
     signing_root = compute_signing_root(deposit_message, domain)
     signature = bls.Sign(signing_key, signing_root)
@@ -162,8 +173,8 @@ def partial_deposit(
     deposit_data = signed_deposit.as_dict()  # type: ignore[no-untyped-call]
     deposit_data.update({'deposit_message_root': deposit_message.hash_tree_root})
     deposit_data.update({'deposit_data_root': signed_deposit.hash_tree_root})
-    deposit_data.update({'fork_version': chain_settings.GENESIS_FORK_VERSION})
-    deposit_data.update({'network_name': chain_settings.NETWORK_NAME})
+    deposit_data.update({'fork_version': chain_setting.GENESIS_FORK_VERSION})
+    deposit_data.update({'network_name': chain_setting.NETWORK_NAME})
     deposit_data.update({'deposit_cli_version': DEPOSIT_CLI_VERSION})
     saved_folder = export_deposit_data_json(folder, time.time(), [deposit_data])
 
diff --git a/ethstaker_deposit/credentials.py b/ethstaker_deposit/credentials.py
index 58894d93..3b0cb12e 100644
--- a/ethstaker_deposit/credentials.py
+++ b/ethstaker_deposit/credentials.py
@@ -223,7 +223,7 @@ def save_exit_transaction(self, validator_index: int, epoch: int, folder: str, t
         signing_key = self.signing_sk
 
         signed_voluntary_exit = exit_transaction_generation(
-            chain_settings=self.chain_setting,
+            chain_setting=self.chain_setting,
             signing_key=signing_key,
             validator_index=validator_index,
             epoch=epoch
diff --git a/ethstaker_deposit/intl/en/cli/exit_transaction_keystore.json b/ethstaker_deposit/intl/en/cli/exit_transaction_keystore.json
index 91f33b85..e0e008df 100644
--- a/ethstaker_deposit/intl/en/cli/exit_transaction_keystore.json
+++ b/ethstaker_deposit/intl/en/cli/exit_transaction_keystore.json
@@ -26,6 +26,9 @@
           "help": "The validator index corresponding to the provided keystore.",
           "prompt": "Please enter the validator index of your validator that corresponds to the provided keystore as identified on the beacon chain."
       },
+      "arg_devnet_chain_setting": {
+          "help": "[DEVNET ONLY] Set a specific EXIT_FORK_VERSION and GENESIS_VALIDATORS_ROOT value. This should be a JSON string containing an object with the following keys: network_name, genesis_fork_version, exit_fork_version and genesis_validator_root. It should be similar to what you can find in settings.py. This will override any selected chain."
+      },
       "msg_exit_transaction_creation": "\nCreating your exit transaction...",
       "msg_verify_exit_transaction": "\nVerifying your exit transaction...",
       "err_verify_exit_transaction": "\nThere was a problem verifying your exit transaction.\nPlease try again",
diff --git a/ethstaker_deposit/intl/en/cli/exit_transaction_mnemonic.json b/ethstaker_deposit/intl/en/cli/exit_transaction_mnemonic.json
index b0e8510c..2dc6a4ab 100644
--- a/ethstaker_deposit/intl/en/cli/exit_transaction_mnemonic.json
+++ b/ethstaker_deposit/intl/en/cli/exit_transaction_mnemonic.json
@@ -21,6 +21,9 @@
       "arg_exit_transaction_mnemonic_output_folder": {
           "help": "The folder path where the exit transactions will be saved to. Pointing to `./exit_transactions` by default."
       },
+      "arg_devnet_chain_setting": {
+          "help": "[DEVNET ONLY] Set a specific EXIT_FORK_VERSION and GENESIS_VALIDATORS_ROOT value. This should be a JSON string containing an object with the following keys: network_name, genesis_fork_version, exit_fork_version and genesis_validator_root. It should be similar to what you can find in settings.py. This will override any selected chain."
+      },
       "msg_key_creation": "Creating your keys:\t",
       "msg_exit_transaction_creation": "Creating your exit transactions:\t",
       "msg_verify_exit_transaction": "Verifying your exit transactions:\t",
diff --git a/ethstaker_deposit/intl/en/cli/generate_bls_to_execution_change.json b/ethstaker_deposit/intl/en/cli/generate_bls_to_execution_change.json
index 3a9f9dad..4a346ff7 100644
--- a/ethstaker_deposit/intl/en/cli/generate_bls_to_execution_change.json
+++ b/ethstaker_deposit/intl/en/cli/generate_bls_to_execution_change.json
@@ -9,6 +9,9 @@
             "confirm": "Repeat your execution address for confirmation.",
             "mismatch": "Error: the two entered values do not match. Please type again."
         },
+        "arg_devnet_chain_setting": {
+            "help": "[DEVNET ONLY] Set a specific GENESIS_FORK_VERSION and GENESIS_VALIDATORS_ROOT value. This should be a JSON string containing an object with the following keys: network_name, genesis_fork_version, exit_fork_version and genesis_validator_root. It should be similar to what you can find in settings.py. This will override any selected chain."
+        },
         "arg_validator_indices": {
             "help": "A list of the validator index number(s) of the certain validator(s)",
             "prompt": "Please enter a list of the validator index number(s) of your validator(s) as identified on the beacon chain. Split multiple items with whitespaces or commas."
diff --git a/ethstaker_deposit/intl/en/cli/generate_bls_to_execution_change_keystore.json b/ethstaker_deposit/intl/en/cli/generate_bls_to_execution_change_keystore.json
index a5bb4f25..880ea2ab 100644
--- a/ethstaker_deposit/intl/en/cli/generate_bls_to_execution_change_keystore.json
+++ b/ethstaker_deposit/intl/en/cli/generate_bls_to_execution_change_keystore.json
@@ -9,6 +9,9 @@
           "confirm": "Repeat your execution address for confirmation.",
           "mismatch": "Error: the two entered values do not match. Please type again."
       },
+      "arg_devnet_chain_setting": {
+          "help": "[DEVNET ONLY] Set a specific GENESIS_FORK_VERSION and GENESIS_VALIDATORS_ROOT value. This should be a JSON string containing an object with the following keys: network_name, genesis_fork_version, exit_fork_version and genesis_validator_root. It should be similar to what you can find in settings.py. This will override any selected chain."
+      },
       "arg_validator_index": {
           "help": "The validator index",
           "prompt": "Please enter the validator index number of your validator as identified on the beacon chain."
diff --git a/ethstaker_deposit/intl/en/cli/generate_keys.json b/ethstaker_deposit/intl/en/cli/generate_keys.json
index a78ddcdd..5a3d12fe 100644
--- a/ethstaker_deposit/intl/en/cli/generate_keys.json
+++ b/ethstaker_deposit/intl/en/cli/generate_keys.json
@@ -25,6 +25,9 @@
         },
         "arg_pbkdf2": {
             "help": "Uses the pbkdf2 hashing function instead of scrypt for generated keystore files. "
+        },
+        "arg_devnet_chain_setting": {
+            "help": "[DEVNET ONLY] Set specific GENESIS_FORK_VERSION value. This should be a JSON string containing an object with the following keys: network_name, genesis_fork_version, exit_fork_version and genesis_validator_root. It should be similar to what you can find in settings.py. This will override any selected chain."
         }
     },
     "generate_keys": {
diff --git a/ethstaker_deposit/intl/en/cli/partial_deposit.json b/ethstaker_deposit/intl/en/cli/partial_deposit.json
index d2a1d9b4..51c0ef2c 100644
--- a/ethstaker_deposit/intl/en/cli/partial_deposit.json
+++ b/ethstaker_deposit/intl/en/cli/partial_deposit.json
@@ -28,6 +28,9 @@
           "confirm": "Repeat the withdrawal address for confirmation.",
           "prompt": "Please enter the 20-byte execution address withdrawal credentials of the validator. If you wish to create a validator with 0x00 credentials use the new-mnemonic or existing-mnemonic command."
       },
+      "arg_devnet_chain_setting": {
+          "help": "[DEVNET ONLY] Set specific GENESIS_FORK_VERSION value. This should be a JSON string containing an object with the following keys: network_name, genesis_fork_version, exit_fork_version and genesis_validator_root. It should be similar to what you can find in settings.py. This will override any selected chain."
+      },
       "msg_partial_deposit_creation": "\nCreating your partial deposit...",
       "msg_verify_partial_deposit": "\nVerifying your partial deposit...",
       "err_verify_partial_deposit": "\nThere was a problem verifying your partial deposit.\nPlease try again",
diff --git a/ethstaker_deposit/intl/en/utils/validation.json b/ethstaker_deposit/intl/en/utils/validation.json
index 04cde9ff..23ad0dd1 100644
--- a/ethstaker_deposit/intl/en/utils/validation.json
+++ b/ethstaker_deposit/intl/en/utils/validation.json
@@ -44,5 +44,16 @@
     "validate_keystore_file": {
         "err_file_not_found": "No file was found. Please verify the provided path and try again.",
         "err_invalid_keystore_file": "The discovered file is not the correct keystore file format. Please verify the provided path is to a keystore file and try again."
+    },
+    "validate_devnet_chain_setting": {
+        "err_invalid_devnet_chain_setting": "Invalid JSON string for devnet_chain_setting. It should be a valide JSON object that contains at least the following keys: network_name, genesis_fork_version and exit_fork_version.",
+        "arg_devnet_chain_setting_warning": "**[Warning] Using devnet chain setting with this command.**"
+    },
+    "validate_devnet_chain_setting_json": {
+        "err_devnet_chain_setting_not_object": "Invalid JSON string for devnet_chain_setting. The JSON string does not contain an object at root.",
+        "err_devnet_chain_setting_missing_keys": "Invalid JSON string for devnet_chain_setting. It is missing one or more of these key in the root object: network_name, genesis_fork_version or exit_fork_version.",
+        "err_devnet_chain_setting_invalid_json": "Invalid JSON string for devnet_chain_setting. Decoding the JSON string was impossible. Make sure to pass a valid JSON string.",
+        "err_devnet_chain_setting_key_length": "Invalid JSON string for devnet_chain_setting. The root object should only contain 3 or 4 keys.",
+        "err_devnet_chain_setting_invalid_fourth_key": "Invalid JSON string for devnet_chain_setting. The root object is missing the genesis_validator_root key."
     }
 }
diff --git a/ethstaker_deposit/settings.py b/ethstaker_deposit/settings.py
index 2503ac4b..e5fa6323 100644
--- a/ethstaker_deposit/settings.py
+++ b/ethstaker_deposit/settings.py
@@ -12,6 +12,13 @@ class BaseChainSetting(NamedTuple):
     EXIT_FORK_VERSION: bytes  # capella fork version for voluntary exits (EIP-7044)
     GENESIS_VALIDATORS_ROOT: Optional[bytes] = None
 
+    def __str__(self) -> str:
+        gvr_value = self.GENESIS_VALIDATORS_ROOT.hex() if self.GENESIS_VALIDATORS_ROOT is not None else 'None'
+        return (f'Network {self.NETWORK_NAME}\n'
+                f'  - Genesis fork version: {self.GENESIS_FORK_VERSION.hex()}\n'
+                f'  - Exit fork version: {self.EXIT_FORK_VERSION.hex()}\n'
+                f'  - Genesis validators root: {gvr_value}')
+
 
 MAINNET = 'mainnet'
 SEPOLIA = 'sepolia'
@@ -65,10 +72,10 @@ def get_chain_setting(chain_name: str = MAINNET) -> BaseChainSetting:
 def get_devnet_chain_setting(network_name: str,
                              genesis_fork_version: str,
                              exit_fork_version: str,
-                             genesis_validator_root: str) -> BaseChainSetting:
+                             genesis_validator_root: Optional[str]) -> BaseChainSetting:
     return BaseChainSetting(
         NETWORK_NAME=network_name,
         GENESIS_FORK_VERSION=decode_hex(genesis_fork_version),
         EXIT_FORK_VERSION=decode_hex(exit_fork_version),
-        GENESIS_VALIDATORS_ROOT=decode_hex(genesis_validator_root),
+        GENESIS_VALIDATORS_ROOT=decode_hex(genesis_validator_root) if genesis_validator_root is not None else None,
     )
diff --git a/ethstaker_deposit/utils/click.py b/ethstaker_deposit/utils/click.py
index cae5900c..4d4eb8b6 100644
--- a/ethstaker_deposit/utils/click.py
+++ b/ethstaker_deposit/utils/click.py
@@ -92,6 +92,7 @@ def captive_prompt_callback(
     hide_input: bool = False,
     default: Optional[Union[Callable[[], str], str]] = None,
     prompt_if_none: bool = False,
+    prompt_if_other_is_none: Optional[str] = None,
 ) -> Callable[[click.Context, str, str], Any]:
     '''
     Traps the user in a prompt until the value chosen is acceptable
@@ -103,6 +104,9 @@ def captive_prompt_callback(
     :param hide_input: bool, hides the input as the user types
     :param default: the optional callable that returns a str or a str to be used as the default value if nothing is
     entered by the user
+    :param prompt_if_none: bool, prompt if the source of the parameter is from the default value and it's none
+    :param prompt_if_other_is_none: the optional str of the other parameter to check. prompt if the source of the
+    parameter is from the default value and the value of that other parameter is none
     '''
     def callback(ctx: click.Context, param: Any, user_input: str) -> Any:
         # the callback is called twice, once for the option prompt and once to verify the input
@@ -112,6 +116,10 @@ def callback(ctx: click.Context, param: Any, user_input: str) -> Any:
         if (prompt_if_none and param.prompt is None
                 and ctx.get_parameter_source(param.name) == click.core.ParameterSource.DEFAULT):
             user_input = click.prompt(prompt(), hide_input=hide_input, default=_value_of(default))
+        elif (prompt_if_other_is_none is not None
+                and ctx.params.get(prompt_if_other_is_none, None) is None
+                and ctx.get_parameter_source(param.name) == click.core.ParameterSource.DEFAULT):
+            user_input = click.prompt(prompt(), hide_input=hide_input, default=_value_of(default))
         if config.non_interactive:
             return processing_func(user_input)
         while True:
diff --git a/ethstaker_deposit/utils/exit_transaction.py b/ethstaker_deposit/utils/exit_transaction.py
index d0d3b49f..dfd7934e 100644
--- a/ethstaker_deposit/utils/exit_transaction.py
+++ b/ethstaker_deposit/utils/exit_transaction.py
@@ -13,7 +13,7 @@
 
 
 def exit_transaction_generation(
-        chain_settings: BaseChainSetting,
+        chain_setting: BaseChainSetting,
         signing_key: int,
         validator_index: int,
         epoch: int) -> SignedVoluntaryExit:
@@ -23,8 +23,8 @@ def exit_transaction_generation(
     )
 
     domain = compute_voluntary_exit_domain(
-        fork_version=chain_settings.EXIT_FORK_VERSION,
-        genesis_validators_root=chain_settings.GENESIS_VALIDATORS_ROOT
+        fork_version=chain_setting.EXIT_FORK_VERSION,
+        genesis_validators_root=chain_setting.GENESIS_VALIDATORS_ROOT
     )
 
     signing_root = compute_signing_root(message, domain)
diff --git a/ethstaker_deposit/utils/validation.py b/ethstaker_deposit/utils/validation.py
index ef0b36ee..f251f6cd 100644
--- a/ethstaker_deposit/utils/validation.py
+++ b/ethstaker_deposit/utils/validation.py
@@ -4,7 +4,7 @@
 import re
 import sys
 import concurrent.futures
-from typing import Any, Dict, Sequence
+from typing import Any, Dict, Sequence, Optional
 
 from eth_typing import (
     BLSPubkey,
@@ -43,7 +43,7 @@
     MAX_DEPOSIT_AMOUNT,
 )
 from ethstaker_deposit.utils.crypto import SHA256
-from ethstaker_deposit.settings import BaseChainSetting
+from ethstaker_deposit.settings import BaseChainSetting, get_devnet_chain_setting
 
 
 #
@@ -366,19 +366,19 @@ def validate_keystore_file(file_path: str) -> Keystore:
     return saved_keystore
 
 
-def verify_signed_exit_json(file_folder: str, pubkey: str, chain_settings: BaseChainSetting) -> bool:
+def verify_signed_exit_json(file_folder: str, pubkey: str, chain_setting: BaseChainSetting) -> bool:
     with open(file_folder, 'r', encoding='utf-8') as f:
         deposit_json: SignedVoluntaryExit = json.load(f)
         signature = deposit_json["signature"]
         message = deposit_json["message"]
-        return validate_signed_exit(message["validator_index"], message["epoch"], signature, pubkey, chain_settings)
+        return validate_signed_exit(message["validator_index"], message["epoch"], signature, pubkey, chain_setting)
 
 
 def validate_signed_exit(validator_index: str,
                          epoch: str,
                          signature: str,
                          pubkey: str,
-                         chain_settings: BaseChainSetting) -> bool:
+                         chain_setting: BaseChainSetting) -> bool:
     bls_pubkey = BLSPubkey(bytes.fromhex(pubkey))
     bls_signature = BLSSignature(decode_hex(signature))
     message = VoluntaryExit(  # type: ignore[no-untyped-call]
@@ -387,8 +387,8 @@ def validate_signed_exit(validator_index: str,
     )
 
     domain = compute_voluntary_exit_domain(
-        fork_version=chain_settings.EXIT_FORK_VERSION,
-        genesis_validators_root=chain_settings.GENESIS_VALIDATORS_ROOT
+        fork_version=chain_setting.EXIT_FORK_VERSION,
+        genesis_validators_root=chain_setting.GENESIS_VALIDATORS_ROOT
     )
 
     signing_root = compute_signing_root(message, domain)
@@ -402,7 +402,7 @@ def validate_signed_exit(validator_index: str,
 
 def verify_bls_to_execution_change_keystore_json(file_folder: str,
                                                  pubkey: str,
-                                                 chain_settings: BaseChainSetting) -> bool:
+                                                 chain_setting: BaseChainSetting) -> bool:
     with open(file_folder, 'r', encoding='utf-8') as f:
         deposit_json: SignedBLSToExecutionChangeKeystore = json.load(f)
         signature = deposit_json["signature"]
@@ -411,14 +411,14 @@ def verify_bls_to_execution_change_keystore_json(file_folder: str,
                                                          message["to_execution_address"],
                                                          signature,
                                                          pubkey,
-                                                         chain_settings)
+                                                         chain_setting)
 
 
 def validate_bls_to_execution_change_keystore(validator_index: str,
                                               to_execution_address: str,
                                               signature: str,
                                               pubkey: str,
-                                              chain_settings: BaseChainSetting) -> bool:
+                                              chain_setting: BaseChainSetting) -> bool:
     bls_pubkey = BLSPubkey(bytes.fromhex(pubkey))
     bls_signature = BLSSignature(decode_hex(signature))
     message = BLSToExecutionChangeKeystore(  # type: ignore[no-untyped-call]
@@ -427,9 +427,62 @@ def validate_bls_to_execution_change_keystore(validator_index: str,
     )
 
     domain = compute_bls_to_execution_change_keystore_domain(
-        fork_version=chain_settings.GENESIS_FORK_VERSION,
-        genesis_validators_root=chain_settings.GENESIS_VALIDATORS_ROOT
+        fork_version=chain_setting.GENESIS_FORK_VERSION,
+        genesis_validators_root=chain_setting.GENESIS_VALIDATORS_ROOT
     )
 
     signing_root = compute_signing_root(message, domain)
     return bls.Verify(bls_pubkey, signing_root, bls_signature)
+
+
+#
+# Devnet Chain Setting Validation
+#
+
+def validate_devnet_chain_setting(ctx: click.Context, param: Any, value: Optional[str]) -> Optional[BaseChainSetting]:
+    if value is None:
+        return None
+
+    # Trimming to protect against unnecessaryly large JSON payload, see https://docs.python.org/3/library/json.html
+    trimmed_value = value[:400]
+
+    if validate_devnet_chain_setting_json(trimmed_value):
+        click.echo('\n%s\n' % load_text(['arg_devnet_chain_setting_warning']))
+        devnet_chain_setting_dict = json.loads(trimmed_value)
+        chain_setting = get_devnet_chain_setting(
+            network_name=devnet_chain_setting_dict['network_name'],
+            genesis_fork_version=devnet_chain_setting_dict['genesis_fork_version'],
+            exit_fork_version=devnet_chain_setting_dict['exit_fork_version'],
+            genesis_validator_root=devnet_chain_setting_dict.get('genesis_validator_root', None),
+        )
+        click.echo(str(chain_setting) + '\n')
+        return chain_setting
+    else:
+        raise ValidationError(load_text(['err_invalid_devnet_chain_setting']) + '\n')
+
+
+def validate_devnet_chain_setting_json(json_value: str) -> bool:
+    try:
+        devnet_chain_setting_dict = json.loads(json_value)
+
+        if not isinstance(devnet_chain_setting_dict, dict):
+            raise ValidationError(load_text(['err_devnet_chain_setting_not_object']) + '\n')
+
+        required_keys = ('network_name', 'genesis_fork_version', 'exit_fork_version')
+
+        all_keys = all(key in devnet_chain_setting_dict for key in required_keys)
+
+        if not all_keys:
+            raise ValidationError(load_text(['err_devnet_chain_setting_missing_keys']) + '\n')
+
+        if len(devnet_chain_setting_dict) not in (3, 4):
+            raise ValidationError(load_text(['err_devnet_chain_setting_key_length']) + '\n')
+
+        if len(devnet_chain_setting_dict) == 4 and 'genesis_validator_root' not in devnet_chain_setting_dict:
+            raise ValidationError(load_text(['err_devnet_chain_setting_invalid_fourth_key']) + '\n')
+
+        return True
+    except json.JSONDecodeError:
+        raise ValidationError(load_text(['err_devnet_chain_setting_invalid_json']) + '\n')
+
+        return False


Test patch:
diff --git a/tests/test_cli/test_existing_mnemonic.py b/tests/test_cli/test_existing_mnemonic.py
index 2e0f823b..bf3ba190 100644
--- a/tests/test_cli/test_existing_mnemonic.py
+++ b/tests/test_cli/test_existing_mnemonic.py
@@ -342,3 +342,57 @@ async def test_script_abbreviated_mnemonic() -> None:
 
     # Clean up
     clean_key_folder(my_folder_path)
+
+
+def test_existing_mnemonic_custom_testnet() -> None:
+    # Prepare folder
+    my_folder_path = os.path.join(os.getcwd(), 'TESTING_TEMP_FOLDER')
+    clean_key_folder(my_folder_path)
+    if not os.path.exists(my_folder_path):
+        os.mkdir(my_folder_path)
+
+    devnet_chain = {
+        "network_name": "holeskycopy",
+        "genesis_fork_version": "01017000",
+        "exit_fork_version": "04017000",
+        "genesis_validator_root": "9143aa7c615a7f7115e2b6aac319c03529df8242ae705fba9df39b79c59fa8b1"
+    }
+
+    devnet_chain_setting = json.dumps(devnet_chain)
+
+    runner = CliRunner()
+    inputs = [
+        'TREZOR',
+        'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about',
+        '2', '2', '5', 'MyPasswordIs', 'MyPasswordIs']
+    data = '\n'.join(inputs)
+    arguments = [
+        '--language', 'english',
+        '--ignore_connectivity',
+        'existing-mnemonic',
+        '--withdrawal_address', '',
+        '--folder', my_folder_path,
+        '--mnemonic_password', 'TREZOR',
+        '--devnet_chain_setting', devnet_chain_setting,
+    ]
+    result = runner.invoke(cli, arguments, input=data)
+
+    assert result.exit_code == 0
+
+    # Check files
+    validator_keys_folder_path = os.path.join(my_folder_path, DEFAULT_VALIDATOR_KEYS_FOLDER_NAME)
+    _, _, key_files = next(os.walk(validator_keys_folder_path))
+
+    all_uuid = [
+        get_uuid(validator_keys_folder_path + '/' + key_file)
+        for key_file in key_files
+        if key_file.startswith('keystore')
+    ]
+    assert len(set(all_uuid)) == 5
+
+    # Verify file permissions
+    if os.name == 'posix':
+        for file_name in key_files:
+            assert get_permissions(validator_keys_folder_path, file_name) == '0o440'
+    # Clean up
+    clean_key_folder(my_folder_path)
diff --git a/tests/test_cli/test_exit_transaction_keystore.py b/tests/test_cli/test_exit_transaction_keystore.py
index 2aa6f090..ed27b940 100644
--- a/tests/test_cli/test_exit_transaction_keystore.py
+++ b/tests/test_cli/test_exit_transaction_keystore.py
@@ -1,11 +1,12 @@
 import os
 import time
+import json
 
 from click.testing import CliRunner
 
 from ethstaker_deposit.credentials import Credential
 from ethstaker_deposit.deposit import cli
-from ethstaker_deposit.settings import get_chain_setting
+from ethstaker_deposit.settings import get_chain_setting, get_devnet_chain_setting
 from ethstaker_deposit.utils.constants import DEFAULT_EXIT_TRANSACTION_FOLDER_NAME
 from ethstaker_deposit.utils.intl import (
     load_text,
@@ -290,3 +291,74 @@ def test_invalid_keystore_password() -> None:
     ) in result.output
 
     clean_exit_transaction_folder(my_folder_path)
+
+
+def test_exit_transaction_keystore_custom_testnet() -> None:
+    # Prepare folder
+    my_folder_path = os.path.join(os.getcwd(), 'TESTING_TEMP_FOLDER')
+    exit_transaction_folder_path = os.path.join(my_folder_path, DEFAULT_EXIT_TRANSACTION_FOLDER_NAME)
+    clean_exit_transaction_folder(my_folder_path)
+    if not os.path.exists(my_folder_path):
+        os.mkdir(my_folder_path)
+    if not os.path.exists(exit_transaction_folder_path):
+        os.mkdir(exit_transaction_folder_path)
+
+    # Shared parameters
+    devnet_chain = {
+        "network_name": "holeskycopy",
+        "genesis_fork_version": "01017000",
+        "exit_fork_version": "04017000",
+        "genesis_validator_root": "9143aa7c615a7f7115e2b6aac319c03529df8242ae705fba9df39b79c59fa8b1"
+    }
+    devnet_chain_setting = json.dumps(devnet_chain)
+    keystore_password = 'solo-stakers'
+
+    # Prepare credential
+    credential = Credential(
+        mnemonic='aban aban aban aban aban aban aban aban aban aban aban abou',
+        mnemonic_password='',
+        index=0,
+        amount=0,
+        chain_setting=get_devnet_chain_setting(**devnet_chain),
+        hex_withdrawal_address=None
+    )
+
+    # Save keystore file
+    keystore_filepath = credential.save_signing_keystore(keystore_password, exit_transaction_folder_path, time.time())
+
+    runner = CliRunner()
+    arguments = [
+        '--language', 'english',
+        '--non_interactive',
+        'exit-transaction-keystore',
+        '--output_folder', my_folder_path,
+        '--devnet_chain_setting', devnet_chain_setting,
+        '--keystore', keystore_filepath,
+        '--keystore_password', keystore_password,
+        '--validator_index', '1',
+        '--epoch', '1234',
+    ]
+    result = runner.invoke(cli, arguments)
+
+    assert result.exit_code == 0
+
+    # Check files
+    _, _, exit_transaction_files = next(os.walk(exit_transaction_folder_path))
+
+    # Filter files to signed_exit as keystore file will exist as well
+    exit_transaction_file = [f for f in exit_transaction_files if 'signed_exit' in f]
+
+    assert len(set(exit_transaction_file)) == 1
+
+    json_data = read_json_file(exit_transaction_folder_path, exit_transaction_file[0])
+
+    # Verify file content
+    assert json_data['message']['epoch'] == '1234'
+    assert json_data['message']['validator_index'] == '1'
+    assert json_data['signature']
+
+    # Verify file permissions
+    verify_file_permission(os, folder_path=exit_transaction_folder_path, files=exit_transaction_file)
+
+    # Clean up
+    clean_exit_transaction_folder(my_folder_path)
diff --git a/tests/test_cli/test_exit_transaction_mnemonic.py b/tests/test_cli/test_exit_transaction_mnemonic.py
index 6eb2985c..537aa552 100644
--- a/tests/test_cli/test_exit_transaction_mnemonic.py
+++ b/tests/test_cli/test_exit_transaction_mnemonic.py
@@ -1,6 +1,7 @@
 import asyncio
 import os
 import pytest
+import json
 
 from click.testing import CliRunner
 
@@ -149,3 +150,56 @@ def test_exit_mnemonic_invalid_params(chain, mnemonic, start_index, indices, epo
     result = runner.invoke(cli, arguments, input=data)
 
     assert result.exit_code == assertion
+
+
+def test_exit_transaction_mnemonic_custom_network() -> None:
+    # Prepare folder
+    my_folder_path = os.path.join(os.getcwd(), 'TESTING_TEMP_FOLDER')
+    clean_exit_transaction_folder(my_folder_path)
+    if not os.path.exists(my_folder_path):
+        os.mkdir(my_folder_path)
+
+    devnet_chain = {
+        "network_name": "holeskycopy",
+        "genesis_fork_version": "01017000",
+        "exit_fork_version": "04017000",
+        "genesis_validator_root": "9143aa7c615a7f7115e2b6aac319c03529df8242ae705fba9df39b79c59fa8b1"
+    }
+    devnet_chain_setting = json.dumps(devnet_chain)
+
+    runner = CliRunner()
+    inputs = []
+    data = '\n'.join(inputs)
+    arguments = [
+        '--language', 'english',
+        '--non_interactive',
+        'exit-transaction-mnemonic',
+        '--output_folder', my_folder_path,
+        '--devnet_chain_setting', devnet_chain_setting,
+        '--mnemonic', 'aban aban aban aban aban aban aban aban aban aban aban abou',
+        '--validator_start_index', '0',
+        '--validator_indices', '1',
+        '--epoch', '1234',
+    ]
+    result = runner.invoke(cli, arguments, input=data)
+
+    assert result.exit_code == 0
+
+    # Check files
+    exit_transaction_folder_path = os.path.join(my_folder_path, DEFAULT_EXIT_TRANSACTION_FOLDER_NAME)
+    _, _, exit_transaction_files = next(os.walk(exit_transaction_folder_path))
+
+    assert len(set(exit_transaction_files)) == 1
+
+    json_data = read_json_file(exit_transaction_folder_path, exit_transaction_files[0])
+
+    # Verify file content
+    assert json_data['message']['epoch'] == '1234'
+    assert json_data['message']['validator_index'] == '1'
+    assert json_data['signature']
+
+    # Verify file permissions
+    verify_file_permission(os, folder_path=exit_transaction_folder_path, files=exit_transaction_files)
+
+    # Clean up
+    clean_exit_transaction_folder(my_folder_path)
diff --git a/tests/test_cli/test_generate_bls_to_execution_change.py b/tests/test_cli/test_generate_bls_to_execution_change.py
index 9ed1b13e..fe35168f 100644
--- a/tests/test_cli/test_generate_bls_to_execution_change.py
+++ b/tests/test_cli/test_generate_bls_to_execution_change.py
@@ -1,4 +1,5 @@
 import os
+import json
 
 from click.testing import CliRunner
 
@@ -120,3 +121,48 @@ def test_existing_mnemonic_bls_withdrawal_multiple() -> None:
 
     # Clean up
     clean_btec_folder(my_folder_path)
+
+
+def test_bls_change_custom_testnet() -> None:
+    # Prepare folder
+    my_folder_path = prepare_testing_folder(os)
+
+    devnet_chain = {
+        "network_name": "holeskycopy",
+        "genesis_fork_version": "01017000",
+        "exit_fork_version": "04017000",
+        "genesis_validator_root": "9143aa7c615a7f7115e2b6aac319c03529df8242ae705fba9df39b79c59fa8b1"
+    }
+
+    devnet_chain_setting = json.dumps(devnet_chain)
+
+    runner = CliRunner()
+    inputs = []
+    data = '\n'.join(inputs)
+    arguments = [
+        '--language', 'english',
+        '--non_interactive',
+        'generate-bls-to-execution-change',
+        '--bls_to_execution_changes_folder', my_folder_path,
+        '--devnet_chain_setting', devnet_chain_setting,
+        '--mnemonic', 'sister protect peanut hill ready work profit fit wish want small inflict flip member tail between sick setup bright duck morning sell paper worry',  # noqa: E501
+        '--bls_withdrawal_credentials_list', '0x00bd0b5a34de5fb17df08410b5e615dda87caf4fb72d0aac91ce5e52fc6aa8de',
+        '--validator_start_index', '0',
+        '--validator_indices', '1',
+        '--withdrawal_address', '0x3434343434343434343434343434343434343434',
+    ]
+    result = runner.invoke(cli, arguments, input=data)
+    assert result.exit_code == 0
+
+    # Check files
+    bls_to_execution_changes_folder_path = os.path.join(my_folder_path, DEFAULT_BLS_TO_EXECUTION_CHANGES_FOLDER_NAME)
+    _, _, btec_files = next(os.walk(bls_to_execution_changes_folder_path))
+
+    # TODO verify file content
+    assert len(set(btec_files)) == 1
+
+    # Verify file permissions
+    verify_file_permission(os, folder_path=bls_to_execution_changes_folder_path, files=btec_files)
+
+    # Clean up
+    clean_btec_folder(my_folder_path)
diff --git a/tests/test_cli/test_generate_bls_to_execution_change_keystore.py b/tests/test_cli/test_generate_bls_to_execution_change_keystore.py
index 061ad265..9859f971 100644
--- a/tests/test_cli/test_generate_bls_to_execution_change_keystore.py
+++ b/tests/test_cli/test_generate_bls_to_execution_change_keystore.py
@@ -1,11 +1,12 @@
 import os
 import time
+import json
 
 from click.testing import CliRunner
 
 from ethstaker_deposit.credentials import Credential
 from ethstaker_deposit.deposit import cli
-from ethstaker_deposit.settings import get_chain_setting
+from ethstaker_deposit.settings import get_chain_setting, get_devnet_chain_setting
 from ethstaker_deposit.utils.constants import DEFAULT_BLS_TO_EXECUTION_CHANGES_KEYSTORE_FOLDER_NAME
 from ethstaker_deposit.utils.intl import load_text
 from .helpers import (
@@ -278,3 +279,122 @@ def test_invalid_keystore_password() -> None:
     ) in result.output
 
     clean_btec_keystore_folder(my_folder_path)
+
+
+def test_bls_change_keystore_custom_testnet() -> None:
+    my_folder_path = prepare_testing_folder(os)
+    changes_folder_path = os.path.join(my_folder_path, DEFAULT_BLS_TO_EXECUTION_CHANGES_KEYSTORE_FOLDER_NAME)
+
+    clean_key_folder(my_folder_path)
+    if not os.path.exists(my_folder_path):
+        os.mkdir(my_folder_path)
+    if not os.path.exists(changes_folder_path):
+        os.mkdir(changes_folder_path)
+
+    devnet_chain = {
+        "network_name": "holeskycopy",
+        "genesis_fork_version": "01017000",
+        "exit_fork_version": "04017000",
+        "genesis_validator_root": "9143aa7c615a7f7115e2b6aac319c03529df8242ae705fba9df39b79c59fa8b1"
+    }
+
+    devnet_chain_setting = json.dumps(devnet_chain)
+    keystore_password = 'solo-stakers'
+
+    credential = Credential(
+        mnemonic='aban aban aban aban aban aban aban aban aban aban aban abou',
+        mnemonic_password='',
+        index=0,
+        amount=0,
+        chain_setting=get_devnet_chain_setting(**devnet_chain),
+        hex_withdrawal_address=None
+    )
+
+    keystore_filepath = credential.save_signing_keystore(keystore_password, changes_folder_path, time.time())
+
+    runner = CliRunner()
+    arguments = [
+        '--language', 'english',
+        '--non_interactive',
+        'generate-bls-to-execution-change-keystore',
+        '--output_folder', my_folder_path,
+        '--devnet_chain_setting', devnet_chain_setting,
+        '--keystore', keystore_filepath,
+        '--keystore_password', keystore_password,
+        '--validator_index', '1',
+        '--withdrawal_address', '0xcd60A5f152724480c3a95E4Ff4dacEEf4074854d',
+    ]
+    result = runner.invoke(cli, arguments)
+
+    assert result.exit_code == 0
+
+    _, _, files = next(os.walk(changes_folder_path))
+
+    change_files = [f for f in files if 'bls_to_execution_change_keystore_' in f]
+
+    assert len(set(change_files)) == 1
+
+    json_data = read_json_file(changes_folder_path, change_files[0])
+
+    assert json_data['message']['to_execution_address'] == '0xcd60a5f152724480c3a95e4ff4daceef4074854d'
+    assert json_data['message']['validator_index'] == 1
+    assert json_data['signature']
+
+    verify_file_permission(os, folder_path=changes_folder_path, files=change_files)
+
+    clean_btec_keystore_folder(my_folder_path)
+
+
+def test_invalid_custom_testnet() -> None:
+    my_folder_path = os.path.join(os.getcwd(), 'TESTING_TEMP_FOLDER')
+    changes_folder_path = os.path.join(my_folder_path, DEFAULT_BLS_TO_EXECUTION_CHANGES_KEYSTORE_FOLDER_NAME)
+
+    clean_key_folder(my_folder_path)
+    if not os.path.exists(my_folder_path):
+        os.mkdir(my_folder_path)
+    if not os.path.exists(changes_folder_path):
+        os.mkdir(changes_folder_path)
+
+    devnet_chain = {
+        "network_name": "holeskycopy",
+        "exit_fork_version": "04017000",
+    }
+
+    correct_devnet_chain = {
+        "network_name": "holeskycopy",
+        "genesis_fork_version": "01017000",
+        "exit_fork_version": "04017000",
+        "genesis_validator_root": "9143aa7c615a7f7115e2b6aac319c03529df8242ae705fba9df39b79c59fa8b1"
+    }
+
+    devnet_chain_setting = json.dumps(devnet_chain)
+    keystore_password = 'solo-stakers'
+
+    credential = Credential(
+        mnemonic='aban aban aban aban aban aban aban aban aban aban aban abou',
+        mnemonic_password='',
+        index=0,
+        amount=0,
+        chain_setting=get_devnet_chain_setting(**correct_devnet_chain),
+        hex_withdrawal_address=None
+    )
+
+    keystore_filepath = credential.save_signing_keystore(keystore_password, changes_folder_path, time.time())
+
+    runner = CliRunner()
+    arguments = [
+        '--language', 'english',
+        '--non_interactive',
+        'generate-bls-to-execution-change-keystore',
+        '--output_folder', my_folder_path,
+        '--devnet_chain_setting', devnet_chain_setting,
+        '--keystore', keystore_filepath,
+        '--keystore_password', keystore_password,
+        '--validator_index', '1',
+        '--withdrawal_address', '0xcd60A5f152724480c3a95E4Ff4dacEEf4074854d',
+    ]
+    result = runner.invoke(cli, arguments)
+
+    assert result.exit_code == 1
+
+    clean_btec_keystore_folder(my_folder_path)
diff --git a/tests/test_cli/test_new_mnemonic.py b/tests/test_cli/test_new_mnemonic.py
index 3185bfb8..f8206de7 100644
--- a/tests/test_cli/test_new_mnemonic.py
+++ b/tests/test_cli/test_new_mnemonic.py
@@ -604,3 +604,59 @@ async def test_script_abbreviated_mnemonic() -> None:
 
     # Clean up
     clean_key_folder(my_folder_path)
+
+
+def test_new_mnemonic_custom_testnet(monkeypatch) -> None:
+    # monkeypatch get_mnemonic
+    def mock_get_mnemonic(language, words_path, entropy=None) -> str:
+        return "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
+
+    monkeypatch.setattr(new_mnemonic, "get_mnemonic", mock_get_mnemonic)
+
+    # Prepare folder
+    my_folder_path = os.path.join(os.getcwd(), 'TESTING_TEMP_FOLDER')
+    clean_key_folder(my_folder_path)
+    if not os.path.exists(my_folder_path):
+        os.mkdir(my_folder_path)
+
+    devnet_chain = {
+        "network_name": "holeskycopy",
+        "genesis_fork_version": "01017000",
+        "exit_fork_version": "04017000",
+        "genesis_validator_root": "9143aa7c615a7f7115e2b6aac319c03529df8242ae705fba9df39b79c59fa8b1"
+    }
+
+    devnet_chain_setting = json.dumps(devnet_chain)
+
+    runner = CliRunner()
+    inputs = ['english', 'english', '1', 'MyPasswordIs', 'MyPasswordIs',
+              'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about']
+    data = '\n'.join(inputs)
+    arguments = [
+        '--ignore_connectivity',
+        'new-mnemonic',
+        '--withdrawal_address', '',
+        '--folder', my_folder_path,
+        '--devnet_chain_setting', devnet_chain_setting,
+    ]
+    result = runner.invoke(cli, arguments, input=data)
+    assert result.exit_code == 0
+
+    # Check files
+    validator_keys_folder_path = os.path.join(my_folder_path, DEFAULT_VALIDATOR_KEYS_FOLDER_NAME)
+    _, _, key_files = next(os.walk(validator_keys_folder_path))
+
+    all_uuid = [
+        get_uuid(validator_keys_folder_path + '/' + key_file)
+        for key_file in key_files
+        if key_file.startswith('keystore')
+    ]
+    assert len(set(all_uuid)) == 1
+
+    # Verify file permissions
+    if os.name == 'posix':
+        for file_name in key_files:
+            assert get_permissions(validator_keys_folder_path, file_name) == '0o440'
+
+    # Clean up
+    clean_key_folder(my_folder_path)
diff --git a/tests/test_cli/test_partial_deposit.py b/tests/test_cli/test_partial_deposit.py
index 0b53782f..8ab9ce12 100644
--- a/tests/test_cli/test_partial_deposit.py
+++ b/tests/test_cli/test_partial_deposit.py
@@ -9,7 +9,7 @@
 
 from ethstaker_deposit.credentials import Credential
 from ethstaker_deposit.deposit import cli
-from ethstaker_deposit.settings import get_chain_setting
+from ethstaker_deposit.settings import get_chain_setting, get_devnet_chain_setting
 from ethstaker_deposit.utils.constants import (
     DEFAULT_PARTIAL_DEPOSIT_FOLDER_NAME,
     DEFAULT_VALIDATOR_KEYS_FOLDER_NAME,
@@ -222,3 +222,117 @@ def test_partial_deposit_does_not_match_if_amount_differs() -> None:
 
     clean_folder(my_folder_path, validator_key_folder, True)
     clean_partial_deposit_folder(my_folder_path)
+
+
+@pytest.mark.parametrize(
+    'amount',
+    [
+        ("32"),
+        ("1"),
+    ]
+)
+def test_partial_deposit_custom_network(amount: str) -> None:
+    my_folder_path = os.path.join(os.getcwd(), 'TESTING_TEMP_FOLDER')
+    partial_deposit_folder = os.path.join(my_folder_path, DEFAULT_PARTIAL_DEPOSIT_FOLDER_NAME)
+    clean_partial_deposit_folder(my_folder_path)
+    if not os.path.exists(my_folder_path):
+        os.mkdir(my_folder_path)
+    if not os.path.exists(partial_deposit_folder):
+        os.mkdir(partial_deposit_folder)
+
+    devnet_chain = {
+        "network_name": "holeskycopy",
+        "genesis_fork_version": "01017000",
+        "exit_fork_version": "04017000",
+        "genesis_validator_root": "9143aa7c615a7f7115e2b6aac319c03529df8242ae705fba9df39b79c59fa8b1"
+    }
+    devnet_chain_setting = json.dumps(devnet_chain)
+
+    password = "MyPasswordIs"
+    withdrawal_address = "0xcd60A5f152724480c3a95E4Ff4dacEEf4074854d"
+    mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
+
+    credential = Credential(mnemonic=mnemonic,
+                            mnemonic_password="",
+                            index=0,
+                            amount=32000000000,
+                            chain_setting=get_devnet_chain_setting(**devnet_chain),
+                            hex_withdrawal_address=to_normalized_address(withdrawal_address))
+
+    keystore_file_folder = credential.save_signing_keystore(password, partial_deposit_folder, time.time())
+
+    runner = CliRunner()
+    inputs = ['english', password, amount, withdrawal_address, withdrawal_address]
+    data = '\n'.join(inputs)
+    arguments = [
+        '--ignore_connectivity',
+        'partial-deposit',
+        '--keystore', keystore_file_folder,
+        '--output_folder', my_folder_path,
+        '--devnet_chain_setting', devnet_chain_setting,
+    ]
+    result = runner.invoke(cli, arguments, input=data)
+    assert result.exit_code == 0
+
+    _, _, folder_files = next(os.walk(partial_deposit_folder))
+
+    deposit_files = [deposit_file for deposit_file in folder_files if deposit_file.startswith('deposit')]
+
+    assert len(deposit_files) == 1
+
+    if os.name == 'posix':
+        assert get_permissions(partial_deposit_folder, deposit_files[0]) == '0o440'
+
+    clean_partial_deposit_folder(my_folder_path)
+
+
+def test_invalid_custom_network_json() -> None:
+    my_folder_path = os.path.join(os.getcwd(), 'TESTING_TEMP_FOLDER')
+    partial_deposit_folder = os.path.join(my_folder_path, DEFAULT_PARTIAL_DEPOSIT_FOLDER_NAME)
+    clean_partial_deposit_folder(my_folder_path)
+    if not os.path.exists(my_folder_path):
+        os.mkdir(my_folder_path)
+    if not os.path.exists(partial_deposit_folder):
+        os.mkdir(partial_deposit_folder)
+
+    devnet_chain = {
+        "network_name": "holeskycopy",
+        "genesis_fork_version": "01017000",
+        "exit_fork_version": "04017000",
+        "genesis_validator_root": "9143aa7c615a7f7115e2b6aac319c03529df8242ae705fba9df39b79c59fa8b1"
+    }
+    devnet_chain_setting = 'abcd'
+
+    password = "MyPasswordIs"
+    withdrawal_address = "0xcd60A5f152724480c3a95E4Ff4dacEEf4074854d"
+    mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
+
+    credential = Credential(mnemonic=mnemonic,
+                            mnemonic_password="",
+                            index=0,
+                            amount=32000000000,
+                            chain_setting=get_devnet_chain_setting(**devnet_chain),
+                            hex_withdrawal_address=to_normalized_address(withdrawal_address))
+
+    keystore_file_folder = credential.save_signing_keystore(password, partial_deposit_folder, time.time())
+
+    runner = CliRunner()
+    inputs = ['english', password, '32', withdrawal_address, withdrawal_address]
+    data = '\n'.join(inputs)
+    arguments = [
+        '--ignore_connectivity',
+        'partial-deposit',
+        '--keystore', keystore_file_folder,
+        '--output_folder', my_folder_path,
+        '--devnet_chain_setting', devnet_chain_setting,
+    ]
+    result = runner.invoke(cli, arguments, input=data)
+    assert result.exit_code == 1
+
+    _, _, folder_files = next(os.walk(partial_deposit_folder))
+
+    deposit_files = [deposit_file for deposit_file in folder_files if deposit_file.startswith('deposit')]
+
+    assert len(deposit_files) == 0
+
+    clean_partial_deposit_folder(my_folder_path)
diff --git a/tests/test_utils/test_validation.py b/tests/test_utils/test_validation.py
index 8addf301..1350fa09 100644
--- a/tests/test_utils/test_validation.py
+++ b/tests/test_utils/test_validation.py
@@ -1,5 +1,6 @@
 import pytest
 import sys
+import json
 from typing import (
     Any,
 )
@@ -12,6 +13,7 @@
     validate_partial_deposit_amount,
     validate_password_strength,
     validate_signed_exit,
+    validate_devnet_chain_setting_json,
 )
 
 
@@ -154,6 +156,71 @@ def test_normalize_input_list(input, result):
 def test_validate_signed_exit(
     chain: str, epoch: int, validator_index: int, pubkey: str, signature: str, result: bool
 ) -> None:
-    chain_settings = get_chain_setting(chain)
-
-    assert validate_signed_exit(validator_index, epoch, signature, pubkey, chain_settings) == result
+    chain_setting = get_chain_setting(chain)
+
+    assert validate_signed_exit(validator_index, epoch, signature, pubkey, chain_setting) == result
+
+
+def test_validate_devnet_chain_setting_json() -> None:
+    # Correct devnet chain value
+    corret_devnet_chain = {
+        "network_name": "holeskycopy",
+        "genesis_fork_version": "01017000",
+        "exit_fork_version": "04017000",
+        "genesis_validator_root": "9143aa7c615a7f7115e2b6aac319c03529df8242ae705fba9df39b79c59fa8b1"
+    }
+    assert validate_devnet_chain_setting_json(json.dumps(corret_devnet_chain)) is True
+
+    # Invalid devnet chain value missing 1 required key
+    missing_1_key_devnet_chain = {
+        "network_name": "holeskycopy",
+        "exit_fork_version": "04017000",
+        "genesis_validator_root": "9143aa7c615a7f7115e2b6aac319c03529df8242ae705fba9df39b79c59fa8b1"
+    }
+    with pytest.raises(ValidationError):
+        assert validate_devnet_chain_setting_json(json.dumps(missing_1_key_devnet_chain)) is False
+
+    # Invalid devnet chain value missing 2 required key
+    missing_2_keys_devnet_chain = {
+        "exit_fork_version": "04017000",
+        "genesis_validator_root": "9143aa7c615a7f7115e2b6aac319c03529df8242ae705fba9df39b79c59fa8b1"
+    }
+    with pytest.raises(ValidationError):
+        assert validate_devnet_chain_setting_json(json.dumps(missing_2_keys_devnet_chain)) is False
+
+    # Correct devnet chain value missing 1 optional key
+    correct_missing_genesis_validator_root_devnet_chain = {
+        "network_name": "holeskycopy",
+        "genesis_fork_version": "01017000",
+        "exit_fork_version": "04017000"
+    }
+    assert validate_devnet_chain_setting_json(json.dumps(correct_missing_genesis_validator_root_devnet_chain)) is True
+
+    # Invalid devnet chain value with wrong fourth key name
+    invalid_fourth_key_devnet_chain = {
+        "network_name": "holeskycopy",
+        "genesis_fork_version": "01017000",
+        "exit_fork_version": "04017000",
+        "genesis_validator": "9143aa7c615a7f7115e2b6aac319c03529df8242ae705fba9df39b79c59fa8b1"
+    }
+    with pytest.raises(ValidationError):
+        assert validate_devnet_chain_setting_json(json.dumps(invalid_fourth_key_devnet_chain)) is False
+
+    # Invalid devnet chain value with too many keys
+    invalid_too_many_keys_devnet_chain = {
+        "network_name": "holeskycopy",
+        "genesis_fork_version": "01017000",
+        "exit_fork_version": "04017000",
+        "genesis_validator_root": "9143aa7c615a7f7115e2b6aac319c03529df8242ae705fba9df39b79c59fa8b1",
+        "more_key": "value"
+    }
+    with pytest.raises(ValidationError):
+        assert validate_devnet_chain_setting_json(json.dumps(invalid_too_many_keys_devnet_chain)) is False
+
+    # Invalid devnet chain with invalid JSON string
+    with pytest.raises(ValidationError):
+        assert validate_devnet_chain_setting_json('aaz') is False
+
+    # Invalid devnet chain with missing root JSON object
+    with pytest.raises(ValidationError):
+        assert validate_devnet_chain_setting_json('[1, 2, 3]') is False