class Staker(NucypherTokenActor): """ Baseclass for staking-related operations on the blockchain. """ class StakerError(NucypherTokenActor.ActorError): pass class InsufficientTokens(StakerError): pass def __init__(self, is_me: bool, economics: TokenEconomics = None, *args, **kwargs) -> None: super().__init__(*args, **kwargs) self.log = Logger("staker") self.stake_tracker = StakeTracker( checksum_addresses=[self.checksum_address]) self.staking_agent = StakingEscrowAgent(blockchain=self.blockchain) self.economics = economics or TokenEconomics() self.is_me = is_me self.__worker_address = None def to_dict(self) -> dict: stake_info = [stake.to_stake_info() for stake in self.stakes] worker_address = self.worker_address or BlockchainInterface.NULL_ADDRESS staker_funds = { 'ETH': int(self.eth_balance), 'NU': int(self.token_balance) } staker_payload = { 'staker': self.checksum_address, 'balances': staker_funds, 'worker': worker_address, 'stakes': stake_info } return staker_payload @classmethod def from_dict(cls, staker_payload: dict) -> 'Staker': staker = Staker(is_me=True, checksum_address=staker_payload['checksum_address']) return staker @property def stakes(self) -> List[Stake]: stakes = self.stake_tracker.stakes( checksum_address=self.checksum_address) return stakes @property def is_staking(self) -> bool: """Checks if this Staker currently has active stakes / locked tokens.""" return bool(self.stakes) def locked_tokens(self, periods: int = 0) -> NU: """Returns the amount of tokens this staker has locked for a given duration in periods.""" raw_value = self.staking_agent.get_locked_tokens( staker_address=self.checksum_address, periods=periods) value = NU.from_nunits(raw_value) return value @property def current_stake(self) -> NU: """ The total number of staked tokens, either locked or unlocked in the current period. """ if self.stakes: return NU(sum(int(stake.value) for stake in self.stakes), 'NuNit') else: return NU.ZERO() @only_me def divide_stake(self, stake_index: int, target_value: NU, 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." ) # Select stake to divide from local cache 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( f"New expiration {expiration} must be at least 1 period from the " f"current stake's end period ({current_stake.end_period})." ) # Do it already! modified_stake, new_stake = current_stake.divide( target_value=target_value, additional_periods=additional_periods) # Update staking cache element self.stake_tracker.refresh(checksum_addresses=[self.checksum_address]) 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: """Create a new stake.""" # Duration 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) # Value 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.InsufficientTokens( f"Insufficient token balance ({self.token_agent}) " f"for new stake initialization of {amount}") # Ensure the new stake will not exceed the staking limit if (self.current_stake + amount) > self.economics.maximum_allowed_locked: raise Stake.StakingError( f"Cannot divide stake - " f"Maximum stake value exceeded with a target value of {amount}." ) # Write to blockchain new_stake = Stake.initialize_stake(staker=self, amount=amount, lock_periods=lock_periods) # Update stake tracker cache element self.stake_tracker.refresh(checksum_addresses=[self.checksum_address]) return new_stake # # Reward and Collection # @only_me @save_receipt def set_worker(self, worker_address: str) -> str: # TODO: Set a Worker for this staker, not just in StakingEscrow receipt = self.staking_agent.set_worker( staker_address=self.checksum_address, worker_address=worker_address) self.__worker_address = worker_address return receipt @property def worker_address(self) -> str: if self.__worker_address: return self.__worker_address else: worker_address = self.staking_agent.get_worker_from_staker( staker_address=self.checksum_address) self.__worker_address = worker_address if self.__worker_address == BlockchainInterface.NULL_ADDRESS: return NO_WORKER_ASSIGNED.bool_value(False) return self.__worker_address @only_me @save_receipt def mint(self) -> Tuple[str, str]: """Computes and transfers tokens to the staker's account""" receipt = self.staking_agent.mint(staker_address=self.checksum_address) return receipt def calculate_reward(self) -> int: staking_reward = self.staking_agent.calculate_staking_reward( staker_address=self.checksum_address) return staking_reward @only_me @save_receipt def collect_policy_reward(self, collector_address=None, policy_agent: PolicyManagerAgent = None): """Collect rewarded ETH""" policy_agent = policy_agent or PolicyManagerAgent( blockchain=self.blockchain) withdraw_address = collector_address or self.checksum_address receipt = policy_agent.collect_policy_reward( collector_address=withdraw_address, staker_address=self.checksum_address) return receipt @only_me @save_receipt def collect_staking_reward(self) -> str: """Withdraw tokens rewarded for staking.""" receipt = self.staking_agent.collect_staking_reward( staker_address=self.checksum_address) return receipt @only_me @save_receipt def withdraw(self, amount: NU) -> str: """Withdraw tokens (assuming they're unlocked)""" receipt = self.staking_agent.withdraw( staker_address=self.checksum_address, amount=int(amount)) return receipt