diff --git a/nucypher/blockchain/eth/actors.py b/nucypher/blockchain/eth/actors.py index 3aa242066..7556daaf4 100644 --- a/nucypher/blockchain/eth/actors.py +++ b/nucypher/blockchain/eth/actors.py @@ -795,6 +795,42 @@ class Staker(NucypherTokenActor): return new_stake + @only_me + def prolong_stake(self, + stake_index: int, + additional_periods: int = None, + expiration: maya.MayaDT = None) -> tuple: + + # Calculate duration in periods + if additional_periods and expiration: + raise ValueError("Pass the number of lock periods or an expiration MayaDT; not both.") + + # Update staking cache element + stakes = self.stakes + + # Select stake to divide from local cache + try: + current_stake = stakes[stake_index] + except KeyError: + if len(stakes): + message = f"Cannot prolong stake - No stake exists with index {stake_index}." + else: + message = "Cannot prolong stake - There are no active stakes." + raise Stake.StakingError(message) + + # Calculate stake duration in periods + if expiration: + additional_periods = datetime_to_period(datetime=expiration, seconds_per_period=self.economics.seconds_per_period) - current_stake.final_locked_period + if additional_periods <= 0: + raise Stake.StakingError(f"New expiration {expiration} must be at least 1 period from the " + f"current stake's end period ({current_stake.final_locked_period}).") + + stake = current_stake.prolong(additional_periods=additional_periods) + + # Update staking cache element + self.stakes.refresh() + return stake + def deposit(self, amount: int, lock_periods: int) -> Tuple[str, str]: """Public facing method for token locking.""" if self.is_contract: diff --git a/nucypher/blockchain/eth/token.py b/nucypher/blockchain/eth/token.py index b7a8ac0a0..525bff0fb 100644 --- a/nucypher/blockchain/eth/token.py +++ b/nucypher/blockchain/eth/token.py @@ -455,6 +455,15 @@ class Stake: log.info(f"{staker.checksum_address} Initialized new stake: {amount} tokens for {lock_periods} periods") return stake + def prolong(self, additional_periods: int): + self.sync() + if self.is_expired: + raise self.StakingError(f'Cannot divide an expired stake. Selected stake expired {self.unlock_datetime}.') + receipt = self.staking_agent.prolong_stake(staker_address=self.staker_address, + stake_index=self.index, + periods=additional_periods) + return receipt + class WorkTracker: diff --git a/nucypher/characters/chaotic.py b/nucypher/characters/chaotic.py index 989ef5361..c12f4da5d 100644 --- a/nucypher/characters/chaotic.py +++ b/nucypher/characters/chaotic.py @@ -205,11 +205,6 @@ class Felix(Character, NucypherTokenActor): } ) - @rest_app.route("/", methods=['GET']) - def home(): - rendering = render_template(self.TEMPLATE_NAME) - return rendering - @rest_app.route("/register", methods=['POST']) def register(): """Handle new recipient registration via POST request.""" diff --git a/nucypher/cli/commands/alice.py b/nucypher/cli/commands/alice.py index 60db200b4..e43db60c4 100644 --- a/nucypher/cli/commands/alice.py +++ b/nucypher/cli/commands/alice.py @@ -1,4 +1,3 @@ -import functools import json import click diff --git a/nucypher/cli/commands/deploy.py b/nucypher/cli/commands/deploy.py index a893dbe40..c4658c815 100644 --- a/nucypher/cli/commands/deploy.py +++ b/nucypher/cli/commands/deploy.py @@ -36,7 +36,7 @@ from nucypher.cli.actions import ( confirm_deployment, establish_deployer_registry ) -from nucypher.cli.common_options import ( +from nucypher.cli.options import ( group_options, option_config_root, option_etherscan, @@ -44,7 +44,7 @@ from nucypher.cli.common_options import ( option_hw_wallet, option_poa, option_provider_uri, - ) +) from nucypher.cli.config import group_general_config from nucypher.cli.painting import ( paint_staged_deployment, diff --git a/nucypher/cli/commands/stake.py b/nucypher/cli/commands/stake.py index 441ecc7b8..97a733acc 100644 --- a/nucypher/cli/commands/stake.py +++ b/nucypher/cli/commands/stake.py @@ -636,6 +636,71 @@ def divide(general_config, transacting_staker_options, config_file, force, value painting.paint_stakes(emitter=emitter, stakes=STAKEHOLDER.stakes) +@stake.command() +@group_transacting_staker_options +@option_config_file +@option_force +@option_lock_periods +@click.option('--index', help="The staker-specific stake index to prolong", type=click.INT) +@group_general_config +def prolong(general_config, transacting_staker_options, config_file, force, lock_periods, index): + """Prolong an existing stake's duration.""" + + # Setup + emitter = _setup_emitter(general_config) + STAKEHOLDER = transacting_staker_options.create_character(emitter, config_file) + action_period = STAKEHOLDER.staking_agent.get_current_period() + blockchain = transacting_staker_options.get_blockchain() + economics = STAKEHOLDER.economics + + # Handle account selection + client_account, staking_address = handle_client_account_for_staking( + emitter=emitter, + stakeholder=STAKEHOLDER, + staking_address=transacting_staker_options.staker_options.staking_address, + individual_allocation=STAKEHOLDER.individual_allocation, + force=force) + + # Handle stake update and selection + if transacting_staker_options.staker_options.staking_address and index is not None: # 0 is valid. + STAKEHOLDER.stakes = StakeList(registry=STAKEHOLDER.registry, + checksum_address=transacting_staker_options.staker_options.staking_address) + STAKEHOLDER.stakes.refresh() + current_stake = STAKEHOLDER.stakes[index] + else: + current_stake = select_stake(stakeholder=STAKEHOLDER, emitter=emitter) + + # + # Prolong + # + + # Interactive + if not lock_periods: + stake_extension_range = click.IntRange(min=1, max=economics.maximum_allowed_locked, clamp=False) + max_extension = economics.maximum_allowed_locked - current_stake.periods_remaining + lock_periods = click.prompt(f"Enter number of periods to extend (1-{max_extension})", type=stake_extension_range) + if not force: + click.confirm(f"Publish stake extension of {lock_periods} period(s) to the blockchain?", abort=True) + password = transacting_staker_options.get_password(blockchain, client_account) + + # Non-interactive: Consistency check to prevent the above agreement from going stale. + last_second_current_period = STAKEHOLDER.staking_agent.get_current_period() + if action_period != last_second_current_period: + emitter.echo("Current period advanced before transaction was broadcasted. Please try again.", red='red') + raise click.Abort + + # Authenticate and Execute + STAKEHOLDER.assimilate(checksum_address=current_stake.staker_address, password=password) + emitter.echo("Broadcasting Stake Extension...", color='yellow') + receipt = STAKEHOLDER.prolong_stake(stake_index=current_stake.index, additional_periods=lock_periods) + + # Report + emitter.echo('Successfully Prolonged Stake', color='green', verbosity=1) + paint_receipt_summary(emitter=emitter, receipt=receipt, chain_name=blockchain.client.chain_name) + painting.paint_stakes(emitter=emitter, stakes=STAKEHOLDER.stakes) + return # Exit + + @stake.command('collect-reward') @group_transacting_staker_options @option_config_file diff --git a/tests/blockchain/eth/entities/agents/test_staking_escrow_agent.py b/tests/blockchain/eth/entities/agents/test_staking_escrow_agent.py index aa3c01948..3ec7b5eda 100644 --- a/tests/blockchain/eth/entities/agents/test_staking_escrow_agent.py +++ b/tests/blockchain/eth/entities/agents/test_staking_escrow_agent.py @@ -232,14 +232,14 @@ def test_prolong_stake(agency, testerchain, test_registry): staker_account, worker_account, *other = testerchain.unassigned_accounts stakes = list(staking_agent.get_all_stakes(staker_address=staker_account)) - original_termination = stakes[1] + original_termination = stakes[0][1] - receipt = staking_agent.prolong_stake(staker_account=staker_account, stake_index=0, periods=1) + receipt = staking_agent.prolong_stake(staker_address=staker_account, stake_index=0, periods=1) assert receipt['status'] == 1 # Ensure stake was extended by one period. stakes = list(staking_agent.get_all_stakes(staker_address=staker_account)) - new_termination = stakes[1] + new_termination = stakes[0][1] assert new_termination == original_termination + 1 diff --git a/tests/cli/test_felix.py b/tests/cli/test_felix.py index 3ad662b54..93ee8cfc0 100644 --- a/tests/cli/test_felix.py +++ b/tests/cli/test_felix.py @@ -113,10 +113,6 @@ def test_run_felix(click_runner, web_app = felix.make_web_app() test_client = web_app.test_client() - # Load the landing page - response = test_client.get('/') - assert response.status_code == 200 - # Register a new recipient response = test_client.post('/register', data={'address': testerchain.client.accounts[-1]}) assert response.status_code == 200 diff --git a/tests/cli/test_status.py b/tests/cli/test_status.py index 7968becf7..6554ad826 100644 --- a/tests/cli/test_status.py +++ b/tests/cli/test_status.py @@ -18,8 +18,6 @@ along with nucypher. If not, see . import random import re -import pytest - from nucypher.blockchain.eth.agents import ( PolicyManagerAgent, StakingEscrowAgent, @@ -27,9 +25,8 @@ from nucypher.blockchain.eth.agents import ( NucypherTokenAgent, ContractAgency ) -from nucypher.blockchain.eth.registry import InMemoryContractRegistry from nucypher.blockchain.eth.token import NU -from nucypher.cli.status import status +from nucypher.cli.commands.status import status from nucypher.utilities.sandbox.constants import TEST_PROVIDER_URI, MOCK_REGISTRY_FILEPATH diff --git a/tests/cli/ursula/test_stakeholder_and_ursula.py b/tests/cli/ursula/test_stakeholder_and_ursula.py index 366d516b1..9b54b85f8 100644 --- a/tests/cli/ursula/test_stakeholder_and_ursula.py +++ b/tests/cli/ursula/test_stakeholder_and_ursula.py @@ -173,6 +173,38 @@ def test_staker_divide_stakes(click_runner, assert str(NU(token_economics.minimum_allowed_locked, 'NuNit').to_tokens()) in result.output +def test_stake_prolong(click_runner, + testerchain, + test_registry, + manual_staker, + manual_worker, + stakeholder_configuration_file_location): + + prolong_args = ('stake', 'prolong', + '--config-file', stakeholder_configuration_file_location, + '--index', 0, + '--lock-periods', 1, + '--staking-address', manual_staker, + '--force') + + staker = Staker(is_me=True, checksum_address=manual_staker, registry=test_registry) + staker.stakes.refresh() + stake = staker.stakes[0] + old_termination = stake.final_locked_period + + user_input = INSECURE_DEVELOPMENT_PASSWORD + result = click_runner.invoke(nucypher_cli, + prolong_args, + input=user_input, + catch_exceptions=False) + assert result.exit_code == 0 + + # Ensure Integration with Stakes + stake.sync() + new_termination = stake.final_locked_period + assert new_termination == old_termination + 1 + + def test_stake_set_worker(click_runner, testerchain, test_registry, @@ -194,7 +226,6 @@ def test_stake_set_worker(click_runner, assert result.exit_code == 0 staker = Staker(is_me=True, checksum_address=manual_staker, registry=test_registry) - assert staker.worker_address == manual_worker