Beispiel #1
0
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