Merge pull request #2614 from derekpierre/period-cli-ux

Updated Period length CLI UX
pull/2629/head
KPrasch 2021-03-31 11:42:13 -07:00 committed by GitHub
commit 264aa1e360
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 82 additions and 47 deletions

View File

@ -0,0 +1 @@
Accommodate migrated period duration in CLI UX.

View File

@ -90,7 +90,7 @@ def collect_expiration(alice: Alice, expiration: maya.MayaDT, force: bool) -> ma
default_expiration = None
expiration_prompt = 'Enter policy expiration (Y-M-D H:M:S)'
if alice.duration_periods:
default_expiration = maya.now() + timedelta(days=alice.duration_periods)
default_expiration = maya.now() + timedelta(hours=alice.duration_periods * alice.economics.hours_per_period)
expiration = click.prompt(expiration_prompt, type=click.DateTime(), default=default_expiration)
return expiration

View File

@ -18,15 +18,18 @@
import click
from constant_sorrow.constants import UNKNOWN_DEVELOPMENT_CHAIN_ID
from datetime import datetime
from maya import MayaDT
from tabulate import tabulate
from typing import Type, Union, Dict
from web3.main import Web3
from nucypher.blockchain.economics import BaseEconomics
from nucypher.blockchain.eth.deployers import BaseContractDeployer
from nucypher.blockchain.eth.interfaces import BlockchainDeployerInterface, VersionedContract, BlockchainInterface
from nucypher.blockchain.eth.registry import LocalContractRegistry
from nucypher.blockchain.eth.token import NU
from nucypher.blockchain.eth.utils import calculate_period_duration
from nucypher.characters.control.emitters import StdoutEmitter
from nucypher.cli.literature import (
ABORT_DEPLOYMENT,
@ -92,12 +95,14 @@ def confirm_staged_stake(staker_address: str, value: NU, lock_periods: int) -> b
return True
def confirm_large_stake(value: NU = None, lock_periods: int = None) -> bool:
def confirm_large_and_or_long_stake(value: NU = None, lock_periods: int = None, economics: BaseEconomics = None) -> bool:
"""Interactively confirm a large stake and/or a long stake duration."""
if value and (value > NU.from_tokens(150000)):
if economics and value and (value > (NU.from_nunits(economics.minimum_allowed_locked) * 10)): # > 10x min stake
click.confirm(CONFIRM_LARGE_STAKE_VALUE.format(value=value), abort=True)
if lock_periods and (lock_periods > 365):
click.confirm(CONFIRM_LARGE_STAKE_DURATION.format(lock_periods=lock_periods), abort=True)
if economics and lock_periods and (lock_periods > economics.maximum_rewarded_periods): # > 1 year
lock_days = (lock_periods * economics.hours_per_period) // 24
click.confirm(CONFIRM_LARGE_STAKE_DURATION.format(lock_periods=lock_periods, lock_days=lock_days),
abort=True)
return True
@ -141,7 +146,7 @@ def verify_upgrade_details(blockchain: Union[BlockchainDeployerInterface, Blockc
new_version=new_version), abort=True)
def confirm_staged_grant(emitter, grant_request: Dict, federated: bool) -> None:
def confirm_staged_grant(emitter, grant_request: Dict, federated: bool, seconds_per_period=None) -> None:
pretty_request = grant_request.copy() # WARNING: Do not mutate
@ -154,7 +159,9 @@ def confirm_staged_grant(emitter, grant_request: Dict, federated: bool) -> None:
pretty_request['rate'] = f"{pretty_request['rate']} wei/period * {pretty_request['n']} nodes"
expiration = pretty_request['expiration']
periods = (expiration - datetime.now()).days
periods = calculate_period_duration(future_time=MayaDT.from_datetime(expiration),
seconds_per_period=seconds_per_period)
periods += 1 # current period is always included
pretty_request['expiration'] = f"{pretty_request['expiration']} ({periods} periods)"
# M of N
@ -171,11 +178,6 @@ def confirm_staged_grant(emitter, grant_request: Dict, federated: bool) -> None:
table.append(['Period Rate', f'{period_rate} gwei'])
table.append(['Policy Value', f'{period_rate * periods} gwei'])
# TODO: Use period calculation utilities instead of days
# periods = calculate_period_duration(future_time=maya.MayaDT(pretty_request['expiration']),
# seconds_per_period=StandardTokenEconomics().seconds_per_period)
emitter.echo("\nSuccessfully staged grant, Please review the details:\n", color='green')
emitter.echo(tabulate(table, tablefmt="simple"))
click.confirm('\nGrant access and sign transaction?', abort=True)

View File

@ -17,13 +17,10 @@ along with nucypher. If not, see <https://www.gnu.org/licenses/>.
import click
import os
from constant_sorrow.constants import NO_PASSWORD
from nucypher.blockchain.eth.signers.software import ClefSigner
from nucypher.characters.control.emitters import StdoutEmitter
from nucypher.characters.control.interfaces import AliceInterface
from nucypher.cli.actions.auth import get_client_password, get_nucypher_password
from nucypher.cli.actions.auth import get_nucypher_password
from nucypher.cli.actions.collect import collect_bob_public_keys, collect_policy_parameters
from nucypher.cli.actions.configure import (
destroy_configuration,
@ -65,7 +62,6 @@ from nucypher.cli.types import EIP55_CHECKSUM_ADDRESS
from nucypher.cli.utils import make_cli_character, setup_emitter
from nucypher.config.characters import AliceConfiguration
from nucypher.config.constants import (
NUCYPHER_ENVVAR_ALICE_ETH_PASSWORD,
TEMPORARY_DOMAIN,
)
from nucypher.config.keyring import NucypherKeyring
@ -498,7 +494,10 @@ def grant(general_config,
# Grant
if not force and not general_config.json_ipc:
confirm_staged_grant(emitter=emitter, grant_request=grant_request, federated=ALICE.federated_only)
confirm_staged_grant(emitter=emitter,
grant_request=grant_request,
federated=ALICE.federated_only,
seconds_per_period=(None if ALICE.federated_only else ALICE.economics.seconds_per_period))
emitter.echo(f'Granting Access to {bob_public_keys.verifying_key[:8]}', color='yellow')
return ALICE.controller.grant(request=grant_request)

View File

@ -32,7 +32,7 @@ from nucypher.cli.actions.configure import get_or_update_configuration, handle_m
from nucypher.cli.actions.confirm import (
confirm_enable_restaking,
confirm_enable_winding_down,
confirm_large_stake,
confirm_large_and_or_long_stake,
confirm_staged_stake,
confirm_disable_snapshots
)
@ -545,7 +545,7 @@ def create(general_config: GroupGeneralConfig,
#
if not force:
confirm_large_stake(value=value, lock_periods=lock_periods)
confirm_large_and_or_long_stake(value=value, lock_periods=lock_periods, economics=economics)
paint_staged_stake(emitter=emitter,
blockchain=blockchain,
stakeholder=STAKEHOLDER,
@ -634,7 +634,7 @@ def increase(general_config: GroupGeneralConfig,
current_period = STAKEHOLDER.staker.staking_agent.get_current_period()
unlock_period = current_stake.final_locked_period + 1
confirm_large_stake(value=value, lock_periods=lock_periods)
confirm_large_and_or_long_stake(value=value, lock_periods=lock_periods, economics=STAKEHOLDER.staker.economics)
paint_staged_stake(emitter=emitter,
blockchain=blockchain,
stakeholder=STAKEHOLDER,
@ -878,7 +878,7 @@ def divide(general_config: GroupGeneralConfig,
extension = lock_periods
if not force:
confirm_large_stake(lock_periods=extension, value=value)
confirm_large_and_or_long_stake(lock_periods=extension, value=value, economics=economics)
paint_staged_stake_division(emitter=emitter,
blockchain=blockchain,
stakeholder=STAKEHOLDER,

View File

@ -78,7 +78,7 @@ Accept ursula node operator obligation?"""
CONFIRM_LARGE_STAKE_VALUE = "Wow, {value} - That's a lot of NU - Are you sure this is correct?"
CONFIRM_LARGE_STAKE_DURATION = "Woah, {lock_periods} is a long time - Are you sure this is correct?"
CONFIRM_LARGE_STAKE_DURATION = "Woah, {lock_periods} periods ({lock_days} days) is a long time - Are you sure this is correct?"
PROMPT_STAKE_CREATE_VALUE = "Enter stake value in NU ({lower_limit} - {upper_limit})"

View File

@ -101,13 +101,23 @@ def paint_stakes(emitter: StdoutEmitter,
rows, inactive_substakes = list(), list()
for index, stake in enumerate(stakes):
is_inactive = False
if stake.status().is_child(Stake.Status.INACTIVE):
inactive_substakes.append(index)
is_inactive = True
if stake.status().is_child(Stake.Status.UNLOCKED) and not paint_unlocked:
# This stake is unlocked.
continue
rows.append(list(stake.describe().values()))
stake_description = stake.describe()
if is_inactive:
# stake is inactive - update display values since they don't make much sense to display
stake_description['remaining'] = 'N/A'
stake_description['last_period'] = 'N/A'
rows.append(list(stake_description.values()))
if not rows:
emitter.echo(f"There are no locked stakes\n")
@ -116,7 +126,11 @@ def paint_stakes(emitter: StdoutEmitter,
if not paint_unlocked and inactive_substakes:
emitter.echo(f"Note that some sub-stakes are inactive: {inactive_substakes}\n"
f"Run `nucypher stake list --all` to show all sub-stakes.", color='yellow')
f"Run `nucypher stake list --all` to show all sub-stakes.\n"
f"Run `nucypher stake remove-inactive --all` to remove inactive sub-stakes; removal of inactive "
f"sub-stakes will reduce commitment gas costs.", color='yellow')
# TODO - it would be nice to provide remove-inactive hint when painting_unlocked - however, this same function
# is used by remove-inactive command is run, and it is redundant to be shown then
def prettify_stake(stake, index: int = None) -> str:
@ -171,16 +185,18 @@ def paint_staged_stake(emitter,
start_period,
unlock_period,
division_message: str = None):
economics = stakeholder.staker.economics
start_datetime = datetime_at_period(period=start_period,
seconds_per_period=stakeholder.staker.economics.seconds_per_period,
seconds_per_period=economics.seconds_per_period,
start_of_period=True)
unlock_datetime = datetime_at_period(period=unlock_period,
seconds_per_period=stakeholder.staker.economics.seconds_per_period,
seconds_per_period=economics.seconds_per_period,
start_of_period=True)
locked_days = (lock_periods * economics.hours_per_period) // 24
start_datetime_pretty = start_datetime.local_datetime().strftime("%b %d %H:%M %Z")
unlock_datetime_pretty = unlock_datetime.local_datetime().strftime("%b %d %H:%M %Z")
start_datetime_pretty = start_datetime.local_datetime().strftime("%b %d %Y %H:%M %Z")
unlock_datetime_pretty = unlock_datetime.local_datetime().strftime("%b %d %Y %H:%M %Z")
if division_message:
emitter.echo(f"\n{'' * 30} ORIGINAL STAKE {'' * 28}", bold=True)
@ -192,7 +208,7 @@ def paint_staged_stake(emitter,
Staking address: {staking_address}
~ Chain -> ID # {blockchain.client.chain_id} | {blockchain.client.chain_name}
~ Value -> {stake_value} ({int(stake_value)} NuNits)
~ Duration -> {lock_periods} Days ({lock_periods} Periods)
~ Duration -> {locked_days} Days ({lock_periods} Periods)
~ Enactment -> {start_datetime_pretty} (period #{start_period})
~ Expiration -> {unlock_datetime_pretty} (period #{unlock_period})
""")

View File

@ -18,10 +18,11 @@
import click
import pytest
from nucypher.blockchain.economics import StandardTokenEconomics
from nucypher.blockchain.eth.clients import EthereumTesterClient, PUBLIC_CHAINS
from nucypher.blockchain.eth.token import NU
from nucypher.cli.actions.confirm import (confirm_deployment, confirm_enable_restaking,
confirm_enable_winding_down, confirm_large_stake, confirm_staged_stake)
confirm_enable_winding_down, confirm_large_and_or_long_stake, confirm_staged_stake)
from nucypher.cli.literature import (ABORT_DEPLOYMENT, RESTAKING_AGREEMENT,
WINDING_DOWN_AGREEMENT, CONFIRM_STAGED_STAKE,
CONFIRM_LARGE_STAKE_VALUE, CONFIRM_LARGE_STAKE_DURATION)
@ -163,13 +164,17 @@ def test_confirm_staged_stake_cli_action(test_emitter, mock_stdin, capsys):
assert mock_stdin.empty()
STANDARD_ECONOMICS = StandardTokenEconomics()
MIN_ALLOWED_LOCKED = STANDARD_ECONOMICS.minimum_allowed_locked
@pytest.mark.parametrize('value,duration,must_confirm_value,must_confirm_duration', (
(NU.from_tokens(1), 1, False, False),
(NU.from_tokens(1), 31, False, False),
(NU.from_tokens(15), 31, False, False),
(NU.from_tokens(150001), 31, True, False),
(NU.from_tokens(150000), 366, False, True),
(NU.from_tokens(150001), 366, True, True),
(NU.from_tokens(1), STANDARD_ECONOMICS.minimum_locked_periods + 1, False, False),
(NU.from_tokens(15), STANDARD_ECONOMICS.minimum_locked_periods + 1, False, False),
(((NU.from_nunits(MIN_ALLOWED_LOCKED) * 10) + 1), STANDARD_ECONOMICS.minimum_locked_periods + 1, True, False),
(NU.from_nunits(MIN_ALLOWED_LOCKED) * 10, STANDARD_ECONOMICS.maximum_rewarded_periods + 1, False, True),
(((NU.from_nunits(MIN_ALLOWED_LOCKED) * 10) + 1), STANDARD_ECONOMICS.maximum_rewarded_periods + 1, True, True),
))
def test_confirm_large_stake_cli_action(test_emitter,
mock_stdin,
@ -180,14 +185,16 @@ def test_confirm_large_stake_cli_action(test_emitter,
must_confirm_duration):
asked_about_value = lambda output: CONFIRM_LARGE_STAKE_VALUE.format(value=value) in output
asked_about_duration = lambda output: CONFIRM_LARGE_STAKE_DURATION.format(lock_periods=duration) in output
lock_days = (duration * STANDARD_ECONOMICS.hours_per_period) // 24
asked_about_duration = lambda output: CONFIRM_LARGE_STAKE_DURATION.format(lock_periods=duration,
lock_days=lock_days) in output
# Positive Cases - either do not need to confirm anything, or say yes
if must_confirm_value:
mock_stdin.line(YES)
if must_confirm_duration:
mock_stdin.line(YES)
result = confirm_large_stake(value=value, lock_periods=duration)
result = confirm_large_and_or_long_stake(value=value, lock_periods=duration, economics=STANDARD_ECONOMICS)
assert result
captured = capsys.readouterr()
assert must_confirm_value == asked_about_value(captured.out)
@ -205,7 +212,7 @@ def test_confirm_large_stake_cli_action(test_emitter,
mock_stdin.line(NO)
with pytest.raises(click.Abort):
confirm_large_stake(value=value, lock_periods=duration)
confirm_large_and_or_long_stake(value=value, lock_periods=duration, economics=STANDARD_ECONOMICS)
captured = capsys.readouterr()
assert must_confirm_value == asked_about_value(captured.out)
assert must_confirm_duration == asked_about_duration(captured.out)

View File

@ -957,7 +957,8 @@ def test_create_interactive(click_runner,
lock_periods=lock_periods) in result.output
assert CONFIRM_BROADCAST_CREATE_STAKE in result.output
assert CONFIRM_LARGE_STAKE_VALUE.format(value=value) in result.output
assert CONFIRM_LARGE_STAKE_DURATION.format(lock_periods=lock_periods) in result.output
lock_days = (lock_periods * token_economics.hours_per_period) // 24
assert CONFIRM_LARGE_STAKE_DURATION.format(lock_periods=lock_periods, lock_days=lock_days) in result.output
mock_staking_agent.get_all_stakes.assert_called()
mock_staking_agent.get_current_period.assert_called()
@ -1017,7 +1018,8 @@ def test_create_non_interactive(click_runner,
lock_periods=lock_periods) not in result.output
assert CONFIRM_BROADCAST_CREATE_STAKE in result.output
assert CONFIRM_LARGE_STAKE_VALUE.format(value=value) not in result.output
assert CONFIRM_LARGE_STAKE_DURATION.format(lock_periods=lock_periods) not in result.output
lock_days = (lock_periods * token_economics.hours_per_period) // 24
assert CONFIRM_LARGE_STAKE_DURATION.format(lock_periods=lock_periods, lock_days=lock_days) not in result.output
mock_staking_agent.get_all_stakes.assert_called()
mock_staking_agent.get_current_period.assert_called()
@ -1096,7 +1098,8 @@ def test_create_lock_interactive(click_runner,
lock_periods=lock_periods) in result.output
assert CONFIRM_BROADCAST_CREATE_STAKE in result.output
assert CONFIRM_LARGE_STAKE_VALUE.format(value=value) not in result.output
assert CONFIRM_LARGE_STAKE_DURATION.format(lock_periods=lock_periods) in result.output
lock_days = (lock_periods * token_economics.hours_per_period) // 24
assert CONFIRM_LARGE_STAKE_DURATION.format(lock_periods=lock_periods, lock_days=lock_days) in result.output
mock_staking_agent.get_all_stakes.assert_called()
mock_staking_agent.get_current_period.assert_called()
@ -1154,7 +1157,8 @@ def test_create_lock_non_interactive(click_runner,
lock_periods=lock_periods) not in result.output
assert CONFIRM_BROADCAST_CREATE_STAKE in result.output
assert CONFIRM_LARGE_STAKE_VALUE.format(value=value) not in result.output
assert CONFIRM_LARGE_STAKE_DURATION.format(lock_periods=lock_periods) not in result.output
lock_days = (lock_periods * token_economics.hours_per_period) // 24
assert CONFIRM_LARGE_STAKE_DURATION.format(lock_periods=lock_periods, lock_days=lock_days) not in result.output
mock_staking_agent.get_all_stakes.assert_called()
mock_staking_agent.get_current_period.assert_called()
@ -1378,7 +1382,6 @@ def test_stake_list_all(click_runner, surrogate_stakers, surrogate_stakes, token
for stakes in surrogate_stakes:
for index, sub_stake in enumerate(stakes):
value = NU.from_nunits(sub_stake.locked_value)
remaining = sub_stake.last_period - current_period + 1
start_datetime = datetime_at_period(period=sub_stake.first_period,
seconds_per_period=token_economics.seconds_per_period,
start_of_period=True)
@ -1386,10 +1389,17 @@ def test_stake_list_all(click_runner, surrogate_stakers, surrogate_stakes, token
seconds_per_period=token_economics.seconds_per_period,
start_of_period=True)
enactment = start_datetime.local_datetime().strftime("%b %d %Y")
termination = unlock_datetime.local_datetime().strftime("%b %d %Y")
status = statuses[index]
if status == Stake.Status.INACTIVE:
remaining = 'N/A'
termination = 'N/A'
else:
remaining = sub_stake.last_period - current_period + 1
termination = unlock_datetime.local_datetime().strftime("%b %d %Y")
assert re.search(f"{index}\\s+│\\s+"
f"{value}\\s+│\\s+"
f"{remaining}\\s+│\\s+"
f"{enactment}\\s+│\\s+"
f"{termination}\\s+│\\s+"
f"{statuses[index].name}", result.output, re.MULTILINE)
f"{status.name}", result.output, re.MULTILINE)