Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

make defaults contextful #269

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 16 additions & 4 deletions ethstaker_deposit/cli/generate_keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,12 +133,11 @@ def generate_keys_arguments_decorator(function: Callable[..., Any]) -> Callable[
),
jit_option(
callback=captive_prompt_callback(
lambda amount: validate_deposit_amount(amount),
lambda: load_text(['arg_amount', 'prompt'], func='generate_keys_arguments_decorator'),
default=str(min_activation_amount_eth),
lambda amount, **kwargs: validate_deposit_amount(amount, **kwargs),
get_amount_prompt_from_template,
prompt_if=prompt_if_other_value('compounding', True),
prompt_marker="amount",
),
default=str(min_activation_amount_eth),
help=lambda: load_text(['arg_amount', 'help'], func='generate_keys_arguments_decorator'),
param_decls='--amount',
prompt=False, # the callback handles the prompt
Expand All @@ -162,6 +161,17 @@ def generate_keys_arguments_decorator(function: Callable[..., Any]) -> Callable[
function = decorator(function)
return function

def get_amount_prompt_from_template() -> str:
ctx = click.get_current_context(silent=True)
chain = ctx.params.get('chain', 'mainnet') if ctx is not None else 'mainnet'
chain_setting = get_chain_setting(chain)
min_deposit = chain_setting.MINIMUM_COMPOUNDING_DEPOSIT if ctx.params.get('compounding', False) else 1
multiplier = chain_setting.MULPLIER if ctx.params.get('compounding', False) else 1
activation_amount = str(int(32/multiplier))
template = load_text(['arg_amount', 'prompt'], func='generate_keys_arguments_decorator')
return template.format(min_deposit=min_deposit, activation_amount=activation_amount)



@click.command()
@click.pass_context
Expand All @@ -179,6 +189,8 @@ def generate_keys(ctx: click.Context, validator_start_index: int,
# Get chain setting
chain_setting = devnet_chain_setting if devnet_chain_setting is not None else get_chain_setting(chain)

amounts = [amount * chain_setting.MULPLIER for amount in amounts]

if not os.path.exists(folder):
os.mkdir(folder)
clear_terminal()
Expand Down
2 changes: 1 addition & 1 deletion ethstaker_deposit/cli/partial_deposit.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@
)
@jit_option(
callback=captive_prompt_callback(
lambda amount: validate_deposit_amount(amount),
lambda amount, **kwargs: validate_deposit_amount(amount, **kwargs),
lambda: load_text(['arg_partial_deposit_amount', 'prompt'], func=FUNC_NAME),
default="32",
prompt_if=prompt_if_none,
Expand Down
2 changes: 1 addition & 1 deletion ethstaker_deposit/intl/en/cli/generate_keys.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
},
"arg_amount": {
"help": "The amount to deposit to these validators in ether denomination. Must be at least 1 ether and can not have greater precision than 1 gwei. Use of this option requires compounding validators.",
"prompt": "Please enter the amount you wish to deposit to these validators. Must be at least 1 ether and can not have greater precision than 1 gwei. 32 is required to activate a new validator"
"prompt": "Please enter the amount you wish to deposit to these validators. Must be at least {min_deposit} and can not have greater precision than 1 gwei. {activation_amount} is required to activate a new validator"
},
"arg_pbkdf2": {
"help": "Uses the pbkdf2 hashing function instead of scrypt for generated keystore files. "
Expand Down
14 changes: 11 additions & 3 deletions ethstaker_deposit/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,17 @@ class BaseChainSetting(NamedTuple):
GENESIS_FORK_VERSION: bytes
EXIT_FORK_VERSION: bytes # capella fork version for voluntary exits (EIP-7044)
GENESIS_VALIDATORS_ROOT: Optional[bytes] = None
MULPLIER: int = 1
MINIMUM_COMPOUNDING_DEPOSIT: int = 1

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}')
f' - Genesis validators root: {gvr_value}\n'
f' - Multiplier: {self.MULPLIER}\n'
f' - Minimum compounding deposit: {self.MINIMUM_COMPOUNDING_DEPOSIT}')


MAINNET = 'mainnet'
Expand Down Expand Up @@ -73,13 +77,17 @@ def __str__(self) -> str:
NETWORK_NAME=GNOSIS,
GENESIS_FORK_VERSION=bytes.fromhex('00000064'),
EXIT_FORK_VERSION=bytes.fromhex('03000064'),
GENESIS_VALIDATORS_ROOT=bytes.fromhex('f5dcb5564e829aab27264b9becd5dfaa017085611224cb3036f573368dbb9d47'))
GENESIS_VALIDATORS_ROOT=bytes.fromhex('f5dcb5564e829aab27264b9becd5dfaa017085611224cb3036f573368dbb9d47'),
MULPLIER=32,
MINIMUM_COMPOUNDING_DEPOSIT=0.03125)
# Chiado setting
ChiadoSetting = BaseChainSetting(
NETWORK_NAME=CHIADO,
GENESIS_FORK_VERSION=bytes.fromhex('0000006f'),
EXIT_FORK_VERSION=bytes.fromhex('0300006f'),
GENESIS_VALIDATORS_ROOT=bytes.fromhex('9d642dac73058fbf39c0ae41ab1e34e4d889043cb199851ded7095bc99eb4c1e'))
GENESIS_VALIDATORS_ROOT=bytes.fromhex('9d642dac73058fbf39c0ae41ab1e34e4d889043cb199851ded7095bc99eb4c1e'),
MULPLIER=32,
MINIMUM_COMPOUNDING_DEPOSIT=0.03125)


ALL_CHAINS: Dict[str, BaseChainSetting] = {
Expand Down
25 changes: 20 additions & 5 deletions ethstaker_deposit/utils/click.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from ethstaker_deposit.exceptions import ValidationError
from ethstaker_deposit.utils import config
# To work around an issue with disabling language prompt and CLIRunner() isolation
from ethstaker_deposit.utils.constants import INTL_LANG_OPTIONS
from ethstaker_deposit.utils.constants import CONTEXT_REQUIRING_PROMPTS, INTL_LANG_OPTIONS, get_min_activation_amount
from ethstaker_deposit.utils.intl import (
get_first_options,
)
Expand Down Expand Up @@ -63,6 +63,8 @@ def get_help_record(self, ctx: click.Context) -> Tuple[str, str]:

def get_default(self, ctx: click.Context, call: bool = True) -> Any:
self.default = _value_of(self.callable_default)
if self.name == "amount":
self.default = get_min_activation_amount(ctx.params.get('chain', 'mainnet'))
return super().get_default(ctx, call)


Expand Down Expand Up @@ -126,6 +128,15 @@ def callback(ctx: click.Context, param: Any, user_input: str) -> bool:
return callback


def process_with_optional_context(ctx: click.Context, processing_func: Callable[[str], Any], user_input: str, prompt_marker: str) -> Any:
'''
Processes the user's input with the optional context if the prompt requires it.
'''
if prompt_marker in CONTEXT_REQUIRING_PROMPTS:
return processing_func(user_input, params=ctx.params)
return processing_func(user_input)


def captive_prompt_callback(
processing_func: Callable[[str], Any],
prompt: Callable[[], str],
Expand All @@ -134,6 +145,7 @@ def captive_prompt_callback(
hide_input: bool = False,
default: Optional[Union[Callable[[], str], str]] = None,
prompt_if: Optional[Callable[[click.Context, Any, str], bool]] = None,
prompt_marker: str = '',
) -> Callable[[click.Context, str, str], Any]:
'''
Traps the user in a prompt until the value chosen is acceptable
Expand All @@ -147,25 +159,28 @@ def captive_prompt_callback(
entered by the user
:param prompt_if: the optional callable, prompt if the source of the parameter is from the default value and this
call returns true
:param prompt_marker: the optional marker to indicate the type of prompt, for example "amount"
'''
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
# To avoid showing confirmation prompt twice, we introduce a flag to prompt inside
# the callback
# See https://github.com/pallets/click/discussions/2673
default_value = _value_of(default) if default is not None else param.default
if (prompt_if is not None
and ctx.get_parameter_source(param.name) == click.core.ParameterSource.DEFAULT
and prompt_if(ctx, param, user_input)):
user_input = click.prompt(prompt(), hide_input=hide_input, default=_value_of(default))
user_input = click.prompt(prompt(), hide_input=hide_input, default=default_value)
if config.non_interactive:
return processing_func(user_input)
return process_with_optional_context(ctx, processing_func, user_input)
while True:
try:
processed_input = processing_func(user_input)
processed_input = process_with_optional_context(ctx, processing_func, user_input, prompt_marker)
# Logic for confirming user input:
if confirmation_prompt is not None and processed_input not in ('', None):
confirmation_input = click.prompt(confirmation_prompt(), hide_input=hide_input)
if processing_func(confirmation_input) != processed_input:
processed_value = process_with_optional_context(ctx, processing_func, confirmation_input, prompt_marker)
if processed_value != processed_input:
raise ValidationError(confirmation_mismatch_msg())
return processed_input
except ValidationError as e:
Expand Down
19 changes: 19 additions & 0 deletions ethstaker_deposit/utils/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,25 @@
INTL_CONTENT_PATH = os.path.join('ethstaker_deposit', 'intl')


CHAIN_MIN_ACTIVATION_OVERRIDES: Dict[str, int] = {
'chiado': 1 * ETH2GWEI,
'gnosis': 1 * ETH2GWEI,
}


CONTEXT_REQUIRING_PROMPTS = [
"amount",
]


def get_min_activation_amount(chain: str) -> int:
"""
Returns the minimum activation amount for the specified chain.
Defaults to 32 ETH unless overridden for a specific chain.
"""
return CHAIN_MIN_ACTIVATION_OVERRIDES.get(chain, MIN_ACTIVATION_AMOUNT) // ETH2GWEI


def _add_index_to_options(d: Dict[str, list[str]]) -> Dict[str, list[str]]:
'''
Adds the (1 indexed) index (in the dict) to the first element of value list.
Expand Down
11 changes: 8 additions & 3 deletions ethstaker_deposit/utils/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
MAX_DEPOSIT_AMOUNT,
)
from ethstaker_deposit.utils.crypto import SHA256
from ethstaker_deposit.settings import BaseChainSetting, get_devnet_chain_setting
from ethstaker_deposit.settings import BaseChainSetting, get_chain_setting, get_devnet_chain_setting


#
Expand Down Expand Up @@ -198,7 +198,7 @@ def validate_yesno(ctx: click.Context, param: Any, value: str) -> bool:
raise ValidationError(load_text(['err_invalid_bool_value']))


def validate_deposit_amount(amount: str) -> int:
def validate_deposit_amount(amount: str, **kwargs) -> int:
'''
Verifies that `amount` is a valid gwei denomination and 1 ether <= amount <= MAX_DEPOSIT_AMOUNT gwei
Amount is expected to be in ether and the returned value will be converted to gwei and represented as an int
Expand All @@ -207,10 +207,15 @@ def validate_deposit_amount(amount: str) -> int:
decimal_ether = Decimal(amount)
amount_gwei = decimal_ether * Decimal(ETH2GWEI)

params = kwargs.get('params', {})
chain = params.get('chain', 'mainnet')
chain_setting = get_chain_setting(chain)
min_amount = chain_setting.MINIMUM_COMPOUNDING_DEPOSIT if params.get('compounding', False) else 1

if amount_gwei % 1 != 0:
raise ValidationError(load_text(['err_not_gwei_denomination']))

if amount_gwei < 1 * ETH2GWEI:
if amount_gwei < min_amount * ETH2GWEI:
raise ValidationError(load_text(['err_min_deposit']))

if amount_gwei > MAX_DEPOSIT_AMOUNT:
Expand Down