def test_handle_etherscan_tx_error(self): """ Checks handle_etherscan_tx_error() error handling. """ # no transaction found response_json = { 'jsonrpc': '2.0', 'id': 1, 'error': { 'message': 'Insufficient funds. ' 'The account you tried to send transaction from does not ' 'have enough funds. Required 10001500000000000000 and' 'got: 53856999715015294.', 'code': -32010, 'data': None } } with self.assertRaises(InsufficientFundsException): PyWalib.handle_etherscan_tx_error(response_json) # unknown error response_json = { 'jsonrpc': '2.0', 'id': 1, 'error': { 'message': 'Unknown error', 'code': 0, 'data': None } } with self.assertRaises(UnknownEtherscanException): PyWalib.handle_etherscan_tx_error(response_json) # no error response_json = {'jsonrpc': '2.0', 'id': 1} self.assertEqual( PyWalib.handle_etherscan_tx_error(response_json), None)
def test_get_nonce(self): """ Checks get_nonce() returns the next nonce, i.e. transaction count. """ address = ADDRESS nonce = PyWalib.get_nonce(address) transactions = PyWalib.get_out_transaction_history(address) last_transaction = transactions[-1] last_nonce = int(last_transaction['nonce']) self.assertEqual(nonce, last_nonce + 1)
def test_get_nonce_no_out_transaction(self): """ Makes sure get_nonce() doesn't crash on no out transaction, but just returns 0. """ # the VOID_ADDRESS has a lot of in transactions, # but no out ones, so the nonce should be 0 address = VOID_ADDRESS # truncated for readability transactions = [{ 'blockHash': ('0x7e5a9336dd82efff0bfe8c25ccb0e8c' 'f44b4c6f781b25b3fc3578f004f60b872'), 'from': '0x22f2dcff5ad78c3eb6850b5cb951127b659522e6', 'timeStamp': '1438922865', 'to': '0x0000000000000000000000000000000000000000', 'value': '0' }] with patch_requests_get() as m_get: m_get.return_value.status_code = http.HTTPStatus.OK m_get.return_value.json.return_value = { 'status': '1', 'message': 'OK', 'result': transactions, } nonce = PyWalib.get_nonce(address) url = mock.ANY headers = REQUESTS_HEADERS self.assertEqual(m_get.call_args_list, [mock.call(url, headers=headers)]) self.assertEqual(nonce, 0)
def test_get_balance(self): """ Checks get_balance() returns a float. """ address = ADDRESS balance_eth = PyWalib.get_balance(address) self.assertTrue(type(balance_eth), float)
def test_get_transaction_history(self): """ Checks get_transaction_history() works as expected. """ address = ADDRESS m_transactions = M_TRANSACTIONS with patch_requests_get() as m_get: m_get.return_value.status_code = http.HTTPStatus.OK m_get.return_value.json.return_value = { 'status': '1', 'message': 'OK', 'result': m_transactions, } transactions = PyWalib.get_transaction_history(address) url = mock.ANY headers = REQUESTS_HEADERS self.assertEqual(m_get.call_args_list, [mock.call(url, headers=headers)]) self.helper_test_get_history(transactions) # value is stored in Wei self.assertEqual(transactions[1]['value'], '10000000000000000') # but converted to Ether is also accessible self.assertEqual(transactions[1]['extra_dict']['value_eth'], 0.01) # history contains all send or received transactions self.assertEqual(transactions[1]['extra_dict']['sent'], True) self.assertEqual(transactions[1]['extra_dict']['received'], False) self.assertEqual(transactions[2]['extra_dict']['sent'], True) self.assertEqual(transactions[2]['extra_dict']['received'], False)
def fetch_balance(self): """ Fetches the new balance & sets accounts_balance property. """ if self.current_account is None: return address = '0x' + self.current_account.address.hex() chain_id = Settings.get_stored_network() try: balance = PyWalib.get_balance(address, chain_id) except ConnectionError: Dialog.on_balance_connection_error() Logger.warning('ConnectionError', exc_info=True) return except ValueError: # most likely the JSON object could not be decoded, refs #91 # currently logged as an error, because we want more insight # in order to eventually handle it more specifically Dialog.on_balance_value_error() Logger.error('ValueError', exc_info=True) return except UnknownEtherscanException: # also handles uknown errors, refs #112 Dialog.on_balance_unknown_error() Logger.error('UnknownEtherscanException', exc_info=True) return # triggers accounts_balance observers update self.accounts_balance[address] = balance
def test_get_out_transaction_history(self): """ Checks get_out_transaction_history() works as expected. """ address = ADDRESS m_transactions = M_TRANSACTIONS with patch_requests_get() as m_get: m_get.return_value.status_code = http.HTTPStatus.OK m_get.return_value.json.return_value = { 'status': '1', 'message': 'OK', 'result': m_transactions, } transactions = PyWalib.get_out_transaction_history(address) url = mock.ANY headers = REQUESTS_HEADERS self.assertEqual(m_get.call_args_list, [mock.call(url, headers=headers)]) self.helper_test_get_history(transactions) # 4 transactions including 3 out transactions self.assertEquals(len(m_transactions), 4) self.assertEquals(len(transactions), 3) for i in range(len(transactions)): transaction = transactions[i] extra_dict = transaction['extra_dict'] # this is only sent transactions self.assertEqual(extra_dict['sent'], True) self.assertEqual(extra_dict['received'], False) # nonce should be incremented each time self.assertEqual(transaction['nonce'], str(i))
def get_keystore_path(): """ This is the Kivy default keystore path. """ keystore_path = os.environ.get('KEYSTORE_PATH') if keystore_path is None: Controller.patch_keystore_path() keystore_path = PyWalib.get_default_keystore_path() return keystore_path
def test_get_nonce_no_out_transaction(self): """ Makes sure get_nonce() doesn't crash on no out transaction, but just returns 0. """ # the VOID_ADDRESS has a lot of in transactions, # but no out ones, so the nonce should be 0 address = VOID_ADDRESS nonce = PyWalib.get_nonce(address) self.assertEqual(nonce, 0)
def test_get_nonce_no_transaction(self): """ Makes sure get_nonce() doesn't crash on no transaction, but just returns 0. """ # the newly created address has no in or out transaction history account = self.helper_new_account() address = account.address nonce = PyWalib.get_nonce(address) self.assertEqual(nonce, 0)
def test_address_hex(self): """ Checks handle_etherscan_error() error handling. """ expected_addresss = ADDRESS # no 0x prefix address_no_prefix = ADDRESS.lower().strip("0x") address = address_no_prefix normalized = PyWalib.address_hex(address) self.assertEqual(normalized, expected_addresss) # uppercase address = "0x" + address_no_prefix.upper() normalized = PyWalib.address_hex(address) self.assertEqual(normalized, expected_addresss) # prefix cannot be uppercase address = "0X" + address_no_prefix.upper() with self.assertRaises(Exception) as context: PyWalib.address_hex(address) self.assertEqual(context.exception.message, "Invalid address format: '%s'" % (address))
def pywalib(self): """ Gets or creates the PyWalib object. Also recreates the object if the keystore_path changed. """ keystore_path = Settings.get_keystore_path() chain_id = Settings.get_stored_network() if self._pywalib is None or \ self._pywalib.keystore_dir != keystore_path or \ self._pywalib.chain_id != chain_id: self._pywalib = PyWalib( keystore_dir=keystore_path, chain_id=chain_id) return self._pywalib
def test_get_default_keystore_path(self): """ Checks we the default keystore directory exists or create it. Verify the path is correct and that we have read/write access to it. """ keystore_dir = PyWalib.get_default_keystore_path() if not os.path.exists(keystore_dir): os.makedirs(keystore_dir) # checks path correctness self.assertTrue(keystore_dir.endswith(".config/pyethapp/keystore/")) # checks read/write access self.assertEqual(os.access(keystore_dir, os.R_OK), True) self.assertEqual(os.access(keystore_dir, os.W_OK), True)
def test_get_nonce(self): """ Checks get_nonce() returns the next nonce, i.e. transaction count. """ address = ADDRESS m_transactions = M_TRANSACTIONS with patch_requests_get() as m_get: m_get.return_value.status_code = http.HTTPStatus.OK m_get.return_value.json.return_value = { 'status': '1', 'message': 'OK', 'result': m_transactions, } nonce = PyWalib.get_nonce(address) transactions = PyWalib.get_out_transaction_history(address) url = mock.ANY headers = REQUESTS_HEADERS self.assertEqual(m_get.call_args_list, 2 * [mock.call(url, headers=headers)]) last_transaction = transactions[-1] last_nonce = int(last_transaction['nonce']) self.assertEqual(nonce, last_nonce + 1)
def test_handle_etherscan_error(self): """ Checks handle_etherscan_error() error handling. """ # no transaction found response_json = { 'message': 'No transactions found', 'result': [], 'status': '0' } with self.assertRaises(NoTransactionFoundException): PyWalib.handle_etherscan_error(response_json) # unknown error response_json = { 'message': 'Unknown error', 'result': [], 'status': '0' } with self.assertRaises(UnknownEtherscanException): PyWalib.handle_etherscan_error(response_json) # no error response_json = {'message': 'OK', 'result': [], 'status': '1'} self.assertEqual(PyWalib.handle_etherscan_error(response_json), None)
def test_get_out_transaction_history(self): """ Checks get_out_transaction_history() works as expected. """ address = ADDRESS transactions = PyWalib.get_out_transaction_history(address) self.helper_get_history(transactions) for i in range(len(transactions)): transaction = transactions[i] extra_dict = transaction['extra_dict'] # this is only sent transactions self.assertEqual(extra_dict['sent'], True) self.assertEqual(extra_dict['received'], False) # nonce should be incremented each time self.assertEqual(transaction['nonce'], str(i))
def test_get_transaction_history(self): """ Checks get_transaction_history() works as expected. """ address = ADDRESS transactions = PyWalib.get_transaction_history(address) self.helper_get_history(transactions) # value is stored in Wei self.assertEqual(transactions[1]['value'], '200000000000000000') # but converted to Ether is also accessible self.assertEqual(transactions[1]['extra_dict']['value_eth'], 0.2) # history contains all send or received transactions self.assertEqual(transactions[1]['extra_dict']['sent'], False) self.assertEqual(transactions[1]['extra_dict']['received'], True) self.assertEqual(transactions[2]['extra_dict']['sent'], True) self.assertEqual(transactions[2]['extra_dict']['received'], False)
def _load_history(self): account = self.current_account address = '0x' + account.address.encode("hex") try: transactions = PyWalib.get_transaction_history(address) # new transactions first transactions.reverse() except ConnectionError: Controller.on_history_connection_error() return except NoTransactionFoundException: transactions = [] list_items = [] for transaction in transactions: list_item = History.create_item_from_dict(transaction) list_items.append(list_item) self.update_history_list(list_items)
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(PyWalib.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. """ pywalib = self.pywalib account = self.helper_new_account() address = account.address self.assertEqual(len(pywalib.get_account_list()), 1) # deletes the account and verifies it's not loaded anymore pywalib.delete_account(account) self.assertEqual(len(pywalib.get_account_list()), 0) # even recreating the PyWalib object pywalib = PyWalib(self.keystore_dir) self.assertEqual(len(pywalib.get_account_list()), 0) # tries to reload it from the backup/trash location deleted_keystore_dir = PyWalib.deleted_account_dir(self.keystore_dir) pywalib = PyWalib(deleted_keystore_dir) self.assertEqual(len(pywalib.get_account_list()), 1) self.assertEqual(pywalib.get_account_list()[0].address, address)
def test_get_nonce_no_transaction(self): """ Makes sure get_nonce() doesn't crash on no transaction, but just returns 0. """ # the newly created address has no in or out transaction history account = self.helper_new_account() address = account.address with patch_requests_get() as m_get: m_get.return_value.status_code = http.HTTPStatus.OK m_get.return_value.json.return_value = { 'status': '0', 'message': 'No transactions found', 'result': [], } nonce = PyWalib.get_nonce(address) url = mock.ANY headers = REQUESTS_HEADERS self.assertEqual(m_get.call_args_list, [mock.call(url, headers=headers)]) self.assertEqual(nonce, 0)
def fetch_history(self): if self.current_account is None: return chain_id = Settings.get_stored_network() address = '0x' + self.current_account.address.hex() try: transactions = PyWalib.get_transaction_history(address, chain_id) except ConnectionError: Dialog.on_history_connection_error() Logger.warning('ConnectionError', exc_info=True) return except NoTransactionFoundException: transactions = [] except ValueError: # most likely the JSON object could not be decoded, refs #91 Dialog.on_history_value_error() # currently logged as an error, because we want more insight # in order to eventually handle it more specifically Logger.error('ValueError', exc_info=True) return # triggers accounts_history observers update self.controller.accounts_history[address] = transactions
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 """ pywalib = self.pywalib account = self.helper_new_account() # creates a file in the backup/trash folder that would conflict # with the deleted account deleted_keystore_dir = PyWalib.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(pywalib.get_account_list()), 1) pywalib.delete_account(account) self.assertEqual(len(pywalib.get_account_list()), 0)
def test_handle_web3_exception(self): """ Checks handle_web3_exception() error handling. """ # insufficient funds exception = ValueError({ 'code': -32000, 'message': 'insufficient funds for gas * price + value' }) with self.assertRaises(InsufficientFundsException) as e: PyWalib.handle_web3_exception(exception) self.assertEqual(e.exception.args[0], exception.args[0]) # unknown error code exception = ValueError({'code': 0, 'message': 'Unknown error'}) with self.assertRaises(UnknownEtherscanException) as e: PyWalib.handle_web3_exception(exception) self.assertEqual(e.exception.args[0], exception.args[0]) # no code exception = ValueError({'message': 'Unknown error'}) with self.assertRaises(UnknownEtherscanException) as e: PyWalib.handle_web3_exception(exception) self.assertEqual(e.exception.args[0], exception.args[0])
class PywalibTestCase(unittest.TestCase): """ Simple test cases, verifying pywalib works as expected. """ def setUp(self): self.keystore_dir = mkdtemp() self.pywalib = PyWalib(self.keystore_dir) def tearDown(self): shutil.rmtree(self.keystore_dir, ignore_errors=True) def helper_new_account(self, password=PASSWORD, security_ratio=1): """ Helper method for fast account creation. """ account = self.pywalib.new_account(password, security_ratio) return account 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 """ pywalib = self.pywalib # 1) verifies the current account list is empty account_list = pywalib.get_account_list() self.assertEqual(len(account_list), 0) # 2) creates a new account and verify we can retrieve it password = PASSWORD # weak account, but fast creation security_ratio = 1 account = pywalib.new_account(password, security_ratio) account_list = pywalib.get_account_list() self.assertEqual(len(account_list), 1) self.assertEqual(account, pywalib.get_main_account()) # 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 helper_test_new_account_security_ratio_ok(self, security_ratio): """ Helper method to unit test `new_account()` security_ratio parameter on happy scenarios. """ pywalib = self.pywalib password = PASSWORD with mock.patch.object(pywalib.account_utils, 'new_account') as m_new_account: self.assertIsNotNone(pywalib.new_account(password, security_ratio)) self.assertEqual(m_new_account.call_args_list, [mock.call(iterations=mock.ANY, password='******')]) def helper_test_new_account_security_ratio_error(self, security_ratio): """ Helper method to unit test `new_account()` security_ratio parameter on rainy scenarios. """ password = PASSWORD with self.assertRaises(ValueError) as ex_info: self.pywalib.new_account(password, security_ratio) self.assertEqual(ex_info.exception.args[0], 'security_ratio must be within 1 and 100') def test_new_account_security_ratio(self): """ Checks the security_ratio parameter behave as expected. Possible value are: - security_ratio == None - security_ratio >= 1 - security_ratio <= 100 """ security_ratio = None self.helper_test_new_account_security_ratio_ok(security_ratio) security_ratio = 1 self.helper_test_new_account_security_ratio_ok(security_ratio) security_ratio = 100 self.helper_test_new_account_security_ratio_ok(security_ratio) # anything else would fail security_ratio = 0 self.helper_test_new_account_security_ratio_error(security_ratio) security_ratio = 101 self.helper_test_new_account_security_ratio_error(security_ratio) def test_update_account_password(self): """ Verifies updating account password works. """ pywalib = self.pywalib current_password = "******" # weak account, but fast creation security_ratio = 1 account = pywalib.new_account(current_password, security_ratio) # first try when the account is already unlocked self.assertFalse(account.locked) new_password = "******" # on unlocked account the current_password is optional pywalib.update_account_password(account, new_password, current_password=None) # verify it worked account.lock() account.unlock(new_password) self.assertFalse(account.locked) # now try when the account is first locked account.lock() current_password = "******" with self.assertRaises(ValueError): pywalib.update_account_password(account, new_password, current_password) current_password = new_password pywalib.update_account_password(account, new_password, current_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(PyWalib.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. """ pywalib = self.pywalib account = self.helper_new_account() address = account.address self.assertEqual(len(pywalib.get_account_list()), 1) # deletes the account and verifies it's not loaded anymore pywalib.delete_account(account) self.assertEqual(len(pywalib.get_account_list()), 0) # even recreating the PyWalib object pywalib = PyWalib(self.keystore_dir) self.assertEqual(len(pywalib.get_account_list()), 0) # tries to reload it from the backup/trash location deleted_keystore_dir = PyWalib.deleted_account_dir(self.keystore_dir) pywalib = PyWalib(deleted_keystore_dir) self.assertEqual(len(pywalib.get_account_list()), 1) self.assertEqual(pywalib.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 """ pywalib = self.pywalib account = self.helper_new_account() # creates a file in the backup/trash folder that would conflict # with the deleted account deleted_keystore_dir = PyWalib.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(pywalib.get_account_list()), 1) pywalib.delete_account(account) self.assertEqual(len(pywalib.get_account_list()), 0) def test_get_balance(self): """ Checks get_balance() returns a float. """ pywalib = self.pywalib address = ADDRESS with patch_requests_get() as m_get: m_get.return_value.status_code = http.HTTPStatus.OK m_get.return_value.json.return_value = { 'status': '1', 'message': 'OK', 'result': '350003576885437676061958', } balance_eth = pywalib.get_balance(address) url = mock.ANY headers = REQUESTS_HEADERS self.assertEqual(m_get.call_args_list, [mock.call(url, headers=headers)]) self.assertEqual(balance_eth, 350003.577) def test_get_balance_web3(self): """ Checks get_balance() returns a float. """ pywalib = self.pywalib address = ADDRESS with mock.patch('web3.eth.Eth.getBalance') as m_getBalance: m_getBalance.return_value = 350003576885437676061958 balance_eth = pywalib.get_balance_web3(address) checksum_address = to_checksum_address(address) self.assertEqual(m_getBalance.call_args_list, [mock.call(checksum_address)]) self.assertTrue(type(balance_eth), float) self.assertEqual(balance_eth, 350003.577) def helper_test_get_history(self, transactions): """ Helper method to test history related methods. """ self.assertEqual(type(transactions), list) self.assertTrue(len(transactions) > 1) # ordered by timeStamp self.assertTrue( transactions[0]['timeStamp'] < transactions[1]['timeStamp']) # and a bunch of other things self.assertEqual( set(transactions[0].keys()), set([ 'nonce', 'contractAddress', 'cumulativeGasUsed', 'hash', 'blockHash', 'extra_dict', 'timeStamp', 'gas', 'value', 'blockNumber', 'to', 'confirmations', 'input', 'from', 'transactionIndex', 'isError', 'gasPrice', 'gasUsed', 'txreceipt_status', ])) def test_get_transaction_history(self): """ Checks get_transaction_history() works as expected. """ address = ADDRESS m_transactions = M_TRANSACTIONS with patch_requests_get() as m_get: m_get.return_value.status_code = http.HTTPStatus.OK m_get.return_value.json.return_value = { 'status': '1', 'message': 'OK', 'result': m_transactions, } transactions = PyWalib.get_transaction_history(address) url = mock.ANY headers = REQUESTS_HEADERS self.assertEqual(m_get.call_args_list, [mock.call(url, headers=headers)]) self.helper_test_get_history(transactions) # value is stored in Wei self.assertEqual(transactions[1]['value'], '10000000000000000') # but converted to Ether is also accessible self.assertEqual(transactions[1]['extra_dict']['value_eth'], 0.01) # history contains all send or received transactions self.assertEqual(transactions[1]['extra_dict']['sent'], True) self.assertEqual(transactions[1]['extra_dict']['received'], False) self.assertEqual(transactions[2]['extra_dict']['sent'], True) self.assertEqual(transactions[2]['extra_dict']['received'], False) def test_get_out_transaction_history(self): """ Checks get_out_transaction_history() works as expected. """ address = ADDRESS m_transactions = M_TRANSACTIONS with patch_requests_get() as m_get: m_get.return_value.status_code = http.HTTPStatus.OK m_get.return_value.json.return_value = { 'status': '1', 'message': 'OK', 'result': m_transactions, } transactions = PyWalib.get_out_transaction_history(address) url = mock.ANY headers = REQUESTS_HEADERS self.assertEqual(m_get.call_args_list, [mock.call(url, headers=headers)]) self.helper_test_get_history(transactions) # 4 transactions including 3 out transactions self.assertEquals(len(m_transactions), 4) self.assertEquals(len(transactions), 3) for i in range(len(transactions)): transaction = transactions[i] extra_dict = transaction['extra_dict'] # this is only sent transactions self.assertEqual(extra_dict['sent'], True) self.assertEqual(extra_dict['received'], False) # nonce should be incremented each time self.assertEqual(transaction['nonce'], str(i)) def test_get_nonce(self): """ Checks get_nonce() returns the next nonce, i.e. transaction count. """ address = ADDRESS m_transactions = M_TRANSACTIONS with patch_requests_get() as m_get: m_get.return_value.status_code = http.HTTPStatus.OK m_get.return_value.json.return_value = { 'status': '1', 'message': 'OK', 'result': m_transactions, } nonce = PyWalib.get_nonce(address) transactions = PyWalib.get_out_transaction_history(address) url = mock.ANY headers = REQUESTS_HEADERS self.assertEqual(m_get.call_args_list, 2 * [mock.call(url, headers=headers)]) last_transaction = transactions[-1] last_nonce = int(last_transaction['nonce']) self.assertEqual(nonce, last_nonce + 1) def test_get_nonce_no_out_transaction(self): """ Makes sure get_nonce() doesn't crash on no out transaction, but just returns 0. """ # the VOID_ADDRESS has a lot of in transactions, # but no out ones, so the nonce should be 0 address = VOID_ADDRESS # truncated for readability transactions = [{ 'blockHash': ('0x7e5a9336dd82efff0bfe8c25ccb0e8c' 'f44b4c6f781b25b3fc3578f004f60b872'), 'from': '0x22f2dcff5ad78c3eb6850b5cb951127b659522e6', 'timeStamp': '1438922865', 'to': '0x0000000000000000000000000000000000000000', 'value': '0' }] with patch_requests_get() as m_get: m_get.return_value.status_code = http.HTTPStatus.OK m_get.return_value.json.return_value = { 'status': '1', 'message': 'OK', 'result': transactions, } nonce = PyWalib.get_nonce(address) url = mock.ANY headers = REQUESTS_HEADERS self.assertEqual(m_get.call_args_list, [mock.call(url, headers=headers)]) self.assertEqual(nonce, 0) def test_get_nonce_no_transaction(self): """ Makes sure get_nonce() doesn't crash on no transaction, but just returns 0. """ # the newly created address has no in or out transaction history account = self.helper_new_account() address = account.address with patch_requests_get() as m_get: m_get.return_value.status_code = http.HTTPStatus.OK m_get.return_value.json.return_value = { 'status': '0', 'message': 'No transactions found', 'result': [], } nonce = PyWalib.get_nonce(address) url = mock.ANY headers = REQUESTS_HEADERS self.assertEqual(m_get.call_args_list, [mock.call(url, headers=headers)]) self.assertEqual(nonce, 0) def test_handle_web3_exception(self): """ Checks handle_web3_exception() error handling. """ # insufficient funds exception = ValueError({ 'code': -32000, 'message': 'insufficient funds for gas * price + value' }) with self.assertRaises(InsufficientFundsException) as e: PyWalib.handle_web3_exception(exception) self.assertEqual(e.exception.args[0], exception.args[0]) # unknown error code exception = ValueError({'code': 0, 'message': 'Unknown error'}) with self.assertRaises(UnknownEtherscanException) as e: PyWalib.handle_web3_exception(exception) self.assertEqual(e.exception.args[0], exception.args[0]) # no code exception = ValueError({'message': 'Unknown error'}) with self.assertRaises(UnknownEtherscanException) as e: PyWalib.handle_web3_exception(exception) self.assertEqual(e.exception.args[0], exception.args[0]) def test_transact(self): """ Basic transact() test, makes sure web3 sendRawTransaction gets called. """ pywalib = self.pywalib account = self.helper_new_account() to = ADDRESS sender = account.address value_wei = 100 with mock.patch('web3.eth.Eth.sendRawTransaction') \ as m_sendRawTransaction: pywalib.transact(to=to, value=value_wei, sender=sender) self.assertTrue(m_sendRawTransaction.called) def test_transact_no_sender(self): """ The sender parameter should default to the main account. Makes sure the transaction is being signed by the available account. """ pywalib = self.pywalib account = self.helper_new_account() to = ADDRESS value_wei = 100 with mock.patch('web3.eth.Eth.sendRawTransaction') \ as m_sendRawTransaction, \ mock.patch('web3.eth.Eth.account.signTransaction') \ as m_signTransaction: pywalib.transact(to=to, value=value_wei) self.assertTrue(m_sendRawTransaction.called) m_signTransaction.call_args_list transaction = { 'chainId': 1, 'gas': 25000, 'gasPrice': 4000000000, 'nonce': 0, 'to': to_checksum_address(to), 'value': value_wei, } expected_call = mock.call(transaction, account.privkey) self.assertEqual(m_signTransaction.call_args_list, [expected_call]) def test_transact_no_funds(self): """ Tries to send a transaction from an address with no funds. """ pywalib = self.pywalib account = self.helper_new_account() to = ADDRESS sender = account.address value_wei = 100 with self.assertRaises(InsufficientFundsException): pywalib.transact(to=to, value=value_wei, sender=sender) def test_get_default_keystore_path(self): """ Checks we the default keystore directory exists or create it. Verify the path is correct and that we have read/write access to it. """ keystore_dir = PyWalib.get_default_keystore_path() if not os.path.exists(keystore_dir): os.makedirs(keystore_dir) # checks path correctness self.assertTrue(keystore_dir.endswith(".config/pyethapp/keystore/")) # checks read/write access self.assertEqual(os.access(keystore_dir, os.R_OK), True) self.assertEqual(os.access(keystore_dir, os.W_OK), True)
class Controller(FloatLayout): current_account = ObjectProperty(None, allownone=True) # keeps track of all dialogs alive dialogs = [] def __init__(self, **kwargs): super(Controller, self).__init__(**kwargs) keystore_path = Controller.get_keystore_path() self.pywalib = PyWalib(keystore_path) Clock.schedule_once(lambda dt: self.load_landing_page()) @property def overview(self): overview_bnavigation_id = self.ids.overview_bnavigation_id return overview_bnavigation_id.ids.overview_id @property def history(self): return self.overview.ids.history_id @property def send(self): overview_bnavigation_id = self.ids.overview_bnavigation_id return overview_bnavigation_id.ids.send_id @property def toolbar(self): return self.ids.toolbar_id def set_toolbar_title(self, title): self.toolbar.title_property = title def open_account_list_helper(self, on_selected_item): title = "Select account" items = [] pywalib = self.pywalib account_list = pywalib.get_account_list() for account in account_list: address = '0x' + account.address.encode("hex") item = OneLineListItem(text=address) # makes sure the address doesn't wrap in multiple lines, # but gets shortened item.ids._lbl_primary.shorten = True item.account = account items.append(item) dialog = Controller.create_list_dialog(title, items, on_selected_item) dialog.open() def open_account_list_overview(self): def on_selected_item(instance, value): self.set_current_account(value.account) self.open_account_list_helper(on_selected_item) def set_current_account(self, account): self.current_account = account def on_current_account(self, instance, value): """ Updates Overview.current_account and History.current_account, then fetch account data. """ self.overview.current_account = value self.history.current_account = value self._load_balance() @staticmethod def show_invalid_form_dialog(): title = "Invalid form" body = "Please check form fields." dialog = Controller.create_dialog(title, body) dialog.open() @staticmethod def patch_keystore_path(): """ Changes pywalib default keystore path depending on platform. Currently only updates it on Android. """ if platform != "android": return import pywalib # uses kivy user_data_dir (/sdcard/<app_name>) pywalib.KEYSTORE_DIR_PREFIX = App.get_running_app().user_data_dir @staticmethod def get_keystore_path(): """ This is the Kivy default keystore path. """ keystore_path = os.environ.get('KEYSTORE_PATH') if keystore_path is None: Controller.patch_keystore_path() keystore_path = PyWalib.get_default_keystore_path() return keystore_path @staticmethod def src_dir(): return os.path.dirname(os.path.abspath(__file__)) @staticmethod def create_list_dialog(title, items, on_selected_item): """ Creates a dialog from given title and list. items is a list of BaseListItem objects. """ # select_list = PWSelectList(items=items, on_release=on_release) select_list = PWSelectList(items=items) select_list.bind(selected_item=on_selected_item) content = select_list dialog = MDDialog(title=title, content=content, size_hint=(.9, .9)) # workaround for MDDialog container size (too small by default) dialog.ids.container.size_hint_y = 1 # close the dialog as we select the element select_list.bind( selected_item=lambda instance, value: dialog.dismiss()) dialog.add_action_button("Dismiss", action=lambda *x: dialog.dismiss()) return dialog @staticmethod def on_dialog_dismiss(dialog): """ Removes it from the dialogs track list. """ Controller.dialogs.remove(dialog) @staticmethod def dismiss_all_dialogs(): """ Dispatches dismiss event for all dialogs. """ dialogs = Controller.dialogs[:] for dialog in dialogs: dialog.dispatch('on_dismiss') @staticmethod def create_dialog(title, body): """ Creates a dialog from given title and body. Adds it to the dialogs track list. """ content = MDLabel(font_style='Body1', theme_text_color='Secondary', text=body, size_hint_y=None, valign='top') content.bind(texture_size=content.setter('size')) dialog = MDDialog(title=title, content=content, size_hint=(.8, None), height=dp(200), auto_dismiss=False) dialog.add_action_button("Dismiss", action=lambda *x: dialog.dismiss()) dialog.bind(on_dismiss=Controller.on_dialog_dismiss) Controller.dialogs.append(dialog) return dialog @staticmethod def on_balance_connection_error(): title = "Network error" body = "Couldn't load balance, no network access." dialog = Controller.create_dialog(title, body) dialog.open() @staticmethod def on_history_connection_error(): title = "Network error" body = "Couldn't load history, no network access." dialog = Controller.create_dialog(title, body) dialog.open() @staticmethod def show_not_implemented_dialog(): title = "Not implemented" body = "This feature is not yet implemented." dialog = Controller.create_dialog(title, body) dialog.open() @mainthread def update_balance_label(self, balance): overview_id = self.overview overview_id.balance_property = balance def get_overview_title(self): overview_id = self.overview return overview_id.get_title() @staticmethod @mainthread def snackbar_message(text): Snackbar(text=text).show() def load_landing_page(self): """ Loads the landing page. """ try: # will trigger account data fetching self.current_account = self.pywalib.get_main_account() self.ids.screen_manager_id.current = "overview" self.ids.screen_manager_id.transition.direction = "right" except IndexError: self.load_create_new_account() @run_in_thread def _load_balance(self): account = self.current_account try: balance = self.pywalib.get_balance(account.address.encode("hex")) except ConnectionError: Controller.on_balance_connection_error() return self.update_balance_label(balance) def load_manage_keystores(self): """ Loads the manage keystores screen. """ # loads the manage keystores screen self.ids.screen_manager_id.transition.direction = "left" self.ids.screen_manager_id.current = 'manage_keystores' def load_create_new_account(self): """ Loads the create new account tab from the maage keystores screen. """ self.load_manage_keystores() # loads the create new account tab manage_keystores = self.ids.manage_keystores_id create_new_account = manage_keystores.ids.create_new_account_id create_new_account.dispatch('on_tab_press') def load_about_screen(self): """ Loads the about screen. """ self.ids.screen_manager_id.transition.direction = "left" self.ids.screen_manager_id.current = "about"
def __init__(self, **kwargs): super(Controller, self).__init__(**kwargs) keystore_path = Controller.get_keystore_path() self.pywalib = PyWalib(keystore_path) Clock.schedule_once(lambda dt: self.load_landing_page())
class PywalibTestCase(unittest.TestCase): """ Simple test cases, verifying pywalib works as expected. """ def setUp(self): self.keystore_dir = mkdtemp() self.pywalib = PyWalib(self.keystore_dir) def tearDown(self): shutil.rmtree(self.keystore_dir, ignore_errors=True) def helper_new_account(self, password=PASSWORD, security_ratio=1): """ Helper method for fast account creation. """ account = self.pywalib.new_account(password, security_ratio) return account 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 """ pywalib = self.pywalib # 1) verifies the current account list is empty account_list = pywalib.get_account_list() self.assertEqual(len(account_list), 0) # 2) creates a new account and verify we can retrieve it password = PASSWORD # weak account, but fast creation security_ratio = 1 account = pywalib.new_account(password, security_ratio) account_list = pywalib.get_account_list() self.assertEqual(len(account_list), 1) self.assertEqual(account, pywalib.get_main_account()) # 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_update_account_password(self): """ Verifies updating account password works. """ pywalib = self.pywalib current_password = "******" # weak account, but fast creation security_ratio = 1 account = pywalib.new_account(current_password, security_ratio) # first try when the account is already unlocked self.assertFalse(account.locked) new_password = "******" # on unlocked account the current_password is optional pywalib.update_account_password(account, new_password, current_password=None) # verify it worked account.lock() account.unlock(new_password) self.assertFalse(account.locked) # now try when the account is first locked account.lock() current_password = "******" with self.assertRaises(ValueError): pywalib.update_account_password(account, new_password, current_password) current_password = new_password pywalib.update_account_password(account, new_password, current_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(PyWalib.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. """ pywalib = self.pywalib account = self.helper_new_account() address = account.address self.assertEqual(len(pywalib.get_account_list()), 1) # deletes the account and verifies it's not loaded anymore pywalib.delete_account(account) self.assertEqual(len(pywalib.get_account_list()), 0) # even recreating the PyWalib object pywalib = PyWalib(self.keystore_dir) self.assertEqual(len(pywalib.get_account_list()), 0) # tries to reload it from the backup/trash location deleted_keystore_dir = PyWalib.deleted_account_dir(self.keystore_dir) pywalib = PyWalib(deleted_keystore_dir) self.assertEqual(len(pywalib.get_account_list()), 1) self.assertEqual(pywalib.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 """ pywalib = self.pywalib account = self.helper_new_account() # creates a file in the backup/trash folder that would conflict # with the deleted account deleted_keystore_dir = PyWalib.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(pywalib.get_account_list()), 1) pywalib.delete_account(account) self.assertEqual(len(pywalib.get_account_list()), 0) def test_handle_etherscan_error(self): """ Checks handle_etherscan_error() error handling. """ # no transaction found response_json = { 'message': 'No transactions found', 'result': [], 'status': '0' } with self.assertRaises(NoTransactionFoundException): PyWalib.handle_etherscan_error(response_json) # unknown error response_json = { 'message': 'Unknown error', 'result': [], 'status': '0' } with self.assertRaises(UnknownEtherscanException): PyWalib.handle_etherscan_error(response_json) # no error response_json = {'message': 'OK', 'result': [], 'status': '1'} self.assertEqual(PyWalib.handle_etherscan_error(response_json), None) def test_address_hex(self): """ Checks handle_etherscan_error() error handling. """ expected_addresss = ADDRESS # no 0x prefix address_no_prefix = ADDRESS.lower().strip("0x") address = address_no_prefix normalized = PyWalib.address_hex(address) self.assertEqual(normalized, expected_addresss) # uppercase address = "0x" + address_no_prefix.upper() normalized = PyWalib.address_hex(address) self.assertEqual(normalized, expected_addresss) # prefix cannot be uppercase address = "0X" + address_no_prefix.upper() with self.assertRaises(Exception) as context: PyWalib.address_hex(address) self.assertEqual(context.exception.message, "Invalid address format: '%s'" % (address)) def test_get_balance(self): """ Checks get_balance() returns a float. """ address = ADDRESS balance_eth = PyWalib.get_balance(address) self.assertTrue(type(balance_eth), float) def helper_get_history(self, transactions): """ Helper method to test history related methods. """ self.assertEqual(type(transactions), list) self.assertTrue(len(transactions) > 1) # ordered by timeStamp self.assertTrue( transactions[0]['timeStamp'] < transactions[1]['timeStamp']) # and a bunch of other things self.assertEqual( set(transactions[0].keys()), set([ 'nonce', 'contractAddress', 'cumulativeGasUsed', 'hash', 'blockHash', 'extra_dict', 'timeStamp', 'gas', 'value', 'blockNumber', 'to', 'confirmations', 'input', 'from', 'transactionIndex', 'isError', 'gasPrice', 'gasUsed' ])) def test_get_transaction_history(self): """ Checks get_transaction_history() works as expected. """ address = ADDRESS transactions = PyWalib.get_transaction_history(address) self.helper_get_history(transactions) # value is stored in Wei self.assertEqual(transactions[1]['value'], '200000000000000000') # but converted to Ether is also accessible self.assertEqual(transactions[1]['extra_dict']['value_eth'], 0.2) # history contains all send or received transactions self.assertEqual(transactions[1]['extra_dict']['sent'], False) self.assertEqual(transactions[1]['extra_dict']['received'], True) self.assertEqual(transactions[2]['extra_dict']['sent'], True) self.assertEqual(transactions[2]['extra_dict']['received'], False) def test_get_out_transaction_history(self): """ Checks get_out_transaction_history() works as expected. """ address = ADDRESS transactions = PyWalib.get_out_transaction_history(address) self.helper_get_history(transactions) for i in range(len(transactions)): transaction = transactions[i] extra_dict = transaction['extra_dict'] # this is only sent transactions self.assertEqual(extra_dict['sent'], True) self.assertEqual(extra_dict['received'], False) # nonce should be incremented each time self.assertEqual(transaction['nonce'], str(i)) def test_get_nonce(self): """ Checks get_nonce() returns the next nonce, i.e. transaction count. """ address = ADDRESS nonce = PyWalib.get_nonce(address) transactions = PyWalib.get_out_transaction_history(address) last_transaction = transactions[-1] last_nonce = int(last_transaction['nonce']) self.assertEqual(nonce, last_nonce + 1) def test_get_nonce_no_out_transaction(self): """ Makes sure get_nonce() doesn't crash on no out transaction, but just returns 0. """ # the VOID_ADDRESS has a lot of in transactions, # but no out ones, so the nonce should be 0 address = VOID_ADDRESS nonce = PyWalib.get_nonce(address) self.assertEqual(nonce, 0) def test_get_nonce_no_transaction(self): """ Makes sure get_nonce() doesn't crash on no transaction, but just returns 0. """ # the newly created address has no in or out transaction history account = self.helper_new_account() address = account.address nonce = PyWalib.get_nonce(address) self.assertEqual(nonce, 0) def test_handle_etherscan_tx_error(self): """ Checks handle_etherscan_tx_error() error handling. """ # no transaction found response_json = { 'jsonrpc': '2.0', 'id': 1, 'error': { 'message': 'Insufficient funds. ' 'The account you tried to send transaction from does not ' 'have enough funds. Required 10001500000000000000 and' 'got: 53856999715015294.', 'code': -32010, 'data': None } } with self.assertRaises(InsufficientFundsException): PyWalib.handle_etherscan_tx_error(response_json) # unknown error response_json = { 'jsonrpc': '2.0', 'id': 1, 'error': { 'message': 'Unknown error', 'code': 0, 'data': None } } with self.assertRaises(UnknownEtherscanException): PyWalib.handle_etherscan_tx_error(response_json) # no error response_json = {'jsonrpc': '2.0', 'id': 1} self.assertEqual(PyWalib.handle_etherscan_tx_error(response_json), None) def test_transact_no_found(self): """ Tries to send a transaction from an address with no found. """ pywalib = self.pywalib account = self.helper_new_account() to = ADDRESS sender = account.address value_wei = 100 with self.assertRaises(InsufficientFundsException): pywalib.transact(to=to, value=value_wei, sender=sender) def test_get_default_keystore_path(self): """ Checks we the default keystore directory exists or create it. Verify the path is correct and that we have read/write access to it. """ keystore_dir = PyWalib.get_default_keystore_path() if not os.path.exists(keystore_dir): os.makedirs(keystore_dir) # checks path correctness self.assertTrue(keystore_dir.endswith(".config/pyethapp/keystore/")) # checks read/write access self.assertEqual(os.access(keystore_dir, os.R_OK), True) self.assertEqual(os.access(keystore_dir, os.W_OK), True)
def setUp(self): self.keystore_dir = mkdtemp() self.pywalib = PyWalib(self.keystore_dir)