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

Add generate_bls_to_execution_change #313

Merged
merged 33 commits into from
Mar 13, 2023
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
d3fd1da
Extract `load_mnemonic_arguments_decorator` func
hwwhww Jan 4, 2023
7404b69
[WIP] Add generate_bls_to_execution_change
hwwhww Jan 6, 2023
d68a1c7
Always use `GENESIS_FORK_VERSION` to sign BLSToExecutionChange and fi…
hwwhww Jan 15, 2023
6a4ba11
Add `0x` prefix to the bytes type fields of the output JSON file
hwwhww Jan 15, 2023
47bf1aa
WIP validation
hwwhww Jan 16, 2023
63fdc5e
Add `--devnet_chain_setting` for devnet testing
hwwhww Jan 16, 2023
c5651ea
Improve `validate_bls_to_execution_change`
hwwhww Jan 16, 2023
2b5c218
Add arguments description to README.md
hwwhww Jan 16, 2023
1f8bab5
Echo the folder path
hwwhww Jan 16, 2023
f5e2803
Arrange JSON fields by adding `metadata`
hwwhww Jan 16, 2023
245ee7b
Normalize input `bls_withdrawal_credentials` and add `test_existing_m…
hwwhww Jan 22, 2023
3b7e144
Generate multiple changes
hwwhww Jan 23, 2023
676279e
Add checksum address validation
hwwhww Jan 23, 2023
75e8ea0
Add test case of multiple validator indices
hwwhww Jan 23, 2023
0be70aa
Update README
hwwhww Jan 23, 2023
962ce8c
Add retype confirmation and fix typo
hwwhww Jan 26, 2023
1912868
Remove shutdown testnets and fill Sepolia and Goerli `genesis_validat…
hwwhww Jan 27, 2023
c9b4e01
Merge branch 'dev' into bls-to-execution-change
hwwhww Feb 1, 2023
33f847b
Update README.md
hwwhww Feb 10, 2023
7df39ba
Apply @james-prysm's suggestions
hwwhww Feb 10, 2023
27af99f
Update param description
hwwhww Feb 10, 2023
9b86724
Run test_btec_script.py in CI
hwwhww Feb 10, 2023
ca5062a
Kick the cache
hwwhww Feb 10, 2023
d83c312
Run test_binary_btec_script.py in CI
hwwhww Feb 10, 2023
2c3fb22
Strip brackets and extra spaces
hwwhww Feb 13, 2023
db3e628
fix error message grammar
JohnWestlund Feb 18, 2023
2cd1d99
Merge pull request #332 from JohnWestlund/bls-to-execution-change
hwwhww Mar 10, 2023
aae55a6
Apply PR feedback by catching the exceptions. Improve error handling …
hwwhww Mar 13, 2023
f79bbac
Improve error handling
hwwhww Mar 13, 2023
9aed027
Confirm deposit `eth1_withdrawal_address` input
hwwhww Mar 13, 2023
ee052fa
Add more description of mnemonic language per @infosecual's suggestion
hwwhww Mar 13, 2023
69f778d
Make `--non_interactive` flag visible and add notes in docs
hwwhww Mar 13, 2023
ed092fc
clean folders first
hwwhww Mar 13, 2023
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
validator_keys
bls_to_execution_changes

# Python testing & linting:
build/
Expand Down
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
- [`new-mnemonic` Arguments](#new-mnemonic-arguments)
- [`existing-mnemonic` Arguments](#existing-mnemonic-arguments)
- [Successful message](#successful-message)
- [`generate-bls-to-execution-change` Arguments](#generate-bls-to-execution-change-arguments)
- [Option 2. Build `deposit-cli` with native Python](#option-2-build-deposit-cli-with-native-python)
- [Step 0. Python version checking](#step-0-python-version-checking)
- [Step 1. Installation](#step-1-installation-1)
Expand Down Expand Up @@ -128,6 +129,7 @@ The CLI offers different commands depending on what you want to do with the tool
| ------- | ----------- |
| `new-mnemonic` | (Recommended) This command is used to generate keystores with a new mnemonic. |
| `existing-mnemonic` | This command is used to re-generate or derive new keys from your existing mnemonic. Use this command, if (i) you have already generated keys with this CLI before, (ii) you want to reuse your mnemonic that you know is secure that you generated elsewhere (reusing your eth1 mnemonic .etc), or (iii) you lost your keystores and need to recover your keys. |
| ``
hwwhww marked this conversation as resolved.
Show resolved Hide resolved

###### `new-mnemonic` Arguments

Expand Down Expand Up @@ -168,6 +170,22 @@ Success!
Your keys can be found at: <YOUR_FOLDER_PATH>
```

###### `generate-bls-to-execution-change` Arguments

You can use `bls-to-execution-change --help` to see all arguments. Note that if there are missing arguments that the CLI needs, it will ask you for them.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you try to run the bls-to-execution-change subcommand with --help it will actually skip the help menu and drop right into interactive mode:

user@laptop:~/repos/staking-deposit-cli$ python3 ./staking_deposit/deposit.py generate-bls-to-execution-change --help

***Using the tool on an offline and secure device is highly recommended to keep your mnemonic safe.***

Please choose your language ['1. العربية', '2. ελληνικά', '3. English', '4. Français', '5. Bahasa melayu', '6. Italiano', '7. 日本語', '8. 한국어', '9. Português do Brasil', '10. român', '11. Türkçe', '12. 简体中文']:  [English]: 


| Argument | Type | Description |
| -------- | -------- | -------- |
| `--bls_to_execution_changes_folder` | String. Pointing to `./bls_to_execution_changes` by default | The folder path for the `bls_to_execution_change-*` JSON file(s) |
| `--chain` | String. `mainnet` by default | The chain setting for the signing domain. |
| `--mnemonic` | String. mnemonic split by space. | The mnemonic you used to create withdrawal credentials. |
| `--mnemonic_password` | Optional string. Empty by default. | The mnemonic password you used in your key generation. Note: It's not the keystore password. |
| `--validator_start_index` | Non-negative integer | The index of the first validator's keys you generated withdrawal credentials with the mnemonic. |

This comment was marked as resolved.

This comment was marked as resolved.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find the wording of this description a little confusing. Perhaps something like this?

Suggested change
| `--validator_start_index` | Non-negative integer | The index of the first validator's keys you generated withdrawal credentials with the mnemonic. |
| `--validator_start_index` | Non-negative integer | From your mnemonic, the index position for the keys to start generating withdrawal credentials from. |

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is indeed too complicated to explain the ERC-2334 in one sentence. 😭

I feel "From your mnemonic" might sound like the word index of a mnemonic. How about "The index position for the keys to start generating withdrawal credentials in ERC-2334 format"?

hwwhww marked this conversation as resolved.
Show resolved Hide resolved
| `--validator_indices` | String of integer(s) | A list of the index number of your validator(s) in beacon chain state. Split multiple items with whitespaces or commas. |
hwwhww marked this conversation as resolved.
Show resolved Hide resolved
| `--bls_withdrawal_credentials_list` | String of hexstring(s). | A list of the old BLS withdrawal credentials of the given validator(s). It is for confirming you are using the correct keys. Split multiple items with whitespaces or commas. |
| `--execution_address` | String. 20-byte Execution (Eth1) address in hexadecimal encoded form | The execution (Eth1) address you want to change to for withdrawals. |
| `--devnet_chain_setting` | String. JSON string `'{"network_name": "<NETWORK_NAME>", "genesis_fork_version": "<GENESIS_FORK_VERSION>", "genesis_validator_root": "<GENESIS_VALIDATOR_ROOT>"}'` | The custom chain setting of a devnet or testnet. Note that it will override your `--chain` choice. |

#### Option 2. Build `deposit-cli` with native Python

##### Step 0. Python version checking
Expand Down Expand Up @@ -228,6 +246,7 @@ See [here](#commands)

See [here](#new-mnemonic-arguments) for `new-mnemonic` arguments
See [here](#existing-mnemonic-arguments) for `existing-mnemonic` arguments
See [here](#generate-bls-to-execution-change-arguments) for `generate-bls-to-execution-change` arguments

###### Successful message
See [here](#successful-message)
Expand Down Expand Up @@ -295,6 +314,7 @@ See [here](#commands)

See [here](#new-mnemonic-arguments) for `new-mnemonic` arguments
See [here](#existing-mnemonic-arguments) for `existing-mnemonic` arguments
See [here](#generate-bls-to-execution-change-arguments) for `generate-bls-to-execution-change` arguments

#### Option 4. Use Docker image

Expand Down Expand Up @@ -378,6 +398,7 @@ See [here](#commands)

See [here](#new-mnemonic-arguments) for `new-mnemonic` arguments
See [here](#existing-mnemonic-arguments) for `existing-mnemonic` arguments
See [here](#generate-bls-to-execution-change-arguments) for `generate-bls-to-execution-change` arguments

#### Option 2. Build `deposit-cli` with native Python

Expand Down Expand Up @@ -440,6 +461,7 @@ See [here](#commands)

See [here](#new-mnemonic-arguments) for `new-mnemonic` arguments
See [here](#existing-mnemonic-arguments) for `existing-mnemonic` arguments
See [here](#generate-bls-to-execution-change-arguments) for `generate-bls-to-execution-change` arguments

#### Option 3. Build `deposit-cli` with `virtualenv`

Expand Down Expand Up @@ -504,6 +526,7 @@ See [here](#commands)

See [here](#new-mnemonic-arguments) for `new-mnemonic` arguments
See [here](#existing-mnemonic-arguments) for `existing-mnemonic` arguments
See [here](#generate-bls-to-execution-change-arguments) for `generate-bls-to-execution-change` arguments

## Development

Expand Down
56 changes: 35 additions & 21 deletions staking_deposit/cli/existing_mnemonic.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import click
from typing import (
Any,
Callable,
)

from staking_deposit.exceptions import ValidationError
Expand All @@ -22,6 +23,39 @@
)


def load_mnemonic_arguments_decorator(function: Callable[..., Any]) -> Callable[..., Any]:
'''
This is a decorator that, when applied to a parent-command, implements the
to obtain the necessary arguments for the generate_keys() subcommand.
'''
decorators = [
jit_option(
callback=validate_mnemonic,
help=lambda: load_text(['arg_mnemonic', 'help'], func='existing_mnemonic'),
param_decls='--mnemonic',
prompt=lambda: load_text(['arg_mnemonic', 'prompt'], func='existing_mnemonic'),
type=str,
),
jit_option(
callback=captive_prompt_callback(
lambda x: x,
lambda: load_text(['arg_mnemonic_password', 'prompt'], func='existing_mnemonic'),
lambda: load_text(['arg_mnemonic_password', 'confirm'], func='existing_mnemonic'),
lambda: load_text(['arg_mnemonic_password', 'mismatch'], func='existing_mnemonic'),
True,
),
default='',
help=lambda: load_text(['arg_mnemonic_password', 'help'], func='existing_mnemonic'),
hidden=True,
param_decls='--mnemonic-password',
prompt=False,
),
]
for decorator in reversed(decorators):
function = decorator(function)
return function


def validate_mnemonic(ctx: click.Context, param: Any, mnemonic: str) -> str:
mnemonic = reconstruct_mnemonic(mnemonic, WORD_LISTS_PATH)
if mnemonic is not None:
Expand All @@ -33,27 +67,7 @@ def validate_mnemonic(ctx: click.Context, param: Any, mnemonic: str) -> str:
@click.command(
help=load_text(['arg_existing_mnemonic', 'help'], func='existing_mnemonic'),
)
@jit_option(
callback=validate_mnemonic,
help=lambda: load_text(['arg_mnemonic', 'help'], func='existing_mnemonic'),
param_decls='--mnemonic',
prompt=lambda: load_text(['arg_mnemonic', 'prompt'], func='existing_mnemonic'),
type=str,
)
@jit_option(
callback=captive_prompt_callback(
lambda x: x,
lambda: load_text(['arg_mnemonic_password', 'prompt'], func='existing_mnemonic'),
lambda: load_text(['arg_mnemonic_password', 'confirm'], func='existing_mnemonic'),
lambda: load_text(['arg_mnemonic_password', 'mismatch'], func='existing_mnemonic'),
True,
),
default='',
help=lambda: load_text(['arg_mnemonic_password', 'help'], func='existing_mnemonic'),
hidden=True,
param_decls='--mnemonic-password',
prompt=False,
)
@load_mnemonic_arguments_decorator
@jit_option(
callback=captive_prompt_callback(
lambda num: validate_int_range(num, 0, 2**32),
Expand Down
196 changes: 196 additions & 0 deletions staking_deposit/cli/generate_bls_to_execution_change.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
import os
import click
import json
from typing import (
Any,
Sequence,
)

from eth_typing import HexAddress

from staking_deposit.credentials import (
CredentialList,
)
from staking_deposit.utils.validation import (
validate_bls_withdrawal_credentials_list,
validate_bls_withdrawal_credentials_matching,
validate_eth1_withdrawal_address,
validate_int_range,
verify_bls_to_execution_change_json,
validate_validator_indices,
)
from staking_deposit.utils.constants import (
DEFAULT_BLS_TO_EXECUTION_CHANGES_FOLDER_NAME,
MAX_DEPOSIT_AMOUNT,
)
from staking_deposit.utils.click import (
captive_prompt_callback,
choice_prompt_func,
jit_option,
)
from staking_deposit.exceptions import ValidationError
from staking_deposit.utils.intl import (
closest_match,
load_text,
)
from staking_deposit.settings import (
ALL_CHAINS,
MAINNET,
PRATER,
get_chain_setting,
get_devnet_chain_setting,
)
from .existing_mnemonic import (
load_mnemonic_arguments_decorator,
)


def get_password(text: str) -> str:
return click.prompt(text, hide_input=True, show_default=False, type=str)


FUNC_NAME = 'generate_bls_to_execution_change'


@click.command()
@jit_option(
default=os.getcwd(),
help=lambda: load_text(['arg_bls_to_execution_changes_folder', 'help'], func=FUNC_NAME),
param_decls='--bls_to_execution_changes_folder',
type=click.Path(exists=True, file_okay=False, dir_okay=True),
)
@jit_option(
callback=captive_prompt_callback(
lambda x: closest_match(x, list(ALL_CHAINS.keys())),
choice_prompt_func(
lambda: load_text(['arg_chain', 'prompt'], func=FUNC_NAME),
list(ALL_CHAINS.keys())
),
),
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),
# Since `prater` is alias of `goerli`, do not show `prater` in the prompt message.
list(key for key in ALL_CHAINS.keys() if key != PRATER)
),
)
@load_mnemonic_arguments_decorator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we have a prompt or something to make sure the user is doing this offline? to get the signatures in an airgapped location or something.

@jit_option(
callback=captive_prompt_callback(
lambda num: validate_int_range(num, 0, 2**32),
lambda: load_text(['arg_validator_start_index', 'prompt'], func=FUNC_NAME),
),
default=0,
help=lambda: load_text(['arg_validator_start_index', 'help'], func=FUNC_NAME),
param_decls="--validator_start_index",
prompt=lambda: load_text(['arg_validator_start_index', 'prompt'], func=FUNC_NAME),
)
@jit_option(
callback=captive_prompt_callback(
lambda validator_indices: validate_validator_indices(validator_indices),
lambda: load_text(['arg_validator_indices', 'prompt'], func=FUNC_NAME),
),
help=lambda: load_text(['arg_validator_indices', 'help'], func=FUNC_NAME),
param_decls='--validator_indices',
prompt=lambda: load_text(['arg_validator_indices', 'prompt'], func=FUNC_NAME),
)
@jit_option(
callback=captive_prompt_callback(
lambda bls_withdrawal_credentials_list:
validate_bls_withdrawal_credentials_list(bls_withdrawal_credentials_list),
lambda: load_text(['arg_bls_withdrawal_credentials_list', 'prompt'], func=FUNC_NAME),
),
help=lambda: load_text(['arg_bls_withdrawal_credentials_list', 'help'], func=FUNC_NAME),
param_decls='--bls_withdrawal_credentials_list',
prompt=lambda: load_text(['arg_bls_withdrawal_credentials_list', 'prompt'], func=FUNC_NAME),
)
@jit_option(
callback=captive_prompt_callback(
hwwhww marked this conversation as resolved.
Show resolved Hide resolved
lambda address: validate_eth1_withdrawal_address(None, None, address),
lambda: load_text(['arg_execution_address', 'prompt'], func=FUNC_NAME),
lambda: load_text(['arg_execution_address', 'confirm'], func=FUNC_NAME),
lambda: load_text(['arg_execution_address', 'mismatch'], func=FUNC_NAME),
),
help=lambda: load_text(['arg_execution_address', 'help'], func=FUNC_NAME),
param_decls='--execution_address',
prompt=lambda: load_text(['arg_execution_address', 'prompt'], func=FUNC_NAME),
)
@jit_option(
# Only for devnet tests
default=None,
help="[DEVNET ONLY] Set specific GENESIS_FORK_VERSION value",
param_decls='--devnet_chain_setting',
)
@click.pass_context
def generate_bls_to_execution_change(
ctx: click.Context,
bls_to_execution_changes_folder: str,
chain: str,
mnemonic: str,
mnemonic_password: str,
validator_start_index: int,
validator_indices: Sequence[int],
bls_withdrawal_credentials_list: Sequence[bytes],
execution_address: HexAddress,
devnet_chain_setting: str,
**kwargs: Any) -> None:
# Generate folder
bls_to_execution_changes_folder = os.path.join(
bls_to_execution_changes_folder,
DEFAULT_BLS_TO_EXECUTION_CHANGES_FOLDER_NAME,
)
if not os.path.exists(bls_to_execution_changes_folder):
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'],
genesis_validator_root=devnet_chain_setting_dict['genesis_validator_root'],
)

if len(validator_indices) != len(bls_withdrawal_credentials_list):
raise ValueError(
"The size of `validator_indices` (%d) should be as same as `bls_withdrawal_credentials_list` (%d)."
% (len(validator_indices), len(bls_withdrawal_credentials_list))
)

num_validators = len(validator_indices)
amounts = [MAX_DEPOSIT_AMOUNT] * num_validators

credentials = CredentialList.from_mnemonic(
mnemonic=mnemonic,
mnemonic_password=mnemonic_password,
num_keys=num_validators,
amounts=amounts,
chain_setting=chain_setting,
start_index=validator_start_index,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this validator_start_index throws an uncaught exception if the user misenters, you can't tell from the response that this was incorrect and user probably might not know what the right answer is.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @james-prysm could you elaborate on this case more? I tried to input random English and the cli did detect my wrong input and asked for re-input. What was the testing input you used?

Copy link

@infosecual infosecual Feb 17, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I broke it :)

https://gist.github.com/infosecual/d7de7005bf54732a863dce21d503e445

using "1,2,3,4,5,6,7,8,9,10"

as the input to: Please enter a list of the old BLS withdrawal credentials of your validator(s). Split multiple items with whitespaces or commas.:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hwwhww I totally missed this, put in the wrong validator_start_index with the corresponding validator indexs and it broke.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use case is
using a mnemonic of 1800 keys, 0~1799 index
input for validator_start_index: 1
validator_index: 0
withdrawal_credentials: 0x004652a99d8c9278708a98dab2c09b45cf09ea732037a17583ae2513a329fd8a
execution_address:
i get the following error
File "staking_deposit/utils/validation.py", line 246, in validate_bls_withdrawal_credentials_matching staking_deposit.exceptions.ValidationError: The given withdrawal credentials is matching the old BLS withdrawal credentials that mnemonic generated. [76365] Failed to execute script 'deposit' due to unhandled exception!
if i used validator_start_index:0 it works

hex_eth1_withdrawal_address=execution_address,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can both non hex and hex be handled? or if the error for non hex lets the user know,I believe some users will get the withdrawal pub key from the intial deposit.json which doesn't have 0x

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bls_withdrawal_credentials_list can be with 0x prefix or no 0x.
eth1_withdrawal_address requires the checksum form and 0x prefix.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hwwhww Does this mean if the user pastes an all lowercase address it will be blocked?

Copy link
Contributor Author

@hwwhww hwwhww Feb 10, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@wackerow
in a very edge case, an all-number address is a valid checksum address 😄 but I understand what you mean, yes, the input addresses have to be checksummed and capital letters matter.

)

# Check if the given old bls_withdrawal_credentials is as same as the mnemonic generated
for i, credential in enumerate(credentials.credentials):
validate_bls_withdrawal_credentials_matching(bls_withdrawal_credentials_list[i], credential)

btec_file = credentials.export_bls_to_execution_change_json(bls_to_execution_changes_folder, validator_indices)

json_file_validation_result = verify_bls_to_execution_change_json(
btec_file,
credentials.credentials,
input_validator_indices=validator_indices,
input_execution_address=execution_address,
chain_setting=chain_setting,
)
if not json_file_validation_result:
raise ValidationError(load_text(['err_verify_btec']))

click.echo(load_text(['msg_creation_success']) + str(bls_to_execution_changes_folder))

click.pause(load_text(['msg_pause']))
13 changes: 1 addition & 12 deletions staking_deposit/cli/generate_keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
)

from eth_typing import HexAddress
from eth_utils import is_hex_address, to_normalized_address

from staking_deposit.credentials import (
CredentialList,
Expand All @@ -16,6 +15,7 @@
verify_deposit_data_json,
validate_int_range,
validate_password_strength,
validate_eth1_withdrawal_address,
)
from staking_deposit.utils.constants import (
MAX_DEPOSIT_AMOUNT,
Expand Down Expand Up @@ -43,17 +43,6 @@ def get_password(text: str) -> str:
return click.prompt(text, hide_input=True, show_default=False, type=str)


def validate_eth1_withdrawal_address(cts: click.Context, param: Any, address: str) -> HexAddress:
if address is None:
return None
if not is_hex_address(address):
raise ValueError(load_text(['err_invalid_ECDSA_hex_addr']))

normalized_address = to_normalized_address(address)
click.echo('\n%s\n' % load_text(['msg_ECDSA_addr_withdrawal']))
return normalized_address


def generate_keys_arguments_decorator(function: Callable[..., Any]) -> Callable[..., Any]:
'''
This is a decorator that, when applied to a parent-command, implements the
Expand Down
Loading