From e8f44e611e8ae7d47651152e284f23fffc1e38dc Mon Sep 17 00:00:00 2001 From: Kieran Prasch Date: Thu, 22 Mar 2018 17:02:09 -0700 Subject: [PATCH] [KMS-ETH]- Extracts policies module, API updates, further collapse contract wrapper logic up inheritance tree. Enhances docstrings. --- nkms_eth/actors.py | 160 +++++++++++++-------------------- nkms_eth/agents.py | 90 +++++++++++++++---- nkms_eth/base.py | 137 ---------------------------- nkms_eth/blockchain.py | 2 +- nkms_eth/deployers.py | 164 ++++++++++++++++++++++++---------- nkms_eth/policies.py | 42 ++++++--- tests/conftest.py | 22 +++-- tests/entities/test_actors.py | 32 +++---- tests/utilities.py | 2 +- 9 files changed, 314 insertions(+), 337 deletions(-) delete mode 100644 nkms_eth/base.py diff --git a/nkms_eth/actors.py b/nkms_eth/actors.py index e92f9ad26..e8eacbfc1 100644 --- a/nkms_eth/actors.py +++ b/nkms_eth/actors.py @@ -1,73 +1,43 @@ +from abc import ABC from collections import OrderedDict -from typing import Tuple, List +from datetime import datetime +from typing import Tuple, List, Union -# from nkms_eth.agents import MinerAgent, PolicyAgent -from nkms_eth.base import Actor +from nkms_eth.agents import NuCypherKMSTokenAgent +from nkms_eth.policies import BlockchainArrangement -class PolicyArrangement: - def __init__(self, author: 'PolicyAuthor', miner: 'Miner', value: int, - periods: int, arrangement_id: bytes=None): +class TokenActor(ABC): - if arrangement_id is None: - self.id = self.__class__._generate_arrangement_id() # TODO: Generate policy ID + def __init__(self, token_agent: NuCypherKMSTokenAgent, address: Union[bytes, str]): + self.token_agent = token_agent - # The relationship exists between two addresses - self.author = author - self.policy_agent = author.policy_agent - - self.miner = miner - - # Arrangement value, rate, and duration - rate = value // periods - self._rate = rate - - self.value = value - self.periods = periods # TODO: datetime -> duration in blocks - - self.is_published = False - - @staticmethod - def _generate_arrangement_id(policy_hrac: bytes) -> bytes: - pass # TODO + if isinstance(address, bytes): + address = address.hex() + self.address = address def __repr__(self): class_name = self.__class__.__name__ - r = "{}(client={}, node={})" - r = r.format(class_name, self.author, self.miner) + r = "{}(address='{}')" + r.format(class_name, self.address) return r - def publish(self, gas_price: int) -> str: + def eth_balance(self): + """Return this actors's current ETH balance""" - payload = {'from': self.author.address, - 'value': self.value, - 'gas_price': gas_price} + balance = self.token_agent._blockchain._chain.web3.eth.getBalance(self.address) + return balance - txhash = self.policy_agent.transact(payload).createPolicy(self.id, - self.miner.address, - self.periods) + def token_balance(self): + """Return this actors's current token balance""" - self.policy_agent._blockchain._chain.wait.for_receipt(txhash) - self.publish_transaction = txhash - self.is_published = True - return txhash - - def __update_periods(self) -> None: - blockchain_record = self.policy_agent.fetch_arrangement_data(self.id) - client, delegate, rate, *periods = blockchain_record - self._elapsed_periods = periods - - def revoke(self, gas_price: int) -> str: - """Revoke this arrangement and return the transaction hash as hex.""" - txhash = self.policy_agent.revoke_arrangement(self.id, author=self.author, gas_price=gas_price) - self.revoke_transaction = txhash - return txhash + balance = self.token_agent.get_balance(address=self.address) + return balance -class Miner(Actor): +class Miner(TokenActor): """ - Practically carrying a pickaxe. - Intended for use as an Ursula mixin. + Ursula - practically carrying a pickaxe. Accepts a running blockchain, deployed token contract, and deployed escrow contract. If the provided token and escrow contracts are not deployed, @@ -76,28 +46,30 @@ class Miner(Actor): """ def __init__(self, miner_agent, address): - super().__init__(address) + super().__init__(token_agent=miner_agent.token_agent, address=address) self.miner_agent = miner_agent miner_agent.miners.append(self) # Track Miners - self._token_agent = miner_agent._token - self._blockchain = self._token_agent._blockchain + self.token_agent = miner_agent.token_agent + self._blockchain = self.token_agent._blockchain - self._transactions = list() + self._transactions = OrderedDict() self._locked_tokens = self._update_locked_tokens() def _update_locked_tokens(self) -> None: + """Query the contract for the amount of locked tokens on this miner's eth address and cache it""" + self._locked_tokens = self.miner_agent.call().getLockedTokens(self.address) return None def _approve_escrow(self, amount: int) -> str: """Approve the transfer of token from the miner's address to the escrow contract.""" - txhash = self._token_agent.transact({'from': self.address}).approve(self.miner_agent._contract.address, amount) + txhash = self.token_agent.transact({'from': self.address}).approve(self.miner_agent._contract.address, amount) self._blockchain._chain.wait.for_receipt(txhash, timeout=self._blockchain._timeout) - self._transactions.append(txhash) + self._transactions[datetime.now()] = txhash return txhash @@ -107,12 +79,16 @@ class Miner(Actor): deposit_txhash = self.miner_agent.transact({'from': self.address}).deposit(amount, locktime) self._blockchain._chain.wait.for_receipt(deposit_txhash, timeout=self._blockchain._timeout) - self._transactions.append(deposit_txhash) + self._transactions[datetime.now()] = deposit_txhash return deposit_txhash @property - def is_staking(self): + def is_staking(self, query=True): + """Checks if this Miner currently has locked tokens.""" + + if query: + self._update_locked_tokens() return bool(self._locked_tokens > 0) def lock(self, amount: int, locktime: int) -> Tuple[str, str, str]: @@ -124,7 +100,7 @@ class Miner(Actor): lock_txhash = self.miner_agent.transact({'from': self.address}).switchLock() self._blockchain._chain.wait.for_receipt(lock_txhash, timeout=self._blockchain._timeout) - self._transactions.extend([approve_txhash, deposit_txhash, lock_txhash]) + self._transactions[datetime.now()] = (approve_txhash, deposit_txhash, lock_txhash) return approve_txhash, deposit_txhash, lock_txhash @@ -134,7 +110,7 @@ class Miner(Actor): txhash = self.miner_agent.transact({'from': self.address}).confirmActivity() self._blockchain._chain.wait.for_receipt(txhash) - self._transactions.append(txhash) + self._transactions[datetime.now()] = txhash return txhash @@ -144,7 +120,7 @@ class Miner(Actor): txhash = self.miner_agent.transact({'from': self.address}).mint() self._blockchain._chain.wait.for_receipt(txhash, timeout=self._blockchain._timeout) - self._transactions.append(txhash) + self._transactions[datetime.now()] = txhash return txhash @@ -154,7 +130,7 @@ class Miner(Actor): txhash = policy_manager.transact({'from': self.address}).withdraw() self._blockchain._chain.wait.for_receipt(txhash) - self._transactions.append(txhash) + self._transactions[datetime.now()] = txhash return txhash @@ -164,45 +140,34 @@ class Miner(Actor): txhash = self.miner_agent.transact({'from': self.address}).setMinerId(miner_id) self._blockchain._chain.wait.for_receipt(txhash) - self._transactions.append(txhash) + self._transactions[datetime.now()] = txhash return txhash def fetch_miner_ids(self) -> tuple: """Retrieve all stored Miner IDs on this miner""" - count = self.escrow().getMinerInfo(self.escrow.MinerInfoField.MINER_IDS_LENGTH.value, - self.address, - 0).encode('latin-1') + count = self.miner_agent.call().getMinerInfo(self.miner_agent.MinerInfoField.MINER_IDS_LENGTH.value, + self.address, + 0).encode('latin-1') count = self._blockchain._chain.web3.toInt(count) miner_ids = list() for index in range(count): - miner_id = self.miner_agent.call().getMinerInfo(self.escrow.MinerInfoField.MINER_ID.value, self.address, index) + miner_id = self.miner_agent.call().getMinerInfo(self.miner_agent.MinerInfoField.MINER_ID.value, self.address, index) encoded_miner_id = miner_id.encode('latin-1') # TODO change when v4 of web3.py is released miner_ids.append(encoded_miner_id) return tuple(miner_ids) - def eth_balance(self): - return self._blockchain._chain.web3.eth.getBalance(self.address) - - def token_balance(self) -> int: - """Check miner's current token balance""" - - # self._token_agent._check_contract_deployment() - balance = self._token_agent.call().balanceOf(self.address) - - return balance - def withdraw(self, amount: int=0, entire_balance=False) -> str: """Withdraw tokens""" tokens_amount = self._blockchain._chain.web3.toInt( - self.escrow().getMinerInfo(self.escrow.MinerInfoField.VALUE.value, self.address, 0).encode('latin-1')) + self.miner_agent.call().getMinerInfo(self.miner_agent.MinerInfoField.VALUE.value, self.address, 0).encode('latin-1')) - txhash = self.escrow.transact({'from': self.address}).withdraw(tokens_amount) + txhash = self.miner_agent.call().transact({'from': self.address}).withdraw(tokens_amount) self._blockchain._chain.wait.for_receipt(txhash, timeout=self._blockchain._timeout) @@ -210,39 +175,39 @@ class Miner(Actor): raise Exception("Specify an amount or entire balance, not both") if entire_balance: - txhash = self.escrow.transact({'from': self.address}).withdraw(tokens_amount) + txhash = self.miner_agent.call().transact({'from': self.address}).withdraw(tokens_amount) else: - txhash = self.escrow.transact({'from': self.address}).withdraw(amount) + txhash = self.miner_agent.call().transact({'from': self.address}).withdraw(amount) - self._transactions.append(txhash) self._blockchain._chain.wait.for_receipt(txhash, timeout=self._blockchain._timeout) return txhash -class PolicyAuthor(Actor): +class PolicyAuthor(TokenActor): """Alice""" def __init__(self, address: bytes, policy_agent): + super().__init__(token_agent=policy_agent._token, address=address) self.policy_agent = policy_agent - super().__init__(address) + self._arrangements = OrderedDict() # Track authored policies by id - def make_arrangement(self, miner: Miner, periods: int, rate: int, arrangement_id: bytes=None) -> PolicyArrangement: + def make_arrangement(self, miner: Miner, periods: int, rate: int, arrangement_id: bytes=None) -> 'BlockchainArrangement': """ Create a new arrangement to carry out a blockchain policy for the specified rate and time. """ value = rate * periods - arrangement = PolicyArrangement(author=self, - miner=miner, - value=value, - periods=periods) + arrangement = BlockchainArrangement(author=self, + miner=miner, + value=value, + periods=periods) self._arrangements[arrangement.id] = {arrangement_id: arrangement} return arrangement - def get_arrangement(self, arrangement_id: bytes) -> PolicyArrangement: + def get_arrangement(self, arrangement_id: bytes) -> BlockchainArrangement: """Fetch a published arrangement from the blockchain""" blockchain_record = self.policy_agent.call().policies(arrangement_id) @@ -251,13 +216,13 @@ class PolicyAuthor(Actor): duration = end_block - start_block miner = Miner(address=miner_address, miner_agent=self.policy_agent.miner_agent) - arrangement = PolicyArrangement(author=self, miner=miner, periods=duration) + arrangement = BlockchainArrangement(author=self, miner=miner, periods=duration) arrangement.is_published = True return arrangement def revoke_arrangement(self, arrangement_id): - """Lookup the arrangement in the cache and revoke it on the blockchain""" + """Get the arrangement from the cache and revoke it on the blockchain""" try: arrangement = self._arrangements[arrangement_id] except KeyError: @@ -267,9 +232,12 @@ class PolicyAuthor(Actor): return txhash def recruit(self, quantity: int) -> List[str]: + """Uses sampling logic to gather""" + miner_addresses = self.policy_agent.miner_agent.sample(quantity=quantity) return miner_addresses def balance(self): + """Get the balance of this actor's address""" return self.policy_agent.miner_agent.call().balanceOf(self.address) diff --git a/nkms_eth/agents.py b/nkms_eth/agents.py index 975100095..8568334e8 100644 --- a/nkms_eth/agents.py +++ b/nkms_eth/agents.py @@ -1,27 +1,80 @@ import random +from abc import ABC from typing import Set, Generator, List -from nkms_eth.actors import PolicyAuthor -from nkms_eth.base import EthereumContractAgent -from nkms_eth.blockchain import TheBlockchain -from nkms_eth.deployers import MinerEscrowDeployer, NuCypherKMSTokenDeployer, PolicyManagerDeployer +from nkms_eth.deployers import MinerEscrowDeployer, NuCypherKMSTokenDeployer, PolicyManagerDeployer, ContractDeployer + + +class EthereumContractAgent(ABC): + _principal_contract_name = NotImplemented + + _contract_subclasses = list() + + class ContractNotDeployed(ContractDeployer.ContractDeploymentError): + pass + + def __init__(self, blockchain, *args, **kwargs): + + self._blockchain = blockchain + self._contract = self.__fetch_contract() + + @classmethod + def __init_subclass__(cls, deployer, **kwargs): + """ + https://www.python.org/dev/peps/pep-0487/#proposal + """ + super().__init_subclass__(**kwargs) + cls._deployer = deployer + cls._principal_contract_name = deployer._contract_name + cls._contract_subclasses.append(cls) + + def __repr__(self): + class_name = self.__class__.__name__ + r = "{}(blockchain={}, contract={})" + return r.format(class_name, self._blockchain, self._contract) + + def __eq__(self, other): + return bool(self.contract_address == other.contract_address) + + @property + def contract_address(self): + return self._contract.address + + @property + def contract_name(self) -> str: + return self._principal_contract_name + + @property + def origin(self) -> str: + return self._blockchain._chain.web3.eth.accounts[0] # TODO: make swappable + + def __fetch_contract(self): + contract = self._blockchain._chain.provider.get_contract(self._principal_contract_name) + return contract + + def call(self): + return self._contract.call() + + def transact(self, payload: dict): + """Packs kwargs into payload dictionary and transmits an eth contract transaction""" + return self._contract.transact(payload) + + def get_balance(self, address: str=None) -> int: + """Get the balance of a token address, or of this contract address""" + if address is None: + address = self.contract_address + return self.call().balanceOf(address) class NuCypherKMSTokenAgent(EthereumContractAgent, deployer=NuCypherKMSTokenDeployer): - def __init__(self, blockchain: TheBlockchain): - self._blockchain = blockchain - super().__init__(self) + _principal_contract_name = NotImplemented def registrar(self): """Retrieve all known addresses for this contract""" all_known_address = self._blockchain._chain.registrar.get_contract_address(self._principal_contract_name) return all_known_address - def balance(self, address: str) -> int: - """Get the balance of a token address""" - return self.call().balanceOf(address) - class MinerAgent(EthereumContractAgent, deployer=MinerEscrowDeployer): """ @@ -36,20 +89,21 @@ class MinerAgent(EthereumContractAgent, deployer=MinerEscrowDeployer): class NotEnoughUrsulas(Exception): pass - def __init__(self, token: NuCypherKMSTokenAgent): - super().__init__(agent=token) - self._token = token + def __init__(self, token_agent: NuCypherKMSTokenAgent): + super().__init__(blockchain=token_agent._blockchain) + self.token_agent = token_agent self.miners = list() def get_miner_ids(self) -> Set[str]: """ Fetch all miner IDs from the local cache and return them in a set """ + return {miner.get_id() for miner in self.miners} def swarm(self) -> Generator[str, None, None]: """ - Generates all miner addresses via cumulative sum on-network. + Returns an iterator of all miner addresses via cumulative sum, on-network. """ count = self.call().getMinerInfo(self._deployer.MinerInfoField.MINERS_LENGTH.value, self._deployer.null_address, 0).encode('latin-1') count = self._blockchain._chain.web3.toInt(count) @@ -106,17 +160,19 @@ class MinerAgent(EthereumContractAgent, deployer=MinerEscrowDeployer): class PolicyAgent(EthereumContractAgent, deployer=PolicyManagerDeployer): def __init__(self, miner_agent): - super().__init__(miner_agent) + super().__init__(blockchain=miner_agent._blockchain) self.miner_agent = miner_agent def fetch_arrangement_data(self, arrangement_id: bytes) -> list: blockchain_record = self.call().policies(arrangement_id) return blockchain_record - def revoke_arrangement(self, arrangement_id: bytes, author: 'PolicyAuthor', gas_price: int): + def revoke_arrangement(self, arrangement_id: bytes, author, gas_price: int): """ Revoke by arrangement ID; Only the policy author can revoke the policy """ + txhash = self.transact({'from': author.address, 'gas_price': gas_price}).revokePolicy(arrangement_id) self._blockchain._chain.wait.for_receipt(txhash) + return txhash diff --git a/nkms_eth/base.py b/nkms_eth/base.py deleted file mode 100644 index 504957de2..000000000 --- a/nkms_eth/base.py +++ /dev/null @@ -1,137 +0,0 @@ -from abc import ABC, abstractmethod - -from nkms_eth.blockchain import TheBlockchain - - -class Actor(ABC): - def __init__(self, address): - if isinstance(address, bytes): - address = address.hex() - self.address = address - - def __repr__(self): - class_name = self.__class__.__name__ - r = "{}(address='{}')" - r.format(class_name, self.address) - return r - - -class ContractDeployer(ABC): - - _contract_name = NotImplemented - - class ContractDeploymentError(Exception): - pass - - def __init__(self, blockchain): - self.__armed = False - self._contract = None - - # Sanity check - if not isinstance(blockchain, TheBlockchain): - error = 'Only TheBlockchain can be used to create a deployer, got {}.' - raise ValueError(error.format(type(blockchain))) - self._blockchain = blockchain - - def __eq__(self, other): - return self._contract.address == other.address - - @property - def contract_address(self) -> str: - try: - address = self._contract.address - except AttributeError: - cls = self.__class__ - raise cls.ContractDeploymentError('Contract not deployed') - else: - return address - - @property - def is_deployed(self) -> bool: - return bool(self._contract is not None) - - @property - def is_armed(self) -> bool: - return bool(self.__armed is True) - - def _verify_contract_deployment(self) -> None: - """Raises ContractDeploymentError if the contract has not been armed and deployed.""" - if not self._contract: - class_name = self.__class__.__name__ - message = '{} contract is not deployed. Arm, then deploy.'.format(class_name) - raise self.ContractDeploymentError(message) - return None - - def arm(self) -> None: - self.__armed = True - return None - - @abstractmethod - def deploy(self) -> str: - raise NotImplementedError - - # TODO - # @abstractmethod - # def make_agent(self) -> 'EthereumContractAgent': - # raise NotImplementedError - - @classmethod - def from_blockchain(cls, blockchain: TheBlockchain) -> 'ContractDeployer': - """ - Returns the NuCypherKMSToken object, - or raises UnknownContract if the contract has not been deployed. - """ - contract = blockchain._chain.provider.get_contract(cls._contract_name) - instance = cls(blockchain=blockchain) - instance._contract = contract - return instance - - -class EthereumContractAgent(ABC): - _deployer = NotImplemented - _principal_contract_name = NotImplemented - - class ContractNotDeployed(ContractDeployer.ContractDeploymentError): - pass - - def __init__(self, agent, *args, **kwargs): - - self._blockchain = agent._blockchain - - contract = self._blockchain._chain.provider.get_contract(self._principal_contract_name) - self._contract = contract - - @classmethod - def __init_subclass__(cls, deployer, **kwargs): - """ - https://www.python.org/dev/peps/pep-0487/#proposal - """ - cls._deployer = deployer - cls._principal_contract_name = deployer._contract_name - super().__init_subclass__(**kwargs) - - def __repr__(self): - class_name = self.__class__.__name__ - r = "{}(blockchain={}, contract={})" - return r.format(class_name, self._blockchain, self._contract) - - def __eq__(self, other): - return bool(self.contract_address == other.contract_address) - - def call(self): - return self._contract.call() - - def transact(self, *args, **kwargs): - return self._contract.transact(*args, **kwargs) - - @property - def origin(self): - return self._blockchain._chain.web3.eth.accounts[0] # TODO - - @property - def contract_address(self): - return self._contract.address - - @property - def contract_name(self): - return self._principal_contract_name diff --git a/nkms_eth/blockchain.py b/nkms_eth/blockchain.py index eded3192d..d8ec48416 100644 --- a/nkms_eth/blockchain.py +++ b/nkms_eth/blockchain.py @@ -83,4 +83,4 @@ class TheBlockchain: while not_time_yet: self._chain.wait.for_block(self._chain.web3.eth.blockNumber+step) current_block = self._chain.web3.eth.getBlock(self._chain.web3.eth.blockNumber) - not_time_yet = current_block.timestamp < end_timestamp + not_time_yet = current_block.timestamp < end_timestamp \ No newline at end of file diff --git a/nkms_eth/deployers.py b/nkms_eth/deployers.py index 132f0c6c4..e86ae7da8 100644 --- a/nkms_eth/deployers.py +++ b/nkms_eth/deployers.py @@ -1,23 +1,86 @@ +from abc import ABC, abstractmethod from typing import Tuple -from nkms_eth.base import ContractDeployer from nkms_eth.config import NuCypherMinerConfig, NuCypherTokenConfig from .blockchain import TheBlockchain -addr = str + +class ContractDeployer(ABC): + + _contract_name = NotImplemented + _agency = NotImplemented + + class ContractDeploymentError(Exception): + pass + + def __init__(self, blockchain: TheBlockchain): + self.__armed = False + self._contract = None + + # Sanity check + if not isinstance(blockchain, TheBlockchain): + error = 'Only TheBlockchain can be used to create a deployer, got {}.' + raise ValueError(error.format(type(blockchain))) + self._blockchain = blockchain + + def __eq__(self, other): + return self._contract.address == other.address + + @property + def contract_address(self) -> str: + try: + address = self._contract.address + except AttributeError: + cls = self.__class__ + raise cls.ContractDeploymentError('Contract not deployed') + else: + return address + + @property + def is_deployed(self) -> bool: + return bool(self._contract is not None) + + @property + def is_armed(self) -> bool: + return bool(self.__armed is True) + + def _ensure_contract_deployment(self) -> None: + """Raises ContractDeploymentError if the contract has not been armed and deployed.""" + + if self._contract is None: + class_name = self.__class__.__name__ + message = '{} contract is not deployed. Arm, then deploy.'.format(class_name) + raise self.ContractDeploymentError(message) + return None + + def arm(self) -> None: + if self.__armed is True: + raise self.ContractDeploymentError("Deployer already armed, use .deploy() to deploy.") + self.__armed = True + return None + + @abstractmethod + def deploy(self) -> str: + raise NotImplementedError + + @classmethod + def from_blockchain(cls, blockchain: TheBlockchain) -> 'ContractDeployer': + """Returns the NuCypherKMSToken object, or raises UnknownContract if the contract has not been deployed.""" + + contract = blockchain._chain.provider.get_contract(cls._contract_name) + instance = cls(blockchain=blockchain) + instance._contract = contract + + return instance class NuCypherKMSTokenDeployer(ContractDeployer, NuCypherTokenConfig): _contract_name = 'NuCypherKMSToken' - def __init__(self, blockchain: TheBlockchain): + def __init__(self, blockchain): super().__init__(blockchain=blockchain) - self.__creator = self._blockchain._chain.web3.eth.accounts[0] - - @property - def origin(self): - return self.__creator + self._creator = self._blockchain._chain.web3.eth.accounts[0] def deploy(self) -> str: """ @@ -39,7 +102,7 @@ class NuCypherKMSTokenDeployer(ContractDeployer, NuCypherTokenConfig): the_nucypher_token_contract, deployment_txhash = self._blockchain._chain.provider.deploy_contract( self._contract_name, deploy_args=[self.saturation], - deploy_transaction={'from': self.origin}) + deploy_transaction={'from': self._creator}) self._blockchain._chain.wait.for_receipt(deployment_txhash, timeout=self._blockchain._timeout) self._contract = the_nucypher_token_contract @@ -47,42 +110,16 @@ class NuCypherKMSTokenDeployer(ContractDeployer, NuCypherTokenConfig): return deployment_txhash -class PolicyManagerDeployer(ContractDeployer): - - _contract_name = 'PolicyManager' - - def __init__(self, miner_agent): - super().__init__(miner_agent) - self.miner_agent = miner_agent - self.token_agent = miner_agent._token_agent - - def deploy(self) -> Tuple[str, str]: - if self.is_armed is False: - raise self.ContractDeploymentError('PolicyManager contract not armed') - if self.is_deployed is True: - raise self.ContractDeploymentError('PolicyManager contract already deployed') - - # Creator deploys the policy manager - the_policy_manager_contract, deploy_txhash = self._blockchain._chain.provider.deploy_contract( - self._contract_name, - deploy_args=[self.miner_agent._contract.address], - deploy_transaction={'from': self.token_agent.creator}) - - self._contract = the_policy_manager_contract - - set_txhash = self.miner_agent.transact({'from': self.token_agent.creator}).setPolicyManager(the_policy_manager_contract.address) - self._blockchain._chain.wait.for_receipt(set_txhash) - - return deploy_txhash, set_txhash - - class MinerEscrowDeployer(ContractDeployer, NuCypherMinerConfig): + """ + Depends on NuCypherTokenAgent + """ _contract_name = 'MinersEscrow' def __init__(self, token_agent): - self._token_agent = token_agent super().__init__(blockchain=token_agent._blockchain) + self.token_agent = token_agent def deploy(self) -> Tuple[str, str, str]: """ @@ -103,21 +140,54 @@ class MinerEscrowDeployer(ContractDeployer, NuCypherMinerConfig): message = '{} contract already deployed, use .get() to retrieve it.'.format(class_name) raise self.ContractDeploymentError(message) - deploy_args = [self._token_agent._contract.address] + self.mining_coefficient - deploy_tx = {'from': self._token_agent.origin} + deploy_args = [self.token_agent._contract.address] + self.mining_coefficient + deploy_tx = {'from': self.token_agent.origin} the_escrow_contract, deploy_txhash = self._blockchain._chain.provider.deploy_contract(self._contract_name, deploy_args=deploy_args, deploy_transaction=deploy_tx) - self._blockchain._chain.wait.for_receipt(deploy_txhash, timeout=self._blockchain._timeout) + timeout = self._blockchain._timeout + self._blockchain._chain.wait.for_receipt(deploy_txhash, timeout=timeout) self._contract = the_escrow_contract - reward_txhash = self._token_agent.transact({'from': self._token_agent.origin}).transfer(self.contract_address, - self.reward) - self._blockchain._chain.wait.for_receipt(reward_txhash, timeout=self._blockchain._timeout) + reward_txhash = self.token_agent.transact({'from': self.token_agent.origin}).transfer(self.contract_address, + self.reward) + self._blockchain._chain.wait.for_receipt(reward_txhash, timeout=timeout) - init_txhash = self._contract.transact({'from': self._token_agent.origin}).initialize() - self._blockchain._chain.wait.for_receipt(init_txhash, timeout=self._blockchain._timeout) + init_txhash = self._contract.transact({'from': self.token_agent.origin}).initialize() + self._blockchain._chain.wait.for_receipt(init_txhash, timeout=timeout) return deploy_txhash, reward_txhash, init_txhash + + +class PolicyManagerDeployer(ContractDeployer): + """ + Depends on MinerAgent and NuCypherTokenAgent + """ + + _contract_name = 'PolicyManager' + + def __init__(self, miner_agent): + super().__init__(blockchain=token_agent._blockchain) + self.token_agent = miner_agent._token_agent + self.miner_agent = miner_agent + + def deploy(self) -> Tuple[str, str]: + if self.is_armed is False: + raise self.ContractDeploymentError('PolicyManager contract not armed') + if self.is_deployed is True: + raise self.ContractDeploymentError('PolicyManager contract already deployed') + + # Creator deploys the policy manager + the_policy_manager_contract, deploy_txhash = self._blockchain._chain.provider.deploy_contract( + self._contract_name, + deploy_args=[self.miner_agent._contract.address], + deploy_transaction={'from': self.token_agent.creator}) + + self._contract = the_policy_manager_contract + + set_txhash = self.miner_agent.transact({'from': self.token_agent.creator}).setPolicyManager(the_policy_manager_contract.address) + self._blockchain._chain.wait.for_receipt(set_txhash) + + return deploy_txhash, set_txhash diff --git a/nkms_eth/policies.py b/nkms_eth/policies.py index f14c44824..c6bcc5529 100644 --- a/nkms_eth/policies.py +++ b/nkms_eth/policies.py @@ -7,15 +7,19 @@ from nkms_eth.token import NuCypherKMSToken class PolicyArrangement: - def __init__(self, author: 'PolicyAuthor', miner: 'Miner', value: int, - periods: int, arrangement_id: bytes=None): + """ + A relationship between Alice and a single Ursula as part of BlockchainPolicy + """ + + def __init__(self, author, miner, value: int, periods: int, arrangement_id: bytes=None): if arrangement_id is None: self.id = self.__class__._generate_arrangement_id() # TODO: Generate policy ID - # The relationship between two addresses + # The relationship exists between two addresses self.author = author - self.policy_manager = author.policy_manager + self.policy_agent = author.policy_agent + self.miner = miner @@ -28,7 +32,6 @@ class PolicyArrangement: self.is_published = False - @staticmethod def _generate_arrangement_id(policy_hrac: bytes) -> bytes: pass # TODO @@ -45,23 +48,25 @@ class PolicyArrangement: 'value': self.value, 'gas_price': gas_price} - txhash = self.policy_manager.transact(payload).createPolicy(self.id, - self.miner.address, - self.periods) - self.policy_manager.blockchain._chain.wait.for_receipt(txhash) + txhash = self.policy_agent.transact(payload).createPolicy(self.id, + self.miner.address, + self.periods) + + self.policy_agent._blockchain._chain.wait.for_receipt(txhash) + self.publish_transaction = txhash self.is_published = True return txhash def __update_periods(self) -> None: - blockchain_record = self.policy_manager.fetch_arrangement_data(self.id) + blockchain_record = self.policy_agent.fetch_arrangement_data(self.id) client, delegate, rate, *periods = blockchain_record self._elapsed_periods = periods def revoke(self, gas_price: int) -> str: """Revoke this arrangement and return the transaction hash as hex.""" - txhash = self.policy_manager.revoke_arrangement(self.id, author=self.author, gas_price=gas_price) + txhash = self.policy_agent.revoke_arrangement(self.id, author=self.author, gas_price=gas_price) self.revoke_transaction = txhash return txhash @@ -195,3 +200,18 @@ class PolicyAuthor: def balance(self): return self.policy_manager.token().balanceOf(self.address) + + def revoke(self, gas_price: int) -> str: + """Revoke this arrangement and return the transaction hash as hex.""" + + txhash = self.policy_agent.revoke_arrangement(self.id, author=self.author, gas_price=gas_price) + self.revoke_transaction = txhash + + return txhash + + +class BlockchainPolicy: + """A collection of n BlockchainArrangements representing a single Policy""" + + def __init__(self): + self._arrangements = list() diff --git a/tests/conftest.py b/tests/conftest.py index 16ac5fab9..8849fa42e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,11 +2,10 @@ import pytest from nkms_eth.agents import NuCypherKMSTokenAgent, MinerAgent from nkms_eth.blockchain import TheBlockchain -from nkms_eth.deployers import NuCypherKMSTokenDeployer -from tests.utilities import TesterBlockchain, MockMinerEscrowDeployer +from tests.utilities import TesterBlockchain, MockNuCypherKMSTokenDeployer, MockMinerEscrowDeployer -@pytest.fixture(scope='function') +@pytest.fixture() def testerchain(): chain = TesterBlockchain() yield chain @@ -14,7 +13,7 @@ def testerchain(): TheBlockchain._TheBlockchain__instance = None -@pytest.fixture(scope='function') +@pytest.fixture() def mock_token_deployer(testerchain): token_deployer = MockNuCypherKMSTokenDeployer(blockchain=testerchain) token_deployer.arm() @@ -22,12 +21,6 @@ def mock_token_deployer(testerchain): yield token_deployer -@pytest.fixture(scope='function') -def token_agent(testerchain): - token = NuCypherKMSTokenAgent(blockchain=testerchain) - yield token - - @pytest.fixture(scope='function') def mock_miner_escrow_deployer(token_agent): escrow = MockMinerEscrowDeployer(token_agent) @@ -38,8 +31,13 @@ def mock_miner_escrow_deployer(token_agent): # Unused args preserve fixture dependency order # +@pytest.fixture() +def token_agent(testerchain, mock_token_deployer): + token = NuCypherKMSTokenAgent(blockchain=testerchain) + yield token -@pytest.fixture(scope='function') + +@pytest.fixture() def miner_agent(token_agent, mock_token_deployer, mock_miner_escrow_deployer): miner_agent = MinerAgent(token_agent) - yield miner_agent \ No newline at end of file + yield miner_agent diff --git a/tests/entities/test_actors.py b/tests/entities/test_actors.py index ab94fc3b4..0f881857e 100644 --- a/tests/entities/test_actors.py +++ b/tests/entities/test_actors.py @@ -18,32 +18,34 @@ def test_deposit(testerchain, mock_token_deployer, token_agent, miner_agent): miner.lock(amount=1000*M, locktime=100) -class MockNucypherMinerConfig(object): - pass +def test_mine_withdraw(testerchain, mock_token_deployer, token_agent, miner_agent, mock_miner_escrow_deployer): + """ + - Airdrop tokens to everyone + - Create an Ursula (Miner) + - Ursula locks tokens + - Spawn additional miners + - Wait + - Ursula mints new tokens + """ - -def test_mine_withdraw(testerchain, mock_token_deployer, token_agent, miner_agent): mock_token_deployer._global_airdrop(amount=10000) - ursula_address = testerchain._chain.web3.eth.accounts[1] - miner = Miner(miner_agent=miner_agent, address=ursula_address) + _origin, ursula_address, *everyone_else = testerchain._chain.web3.eth.accounts - ursula = miner - initial_balance = token_agent.balance(address=ursula.address) + ursula = Miner(miner_agent=miner_agent, address=ursula_address) + initial_balance = ursula.token_balance() - # Create a random set of miners (we have 9 in total) - for address in testerchain._chain.web3.eth.accounts[1:]: - miner = Miner(miner_agent=miner_agent, address=address) - amount = (10+random.randrange(9000)) * M - miner.lock(amount=amount, locktime=1) + amount = (10 + random.randrange(9000)) * M + ursula.lock(amount=amount, locktime=1) - testerchain.wait_time(MockNuCypherMinerConfig._hours_per_period*2) + spawn_miners(miner_agent=miner_agent, addresses=everyone_else, locktime=1, m=M) + testerchain.wait_time(wait_hours=miner_agent._deployer._hours_per_period*2) ursula.mint() ursula.withdraw(entire_balance=True) - final_balance = token_agent.balance(ursula.address) + final_balance = token_agent.balance(ursula.address) assert final_balance > initial_balance diff --git a/tests/utilities.py b/tests/utilities.py index 1b4159934..b837f1d47 100644 --- a/tests/utilities.py +++ b/tests/utilities.py @@ -21,7 +21,7 @@ class MockNuCypherKMSTokenDeployer(NuCypherKMSTokenDeployer): def txs(): for address in addresses: - yield self._contract.transact({'from': self.origin}).transfer(address, amount * (10 ** 6)) + yield self._contract.transact({'from': self._creator}).transfer(address, amount * (10 ** 6)) for tx in txs(): self._blockchain._chain.wait.for_receipt(tx, timeout=10)