Relocate staking methods to the Stake class, away from Actors; Staking API bug fixes and consistency check.

pull/866/head
Kieran Prasch 2019-04-16 03:59:45 +03:00
parent 07aa603d30
commit 13d4b5c448
No known key found for this signature in database
GPG Key ID: 199AB839D4125A62
6 changed files with 321 additions and 238 deletions

View File

@ -16,28 +16,38 @@ along with nucypher. If not, see <https://www.gnu.org/licenses/>.
""" """
import json import json
from collections import OrderedDict from datetime import datetime
from json import JSONDecodeError from json import JSONDecodeError
from typing import Tuple, List, Dict, Union
import maya import maya
from constant_sorrow import constants from constant_sorrow import constants
from constant_sorrow.constants import CONTRACT_NOT_DEPLOYED, NO_DEPLOYER_ADDRESS from constant_sorrow.constants import CONTRACT_NOT_DEPLOYED, NO_DEPLOYER_ADDRESS, EMPTY_STAKING_SLOT
from datetime import datetime
from twisted.internet import task, reactor from twisted.internet import task, reactor
from twisted.logger import Logger from twisted.logger import Logger
from typing import Tuple, List, Dict, Union
from nucypher.blockchain.economics import TokenEconomics from nucypher.blockchain.economics import TokenEconomics
from nucypher.blockchain.eth.agents import NucypherTokenAgent, MinerAgent, PolicyAgent, MiningAdjudicatorAgent, \ from nucypher.blockchain.eth.agents import (
NucypherTokenAgent,
MinerAgent,
PolicyAgent,
MiningAdjudicatorAgent,
EthereumContractAgent EthereumContractAgent
)
from nucypher.blockchain.eth.chains import Blockchain from nucypher.blockchain.eth.chains import Blockchain
from nucypher.blockchain.eth.deployers import NucypherTokenDeployer, MinerEscrowDeployer, PolicyManagerDeployer, \ from nucypher.blockchain.eth.deployers import (
UserEscrowProxyDeployer, UserEscrowDeployer, MiningAdjudicatorDeployer NucypherTokenDeployer,
MinerEscrowDeployer,
PolicyManagerDeployer,
UserEscrowProxyDeployer,
UserEscrowDeployer,
MiningAdjudicatorDeployer
)
from nucypher.blockchain.eth.interfaces import BlockchainDeployerInterface from nucypher.blockchain.eth.interfaces import BlockchainDeployerInterface
from nucypher.blockchain.eth.registry import AllocationRegistry from nucypher.blockchain.eth.registry import AllocationRegistry
from nucypher.blockchain.eth.token import NU, Stake
from nucypher.blockchain.eth.utils import (datetime_to_period, from nucypher.blockchain.eth.utils import (datetime_to_period,
calculate_period_duration) calculate_period_duration)
from nucypher.blockchain.eth.token import NU, Stake
def only_me(func): def only_me(func):
@ -56,10 +66,7 @@ class NucypherTokenActor:
class ActorError(Exception): class ActorError(Exception):
pass pass
def __init__(self, def __init__(self, checksum_address: str = None, blockchain: Blockchain = None):
checksum_address: str = None,
blockchain: Blockchain = None
) -> None:
""" """
:param checksum_address: If not passed, we assume this is an unknown actor :param checksum_address: If not passed, we assume this is an unknown actor
""" """
@ -322,6 +329,7 @@ class Miner(NucypherTokenActor):
self.__uptime_period = constants.NO_STAKES self.__uptime_period = constants.NO_STAKES
self.__terminal_period = constants.NO_STAKES self.__terminal_period = constants.NO_STAKES
# Read on-chain stakes
self.__read_stakes() self.__read_stakes()
if self.stakes and start_staking_loop: if self.stakes and start_staking_loop:
self.stake() self.stake()
@ -329,11 +337,14 @@ class Miner(NucypherTokenActor):
# #
# Staking # Staking
# #
@only_me @only_me
def stake(self, confirm_now: bool = True) -> None: def stake(self, confirm_now: bool = True) -> None:
"""High-level staking looping call initialization""" """
# TODO #841: Check if there is an active stake in the current period: Resume staking daemon High-level staking looping call initialization, this function aims
to be safely called at any time - For example, it is okay to call
this function multiple time within the same period.
"""
# Get the last stake end period of all stakes # Get the last stake end period of all stakes
terminal_period = max(stake.end_period for stake in self.stakes.values()) terminal_period = max(stake.end_period for stake in self.stakes.values())
@ -343,10 +354,15 @@ class Miner(NucypherTokenActor):
# record start time and periods # record start time and periods
self.__start_time = maya.now() self.__start_time = maya.now()
self.__uptime_period = self.miner_agent.get_current_period() self.__uptime_period = self.miner_agent.get_current_period()
self.__terminal_period = self.__uptime_period + terminal_period self.__terminal_period = terminal_period
self.__current_period = self.__uptime_period self.__current_period = self.__uptime_period
self.start_staking_loop() self.start_staking_loop()
@property
def last_active_period(self) -> int:
period = self.miner_agent.get_last_active_period(address=self.checksum_public_address)
return period
@only_me @only_me
def _confirm_period(self): def _confirm_period(self):
@ -355,10 +371,16 @@ class Miner(NucypherTokenActor):
if self.__current_period != period: if self.__current_period != period:
# check for stake expiration # Let's see how much time has passed
delta = self.__current_period - self.last_active_period
if delta > 1:
self.log.warn(f"{delta} Missed staking confirmation(s) detected")
self.__read_stakes() # Invalidate the stake cache
# Check for stake expiration
stake_expired = self.__current_period >= self.__terminal_period stake_expired = self.__current_period >= self.__terminal_period
if stake_expired: if stake_expired:
self.log.info('Stake duration expired') self.log.info('STOPPED STAKING - Final stake ended.')
return True return True
self.confirm_activity() self.confirm_activity()
@ -394,14 +416,14 @@ class Miner(NucypherTokenActor):
return d return d
@property @property
def is_staking(self): def is_staking(self) -> bool:
"""Checks if this Miner currently has locked tokens.""" """Checks if this Miner currently has locked tokens."""
return bool(self.locked_tokens > 0) return bool(self.locked_tokens > 0)
@property @property
def locked_tokens(self): def locked_tokens(self) -> NU:
"""Returns the amount of tokens this miner has locked.""" """Returns the amount of tokens this miner has locked."""
return self.miner_agent.get_locked_tokens(miner_address=self.checksum_public_address) return NU(self.miner_agent.get_locked_tokens(miner_address=self.checksum_public_address), 'NU')
@property @property
def total_staked(self) -> NU: def total_staked(self) -> NU:
@ -410,15 +432,93 @@ class Miner(NucypherTokenActor):
else: else:
return NU.ZERO() return NU.ZERO()
@only_me
def divide_stake(self,
stake_index: int,
target_value: NU,
additional_periods: int = None,
expiration: maya.MayaDT = None) -> tuple:
# Validate function input
if additional_periods and expiration:
raise ValueError("Pass the number of lock periods or an expiration MayaDT; not both.")
# Select stake to divide
try:
current_stake = self.stakes[stake_index]
except KeyError:
if len(self.stakes):
message = f"Cannot divide stake - No stake exists with index {stake_index}."
else:
message = "Cannot divide stake - There are no active stakes."
raise Stake.StakingError(message)
# Calculate stake duration in periods
if expiration:
additional_periods = datetime_to_period(datetime=expiration) - current_stake.end_period
if additional_periods <= 0:
raise Stake.StakingError("Expiration {} must be at least 1 period from now.".format(expiration))
# Check the new stake will not exceed the staking limit
if (self.total_staked + target_value) > self.economics.maximum_allowed_locked:
raise Stake.StakingError(f"Cannot divide stake - Maximum stake value exceeded with a target value of {target_value}.")
# Do it already!
modified_stake, new_stake = current_stake.divide(target_value=target_value,
additional_periods=additional_periods)
# Update stake cache
self.__read_stakes()
return modified_stake, new_stake
@only_me
def initialize_stake(self,
amount: NU,
lock_periods: int = None,
expiration: maya.MayaDT = None,
entire_balance: bool = False) -> Stake:
if lock_periods and expiration:
raise ValueError("Pass the number of lock periods or an expiration MayaDT; not both.")
if expiration:
lock_periods = calculate_period_duration(future_time=expiration)
if entire_balance and amount:
raise ValueError("Specify an amount or entire balance, not both")
if entire_balance:
amount = self.token_balance
if not self.token_balance >= amount:
raise self.MinerError(f"Insufficient token balance ({self.token_agent}) for new stake initialization of {amount}")
# Write to blockchain
stake = Stake.initialize_stake(miner=self, amount=amount, lock_periods=lock_periods)
# Update local on-chain stake cache
self.__read_stakes()
return stake
#
# Staking Cache
#
def __read_stakes(self) -> None: def __read_stakes(self) -> None:
stakes_reader = self.miner_agent.get_all_stakes(miner_address=self.checksum_public_address) stakes_reader = self.miner_agent.get_all_stakes(miner_address=self.checksum_public_address)
stakes = dict() stakes = dict()
terminal_period = 0
for index, stake_info in enumerate(stakes_reader): for index, stake_info in enumerate(stakes_reader):
stake = Stake.from_stake_info(owner_address=self.checksum_public_address, if not stake_info:
stake_info=stake_info, stake = EMPTY_STAKING_SLOT
index=index, else:
economics=self.economics) stake = Stake.from_stake_info(miner=self, stake_info=stake_info, index=index)
if stake.end_period > terminal_period:
terminal_period = stake.end_period
stakes[index] = stake stakes[index] = stake
self.__terminal_period = terminal_period
self.__stakes = stakes self.__stakes = stakes
@property @property
@ -426,160 +526,8 @@ class Miner(NucypherTokenActor):
"""Return all cached stakes from the blockchain.""" """Return all cached stakes from the blockchain."""
return self.__stakes return self.__stakes
@only_me def refresh_staking_cache(self):
def deposit(self, amount: int, lock_periods: int) -> Tuple[str, str]: return self.__read_stakes()
"""Public facing method for token locking."""
approve_txhash = self.token_agent.approve_transfer(amount=amount,
target_address=self.miner_agent.contract_address,
sender_address=self.checksum_public_address)
deposit_txhash = self.miner_agent.deposit_tokens(amount=amount,
lock_periods=lock_periods,
sender_address=self.checksum_public_address)
return approve_txhash, deposit_txhash
@only_me
def divide_stake(self,
stake_index: int,
target_value: NU,
additional_periods: int = None,
expiration: maya.MayaDT = None) -> dict:
"""
Modifies the unlocking schedule and value of already locked tokens.
This actor requires that is_me is True, and that the expiration datetime is after the existing
locking schedule of this miner, or an exception will be raised.
:param stake_index: The miner's stake index of the stake to divide
:param additional_periods: The number of periods to extend the stake by
:param target_value: The quantity of tokens in the smallest denomination to divide.
:param expiration: The new expiration date to set as an end period for stake division.
:return: Returns the blockchain transaction hash
"""
# Validate function input
if additional_periods and expiration:
raise ValueError("Pass the number of lock periods or an expiration MayaDT; not both.")
# Re-read stakes from blockchain and select stake to divide
self.__read_stakes()
current_stake = self.stakes[stake_index]
# Ensure selected stake is active
if current_stake.is_expired:
raise self.MinerError(f'Cannot dive an expired stake')
# Validate stake division parameters
if expiration:
additional_periods = datetime_to_period(datetime=expiration) - current_stake.end_period
if additional_periods <= 0:
raise self.MinerError("Expiration {} must be at least 1 period from now.".format(expiration))
if target_value >= current_stake.value:
raise self.MinerError(f"Cannot divide stake; Value ({target_value}) must be less "
f"than the existing stake value {current_stake.value}.")
#
# Generate Stakes
#
# Modified Original Stake
new_stake_1 = Stake(owner_address=self.checksum_public_address,
index=len(self.stakes)+1,
start_period=current_stake.start_period,
end_period=current_stake.end_period,
value=target_value,
economics=self.economics)
# New Derived Stake
new_stake_2 = Stake(owner_address=self.checksum_public_address,
index=len(self.stakes)+1,
start_period=current_stake.start_period,
end_period=current_stake.end_period + additional_periods,
value=current_stake.value - target_value,
economics=self.economics)
# Ensure both halves are for valid amounts
new_stake_1.validate_value()
new_stake_2.validate_value()
# Transmit the stake division transaction
tx = self.miner_agent.divide_stake(miner_address=self.checksum_public_address,
stake_index=stake_index,
target_value=int(target_value),
periods=additional_periods)
self.blockchain.wait_for_receipt(tx)
# Update stake cache
self.__read_stakes()
return tx
@only_me
def __validate_stake(self, stake: Stake) -> bool:
stake.validate_value()
stake.validate_duration()
if not self.token_balance >= stake.value:
raise self.MinerError(f"Insufficient token balance ({self.token_agent}) for new stake of {stake.value}")
else:
return True
@only_me
def initialize_stake(self,
amount: NU,
lock_periods: int = None,
expiration: maya.MayaDT = None,
entire_balance: bool = False) -> dict:
"""
High level staking method for Miners.
:param amount: Amount of tokens to stake denominated in the smallest unit.
:param lock_periods: Duration of stake in periods.
:param expiration: A MayaDT object representing the time the stake expires; used to calculate lock_periods.
:param entire_balance: If True, stake the entire balance of this node, or the maximum possible.
"""
if lock_periods and expiration:
raise ValueError("Pass the number of lock periods or an expiration MayaDT; not both.")
if entire_balance and amount:
raise self.MinerError("Specify an amount or entire balance, not both")
if expiration:
lock_periods = calculate_period_duration(future_time=expiration)
if entire_balance is True:
amount = self.token_balance
amount = NU(int(amount), 'NuNit')
current_period = self.miner_agent.get_current_period()
stake = Stake(owner_address=self.checksum_public_address,
start_period=current_period+1,
end_period=current_period + lock_periods,
value=amount,
economics=self.economics)
staking_transactions = OrderedDict() # type: OrderedDict # Time series of txhases
# Validate
assert self.__validate_stake(stake=stake)
# Transact
approve_txhash, initial_deposit_txhash = self.deposit(amount=int(amount), lock_periods=lock_periods)
self._transaction_cache.append((datetime.utcnow(), initial_deposit_txhash))
staking_transactions['approve'] = approve_txhash
staking_transactions['deposit'] = initial_deposit_txhash
self.__read_stakes() # update local on-chain stake cache
self.log.info("{} Initialized new stake: {} tokens for {} periods".format(self.checksum_public_address, amount, lock_periods))
return staking_transactions
# #
# Reward and Collection # Reward and Collection

View File

@ -174,6 +174,10 @@ class MinerAgent(EthereumContractAgent):
self.blockchain.wait_for_receipt(tx) self.blockchain.wait_for_receipt(tx)
return tx return tx
def get_last_active_period(self, address: str) -> int:
period = self.contract.functions.getLastActivePeriod(address).call()
return int(period)
def confirm_activity(self, node_address: str) -> str: def confirm_activity(self, node_address: str) -> str:
"""Miner rewarded for every confirmed period""" """Miner rewarded for every confirmed period"""
@ -496,4 +500,4 @@ class MiningAdjudicatorAgent(EthereumContractAgent):
:param precomputed_data: :param precomputed_data:
:return: :return:
""" """
# TODO: #931 - Challenge Agent and Actor # TODO: #931 - Challenge Agent and Actor - "Investigator"

View File

@ -1,13 +1,12 @@
from _pydecimal import Decimal from _pydecimal import Decimal
import eth_utils
import maya
from eth_utils import currency
from nacl.hash import sha256
from typing import Union, Tuple from typing import Union, Tuple
from nucypher.blockchain.economics import TokenEconomics import maya
from nucypher.blockchain.eth.agents import NucypherTokenAgent from constant_sorrow.constants import NEW_STAKE, NO_STAKING_RECEIPT
from eth_utils import currency
from twisted.logger import Logger
from nucypher.blockchain.eth.agents import MinerAgent
from nucypher.blockchain.eth.utils import datetime_at_period, datetime_to_period from nucypher.blockchain.eth.utils import datetime_at_period, datetime_to_period
@ -132,17 +131,23 @@ class Stake:
A quantity of tokens and staking duration for one stake for one miner. A quantity of tokens and staking duration for one stake for one miner.
""" """
class StakingError(Exception):
"""Raised when a staking operation cannot be executed due to failure."""
__ID_LENGTH = 16 __ID_LENGTH = 16
def __init__(self, def __init__(self,
owner_address: str, miner,
value: NU, value: NU,
start_period: int, start_period: int,
end_period: int, end_period: int,
index: int = None, index: int,
economics: TokenEconomics = None,
validate_now: bool = True): validate_now: bool = True):
self.miner = miner
owner_address = miner.checksum_public_address
self.log = Logger(f'stake-{owner_address}-{index}')
# Stake Metadata # Stake Metadata
self.owner_address = owner_address self.owner_address = owner_address
self.index = index self.index = index
@ -156,16 +161,23 @@ class Stake:
self.end_datetime = datetime_at_period(period=end_period) self.end_datetime = datetime_at_period(period=end_period)
self.duration_delta = self.end_datetime - self.start_datetime self.duration_delta = self.end_datetime - self.start_datetime
self.blockchain = miner.blockchain
# Agency
self.miner_agent = miner.miner_agent
self.token_agent = miner.token_agent
# Economics # Economics
if not economics: self.economics = miner.economics
economics = TokenEconomics() self.minimum_nu = NU(int(self.economics.minimum_allowed_locked), 'NuNit')
self.economics = economics self.maximum_nu = NU(int(self.economics.maximum_allowed_locked), 'NuNit')
self.minimum_nu = NU(int(economics.minimum_allowed_locked), 'NuNit')
self.maximum_nu = NU(int(economics.maximum_allowed_locked), 'NuNit')
if validate_now: if validate_now:
self.validate_duration() self.validate_duration()
self.transactions = NO_STAKING_RECEIPT
self.receipt = NO_STAKING_RECEIPT
def __repr__(self) -> str: def __repr__(self) -> str:
r = f'Stake(index={self.index}, value={self.value}, end_period={self.end_period})' r = f'Stake(index={self.index}, value={self.value}, end_period={self.end_period})'
return r return r
@ -173,55 +185,46 @@ class Stake:
def __eq__(self, other) -> bool: def __eq__(self, other) -> bool:
return bool(self.value == other.value) return bool(self.value == other.value)
@property #
def is_active(self): # Metadata
now = maya.now() #
if now >= self.end_datetime:
return False
else:
return True
@property @property
def is_expired(self): def is_expired(self) -> bool:
now = maya.now() current_period = self.miner_agent.get_current_period()
if now >= self.end_datetime: if current_period >= self.end_period:
return True return True
else: else:
return False return False
@property
def is_active(self) -> bool:
return not self.is_expired
@classmethod @classmethod
def from_stake_info(cls, def from_stake_info(cls,
owner_address: str, miner,
index: int, index: int,
stake_info: Tuple[int, int, int], stake_info: Tuple[int, int, int]
economics: TokenEconomics) -> 'Stake': ) -> 'Stake':
"""Reads staking values as they exist on the blockchain""" """Reads staking values as they exist on the blockchain"""
start_period, end_period, value = stake_info start_period, end_period, value = stake_info
instance = cls(owner_address=owner_address,
instance = cls(miner=miner,
index=index, index=index,
start_period=start_period, start_period=start_period,
end_period=end_period, end_period=end_period,
value=NU(value, 'NuNit'), value=NU(value, 'NuNit'))
economics=economics)
return instance return instance
def to_stake_info(self) -> Tuple[int, int, int]: def to_stake_info(self) -> Tuple[int, int, int]:
"""Returns a tuple representing the blockchain record of a stake""" """Returns a tuple representing the blockchain record of a stake"""
return self.start_period, self.end_period, int(self.value) return self.start_period, self.end_period, int(self.value)
@property #
def id(self) -> str: # Duration
"""TODO: Unique staking ID, currently unused""" #
digest_elements = list()
digest_elements.append(eth_utils.to_canonical_address(address=self.owner_address))
digest_elements.append(str(self.index).encode())
digest_elements.append(str(self.start_period).encode())
digest_elements.append(str(self.end_period).encode())
digest_elements.append(str(self.value).encode())
digest = b'|'.join(digest_elements)
stake_id = sha256(digest).hex()[:16] # type: str
return stake_id[:self.__ID_LENGTH]
@property @property
def periods_remaining(self) -> int: def periods_remaining(self) -> int:
@ -240,6 +243,10 @@ class Stake:
result = delta.seconds result = delta.seconds
return result return result
#
# Validation
#
@staticmethod @staticmethod
def __handle_validation_failure(rulebook: Tuple[Tuple[bool, str], ...]) -> bool: def __handle_validation_failure(rulebook: Tuple[Tuple[bool, str], ...]) -> bool:
"""Validate a staking rulebook""" """Validate a staking rulebook"""
@ -275,14 +282,134 @@ class Stake:
rulebook = ( rulebook = (
(self.economics.minimum_locked_periods <= self.duration, (self.economics.minimum_locked_periods <= self.duration,
'Locktime ({duration}) too short; must be at least {minimum}' 'Stake duration of ({duration}) is too short; must be at least {minimum} periods.'
.format(minimum=self.economics.minimum_locked_periods, duration=self.duration)), .format(minimum=self.economics.minimum_locked_periods, duration=self.duration)),
(self.economics.maximum_locked_periods >= self.duration, (self.economics.maximum_locked_periods >= self.duration,
'Locktime ({duration}) too long; must be no more than {maximum}' 'Stake duration of ({duration}) is too long; must be no more than {maximum} periods.'
.format(maximum=self.economics.maximum_locked_periods, duration=self.duration)), .format(maximum=self.economics.maximum_locked_periods, duration=self.duration)),
) )
if raise_on_fail is True: if raise_on_fail is True:
self.__handle_validation_failure(rulebook=rulebook) self.__handle_validation_failure(rulebook=rulebook)
return all(rulebook) return all(rulebook)
#
# Blockchain
#
@classmethod
def __deposit(cls, miner, amount: int, lock_periods: int) -> Tuple[str, str]:
"""Public facing method for token locking."""
approve_txhash = miner.token_agent.approve_transfer(amount=amount,
target_address=miner.miner_agent.contract_address,
sender_address=miner.checksum_public_address)
deposit_txhash = miner.miner_agent.deposit_tokens(amount=amount,
lock_periods=lock_periods,
sender_address=miner.checksum_public_address)
return approve_txhash, deposit_txhash
def divide(self, target_value: NU, additional_periods: int = None) -> tuple:
"""
Modifies the unlocking schedule and value of already locked tokens.
This actor requires that is_me is True, and that the expiration datetime is after the existing
locking schedule of this miner, or an exception will be raised.
"""
# Re-read stakes from blockchain and select stake to divide
current_stake = self
# Ensure selected stake is active
if current_stake.is_expired:
raise self.StakingError(f'Cannot divide an expired stake')
if target_value >= current_stake.value:
raise self.StakingError(f"Cannot divide stake; Target value ({target_value}) must be less "
f"than the existing stake value {current_stake.value}.")
#
# Generate SubStakes
#
# Modified Original Stake
modified_stake = Stake(miner=self.miner,
index=self.index,
start_period=current_stake.start_period,
end_period=current_stake.end_period,
value=target_value)
# New Derived Stake
end_period = current_stake.end_period + additional_periods
new_stake = Stake(miner=self.miner,
start_period=current_stake.start_period,
end_period=end_period,
value=current_stake.value - target_value,
index=NEW_STAKE)
#
# Validate
#
# Ensure both halves are for valid amounts
modified_stake.validate_value()
new_stake.validate_value()
# Detect recycled or inconsistent slot then check start period or fail
stake_info = self.miner_agent.get_stake_info(miner_address=self.owner_address, stake_index=self.index)
first_period, last_period, locked_value = stake_info
if not modified_stake.start_period == first_period:
raise self.StakingError("Inconsistent staking cache, aborting stake division. ")
#
# Transact
#
# Transmit the stake division transaction
tx = self.miner_agent.divide_stake(miner_address=self.owner_address,
stake_index=self.index,
target_value=int(target_value),
periods=additional_periods)
receipt = self.blockchain.wait_for_receipt(tx)
new_stake.receipt = receipt
return modified_stake, new_stake
@classmethod
def initialize_stake(cls, miner, amount: NU, lock_periods: int = None) -> 'Stake':
# Value
amount = NU(int(amount), 'NuNit')
# Duration
current_period = miner.miner_agent.get_current_period()
end_period = current_period + lock_periods
stake = Stake(miner=miner,
start_period=current_period+1,
end_period=end_period,
value=amount,
index=NEW_STAKE)
# Validate
stake.validate_value()
stake.validate_duration()
# Transact
approve_txhash, initial_deposit_txhash = stake.__deposit(amount=int(amount),
lock_periods=lock_periods,
miner=miner)
# Store the staking transactions on the instance
staking_transactions = dict(approve=approve_txhash, deposit=initial_deposit_txhash)
stake.transactions = staking_transactions
# Log and return Stake instance
log = Logger(f'stake-{miner.checksum_public_address}-creation')
log.info("{} Initialized new stake: {} tokens for {} periods".format(miner.checksum_public_address,
amount,
lock_periods))
return stake

View File

@ -364,13 +364,13 @@ def ursula(click_config,
click.confirm("Is this correct?", abort=True) click.confirm("Is this correct?", abort=True)
txhash_bytes = URSULA.divide_stake(stake_index=index, modified_stake, new_stake = URSULA.divide_stake(stake_index=index,
target_value=value, target_value=value,
additional_periods=extension) additional_periods=extension)
if not quiet: if not quiet:
click.secho('Successfully divided stake', fg='green') click.secho('Successfully divided stake', fg='green')
click.secho(f'Transaction Hash ........... {txhash_bytes.hex()}') click.secho(f'Transaction Hash ........... {new_stake.receipt}')
# Show the resulting stake list # Show the resulting stake list
painting.paint_stakes(stakes=URSULA.stakes) painting.paint_stakes(stakes=URSULA.stakes)
@ -421,8 +421,8 @@ def ursula(click_config,
if not force: if not force:
click.confirm("Publish staged stake to the blockchain?", abort=True) click.confirm("Publish staged stake to the blockchain?", abort=True)
staking_transactions = URSULA.initialize_stake(amount=int(value), lock_periods=duration) stake = URSULA.initialize_stake(amount=int(value), lock_periods=duration)
painting.paint_staking_confirmation(ursula=URSULA, transactions=staking_transactions) painting.paint_staking_confirmation(ursula=URSULA, transactions=stake.transactions)
return return
elif action == 'confirm-activity': elif action == 'confirm-activity':

View File

@ -82,9 +82,8 @@ def test_miner_divides_stake(miner, token_economics):
expected_yet_another_stake = Stake(start_period=current_period + 1, expected_yet_another_stake = Stake(start_period=current_period + 1,
end_period=current_period + 34, end_period=current_period + 34,
value=yet_another_stake_value, value=yet_another_stake_value,
owner_address=miner.checksum_public_address, miner=miner,
index=3, index=3)
economics=token_economics)
assert 4 == len(miner.stakes), 'A new stake was not added after two stake divisions' assert 4 == len(miner.stakes), 'A new stake was not added after two stake divisions'
assert expected_old_stake == miner.stakes[stake_index + 1].to_stake_info(), 'Old stake values are invalid after two stake divisions' assert expected_old_stake == miner.stakes[stake_index + 1].to_stake_info(), 'Old stake values are invalid after two stake divisions'

View File

@ -3,6 +3,7 @@ from decimal import InvalidOperation, Decimal
import pytest import pytest
from web3 import Web3 from web3 import Web3
from nucypher.blockchain.economics import TokenEconomics
from nucypher.blockchain.eth.token import NU, Stake from nucypher.blockchain.eth.token import NU, Stake
from nucypher.utilities.sandbox.constants import INSECURE_DEVELOPMENT_PASSWORD from nucypher.utilities.sandbox.constants import INSECURE_DEVELOPMENT_PASSWORD
@ -98,21 +99,25 @@ def test_NU(token_economics):
_nan = NU(float('NaN'), 'NU') _nan = NU(float('NaN'), 'NU')
def test_stake(): def test_stake(testerchain, three_agents):
class FakeUrsula: class FakeUrsula:
token_agent, miner_agent, _policy_agent = three_agents
burner_wallet = Web3().eth.account.create(INSECURE_DEVELOPMENT_PASSWORD) burner_wallet = Web3().eth.account.create(INSECURE_DEVELOPMENT_PASSWORD)
checksum_public_address = burner_wallet.address checksum_public_address = burner_wallet.address
miner_agent = None miner_agent = miner_agent
token_agent = token_agent
blockchain = testerchain
economics = TokenEconomics()
ursula = FakeUrsula() ursula = FakeUrsula()
stake = Stake(owner_address=ursula.checksum_public_address, stake = Stake(miner=ursula,
start_period=1, start_period=1,
end_period=100, end_period=100,
value=NU(100, 'NU'), value=NU(100, 'NU'),
index=0) index=0)
assert len(stake.id) == 16
assert stake.value, 'NU' == NU(100, 'NU') assert stake.value, 'NU' == NU(100, 'NU')
assert isinstance(stake.time_remaining(), int) # seconds assert isinstance(stake.time_remaining(), int) # seconds