def test_init_ursula_stake(click_runner, configuration_file_location,
                           funded_blockchain, stake_value, token_economics):

    stake_args = ('ursula', 'stake', '--config-file',
                  configuration_file_location, '--value',
                  stake_value.to_tokens(), '--duration',
                  token_economics.minimum_locked_periods, '--force')

    result = click_runner.invoke(nucypher_cli,
                                 stake_args,
                                 input=INSECURE_DEVELOPMENT_PASSWORD,
                                 catch_exceptions=False)
    assert result.exit_code == 0

    with open(configuration_file_location, 'r') as config_file:
        config_data = json.loads(config_file.read())

    # Verify the stake is on-chain
    staking_agent = StakingEscrowAgent()
    stakes = list(
        staking_agent.get_all_stakes(
            staker_address=config_data['checksum_address']))
    assert len(stakes) == 1
    start_period, end_period, value = stakes[0]
    assert NU(int(value), 'NuNit') == stake_value
Exemplo n.º 2
0
class StakeTracker:

    REFRESH_RATE = 60

    tracking_addresses = set()

    __stakes = dict()  # type: Dict[str: List[Stake]]
    __actions = list()  # type: List[Tuple[Callable, tuple]]

    def __init__(self,
                 checksum_addresses: List[str],
                 refresh_rate: int = None,
                 start_now: bool = False,
                 *args,
                 **kwargs):

        super().__init__(*args, **kwargs)

        self.log = Logger('stake-tracker')
        self.staking_agent = StakingEscrowAgent()

        self._refresh_rate = refresh_rate or self.REFRESH_RATE
        self._tracking_task = task.LoopingCall(self.__update)

        self.__current_period = None
        self.__stakes = dict()
        self.__start_time = NOT_STAKING
        self.__uptime_period = NOT_STAKING
        self.__terminal_period = NOT_STAKING
        self._abort_on_stake_tracking_error = True

        # "load-in":  Read on-chain stakes
        for checksum_address in checksum_addresses:
            if not is_checksum_address(checksum_address):
                raise ValueError(
                    f'{checksum_address} is not a valid EIP-55 checksum address'
                )
            self.tracking_addresses.add(checksum_address)

        if start_now:
            self.start()  # deamonize
        else:
            self.refresh(checksum_addresses=checksum_addresses)  # read-once

    @validate_checksum_address
    def __getitem__(self, checksum_address: str):
        stakes = self.stakes(checksum_address=checksum_address)
        return stakes

    def add_action(self, func: Callable, args=()) -> None:
        self.__actions.append((func, args))

    def clear_actions(self) -> None:
        self.__actions.clear()

    @property
    def current_period(self):
        return self.__current_period

    @validate_checksum_address
    def stakes(self, checksum_address: str) -> List[Stake]:
        """Return all cached stake instances from the blockchain."""
        try:
            return self.__stakes[checksum_address]
        except KeyError:
            return NO_STAKES.bool_value(False)
        except TypeError:
            if self.__stakes in (UNKNOWN_STAKES, NO_STAKES):
                return NO_STAKES.bool_value(False)
            raise

    @validate_checksum_address
    def refresh(self, checksum_addresses: List[str] = None) -> None:
        """Public staking cache invalidation method"""
        return self.__read_stakes(checksum_addresses=checksum_addresses)

    def stop(self) -> None:
        self._tracking_task.stop()
        self.log.info(f"STOPPED STAKE TRACKING")

    def start(self, force: bool = False) -> None:
        """
        High-level stake tracking initialization, this function aims
        to be safely called at any time - For example, it is okay to call
        this function multiple times within the same period.
        """
        if self._tracking_task.running and not force:
            return

        # Record the start time and period
        self.__start_time = maya.now()
        self.__uptime_period = self.staking_agent.get_current_period()
        self.__current_period = self.__uptime_period

        d = self._tracking_task.start(interval=self._refresh_rate)
        d.addErrback(self.handle_tracking_errors)
        self.log.info(
            f"STARTED STAKE TRACKING for {len(self.tracking_addresses)} addresses"
        )

    def _crash_gracefully(self, failure=None) -> None:
        """
        A facility for crashing more gracefully in the event that
        an exception is unhandled in a different thread.
        """
        self._crashed = failure
        failure.raiseException()

    def handle_tracking_errors(self, *args, **kwargs) -> None:
        failure = args[0]
        if self._abort_on_stake_tracking_error:
            self.log.critical(
                f"Unhandled error during node stake tracking. {failure}")
            reactor.callFromThread(self._crash_gracefully, failure=failure)
        else:
            self.log.warn(
                f"Unhandled error during stake tracking: {failure.getTraceback()}"
            )

    def __update(self) -> None:
        self.log.info(
            f"Checking for new period. Current period is {self.__current_period}"
        )
        onchain_period = self.staking_agent.get_current_period(
        )  # < -- Read from contract
        if self.__current_period != onchain_period:
            self.__current_period = onchain_period
            self.__read_stakes()
            for action, args in self.__actions:
                action(*args)

    @validate_checksum_address
    def __read_stakes(self, checksum_addresses: List[str] = None) -> None:
        """Rewrite the local staking cache by reading on-chain stakes"""

        if not checksum_addresses:
            checksum_addresses = self.tracking_addresses

        for checksum_address in checksum_addresses:

            if not is_checksum_address(checksum_address):
                if self._abort_on_stake_tracking_error:
                    raise ValueError(
                        f'{checksum_address} is not a valid EIP-55 checksum address'
                    )
                self.tracking_addresses.remove(checksum_address)  # Prune

            existing_records = len(
                self.stakes(checksum_address=checksum_address))

            # Candidate replacement cache values
            onchain_stakes, terminal_period = list(), 0

            # Read from blockchain
            stakes_reader = self.staking_agent.get_all_stakes(
                staker_address=checksum_address)
            for onchain_index, stake_info in enumerate(stakes_reader):

                if not stake_info:
                    onchain_stake = EMPTY_STAKING_SLOT

                else:
                    onchain_stake = Stake.from_stake_info(
                        checksum_address=checksum_address,
                        stake_info=stake_info,
                        index=onchain_index)

                    # rack the latest terminal period
                    if onchain_stake.end_period > terminal_period:
                        terminal_period = onchain_stake.end_period

                # Store the replacement stake
                onchain_stakes.append(onchain_stake)

            # Commit the new stake and terminal values to the cache
            if not onchain_stakes:
                self.__stakes[checksum_address] = NO_STAKES.bool_value(False)
            else:
                self.__terminal_period = terminal_period
                self.__stakes[checksum_address] = onchain_stakes
                new_records = existing_records - len(
                    self.__stakes[checksum_address])
                self.log.debug(
                    f"Updated local staking cache ({new_records} new stakes).")

            # Record most recent cache update
            self.__updated = maya.now()
Exemplo n.º 3
0
class StakeHolder(BaseConfiguration):

    _NAME = 'stakeholder'
    TRANSACTION_GAS = {}

    class NoFundingAccount(BaseConfiguration.ConfigurationError):
        pass

    class NoStakes(BaseConfiguration.ConfigurationError):
        pass

    def __init__(self,
                 blockchain: BlockchainInterface,
                 sync_now: bool = True,
                 *args,
                 **kwargs):

        super().__init__(*args, **kwargs)

        self.log = Logger(f"stakeholder")

        # Blockchain and Contract connection
        self.blockchain = blockchain
        self.staking_agent = StakingEscrowAgent(blockchain=blockchain)
        self.token_agent = NucypherTokenAgent(blockchain=blockchain)
        self.economics = TokenEconomics()

        # Mode
        self.connect(blockchain=blockchain)

        self.__accounts = list()
        self.__stakers = dict()
        self.__transacting_powers = dict()

        self.__get_accounts()

        if sync_now:
            self.read_onchain_stakes()  # Stakes

    #
    # Configuration
    #

    def static_payload(self) -> dict:
        """Values to read/write from stakeholder JSON configuration files"""
        payload = dict(provider_uri=self.blockchain.provider_uri,
                       blockchain=self.blockchain.to_dict(),
                       accounts=self.__accounts,
                       stakers=self.__serialize_stakers())
        return payload

    @classmethod
    def from_configuration_file(cls,
                                filepath: str = None,
                                sync_now: bool = True,
                                **overrides) -> 'StakeHolder':
        filepath = filepath or cls.default_filepath()
        payload = cls._read_configuration_file(filepath=filepath)

        # Sub config
        blockchain_payload = payload.pop('blockchain')
        blockchain = BlockchainInterface.from_dict(payload=blockchain_payload)
        blockchain.connect(sync_now=sync_now)  # TODO: Leave this here?

        payload.update(dict(blockchain=blockchain))

        payload.update(overrides)
        instance = cls(filepath=filepath, **payload)
        return instance

    @validate_checksum_address
    def attach_transacting_power(self,
                                 checksum_address: str,
                                 password: str = None) -> None:
        try:
            transacting_power = self.__transacting_powers[checksum_address]
        except KeyError:
            transacting_power = TransactingPower(blockchain=self.blockchain,
                                                 password=password,
                                                 account=checksum_address)
            self.__transacting_powers[checksum_address] = transacting_power
        transacting_power.activate(password=password)

    def to_configuration_file(self, *args, **kwargs) -> str:
        filepath = super().to_configuration_file(*args, **kwargs)
        return filepath

    def connect(self, blockchain: BlockchainInterface = None) -> None:
        """Go Online"""
        if not self.staking_agent:
            self.staking_agent = StakingEscrowAgent(blockchain=blockchain)
        if not self.token_agent:
            self.token_agent = NucypherTokenAgent(blockchain=blockchain)
        self.blockchain = self.token_agent.blockchain

    #
    # Account Utilities
    #

    @property
    def accounts(self) -> list:
        return self.__accounts

    def __get_accounts(self) -> None:
        accounts = self.blockchain.client.accounts
        self.__accounts.extend(accounts)

    #
    # Staking Utilities
    #

    def read_onchain_stakes(self, account: str = None) -> None:
        if account:
            accounts = [account]
        else:
            accounts = self.__accounts

        for account in accounts:
            stakes = list(
                self.staking_agent.get_all_stakes(staker_address=account))
            if stakes:
                staker = Staker(is_me=True,
                                checksum_address=account,
                                blockchain=self.blockchain)
                self.__stakers[account] = staker

    @property
    def total_stake(self) -> NU:
        total = sum(staker.locked_tokens() for staker in self.stakers)
        return total

    @property
    def stakers(self) -> List[Staker]:
        return list(self.__stakers.values())

    @property
    def stakes(self) -> list:
        payload = list()
        for staker in self.__stakers.values():
            payload.extend(staker.stakes)
        return payload

    @property
    def account_balances(self) -> dict:
        balances = dict()
        for account in self.__accounts:
            funds = {
                'ETH': self.blockchain.client.get_balance(account),
                'NU': self.token_agent.get_balance(account)
            }
            balances.update({account: funds})
        return balances

    @property
    def staker_balances(self) -> dict:
        balances = dict()
        for staker in self.stakers:
            staker_funds = {
                'ETH': staker.eth_balance,
                'NU': staker.token_balance
            }
            balances[staker.checksum_address] = {
                staker.checksum_address: staker_funds
            }
        return balances

    def __serialize_stakers(self) -> list:
        payload = list()
        for staker in self.stakers:
            payload.append(staker.to_dict())
        return payload

    def get_active_staker(self, address: str) -> Staker:
        self.read_onchain_stakes(account=address)
        try:
            return self.__stakers[address]
        except KeyError:
            raise self.NoStakes(f"{address} does not have any stakes.")

    def create_worker_configuration(self, staking_address: str,
                                    worker_address: str, password: str,
                                    **configuration):
        """Generates a worker JSON configuration file for a given staking address."""
        from nucypher.config.characters import UrsulaConfiguration
        worker_configuration = UrsulaConfiguration.generate(
            checksum_address=staking_address,
            worker_address=worker_address,
            password=password,
            config_root=self.config_root,
            federated_only=False,
            provider_uri=self.blockchain.provider_uri,
            **configuration)
        return worker_configuration

    #
    # Actions
    #

    def set_worker(self,
                   staker_address: str,
                   worker_address: str,
                   password: str = None):
        self.attach_transacting_power(checksum_address=staker_address,
                                      password=password)
        staker = self.get_active_staker(address=staker_address)
        receipt = self.staking_agent.set_worker(
            staker_address=staker.checksum_address,
            worker_address=worker_address)

        self.to_configuration_file(override=True)
        return receipt

    def initialize_stake(
        self,
        amount: NU,
        duration: int,
        checksum_address: str,
        password: str = None,
    ) -> Stake:

        # Existing Staker address
        if not is_checksum_address(checksum_address):
            raise ValueError(
                f"{checksum_address} is an invalid EIP-55 checksum address.")

        try:
            staker = self.__stakers[checksum_address]
        except KeyError:
            if checksum_address not in self.__accounts:
                raise ValueError(
                    f"{checksum_address} is an unknown wallet address.")
            else:
                staker = Staker(is_me=True,
                                checksum_address=checksum_address,
                                blockchain=self.blockchain)

        # Don the transacting power for the staker's account.
        self.attach_transacting_power(checksum_address=staker.checksum_address,
                                      password=password)
        new_stake = staker.initialize_stake(amount=amount,
                                            lock_periods=duration)

        # Update local cache and save to disk.
        self.__stakers[checksum_address] = staker
        staker.stake_tracker.refresh(
            checksum_addresses=[staker.checksum_address])
        self.to_configuration_file(override=True)

        return new_stake

    def divide_stake(
        self,
        address: str,
        index: int,
        value: NU,
        duration: int,
        password: str = None,
    ):

        staker = self.get_active_staker(address=address)
        if not staker.is_staking:
            raise Stake.StakingError(
                f"{staker.checksum_address} has no published stakes.")

        self.attach_transacting_power(checksum_address=staker.checksum_address,
                                      password=password)
        result = staker.divide_stake(stake_index=index,
                                     additional_periods=duration,
                                     target_value=value)

        # Save results to disk
        self.to_configuration_file(override=True)
        return result

    def calculate_rewards(self) -> dict:
        rewards = dict()
        for staker in self.stakers:
            reward = staker.calculate_reward()
            rewards[staker.checksum_address] = reward
        return rewards

    def collect_rewards(self,
                        staker_address: str,
                        password: str = None,
                        withdraw_address: str = None,
                        staking: bool = True,
                        policy: bool = True) -> Dict[str, dict]:

        if not staking and not policy:
            raise ValueError(
                "Either staking or policy must be True in order to collect rewards"
            )

        try:
            staker = self.get_active_staker(address=staker_address)
        except self.NoStakes:
            staker = Staker(is_me=True,
                            checksum_address=staker_address,
                            blockchain=self.blockchain)

        self.attach_transacting_power(checksum_address=staker.checksum_address,
                                      password=password)

        receipts = dict()
        if staking:
            receipts['staking_reward'] = staker.collect_staking_reward()
        if policy:
            receipts['policy_reward'] = staker.collect_policy_reward(
                collector_address=withdraw_address)

        self.to_configuration_file(override=True)
        return receipts