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)
def agent(testerchain, test_registry, allocation_value, agency, mock_transacting_power_activation) -> PreallocationEscrowAgent: deployer_address, beneficiary_address, *everybody_else = testerchain.client.accounts escrow_deployer = PreallocationEscrowDeployer(deployer_address=deployer_address, registry=test_registry, allocation_registry=TEST_ALLOCATION_REGISTRY) mock_transacting_power_activation(account=deployer_address, password=INSECURE_DEVELOPMENT_PASSWORD) _receipt = escrow_deployer.deploy() escrow_deployer.initial_deposit(value=allocation_value, duration_seconds=TEST_LOCK_DURATION_IN_SECONDS) assert escrow_deployer.contract.functions.getLockedTokens().call() == allocation_value escrow_deployer.assign_beneficiary(checksum_address=beneficiary_address) escrow_deployer.enroll_principal_contract() assert escrow_deployer.contract.functions.getLockedTokens().call() == allocation_value agent = escrow_deployer.make_agent() direct_agent = PreallocationEscrowAgent(registry=test_registry, allocation_registry=TEST_ALLOCATION_REGISTRY, beneficiary=beneficiary_address) assert direct_agent == agent assert direct_agent.contract.abi == agent.contract.abi assert direct_agent.contract.address == agent.contract.address assert agent.principal_contract.address == escrow_deployer.contract.address assert agent.principal_contract.abi == escrow_deployer.contract.abi assert direct_agent.contract.abi == escrow_deployer.contract.abi assert direct_agent.contract.address == escrow_deployer.contract.address yield agent TEST_ALLOCATION_REGISTRY.clear()
def preallocation_escrow_agent(beneficiary, agency_local_registry, mock_allocation_registry, test_registry_source_manager, individual_allocation): preallocation_escrow_agent = PreallocationEscrowAgent(beneficiary=beneficiary, registry=agency_local_registry, allocation_registry=individual_allocation) return preallocation_escrow_agent
def preallocation_escrow_agent(beneficiary, test_registry, mock_allocation_registry): individual_allocation = IndividualAllocationRegistry.from_allocation_file( MOCK_INDIVIDUAL_ALLOCATION_FILEPATH) preallocation_escrow_agent = PreallocationEscrowAgent( beneficiary=beneficiary, registry=test_registry, allocation_registry=individual_allocation) return preallocation_escrow_agent
def test_nucypher_deploy_allocation_contracts(click_runner, testerchain, registry_filepath, mock_allocation_infile, token_economics): # # Main # deploy_command = ('allocations', '--registry-infile', registry_filepath, '--allocation-infile', mock_allocation_infile, '--allocation-outfile', MOCK_ALLOCATION_REGISTRY_FILEPATH, '--provider', TEST_PROVIDER_URI, '--poa') account_index = '0\n' yes = 'Y\n' no = 'N\n' user_input = account_index + yes + no + yes result = click_runner.invoke(deploy, deploy_command, input=user_input, catch_exceptions=False) assert result.exit_code == 0 for allocation_address in testerchain.unassigned_accounts: assert allocation_address in result.output # ensure that a pre-allocation recipient has the allocated token quantity. beneficiary = testerchain.client.accounts[-1] allocation_registry = AllocationRegistry(filepath=MOCK_ALLOCATION_REGISTRY_FILEPATH) registry = LocalContractRegistry(filepath=registry_filepath) preallocation_escrow_agent = PreallocationEscrowAgent(registry=registry, beneficiary=beneficiary, allocation_registry=allocation_registry) assert preallocation_escrow_agent.unvested_tokens == 2 * token_economics.minimum_allowed_locked
def paint_deployer_contract_inspection(emitter, registry, deployer_address) -> None: blockchain = BlockchainInterfaceFactory.get_interface() sep = '-' * 45 emitter.echo(sep) provider_info = f""" * Web3 Provider ==================================================================== Provider URI ............. {blockchain.provider_uri} Registry ................ {registry.filepath} * Standard Deployments ===================================================================== """ emitter.echo(provider_info) try: token_agent = ContractAgency.get_agent(NucypherTokenAgent, registry=registry) token_contract_info = f""" {token_agent.contract_name} ........... {token_agent.contract_address} ~ Ethers ............ {Web3.fromWei(blockchain.client.get_balance(token_agent.contract_address), 'ether')} ETH ~ Tokens ............ {NU.from_nunits(token_agent.get_balance(token_agent.contract_address))}""" except BaseContractRegistry.UnknownContract: message = f"\n{NucypherTokenAgent.contract_name} is not enrolled in {registry.filepath}" emitter.echo(message, color='yellow') emitter.echo(sep, nl=False) else: emitter.echo(token_contract_info) banner = """ * Proxy-Contract Deployments =====================================================================""" emitter.echo(banner) from nucypher.blockchain.eth.actors import ContractAdministrator for contract_deployer_class in ContractAdministrator.dispatched_upgradeable_deployer_classes: try: bare_contract = blockchain.get_contract_by_name( contract_name=contract_deployer_class.contract_name, proxy_name=DispatcherDeployer.contract_name, registry=registry, use_proxy_address=False) dispatcher_deployer = DispatcherDeployer( registry=registry, target_contract=bare_contract, deployer_address=deployer_address, bare=True) # acquire agency for the dispatcher itself. agent = contract_deployer_class.agency(registry=registry, contract=bare_contract) proxy_payload = f""" {agent.contract_name} .... {bare_contract.address} ~ Owner .............. {bare_contract.functions.owner().call()} ~ Ethers ............. {Web3.fromWei(blockchain.client.get_balance(bare_contract.address), 'ether')} ETH ~ Tokens ............. {NU.from_nunits(token_agent.get_balance(bare_contract.address))} ~ Dispatcher ......... {dispatcher_deployer.contract_address} ~ Owner .......... {dispatcher_deployer.contract.functions.owner().call()} ~ Target ......... {dispatcher_deployer.contract.functions.target().call()} ~ Ethers ......... {Web3.fromWei(blockchain.client.get_balance(dispatcher_deployer.contract_address), 'ether')} ETH ~ Tokens ......... {NU.from_nunits(token_agent.get_balance(dispatcher_deployer.contract_address))}""" emitter.echo(proxy_payload) emitter.echo(sep, nl=False) except BaseContractRegistry.UnknownContract: message = f"\n{contract_deployer_class.contract_name} is not enrolled in {registry.filepath}" emitter.echo(message, color='yellow') emitter.echo(sep, nl=False) try: # # StakingInterface # staking_interface_agent = PreallocationEscrowAgent.StakingInterfaceAgent( registry=registry) bare_contract = blockchain.get_contract_by_name( contract_name=staking_interface_agent.contract_name, proxy_name=StakingInterfaceRouterDeployer.contract_name, use_proxy_address=False, registry=registry) router_deployer = StakingInterfaceRouterDeployer( registry=registry, target_contract=bare_contract, deployer_address=deployer_address, bare=True) # acquire agency for the dispatcher itself. preallocation_escrow_payload = f""" {staking_interface_agent.contract_name} ......... {bare_contract.address} ~ Ethers ............... {Web3.fromWei(blockchain.client.get_balance(bare_contract.address), 'ether')} ETH ~ Tokens ............... {NU.from_nunits(token_agent.get_balance(bare_contract.address))} ~ StakingInterfaceRouter {router_deployer.contract.address} ~ Owner .......... {router_deployer.contract.functions.owner().call()} ~ Target ......... {router_deployer.contract.functions.target().call()} ~ Ethers ......... {Web3.fromWei(blockchain.client.get_balance(router_deployer.contract_address), 'ether')} ETH ~ Tokens ......... {NU.from_nunits(token_agent.get_balance(router_deployer.contract_address))}""" emitter.echo(preallocation_escrow_payload) emitter.echo(sep) except BaseContractRegistry.UnknownContract: message = f"\nStakingInterface is not enrolled in {registry.filepath}" emitter.echo(message, color='yellow') try: policy_agent = ContractAgency.get_agent(PolicyManagerAgent, registry=registry) paint_fee_rate_range(emitter, policy_agent) emitter.echo(sep, nl=False) except BaseContractRegistry.UnknownContract: message = f"\n{PolicyManagerDeployer.contract_name} is not enrolled in {registry.filepath}" emitter.echo(message, color='yellow') emitter.echo(sep, nl=False)
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