From 7d3c80ae2b33a7b91a4b7aa9fce9c4a4048be1e2 Mon Sep 17 00:00:00 2001 From: Kieran Prasch Date: Wed, 26 Jun 2019 17:28:16 -0700 Subject: [PATCH] Stakeholder power-tracking, and quasi-character blockchain persistence --- nucypher/blockchain/eth/actors.py | 179 +++++++++++++++++++++--------- 1 file changed, 129 insertions(+), 50 deletions(-) diff --git a/nucypher/blockchain/eth/actors.py b/nucypher/blockchain/eth/actors.py index 3b07c5a07..f438597f6 100644 --- a/nucypher/blockchain/eth/actors.py +++ b/nucypher/blockchain/eth/actors.py @@ -32,8 +32,9 @@ from constant_sorrow.constants import ( NO_FUNDING_ACCOUNT ) from eth_tester.exceptions import TransactionFailed -from eth_utils import keccak +from eth_utils import keccak, is_checksum_address from twisted.logger import Logger +from web3 import Web3 from nucypher.blockchain.economics import TokenEconomics from nucypher.blockchain.eth.agents import ( @@ -143,7 +144,9 @@ class Deployer(NucypherTokenActor): pass def __init__(self, - blockchain: BlockchainInterface, + blockchain: BlockchainDeployerInterface, + client_password: str = None, + device = NO_STAKING_DEVICE, deployer_address: str = None, client_password: str = None, bare: bool = True @@ -169,6 +172,13 @@ class Deployer(NucypherTokenActor): blockchain.transacting_power.activate() self.log = Logger("Deployment-Actor") + # TODO: Does this class want to be a Character implementing PowerUp consumption? + self.transacting_power = TransactingPower(blockchain=blockchain, + device=device, + password=client_password, + account=deployer_address) + self.transacting_power.activate() + def __repr__(self): r = '{name}({blockchain}, {deployer_address})'.format(name=self.__class__.__name__, blockchain=self.blockchain, @@ -678,42 +688,76 @@ class StakeHolder(BaseConfiguration): class NoFundingAccount(BaseConfiguration.ConfigurationError): pass + class NoStakes(BaseConfiguration.ConfigurationError): + pass + def __init__(self, blockchain: BlockchainInterface, - funding_account: str = NO_FUNDING_ACCOUNT, + funding_account: str, + funding_password: str = None, offline_mode: bool = False, sync_now: bool = True, *args, **kwargs): super().__init__(*args, **kwargs) - # Blockchain and Contract connection + self.log = Logger(f"stakeholder") self.blockchain = blockchain - self.staking_agent = StakingEscrowAgent(blockchain=blockchain) - self.token_agent = NucypherTokenAgent(blockchain=blockchain) - - # Mode self.offline_mode = offline_mode + self.eth_funding = Web3.toWei('0.1', 'ether') # TODO: How much ETH to use while funding new stakes. # Setup + if funding_account is not NO_FUNDING_ACCOUNT: + if not is_checksum_address(funding_account): + raise ValueError(f"{funding_account} is not a valid EIP-55 checksum address.") + + self.funding_power = TransactingPower(blockchain=blockchain, + account=funding_account, + password=funding_password) + self.funding_power.activate() self.__funding_account = funding_account + + self.staking_agent = StakingEscrowAgent(blockchain=blockchain) + self.token_agent = NucypherTokenAgent(blockchain=blockchain) + self.economics = TokenEconomics() + self.__accounts = list() self.__stakers = dict() + self.__transacting_powers = dict() + self.__get_accounts() if sync_now: - self.read_stakes() # Stakes + self.read_onchain_stakes() # Stakes def static_payload(self) -> dict: """Values to read/write from stakeholder JSON configuration files""" payload = dict(provider_uri=self.blockchain.provider_uri, + blockchain=self.blockchain.to_dict(), + funding_account=self.funding_account, accounts=self.__accounts, stakers=self.__serialize_stake_info()) return payload + @classmethod + def from_configuration_file(cls, filepath: str = None, **overrides) -> 'StakeHolder': + filepath = filepath or cls.default_filepath() + payload = cls._read_configuration_file(filepath=filepath) + blockchain_payload = payload.pop('blockchain') + blockchain = BlockchainInterface.from_dict(payload=blockchain_payload) + instance = cls(filepath=filepath, + blockchain=blockchain, + **payload, **overrides) + + return instance + @validate_checksum_address - def attach_transacting_power(self, checksum_address: str, password: str) -> None: - transacting_power = TransactingPower(blockchain=self.blockchain, account=checksum_address) + def attach_transacting_power(self, checksum_address: str, password: str = None) -> None: + try: + transacting_power = self.__transacting_powers[checksum_address] + except KeyError: + transacting_power = TransactingPower(blockchain=self.blockchain, account=checksum_address) + self.__transacting_powers[checksum_address] = transacting_power transacting_power.activate(password=password) # @@ -744,15 +788,20 @@ class StakeHolder(BaseConfiguration): @property def funding_eth(self) -> Decimal: - ethers = self.blockchain.client.w3.getBalance(self.funding_account) + ethers = self.blockchain.client.get_balance(self.funding_account) return ethers # # Staking Utilities # - def read_stakes(self) -> None: - for account in self.__accounts: + def read_onchain_stakes(self, account: str = None) -> None: + if account: + accounts = [account] + else: + accounts = self.__accounts + + for account in accounts: stakes = list(self.staking_agent.get_all_stakes(staker_address=account)) if stakes: staker = Staker(is_me=True, checksum_address=account, blockchain=self.blockchain) @@ -774,6 +823,15 @@ class StakeHolder(BaseConfiguration): payload.extend(staker.stakes) return payload + @property + def account_balances(self) -> dict: + balances = dict() + for account in self.__accounts: + funds = {'ETH': self.blockchain.client.get_balance(account), + 'NU': self.token_agent.get_balance(account)} + balances.update({account: funds}) + return balances + @property def staker_balances(self) -> dict: balances = dict() @@ -796,60 +854,53 @@ class StakeHolder(BaseConfiguration): return payload def get_active_staker(self, address: str) -> Staker: - self.read_stakes() - try: - staker = self.__stakers[address] - except KeyError: - raise self.ConfigurationError(f"{address} does not have any stakes.") - return staker + self.read_onchain_stakes(account=address) + for staker in self.__stakers: + if staker.checksum_address == address: + return staker + else: + raise self.NoStakes(f"{address} does not have any stakes.") def __create_staker(self, password: str = None) -> Staker: + """Create a new account and return it as a Staker instance.""" if self.device: - # TODO: Implement TrustedDevice - raise NotImplementedError + # TODO: More formal check for wallet here? + # With devices, the last account is is always an unsed one. + new_account = self.blockchain.client.accounts[-1] else: if not password: raise self.ConfigurationError("No password supplied to create new staking account.") new_account = self.blockchain.client.new_account(password=password) self.__accounts.append(new_account) - staker = Staker(is_me=True, checksum_address=new_account) + staker = Staker(blockchain=self.blockchain, is_me=True, checksum_address=new_account) return staker - def create_worker_configuration(self, staker_address: str, **configuration) -> str: + def create_worker_configuration(self, worker_address: str, password: str, **configuration) -> str: """Generates a worker JSON configuration file for a given staking address.""" from nucypher.config.characters import UrsulaConfiguration - staker = self.get_active_staker(address=staker_address) - ursula_configuration = UrsulaConfiguration.generate(checksum_address=staker.checksum_address, + worker_configuration = UrsulaConfiguration.generate(worker_address=worker_address, + password=password, config_root=self.config_root, federated_only=False, + provider_uri=self.blockchain.provider_uri, **configuration) - filepath = ursula_configuration.to_configuration_file() - return filepath + return worker_configuration # # Actions # - def set_worker(self, staker_address: str, worker_address: str): - self.attach_transacting_power(checksum_address=staker_address) + def set_worker(self, staker_address: str, worker_address: str, password: str = None): + self.attach_transacting_power(checksum_address=staker_address, password=password) staker = self.get_active_staker(address=staker_address) result = self.staking_agent.set_worker(staker_address=staker.checksum_address, worker_address=worker_address) + + self.to_configuration_file(override=True) return result - def initialize_stake(self, - amount: NU, - duration: int, - password: str = None, - staker_address: str = None, - ) -> Stake: - - if staker_address: - staker = self.get_active_staker(address=staker_address) - else: - # Password optional if using device API - staker = self.__create_staker(password=password) - + def __prepare_new_staker(self, staker: Staker, amount: NU): + """Creates a new ethereum account, then transfers it ETH and NU for staking.""" if self.funding_tokens < amount: delta = amount - staker.token_balance raise self.ConfigurationError(f"{self.funding_account} Insufficient NU (need {delta} more).") @@ -857,19 +908,43 @@ class StakeHolder(BaseConfiguration): raise self.ConfigurationError(f"{self.funding_account} has no ETH") # Transfer NU - self.attach_transacting_power(checksum_address=self.funding_account) - _result = self.token_agent.transfer(amount=int(amount), + self.funding_power.activate() + + # Send new staker account ETH + tx = {'to': staker.checksum_address, + 'from': self.funding_account, + 'value': self.eth_funding} + _ether_transfer_receipt = self.blockchain.client.send_transaction(transaction=tx) + + # Send new staker account NU + _result = self.token_agent.transfer(amount=amount.to_nunits(), sender_address=self.funding_account, target_address=staker.checksum_address) - # Initialize Stake - self.attach_transacting_power(checksum_address=staker.checksum_address) - new_stake = staker.initialize_stake(amount=amount, lock_periods=duration) + def initialize_stake(self, + amount: NU, + duration: int, + checksum_address: str = None, + password: str = None, + fund_now: bool = True + ) -> Stake: + if password and not checksum_address: + staker = self.__create_staker(password=password) + else: + staker = Staker(is_me=True, checksum_address=checksum_address, blockchain=self.blockchain) + + if fund_now: + self.__prepare_new_staker(staker=staker, amount=amount) + + self.attach_transacting_power(checksum_address=staker.checksum_address, password=password) + new_stake = staker.initialize_stake(amount=amount, lock_periods=duration) + self.to_configuration_file(override=True) return new_stake def divide_stake(self, address: str, + password: str, index: int, value: NU, duration: int): @@ -878,10 +953,11 @@ class StakeHolder(BaseConfiguration): if not staker.is_staking: raise Stake.StakingError(f"{staker.checksum_address} has no published stakes.") - self.attach_transacting_power(checksum_address=staker.checksum_address) + self.attach_transacting_power(checksum_address=staker.checksum_address, password=password) result = staker.divide_stake(stake_index=index, additional_periods=duration, target_value=value) + self.to_configuration_file(override=True) return result def sweep(self) -> dict: @@ -891,10 +967,12 @@ class StakeHolder(BaseConfiguration): self.attach_transacting_power(checksum_address=staker.checksum_address) receipt = self.collect_rewards(staker_address=staker.checksum_address) receipts[staker.checksum_address] = receipt + self.to_configuration_file(override=True) return receipts def collect_rewards(self, staker_address: str, + password: str, staking: bool = True, policy: bool = True) -> Dict[str, dict]: @@ -902,11 +980,12 @@ class StakeHolder(BaseConfiguration): raise ValueError("Either staking or policy must be True in order to collect rewards") staker = self.get_active_staker(address=staker_address) - self.attach_transacting_power(checksum_address=staker.checksum_address) + self.attach_transacting_power(checksum_address=staker.checksum_address, password=password) receipts = dict() if staking: receipts['staking_reward'] = staker.collect_staking_reward() if policy: receipts['policy_reward'] = staker.collect_policy_reward(collector_address=self.funding_account) + self.to_configuration_file(override=True) return receipts