Detailing StakeHolder actions

pull/1124/head
Kieran Prasch 2019-06-11 13:04:10 -07:00
parent d7b03de176
commit 3953da73ad
No known key found for this signature in database
GPG Key ID: 199AB839D4125A62
2 changed files with 208 additions and 93 deletions

View File

@ -28,8 +28,8 @@ from constant_sorrow.constants import (
CONTRACT_NOT_DEPLOYED, CONTRACT_NOT_DEPLOYED,
NO_DEPLOYER_ADDRESS, NO_DEPLOYER_ADDRESS,
WORKER_NOT_RUNNING, WORKER_NOT_RUNNING,
WORKER_NOT_RUNNING,
NO_WORKER_ASSIGNED, NO_WORKER_ASSIGNED,
NO_FUNDING_ACCOUNT
) )
from eth_tester.exceptions import TransactionFailed from eth_tester.exceptions import TransactionFailed
from eth_utils import keccak from eth_utils import keccak
@ -665,12 +665,17 @@ class Investigator(NucypherTokenActor):
class StakeHolder(BaseConfiguration): class StakeHolder(BaseConfiguration):
_NAME = 'stakeholder' _NAME = 'stakeholder'
TRANSACTION_GAS = {}
class NoFundingAccount(BaseConfiguration.ConfigurationError):
pass
def __init__(self, def __init__(self,
blockchain: BlockchainInterface = None, blockchain: BlockchainInterface = None,
staking_agent: StakingEscrowAgent = None, staking_agent: StakingEscrowAgent = None,
master_address: str = None, funding_account: str = NO_FUNDING_ACCOUNT,
trezor: bool = False, offline_mode: bool = False,
sync_now: bool = True,
*args, **kwargs): *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@ -678,23 +683,80 @@ class StakeHolder(BaseConfiguration):
# Blockchain and Contract connection # Blockchain and Contract connection
if not staking_agent: if not staking_agent:
staking_agent = StakingEscrowAgent(blockchain=blockchain) staking_agent = StakingEscrowAgent(blockchain=blockchain)
self.staking_agent = staking_agent self.staking_agent = staking_agent
self.blockchain = staking_agent.blockchain self.blockchain = staking_agent.blockchain
self.token_agent = NucypherTokenAgent(blockchain=self.blockchain)
# Default State # Mode
self.trezor = trezor self.offline_mode = offline_mode
self.master_account = master_address
self.__accounts = list()
self.__stakers = dict()
# Setup # Setup
self.__funding_account = funding_account
self.__accounts = list()
self.__stakers = dict()
self.__get_accounts() self.__get_accounts()
self.sync() # Stakes
if sync_now:
self.read_stakes() # Stakes
def static_payload(self) -> dict:
"""Values to read/write from stakeholder JSON configuration files"""
payload = dict(accounts=self.__accounts,
stakers=self.__serialize_stake_info())
return payload
#
# Account Utilities
#
@property @property
def accounts(self): def accounts(self) -> list:
return self.__accounts return self.__accounts
def __get_accounts(self) -> None:
accounts = self.blockchain.interface.accounts
self.__accounts.extend(accounts)
def set_funding_account(self, address: str):
self.__funding_account = address
@property
def funding_account(self) -> str:
if self.__funding_account is NO_FUNDING_ACCOUNT:
raise self.NoFundingAccount
return self.__funding_account
@property
def funding_tokens(self) -> NU:
nunits = self.token_agent.get_balance(address=self.funding_account)
return NU.from_nunits(nunits)
@property
def funding_eth(self) -> Decimal:
ethers = self.blockchain.interface.client.w3.getBalance(self.funding_account)
return ethers
#
# Staking Utilities
#
def read_stakes(self) -> None:
for account in self.__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[staker] = staker.stakes
@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.keys())
@property @property
def stakes(self) -> list: def stakes(self) -> list:
payload = list() payload = list()
@ -703,79 +765,110 @@ class StakeHolder(BaseConfiguration):
return payload return payload
@property @property
def stakers(self) -> List[Staker]: def staker_balances(self) -> dict:
return list(self.__stakers.keys()) balances = dict()
for staker in self.stakers:
staker_funds = {'ETH': staker.eth_balance, 'NU': staker.token_balance}
balances = {staker.checksum_address: staker_funds}
return balances
def __serialize_stake_info(self) -> list: def __serialize_stake_info(self) -> list:
payload = list() payload = list()
for staker in self.stakers: for staker in self.stakers:
stake_info = [stake.to_stake_info() for stake in staker.stakes] stake_info = [stake.to_stake_info() for stake in staker.stakes]
worker_address = staker.worker_address worker_address = staker.worker_address or NO_WORKER_ASSIGNED
staker_payload = {'staker': staker.checksum_public_address, staker_funds = {'ETH': int(staker.eth_balance), 'NU': int(staker.token_balance)}
'worker': worker_address or NO_WORKER_ASSIGNED, staker_payload = {'staker': staker.checksum_address,
'balances': staker_funds,
'worker': worker_address,
'stakes': stake_info} 'stakes': stake_info}
payload.append(staker_payload) payload.append(staker_payload)
return payload return payload
def static_payload(self) -> dict: def get_active_staker(self, address: str) -> Staker:
payload = dict(trezor=self.trezor, self.read_stakes()
accounts=self.__accounts,
stakers=self.__serialize_stake_info())
return payload
def sync(self) -> None:
for account in self.__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[staker] = staker.stakes
def get_balances(self, address: str) -> dict:
staker = self.get_staker(address=address)
balances = {'ETH': staker.eth_balance,
'NU': staker.token_balance}
return balances
def __get_accounts(self) -> None:
try:
accounts = self.blockchain.interface.accounts[1:]
except IndexError:
raise self.ConfigurationError(f"A minimum of two accounts are "
f"required to create a {self.__class__.__name__}")
else:
for account in accounts:
self.__accounts.append(account)
if not self.master_account:
self.master_account = accounts[0]
def get_staker(self, address: str) -> Staker:
try: try:
staker = self.__stakers[address] staker = self.__stakers[address]
except KeyError: except KeyError:
if address not in self.__accounts: raise self.ConfigurationError(f"{address} does not have any stakes.")
raise RuntimeError(f"Unknown or locked account {address}")
staker = Staker(is_me=True, checksum_address=address)
return staker return staker
def set_worker(self, index: int, worker_address: str): def __create_staker(self, password: str) -> Staker:
stake = self.__stakers[index] if self.device:
result = self.staking_agent.set_worker(staker_address=stake.owner_address, worker_address=worker_address) raise NotImplementedError
else:
new_account = self.blockchain.interface.client.new_account(password=password)
self.__accounts.append(new_account)
staker = Staker(is_me=True, checksum_address=new_account)
return staker
def create_worker_configuration(self, staker_address: str, **configuration) -> str:
from nucypher.config.characters import UrsulaConfiguration
staker = self.get_active_staker(address=staker_address)
ursula_configuration = UrsulaConfiguration.generate(checksum_address=staker.checksum_address,
config_root=self.config_root,
federated_only=False,
**configuration)
filepath = ursula_configuration.to_configuration_file()
return filepath
#
# Actions
#
def set_worker(self, staker_address: str, worker_address: str):
staker = self.get_active_staker(address=staker_address)
result = self.staking_agent.set_worker(staker_address=staker.checksum_address,
worker_address=worker_address)
return result return result
def initialize_stake(self, address: str, amount: NU, duration: int): def initialize_stake(self,
staker = self.get_staker(address=address) password: str,
new_stake = Stake.initialize_stake(staker=staker, amount=amount, lock_periods=duration) amount: NU,
duration: int,
) -> Stake:
staker = self.__create_staker(password=password)
if self.funding_tokens < amount:
delta = amount - staker.token_balance
raise self.ConfigurationError(f"{self.funding_account} Insufficient NU (need {delta} more).")
if not self.funding_eth:
raise self.ConfigurationError(f"{self.funding_account} has no ETH")
_result = self.token_agent.transfer(amount=int(amount),
sender_address=self.funding_account,
target_address=staker.checksum_address)
new_stake = staker.initialize_stake(amount=amount, lock_periods=duration)
return new_stake return new_stake
def divide_stake(self, address: str, index: int, value: int): def divide_stake(self,
staker = self.get_staker(address=address) address: str,
index: int,
value: NU,
duration: int):
staker = self.get_active_staker(address=address)
if not staker.is_staking: if not staker.is_staking:
raise Stake.StakingError(f"{staker.checksum_public_address} has no published stakes.") raise Stake.StakingError(f"{staker.checksum_address} has no published stakes.")
try: result = staker.divide_stake(stake_index=index,
stake = staker.stakes[index] additional_periods=duration,
except IndexError: target_value=value)
raise
result = stake.divide(target_value=value)
return result return result
def sweep(self) -> dict:
"""Collect all rewards from all staking accounts"""
receipts = dict()
for staker in self.stakers:
receipt = self.collect_rewards(staker_address=staker.checksum_address)
receipts[staker.checksum_address] = receipt
return receipts
def collect_rewards(self,
staker_address: str,
staking: bool = True,
policy: bool = True) -> dict:
if not staking and not policy:
raise ValueError("Either staking or policy must be True in order to collect rewards")
staker = self.get_active_staker(address=staker_address)
if staking:
receipt = staker.collect_staking_reward()
if policy:
receipt = staker.collect_policy_reward(collector_address=self.funding_account)
return receipt

View File

@ -1,48 +1,70 @@
import json import json
import os import os
import pytest
from nucypher.blockchain.eth.actors import StakeHolder from nucypher.blockchain.eth.actors import StakeHolder
def test_software_stakeholder(testerchain, agency, blockchain_ursulas): @pytest.fixture(scope='session')
def stakeholder_config_file_location():
path = os.path.join('/', 'tmp', 'nucypher-test-stakeholder.json') path = os.path.join('/', 'tmp', 'nucypher-test-stakeholder.json')
return path
@pytest.fixture(scope='module')
def staking_software_stakeholder(testerchain,
agency,
blockchain_ursulas,
stakeholder_config_file_location):
# Setup
path = stakeholder_config_file_location
if os.path.exists(path): if os.path.exists(path):
os.remove(path) os.remove(path)
# Create stakeholder from on-chain values given accounts over a web3 provider # Create stakeholder from on-chain values given accounts over a web3 provider
stakeholder = StakeHolder(blockchain=testerchain, trezor=False) stakeholder = StakeHolder(blockchain=testerchain, trezor=False)
# Teardown
yield stakeholder
if os.path.exists(path):
os.remove(path)
def test_software_stakeholder_configuration(testerchain,
staking_software_stakeholder,
stakeholder_config_file_location):
stakeholder = staking_software_stakeholder
path = stakeholder_config_file_location
# Check attributes can be successfully read
assert stakeholder.total_stake
assert stakeholder.trezor is False assert stakeholder.trezor is False
assert len(stakeholder.stakes) assert stakeholder.stakes
assert len(stakeholder.accounts) assert stakeholder.accounts
# Save to file # Save the stakeholder JSON config
try: stakeholder.to_configuration_file(filepath=path)
with open(stakeholder.filepath, 'r') as file:
# Save the stakeholder JSN config # Ensure file contents are serializable
stakeholder.to_configuration_file(filepath=path) contents = file.read()
with open(path, 'r') as file: first_config_contents = json.loads(contents)
# Ensure file contents are serializable # Destroy this stake holder, leaving only the configuration file behind
contents = file.read() del stakeholder
deserialized_contents = json.loads(contents)
del stakeholder # Restore StakeHolder instance from JSON config
the_same_stakeholder = StakeHolder.from_configuration_file(filepath=path)
# Restore StakeHolder instance from JSON config # Save the JSON config again
stakeholder = StakeHolder.from_configuration_file(filepath=path) stakeholder.to_configuration_file(filepath=path, override=True)
with open(the_same_stakeholder.filepath, 'r') as file:
contents = file.read()
second_config_contents = json.loads(contents)
# Save the JSON config again # Ensure the stakeholder was accurately restored from JSON config
stakeholder.to_configuration_file(filepath=path, override=True) assert first_config_contents == second_config_contents
with open(path, 'r') as file: assert stakeholder == the_same_stakeholder
contents = file.read()
deserialized_contents_2 = json.loads(contents)
# Ensure the stakeholder was accurately restored from JSON config
assert deserialized_contents == deserialized_contents_2
finally:
if os.path.exists(path):
os.remove(path)