def helper_test_on_send_click(self, app): """ Verifies clicking "Send" Ethers works as expected, refs #63. Also checks for the amount field, refs #152. """ controller = app.controller # TODO: use dispatch('on_release') on navigation drawer controller.load_landing_page() send = controller.send send_button_id = send.ids.send_button_id # verifies clicking send button doesn't crash the application send_button_id.dispatch('on_release') dialogs = Dialog.dialogs # but it would still raise some popups since the form is invalid self.assertEqual(len(dialogs), 2) self.assertEqual(dialogs[0].title, 'Input error') self.assertEqual(dialogs[1].title, 'Invalid form') Dialog.dismiss_all_dialogs() self.assertEqual(len(dialogs), 0) # also checks for the amount field, refs #152 send_amount_id = send.ids.send_amount_id send_amount_id.text = '0.1' # the send_amount property should get updated from the input self.assertEqual(send.send_amount, 0.1) # blank amount shouldn't crash the app, just get ignored send_amount_id.text = '' self.assertEqual(send.send_amount, 0.1)
def helper_test_delete_account_none_selected(self, app): """ Tries to delete account when none are selected, refs #90. """ controller = app.controller pywalib = controller.pywalib manage_existing = controller.manage_existing # makes sure an account is selected pywalib.new_account(password="******", security_ratio=1) controller.current_account = pywalib.get_account_list()[0] # ManageExisting and Controller current_account should be in sync self.assertEqual(manage_existing.current_account, controller.current_account) # chaning in the Controller, should trigger the change on the other self.assertTrue(manage_existing.current_account is not None) controller.current_account = None self.assertIsNone(manage_existing.current_account) # let's try to delete this "None account" delete_button_id = manage_existing.ids.delete_button_id delete_button_id.dispatch('on_release') # an error dialog should pop dialogs = Dialog.dialogs self.assertEqual(len(dialogs), 1) dialog = dialogs[0] self.assertEqual(dialog.title, 'No account selected.') Dialog.dismiss_all_dialogs()
def helper_test_delete_last_account(self, app): """ Trying to delete the last account, should not crash the app, refs #120. """ controller = app.controller pywalib = controller.pywalib manage_existing = controller.manage_existing # makes sure there's only one account left self.assertEqual(len(pywalib.get_account_list()), 1) # deletes it delete_button_id = manage_existing.ids.delete_button_id delete_button_id.dispatch('on_release') # a confirmation popup should show dialogs = Dialog.dialogs self.assertEqual(len(dialogs), 1) dialog = dialogs[0] self.assertEqual(dialog.title, 'Are you sure?') # confirm it manage_existing.on_delete_account_yes(dialog) # account was deleted dialog message dialogs = Dialog.dialogs self.assertEqual(len(dialogs), 1) dialog = dialogs[0] self.assertEqual(dialog.title, 'Account deleted, redirecting...') Dialog.dismiss_all_dialogs() self.advance_frames(1) # verifies the account was deleted self.assertEqual(len(pywalib.get_account_list()), 0) # this should be done by the events, but doesn't seem to happen # so we have to trigger it manually controller.history.current_account = None self.advance_frames(1)
def helper_test_delete_account(self, app): """ Deletes account from the UI. """ controller = app.controller pywalib = controller.pywalib # makes sure we have an account to play with self.assertEqual(len(pywalib.get_account_list()), 1) # makes sure the account appears in the switch account view switch_account = self.helper_load_switch_account(app) account_list_id = switch_account.ids.account_list_id children = account_list_id.children self.assertEqual(len(children), 1) item = children[0] self.assertEqual(type(item), kivymd.list.OneLineListItem) self.assertEqual(item.account, pywalib.get_account_list()[0]) # go to the manage account screen # TODO: use dispatch('on_release') on navigation drawer controller.load_manage_keystores() # TODO: broken in #124 # self.advance_frames(1) self.advance_frames(30) self.assertEqual('Manage existing', app.controller.toolbar.title) # verifies an account is showing manage_existing = controller.manage_existing account_address_id = manage_existing.ids.account_address_id account = pywalib.get_account_list()[0] account_address = '0x' + account.address.hex() self.assertEqual(account_address_id.text, account_address) # clicks delete delete_button_id = manage_existing.ids.delete_button_id delete_button_id.dispatch('on_release') # a confirmation popup should show dialogs = Dialog.dialogs self.assertEqual(len(dialogs), 1) dialog = dialogs[0] self.assertEqual(dialog.title, 'Are you sure?') # confirm it # TODO: click on the dialog action button itself manage_existing.on_delete_account_yes(dialog) # the dialog should be replaced by another one dialogs = Dialog.dialogs self.assertEqual(len(dialogs), 1) dialog = dialogs[0] self.assertEqual(dialog.title, 'Account deleted, redirecting...') Dialog.dismiss_all_dialogs() # and the account deleted self.assertEqual(len(pywalib.get_account_list()), 0) # makes sure the account was also cleared from the selection view switch_account = self.helper_load_switch_account(app) account_list_id = switch_account.ids.account_list_id # TODO: broken in #124 self.advance_frames(30) self.assertEqual(len(account_list_id.children), 0)
def show_redirect_dialog(self): title = "Account deleted, redirecting..." body = "" body += "Your account was deleted, " body += "you will be redirected to the overview." dialog = Dialog.create_dialog(title, body) dialog.open()
def verify_amount_field(self): title = "Input error" body = "Invalid amount field" if self.send_amount == 0: dialog = Dialog.create_dialog(title, body) dialog.open() return False return True
def update_password(self): """ Update account password with new password provided. """ if not self.verify_fields(): Dialog.show_invalid_form_dialog() return Dialog.snackbar_message("Verifying current password...") if not self.verify_current_password_field(): Dialog.snackbar_message("Wrong account password") return pywalib = self.controller.pywalib account = self.current_account new_password = self.new_password1 Dialog.snackbar_message("Updating account...") pywalib.update_account_password(account, new_password=new_password) Dialog.snackbar_message("Updated!")
def show_storage_permissions_required_dialog(self): title = "External storage permissions required" body = "" body += "In order to save your keystore, PyWallet requires access " body += "to your device storage. " body += "Please allow PyWallet to access it when prompted." dialog = Dialog.create_dialog(title, body) dialog.open() return dialog
def prompt_no_account_error(self): """ Prompts an error since no account are selected for deletion, refs: https://github.com/AndreMiras/PyWallet/issues/90 """ title = "No account selected." body = "No account selected for deletion." dialog = Dialog.create_dialog(title, body) dialog.open()
def helper_test_on_send_click(self, app): """ Verifies clicking "Send" Ethers works as expected, refs #63. Also checks for the amount field, refs #152. """ controller = app.controller # TODO: use dispatch('on_release') on navigation drawer controller.load_landing_page() send = controller.send send_button_id = send.ids.send_button_id # verifies clicking send button doesn't crash the application send_button_id.dispatch('on_release') dialogs = Dialog.dialogs # but it would still raise a popup since the form is invalid self.assertEqual(len(dialogs), 1) self.assertEqual(dialogs[0].title, 'Input error') Dialog.dismiss_all_dialogs() self.assertEqual(len(dialogs), 0)
def setup(self): self.controller = App.get_running_app().controller self.keystore_path = Settings.get_keystore_path() accounts = self.controller.pywalib.get_account_list() if len(accounts) == 0: title = "No keystore found." body = "Import or create one." dialog = Dialog.create_dialog(title, body) dialog.open()
def verify_to_address_field(self): title = "Input error" body = "Invalid address field" try: to_checksum_address(self.send_to_address) except ValueError: dialog = Dialog.create_dialog(title, body) dialog.open() return False return True
def helper_confirm_account_deletion(self, app): """ Helper method for confirming account deletion popups. """ controller = app.controller manage_existing = controller.manage_existing # a confirmation popup should show dialogs = Dialog.dialogs self.assertEqual(len(dialogs), 1) dialog = dialogs[0] self.assertEqual(dialog.title, 'Are you sure?') # confirm it # TODO: click on the dialog action button itself manage_existing.on_delete_account_yes(dialog) # the dialog should be replaced by another one dialogs = Dialog.dialogs self.assertEqual(len(dialogs), 1) dialog = dialogs[0] self.assertEqual(dialog.title, 'Account deleted, redirecting...') Dialog.dismiss_all_dialogs() self.assertEqual(len(dialogs), 0)
def create_account(self): """ Creates an account from provided form. Verify we can unlock it. Disables widgets during the process, so the user doesn't try to create another account during the process. """ # circular ref from pywallet.controller import Controller self.toggle_widgets(False) if not self.verify_fields(): Dialog.show_invalid_form_dialog() self.toggle_widgets(True) return pywalib = self.controller.pywalib password = self.new_password1 security_ratio = self.security_slider_value # dividing again by 10, because otherwise it's # too slow on smart devices security_ratio /= 10.0 Dialog.snackbar_message("Creating account...") account = pywalib.new_account(password=password, security_ratio=security_ratio) Dialog.snackbar_message("Created!") self.toggle_widgets(True) Controller.set_account_alias(account, self.alias) self.on_account_created(account) CreateNewAccount.try_unlock(account, password) self.show_redirect_dialog() self.controller.load_landing_page() return account
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 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 prompt_password_dialog(self): """ Prompt the password dialog. """ title = "Enter your password" content = PasswordForm() dialog = Dialog.create_dialog_content_helper(title=title, content=content) # workaround for MDDialog container size (too small by default) dialog.ids.container.size_hint_y = 1 dialog.add_action_button( "Unlock", action=lambda *x: self.on_unlock_clicked(dialog, content.password)) return dialog
def helper_test_delete_account_twice(self, app): """ Trying to delete the same account twice, shoult not crash the app, refs #51. """ controller = app.controller pywalib = controller.pywalib manage_existing = controller.manage_existing # makes sure an account is selected pywalib.new_account(password="******", security_ratio=None) controller.current_account = pywalib.get_account_list()[0] self.assertTrue(manage_existing.current_account is not None) account_count_before = len(pywalib.get_account_list()) # let's try to delete this account once delete_button_id = manage_existing.ids.delete_button_id delete_button_id.dispatch('on_release') self.helper_confirm_account_deletion(app) # the account should be deleted self.assertEqual(len(pywalib.get_account_list()), account_count_before - 1) # makes sure the account was also cleared from the selection view switch_account = self.helper_load_switch_account(app) account_list_id = switch_account.ids.account_list_id self.assertEqual(len(account_list_id.children), len(pywalib.get_account_list())) # TODO: the selected account should now be None # self.assertIsNone(manage_existing.current_account) # self.assertIsNone(controller.current_account) # let's try to delete this account a second time delete_button_id = manage_existing.ids.delete_button_id delete_button_id.dispatch('on_release') # TODO: the second time an error dialog should pop # dialogs = Dialog.dialogs # self.assertEqual(len(dialogs), 1) # dialog = dialogs[0] # self.assertEqual(dialog.title, 'No account selected.') Dialog.dismiss_all_dialogs()
def prompt_delete_account_dialog(self): """ Prompt a confirmation dialog before deleting the account. """ if self.current_account is None: self.prompt_no_account_error() return title = "Are you sure?" body = "" body += "This action cannot be undone.\n" body += "Are you sure you want to delete this account?\n" dialog = Dialog.create_dialog_helper(title, body) dialog.add_action_button("No", action=lambda *x: dialog.dismiss()) dialog.add_action_button( "Yes", action=lambda *x: self.on_delete_account_yes(dialog)) dialog.open()
def try_unlock(account, password): """ Just as a security measure, verifies we can unlock the newly created account with provided password. """ # making sure it's locked first account.lock() try: account.unlock(password) except ValueError: title = "Unlock error" body = "" body += "Couldn't unlock your account.\n" body += "The issue should be reported." dialog = Dialog.create_dialog(title, body) dialog.open() return
def helper_test_dismiss_dialog_twice(self, app): """ If by some choice the dismiss event of a dialog created with Controller.create_dialog_helper() is fired twice, it should be handled gracefully, refs #89. """ title = "title" body = "body" # makes sure the controller has no dialog self.assertEqual(Dialog.dialogs, []) # creates one and verifies it was created dialog = Dialog.create_dialog_helper(title, body) self.assertEqual(len(Dialog.dialogs), 1) # dimisses it once and verifies it was handled dialog.dispatch('on_dismiss') self.assertEqual(Dialog.dialogs, []) # then a second time and it should not crash dialog.dispatch('on_dismiss') self.assertEqual(Dialog.dialogs, [])
def helper_test_controller_fetch_balance(self, app): """ Verifies Controller.fetch_balance() works in most common cases. 1) simple case, library PyWalib.get_balance() gets called 2) ConnectionError should be handled 3) handles 503 "service is unavailable", refs #91 4) UnknownEtherscanException should be handled """ controller = app.controller account = controller.current_account balance = 42 # 1) simple case, library PyWalib.get_balance() gets called with mock.patch('pywalib.PyWalib.get_balance') as mock_get_balance, \ patch_get_store_path(self.temp_path): mock_get_balance.return_value = balance thread = controller.fetch_balance() thread.join() address = '0x' + account.address.hex() mock_get_balance.assert_called_with(address, pywalib.ChainID.MAINNET) # and the balance updated self.assertEqual(controller.accounts_balance[address], balance) # 2) ConnectionError should be handled self.assertEqual(len(Dialog.dialogs), 0) with mock.patch('pywalib.PyWalib.get_balance') as mock_get_balance, \ mock.patch('pywallet.controller.Logger') as mock_logger: mock_get_balance.side_effect = requests.exceptions.ConnectionError thread = controller.fetch_balance() thread.join() self.assertEqual(len(Dialog.dialogs), 1) dialog = Dialog.dialogs[0] self.assertEqual(dialog.title, 'Network error') Dialog.dismiss_all_dialogs() # the error should be logged mock_logger.warning.assert_called_with('ConnectionError', exc_info=True) # 3) handles 503 "service is unavailable", refs #91 self.assertEqual(len(Dialog.dialogs), 0) response = requests.Response() response.status_code = 503 response.raw = io.BytesIO(b'The service is unavailable.') with mock.patch('requests.get') as mock_requests_get, \ mock.patch('pywallet.controller.Logger') as mock_logger: mock_requests_get.return_value = response thread = controller.fetch_balance() thread.join() self.assertEqual(len(Dialog.dialogs), 1) dialog = Dialog.dialogs[0] self.assertEqual(dialog.title, 'Unknown error') Dialog.dismiss_all_dialogs() # the error should be logged mock_logger.error.assert_called_with('UnknownEtherscanException', exc_info=True) # 4) UnknownEtherscanException should be handled self.assertEqual(len(Dialog.dialogs), 0) with mock.patch('pywalib.PyWalib.get_balance') as mock_get_balance, \ mock.patch('pywallet.controller.Logger') as mock_logger: mock_get_balance.side_effect = pywalib.UnknownEtherscanException thread = controller.fetch_balance() thread.join() self.assertEqual(len(Dialog.dialogs), 1) dialog = Dialog.dialogs[0] self.assertEqual(dialog.title, 'Unknown error') Dialog.dismiss_all_dialogs() # the error should be logged mock_logger.error.assert_called_with('UnknownEtherscanException', exc_info=True)
def on_send_click(self): if not self.verify_fields(): Dialog.show_invalid_form_dialog() return dialog = self.prompt_password_dialog() dialog.open()
def unlock_send_transaction(self): """ Unlocks the account with password in order to sign and publish the transaction. """ controller = App.get_running_app().controller pywalib = controller.pywalib address = to_checksum_address(self.send_to_address) amount_eth = round(self.send_amount, ROUND_DIGITS) amount_wei = int(amount_eth * pow(10, 18)) gas_price_gwei = Settings.get_stored_gas_price() gas_price_wei = int(gas_price_gwei * (10 ** 9)) # TODO: not the main account, but the current account account = controller.current_account Dialog.snackbar_message("Unlocking account...") try: account.unlock(self.password) except ValueError: Dialog.snackbar_message("Could not unlock account") return Dialog.snackbar_message("Unlocked! Sending transaction...") sender = account.address try: pywalib.transact( address, value=amount_wei, data='', sender=sender, gasprice=gas_price_wei) except InsufficientFundsException: Dialog.snackbar_message("Insufficient funds") return except UnknownEtherscanException: Dialog.snackbar_message("Unknown error") Logger.error('UnknownEtherscanException', exc_info=True) return # TODO: handle ConnectionError Dialog.snackbar_message("Sent!")