mirror of https://github.com/nucypher/nucypher.git
[KMS-ETH]- Extracts policies module, API updates, further collapse contract wrapper logic up inheritance tree. Enhances docstrings.
parent
84f9454436
commit
e8f44e611e
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
137
nkms_eth/base.py
137
nkms_eth/base.py
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
yield miner_agent
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue