diff --git a/nkms_eth/agents.py b/nkms_eth/agents.py index 183ca5f9a..2f3c8ae08 100644 --- a/nkms_eth/agents.py +++ b/nkms_eth/agents.py @@ -2,7 +2,6 @@ import random from typing import Set, Generator, List from nkms_eth.actors import PolicyAuthor -from nkms_eth.base import ContractAgent from nkms_eth.base import EthereumContractAgent from nkms_eth.blockchain import TheBlockchain from nkms_eth.deployers import MinerEscrowDeployer, NuCypherKMSTokenDeployer, PolicyManagerDeployer @@ -24,7 +23,7 @@ class NuCypherKMSTokenAgent(EthereumContractAgent, deployer=NuCypherKMSTokenDepl return self.call().balanceOf(address) -class MinerAgent(ContractAgent): +class MinerAgent(EthereumContractAgent, deployer=MinerEscrowDeployer): """ Wraps NuCypher's Escrow solidity smart contract, and manages a PopulusContract. @@ -33,8 +32,6 @@ class MinerAgent(ContractAgent): for a duration measured in periods. """ - __deployer = MinerEscrowDeployer - _contract_name = __deployer.contract_name class NotEnoughUrsulas(Exception): pass @@ -54,11 +51,11 @@ class MinerAgent(ContractAgent): """ Generates all miner addresses via cumulative sum on-network. """ - count = self.call().getMinerInfo(self.MinerInfoField.MINERS_LENGTH.value, self.null_addr, 0).encode('latin-1') + 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) for index in range(count): - addr = self.call().getMinerInfo(self.MinerInfoField.MINER.value, self.null_addr, index).encode('latin-1') + addr = self.call().getMinerInfo(self._deployer.MinerInfoField.MINER.value, self._deployer.null_address, index).encode('latin-1') yield self._blockchain._chain.web3.toChecksumAddress(addr) def sample(self, quantity: int=10, additional_ursulas: float=1.7, attempts: int=5, duration: int=10) -> List[str]: @@ -95,7 +92,7 @@ class MinerAgent(ContractAgent): points = [0] + sorted(system_random.randrange(n_tokens) for _ in range(n_select)) deltas = [i-j for i, j in zip(points[1:], points[:-1])] - addrs, addr, shift = set(), MinerEscrowDeployer.null_address, 0 + addrs, addr, shift = set(), MinerEscrowDeployer._null_addr, 0 for delta in deltas: addr, shift = self.call().findCumSum(addr, delta+shift, duration) addrs.add(addr) diff --git a/nkms_eth/base.py b/nkms_eth/base.py index d50c8ca27..aa44b3904 100644 --- a/nkms_eth/base.py +++ b/nkms_eth/base.py @@ -1,7 +1,5 @@ from abc import ABC, abstractmethod -from nkms_eth.blockchain import TheBlockchain - class Actor(ABC): def __init__(self, address): @@ -17,13 +15,13 @@ class Actor(ABC): class ContractDeployer(ABC): - __contract_name = None + _contract_name = NotImplemented class ContractDeploymentError(Exception): pass def __init__(self, blockchain): - self._armed = False + self.__armed = False self.__contract = None self._blockchain = blockchain @@ -39,9 +37,12 @@ class ContractDeployer(ABC): return bool(self.__contract is not None) @property + def is_armed(self) -> bool: + return bool(self.__armed is True) + @classmethod def contract_name(cls) -> str: - return cls.__contract_name + return cls._contract_name def _verify_contract_deployment(self) -> None: """Raises ContractDeploymentError if the contract has not been armed and deployed.""" @@ -76,25 +77,39 @@ class ContractDeployer(ABC): # return instance -class ContractAgent(ABC): - __deployer = None - _contract_name = None +class EthereumContractAgent(ABC): + _deployer = NotImplemented + _principal_contract_name = NotImplemented class ContractNotDeployed(Exception): pass def __init__(self, agent, *args, **kwargs): - contract = agent._blockchain._chain.provider.get_contract(agent._contract_name) - self._contract = contract - self._blockchain = agent._blockchain + if not self._blockchain: + self._blockchain = agent._blockchain + + contract = self._blockchain._chain.provider.get_contract(self._principal_contract_name) + self.__contract = contract + + def __init_subclass__(cls, deployer): + cls._deployer = deployer + cls._principal_contract_name = deployer.contract_name() def __repr__(self): class_name = self.__class__.__name__ r = "{}(blockchain={}, contract={})" - return r.format(class_name, self._blockchain, self._contract) + return r.format(class_name, self._blockchain, self.__contract) def call(self): - return self._contract.call() + return self.__contract.call() def transact(self, *args, **kwargs): - return self._contract.transact(*args, **kwargs) + return self.__contract.transact(*args, **kwargs) + + @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 16be5f437..eded3192d 100644 --- a/nkms_eth/blockchain.py +++ b/nkms_eth/blockchain.py @@ -12,13 +12,14 @@ class TheBlockchain: temp: Local private chain whos data directory is removed when the chain is shutdown. Runs via geth. """ - __network = '' + _network = '' __instance = None + _default_timeout = 60 - class AlreadyRunning(Exception): + class IsAlreadyRunning(Exception): pass - def __init__(self, populus_config: PopulusConfig=None, timeout=60): + def __init__(self, populus_config: PopulusConfig=None, timeout=None): """ Configures a populus project and connects to blockchain.network. Transaction timeouts specified measured in seconds. @@ -29,19 +30,21 @@ class TheBlockchain: # Singleton if TheBlockchain.__instance is not None: - message = 'Blockchain: is already running. Use .get() to retrieve'.format(self.__network) - raise TheBlockchain.AlreadyRunning(message) + message = 'Blockchain:{} is already running. Use .get() to retrieve'.format(self._network) + raise TheBlockchain.IsAlreadyRunning(message) TheBlockchain.__instance = self if populus_config is None: populus_config = PopulusConfig() - self._populus_config = populus_config - self._timeout = timeout self._project = populus_config.project # Opens and preserves connection to a running populus blockchain - self._chain = self._project.get_chain(self.__network).__enter__() + self._chain = self._project.get_chain(self._network).__enter__() + + if timeout is None: + timeout = TheBlockchain._default_timeout + self._timeout = timeout @classmethod def get(cls): @@ -58,8 +61,8 @@ class TheBlockchain: def __repr__(self): class_name = self.__class__.__name__ - r = "{}(network={}, timeout={})" - return r.format(class_name, self.__network, self._timeout) + r = "{}(network={})" + return r.format(class_name, self._network) def get_contract(self, name): """ diff --git a/nkms_eth/config.py b/nkms_eth/config.py index 4f13f794f..f833cd3c2 100644 --- a/nkms_eth/config.py +++ b/nkms_eth/config.py @@ -28,7 +28,7 @@ class MinerConfig: __max_allowed_locked = 10 ** 7 * TokenConfig._M __reward = TokenConfig._reward - __null_addr = '0x' + '0' * 40 + _null_addr = '0x' + '0' * 40 __mining_coeff = [ __hours_per_period, @@ -61,7 +61,7 @@ class MinerConfig: @property def null_address(self): - return self.__null_addr + return self._null_addr @property def mining_coefficient(self): diff --git a/tests/conftest.py b/tests/conftest.py index 834c47a2d..1a4b064e4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,7 +10,7 @@ def testerchain(): chain = TesterBlockchain() yield chain del chain - TesterBlockchain.__instance = None + TheBlockchain._TheBlockchain__instance = None @pytest.fixture(scope='function') diff --git a/tests/entities/test_escrow.py b/tests/entities/test_escrow.py index a17f3f49b..4b5b2a332 100644 --- a/tests/entities/test_escrow.py +++ b/tests/entities/test_escrow.py @@ -23,8 +23,8 @@ def test_create_escrow(testerchain): same_token.arm() same_token.deploy() - assert len(token._contract.address) == 42 - assert token._contract.address == same_token._contract.address + assert len(token.__contract.address) == 42 + assert token.__contract.address == same_token._contract.address with raises(NoKnownAddress): MinerAgent.get(token=token) @@ -38,8 +38,8 @@ def test_create_escrow(testerchain): same_escrow.arm() same_escrow.deploy() - assert len(escrow._contract.address) == 42 - assert escrow._contract.address == same_escrow._contract.address + assert len(escrow.__contract.address) == 42 + assert escrow.__contract.address == same_escrow._contract.address def test_get_swarm(testerchain, token, escrow): diff --git a/tests/test_populus_project.py b/tests/test_populus_project.py index 6c53acd4b..6a3c3e6f7 100644 --- a/tests/test_populus_project.py +++ b/tests/test_populus_project.py @@ -1,10 +1,11 @@ from os.path import join, dirname, abspath import nkms_eth -from nkms_eth.token import NuCypherKMSTokenAgent +from nkms_eth.agents import NuCypherKMSTokenAgent +from nkms_eth.deployers import NuCypherKMSTokenDeployer -def test_testerchain_create(testerchain): +def test_testerchain_creation(testerchain): # Ensure we are testing on the correct network... assert testerchain._network == 'tester' @@ -25,5 +26,5 @@ def test_nucypher_populus_project(testerchain): assert testerchain._project.project_dir == populus_project_dir # Ensure that solidity smart contacts are available, post-compile. - token_contract_identifier = NuCypherKMSTokenAgent._NuCypherKMSToken__contract_name + token_contract_identifier = NuCypherKMSTokenDeployer(blockchain=testerchain).contract_name() assert token_contract_identifier in testerchain._project.compiled_contract_data \ No newline at end of file diff --git a/tests/utilities.py b/tests/utilities.py index b80f17bb8..0b4a7faae 100644 --- a/tests/utilities.py +++ b/tests/utilities.py @@ -1,13 +1,30 @@ from nkms_eth.blockchain import TheBlockchain -from nkms_eth.escrow import MinerAgent +from nkms_eth.deployers import MinerEscrowDeployer, NuCypherKMSTokenDeployer class TesterBlockchain(TheBlockchain): """Transient chain""" - __network = 'tester' + _network = 'tester' -class MockMinerEscrow(MinerAgent): +class MockMinerEscrowDeployer(MinerEscrowDeployer): """Speed things up a bit""" __hours_per_period = 1 __min_release_periods = 1 + + +class MockNuCypherKMSTokenDeployer(NuCypherKMSTokenDeployer): + + def _global_airdrop(self, amount: int): + """Airdrops from creator address to all other addresses!""" + + _creator, *addresses = self._blockchain._chain.web3.eth.accounts + + def txs(): + for address in addresses: + yield self._contract.transact({'from': self.origin}).transfer(address, amount * (10 ** 6)) + + for tx in txs(): + self._blockchain._chain.wait.for_receipt(tx, timeout=10) + + return self