diff --git a/nkms_eth/actors.py b/nkms_eth/actors.py new file mode 100644 index 000000000..51c82eb4c --- /dev/null +++ b/nkms_eth/actors.py @@ -0,0 +1,281 @@ +from collections import OrderedDict +from typing import Tuple, List + +from nkms_eth.agents import MinerAgent, PolicyAgent +from nkms_eth.base import Actor + + +class PolicyArrangement: + def __init__(self, author: 'PolicyAuthor', miner: '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 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 + + def __repr__(self): + class_name = self.__class__.__name__ + r = "{}(client={}, node={})" + r = r.format(class_name, self.author, self.miner) + return r + + def publish(self, gas_price: int) -> str: + + payload = {'from': self.author.address, + '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) + 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) + 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) + self.revoke_transaction = txhash + return txhash + + +class Miner(Actor): + """ + Practically carrying a pickaxe. + Intended for use as an Ursula mixin. + + Accepts a running blockchain, deployed token contract, and deployed escrow contract. + If the provided token and escrow contracts are not deployed, + ContractDeploymentError will be raised. + + """ + + def __init__(self, miner_agent: MinerAgent, address): + super().__init__(address) + + self.miner_agent = miner_agent + if not miner_agent._contract: + raise MinerAgent.ContractDeploymentError('Escrow contract not deployed. Arm then deploy.') + else: + miner_agent.miners.append(self) + + self._token = miner_agent._token + self._blockchain = self._token._blockchain + + self._transactions = list() + self._locked_tokens = self._update_locked_tokens() + + def _update_locked_tokens(self) -> None: + 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.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) + + return txhash + + def _send_tokens_to_escrow(self, amount, locktime) -> str: + """Send tokes to the escrow from the miner's address""" + + 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) + + return deposit_txhash + + @property + def is_staking(self): + return bool(self._locked_tokens > 0) + + def lock(self, amount: int, locktime: int) -> Tuple[str, str, str]: + """Deposit and lock tokens for mining.""" + + approve_txhash = self._approve_escrow(amount=amount) + deposit_txhash = self._send_tokens_to_escrow(amount=amount, locktime=locktime) + + 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]) + + return approve_txhash, deposit_txhash, lock_txhash + + def confirm_activity(self) -> str: + """Miner rewarded for every confirmed period""" + + txhash = self.miner_agent.transact({'from': self.address}).confirmActivity() + self._blockchain._chain.wait.for_receipt(txhash) + + self._transactions.append(txhash) + + return txhash + + def mint(self) -> str: + """Computes and transfers tokens to the miner's account""" + + txhash = self.miner_agent.transact({'from': self.address}).mint() + self._blockchain._chain.wait.for_receipt(txhash, timeout=self._blockchain._timeout) + + self._transactions.append(txhash) + + return txhash + + def collect_policy_reward(self, policy_manager): + """Collect policy reward in ETH""" + + txhash = policy_manager.transact({'from': self.address}).withdraw() + self._blockchain._chain.wait.for_receipt(txhash) + + self._transactions.append(txhash) + + return txhash + + def publish_miner_id(self, miner_id) -> str: + """Store a new Miner ID""" + + txhash = self.miner_agent.transact({'from': self.address}).setMinerId(miner_id) + self._blockchain._chain.wait.for_receipt(txhash) + + self._transactions.append(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._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) + 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._check_contract_deployment() + balance = self._token().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')) + + txhash = self.escrow.transact({'from': self.address}).withdraw(tokens_amount) + + self._blockchain._chain.wait.for_receipt(txhash, timeout=self._blockchain._timeout) + + if entire_balance and amount: + raise Exception("Specify an amount or entire balance, not both") + + if entire_balance: + txhash = self.escrow.transact({'from': self.address}).withdraw(tokens_amount) + else: + txhash = self.escrow.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): + """Alice""" + + def __init__(self, address: bytes, policy_agent: PolicyAgent): + + if policy_agent.is_deployed is False: + raise PolicyAgent.ContractDeploymentError('PolicyManager contract not deployed.') + 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: + """ + 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) + + self._arrangements[arrangement.id] = {arrangement_id: arrangement} + return arrangement + + def get_arrangement(self, arrangement_id: bytes) -> PolicyArrangement: + """Fetch a published arrangement from the blockchain""" + + blockchain_record = self.policy_agent().policies(arrangement_id) + author_address, miner_address, rate, start_block, end_block, downtime_index = blockchain_record + + duration = end_block - start_block + + miner = Miner(address=miner_address, miner_agent=self.policy_agent.escrow) + arrangement = PolicyArrangement(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""" + try: + arrangement = self._arrangements[arrangement_id] + except KeyError: + raise Exception('No such arrangement') + else: + txhash = arrangement.revoke() + return txhash + + def select_miners(self, quantity: int) -> List[str]: + miner_addresses = self.policy_agent.escrow.sample(quantity=quantity) + return miner_addresses + + def balance(self): + return self.policy_agent.token().balanceOf(self.address) + diff --git a/nkms_eth/agents.py b/nkms_eth/agents.py new file mode 100644 index 000000000..2eed67de1 --- /dev/null +++ b/nkms_eth/agents.py @@ -0,0 +1,152 @@ +from typing import Generator, List + +from nkms_eth.base import ContractAgent + + +class MinerAgent(ContractAgent): + """ + Wraps NuCypher's Escrow solidity smart contract, and manages a PopulusContract. + + In order to become a participant of the network, + a miner locks tokens by depositing to the Escrow contract address + for a duration measured in periods. + + """ + _contract_name = MinerEscrowDeployer.contract_name + + _contract_name = 'MinersEscrow' + hours_per_period = 1 # 24 Hours TODO + min_release_periods = 1 # 30 Periods + max_awarded_periods = 365 # Periods + min_allowed_locked = 10 ** 6 + max_allowed_locked = 10 ** 7 * NuCypherKMSToken.M + reward = NuCypherKMSToken.saturation - NuCypherKMSToken.premine + null_addr = '0x' + '0' * 40 + + mining_coeff = [ + hours_per_period, + 2 * 10 ** 7, + max_awarded_periods, + max_awarded_periods, + min_release_periods, + min_allowed_locked, + max_allowed_locked + ] + + class MinerInfoField(Enum): + MINERS_LENGTH = 0 + MINER = 1 + VALUE = 2 + DECIMALS = 3 + LOCKED_VALUE = 4 + RELEASE = 5 + MAX_RELEASE_PERIODS = 6 + RELEASE_RATE = 7 + CONFIRMED_PERIODS_LENGTH = 8 + CONFIRMED_PERIOD = 9 + CONFIRMED_PERIOD_LOCKED_VALUE = 10 + LAST_ACTIVE_PERIOD_F = 11 + DOWNTIME_LENGTH = 12 + DOWNTIME_START_PERIOD = 13 + DOWNTIME_END_PERIOD = 14 + MINER_IDS_LENGTH = 15 + MINER_ID = 16 + + class NotEnoughUrsulas(Exception): + pass + + def __init__(self, token: NuCypherKMSTokenAgent): + super().__init__(agent=token) + self._token = token + self.miners = list() + + def get_miner_ids(self) -> Set[str]: + """Fetch all miner IDs 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. + """ + count = self.call().getMinerInfo(self.MinerInfoField.MINERS_LENGTH.value, self.null_addr, 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') + 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]: + """ + Select n random staking Ursulas, according to their stake distribution. + The returned addresses are shuffled, so one can request more than needed and + throw away those which do not respond. + + _startIndex + v + |-------->*--------------->*---->*------------->| + | ^ + | stopIndex + | + | _delta + |---------------------------->| + | + | shift + | |----->| + + + See full diagram here: https://github.com/nucypher/kms-whitepaper/blob/master/pdf/miners-ruler.pdf + + """ + + system_random = random.SystemRandom() + n_select = round(quantity*additional_ursulas) # Select more Ursulas + n_tokens = self.__call__().getAllLockedTokens() + + if not n_tokens > 0: + raise self.NotEnoughUrsulas('There are no locked tokens.') + + for _ in range(attempts): + 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 + for delta in deltas: + addr, shift = self.__call__().findCumSum(addr, delta+shift, duration) + addrs.add(addr) + + if len(addrs) >= quantity: + return system_random.sample(addrs, quantity) + + raise self.NotEnoughUrsulas('Selection failed after {} attempts'.format(attempts)) + + +class PolicyAgent(ContractAgent): + + 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): + """ + 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 + + +class NuCypherKMSTokenAgent(ContractAgent): + + def __repr__(self): + class_name = self.__class__.__name__ + r = "{}(blockchain={}, contract={})" + return r.format(class_name, self._blockchain, self._contract) + + def registrar(self): + """Retrieve all known addresses for this contract""" + all_known_address = self._blockchain._chain.registrar.get_contract_address(NuCypherKMSTokenDeployer.contract_name()) + return all_known_address + + def check_balance(self, address: str) -> int: + """Get the balance of a token address""" + return self.__call__().balanceOf(address) diff --git a/nkms_eth/escrow.py b/nkms_eth/escrow.py deleted file mode 100644 index 2a4c9bd03..000000000 --- a/nkms_eth/escrow.py +++ /dev/null @@ -1,191 +0,0 @@ -import random -from typing import List, Tuple, Set, Generator -from enum import Enum - -from populus.contracts.contract import PopulusContract - -from nkms_eth.token import NuCypherKMSToken -from .blockchain import Blockchain - -addr = str - - -class MinerEscrow: - """ - Wraps NuCypher's Escrow solidity smart contract, and manages a PopulusContract. - - In order to become a participant of the network, - a miner locks tokens by depositing to the Escrow contract address - for a duration measured in periods. - - """ - - _contract_name = 'MinersEscrow' - hours_per_period = 1 # 24 Hours TODO - min_release_periods = 1 # 30 Periods - max_awarded_periods = 365 # Periods - min_allowed_locked = 10 ** 6 - max_allowed_locked = 10 ** 7 * NuCypherKMSToken.M - reward = NuCypherKMSToken.saturation - NuCypherKMSToken.premine - null_addr = '0x' + '0' * 40 - - mining_coeff = [ - hours_per_period, - 2 * 10 ** 7, - max_awarded_periods, - max_awarded_periods, - min_release_periods, - min_allowed_locked, - max_allowed_locked - ] - - class MinerInfoField(Enum): - MINERS_LENGTH = 0 - MINER = 1 - VALUE = 2 - DECIMALS = 3 - LOCKED_VALUE = 4 - RELEASE = 5 - MAX_RELEASE_PERIODS = 6 - RELEASE_RATE = 7 - CONFIRMED_PERIODS_LENGTH = 8 - CONFIRMED_PERIOD = 9 - CONFIRMED_PERIOD_LOCKED_VALUE = 10 - LAST_ACTIVE_PERIOD_F = 11 - DOWNTIME_LENGTH = 12 - DOWNTIME_START_PERIOD = 13 - DOWNTIME_END_PERIOD = 14 - MINER_IDS_LENGTH = 15 - MINER_ID = 16 - - class ContractDeploymentError(Exception): - pass - - class NotEnoughUrsulas(Exception): - pass - - def __init__(self, blockchain: Blockchain, token: NuCypherKMSToken, contract: PopulusContract=None): - self.blockchain = blockchain - self._contract = contract - self.token = token - self.armed = False - self.miners = list() - - def __call__(self): - """Gateway to contract function calls without state change.""" - return self._contract.call() - - def __eq__(self, other: 'MinerEscrow'): - """If two deployed escrows have the same contract address, they are equal.""" - return self._contract.address == other._contract.address - - def arm(self) -> None: - self.armed = True - - def deploy(self) -> Tuple[str, str, str]: - """ - Deploy and publish the NuCypherKMS Token contract - to the blockchain network specified in self.blockchain.network. - - The contract must be armed before it can be deployed. - Deployment can only ever be executed exactly once! - - Returns transaction hashes in a tuple: deploy, reward, and initialize. - """ - - if self.armed is False: - raise self.ContractDeploymentError('use .arm() to arm the contract, then .deploy().') - - if self._contract is not None: - class_name = self.__class__.__name__ - message = '{} contract already deployed, use .get() to retrieve it.'.format(class_name) - raise self.ContractDeploymentError(message) - - the_escrow_contract, deploy_txhash = self.blockchain._chain.provider.deploy_contract(self._contract_name, - deploy_args=[self.token._contract.address] + self.mining_coeff, - deploy_transaction={'from': self.token.creator}) - - self.blockchain._chain.wait.for_receipt(deploy_txhash, timeout=self.blockchain._timeout) - self._contract = the_escrow_contract - - reward_txhash = self.token.transact({'from': self.token.creator}).transfer(self._contract.address, self.reward) - self.blockchain._chain.wait.for_receipt(reward_txhash, timeout=self.blockchain._timeout) - - init_txhash = self._contract.transact({'from': self.token.creator}).initialize() - self.blockchain._chain.wait.for_receipt(init_txhash, timeout=self.blockchain._timeout) - - return deploy_txhash, reward_txhash, init_txhash - - @classmethod - def get(cls, blockchain, token) -> 'MinerEscrow': - """ - Returns the Escrow object, - or raises UnknownContract if the contract has not been deployed. - """ - contract = blockchain._chain.provider.get_contract(cls._contract_name) - return cls(blockchain=blockchain, token=token, contract=contract) - - def transact(self, *args, **kwargs): - if self._contract is None: - raise self.ContractDeploymentError('Contract must be deployed before executing transactions.') - return self._contract.transact(*args, **kwargs) - - def get_dht(self) -> Set[str]: - """Fetch all miner IDs 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. - """ - count = self.blockchain._chain.web3.toInt( - self.__call__().getMinerInfo(self.MinerInfoField.MINERS_LENGTH.value, self.null_addr, 0) - .encode('latin-1')) - for index in range(count): - yield self.blockchain._chain.web3.toChecksumAddress( - self.__call__().getMinerInfo(self.MinerInfoField.MINER.value, self.null_addr, index).encode('latin-1')) - - def sample(self, quantity: int=10, additional_ursulas: float=1.7, attempts: int=5, duration: int=10) -> List[addr]: - """ - Select n random staking Ursulas, according to their stake distribution. - The returned addresses are shuffled, so one can request more than needed and - throw away those which do not respond. - - _startIndex - v - |-------->*--------------->*---->*------------->| - | ^ - | stopIndex - | - | _delta - |---------------------------->| - | - | shift - | |----->| - - - See full diagram here: https://github.com/nucypher/kms-whitepaper/blob/master/pdf/miners-ruler.pdf - - """ - - system_random = random.SystemRandom() - n_select = round(quantity*additional_ursulas) # Select more Ursulas - n_tokens = self.__call__().getAllLockedTokens() - - if not n_tokens > 0: - raise self.NotEnoughUrsulas('There are no locked tokens.') - - for _ in range(attempts): - 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, index, shift = set(), self.null_addr, 0, 0 - for delta in deltas: - addr, index, shift = self.__call__().findCumSum(index, delta+shift, duration) - addrs.add(addr) - - if len(addrs) >= quantity: - return system_random.sample(addrs, quantity) - - raise self.NotEnoughUrsulas('Selection failed after {} attempts'.format(attempts)) - diff --git a/nkms_eth/miner.py b/nkms_eth/miner.py deleted file mode 100644 index 243b58c31..000000000 --- a/nkms_eth/miner.py +++ /dev/null @@ -1,165 +0,0 @@ -from typing import Tuple - -from .escrow import MinerEscrow - - -class Miner: - """ - Practically carrying a pickaxe. - Intended for use as an Ursula mixin. - - Accepts a running blockchain, deployed token contract, and deployed escrow contract. - If the provided token and escrow contracts are not deployed, - ContractDeploymentError will be raised. - - """ - - def __init__(self, escrow: MinerEscrow, address=None): - - self.escrow = escrow - if not escrow._contract: - raise MinerEscrow.ContractDeploymentError('Escrow contract not deployed. Arm then deploy.') - else: - escrow.miners.append(self) - - self._token = escrow.token - self._blockchain = self._token.blockchain - - self.address = address - self._transactions = [] - self._locked_tokens = self._update_locked_tokens() - - def __repr__(self): - class_name = self.__class__.__name__ - r = "{}(address='{}')" - r.format(class_name, self.address) - return r - - def __del__(self): - """Removes this miner from the escrow's list of miners on delete.""" - self.escrow.miners.remove(self) - - def _update_locked_tokens(self) -> None: - self._locked_tokens = self.escrow().getLockedTokens(self.address) - - def _approve_escrow(self, amount: int) -> str: - """Approve the transfer of token from the miner's address to the escrow contract.""" - - txhash = self._token.transact({'from': self.address}).approve(self.escrow._contract.address, amount) - self._blockchain._chain.wait.for_receipt(txhash, timeout=self._blockchain._timeout) - - self._transactions.append(txhash) - - return txhash - - def _send_tokens_to_escrow(self, amount, locktime) -> str: - """Send tokes to the escrow from the miner's address""" - - deposit_txhash = self.escrow.transact({'from': self.address}).deposit(amount, locktime) - self._blockchain._chain.wait.for_receipt(deposit_txhash, timeout=self._blockchain._timeout) - - self._transactions.append(deposit_txhash) - - return deposit_txhash - - def lock(self, amount: int, locktime: int) -> Tuple[str, str, str]: - """Deposit and lock tokens for mining.""" - - approve_txhash = self._approve_escrow(amount=amount) - deposit_txhash = self._send_tokens_to_escrow(amount=amount, locktime=locktime) - - lock_txhash = self.escrow.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]) - - return approve_txhash, deposit_txhash, lock_txhash - - def confirm_activity(self) -> str: - """Miner rewarded for every confirmed period""" - - txhash = self.escrow.transact({'from': self.address}).confirmActivity() - self._blockchain._chain.wait.for_receipt(txhash) - - self._transactions.append(txhash) - - return txhash - - def mint(self) -> str: - """Computes and transfers tokens to the miner's account""" - - txhash = self.escrow.transact({'from': self.address}).mint() - self._blockchain._chain.wait.for_receipt(txhash, timeout=self._blockchain._timeout) - - self._transactions.append(txhash) - - return txhash - - def collect_policy_reward(self, policy_manager): - """Collect policy reward in ETH""" - - txhash = policy_manager.transact({'from': self.address}).withdraw() - self._blockchain._chain.wait.for_receipt(txhash) - - self._transactions.append(txhash) - - return txhash - - def publish_miner_id(self, miner_id) -> str: - """Store a new Miner ID""" - - txhash = self.escrow.transact({'from': self.address}).setMinerId(miner_id) - self._blockchain._chain.wait.for_receipt(txhash) - - self._transactions.append(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._blockchain._chain.web3.toInt(count) - - # TODO change when v4 web3.py will released - miner_ids = tuple(self.escrow().getMinerInfo(self.escrow.MinerInfoField.MINER_ID.value, self.address, index) - .encode('latin-1') for index in range(count)) - - 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._check_contract_deployment() - balance = self._token().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')) - - txhash = self.escrow.transact({'from': self.address}).withdraw(tokens_amount) - - self._blockchain._chain.wait.for_receipt(txhash, timeout=self._blockchain._timeout) - - if entire_balance and amount: - raise Exception("Specify an amount or entire balance, not both") - - if entire_balance: - txhash = self.escrow.transact({'from': self.address}).withdraw(tokens_amount) - else: - txhash = self.escrow.transact({'from': self.address}).withdraw(amount) - - self._transactions.append(txhash) - self._blockchain._chain.wait.for_receipt(txhash, timeout=self._blockchain._timeout) - - return txhash