Agent validation and self-awareness, and documentation.

pull/330/head
Kieran Prasch 2018-06-10 12:58:49 -07:00
parent 7a35c4b24b
commit 8bc795e630
1 changed files with 62 additions and 21 deletions

View File

@ -7,22 +7,27 @@ from nucypher.blockchain.eth.agents import NucypherTokenAgent, MinerAgent, Polic
from constant_sorrow import constants
class NucypherTokenActor:
"""
Concrete base class for any actor that will interface with NuCypher's ethereum smart contracts
Concrete base class for any actor that will interface with NuCypher's ethereum smart contracts.
"""
class ActorError(Exception):
pass
def __init__(self, ether_address: Union[str, bytes, None]=None,
token_agent: NucypherTokenAgent=None, *args, **kwargs):
def __init__(self, ether_address: str=None, token_agent: NucypherTokenAgent=None, *args, **kwargs):
"""
:param ether_address: If not passed, we assume this is an unknown actor
:param token_agent: The token agent with the blockchain attached; If not passed, A default
token agent and blockchain connection will be created from default values.
"""
# Auto-connect, if needed
self.token_agent = token_agent if token_agent is not None else NucypherTokenAgent()
self.__ether_address = ether_address if ether_address is not None else constants.UNKNOWN_ACTOR
self.ether_address = ether_address if ether_address is not None else constants.UNKNOWN_ACTOR
self._transaction_cache = list() # track transactions transmitted
def __repr__(self):
@ -33,15 +38,9 @@ class NucypherTokenActor:
@classmethod
def from_config(cls, config):
"""Read actor data from a configuration file, and create an actor instance."""
raise NotImplementedError
#
# Crypto-asset balances
#
@property
def ether_address(self):
return self.__ether_address
def eth_balance(self):
"""Return this actors's current ETH balance"""
balance = self.token_agent.blockchain.interface.w3.eth.getBalance(self.ether_address)
@ -55,13 +54,13 @@ class NucypherTokenActor:
class Miner(NucypherTokenActor):
"""
Ursula, practically carrying a pickaxe.
Ursula baseclass for blockchain operations, practically carrying a pickaxe.
"""
class MinerError(NucypherTokenActor.ActorError):
pass
def __init__(self, miner_agent: MinerAgent=None, *args, **kwargs):
def __init__(self, is_me=True, miner_agent: MinerAgent=None, *args, **kwargs):
miner_agent = miner_agent if miner_agent is not None else MinerAgent()
super().__init__(token_agent=miner_agent.token_agent, *args, **kwargs)
@ -71,6 +70,7 @@ class Miner(NucypherTokenActor):
self.blockchain = self.token_agent.blockchain
# Establish initial state
self.is_me = is_me
self.__locked_tokens = constants.LOCKED_TOKENS_UNAVAILIBLE
self.__datastore_entries = constants.CONTRACT_DATASTORE_UNAVAILIBLE
self.__node_datastore = constants.CONTRACT_DATASTORE_UNAVAILIBLE
@ -81,6 +81,7 @@ class Miner(NucypherTokenActor):
@classmethod
def from_config(cls, blockchain_config) -> 'Miner':
"""Read miner data from a configuration file, and create an miner instance."""
# Use BlockchainConfig to default to the first wallet address
wallet_address = blockchain_config.wallet_addresses[0]
@ -93,6 +94,8 @@ class Miner(NucypherTokenActor):
#
def _approve_escrow(self, amount: int) -> str:
"""Approve the transfer of token from the miner's address to the escrow contract."""
if not self.is_me:
raise self.MinerError("Cannot execute contract staking functions with a non-self Miner instance.")
txhash = self.token_agent.contract.functions.approve(self.miner_agent.contract_address, amount).transact({'from': self.ether_address})
self.blockchain.wait_for_receipt(txhash)
@ -103,6 +106,8 @@ class Miner(NucypherTokenActor):
def _send_tokens_to_escrow(self, amount, lock_periods) -> str:
"""Send tokes to the escrow from the miner's address"""
if not self.is_me:
raise self.MinerError("Cannot execute contract staking functions with a non-self Miner instance.")
deposit_txhash = self.miner_agent.contract.functions.deposit(amount, lock_periods).transact({'from': self.ether_address})
@ -113,6 +118,9 @@ class Miner(NucypherTokenActor):
def deposit(self, amount: int, lock_periods: int) -> Tuple[str, str]:
"""Public facing method for token locking."""
if not self.is_me:
raise self.MinerError("Cannot execute contract staking functions with a non-self Miner instance.")
approve_txhash = self._approve_escrow(amount=amount)
deposit_txhash = self._send_tokens_to_escrow(amount=amount, lock_periods=lock_periods)
@ -145,6 +153,9 @@ class Miner(NucypherTokenActor):
#
# TODO add divide_stake method
def switch_lock(self):
if not self.is_me:
raise self.MinerError("Cannot execute contract staking functions with a non-self Miner instance.")
lock_txhash = self.miner_agent.contract.functions.switchLock().transact({'from': self.ether_address})
self.blockchain.wait_for_receipt(lock_txhash)
@ -152,6 +163,8 @@ class Miner(NucypherTokenActor):
return lock_txhash
def __validate_stake(self, amount: int, lock_periods: int) -> bool:
if not self.is_me:
raise self.MinerError("Cannot execute contract staking functions with a non-self Miner instance.")
from .constants import validate_locktime, validate_stake_amount
assert validate_stake_amount(amount=amount)
@ -165,9 +178,16 @@ class Miner(NucypherTokenActor):
def stake(self, amount, lock_periods, entire_balance=False):
"""
High level staking method for Miners.
"""
# manual type checking below this point; force an int to allow use of constants
:param amount: Amount of tokens to stake denominated in the smallest unit.
:param lock_periods: Duration of stake in periods.
:param entire_balance: If True, stake the entire balance of this node, or the maximum possible.
"""
if not self.is_me:
raise self.MinerError("Cannot execute contract staking functions with a non-self Miner instance.")
# manual type checking below this point; force an int to allow use of constants
amount, lock_periods = int(amount), int(lock_periods)
staking_transactions = OrderedDict() # Time series of txhases
@ -193,6 +213,9 @@ class Miner(NucypherTokenActor):
def confirm_activity(self) -> str:
"""Miner rewarded for every confirmed period"""
if not self.is_me:
raise self.MinerError("Cannot execute contract staking functions with a non-self Miner instance.")
txhash = self.miner_agent.contract.functions.confirmActivity().transact({'from': self.ether_address})
self.blockchain.wait_for_receipt(txhash)
@ -203,6 +226,9 @@ class Miner(NucypherTokenActor):
def mint(self) -> Tuple[str, str]:
"""Computes and transfers tokens to the miner's account"""
if not self.is_me:
raise self.MinerError("Cannot execute contract staking functions with a non-self Miner instance.")
mint_txhash = self.miner_agent.contract.functions.mint().transact({'from': self.ether_address})
self.blockchain.wait_for_receipt(mint_txhash)
@ -213,6 +239,9 @@ class Miner(NucypherTokenActor):
def collect_policy_reward(self, policy_manager):
"""Collect rewarded ETH"""
if not self.is_me:
raise self.MinerError("Cannot execute contract staking functions with a non-self Miner instance.")
policy_reward_txhash = policy_manager.contract.functions.withdraw().transact({'from': self.ether_address})
self.blockchain.wait_for_receipt(policy_reward_txhash)
@ -223,6 +252,9 @@ class Miner(NucypherTokenActor):
def collect_staking_reward(self) -> str:
"""Withdraw tokens rewarded for staking."""
if not self.is_me:
raise self.MinerError("Cannot execute contract staking functions with a non-self Miner instance.")
token_amount = self.miner_agent.contract.functions.minerInfo(self.ether_address).call()[0]
staked_amount = max(self.miner_agent.contract.functions.getLockedTokens(self.ether_address).call(),
self.miner_agent.contract.functions.getLockedTokens(self.ether_address, 1).call())
@ -238,9 +270,12 @@ class Miner(NucypherTokenActor):
# Miner Datastore
#
def publish_datastore(self, data) -> str:
def _publish_datastore(self, data) -> str:
"""Publish new data to the MinerEscrow contract as a public record associated with this miner."""
if not self.is_me:
raise self.MinerError("Cannot write to contract datastore with a non-self Miner instance.")
txhash = self.miner_agent.contract.functions.setMinerId(data).transact({'from': self.ether_address})
self.blockchain.wait_for_receipt(txhash)
@ -260,13 +295,14 @@ class Miner(NucypherTokenActor):
yield value
self.__node_datastore = node_datastore_reader()
def read_datastore(self, index: int=None, refresh=False):
def _read_datastore(self, index: int=None, refresh=False):
"""
Read a value from the nodes datastore, within the MinersEscrow ethereum contract.
since there may be multiple values, select one, and return it. The most recently
pushed entry is returned by default, and can be specified with the index parameter.
If refresh it True, read the node's data from the blockchain before returning.
"""
if refresh is True:
self.__fetch_node_datastore()
@ -285,13 +321,18 @@ class Miner(NucypherTokenActor):
class PolicyAuthor(NucypherTokenActor):
"""Alice, mocking up new policies!"""
"""Alice base class for blockchain operations, mocking up new policies!"""
def __init__(self, policy_agent: PolicyAgent=None, *args, **kwargs):
"""
:param policy_agent: A policy agent with the blockchain attached; If not passed, A default policy
agent and blockchain connection will be created from default values.
"""
# From defaults
if policy_agent is None:
# all defaults
# From defaults
self.token_agent = NucypherTokenAgent()
self.miner_agent = MinerAgent(token_agent=self.token_agent)
self.policy_agent = PolicyAgent(miner_agent=self.miner_agent)