electrum

Electrum Bitcoin wallet
git clone https://git.parazyd.org/electrum
Log | Files | Refs | Submodules

commit a1681eeeba4c212dcbb6087f1ed3201378492bd8
parent f33fbefce08468d22ecbb60802e7e38ff8db5f15
Author: qua-non <akshayaurora@gmail.com>
Date:   Sun,  2 Mar 2014 00:41:58 +0530

handle app start, background wallet interfacing. UX to be merged next.

Diffstat:
Mgui/kivy/dialog.py | 14+++++++++++---
Mgui/kivy/installwizard.py | 17++++++++++-------
Mgui/kivy/main.kv | 5+++--
Mgui/kivy/main_window.py | 509++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
4 files changed, 515 insertions(+), 30 deletions(-)

diff --git a/gui/kivy/dialog.py b/gui/kivy/dialog.py @@ -495,8 +495,8 @@ class RestoreSeedDialog(CreateAccountDialog): tis._keyboard.bind(on_key_down=self.on_key_down) stepper = self.ids.stepper stepper.opacity = 1 - stepper.source = ('atlas://gui/kivy/theming" - "/light/stepper_restore_seed') + stepper.source = ('atlas://gui/kivy/theming' + '/light/stepper_restore_seed') self._back = _back = partial(self.ids.back.dispatch, 'on_release') app.navigation_higherarchy.append(_back) @@ -582,7 +582,15 @@ class ChangePasswordDialog(CreateAccountDialog): if value: stepper = self.ids.stepper stepper.opacity = 1 - self.ids.ti_wallet_name.focus = True + t_wallet_name = self.ids.ti_wallet_name + if self.mode in ('create', 'restore'): + t_wallet_name.text = 'Default Wallet' + t_wallet_name.readonly = True + self.ids.ti_new_password.focus = True + else: + t_wallet_name.text = '' + t_wallet_name.readonly = False + t_wallet_name.focus = True stepper.source = 'atlas://gui/kivy/theming/light/stepper_left' self._back = _back = partial(self.ids.back.dispatch, 'on_release') app.navigation_higherarchy.append(_back) diff --git a/gui/kivy/installwizard.py b/gui/kivy/installwizard.py @@ -20,7 +20,7 @@ app = App.get_running_app() class InstallWizard(Widget): - '''Instalation Wizzard. Responsible for instantiating the + '''Installation Wizard. Responsible for instantiating the creation/restoration of wallets. events:: @@ -232,7 +232,7 @@ class InstallWizard(Widget): ti_new_password.focus = True else: ti_password.focus = True - return app.show_error(_('Passwords do not match')) + return app.show_error(_('Passwords do not match'), duration=.5) if mode == 'restore': try: @@ -253,7 +253,7 @@ class InstallWizard(Widget): try: seed = wallet.decode_seed(password) except BaseException: - return app.show_error(_('Incorrect Password')) + return app.show_error(_('Incorrect Password'), duration=.5) # test carefully try: @@ -291,6 +291,7 @@ class InstallWizard(Widget): if mode in ('restore', 'create'): # auto cycle self.config.set_key('auto_cycle', True, True) + # start wallet threads wallet.start_threads(self.network) @@ -303,14 +304,16 @@ class InstallWizard(Widget): def on_complete(*l): if not self.network: - app.show_info(_("This wallet was restored offline." - "It may contain more addresses than displayed.")) + app.show_info( + _("This wallet was restored offline. It may contain more" + " addresses than displayed."), duration=.5) return self.dispatch('on_wizard_complete', wallet) if wallet.is_found(): - app.show_info(_("Recovery successful")) + app.show_info(_("Recovery successful"), duration=.5) else: - app.show_info(_("No transactions found for this seed")) + app.show_info(_("No transactions found for this seed"), + duration=.5) return self.dispatch('on_wizard_complete', wallet) self.waiting_dialog(lambda: wallet.restore(get_text), diff --git a/gui/kivy/main.kv b/gui/kivy/main.kv @@ -104,7 +104,7 @@ size_hint: None, 1 width: (root.width - dp(20)) if root.fs else (0 if not root.icon else '32dp') Widget: - size_hint_y: None + size_hint_x: None width: '5dp' Label: id: lbl @@ -112,7 +112,8 @@ font_size: '12sp' text: root.message text_size: self.width, None - size_hint: None, 1 + valign: 'middle' + size_hint: 1, 1 width: 0 if root.fs else (root.width - img.width) <-CreateAccountDialog> diff --git a/gui/kivy/main_window.py b/gui/kivy/main_window.py @@ -1,7 +1,9 @@ import sys +from decimal import Decimal from electrum import WalletStorage, Wallet -from electrum.i18n import _ +from electrum.i18n import _, set_language +from electrum.wallet import format_satoshis from kivy.app import App from kivy.core.window import Window @@ -10,11 +12,15 @@ from kivy.logger import Logger from kivy.utils import platform from kivy.properties import (OptionProperty, AliasProperty, ObjectProperty, StringProperty, ListProperty) +from kivy.clock import Clock #inclusions for factory so that widgets can be used in kv from gui.kivy.drawer import Drawer from gui.kivy.dialog import InfoBubble +# delayed imports +notification = None + class ElectrumWindow(App): title = _('Electrum App') @@ -25,10 +31,10 @@ class ElectrumWindow(App): :attr:`wallet` is a `ObjectProperty` defaults to None. ''' - conf = ObjectProperty(None) + electrum_config = ObjectProperty(None) '''Holds the electrum config - :attr:`conf` is a `ObjectProperty`, defaults to None. + :attr:`electrum_config` is a `ObjectProperty`, defaults to None. ''' status = StringProperty(_('Uninitialised')) @@ -37,10 +43,60 @@ class ElectrumWindow(App): :attr:`status` is a `StringProperty` defaults to _'uninitialised' ''' - base_unit = StringProperty('BTC') + def _get_num_zeros(self): + try: + return self.electrum_config.get('num_zeros', 0) + except AttributeError: + return 0 + + def _set_num_zeros(self): + try: + self.electrum_config.set_key('num_zeros', value, True) + except AttributeError: + Logger.error('Electrum: Config not available ' + 'While trying to save value to config') + + num_zeros = AliasProperty(_get_num_zeros , _set_num_zeros) + '''Number of zeros used while representing the value in base_unit. + ''' + + def _get_decimal(self): + try: + return self.electrum_config.get('decimal_point', 8) + except AttributeError: + return 8 + + def _set_decimal(self, value): + try: + self.electrum_config.set_key('decimal_point', value, True) + except AttributeError: + Logger.error('Electrum: Config not set ' + 'While trying to save value to config') + + decimal_point = AliasProperty(_get_decimal, _set_decimal) + '''This defines the decimal point to be used determining the + :attr:`base_unit`. + + :attr:`decimal_point` is a `AliasProperty` defaults to the value gotten + from electrum config. + ''' + + def _get_bu(self): + assert self.decimal_point in (5,8) + return "BTC" if self.decimal_point == 8 else "mBTC" + + def _set_bu(self, value): + try: + self.electrum_config.set_key('base_unit', value, True) + except AttributeError: + Logger.error('Electrum: Config not set ' + 'While trying to save value to config') + + base_unit = AliasProperty(_get_bu, _set_bu, bind=('decimal_point',)) '''BTC or UBTC or ... - :attr:`base_unit` is a `StringProperty` defaults to 'BTC' + :attr:`base_unit` is a `AliasProperty` defaults to the unit set in + electrum config. ''' _ui_mode = OptionProperty('phone', options=('tablet', 'phone')) @@ -84,13 +140,20 @@ class ElectrumWindow(App): def __init__(self, **kwargs): # initialize variables self.info_bubble = None + self.console = None + self.exchanger = None + super(ElectrumWindow, self).__init__(**kwargs) self.network = network = kwargs.get('network') self.electrum_config = config = kwargs.get('config') - def load_wallet(self, wallet): - # TODO - pass + # create triggers so as to minimize updation a max of 5 times a sec + self._trigger_update_status = Clock.create_trigger(self.update_status, + .2) + self._trigger_update_console = Clock.create_trigger(self.update_console, + .2) + self._trigger_notify_transactions = \ + Clock.create_trigger(self.notify_transactions, .2) def build(self): from kivy.lang import Builder @@ -98,15 +161,21 @@ class ElectrumWindow(App): def _pause(self): if platform == 'android': + # move activity to back from jnius import autoclass python_act = autoclass('org.renpy.android.PythonActivity') mActivity = python_act.mActivity mActivity.moveTaskToBack(True) def on_start(self): + ''' This is the start point of the kivy ui + ''' Window.bind(size=self.on_size, on_keyboard=self.on_keyboard) - Window.bind(keyboard_height=self.on_keyboard_height) + Window.bind(on_key_down=self.on_key_down) + if platform == 'android': + # + Window.bind(keyboard_height=self.on_keyboard_height) self.on_size(Window, Window.size) config = self.electrum_config storage = WalletStorage(config) @@ -127,8 +196,11 @@ class ElectrumWindow(App): self.on_resume() + def on_stop(self): + self.wallet.stop_threads() + def on_back(self): - ''' Manage screen higherarchy + ''' Manage screen hierarchy ''' try: self.navigation_higherarchy.pop()() @@ -146,9 +218,28 @@ class ElectrumWindow(App): Window.children[1] Animation(y=Window.keyboard_height, d=.1).start(active_widg) + def on_key_down(self, instance, key, keycode, codepoint, modifiers): + if 'ctrl' in modifiers: + # q=24 w=25 + if keycode in (24, 25): + self.stop() + elif keycode == 27: + # r=27 + # force update wallet + self.update_wallet() + elif keycode == 112: + # pageup + #TODO move to next tab + pass + elif keycode == 117: + # pagedown + #TODO move to prev tab + pass + #TODO: alt+tab_number to activate the particular tab + def on_keyboard(self, instance, key, keycode, codepoint, modifiers): # override settings button - if key in (319, 282): + if key in (319, 282): #f1/settings button on android self.gui.main_gui.toggle_settings(self) return True if key == 27: @@ -160,14 +251,18 @@ class ElectrumWindow(App): Logger.debug('Electrum: No Wallet set/found. Exiting...') app.show_error('Electrum: No Wallet set/found. Exiting...', exit=True) - return + Logger.info('wizard complete') + + self.init_ui() # plugins that need to change the GUI do it here #run_hook('init') self.load_wallet(wallet) - Clock.schedule_once(update_wallet) + # check and remove this load_wallet calls update_wallet no + # need for this here + #Clock.schedule_once(update_wallet) #self.windows.append(w) #if url: w.set_url(url) @@ -177,7 +272,381 @@ class ElectrumWindow(App): #self.app.exec_() - wallet.stop_threads() + def init_ui(self): + ''' Initialize The Ux part of electrum. This function performs the basic + tasks of setting up the ui. + ''' + + # unused? + #self._close_electrum = False + + #self._tray_icon = 'icons/" + (electrum_dark_icon.png'\ + # if platform == 'mac' else 'electrum_light_icon.png') + + #setup tray + #self.tray = SystemTrayIcon(self.icon, self) + #self.tray.setToolTip('Electrum') + #self.tray.activated.connect(self.tray_activated) + + set_language(self.electrum_config.get('language')) + + self.funds_error = False + self.completions = [] + + # setup UX + #self.load_dashboard + + self.icon = "icons/electrum.png" + + # load and focus the ui + + # connect callbacks + if self.network: + self.network.register_callback( + 'updated', self._trigger_update_status) + self.network.register_callback( + 'banner', self._trigger_update_console) + self.network.register_callback( + 'disconnected', self._trigger_update_status) + self.network.register_callback( + 'disconnecting', self._trigger_update_status) + self.network.register_callback('new_transaction', + self._trigger_notify_transactions) + + # set initial message + self.update_console() + + self.wallet = None + + def create_quote_text(self, btc_balance, mode='normal'): + ''' + ''' + if not self.exchanger: + from plugins.exchange_rate import Exchanger + self.exchanger = Exchanger(self) + self.exchanger.start() + quote_currency = self.electrum_config.get("currency", 'EUR') + quote_balance = self.exchanger.exchange(btc_balance, quote_currency) + + if mode == 'symbol': + if quote_currency: + quote_currency = self.exchanger.symbols[quote_currency] + + if quote_balance is None: + quote_text = "" + else: + quote_text = " (%.2f %s)" % (quote_balance, quote_currency) + return quote_text + + def set_currencies(self, quote_currencies): + self._trigger_update_status + #self.currencies = sorted(quote_currencies.keys()) + + def update_console(self, *dt): + if self.console: + self.console.showMessage(self.network.banner) + + def load_wallet(self, wallet): + self.wallet = wallet + self.accounts_expanded = self.wallet.storage.get('accounts_expanded', {}) + self.current_account = self.wallet.storage.get('current_account', None) + + title = 'Electrum ' + self.wallet.electrum_version + ' - '\ + + self.wallet.storage.path + if wallet.is_watching_only(): + title += ' [{}]'.format(_('watching only')) + self.title = title + self.update_wallet() + # Once GUI has been initialized check if we want to announce something + # since the callback has been called before the GUI was initialized + self.notify_transactions() + self.update_account_selector() + #TODO + #self.new_account.setEnabled(self.wallet.seed_version>4) + #self.update_lock_icon() + #self.update_buttons_on_seed() + + #run_hook('load_wallet', wallet) + + def update_status(self, *dt): + if not self.wallet: + return + if self.network is None or not self.network.is_running(): + text = _("Offline") + #icon = QIcon(":icons/status_disconnected.png") + + elif self.network.is_connected(): + unconfirmed = '' + quote_text = '.' + if not self.wallet.up_to_date: + text = _("Synchronizing...") + #icon = QIcon(":icons/status_waiting.png") + elif self.network.server_lag > 1: + text = _("Server is lagging (%d blocks)"%self.network.server_lag) + #icon = QIcon(":icons/status_lagging.png") + else: + c, u = self.wallet.get_account_balance(self.current_account) + text = self.format_amount(c) + if u: + unconfirmed = " [%s unconfirmed]"\ + %( self.format_amount(u, True).strip()) + quote_text = self.create_quote_text(Decimal(c+u)/100000000) or '.' + + #r = {} + #run_hook('set_quote_text', c+u, r) + #quote = r.get(0) + #if quote: + # text += " (%s)"%quote + + self.notify(_("Balance: ") + text) + #icon = QIcon(":icons/status_connected.png") + else: + text = _("Not connected") + #icon = QIcon(":icons/status_disconnected.png") + + #TODO + #status_card = self.root.main_screen.ids.tabs.ids.\ + # screen_dashboard.ids.status_card + self.status = text.strip() + #status_card.quote_text = quote_text.strip() + #status_card.uncomfirmed = unconfirmed.strip() + ##app.base_unit = self.base_unit().strip() + + def format_amount(self, x, is_diff=False, whitespaces=False): + ''' + ''' + return format_satoshis(x, is_diff, self.num_zeros, self.decimal_point, whitespaces) + + def update_wallet(self): + ''' + ''' + self.update_status() + if (self.wallet.up_to_date or + not self.network or not self.network.is_connected()): + #TODO + #self.update_history_tab() + #self.update_receive_tab() + #self.update_contacts_tab() + self.update_completions() + + def update_account_selector(self): + # account selector + #TODO + return + accounts = self.wallet.get_account_names() + self.account_selector.clear() + if len(accounts) > 1: + self.account_selector.addItems([_("All accounts")] + accounts.values()) + self.account_selector.setCurrentIndex(0) + self.account_selector.show() + else: + self.account_selector.hide() + + def update_history_tab(self, see_all=False): + def parse_histories(items): + results = [] + for item in items: + tx_hash, conf, is_mine, value, fee, balance, timestamp = item + if conf > 0: + try: + time_str = datetime.datetime.fromtimestamp( + timestamp).isoformat(' ')[:-3] + except: + time_str = _("unknown") + + if conf == -1: + time_str = _('unverified') + icon = "atlas://gui/kivy/theming/light/close" + elif conf == 0: + time_str = _('pending') + icon = "atlas://gui/kivy/theming/light/unconfirmed" + elif conf < 6: + time_str = '' # add new to fix error when conf < 0 + conf = max(1, conf) + icon = "atlas://gui/kivy/theming/light/clock{}".format(conf) + else: + icon = "atlas://gui/kivy/theming/light/confirmed" + + if value is not None: + v_str = self.format_amount(value, True, whitespaces=True) + else: + v_str = '--' + + balance_str = self.format_amount(balance, whitespaces=True) + + if tx_hash: + label, is_default_label = self.wallet.get_label(tx_hash) + else: + label = _('Pruned transaction outputs') + is_default_label = False + + results.append(( + conf, icon, time_str, label, v_str, balance_str, tx_hash)) + + return results + + history_card = self.root.main_screen.ids.tabs.ids.\ + screen_dashboard.ids.recent_activity_card + histories = parse_histories(reversed( + self.wallet.get_tx_history(self.current_account))) + #history_view.content_adapter.data = histories + + # repopulate History Card + last_widget = history_card.ids.content.children[-1] + history_card.ids.content.clear_widgets() + history_add = history_card.ids.content.add_widget + history_add(last_widget) + RecentActivityItem = Factory.RecentActivityItem + + history_card.ids.btn_see_all.opacity = (0 if see_all or + len(histories) < 8 else 1) + if not see_all: + histories = histories[:8] + + create_quote_text = self.create_quote_text + for items in histories: + conf, icon, date_time, address, amount, balance, tx = items + ri = RecentActivityItem() + ri.icon = icon + ri.date = date_time + ri.address = address + ri.amount = amount + ri.quote_text = create_quote_text( + Decimal(amount)/100000000, mode='symbol') + ri.balance = balance + ri.confirmations = conf + ri.tx_hash = tx + history_add(ri) + + def update_receive_tab(self): + #TODO move to address managment + return + data = [] + + if self.current_account is None: + account_items = self.wallet.accounts.items() + elif self.current_account != -1: + account_items = [(self.current_account, self.wallet.accounts.get(self.current_account))] + else: + account_items = [] + + for k, account in account_items: + name = account.get('name', str(k)) + c, u = self.wallet.get_account_balance(k) + data = [(name, '', self.format_amount(c + u), '')] + + for is_change in ([0, 1] if self.expert_mode else [0]): + if self.expert_mode: + name = "Receiving" if not is_change else "Change" + seq_item = (name, '', '', '') + data.append(seq_item) + else: + seq_item = data + is_red = False + gap = 0 + + for address in account[is_change]: + h = self.wallet.history.get(address, []) + + if h == []: + gap += 1 + if gap > self.wallet.gap_limit: + is_red = True + else: + gap = 0 + + num_tx = '*' if h == ['*'] else "%d" % len(h) + item = (address, self.wallet.labels.get(address, ''), '', num_tx) + data.append(item) + self.update_receive_item(item) + + if self.wallet.imported_keys and (self.current_account is None + or self.current_account == -1): + c, u = self.wallet.get_imported_balance() + data.append((_('Imported'), '', self.format_amount(c + u), '')) + for address in self.wallet.imported_keys.keys(): + item = (address, self.wallet.labels.get(address, ''), '', '') + data.append(item) + self.update_receive_item(item) + + receive_list = app.root.main_screen.ids.tabs.ids\ + .screen_receive.receive_view + receive_list.content_adapter.data = data + + def update_contacts_tab(self): + data = [] + for address in self.wallet.addressbook: + label = self.wallet.labels.get(address, '') + item = (address, label, "%d" % self.wallet.get_num_tx(address)) + data.append(item) + # item.setFont(0, QFont(MONOSPACE_FONT)) + # # 32 = label can be edited (bool) + # item.setData(0,32, True) + # # 33 = payto string + # item.setData(0,33, address) + + self.run_hook('update_contacts_tab') + + contact_list = app.root.main_screen.ids.tabs.ids.\ + screen_contacts.ids.contacts_list + contact_list.content_adapter.data = data + + def update_completions(self): + l = [] + for addr, label in self.wallet.labels.items(): + if addr in self.wallet.addressbook: + l.append(label + ' <' + addr + '>') + + #self.run_hook('update_completions', l) + self.completions = l + + def notify_transactions(self, *dt): + ''' + ''' + if not self.network or not self.network.is_connected(): + return + + iface = self.network.interface + if len(iface.pending_transactions_for_notifications) > 0: + # Combine the transactions if there are more then three + tx_amount = len(iface.pending_transactions_for_notifications) + if(tx_amount >= 3): + total_amount = 0 + for tx in iface.pending_transactions_for_notifications: + is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx) + if(v > 0): + total_amount += v + self.notify(_("{txs}s new transactions received. Total amount" + "received in the new transactions {amount}s" + "{unit}s").format(txs=tx_amount, + amount=self.format_amount(total_amount), + unit=self.base_unit())) + + iface.pending_transactions_for_notifications = [] + else: + for tx in iface.pending_transactions_for_notifications: + if tx: + iface.pending_transactions_for_notifications.remove(tx) + is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx) + if(v > 0): + from pudb import set_trace; set_trace() + self.notify( + _("New transaction received. {amount}s {unit}s"). + format( amount=self.format_amount(v), + unit=self.base_unit())) + + def notify(self, message): + try: + global notification, os + if not notification: + from plyer import notification + import os + icon = (os.path.dirname(os.path.realpath(__file__)) + + '/../../' + self.icon) + notification.notify('Electrum', message, + app_icon=icon, app_name='Electrum') + except ImportError: + Logger.Error('Notification: needs plyer; `sudo pip install plyer`') def on_pause(self): ''' @@ -231,7 +700,8 @@ class ElectrumWindow(App): pos=None, arrow_pos=None, exit=False, - icon='atlas://gui/kivy/theming/light/error',): + icon='atlas://gui/kivy/theming/light/error', + duration=0): ''' Show a error Message Bubble. ''' self.show_info_bubble( @@ -240,16 +710,19 @@ class ElectrumWindow(App): width=width, pos=pos or Window.center, arrow_pos=arrow_pos, - exit=exit) + exit=exit, + duration=duration) def show_info(self, error, width='200dp', pos=None, arrow_pos=None, - exit=False): + exit=False, + duration=0): ''' Show a Info Message Bubble. ''' - self.show_error(error, icon='atlas://gui/kivy/theming/light/error') + self.show_error(error, icon='atlas://gui/kivy/theming/light/error', + duration=duration) def show_info_bubble(self, text=_('Hello World'),