mirror of https://github.com/nucypher/nucypher.git
Detailing StakeHolder actions
parent
d7b03de176
commit
3953da73ad
|
@ -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
|
||||||
|
|
|
@ -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:
|
|
||||||
|
|
||||||
# Save the stakeholder JSN config
|
|
||||||
stakeholder.to_configuration_file(filepath=path)
|
stakeholder.to_configuration_file(filepath=path)
|
||||||
with open(path, 'r') as file:
|
with open(stakeholder.filepath, 'r') as file:
|
||||||
|
|
||||||
# Ensure file contents are serializable
|
# Ensure file contents are serializable
|
||||||
contents = file.read()
|
contents = file.read()
|
||||||
deserialized_contents = json.loads(contents)
|
first_config_contents = json.loads(contents)
|
||||||
|
|
||||||
|
# Destroy this stake holder, leaving only the configuration file behind
|
||||||
del stakeholder
|
del stakeholder
|
||||||
|
|
||||||
# Restore StakeHolder instance from JSON config
|
# Restore StakeHolder instance from JSON config
|
||||||
stakeholder = StakeHolder.from_configuration_file(filepath=path)
|
the_same_stakeholder = StakeHolder.from_configuration_file(filepath=path)
|
||||||
|
|
||||||
# Save the JSON config again
|
# Save the JSON config again
|
||||||
stakeholder.to_configuration_file(filepath=path, override=True)
|
stakeholder.to_configuration_file(filepath=path, override=True)
|
||||||
with open(path, 'r') as file:
|
with open(the_same_stakeholder.filepath, 'r') as file:
|
||||||
contents = file.read()
|
contents = file.read()
|
||||||
deserialized_contents_2 = json.loads(contents)
|
second_config_contents = json.loads(contents)
|
||||||
|
|
||||||
# Ensure the stakeholder was accurately restored from JSON config
|
# Ensure the stakeholder was accurately restored from JSON config
|
||||||
assert deserialized_contents == deserialized_contents_2
|
assert first_config_contents == second_config_contents
|
||||||
|
assert stakeholder == the_same_stakeholder
|
||||||
finally:
|
|
||||||
if os.path.exists(path):
|
|
||||||
os.remove(path)
|
|
||||||
|
|
Loading…
Reference in New Issue