[KMS-ETH]- Actor and Contract subclasses, for client interaction and KMS integration via Mixin.

pull/195/head^2
Kieran Prasch 2018-03-07 11:22:59 -08:00
parent 1e2d277d59
commit d538f5d745
4 changed files with 433 additions and 356 deletions

281
nkms_eth/actors.py Normal file
View File

@ -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)

152
nkms_eth/agents.py Normal file
View File

@ -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)

View File

@ -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))

View File

@ -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