Пример #1
0
 def __init__(self):
     keystore_dir = self.get_keystore_path()
     self.account_utils = AccountUtils(keystore_dir=keystore_dir)
     self._pyetheroll = None
     # per address cached merged logs, used to compare with next pulls
     self.merged_logs = {}
     self.last_roll_activity = None
Пример #2
0
 def __init__(self, keystore_dir=None, chain_id=ChainID.MAINNET):
     if keystore_dir is None:
         keystore_dir = PyWalib.get_default_keystore_path()
     self.keystore_dir = keystore_dir
     self.account_utils = AccountUtils(keystore_dir=self.keystore_dir)
     self.chain_id = chain_id
     self.provider = HTTPProviderFactory.create(self.chain_id)
     self.web3 = Web3(self.provider)
Пример #3
0
 def test_get_or_create(self):
     """
     Checks if the singleton is handled properly.
     It should only be different when changing keystore_dir.
     """
     account_utils = AccountUtils.get_or_create(self.keystore_dir)
     assert account_utils == AccountUtils.get_or_create(self.keystore_dir)
     with TemporaryDirectory() as keystore_dir:
         assert account_utils != AccountUtils.get_or_create(keystore_dir)
         assert AccountUtils.get_or_create(keystore_dir) == \
             AccountUtils.get_or_create(keystore_dir)
Пример #4
0
 def test_pyethapp(self):
     from pyethapp.accounts import Account
     from ethereum_utils import AccountUtils
     AccountUtils.patch_ethereum_tools_keys()
     password = "******"
     uuid = None
     account = Account.new(password, uuid=uuid)
     # restore iterations
     address = account.address.hex()
     self.assertIsNotNone(account)
     self.assertIsNotNone(address)
Пример #5
0
 def __init__(self, osc_server_port=None):
     """
     Set `osc_server_port` to enable UI synchronization with service.
     """
     keystore_dir = self.get_keystore_path()
     self.account_utils = AccountUtils(keystore_dir=keystore_dir)
     self._pyetheroll = None
     # per address cached merged logs, used to compare with next pulls
     self.merged_logs = {}
     self.last_roll_activity = None
     self.osc_app_client = None
     if osc_server_port is not None:
         self.osc_app_client = OscAppClient('localhost', osc_server_port)
Пример #6
0
 def account_utils(self):
     """
     Gets or creates the AccountUtils object so it loads lazily.
     """
     from ethereum_utils import AccountUtils
     keystore_dir = Settings.get_keystore_path()
     return AccountUtils.get_or_create(keystore_dir)
Пример #7
0
 def account_utils(self):
     """
     Gets or creates the AccountUtils object so it loads lazily.
     """
     from ethereum_utils import AccountUtils
     if self._account_utils is None:
         keystore_dir = self.get_keystore_path()
         self._account_utils = AccountUtils(keystore_dir=keystore_dir)
     return self._account_utils
Пример #8
0
 def test_delete_account(self):
     """
     Creates a new account and delete it.
     Then verify we can load the account from the backup/trash location.
     """
     password = PASSWORD
     account = self.account_utils.new_account(password, iterations=1)
     address = account.address
     self.assertEqual(len(self.account_utils.get_account_list()), 1)
     # deletes the account and verifies it's not loaded anymore
     self.account_utils.delete_account(account)
     self.assertEqual(len(self.account_utils.get_account_list()), 0)
     # even recreating the AccountUtils object
     self.account_utils = AccountUtils(self.keystore_dir)
     self.assertEqual(len(self.account_utils.get_account_list()), 0)
     # tries to reload it from the backup/trash location
     deleted_keystore_dir = AccountUtils.deleted_account_dir(
         self.keystore_dir)
     self.account_utils = AccountUtils(deleted_keystore_dir)
     self.assertEqual(len(self.account_utils.get_account_list()), 1)
     self.assertEqual(self.account_utils.get_account_list()[0].address,
                      address)
Пример #9
0
 def test_deleted_account_dir(self):
     """
     The deleted_account_dir() helper method should be working
     with and without trailing slash.
     """
     expected_deleted_keystore_dir = '/tmp/keystore-deleted'
     keystore_dirs = [
         # without trailing slash
         '/tmp/keystore',
         # with one trailing slash
         '/tmp/keystore/',
         # with two trailing slashes
         '/tmp/keystore//',
     ]
     for keystore_dir in keystore_dirs:
         self.assertEqual(AccountUtils.deleted_account_dir(keystore_dir),
                          expected_deleted_keystore_dir)
Пример #10
0
 def test_get_account_list_no_dir(self):
     """
     The keystore directory should be created if it doesn't exist, refs:
     https://github.com/AndreMiras/PyWallet/issues/133
     """
     # nominal case when the directory already exists
     with TemporaryDirectory() as keystore_dir:
         self.assertTrue(os.path.isdir(keystore_dir))
         account_utils = AccountUtils(keystore_dir)
         self.assertEqual(account_utils.get_account_list(), [])
     # when the directory doesn't exist it should also be created
     self.assertFalse(os.path.isdir(keystore_dir))
     account_utils = AccountUtils(keystore_dir)
     self.assertTrue(os.path.isdir(keystore_dir))
     self.assertEqual(account_utils.get_account_list(), [])
     shutil.rmtree(keystore_dir, ignore_errors=True)
Пример #11
0
 def player_roll_dice(self,
                      bet_size_ether,
                      chances,
                      wallet_path,
                      wallet_password,
                      gas_price_gwei=DEFAULT_GAS_PRICE_GWEI):
     """
     Signs and broadcasts `playerRollDice` transaction.
     Returns transaction hash.
     """
     roll_under = chances
     # `w3.toWei` one has some issues on Android, see:
     # https://github.com/AndreMiras/EtherollApp/issues/77
     # value_wei = w3.toWei(bet_size_ether, 'ether')
     value_wei = int(bet_size_ether * 1e18)
     gas = 310000
     gas_price = w3.toWei(gas_price_gwei, 'gwei')
     # since Account.load is hanging while decrypting the password
     # we set password to None and use `w3.eth.account.decrypt` instead
     account = Account.load(wallet_path, password=None)
     from_address_normalized = checksum_encode(account.address)
     nonce = self.web3.eth.getTransactionCount(from_address_normalized)
     transaction = {
         'chainId': self.chain_id.value,
         'gas': gas,
         'gasPrice': gas_price,
         'nonce': nonce,
         'value': value_wei,
     }
     transaction = self.contract.functions.playerRollDice(
         roll_under).buildTransaction(transaction)
     private_key = AccountUtils.get_private_key(wallet_path,
                                                wallet_password)
     signed_tx = self.web3.eth.account.signTransaction(
         transaction, private_key)
     tx_hash = self.web3.eth.sendRawTransaction(signed_tx.rawTransaction)
     return tx_hash
Пример #12
0
 def test_delete_account_already_exists(self):
     """
     If the destination (backup/trash) directory where the account is moved
     already exists, it should be handled gracefully.
     This could happens if the account gets deleted, then reimported and
     deleted again, refs:
     https://github.com/AndreMiras/PyWallet/issues/88
     """
     password = PASSWORD
     account = self.account_utils.new_account(password, iterations=1)
     # creates a file in the backup/trash folder that would conflict
     # with the deleted account
     deleted_keystore_dir = AccountUtils.deleted_account_dir(
         self.keystore_dir)
     os.makedirs(deleted_keystore_dir)
     account_filename = os.path.basename(account.path)
     deleted_account_path = os.path.join(deleted_keystore_dir,
                                         account_filename)
     # create that file
     open(deleted_account_path, 'a').close()
     # then deletes the account and verifies it worked
     self.assertEqual(len(self.account_utils.get_account_list()), 1)
     self.account_utils.delete_account(account)
     self.assertEqual(len(self.account_utils.get_account_list()), 0)
Пример #13
0
class TestAccountUtils(unittest.TestCase):
    def setUp(self):
        self.keystore_dir = mkdtemp()
        self.account_utils = AccountUtils(self.keystore_dir)

    def tearDown(self):
        shutil.rmtree(self.keystore_dir, ignore_errors=True)

    def test_new_account(self):
        """
        Simple account creation test case.
        1) verifies the current account list is empty
        2) creates a new account and verify we can retrieve it
        3) tries to unlock the account
        """
        # 1) verifies the current account list is empty
        self.assertEqual(self.account_utils.get_account_list(), [])
        # 2) creates a new account and verify we can retrieve it
        password = PASSWORD
        account = self.account_utils.new_account(password, iterations=1)
        self.assertEqual(len(self.account_utils.get_account_list()), 1)
        self.assertEqual(account, self.account_utils.get_account_list()[0])
        # 3) tries to unlock the account
        # it's unlocked by default after creation
        self.assertFalse(account.locked)
        # let's lock it and unlock it back
        account.lock()
        self.assertTrue(account.locked)
        account.unlock(password)
        self.assertFalse(account.locked)

    def test_get_account_list(self):
        """
        Makes sure get_account_list() loads properly accounts from file system.
        """
        password = PASSWORD
        self.assertEqual(self.account_utils.get_account_list(), [])
        account = self.account_utils.new_account(password, iterations=1)
        self.assertEqual(len(self.account_utils.get_account_list()), 1)
        account = self.account_utils.get_account_list()[0]
        self.assertIsNotNone(account.path)
        # removes the cache copy and checks again if it gets loaded
        self.account_utils._accounts = None
        self.assertEqual(len(self.account_utils.get_account_list()), 1)
        account = self.account_utils.get_account_list()[0]
        self.assertIsNotNone(account.path)

    def test_get_account_list_error(self):
        """
        get_account_list() should not cache empty account on PermissionError.
        """
        # creates a temporary account that we'll try to list
        password = PASSWORD
        account = Account.new(password, uuid=None, iterations=1)
        account.path = os.path.join(self.keystore_dir, account.address.hex())
        with open(account.path, 'w') as f:
            f.write(account.dump())
        # `listdir()` can raise a `PermissionError`
        with mock.patch('os.listdir') as mock_listdir:
            mock_listdir.side_effect = PermissionError
            with self.assertRaises(PermissionError):
                self.account_utils.get_account_list()
        # the empty account list should not be catched and loading it again
        # should show the existing account on file system
        self.assertEqual(len(self.account_utils.get_account_list()), 1)
        self.assertEqual(self.account_utils.get_account_list()[0].address,
                         account.address)

    def test_get_account_list_no_dir(self):
        """
        The keystore directory should be created if it doesn't exist, refs:
        https://github.com/AndreMiras/PyWallet/issues/133
        """
        # nominal case when the directory already exists
        with TemporaryDirectory() as keystore_dir:
            self.assertTrue(os.path.isdir(keystore_dir))
            account_utils = AccountUtils(keystore_dir)
            self.assertEqual(account_utils.get_account_list(), [])
        # when the directory doesn't exist it should also be created
        self.assertFalse(os.path.isdir(keystore_dir))
        account_utils = AccountUtils(keystore_dir)
        self.assertTrue(os.path.isdir(keystore_dir))
        self.assertEqual(account_utils.get_account_list(), [])
        shutil.rmtree(keystore_dir, ignore_errors=True)

    def test_deleted_account_dir(self):
        """
        The deleted_account_dir() helper method should be working
        with and without trailing slash.
        """
        expected_deleted_keystore_dir = '/tmp/keystore-deleted'
        keystore_dirs = [
            # without trailing slash
            '/tmp/keystore',
            # with one trailing slash
            '/tmp/keystore/',
            # with two trailing slashes
            '/tmp/keystore//',
        ]
        for keystore_dir in keystore_dirs:
            self.assertEqual(AccountUtils.deleted_account_dir(keystore_dir),
                             expected_deleted_keystore_dir)

    def test_delete_account(self):
        """
        Creates a new account and delete it.
        Then verify we can load the account from the backup/trash location.
        """
        password = PASSWORD
        account = self.account_utils.new_account(password, iterations=1)
        address = account.address
        self.assertEqual(len(self.account_utils.get_account_list()), 1)
        # deletes the account and verifies it's not loaded anymore
        self.account_utils.delete_account(account)
        self.assertEqual(len(self.account_utils.get_account_list()), 0)
        # even recreating the AccountUtils object
        self.account_utils = AccountUtils(self.keystore_dir)
        self.assertEqual(len(self.account_utils.get_account_list()), 0)
        # tries to reload it from the backup/trash location
        deleted_keystore_dir = AccountUtils.deleted_account_dir(
            self.keystore_dir)
        self.account_utils = AccountUtils(deleted_keystore_dir)
        self.assertEqual(len(self.account_utils.get_account_list()), 1)
        self.assertEqual(self.account_utils.get_account_list()[0].address,
                         address)

    def test_delete_account_already_exists(self):
        """
        If the destination (backup/trash) directory where the account is moved
        already exists, it should be handled gracefully.
        This could happens if the account gets deleted, then reimported and
        deleted again, refs:
        https://github.com/AndreMiras/PyWallet/issues/88
        """
        password = PASSWORD
        account = self.account_utils.new_account(password, iterations=1)
        # creates a file in the backup/trash folder that would conflict
        # with the deleted account
        deleted_keystore_dir = AccountUtils.deleted_account_dir(
            self.keystore_dir)
        os.makedirs(deleted_keystore_dir)
        account_filename = os.path.basename(account.path)
        deleted_account_path = os.path.join(deleted_keystore_dir,
                                            account_filename)
        # create that file
        open(deleted_account_path, 'a').close()
        # then deletes the account and verifies it worked
        self.assertEqual(len(self.account_utils.get_account_list()), 1)
        self.account_utils.delete_account(account)
        self.assertEqual(len(self.account_utils.get_account_list()), 0)
Пример #14
0
class MonitorRollsService():

    def __init__(self):
        keystore_dir = self.get_keystore_path()
        self.account_utils = AccountUtils(keystore_dir=keystore_dir)
        self._pyetheroll = None
        # per address cached merged logs, used to compare with next pulls
        self.merged_logs = {}
        self.last_roll_activity = None

    def run(self):
        """
        Blocking pull loop call.
        Service will stop after a period of time with no roll activity.
        """
        self.last_roll_activity = time()
        elapsed = (time() - self.last_roll_activity)
        while elapsed < NO_ROLL_ACTIVITY_PERDIOD_SECONDS:
            self.pull_accounts_rolls()
            sleep(PULL_FREQUENCY_SECONDS)
            elapsed = (time() - self.last_roll_activity)
        # service decided to die naturally after no roll activity
        self.set_auto_restart_service(False)

    @staticmethod
    def set_auto_restart_service(restart=True):
        """
        Makes sure the service restarts automatically on Android when killed.
        """
        if platform != 'android':
            return
        from jnius import autoclass
        PythonService = autoclass('org.kivy.android.PythonService')
        PythonService.mService.setAutoRestartService(restart)

    @property
    def pyetheroll(self):
        """
        Gets or creates the Etheroll object.
        Also recreates the object if the chain_id changed.
        """
        chain_id = self.get_stored_network()
        if self._pyetheroll is None or self._pyetheroll.chain_id != chain_id:
            self._pyetheroll = Etheroll(chain_id)
        return self._pyetheroll

    @staticmethod
    def user_data_dir():
        """
        Fakes kivy.app.App().user_data_dir behavior.
        On Android, `/sdcard/<app_name>` is returned.
        """
        # TODO: hardcoded
        app_name = 'etheroll'
        data_dir = os.path.join('/sdcard', app_name)
        data_dir = os.path.expanduser(data_dir)
        if not os.path.exists(data_dir):
            os.mkdir(data_dir)
        return data_dir

    # TODO: merge at least "store.json" const with src/etheroll/store.py
    @classmethod
    def get_store_path(cls):
        """
        Returns the full user store path.
        """
        user_data_dir = cls.user_data_dir()
        store_path = os.path.join(user_data_dir, 'store.json')
        return store_path

    @classmethod
    def get_store(cls):
        """
        Returns user Store object.
        """
        store_path = cls.get_store_path()
        store = JsonStore(store_path)
        return store

    # TODO: refactore and share the one from settings or somewhere
    @classmethod
    def get_stored_network(cls):
        """
        Retrieves last stored network value, defaults to Mainnet.
        """
        store = cls.get_store()
        try:
            network_dict = store['network']
        except KeyError:
            network_dict = {}
        network_name = network_dict.get(
            'value', ChainID.MAINNET.name)
        network = ChainID[network_name]
        return network

    @classmethod
    def get_keystore_path(cls):
        KEYSTORE_DIR_PREFIX = cls.user_data_dir()
        keystore_dir = os.path.join(
            KEYSTORE_DIR_PREFIX, KEYSTORE_DIR_SUFFIX)
        return keystore_dir

    def pull_account_rolls(self, account):
        """
        Retrieves the merged logs for the given account and compares it with
        existing cached value. If it differs notifies and updates cache.
        """
        address = "0x" + account.address.hex()
        merged_logs = self.pyetheroll.get_merged_logs(address=address)
        try:
            merged_logs_cached = self.merged_logs[address]
        except KeyError:
            # not yet cahed, let's cache it for the first time
            self.merged_logs[address] = merged_logs
            return
        if merged_logs_cached != merged_logs:
            # since it differs, updates the cache and notifies
            self.merged_logs[address] = merged_logs
            self.do_notify(merged_logs)
            self.last_roll_activity = time()

    def pull_accounts_rolls(self):
        accounts = self.account_utils.get_account_list()
        for account in accounts:
            self.pull_account_rolls(account)

    def do_notify(self, merged_logs):
        """
        Notifies the with last roll.
        If the roll has no bet result, notifies it was just placed on the
        blockchain, but not yet resolved by the oracle.
        If it has a result, notifies it.
        """
        merged_log = merged_logs[-1]
        bet_log = merged_log['bet_log']
        bet_result = merged_log['bet_result']
        bet_value_ether = bet_log['bet_value_ether']
        roll_under = bet_log['roll_under']
        ticker = "Ticker"
        # the bet was just placed, but not resolved by the oracle
        if bet_result is None:
            title = "Bet confirmed on chain"
            message = (
                '{bet_value_ether:.{round_digits}f} ETH '
                'to roll under {roll_under}').format(**{
                    'bet_value_ether': bet_value_ether,
                    'round_digits': ROUND_DIGITS,
                    'roll_under': roll_under})
        else:
            dice_result = bet_result['dice_result']
            player_won = dice_result < roll_under
            sign = '<' if player_won else '>'
            title = 'You '
            title += 'won' if player_won else 'lost'
            message = '{0} {1} {2}'.format(
                dice_result, sign, roll_under)
        kwargs = {'title': title, 'message': message, 'ticker': ticker}
        notification.notify(**kwargs)
Пример #15
0
 def setUp(self):
     self.keystore_dir = mkdtemp()
     self.account_utils = AccountUtils(self.keystore_dir)
class TestAccountUtils(unittest.TestCase):

    def setUp(self):
        self.keystore_dir = mkdtemp()
        self.account_utils = AccountUtils(self.keystore_dir)

    def tearDown(self):
        shutil.rmtree(self.keystore_dir, ignore_errors=True)

    def test_new_account(self):
        """
        Simple account creation test case.
        1) verifies the current account list is empty
        2) creates a new account and verify we can retrieve it
        3) tries to unlock the account
        """
        # 1) verifies the current account list is empty
        account_list = self.account_utils.get_account_list()
        self.assertEqual(len(account_list), 0)
        # 2) creates a new account and verify we can retrieve it
        password = PASSWORD
        account = self.account_utils.new_account(password)
        account_list = self.account_utils.get_account_list()
        self.assertEqual(len(account_list), 1)
        self.assertEqual(account, self.account_utils.get_account_list()[0])
        # 3) tries to unlock the account
        # it's unlocked by default after creation
        self.assertFalse(account.locked)
        # let's lock it and unlock it back
        account.lock()
        self.assertTrue(account.locked)
        account.unlock(password)
        self.assertFalse(account.locked)

    def test_deleted_account_dir(self):
        """
        The deleted_account_dir() helper method should be working
        with and without trailing slash.
        """
        expected_deleted_keystore_dir = '/tmp/keystore-deleted'
        keystore_dirs = [
            # without trailing slash
            '/tmp/keystore',
            # with one trailing slash
            '/tmp/keystore/',
            # with two trailing slashes
            '/tmp/keystore//',
        ]
        for keystore_dir in keystore_dirs:
            self.assertEqual(
                AccountUtils.deleted_account_dir(keystore_dir),
                expected_deleted_keystore_dir)

    def test_delete_account(self):
        """
        Creates a new account and delete it.
        Then verify we can load the account from the backup/trash location.
        """
        password = PASSWORD
        account = self.account_utils.new_account(password)
        address = account.address
        self.assertEqual(len(self.account_utils.get_account_list()), 1)
        # deletes the account and verifies it's not loaded anymore
        self.account_utils.delete_account(account)
        self.assertEqual(len(self.account_utils.get_account_list()), 0)
        # even recreating the AccountUtils object
        self.account_utils = AccountUtils(self.keystore_dir)
        self.assertEqual(len(self.account_utils.get_account_list()), 0)
        # tries to reload it from the backup/trash location
        deleted_keystore_dir = AccountUtils.deleted_account_dir(
            self.keystore_dir)
        self.account_utils = AccountUtils(deleted_keystore_dir)
        self.assertEqual(len(self.account_utils.get_account_list()), 1)
        self.assertEqual(
            self.account_utils.get_account_list()[0].address, address)

    def test_delete_account_already_exists(self):
        """
        If the destination (backup/trash) directory where the account is moved
        already exists, it should be handled gracefully.
        This could happens if the account gets deleted, then reimported and
        deleted again, refs:
        https://github.com/AndreMiras/PyWallet/issues/88
        """
        password = PASSWORD
        account = self.account_utils.new_account(password)
        # creates a file in the backup/trash folder that would conflict
        # with the deleted account
        deleted_keystore_dir = AccountUtils.deleted_account_dir(
            self.keystore_dir)
        os.makedirs(deleted_keystore_dir)
        account_filename = os.path.basename(account.path)
        deleted_account_path = os.path.join(
            deleted_keystore_dir, account_filename)
        # create that file
        open(deleted_account_path, 'a').close()
        # then deletes the account and verifies it worked
        self.assertEqual(len(self.account_utils.get_account_list()), 1)
        self.account_utils.delete_account(account)
        self.assertEqual(len(self.account_utils.get_account_list()), 0)
Пример #17
0
class PyWalib:
    def __init__(self, keystore_dir=None, chain_id=ChainID.MAINNET):
        if keystore_dir is None:
            keystore_dir = PyWalib.get_default_keystore_path()
        self.keystore_dir = keystore_dir
        self.account_utils = AccountUtils(keystore_dir=self.keystore_dir)
        self.chain_id = chain_id
        self.provider = HTTPProviderFactory.create(self.chain_id)
        self.web3 = Web3(self.provider)

    @staticmethod
    def handle_etherscan_error(response_json):
        """
        Raises an exception on unexpected response.
        """
        status = response_json["status"]
        message = response_json["message"]
        if status != "1":
            if message == "No transactions found":
                raise NoTransactionFoundException()
            else:
                raise UnknownEtherscanException(response_json)
        assert message == "OK"

    @staticmethod
    def get_balance(address, chain_id=ChainID.MAINNET):
        """
        Retrieves the balance from etherscan.io.
        The balance is returned in ETH rounded to the second decimal.
        """
        address = to_checksum_address(address)
        url = get_etherscan_prefix(chain_id)
        url += '?module=account&action=balance'
        url += '&address=%s' % address
        url += '&tag=latest'
        if ETHERSCAN_API_KEY:
            '&apikey=%' % ETHERSCAN_API_KEY
        # TODO: handle 504 timeout, 403 and other errors from etherscan
        response = requests.get(url)
        response_json = response.json()
        PyWalib.handle_etherscan_error(response_json)
        balance_wei = int(response_json["result"])
        balance_eth = balance_wei / float(pow(10, 18))
        balance_eth = round(balance_eth, ROUND_DIGITS)
        return balance_eth

    def get_balance_web3(self, address):
        """
        The balance is returned in ETH rounded to the second decimal.
        """
        address = to_checksum_address(address)
        balance_wei = self.web3.eth.getBalance(address)
        balance_eth = balance_wei / float(pow(10, 18))
        balance_eth = round(balance_eth, ROUND_DIGITS)
        return balance_eth

    @staticmethod
    def get_transaction_history(address, chain_id=ChainID.MAINNET):
        """
        Retrieves the transaction history from etherscan.io.
        """
        address = to_checksum_address(address)
        url = get_etherscan_prefix(chain_id)
        url += '?module=account&action=txlist'
        url += '&sort=asc'
        url += '&address=%s' % address
        if ETHERSCAN_API_KEY:
            '&apikey=%' % ETHERSCAN_API_KEY
        # TODO: handle 504 timeout, 403 and other errors from etherscan
        response = requests.get(url)
        response_json = response.json()
        PyWalib.handle_etherscan_error(response_json)
        transactions = response_json['result']
        for transaction in transactions:
            value_wei = int(transaction['value'])
            value_eth = value_wei / float(pow(10, 18))
            value_eth = round(value_eth, ROUND_DIGITS)
            from_address = to_checksum_address(transaction['from'])
            to_address = transaction['to']
            # on contract creation, "to" is replaced by the "contractAddress"
            if not to_address:
                to_address = transaction['contractAddress']
            to_address = to_checksum_address(to_address)
            sent = from_address == address
            received = not sent
            extra_dict = {
                'value_eth': value_eth,
                'sent': sent,
                'received': received,
                'from_address': from_address,
                'to_address': to_address,
            }
            transaction.update({'extra_dict': extra_dict})
        # sort by timeStamp
        transactions.sort(key=lambda x: x['timeStamp'])
        return transactions

    @staticmethod
    def get_out_transaction_history(address, chain_id=ChainID.MAINNET):
        """
        Retrieves the outbound transaction history from Etherscan.
        """
        transactions = PyWalib.get_transaction_history(address, chain_id)
        out_transactions = []
        for transaction in transactions:
            if transaction['extra_dict']['sent']:
                out_transactions.append(transaction)
        return out_transactions

    # TODO: can be removed since the migration to web3
    @staticmethod
    def get_nonce(address, chain_id=ChainID.MAINNET):
        """
        Gets the nonce by counting the list of outbound transactions from
        Etherscan.
        """
        try:
            out_transactions = PyWalib.get_out_transaction_history(
                address, chain_id)
        except NoTransactionFoundException:
            out_transactions = []
        nonce = len(out_transactions)
        return nonce

    @staticmethod
    def handle_web3_exception(exception: ValueError):
        """
        Raises the appropriated typed exception on web3 ValueError exception.
        """
        error = exception.args[0]
        code = error.get("code")
        if code in [-32000, -32010]:
            raise InsufficientFundsException(error)
        else:
            raise UnknownEtherscanException(error)

    def transact(self,
                 to,
                 value=0,
                 data='',
                 sender=None,
                 gas=25000,
                 gasprice=DEFAULT_GAS_PRICE_GWEI * (10**9)):
        """
        Signs and broadcasts a transaction.
        Returns transaction hash.
        """
        address = sender or self.get_main_account().address
        from_address_normalized = to_checksum_address(address)
        nonce = self.web3.eth.getTransactionCount(from_address_normalized)
        transaction = {
            'chainId': self.chain_id.value,
            'gas': gas,
            'gasPrice': gasprice,
            'nonce': nonce,
            'value': value,
        }
        account = self.account_utils.get_by_address(address)
        private_key = account.privkey
        signed_tx = self.web3.eth.account.signTransaction(
            transaction, private_key)
        try:
            tx_hash = self.web3.eth.sendRawTransaction(
                signed_tx.rawTransaction)
        except ValueError as e:
            self.handle_web3_exception(e)
        return tx_hash

    @staticmethod
    def deleted_account_dir(keystore_dir):
        """
        Given a `keystore_dir`, returns the corresponding
        `deleted_keystore_dir`.
        >>> keystore_dir = '/tmp/keystore'
        >>> PyWalib.deleted_account_dir(keystore_dir)
        u'/tmp/keystore-deleted'
        >>> keystore_dir = '/tmp/keystore/'
        >>> PyWalib.deleted_account_dir(keystore_dir)
        u'/tmp/keystore-deleted'
        """
        keystore_dir = keystore_dir.rstrip('/')
        keystore_dir_name = os.path.basename(keystore_dir)
        deleted_keystore_dir_name = "%s-deleted" % (keystore_dir_name)
        deleted_keystore_dir = os.path.join(os.path.dirname(keystore_dir),
                                            deleted_keystore_dir_name)
        return deleted_keystore_dir

    # TODO: update docstring
    # TODO: update security_ratio
    def new_account(self, password, security_ratio=None):
        """
        Creates an account on the disk and returns it.
        security_ratio is a ratio of the default PBKDF2 iterations.
        Ranging from 1 to 100 means 100% of the iterations.
        """
        account = self.account_utils.new_account(password=password)
        return account

    def delete_account(self, account):
        """
        Deletes the given `account` from the `keystore_dir` directory.
        In fact, moves it to another location; another directory at the same
        level.
        """
        self.account_utils.delete_account(account)

    def update_account_password(self,
                                account,
                                new_password,
                                current_password=None):
        """
        The current_password is optional if the account is already unlocked.
        """
        self.account_utils.update_account_password(account, new_password,
                                                   current_password)

    @staticmethod
    def get_default_keystore_path():
        """
        Returns the keystore path, which is the same as the default pyethapp
        one.
        """
        keystore_dir = os.path.join(KEYSTORE_DIR_PREFIX, KEYSTORE_DIR_SUFFIX)
        return keystore_dir

    def get_account_list(self):
        """
        Returns the Account list.
        """
        accounts = self.account_utils.get_account_list()
        return accounts

    def get_main_account(self):
        """
        Returns the main Account.
        """
        account = self.get_account_list()[0]
        return account