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
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}')
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
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
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
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}')