示例#1
0
class Worker(NucypherTokenActor):
    """
    Ursula baseclass for blockchain operations, practically carrying a pickaxe.
    """
    class WorkerError(NucypherTokenActor.ActorError):
        pass

    class DetachedWorker(WorkerError):
        """Raised when the Worker is not bonded to a Staker in the StakingEscrow contract."""

    def __init__(self,
                 is_me: bool,
                 work_tracker: WorkTracker = None,
                 worker_address: str = None,
                 start_working_now: bool = True,
                 check_active_worker: bool = True,
                 *args,
                 **kwargs):

        super().__init__(*args, **kwargs)
        self.log = Logger("worker")

        self.__worker_address = worker_address
        self.is_me = is_me

        # Agency
        self.staking_agent = ContractAgency.get_agent(StakingEscrowAgent,
                                                      registry=self.registry)

        # Stakes
        self.__start_time = WORKER_NOT_RUNNING
        self.__uptime_period = WORKER_NOT_RUNNING

        # Workers cannot be started without being assigned a stake first.
        if is_me:
            self.stakes = StakeList(registry=self.registry,
                                    checksum_address=self.checksum_address)
            self.stakes.refresh()
            if check_active_worker and not len(self.stakes):
                raise self.DetachedWorker(
                    f"{self.__worker_address} is not bonded to {self.checksum_address}."
                )

            self.work_tracker = work_tracker or WorkTracker(worker=self)
            if start_working_now:
                self.work_tracker.start(act_now=False)

    @property
    def last_active_period(self) -> int:
        period = self.staking_agent.get_last_active_period(
            staker_address=self.checksum_address)
        return period

    @only_me
    @save_receipt
    def confirm_activity(self) -> str:
        """For each period that the worker confirms activity, the staker is rewarded"""
        receipt = self.staking_agent.confirm_activity(
            worker_address=self.__worker_address)
        return receipt
示例#2
0
    def _learn_about_nodes_contract_info(self):
        agent = self.staking_agent

        block_time = agent.blockchain.client.w3.eth.getBlock('latest').timestamp  # precision in seconds
        current_period = agent.get_current_period()

        nodes_dict = self.known_nodes.abridged_nodes_dict()
        self.log.info(f'Processing {len(nodes_dict)} nodes at '
                      f'{MayaDT(epoch=block_time)} | Period {current_period}')
        data = []
        for staker_address in nodes_dict:
            worker = agent.get_worker_from_staker(staker_address)

            stake = agent.owned_tokens(staker_address)
            staked_nu_tokens = float(NU.from_nunits(stake).to_tokens())
            locked_nu_tokens = float(NU.from_nunits(agent.get_locked_tokens(
                staker_address=staker_address)).to_tokens())

            economics = TokenEconomicsFactory.get_economics(registry=self.registry)
            stakes = StakeList(checksum_address=staker_address, registry=self.registry)
            stakes.refresh()

            # store dates as floats for comparison purposes
            start_date = datetime_at_period(stakes.initial_period,
                                            seconds_per_period=economics.seconds_per_period).datetime().timestamp()
            end_date = datetime_at_period(stakes.terminal_period,
                                          seconds_per_period=economics.seconds_per_period).datetime().timestamp()

            last_confirmed_period = agent.get_last_active_period(staker_address)

            # TODO: do we need to worry about how much information is in memory if number of nodes is
            #  large i.e. should I check for size of data and write within loop if too big
            data.append(self.BLOCKCHAIN_DB_LINE_PROTOCOL.format(
                measurement=self.BLOCKCHAIN_DB_MEASUREMENT,
                staker_address=staker_address,
                worker_address=worker,
                start_date=start_date,
                end_date=end_date,
                stake=staked_nu_tokens,
                locked_stake=locked_nu_tokens,
                current_period=current_period,
                last_confirmed_period=last_confirmed_period,
                timestamp=block_time
            ))

        if not self._blockchain_db_client.write_points(data,
                                                       database=self.BLOCKCHAIN_DB_NAME,
                                                       time_precision='s',
                                                       batch_size=10000,
                                                       protocol='line'):
            # TODO: what do we do here
            self.log.warn(f'Unable to write to database {self.BLOCKCHAIN_DB_NAME} at '
                          f'{MayaDT(epoch=block_time)} | Period {current_period}')
示例#3
0
def test_collect_rewards_integration(click_runner,
                                     testerchain,
                                     agency_local_registry,
                                     stakeholder_configuration_file_location,
                                     blockchain_alice,
                                     blockchain_bob,
                                     random_policy_label,
                                     beneficiary,
                                     preallocation_escrow_agent,
                                     mock_allocation_registry,
                                     manual_worker,
                                     token_economics,
                                     mock_transacting_power_activation,
                                     stake_value,
                                     policy_value,
                                     policy_rate):
    # Disable re-staking
    restake_args = ('stake', 'restake',
                    '--disable',
                    '--config-file', stakeholder_configuration_file_location,
                    '--allocation-filepath', MOCK_INDIVIDUAL_ALLOCATION_FILEPATH,
                    '--force')

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

    half_stake_time = token_economics.minimum_locked_periods // 2  # Test setup
    logger = Logger("Test-CLI")  # Enter the Teacher's Logger, and
    current_period = 0  # State the initial period for incrementing

    staker_address = preallocation_escrow_agent.principal_contract.address
    worker_address = manual_worker

    # The staker is staking.
    stakes = StakeList(registry=agency_local_registry, checksum_address=staker_address)
    stakes.refresh()
    assert stakes

    staking_agent = ContractAgency.get_agent(StakingEscrowAgent, registry=agency_local_registry)
    assert worker_address == staking_agent.get_worker_from_staker(staker_address=staker_address)

    ursula_port = select_test_port()
    ursula = Ursula(is_me=True,
                    checksum_address=staker_address,
                    worker_address=worker_address,
                    registry=agency_local_registry,
                    rest_host='127.0.0.1',
                    rest_port=ursula_port,
                    start_working_now=False,
                    network_middleware=MockRestMiddleware())

    MOCK_KNOWN_URSULAS_CACHE[ursula_port] = ursula
    assert ursula.worker_address == worker_address
    assert ursula.checksum_address == staker_address

    mock_transacting_power_activation(account=worker_address, password=INSECURE_DEVELOPMENT_PASSWORD)

    # Confirm for half the first stake duration
    for _ in range(half_stake_time):
        logger.debug(f">>>>>>>>>>> TEST PERIOD {current_period} <<<<<<<<<<<<<<<<")
        ursula.confirm_activity()
        testerchain.time_travel(periods=1)
        current_period += 1

    # Alice creates a policy and grants Bob access
    blockchain_alice.selection_buffer = 1

    M, N = 1, 1
    days = 3
    now = testerchain.w3.eth.getBlock(block_identifier='latest').timestamp
    expiration = maya.MayaDT(now).add(days=days-1)
    blockchain_policy = blockchain_alice.grant(bob=blockchain_bob,
                                               label=random_policy_label,
                                               m=M, n=N,
                                               value=policy_value,
                                               expiration=expiration,
                                               handpicked_ursulas={ursula})

    # Ensure that the handpicked Ursula was selected for the policy
    arrangement = list(blockchain_policy._accepted_arrangements)[0]
    assert arrangement.ursula == ursula

    # Bob learns about the new staker and joins the policy
    blockchain_bob.start_learning_loop()
    blockchain_bob.remember_node(node=ursula)
    blockchain_bob.join_policy(random_policy_label, bytes(blockchain_alice.stamp))

    # Enrico Encrypts (of course)
    enrico = Enrico(policy_encrypting_key=blockchain_policy.public_key,
                    network_middleware=MockRestMiddleware())

    verifying_key = blockchain_alice.stamp.as_umbral_pubkey()

    for index in range(half_stake_time - 5):
        logger.debug(f">>>>>>>>>>> TEST PERIOD {current_period} <<<<<<<<<<<<<<<<")
        ursula.confirm_activity()

        # Encrypt
        random_data = os.urandom(random.randrange(20, 100))
        message_kit, signature = enrico.encrypt_message(message=random_data)

        # Decrypt
        cleartexts = blockchain_bob.retrieve(message_kit,
                                             enrico=enrico,
                                             alice_verifying_key=verifying_key,
                                             label=random_policy_label)
        assert random_data == cleartexts[0]

        # Ursula Staying online and the clock advancing
        testerchain.time_travel(periods=1)
        current_period += 1

    # Finish the passage of time
    for _ in range(5 - 1):  # minus 1 because the first period was already confirmed in test_ursula_run
        logger.debug(f">>>>>>>>>>> TEST PERIOD {current_period} <<<<<<<<<<<<<<<<")
        ursula.confirm_activity()
        current_period += 1
        testerchain.time_travel(periods=1)

    #
    # WHERES THE MONEY URSULA?? - Collecting Rewards
    #

    balance = testerchain.client.get_balance(beneficiary)

    # Rewards will be unlocked after the
    # final confirmed period has passed (+1).
    logger.debug(f">>>>>>>>>>> TEST PERIOD {current_period} <<<<<<<<<<<<<<<<")
    testerchain.time_travel(periods=1)
    current_period += 1
    logger.debug(f">>>>>>>>>>> TEST PERIOD {current_period} <<<<<<<<<<<<<<<<")

    # Since we are mocking the blockchain connection, manually consume the transacting power of the Beneficiary.
    mock_transacting_power_activation(account=beneficiary, password=INSECURE_DEVELOPMENT_PASSWORD)

    # Collect Policy Reward
    collection_args = ('stake', 'collect-reward',
                       '--config-file', stakeholder_configuration_file_location,
                       '--policy-reward',
                       '--no-staking-reward',
                       '--withdraw-address', beneficiary,
                       '--allocation-filepath', MOCK_INDIVIDUAL_ALLOCATION_FILEPATH,
                       '--force')

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

    # Policy Reward
    collected_policy_reward = testerchain.client.get_balance(beneficiary)
    assert collected_policy_reward > balance

    #
    # Collect Staking Reward
    #
    token_agent = ContractAgency.get_agent(agent_class=NucypherTokenAgent, registry=agency_local_registry)
    balance_before_collecting = token_agent.get_balance(address=staker_address)

    collection_args = ('stake', 'collect-reward',
                       '--config-file', stakeholder_configuration_file_location,
                       '--no-policy-reward',
                       '--staking-reward',
                       '--allocation-filepath', MOCK_INDIVIDUAL_ALLOCATION_FILEPATH,
                       '--force')

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

    # The beneficiary has withdrawn her staking rewards, which are now in the staking contract
    assert token_agent.get_balance(address=staker_address) >= balance_before_collecting
示例#4
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,
                 individual_allocation: IndividualAllocationRegistry = None,
                 *args,
                 **kwargs) -> None:

        super().__init__(*args, **kwargs)
        self.log = Logger("staker")

        self.is_me = is_me
        self.__worker_address = None

        # Blockchain
        self.policy_agent = ContractAgency.get_agent(
            PolicyManagerAgent,
            registry=self.registry)  # type: PolicyManagerAgent
        self.staking_agent = ContractAgency.get_agent(
            StakingEscrowAgent,
            registry=self.registry)  # type: StakingEscrowAgent
        self.economics = TokenEconomicsFactory.get_economics(
            registry=self.registry)

        # Staking via contract
        self.individual_allocation = individual_allocation
        if self.individual_allocation:
            self.beneficiary_address = individual_allocation.beneficiary_address
            self.checksum_address = individual_allocation.contract_address
            self.preallocation_escrow_agent = PreallocationEscrowAgent(
                registry=self.registry,
                allocation_registry=self.individual_allocation,
                beneficiary=self.beneficiary_address)
        else:
            self.beneficiary_address = None
            self.preallocation_escrow_agent = None

        # Check stakes
        self.stakes = StakeList(registry=self.registry,
                                checksum_address=self.checksum_address)

    @property
    def is_contract(self) -> bool:
        return self.preallocation_escrow_agent is not 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

    # TODO: This is unused. Why? Should we remove it?
    @classmethod
    def from_dict(cls, staker_payload: dict) -> 'Staker':
        staker = Staker(is_me=True,
                        checksum_address=staker_payload['checksum_address'])
        return staker

    @property
    def is_staking(self) -> bool:
        """Checks if this Staker currently has active stakes / locked tokens."""
        self.stakes.refresh()
        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."""
        self.stakes.refresh()
        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, i.e., tokens locked in the current period.
        """
        return self.locked_tokens(periods=0)

    @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."
            )

        # 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 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,
                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})."
                )

        # Do it already!
        modified_stake, new_stake = current_stake.divide(
            target_value=target_value, additional_periods=additional_periods)

        # Update staking cache element
        self.stakes.refresh()

        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,
                seconds_per_period=self.economics.seconds_per_period)

        # 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 initialize stake - "
                f"Maximum stake value exceeded for {self.checksum_address} "
                f"with a target value of {amount}.")

        # Write to blockchain
        new_stake = Stake.initialize_stake(staker=self,
                                           amount=amount,
                                           lock_periods=lock_periods)

        # Update staking cache element
        self.stakes.refresh()

        return new_stake

    def deposit(self, amount: int, lock_periods: int) -> Tuple[str, str]:
        """Public facing method for token locking."""
        if self.is_contract:
            approve_receipt = self.token_agent.approve_transfer(
                amount=amount,
                target_address=self.staking_agent.contract_address,
                sender_address=self.beneficiary_address)
            deposit_receipt = self.preallocation_escrow_agent.deposit_as_staker(
                amount=amount, lock_periods=lock_periods)
        else:
            approve_receipt = self.token_agent.approve_transfer(
                amount=amount,
                target_address=self.staking_agent.contract_address,
                sender_address=self.checksum_address)
            deposit_receipt = self.staking_agent.deposit_tokens(
                amount=amount,
                lock_periods=lock_periods,
                sender_address=self.checksum_address)

        return approve_receipt, deposit_receipt

    @property
    def is_restaking(self) -> bool:
        restaking = self.staking_agent.is_restaking(
            staker_address=self.checksum_address)
        return restaking

    @only_me
    @save_receipt
    def _set_restaking_value(self, value: bool) -> dict:
        if self.is_contract:
            receipt = self.preallocation_escrow_agent.set_restaking(
                value=value)
        else:
            receipt = self.staking_agent.set_restaking(
                staker_address=self.checksum_address, value=value)
        return receipt

    def enable_restaking(self) -> dict:
        receipt = self._set_restaking_value(value=True)
        return receipt

    @only_me
    @save_receipt
    def enable_restaking_lock(self, release_period: int):
        current_period = self.staking_agent.get_current_period()
        if release_period < current_period:
            raise ValueError(
                f"Release period for re-staking lock must be in the future.  "
                f"Current period is {current_period}, got '{release_period}'.")
        if self.is_contract:
            receipt = self.preallocation_escrow_agent.lock_restaking(
                release_period=release_period)
        else:
            receipt = self.staking_agent.lock_restaking(
                staker_address=self.checksum_address,
                release_period=release_period)
        return receipt

    @property
    def restaking_lock_enabled(self) -> bool:
        status = self.staking_agent.is_restaking_locked(
            staker_address=self.checksum_address)
        return status

    def disable_restaking(self) -> dict:
        receipt = self._set_restaking_value(value=False)
        return receipt

    #
    # Bonding with Worker
    #

    @only_me
    @save_receipt
    @validate_checksum_address
    def set_worker(self, worker_address: str) -> str:
        if self.is_contract:
            receipt = self.preallocation_escrow_agent.set_worker(
                worker_address=worker_address)
        else:
            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:
            # TODO: This is broken for StakeHolder with different stakers - See #1358
            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 detach_worker(self) -> str:
        if self.is_contract:
            receipt = self.preallocation_escrow_agent.release_worker()
        else:
            receipt = self.staking_agent.release_worker(
                staker_address=self.checksum_address)
        self.__worker_address = BlockchainInterface.NULL_ADDRESS
        return receipt

    #
    # Reward and Collection
    #

    @only_me
    @save_receipt
    def mint(self) -> Tuple[str, str]:
        """Computes and transfers tokens to the staker's account"""
        if self.is_contract:
            receipt = self.preallocation_escrow_agent.mint()
        else:
            receipt = self.staking_agent.mint(
                staker_address=self.checksum_address)
        return receipt

    def calculate_staking_reward(self) -> int:
        staking_reward = self.staking_agent.calculate_staking_reward(
            staker_address=self.checksum_address)
        return staking_reward

    def calculate_policy_reward(self) -> int:
        policy_reward = self.policy_agent.get_reward_amount(
            staker_address=self.checksum_address)
        return policy_reward

    @only_me
    @save_receipt
    @validate_checksum_address
    def collect_policy_reward(self, collector_address=None) -> dict:
        """Collect rewarded ETH."""
        withdraw_address = collector_address or self.checksum_address
        if self.is_contract:
            receipt = self.preallocation_escrow_agent.collect_policy_reward(
                collector_address=withdraw_address)
        else:
            receipt = self.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."""
        if self.is_contract:
            reward_amount = self.calculate_staking_reward()
            self.log.debug(
                f"Withdrawing staking reward ({NU.from_nunits(reward_amount)}) to {self.checksum_address}"
            )
            receipt = self.preallocation_escrow_agent.withdraw_as_staker(
                value=reward_amount)
        else:
            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)"""
        if self.is_contract:
            receipt = self.preallocation_escrow_agent.withdraw_as_staker(
                value=int(amount))
        else:
            receipt = self.staking_agent.withdraw(
                staker_address=self.checksum_address, amount=int(amount))
        return receipt
示例#5
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, *args, **kwargs) -> None:

        super().__init__(*args, **kwargs)
        self.log = Logger("staker")

        self.is_me = is_me
        self.__worker_address = None

        # Blockchain
        self.policy_agent = ContractAgency.get_agent(PolicyManagerAgent,
                                                     registry=self.registry)
        self.staking_agent = ContractAgency.get_agent(StakingEscrowAgent,
                                                      registry=self.registry)
        self.economics = TokenEconomicsFactory.get_economics(
            registry=self.registry)
        self.stakes = StakeList(registry=self.registry,
                                checksum_address=self.checksum_address)

    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 is_staking(self) -> bool:
        """Checks if this Staker currently has active stakes / locked tokens."""
        self.stakes.refresh()
        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."""
        self.stakes.refresh()
        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.
        """
        stake = self.staking_agent.owned_tokens(
            staker_address=self.checksum_address)
        nu_stake = NU.from_nunits(stake)
        return nu_stake

    @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."
            )

        # 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 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.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})."
                )

        # Do it already!
        modified_stake, new_stake = current_stake.divide(
            target_value=target_value, additional_periods=additional_periods)

        # Update staking cache element
        self.stakes.refresh()

        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 initialize stake - "
                f"Maximum stake value exceeded for {self.checksum_address} "
                f"with a target value of {amount}.")

        # Write to blockchain
        new_stake = Stake.initialize_stake(staker=self,
                                           amount=amount,
                                           lock_periods=lock_periods)

        # Update staking cache element
        self.stakes.refresh()

        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) -> dict:
        """Collect rewarded ETH."""
        withdraw_address = collector_address or self.checksum_address
        receipt = self.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
示例#6
0
    def _learn_about_nodes(self, threaded: bool = True):
        if threaded:
            if self.__collecting_nodes:
                self.log.debug(
                    "Skipping Round - Nodes collection thread is already running"
                )
                return
            return reactor.callInThread(self._learn_about_nodes,
                                        threaded=False)
        self.__collecting_nodes = True

        agent = self.staking_agent
        known_nodes = list(self.known_nodes)

        block_time = agent.blockchain.client.w3.eth.getBlock(
            'latest').timestamp  # precision in seconds
        current_period = agent.get_current_period()

        log = f'Processing {len(known_nodes)} nodes at {MayaDT(epoch=block_time)} | Period {current_period}'
        self.log.info(log)

        data = list()
        for node in known_nodes:

            staker_address = node.checksum_address
            worker = agent.get_worker_from_staker(staker_address)

            stake = agent.owned_tokens(staker_address)
            staked_nu_tokens = float(NU.from_nunits(stake).to_tokens())
            locked_nu_tokens = float(
                NU.from_nunits(
                    agent.get_locked_tokens(
                        staker_address=staker_address)).to_tokens())

            economics = EconomicsFactory.get_economics(registry=self.registry)
            stakes = StakeList(checksum_address=staker_address,
                               registry=self.registry)
            stakes.refresh()

            if stakes.initial_period is NOT_STAKING:
                continue  # TODO: Skip this measurement for now

            start_date = datetime_at_period(
                stakes.initial_period,
                seconds_per_period=economics.seconds_per_period)
            start_date = start_date.datetime().timestamp()
            end_date = datetime_at_period(
                stakes.terminal_period,
                seconds_per_period=economics.seconds_per_period)
            end_date = end_date.datetime().timestamp()

            last_confirmed_period = agent.get_last_active_period(
                staker_address)

            num_work_orders = 0  # len(node.work_orders())  # TODO: Only works for is_me with datastore attached

            # TODO: do we need to worry about how much information is in memory if number of nodes is
            #  large i.e. should I check for size of data and write within loop if too big
            data.append(
                self.NODE_LINE_PROTOCOL.format(
                    measurement=self.NODE_MEASUREMENT,
                    staker_address=staker_address,
                    worker_address=worker,
                    start_date=start_date,
                    end_date=end_date,
                    stake=staked_nu_tokens,
                    locked_stake=locked_nu_tokens,
                    current_period=current_period,
                    last_confirmed_period=last_confirmed_period,
                    timestamp=block_time,
                    work_orders=num_work_orders))

        success = self._influx_client.write_points(
            data,
            database=self.INFLUX_DB_NAME,
            time_precision='s',
            batch_size=10000,
            protocol='line')
        self.__collecting_nodes = False
        if not success:
            # TODO: What do we do here - Event hook for alerting?
            self.log.warn(
                f'Unable to write node information to database {self.INFLUX_DB_NAME} at '
                f'{MayaDT(epoch=block_time)} | Period {current_period}')