electrum

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

main_window.py (54019B)


      1 import re
      2 import os
      3 import sys
      4 import time
      5 import datetime
      6 import traceback
      7 from decimal import Decimal
      8 import threading
      9 import asyncio
     10 from typing import TYPE_CHECKING, Optional, Union, Callable, Sequence
     11 
     12 from electrum.storage import WalletStorage, StorageReadWriteError
     13 from electrum.wallet_db import WalletDB
     14 from electrum.wallet import Wallet, InternalAddressCorruption, Abstract_Wallet
     15 from electrum.wallet import check_password_for_directory, update_password_for_directory
     16 
     17 from electrum.plugin import run_hook
     18 from electrum import util
     19 from electrum.util import (profiler, InvalidPassword, send_exception_to_crash_reporter,
     20                            format_satoshis, format_satoshis_plain, format_fee_satoshis,
     21                            maybe_extract_bolt11_invoice)
     22 from electrum.invoices import PR_PAID, PR_FAILED
     23 from electrum import blockchain
     24 from electrum.network import Network, TxBroadcastError, BestEffortRequestFailed
     25 from electrum.interface import PREFERRED_NETWORK_PROTOCOL, ServerAddr
     26 from electrum.logging import Logger
     27 from .i18n import _
     28 from . import KIVY_GUI_PATH
     29 
     30 from kivy.app import App
     31 from kivy.core.window import Window
     32 from kivy.utils import platform
     33 from kivy.properties import (OptionProperty, AliasProperty, ObjectProperty,
     34                              StringProperty, ListProperty, BooleanProperty, NumericProperty)
     35 from kivy.cache import Cache
     36 from kivy.clock import Clock
     37 from kivy.factory import Factory
     38 from kivy.metrics import inch
     39 from kivy.lang import Builder
     40 from .uix.dialogs.password_dialog import OpenWalletDialog, ChangePasswordDialog, PincodeDialog
     41 
     42 ## lazy imports for factory so that widgets can be used in kv
     43 #Factory.register('InstallWizard', module='electrum.gui.kivy.uix.dialogs.installwizard')
     44 #Factory.register('InfoBubble', module='electrum.gui.kivy.uix.dialogs')
     45 #Factory.register('OutputList', module='electrum.gui.kivy.uix.dialogs')
     46 #Factory.register('OutputItem', module='electrum.gui.kivy.uix.dialogs')
     47 
     48 from .uix.dialogs.installwizard import InstallWizard
     49 from .uix.dialogs import InfoBubble, crash_reporter
     50 from .uix.dialogs import OutputList, OutputItem
     51 from .uix.dialogs import TopLabel, RefLabel
     52 from .uix.dialogs.question import Question
     53 
     54 #from kivy.core.window import Window
     55 #Window.softinput_mode = 'below_target'
     56 
     57 # delayed imports: for startup speed on android
     58 notification = app = ref = None
     59 
     60 # register widget cache for keeping memory down timeout to forever to cache
     61 # the data
     62 Cache.register('electrum_widgets', timeout=0)
     63 
     64 from kivy.uix.screenmanager import Screen
     65 from kivy.uix.tabbedpanel import TabbedPanel
     66 from kivy.uix.label import Label
     67 from kivy.core.clipboard import Clipboard
     68 
     69 Factory.register('TabbedCarousel', module='electrum.gui.kivy.uix.screens')
     70 
     71 # Register fonts without this you won't be able to use bold/italic...
     72 # inside markup.
     73 from kivy.core.text import Label
     74 Label.register(
     75     'Roboto',
     76     KIVY_GUI_PATH + '/data/fonts/Roboto.ttf',
     77     KIVY_GUI_PATH + '/data/fonts/Roboto.ttf',
     78     KIVY_GUI_PATH + '/data/fonts/Roboto-Bold.ttf',
     79     KIVY_GUI_PATH + '/data/fonts/Roboto-Bold.ttf',
     80 )
     81 
     82 
     83 from electrum.util import (NoDynamicFeeEstimates, NotEnoughFunds,
     84                            BITCOIN_BIP21_URI_SCHEME, LIGHTNING_URI_SCHEME,
     85                            UserFacingException)
     86 
     87 from .uix.dialogs.lightning_open_channel import LightningOpenChannelDialog
     88 from .uix.dialogs.lightning_channels import LightningChannelsDialog, SwapDialog
     89 
     90 if TYPE_CHECKING:
     91     from . import ElectrumGui
     92     from electrum.simple_config import SimpleConfig
     93     from electrum.plugin import Plugins
     94     from electrum.paymentrequest import PaymentRequest
     95 
     96 
     97 class ElectrumWindow(App, Logger):
     98 
     99     electrum_config = ObjectProperty(None)
    100     language = StringProperty('en')
    101 
    102     # properties might be updated by the network
    103     num_blocks = NumericProperty(0)
    104     num_nodes = NumericProperty(0)
    105     server_host = StringProperty('')
    106     server_port = StringProperty('')
    107     num_chains = NumericProperty(0)
    108     blockchain_name = StringProperty('')
    109     fee_status = StringProperty('Fee')
    110     balance = StringProperty('')
    111     fiat_balance = StringProperty('')
    112     is_fiat = BooleanProperty(False)
    113     blockchain_forkpoint = NumericProperty(0)
    114 
    115     lightning_gossip_num_peers = NumericProperty(0)
    116     lightning_gossip_num_nodes = NumericProperty(0)
    117     lightning_gossip_num_channels = NumericProperty(0)
    118     lightning_gossip_num_queries = NumericProperty(0)
    119 
    120     auto_connect = BooleanProperty(False)
    121     def on_auto_connect(self, instance, x):
    122         net_params = self.network.get_parameters()
    123         net_params = net_params._replace(auto_connect=self.auto_connect)
    124         self.network.run_from_another_thread(self.network.set_parameters(net_params))
    125     def toggle_auto_connect(self, x):
    126         self.auto_connect = not self.auto_connect
    127 
    128     oneserver = BooleanProperty(False)
    129     def on_oneserver(self, instance, x):
    130         net_params = self.network.get_parameters()
    131         net_params = net_params._replace(oneserver=self.oneserver)
    132         self.network.run_from_another_thread(self.network.set_parameters(net_params))
    133     def toggle_oneserver(self, x):
    134         self.oneserver = not self.oneserver
    135 
    136     proxy_str = StringProperty('')
    137     def update_proxy_str(self, proxy: dict):
    138         mode = proxy.get('mode')
    139         host = proxy.get('host')
    140         port = proxy.get('port')
    141         self.proxy_str = (host + ':' + port) if mode else _('None')
    142 
    143     def choose_server_dialog(self, popup):
    144         from .uix.dialogs.choice_dialog import ChoiceDialog
    145         protocol = PREFERRED_NETWORK_PROTOCOL
    146         def cb2(server_str):
    147             popup.ids.server_str.text = server_str
    148         servers = self.network.get_servers()
    149         server_choices = {}
    150         for _host, d in sorted(servers.items()):
    151             port = d.get(protocol)
    152             if port:
    153                 server = ServerAddr(_host, port, protocol=protocol)
    154                 server_choices[server.net_addr_str()] = _host
    155         ChoiceDialog(_('Choose a server'), server_choices, popup.ids.server_str.text, cb2).open()
    156 
    157     def maybe_switch_to_server(self, server_str: str):
    158         net_params = self.network.get_parameters()
    159         try:
    160             server = ServerAddr.from_str_with_inference(server_str)
    161             if not server: raise Exception("failed to parse")
    162         except Exception as e:
    163             self.show_error(_("Invalid server details: {}").format(repr(e)))
    164             return
    165         net_params = net_params._replace(server=server)
    166         self.network.run_from_another_thread(self.network.set_parameters(net_params))
    167 
    168     def choose_blockchain_dialog(self, dt):
    169         from .uix.dialogs.choice_dialog import ChoiceDialog
    170         chains = self.network.get_blockchains()
    171         def cb(name):
    172             with blockchain.blockchains_lock: blockchain_items = list(blockchain.blockchains.items())
    173             for chain_id, b in blockchain_items:
    174                 if name == b.get_name():
    175                     self.network.run_from_another_thread(self.network.follow_chain_given_id(chain_id))
    176         chain_objects = [blockchain.blockchains.get(chain_id) for chain_id in chains]
    177         chain_objects = filter(lambda b: b is not None, chain_objects)
    178         names = [b.get_name() for b in chain_objects]
    179         if len(names) > 1:
    180             cur_chain = self.network.blockchain().get_name()
    181             ChoiceDialog(_('Choose your chain'), names, cur_chain, cb).open()
    182 
    183     use_rbf = BooleanProperty(False)
    184     def on_use_rbf(self, instance, x):
    185         self.electrum_config.set_key('use_rbf', self.use_rbf, True)
    186 
    187     use_gossip = BooleanProperty(False)
    188     def on_use_gossip(self, instance, x):
    189         self.electrum_config.set_key('use_gossip', self.use_gossip, True)
    190         if self.use_gossip:
    191             self.network.start_gossip()
    192         else:
    193             self.network.run_from_another_thread(
    194                 self.network.stop_gossip())
    195 
    196     android_backups = BooleanProperty(False)
    197     def on_android_backups(self, instance, x):
    198         self.electrum_config.set_key('android_backups', self.android_backups, True)
    199 
    200     use_change = BooleanProperty(False)
    201     def on_use_change(self, instance, x):
    202         if self.wallet:
    203             self.wallet.use_change = self.use_change
    204             self.wallet.db.put('use_change', self.use_change)
    205             self.wallet.save_db()
    206 
    207     use_unconfirmed = BooleanProperty(False)
    208     def on_use_unconfirmed(self, instance, x):
    209         self.electrum_config.set_key('confirmed_only', not self.use_unconfirmed, True)
    210 
    211     def switch_to_send_screen(func):
    212         # try until send_screen is available
    213         def wrapper(self, *args):
    214             f = lambda dt: (bool(func(self, *args) and False) if self.send_screen else bool(self.switch_to('send') or True)) if self.wallet else True
    215             Clock.schedule_interval(f, 0.1)
    216         return wrapper
    217 
    218     @switch_to_send_screen
    219     def set_URI(self, uri):
    220         self.send_screen.set_URI(uri)
    221 
    222     @switch_to_send_screen
    223     def set_ln_invoice(self, invoice):
    224         self.send_screen.set_ln_invoice(invoice)
    225 
    226     def on_new_intent(self, intent):
    227         data = str(intent.getDataString())
    228         scheme = str(intent.getScheme()).lower()
    229         if scheme == BITCOIN_BIP21_URI_SCHEME:
    230             self.set_URI(data)
    231         elif scheme == LIGHTNING_URI_SCHEME:
    232             self.set_ln_invoice(data)
    233 
    234     def on_language(self, instance, language):
    235         self.logger.info('language: {}'.format(language))
    236         _.switch_lang(language)
    237 
    238     def update_history(self, *dt):
    239         if self.history_screen:
    240             self.history_screen.update()
    241 
    242     def on_quotes(self, d):
    243         self.logger.info("on_quotes")
    244         self._trigger_update_status()
    245         self._trigger_update_history()
    246 
    247     def on_history(self, d):
    248         self.logger.info("on_history")
    249         if self.wallet:
    250             self.wallet.clear_coin_price_cache()
    251         self._trigger_update_history()
    252 
    253     def on_fee_histogram(self, *args):
    254         self._trigger_update_history()
    255 
    256     def on_request_status(self, event, wallet, key, status):
    257         req = self.wallet.receive_requests.get(key)
    258         if req is None:
    259             return
    260         if self.receive_screen:
    261             if status == PR_PAID:
    262                 self.receive_screen.update()
    263             else:
    264                 self.receive_screen.update_item(key, req)
    265         if self.request_popup and self.request_popup.key == key:
    266             self.request_popup.update_status()
    267         if status == PR_PAID:
    268             self.show_info(_('Payment Received') + '\n' + key)
    269             self._trigger_update_history()
    270 
    271     def on_invoice_status(self, event, wallet, key):
    272         req = self.wallet.get_invoice(key)
    273         if req is None:
    274             return
    275         status = self.wallet.get_invoice_status(req)
    276         if self.send_screen:
    277             if status == PR_PAID:
    278                 self.send_screen.update()
    279             else:
    280                 self.send_screen.update_item(key, req)
    281 
    282         if self.invoice_popup and self.invoice_popup.key == key:
    283             self.invoice_popup.update_status()
    284 
    285     def on_payment_succeeded(self, event, wallet, key):
    286         description = self.wallet.get_label(key)
    287         self.show_info(_('Payment succeeded') + '\n\n' + description)
    288         self._trigger_update_history()
    289 
    290     def on_payment_failed(self, event, wallet, key, reason):
    291         self.show_info(_('Payment failed') + '\n\n' + reason)
    292 
    293     def _get_bu(self):
    294         return self.electrum_config.get_base_unit()
    295 
    296     def _set_bu(self, value):
    297         self.electrum_config.set_base_unit(value)
    298         self._trigger_update_status()
    299         self._trigger_update_history()
    300 
    301     wallet_name = StringProperty(_('No Wallet'))
    302     base_unit = AliasProperty(_get_bu, _set_bu)
    303     fiat_unit = StringProperty('')
    304 
    305     def on_fiat_unit(self, a, b):
    306         self._trigger_update_history()
    307 
    308     def decimal_point(self):
    309         return self.electrum_config.get_decimal_point()
    310 
    311     def btc_to_fiat(self, amount_str):
    312         if not amount_str:
    313             return ''
    314         if not self.fx.is_enabled():
    315             return ''
    316         rate = self.fx.exchange_rate()
    317         if rate.is_nan():
    318             return ''
    319         fiat_amount = self.get_amount(amount_str + ' ' + self.base_unit) * rate / pow(10, 8)
    320         return "{:.2f}".format(fiat_amount).rstrip('0').rstrip('.')
    321 
    322     def fiat_to_btc(self, fiat_amount):
    323         if not fiat_amount:
    324             return ''
    325         rate = self.fx.exchange_rate()
    326         if rate.is_nan():
    327             return ''
    328         satoshis = int(pow(10,8) * Decimal(fiat_amount) / Decimal(rate))
    329         return format_satoshis_plain(satoshis, decimal_point=self.decimal_point())
    330 
    331     def get_amount(self, amount_str):
    332         a, u = amount_str.split()
    333         assert u == self.base_unit
    334         try:
    335             x = Decimal(a)
    336         except:
    337             return None
    338         p = pow(10, self.decimal_point())
    339         return int(p * x)
    340 
    341 
    342     _orientation = OptionProperty('landscape',
    343                                  options=('landscape', 'portrait'))
    344 
    345     def _get_orientation(self):
    346         return self._orientation
    347 
    348     orientation = AliasProperty(_get_orientation,
    349                                 None,
    350                                 bind=('_orientation',))
    351     '''Tries to ascertain the kind of device the app is running on.
    352     Cane be one of `tablet` or `phone`.
    353 
    354     :data:`orientation` is a read only `AliasProperty` Defaults to 'landscape'
    355     '''
    356 
    357     _ui_mode = OptionProperty('phone', options=('tablet', 'phone'))
    358 
    359     def _get_ui_mode(self):
    360         return self._ui_mode
    361 
    362     ui_mode = AliasProperty(_get_ui_mode,
    363                             None,
    364                             bind=('_ui_mode',))
    365     '''Defines tries to ascertain the kind of device the app is running on.
    366     Cane be one of `tablet` or `phone`.
    367 
    368     :data:`ui_mode` is a read only `AliasProperty` Defaults to 'phone'
    369     '''
    370 
    371     def __init__(self, **kwargs):
    372         # initialize variables
    373         self._clipboard = Clipboard
    374         self.info_bubble = None
    375         self.nfcscanner = None
    376         self.tabs = None
    377         self.is_exit = False
    378         self.wallet = None  # type: Optional[Abstract_Wallet]
    379         self.pause_time = 0
    380         self.asyncio_loop = asyncio.get_event_loop()
    381         self.password = None
    382         self._use_single_password = False
    383 
    384         App.__init__(self)#, **kwargs)
    385         Logger.__init__(self)
    386 
    387         self.electrum_config = config = kwargs.get('config', None)  # type: SimpleConfig
    388         self.language = config.get('language', 'en')
    389         self.network = network = kwargs.get('network', None)  # type: Network
    390         if self.network:
    391             self.num_blocks = self.network.get_local_height()
    392             self.num_nodes = len(self.network.get_interfaces())
    393             net_params = self.network.get_parameters()
    394             self.server_host = net_params.server.host
    395             self.server_port = str(net_params.server.port)
    396             self.auto_connect = net_params.auto_connect
    397             self.oneserver = net_params.oneserver
    398             self.proxy_config = net_params.proxy if net_params.proxy else {}
    399             self.update_proxy_str(self.proxy_config)
    400 
    401         self.plugins = kwargs.get('plugins', None)  # type: Plugins
    402         self.gui_object = kwargs.get('gui_object', None)  # type: ElectrumGui
    403         self.daemon = self.gui_object.daemon
    404         self.fx = self.daemon.fx
    405         self.use_rbf = config.get('use_rbf', True)
    406         self.android_backups = config.get('android_backups', False)
    407         self.use_gossip = config.get('use_gossip', False)
    408         self.use_unconfirmed = not config.get('confirmed_only', False)
    409 
    410         # create triggers so as to minimize updating a max of 2 times a sec
    411         self._trigger_update_wallet = Clock.create_trigger(self.update_wallet, .5)
    412         self._trigger_update_status = Clock.create_trigger(self.update_status, .5)
    413         self._trigger_update_history = Clock.create_trigger(self.update_history, .5)
    414         self._trigger_update_interfaces = Clock.create_trigger(self.update_interfaces, .5)
    415 
    416         self._periodic_update_status_during_sync = Clock.schedule_interval(self.update_wallet_synchronizing_progress, .5)
    417 
    418         # cached dialogs
    419         self._settings_dialog = None
    420         self._channels_dialog = None
    421         self._addresses_dialog = None
    422         self.set_fee_status()
    423         self.invoice_popup = None
    424         self.request_popup = None
    425 
    426     def on_pr(self, pr: 'PaymentRequest'):
    427         if not self.wallet:
    428             self.show_error(_('No wallet loaded.'))
    429             return
    430         if pr.verify(self.wallet.contacts):
    431             key = pr.get_id()
    432             invoice = self.wallet.get_invoice(key)  # FIXME wrong key...
    433             if invoice and self.wallet.get_invoice_status(invoice) == PR_PAID:
    434                 self.show_error("invoice already paid")
    435                 self.send_screen.do_clear()
    436             elif pr.has_expired():
    437                 self.show_error(_('Payment request has expired'))
    438             else:
    439                 self.switch_to('send')
    440                 self.send_screen.set_request(pr)
    441         else:
    442             self.show_error("invoice error:" + pr.error)
    443             self.send_screen.do_clear()
    444 
    445     def on_qr(self, data):
    446         from electrum.bitcoin import is_address
    447         data = data.strip()
    448         if is_address(data):
    449             self.set_URI(data)
    450             return
    451         if data.lower().startswith(BITCOIN_BIP21_URI_SCHEME + ':'):
    452             self.set_URI(data)
    453             return
    454         if data.lower().startswith('channel_backup:'):
    455             self.import_channel_backup(data)
    456             return
    457         bolt11_invoice = maybe_extract_bolt11_invoice(data)
    458         if bolt11_invoice is not None:
    459             self.set_ln_invoice(bolt11_invoice)
    460             return
    461         # try to decode transaction
    462         from electrum.transaction import tx_from_any
    463         try:
    464             tx = tx_from_any(data)
    465         except:
    466             tx = None
    467         if tx:
    468             self.tx_dialog(tx)
    469             return
    470         # show error
    471         self.show_error("Unable to decode QR data")
    472 
    473     def update_tab(self, name):
    474         s = getattr(self, name + '_screen', None)
    475         if s:
    476             s.update()
    477 
    478     @profiler
    479     def update_tabs(self):
    480         for name in ['send', 'history', 'receive']:
    481             self.update_tab(name)
    482 
    483     def switch_to(self, name):
    484         s = getattr(self, name + '_screen', None)
    485         panel = self.tabs.ids.panel
    486         tab = self.tabs.ids[name + '_tab']
    487         panel.switch_to(tab)
    488 
    489     def show_request(self, is_lightning, key):
    490         from .uix.dialogs.request_dialog import RequestDialog
    491         self.request_popup = RequestDialog('Request', key)
    492         self.request_popup.open()
    493 
    494     def show_invoice(self, is_lightning, key):
    495         from .uix.dialogs.invoice_dialog import InvoiceDialog
    496         invoice = self.wallet.get_invoice(key)
    497         if not invoice:
    498             return
    499         data = invoice.invoice if is_lightning else key
    500         self.invoice_popup = InvoiceDialog('Invoice', data, key)
    501         self.invoice_popup.open()
    502 
    503     def qr_dialog(self, title, data, show_text=False, text_for_clipboard=None, help_text=None):
    504         from .uix.dialogs.qr_dialog import QRDialog
    505         def on_qr_failure():
    506             popup.dismiss()
    507             msg = _('Failed to display QR code.')
    508             if text_for_clipboard:
    509                 msg += '\n' + _('Text copied to clipboard.')
    510                 self._clipboard.copy(text_for_clipboard)
    511             Clock.schedule_once(lambda dt: self.show_info(msg))
    512         popup = QRDialog(
    513             title, data, show_text,
    514             failure_cb=on_qr_failure,
    515             text_for_clipboard=text_for_clipboard,
    516             help_text=help_text)
    517         popup.open()
    518 
    519     def scan_qr(self, on_complete):
    520         if platform != 'android':
    521             return self.scan_qr_non_android(on_complete)
    522         from jnius import autoclass, cast
    523         from android import activity
    524         PythonActivity = autoclass('org.kivy.android.PythonActivity')
    525         SimpleScannerActivity = autoclass("org.electrum.qr.SimpleScannerActivity")
    526         Intent = autoclass('android.content.Intent')
    527         intent = Intent(PythonActivity.mActivity, SimpleScannerActivity)
    528 
    529         def on_qr_result(requestCode, resultCode, intent):
    530             try:
    531                 if resultCode == -1:  # RESULT_OK:
    532                     #  this doesn't work due to some bug in jnius:
    533                     # contents = intent.getStringExtra("text")
    534                     String = autoclass("java.lang.String")
    535                     contents = intent.getStringExtra(String("text"))
    536                     on_complete(contents)
    537             except Exception as e:  # exc would otherwise get lost
    538                 send_exception_to_crash_reporter(e)
    539             finally:
    540                 activity.unbind(on_activity_result=on_qr_result)
    541         activity.bind(on_activity_result=on_qr_result)
    542         PythonActivity.mActivity.startActivityForResult(intent, 0)
    543 
    544     def scan_qr_non_android(self, on_complete):
    545         from electrum import qrscanner
    546         try:
    547             video_dev = self.electrum_config.get_video_device()
    548             data = qrscanner.scan_barcode(video_dev)
    549             on_complete(data)
    550         except UserFacingException as e:
    551             self.show_error(e)
    552         except BaseException as e:
    553             self.logger.exception('camera error')
    554             self.show_error(repr(e))
    555 
    556     def do_share(self, data, title):
    557         if platform != 'android':
    558             return
    559         from jnius import autoclass, cast
    560         JS = autoclass('java.lang.String')
    561         Intent = autoclass('android.content.Intent')
    562         sendIntent = Intent()
    563         sendIntent.setAction(Intent.ACTION_SEND)
    564         sendIntent.setType("text/plain")
    565         sendIntent.putExtra(Intent.EXTRA_TEXT, JS(data))
    566         PythonActivity = autoclass('org.kivy.android.PythonActivity')
    567         currentActivity = cast('android.app.Activity', PythonActivity.mActivity)
    568         it = Intent.createChooser(sendIntent, cast('java.lang.CharSequence', JS(title)))
    569         currentActivity.startActivity(it)
    570 
    571     def build(self):
    572         return Builder.load_file(KIVY_GUI_PATH + '/main.kv')
    573 
    574     def _pause(self):
    575         if platform == 'android':
    576             # move activity to back
    577             from jnius import autoclass
    578             python_act = autoclass('org.kivy.android.PythonActivity')
    579             mActivity = python_act.mActivity
    580             mActivity.moveTaskToBack(True)
    581 
    582     def handle_crash_on_startup(func):
    583         def wrapper(self, *args, **kwargs):
    584             try:
    585                 return func(self, *args, **kwargs)
    586             except Exception as e:
    587                 self.logger.exception('crash on startup')
    588                 from .uix.dialogs.crash_reporter import CrashReporter
    589                 # show the crash reporter, and when it's closed, shutdown the app
    590                 cr = CrashReporter(self, exctype=type(e), value=e, tb=e.__traceback__)
    591                 cr.on_dismiss = lambda: self.stop()
    592                 Clock.schedule_once(lambda _, cr=cr: cr.open(), 0)
    593         return wrapper
    594 
    595     @handle_crash_on_startup
    596     def on_start(self):
    597         ''' This is the start point of the kivy ui
    598         '''
    599         import time
    600         self.logger.info('Time to on_start: {} <<<<<<<<'.format(time.process_time()))
    601         Window.bind(size=self.on_size, on_keyboard=self.on_keyboard)
    602         #Window.softinput_mode = 'below_target'
    603         self.on_size(Window, Window.size)
    604         self.init_ui()
    605         crash_reporter.ExceptionHook(self)
    606         # init plugins
    607         run_hook('init_kivy', self)
    608         # fiat currency
    609         self.fiat_unit = self.fx.ccy if self.fx.is_enabled() else ''
    610         # default tab
    611         self.switch_to('history')
    612         # bind intent for bitcoin: URI scheme
    613         if platform == 'android':
    614             from android import activity
    615             from jnius import autoclass
    616             PythonActivity = autoclass('org.kivy.android.PythonActivity')
    617             mactivity = PythonActivity.mActivity
    618             self.on_new_intent(mactivity.getIntent())
    619             activity.bind(on_new_intent=self.on_new_intent)
    620         # connect callbacks
    621         if self.network:
    622             interests = ['wallet_updated', 'network_updated', 'blockchain_updated',
    623                          'status', 'new_transaction', 'verified']
    624             util.register_callback(self.on_network_event, interests)
    625             util.register_callback(self.on_fee, ['fee'])
    626             util.register_callback(self.on_fee_histogram, ['fee_histogram'])
    627             util.register_callback(self.on_quotes, ['on_quotes'])
    628             util.register_callback(self.on_history, ['on_history'])
    629             util.register_callback(self.on_channels, ['channels_updated'])
    630             util.register_callback(self.on_channel, ['channel'])
    631             util.register_callback(self.on_invoice_status, ['invoice_status'])
    632             util.register_callback(self.on_request_status, ['request_status'])
    633             util.register_callback(self.on_payment_failed, ['payment_failed'])
    634             util.register_callback(self.on_payment_succeeded, ['payment_succeeded'])
    635             util.register_callback(self.on_channel_db, ['channel_db'])
    636             util.register_callback(self.set_num_peers, ['gossip_peers'])
    637             util.register_callback(self.set_unknown_channels, ['unknown_channels'])
    638         # load wallet
    639         self.load_wallet_by_name(self.electrum_config.get_wallet_path(use_gui_last_wallet=True))
    640         # URI passed in config
    641         uri = self.electrum_config.get('url')
    642         if uri:
    643             self.set_URI(uri)
    644 
    645     def on_channel_db(self, event, num_nodes, num_channels, num_policies):
    646         self.lightning_gossip_num_nodes = num_nodes
    647         self.lightning_gossip_num_channels = num_channels
    648 
    649     def set_num_peers(self, event, num_peers):
    650         self.lightning_gossip_num_peers = num_peers
    651 
    652     def set_unknown_channels(self, event, unknown):
    653         self.lightning_gossip_num_queries = unknown
    654 
    655     def get_wallet_path(self):
    656         if self.wallet:
    657             return self.wallet.storage.path
    658         else:
    659             return ''
    660 
    661     def on_wizard_success(self, storage, db, password):
    662         self.password = password
    663         if self.electrum_config.get('single_password'):
    664             self._use_single_password = check_password_for_directory(self.electrum_config, password)
    665         self.logger.info(f'use single password: {self._use_single_password}')
    666         wallet = Wallet(db, storage, config=self.electrum_config)
    667         wallet.start_network(self.daemon.network)
    668         self.daemon.add_wallet(wallet)
    669         self.load_wallet(wallet)
    670 
    671     def on_wizard_aborted(self):
    672         # wizard did not return a wallet; and there is no wallet open atm
    673         if not self.wallet:
    674             self.stop()
    675 
    676     def load_wallet_by_name(self, path):
    677         if not path:
    678             return
    679         if self.wallet and self.wallet.storage.path == path:
    680             return
    681         if self.password and self._use_single_password:
    682             storage = WalletStorage(path)
    683             # call check_password to decrypt
    684             storage.check_password(self.password)
    685             self.on_open_wallet(self.password, storage)
    686             return
    687         d = OpenWalletDialog(self, path, self.on_open_wallet)
    688         d.open()
    689 
    690     def on_open_wallet(self, password, storage):
    691         if not storage.file_exists():
    692             wizard = InstallWizard(self.electrum_config, self.plugins)
    693             wizard.path = storage.path
    694             wizard.run('new')
    695         else:
    696             assert storage.is_past_initial_decryption()
    697             db = WalletDB(storage.read(), manual_upgrades=False)
    698             assert not db.requires_upgrade()
    699             self.on_wizard_success(storage, db, password)
    700 
    701     def on_stop(self):
    702         self.logger.info('on_stop')
    703         self.stop_wallet()
    704 
    705     def stop_wallet(self):
    706         if self.wallet:
    707             self.daemon.stop_wallet(self.wallet.storage.path)
    708             self.wallet = None
    709 
    710     def on_keyboard(self, instance, key, keycode, codepoint, modifiers):
    711         if key == 27 and self.is_exit is False:
    712             self.is_exit = True
    713             self.show_info(_('Press again to exit'))
    714             return True
    715         # override settings button
    716         if key in (319, 282): #f1/settings button on android
    717             #self.gui.main_gui.toggle_settings(self)
    718             return True
    719 
    720     def settings_dialog(self):
    721         from .uix.dialogs.settings import SettingsDialog
    722         if self._settings_dialog is None:
    723             self._settings_dialog = SettingsDialog(self)
    724         self._settings_dialog.update()
    725         self._settings_dialog.open()
    726 
    727     def lightning_open_channel_dialog(self):
    728         if not self.wallet.has_lightning():
    729             self.show_error(_('Lightning is not enabled for this wallet'))
    730             return
    731         if not self.wallet.lnworker.channels:
    732             warning1 = _("Lightning support in Electrum is experimental. "
    733                          "Do not put large amounts in lightning channels.")
    734             warning2 = _("Funds stored in lightning channels are not recoverable "
    735                          "from your seed. You must backup your wallet file everytime "
    736                          "you create a new channel.")
    737             d = Question(_('Do you want to create your first channel?') +
    738                          '\n\n' + warning1 + '\n\n' + warning2, self.open_channel_dialog_with_warning)
    739             d.open()
    740         else:
    741             d = LightningOpenChannelDialog(self)
    742             d.open()
    743 
    744     def swap_dialog(self):
    745         d = SwapDialog(self, self.electrum_config)
    746         d.open()
    747 
    748     def open_channel_dialog_with_warning(self, b):
    749         if b:
    750             d = LightningOpenChannelDialog(self)
    751             d.open()
    752 
    753     def lightning_channels_dialog(self):
    754         if self._channels_dialog is None:
    755             self._channels_dialog = LightningChannelsDialog(self)
    756         self._channels_dialog.open()
    757 
    758     def on_channel(self, evt, wallet, chan):
    759         if self._channels_dialog:
    760             Clock.schedule_once(lambda dt: self._channels_dialog.update())
    761 
    762     def on_channels(self, evt, wallet):
    763         if self._channels_dialog:
    764             Clock.schedule_once(lambda dt: self._channels_dialog.update())
    765 
    766     def is_wallet_creation_disabled(self):
    767         return bool(self.electrum_config.get('single_password')) and self.password is None
    768 
    769     def wallets_dialog(self):
    770         from .uix.dialogs.wallets import WalletDialog
    771         dirname = os.path.dirname(self.electrum_config.get_wallet_path())
    772         d = WalletDialog(dirname, self.load_wallet_by_name, self.is_wallet_creation_disabled())
    773         d.open()
    774 
    775     def popup_dialog(self, name):
    776         if name == 'settings':
    777             self.settings_dialog()
    778         elif name == 'wallets':
    779             self.wallets_dialog()
    780         elif name == 'status':
    781             popup = Builder.load_file(KIVY_GUI_PATH + f'/uix/ui_screens/{name}.kv')
    782             master_public_keys_layout = popup.ids.master_public_keys
    783             for xpub in self.wallet.get_master_public_keys()[1:]:
    784                 master_public_keys_layout.add_widget(TopLabel(text=_('Master Public Key')))
    785                 ref = RefLabel()
    786                 ref.name = _('Master Public Key')
    787                 ref.data = xpub
    788                 master_public_keys_layout.add_widget(ref)
    789             popup.open()
    790         elif name == 'lightning_channels_dialog' and not self.wallet.can_have_lightning():
    791             self.show_error(_("Not available for this wallet.") + "\n\n" +
    792                             _("Lightning is currently restricted to HD wallets with p2wpkh addresses."))
    793         elif name.endswith("_dialog"):
    794             getattr(self, name)()
    795         else:
    796             popup = Builder.load_file(KIVY_GUI_PATH + f'/uix/ui_screens/{name}.kv')
    797             popup.open()
    798 
    799     @profiler
    800     def init_ui(self):
    801         ''' Initialize The Ux part of electrum. This function performs the basic
    802         tasks of setting up the ui.
    803         '''
    804         #from weakref import ref
    805 
    806         self.funds_error = False
    807         # setup UX
    808         self.screens = {}
    809 
    810         #setup lazy imports for mainscreen
    811         Factory.register('AnimatedPopup',
    812                          module='electrum.gui.kivy.uix.dialogs')
    813         Factory.register('QRCodeWidget',
    814                          module='electrum.gui.kivy.uix.qrcodewidget')
    815 
    816         # preload widgets. Remove this if you want to load the widgets on demand
    817         #Cache.append('electrum_widgets', 'AnimatedPopup', Factory.AnimatedPopup())
    818         #Cache.append('electrum_widgets', 'QRCodeWidget', Factory.QRCodeWidget())
    819 
    820         # load and focus the ui
    821         self.root.manager = self.root.ids['manager']
    822 
    823         self.history_screen = None
    824         self.send_screen = None
    825         self.receive_screen = None
    826         self.icon = os.path.dirname(KIVY_GUI_PATH) + "/icons/electrum.png"
    827         self.tabs = self.root.ids['tabs']
    828 
    829     def update_interfaces(self, dt):
    830         net_params = self.network.get_parameters()
    831         self.num_nodes = len(self.network.get_interfaces())
    832         self.num_chains = len(self.network.get_blockchains())
    833         chain = self.network.blockchain()
    834         self.blockchain_forkpoint = chain.get_max_forkpoint()
    835         self.blockchain_name = chain.get_name()
    836         interface = self.network.interface
    837         if interface:
    838             self.server_host = interface.host
    839         else:
    840             self.server_host = str(net_params.server.host) + ' (connecting...)'
    841         self.proxy_config = net_params.proxy or {}
    842         self.update_proxy_str(self.proxy_config)
    843 
    844     def on_network_event(self, event, *args):
    845         self.logger.info('network event: '+ event)
    846         if event == 'network_updated':
    847             self._trigger_update_interfaces()
    848             self._trigger_update_status()
    849         elif event == 'wallet_updated':
    850             self._trigger_update_wallet()
    851             self._trigger_update_status()
    852         elif event == 'blockchain_updated':
    853             # to update number of confirmations in history
    854             self._trigger_update_wallet()
    855         elif event == 'status':
    856             self._trigger_update_status()
    857         elif event == 'new_transaction':
    858             self._trigger_update_wallet()
    859         elif event == 'verified':
    860             self._trigger_update_wallet()
    861 
    862     @profiler
    863     def load_wallet(self, wallet: 'Abstract_Wallet'):
    864         if self.wallet:
    865             self.stop_wallet()
    866         self.wallet = wallet
    867         self.wallet_name = wallet.basename()
    868         self.update_wallet()
    869         # Once GUI has been initialized check if we want to announce something
    870         # since the callback has been called before the GUI was initialized
    871         if self.receive_screen:
    872             self.receive_screen.clear()
    873         self.update_tabs()
    874         run_hook('load_wallet', wallet, self)
    875         try:
    876             wallet.try_detecting_internal_addresses_corruption()
    877         except InternalAddressCorruption as e:
    878             self.show_error(str(e))
    879             send_exception_to_crash_reporter(e)
    880             return
    881         self.use_change = self.wallet.use_change
    882         self.electrum_config.save_last_wallet(wallet)
    883         self.request_focus_for_main_view()
    884 
    885     def request_focus_for_main_view(self):
    886         if platform != 'android':
    887             return
    888         # The main view of the activity might be not have focus
    889         # in which case e.g. the OS "back" button would not work.
    890         # see #6276 (specifically "method 2" and "method 3")
    891         from jnius import autoclass
    892         PythonActivity = autoclass('org.kivy.android.PythonActivity')
    893         PythonActivity.requestFocusForMainView()
    894 
    895     def update_status(self, *dt):
    896         if not self.wallet:
    897             return
    898         if self.network is None or not self.network.is_connected():
    899             status = _("Offline")
    900         elif self.network.is_connected():
    901             self.num_blocks = self.network.get_local_height()
    902             server_height = self.network.get_server_height()
    903             server_lag = self.num_blocks - server_height
    904             if not self.wallet.up_to_date or server_height == 0:
    905                 num_sent, num_answered = self.wallet.get_history_sync_state_details()
    906                 status = ("{} [size=18dp]({}/{})[/size]"
    907                           .format(_("Synchronizing..."), num_answered, num_sent))
    908             elif server_lag > 1:
    909                 status = _("Server is lagging ({} blocks)").format(server_lag)
    910             else:
    911                 status = ''
    912         else:
    913             status = _("Disconnected")
    914         if status:
    915             self.balance = status
    916             self.fiat_balance = status
    917         else:
    918             c, u, x = self.wallet.get_balance()
    919             l = int(self.wallet.lnworker.get_balance()) if self.wallet.lnworker else 0
    920             balance_sat = c + u + x + l
    921             text = self.format_amount(balance_sat)
    922             self.balance = str(text.strip()) + ' [size=22dp]%s[/size]'% self.base_unit
    923             self.fiat_balance = self.fx.format_amount(balance_sat) + ' [size=22dp]%s[/size]'% self.fx.ccy
    924 
    925     def update_wallet_synchronizing_progress(self, *dt):
    926         if not self.wallet:
    927             return
    928         if not self.wallet.up_to_date:
    929             self._trigger_update_status()
    930 
    931     def get_max_amount(self):
    932         from electrum.transaction import PartialTxOutput
    933         if run_hook('abort_send', self):
    934             return ''
    935         inputs = self.wallet.get_spendable_coins(None)
    936         if not inputs:
    937             return ''
    938         addr = None
    939         if self.send_screen:
    940             addr = str(self.send_screen.address)
    941         if not addr:
    942             addr = self.wallet.dummy_address()
    943         outputs = [PartialTxOutput.from_address_and_value(addr, '!')]
    944         try:
    945             tx = self.wallet.make_unsigned_transaction(coins=inputs, outputs=outputs)
    946         except NoDynamicFeeEstimates as e:
    947             Clock.schedule_once(lambda dt, bound_e=e: self.show_error(str(bound_e)))
    948             return ''
    949         except NotEnoughFunds:
    950             return ''
    951         except InternalAddressCorruption as e:
    952             self.show_error(str(e))
    953             send_exception_to_crash_reporter(e)
    954             return ''
    955         amount = tx.output_value()
    956         __, x_fee_amount = run_hook('get_tx_extra_fee', self.wallet, tx) or (None, 0)
    957         amount_after_all_fees = amount - x_fee_amount
    958         return format_satoshis_plain(amount_after_all_fees, decimal_point=self.decimal_point())
    959 
    960     def format_amount(self, x, is_diff=False, whitespaces=False):
    961         return format_satoshis(
    962             x,
    963             num_zeros=0,
    964             decimal_point=self.decimal_point(),
    965             is_diff=is_diff,
    966             whitespaces=whitespaces,
    967         )
    968 
    969     def format_amount_and_units(self, x) -> str:
    970         if x is None:
    971             return 'none'
    972         if x == '!':
    973             return 'max'
    974         return format_satoshis_plain(x, decimal_point=self.decimal_point()) + ' ' + self.base_unit
    975 
    976     def format_fee_rate(self, fee_rate):
    977         # fee_rate is in sat/kB
    978         return format_fee_satoshis(fee_rate/1000) + ' sat/byte'
    979 
    980     #@profiler
    981     def update_wallet(self, *dt):
    982         self._trigger_update_status()
    983         if self.wallet and (self.wallet.up_to_date or not self.network or not self.network.is_connected()):
    984             self.update_tabs()
    985 
    986     def notify(self, message):
    987         try:
    988             global notification, os
    989             if not notification:
    990                 from plyer import notification
    991             icon = (os.path.dirname(os.path.realpath(__file__))
    992                     + '/../../' + self.icon)
    993             notification.notify('Electrum', message,
    994                             app_icon=icon, app_name='Electrum')
    995         except ImportError:
    996             self.logger.Error('Notification: needs plyer; `sudo python3 -m pip install plyer`')
    997 
    998     def on_pause(self):
    999         self.pause_time = time.time()
   1000         # pause nfc
   1001         if self.nfcscanner:
   1002             self.nfcscanner.nfc_disable()
   1003         return True
   1004 
   1005     def on_resume(self):
   1006         now = time.time()
   1007         if self.wallet and self.has_pin_code() and now - self.pause_time > 5*60:
   1008             d = PincodeDialog(
   1009                 self,
   1010                 check_password=self.check_pin_code,
   1011                 on_success=None,
   1012                 on_failure=self.stop)
   1013             d.open()
   1014         if self.nfcscanner:
   1015             self.nfcscanner.nfc_enable()
   1016 
   1017     def on_size(self, instance, value):
   1018         width, height = value
   1019         self._orientation = 'landscape' if width > height else 'portrait'
   1020         self._ui_mode = 'tablet' if min(width, height) > inch(3.51) else 'phone'
   1021 
   1022     def on_ref_label(self, label, *, show_text_with_qr: bool = True):
   1023         if not label.data:
   1024             return
   1025         self.qr_dialog(label.name, label.data, show_text_with_qr)
   1026 
   1027     def show_error(self, error, width='200dp', pos=None, arrow_pos=None,
   1028                    exit=False, icon=f'atlas://{KIVY_GUI_PATH}/theming/light/error', duration=0,
   1029                    modal=False):
   1030         ''' Show an error Message Bubble.
   1031         '''
   1032         self.show_info_bubble( text=error, icon=icon, width=width,
   1033             pos=pos or Window.center, arrow_pos=arrow_pos, exit=exit,
   1034             duration=duration, modal=modal)
   1035 
   1036     def show_info(self, error, width='200dp', pos=None, arrow_pos=None,
   1037                   exit=False, duration=0, modal=False):
   1038         ''' Show an Info Message Bubble.
   1039         '''
   1040         self.show_error(error, icon=f'atlas://{KIVY_GUI_PATH}/theming/light/important',
   1041             duration=duration, modal=modal, exit=exit, pos=pos,
   1042             arrow_pos=arrow_pos)
   1043 
   1044     def show_info_bubble(self, text=_('Hello World'), pos=None, duration=0,
   1045                          arrow_pos='bottom_mid', width=None, icon='', modal=False, exit=False):
   1046         '''Method to show an Information Bubble
   1047 
   1048         .. parameters::
   1049             text: Message to be displayed
   1050             pos: position for the bubble
   1051             duration: duration the bubble remains on screen. 0 = click to hide
   1052             width: width of the Bubble
   1053             arrow_pos: arrow position for the bubble
   1054         '''
   1055         text = str(text)  # so that we also handle e.g. Exception
   1056         info_bubble = self.info_bubble
   1057         if not info_bubble:
   1058             info_bubble = self.info_bubble = Factory.InfoBubble()
   1059 
   1060         win = Window
   1061         if info_bubble.parent:
   1062             win.remove_widget(info_bubble
   1063                                  if not info_bubble.modal else
   1064                                  info_bubble._modal_view)
   1065 
   1066         if not arrow_pos:
   1067             info_bubble.show_arrow = False
   1068         else:
   1069             info_bubble.show_arrow = True
   1070             info_bubble.arrow_pos = arrow_pos
   1071         img = info_bubble.ids.img
   1072         if text == 'texture':
   1073             # icon holds a texture not a source image
   1074             # display the texture in full screen
   1075             text = ''
   1076             img.texture = icon
   1077             info_bubble.fs = True
   1078             info_bubble.show_arrow = False
   1079             img.allow_stretch = True
   1080             info_bubble.dim_background = True
   1081             info_bubble.background_image = f'atlas://{KIVY_GUI_PATH}/theming/light/card'
   1082         else:
   1083             info_bubble.fs = False
   1084             info_bubble.icon = icon
   1085             #if img.texture and img._coreimage:
   1086             #    img.reload()
   1087             img.allow_stretch = False
   1088             info_bubble.dim_background = False
   1089             info_bubble.background_image = 'atlas://data/images/defaulttheme/bubble'
   1090         info_bubble.message = text
   1091         if not pos:
   1092             pos = (win.center[0], win.center[1] - (info_bubble.height/2))
   1093         info_bubble.show(pos, duration, width, modal=modal, exit=exit)
   1094 
   1095     def tx_dialog(self, tx):
   1096         from .uix.dialogs.tx_dialog import TxDialog
   1097         d = TxDialog(self, tx)
   1098         d.open()
   1099 
   1100     def show_transaction(self, txid):
   1101         tx = self.wallet.db.get_transaction(txid)
   1102         if not tx and self.wallet.lnworker:
   1103             tx = self.wallet.lnworker.lnwatcher.db.get_transaction(txid)
   1104         if tx:
   1105             self.tx_dialog(tx)
   1106         else:
   1107             self.show_error(f'Transaction not found {txid}')
   1108 
   1109     def lightning_tx_dialog(self, tx):
   1110         from .uix.dialogs.lightning_tx_dialog import LightningTxDialog
   1111         d = LightningTxDialog(self, tx)
   1112         d.open()
   1113 
   1114     def sign_tx(self, *args):
   1115         threading.Thread(target=self._sign_tx, args=args).start()
   1116 
   1117     def _sign_tx(self, tx, password, on_success, on_failure):
   1118         try:
   1119             self.wallet.sign_transaction(tx, password)
   1120         except InvalidPassword:
   1121             Clock.schedule_once(lambda dt: on_failure(_("Invalid PIN")))
   1122             return
   1123         on_success = run_hook('tc_sign_wrapper', self.wallet, tx, on_success, on_failure) or on_success
   1124         Clock.schedule_once(lambda dt: on_success(tx))
   1125 
   1126     def _broadcast_thread(self, tx, on_complete):
   1127         status = False
   1128         try:
   1129             self.network.run_from_another_thread(self.network.broadcast_transaction(tx))
   1130         except TxBroadcastError as e:
   1131             msg = e.get_message_for_gui()
   1132         except BestEffortRequestFailed as e:
   1133             msg = repr(e)
   1134         else:
   1135             status, msg = True, tx.txid()
   1136         Clock.schedule_once(lambda dt: on_complete(status, msg))
   1137 
   1138     def broadcast(self, tx):
   1139         def on_complete(ok, msg):
   1140             if ok:
   1141                 self.show_info(_('Payment sent.'))
   1142                 if self.send_screen:
   1143                     self.send_screen.do_clear()
   1144             else:
   1145                 msg = msg or ''
   1146                 self.show_error(msg)
   1147 
   1148         if self.network and self.network.is_connected():
   1149             self.show_info(_('Sending'))
   1150             threading.Thread(target=self._broadcast_thread, args=(tx, on_complete)).start()
   1151         else:
   1152             self.show_info(_('Cannot broadcast transaction') + ':\n' + _('Not connected'))
   1153 
   1154     def description_dialog(self, screen):
   1155         from .uix.dialogs.label_dialog import LabelDialog
   1156         text = screen.message
   1157         def callback(text):
   1158             screen.message = text
   1159         d = LabelDialog(_('Enter description'), text, callback)
   1160         d.open()
   1161 
   1162     def amount_dialog(self, screen, show_max):
   1163         from .uix.dialogs.amount_dialog import AmountDialog
   1164         amount = screen.amount
   1165         if amount:
   1166             amount, u = str(amount).split()
   1167             assert u == self.base_unit
   1168         def cb(amount):
   1169             if amount == '!':
   1170                 screen.is_max = True
   1171                 max_amt = self.get_max_amount()
   1172                 screen.amount = (max_amt + ' ' + self.base_unit) if max_amt else ''
   1173             else:
   1174                 screen.amount = amount
   1175                 screen.is_max = False
   1176         popup = AmountDialog(show_max, amount, cb)
   1177         popup.open()
   1178 
   1179     def addresses_dialog(self):
   1180         from .uix.dialogs.addresses import AddressesDialog
   1181         if self._addresses_dialog is None:
   1182             self._addresses_dialog = AddressesDialog(self)
   1183         self._addresses_dialog.update()
   1184         self._addresses_dialog.open()
   1185 
   1186     def fee_dialog(self):
   1187         from .uix.dialogs.fee_dialog import FeeDialog
   1188         fee_dialog = FeeDialog(self, self.electrum_config, self.set_fee_status)
   1189         fee_dialog.open()
   1190 
   1191     def set_fee_status(self):
   1192         target, tooltip, dyn = self.electrum_config.get_fee_target()
   1193         self.fee_status = target
   1194 
   1195     def on_fee(self, event, *arg):
   1196         self.set_fee_status()
   1197 
   1198     def protected(self, msg, f, args):
   1199         if self.electrum_config.get('pin_code'):
   1200             msg += "\n" + _("Enter your PIN code to proceed")
   1201             on_success = lambda pw: f(*args, self.password)
   1202             d = PincodeDialog(
   1203                 self,
   1204                 message = msg,
   1205                 check_password=self.check_pin_code,
   1206                 on_success=on_success,
   1207                 on_failure=lambda: None)
   1208             d.open()
   1209         else:
   1210             d = Question(
   1211                 msg,
   1212                 lambda b: f(*args, self.password) if b else None,
   1213                 yes_str=_("OK"),
   1214                 no_str=_("Cancel"),
   1215                 title=_("Confirm action"))
   1216             d.open()
   1217 
   1218     def delete_wallet(self):
   1219         basename = os.path.basename(self.wallet.storage.path)
   1220         d = Question(_('Delete wallet?') + '\n' + basename, self._delete_wallet)
   1221         d.open()
   1222 
   1223     def _delete_wallet(self, b):
   1224         if b:
   1225             basename = self.wallet.basename()
   1226             self.protected(_("Are you sure you want to delete wallet {}?").format(basename),
   1227                            self.__delete_wallet, ())
   1228 
   1229     def __delete_wallet(self, pw):
   1230         wallet_path = self.get_wallet_path()
   1231         basename = os.path.basename(wallet_path)
   1232         if self.wallet.has_password():
   1233             try:
   1234                 self.wallet.check_password(pw)
   1235             except InvalidPassword:
   1236                 self.show_error("Invalid password")
   1237                 return
   1238         self.stop_wallet()
   1239         os.unlink(wallet_path)
   1240         self.show_error(_("Wallet removed: {}").format(basename))
   1241         new_path = self.electrum_config.get_wallet_path(use_gui_last_wallet=True)
   1242         self.load_wallet_by_name(new_path)
   1243 
   1244     def show_seed(self, label):
   1245         self.protected(_("Display your seed?"), self._show_seed, (label,))
   1246 
   1247     def _show_seed(self, label, password):
   1248         if self.wallet.has_password() and password is None:
   1249             return
   1250         keystore = self.wallet.keystore
   1251         seed = keystore.get_seed(password)
   1252         passphrase = keystore.get_passphrase(password)
   1253         label.data = seed
   1254         if passphrase:
   1255             label.data += '\n\n' + _('Passphrase') + ': ' + passphrase
   1256 
   1257     def has_pin_code(self):
   1258         return bool(self.electrum_config.get('pin_code'))
   1259 
   1260     def check_pin_code(self, pin):
   1261         if pin != self.electrum_config.get('pin_code'):
   1262             raise InvalidPassword
   1263 
   1264     def change_password(self, cb):
   1265         def on_success(old_password, new_password):
   1266             # called if old_password works on self.wallet
   1267             self.password = new_password
   1268             if self._use_single_password:
   1269                 path = self.wallet.storage.path
   1270                 self.stop_wallet()
   1271                 update_password_for_directory(self.electrum_config, old_password, new_password)
   1272                 self.load_wallet_by_name(path)
   1273                 msg = _("Password updated successfully")
   1274             else:
   1275                 self.wallet.update_password(old_password, new_password)
   1276                 msg = _("Password updated for {}").format(os.path.basename(self.wallet.storage.path))
   1277             self.show_info(msg)
   1278         on_failure = lambda: self.show_error(_("Password not updated"))
   1279         d = ChangePasswordDialog(self, self.wallet, on_success, on_failure)
   1280         d.open()
   1281 
   1282     def change_pin_code(self, cb):
   1283         def on_success(old_password, new_password):
   1284             self.electrum_config.set_key('pin_code', new_password)
   1285             cb()
   1286             self.show_info(_("PIN updated") if new_password else _('PIN disabled'))
   1287         on_failure = lambda: self.show_error(_("PIN not updated"))
   1288         d = PincodeDialog(
   1289             self,
   1290             check_password=self.check_pin_code,
   1291             on_success=on_success,
   1292             on_failure=on_failure,
   1293             is_change=True,
   1294             has_password = self.has_pin_code())
   1295         d.open()
   1296 
   1297     def save_backup(self):
   1298         if platform != 'android':
   1299             self._save_backup()
   1300             return
   1301 
   1302         from android.permissions import request_permissions, Permission
   1303         def cb(permissions, grant_results: Sequence[bool]):
   1304             if not grant_results or not grant_results[0]:
   1305                 self.show_error(_("Cannot save backup without STORAGE permission"))
   1306                 return
   1307             # note: Clock.schedule_once is a hack so that we get called on a non-daemon thread
   1308             #       (needed for WalletDB.write)
   1309             Clock.schedule_once(lambda dt: self._save_backup())
   1310         request_permissions([Permission.WRITE_EXTERNAL_STORAGE], cb)
   1311 
   1312     def _save_backup(self):
   1313         try:
   1314             new_path = self.wallet.save_backup()
   1315         except Exception as e:
   1316             self.logger.exception("Failed to save wallet backup")
   1317             self.show_error("Failed to save wallet backup" + '\n' + str(e))
   1318             return
   1319         if new_path:
   1320             self.show_info(_("Backup saved:") + f"\n{new_path}")
   1321         else:
   1322             self.show_error(_("Backup NOT saved. Backup directory not configured."))
   1323 
   1324     def export_private_keys(self, pk_label, addr):
   1325         if self.wallet.is_watching_only():
   1326             self.show_info(_('This is a watching-only wallet. It does not contain private keys.'))
   1327             return
   1328         def show_private_key(addr, pk_label, password):
   1329             if self.wallet.has_password() and password is None:
   1330                 return
   1331             if not self.wallet.can_export():
   1332                 return
   1333             try:
   1334                 key = str(self.wallet.export_private_key(addr, password))
   1335                 pk_label.data = key
   1336             except InvalidPassword:
   1337                 self.show_error("Invalid PIN")
   1338                 return
   1339         self.protected(_("Decrypt your private key?"), show_private_key, (addr, pk_label))
   1340 
   1341     def import_channel_backup(self, encrypted):
   1342         d = Question(_('Import Channel Backup?'), lambda b: self._import_channel_backup(b, encrypted))
   1343         d.open()
   1344 
   1345     def _import_channel_backup(self, b, encrypted):
   1346         if not b:
   1347             return
   1348         try:
   1349             self.wallet.lnbackups.import_channel_backup(encrypted)
   1350         except Exception as e:
   1351             self.logger.exception("failed to import backup")
   1352             self.show_error("failed to import backup" + '\n' + str(e))
   1353             return
   1354         self.lightning_channels_dialog()