1import re
2import os
3import sys
4import time
5import datetime
6import traceback
7from decimal import Decimal
8import threading
9import asyncio
10from typing import TYPE_CHECKING, Optional, Union, Callable, Sequence
11
12from electrum.storage import WalletStorage, StorageReadWriteError
13from electrum.wallet_db import WalletDB
14from electrum.wallet import Wallet, InternalAddressCorruption, Abstract_Wallet
15from electrum.wallet import update_password_for_directory
16
17from electrum.plugin import run_hook
18from electrum import util
19from electrum.util import (profiler, InvalidPassword, send_exception_to_crash_reporter,
20                           format_satoshis, format_satoshis_plain, format_fee_satoshis,
21                           maybe_extract_bolt11_invoice)
22from electrum.invoices import PR_PAID, PR_FAILED
23from electrum import blockchain
24from electrum.network import Network, TxBroadcastError, BestEffortRequestFailed
25from electrum.interface import PREFERRED_NETWORK_PROTOCOL, ServerAddr
26from electrum.logging import Logger
27
28from electrum.gui import messages
29from .i18n import _
30from . import KIVY_GUI_PATH
31
32from kivy.app import App
33from kivy.core.window import Window
34from kivy.utils import platform
35from kivy.properties import (OptionProperty, AliasProperty, ObjectProperty,
36                             StringProperty, ListProperty, BooleanProperty, NumericProperty)
37from kivy.cache import Cache
38from kivy.clock import Clock
39from kivy.factory import Factory
40from kivy.metrics import inch
41from kivy.lang import Builder
42from .uix.dialogs.password_dialog import OpenWalletDialog, ChangePasswordDialog, PincodeDialog, PasswordDialog
43from .uix.dialogs.choice_dialog import ChoiceDialog
44
45## lazy imports for factory so that widgets can be used in kv
46#Factory.register('InstallWizard', module='electrum.gui.kivy.uix.dialogs.installwizard')
47#Factory.register('InfoBubble', module='electrum.gui.kivy.uix.dialogs')
48#Factory.register('OutputList', module='electrum.gui.kivy.uix.dialogs')
49#Factory.register('OutputItem', module='electrum.gui.kivy.uix.dialogs')
50
51from .uix.dialogs.installwizard import InstallWizard
52from .uix.dialogs import InfoBubble, crash_reporter
53from .uix.dialogs import OutputList, OutputItem
54from .uix.dialogs import TopLabel, RefLabel
55from .uix.dialogs.question import Question
56
57#from kivy.core.window import Window
58#Window.softinput_mode = 'below_target'
59
60# delayed imports: for startup speed on android
61notification = app = ref = None
62
63# register widget cache for keeping memory down timeout to forever to cache
64# the data
65Cache.register('electrum_widgets', timeout=0)
66
67from kivy.uix.screenmanager import Screen
68from kivy.uix.tabbedpanel import TabbedPanel
69from kivy.uix.label import Label
70from kivy.core.clipboard import Clipboard
71
72Factory.register('TabbedCarousel', module='electrum.gui.kivy.uix.screens')
73
74# Register fonts without this you won't be able to use bold/italic...
75# inside markup.
76from kivy.core.text import Label
77Label.register(
78    'Roboto',
79    KIVY_GUI_PATH + '/data/fonts/Roboto.ttf',
80    KIVY_GUI_PATH + '/data/fonts/Roboto.ttf',
81    KIVY_GUI_PATH + '/data/fonts/Roboto-Bold.ttf',
82    KIVY_GUI_PATH + '/data/fonts/Roboto-Bold.ttf',
83)
84
85
86from electrum.util import (NoDynamicFeeEstimates, NotEnoughFunds,
87                           BITCOIN_BIP21_URI_SCHEME, LIGHTNING_URI_SCHEME,
88                           UserFacingException)
89
90from .uix.dialogs.lightning_open_channel import LightningOpenChannelDialog
91from .uix.dialogs.lightning_channels import LightningChannelsDialog, SwapDialog
92
93if TYPE_CHECKING:
94    from . import ElectrumGui
95    from electrum.simple_config import SimpleConfig
96    from electrum.plugin import Plugins
97    from electrum.paymentrequest import PaymentRequest
98
99
100class ElectrumWindow(App, Logger):
101
102    electrum_config = ObjectProperty(None)
103    language = StringProperty('en')
104
105    # properties might be updated by the network
106    num_blocks = NumericProperty(0)
107    num_nodes = NumericProperty(0)
108    server_host = StringProperty('')
109    server_port = StringProperty('')
110    num_chains = NumericProperty(0)
111    blockchain_name = StringProperty('')
112    fee_status = StringProperty('Fee')
113    balance = StringProperty('')
114    fiat_balance = StringProperty('')
115    is_fiat = BooleanProperty(False)
116    blockchain_forkpoint = NumericProperty(0)
117
118    lightning_gossip_num_peers = NumericProperty(0)
119    lightning_gossip_num_nodes = NumericProperty(0)
120    lightning_gossip_num_channels = NumericProperty(0)
121    lightning_gossip_num_queries = NumericProperty(0)
122
123    auto_connect = BooleanProperty(False)
124    def on_auto_connect(self, instance, x):
125        net_params = self.network.get_parameters()
126        net_params = net_params._replace(auto_connect=self.auto_connect)
127        self.network.run_from_another_thread(self.network.set_parameters(net_params))
128    def toggle_auto_connect(self, x):
129        self.auto_connect = not self.auto_connect
130
131    oneserver = BooleanProperty(False)
132    def on_oneserver(self, instance, x):
133        net_params = self.network.get_parameters()
134        net_params = net_params._replace(oneserver=self.oneserver)
135        self.network.run_from_another_thread(self.network.set_parameters(net_params))
136    def toggle_oneserver(self, x):
137        self.oneserver = not self.oneserver
138
139    proxy_str = StringProperty('')
140    def update_proxy_str(self, proxy: dict):
141        mode = proxy.get('mode')
142        host = proxy.get('host')
143        port = proxy.get('port')
144        self.proxy_str = (host + ':' + port) if mode else _('None')
145
146    def choose_server_dialog(self, popup):
147        protocol = PREFERRED_NETWORK_PROTOCOL
148        def cb2(server_str):
149            popup.ids.server_str.text = server_str
150        servers = self.network.get_servers()
151        server_choices = {}
152        for _host, d in sorted(servers.items()):
153            port = d.get(protocol)
154            if port:
155                server = ServerAddr(_host, port, protocol=protocol)
156                server_choices[server.net_addr_str()] = _host
157        ChoiceDialog(_('Choose a server'), server_choices, popup.ids.server_str.text, cb2).open()
158
159    def maybe_switch_to_server(self, server_str: str):
160        net_params = self.network.get_parameters()
161        try:
162            server = ServerAddr.from_str_with_inference(server_str)
163            if not server: raise Exception("failed to parse")
164        except Exception as e:
165            self.show_error(_("Invalid server details: {}").format(repr(e)))
166            return
167        net_params = net_params._replace(server=server)
168        self.network.run_from_another_thread(self.network.set_parameters(net_params))
169
170    def choose_blockchain_dialog(self, dt):
171        chains = self.network.get_blockchains()
172        def cb(name):
173            with blockchain.blockchains_lock: blockchain_items = list(blockchain.blockchains.items())
174            for chain_id, b in blockchain_items:
175                if name == b.get_name():
176                    self.network.run_from_another_thread(self.network.follow_chain_given_id(chain_id))
177        chain_objects = [blockchain.blockchains.get(chain_id) for chain_id in chains]
178        chain_objects = filter(lambda b: b is not None, chain_objects)
179        names = [b.get_name() for b in chain_objects]
180        if len(names) > 1:
181            cur_chain = self.network.blockchain().get_name()
182            ChoiceDialog(_('Choose your chain'), names, cur_chain, cb).open()
183
184    use_rbf = BooleanProperty(False)
185    def on_use_rbf(self, instance, x):
186        self.electrum_config.set_key('use_rbf', self.use_rbf, True)
187
188    use_gossip = BooleanProperty(False)
189    def on_use_gossip(self, instance, x):
190        self.electrum_config.set_key('use_gossip', self.use_gossip, True)
191        if self.network:
192            if self.use_gossip:
193                self.network.start_gossip()
194            else:
195                self.network.run_from_another_thread(
196                    self.network.stop_gossip())
197
198    use_change = BooleanProperty(False)
199    def on_use_change(self, instance, x):
200        if self.wallet:
201            self.wallet.use_change = self.use_change
202            self.wallet.db.put('use_change', self.use_change)
203            self.wallet.save_db()
204
205    use_unconfirmed = BooleanProperty(False)
206    def on_use_unconfirmed(self, instance, x):
207        self.electrum_config.set_key('confirmed_only', not self.use_unconfirmed, True)
208
209    use_recoverable_channels = BooleanProperty(True)
210    def on_use_recoverable_channels(self, instance, x):
211        self.electrum_config.set_key('use_recoverable_channels', self.use_recoverable_channels, True)
212
213    def switch_to_send_screen(func):
214        # try until send_screen is available
215        def wrapper(self, *args):
216            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
217            Clock.schedule_interval(f, 0.1)
218        return wrapper
219
220    @switch_to_send_screen
221    def set_URI(self, uri):
222        self.send_screen.set_URI(uri)
223
224    @switch_to_send_screen
225    def set_ln_invoice(self, invoice):
226        self.send_screen.set_ln_invoice(invoice)
227
228    def on_new_intent(self, intent):
229        data = str(intent.getDataString())
230        scheme = str(intent.getScheme()).lower()
231        if scheme == BITCOIN_BIP21_URI_SCHEME:
232            self.set_URI(data)
233        elif scheme == LIGHTNING_URI_SCHEME:
234            self.set_ln_invoice(data)
235
236    def on_language(self, instance, language):
237        self.logger.info('language: {}'.format(language))
238        _.switch_lang(language)
239
240    def update_history(self, *dt):
241        if self.history_screen:
242            self.history_screen.update()
243
244    def on_quotes(self, d):
245        self.logger.info("on_quotes")
246        self._trigger_update_status()
247        self._trigger_update_history()
248
249    def on_history(self, d):
250        self.logger.info("on_history")
251        if self.wallet:
252            self.wallet.clear_coin_price_cache()
253        self._trigger_update_history()
254
255    def on_fee_histogram(self, *args):
256        self._trigger_update_history()
257
258    def on_request_status(self, event, wallet, key, status):
259        req = self.wallet.receive_requests.get(key)
260        if req is None:
261            return
262        if self.receive_screen:
263            if status == PR_PAID:
264                self.receive_screen.update()
265            else:
266                self.receive_screen.update_item(key, req)
267        if self.request_popup and self.request_popup.key == key:
268            self.request_popup.update_status()
269        if status == PR_PAID:
270            self.show_info(_('Payment Received') + '\n' + key)
271            self._trigger_update_history()
272
273    def on_invoice_status(self, event, wallet, key):
274        req = self.wallet.get_invoice(key)
275        if req is None:
276            return
277        status = self.wallet.get_invoice_status(req)
278        if self.send_screen:
279            if status == PR_PAID:
280                self.send_screen.update()
281            else:
282                self.send_screen.update_item(key, req)
283
284        if self.invoice_popup and self.invoice_popup.key == key:
285            self.invoice_popup.update_status()
286
287    def on_payment_succeeded(self, event, wallet, key):
288        description = self.wallet.get_label(key)
289        self.show_info(_('Payment succeeded') + '\n\n' + description)
290        self._trigger_update_history()
291
292    def on_payment_failed(self, event, wallet, key, reason):
293        self.show_info(_('Payment failed') + '\n\n' + reason)
294
295    def _get_bu(self):
296        return self.electrum_config.get_base_unit()
297
298    def _set_bu(self, value):
299        self.electrum_config.set_base_unit(value)
300        self._trigger_update_status()
301        self._trigger_update_history()
302
303    wallet_name = StringProperty(_('No Wallet'))
304    base_unit = AliasProperty(_get_bu, _set_bu)
305    fiat_unit = StringProperty('')
306
307    def on_fiat_unit(self, a, b):
308        self._trigger_update_history()
309
310    def decimal_point(self):
311        return self.electrum_config.get_decimal_point()
312
313    def btc_to_fiat(self, amount_str):
314        if not amount_str:
315            return ''
316        if not self.fx.is_enabled():
317            return ''
318        rate = self.fx.exchange_rate()
319        if rate.is_nan():
320            return ''
321        fiat_amount = self.get_amount(amount_str + ' ' + self.base_unit) * rate / pow(10, 8)
322        return "{:.2f}".format(fiat_amount).rstrip('0').rstrip('.')
323
324    def fiat_to_btc(self, fiat_amount):
325        if not fiat_amount:
326            return ''
327        rate = self.fx.exchange_rate()
328        if rate.is_nan():
329            return ''
330        satoshis = int(pow(10,8) * Decimal(fiat_amount) / Decimal(rate))
331        return format_satoshis_plain(satoshis, decimal_point=self.decimal_point())
332
333    def get_amount(self, amount_str):
334        a, u = amount_str.split()
335        assert u == self.base_unit
336        try:
337            x = Decimal(a)
338        except:
339            return None
340        p = pow(10, self.decimal_point())
341        return int(p * x)
342
343
344    _orientation = OptionProperty('landscape',
345                                 options=('landscape', 'portrait'))
346
347    def _get_orientation(self):
348        return self._orientation
349
350    orientation = AliasProperty(_get_orientation,
351                                None,
352                                bind=('_orientation',))
353    '''Tries to ascertain the kind of device the app is running on.
354    Cane be one of `tablet` or `phone`.
355
356    :data:`orientation` is a read only `AliasProperty` Defaults to 'landscape'
357    '''
358
359    _ui_mode = OptionProperty('phone', options=('tablet', 'phone'))
360
361    def _get_ui_mode(self):
362        return self._ui_mode
363
364    ui_mode = AliasProperty(_get_ui_mode,
365                            None,
366                            bind=('_ui_mode',))
367    '''Defines tries to ascertain the kind of device the app is running on.
368    Cane be one of `tablet` or `phone`.
369
370    :data:`ui_mode` is a read only `AliasProperty` Defaults to 'phone'
371    '''
372
373    def __init__(self, **kwargs):
374        # initialize variables
375        self._clipboard = Clipboard
376        self.info_bubble = None
377        self.nfcscanner = None
378        self.tabs = None
379        self.is_exit = False
380        self.wallet = None  # type: Optional[Abstract_Wallet]
381        self.pause_time = 0
382        self.asyncio_loop = asyncio.get_event_loop()
383        self.password = None
384        self._use_single_password = False
385        self.resume_dialog = None
386
387        App.__init__(self)#, **kwargs)
388        Logger.__init__(self)
389
390        self.electrum_config = config = kwargs.get('config', None)  # type: SimpleConfig
391        self.language = config.get('language', 'en')
392        self.network = network = kwargs.get('network', None)  # type: Network
393        if self.network:
394            self.num_blocks = self.network.get_local_height()
395            self.num_nodes = len(self.network.get_interfaces())
396            net_params = self.network.get_parameters()
397            self.server_host = net_params.server.host
398            self.server_port = str(net_params.server.port)
399            self.auto_connect = net_params.auto_connect
400            self.oneserver = net_params.oneserver
401            self.proxy_config = net_params.proxy if net_params.proxy else {}
402            self.update_proxy_str(self.proxy_config)
403
404        self.plugins = kwargs.get('plugins', None)  # type: Plugins
405        self.gui_object = kwargs.get('gui_object', None)  # type: ElectrumGui
406        self.daemon = self.gui_object.daemon
407        self.fx = self.daemon.fx
408        self.use_rbf = config.get('use_rbf', True)
409        self.use_gossip = config.get('use_gossip', False)
410        self.use_unconfirmed = not config.get('confirmed_only', False)
411
412        # create triggers so as to minimize updating a max of 2 times a sec
413        self._trigger_update_wallet = Clock.create_trigger(self.update_wallet, .5)
414        self._trigger_update_status = Clock.create_trigger(self.update_status, .5)
415        self._trigger_update_history = Clock.create_trigger(self.update_history, .5)
416        self._trigger_update_interfaces = Clock.create_trigger(self.update_interfaces, .5)
417
418        self._periodic_update_status_during_sync = Clock.schedule_interval(self.update_wallet_synchronizing_progress, .5)
419
420        # cached dialogs
421        self._settings_dialog = None
422        self._channels_dialog = None
423        self._addresses_dialog = None
424        self.set_fee_status()
425        self.invoice_popup = None
426        self.request_popup = None
427
428    def on_pr(self, pr: 'PaymentRequest'):
429        if not self.wallet:
430            self.show_error(_('No wallet loaded.'))
431            return
432        if pr.verify(self.wallet.contacts):
433            key = pr.get_id()
434            invoice = self.wallet.get_invoice(key)  # FIXME wrong key...
435            if invoice and self.wallet.get_invoice_status(invoice) == PR_PAID:
436                self.show_error("invoice already paid")
437                self.send_screen.do_clear()
438            elif pr.has_expired():
439                self.show_error(_('Payment request has expired'))
440            else:
441                self.switch_to('send')
442                self.send_screen.set_request(pr)
443        else:
444            self.show_error("invoice error:" + pr.error)
445            self.send_screen.do_clear()
446
447    def on_qr(self, data: str):
448        from electrum.bitcoin import is_address
449        data = data.strip()
450        if is_address(data):
451            self.set_URI(data)
452            return
453        if data.lower().startswith(BITCOIN_BIP21_URI_SCHEME + ':'):
454            self.set_URI(data)
455            return
456        if data.lower().startswith('channel_backup:'):
457            self.import_channel_backup(data)
458            return
459        bolt11_invoice = maybe_extract_bolt11_invoice(data)
460        if bolt11_invoice is not None:
461            self.set_ln_invoice(bolt11_invoice)
462            return
463        # try to decode transaction
464        from electrum.transaction import tx_from_any
465        try:
466            tx = tx_from_any(data)
467        except:
468            tx = None
469        if tx:
470            self.tx_dialog(tx)
471            return
472        # show error
473        self.show_error("Unable to decode QR data")
474
475    def update_tab(self, name):
476        s = getattr(self, name + '_screen', None)
477        if s:
478            s.update()
479
480    @profiler
481    def update_tabs(self):
482        for name in ['send', 'history', 'receive']:
483            self.update_tab(name)
484
485    def switch_to(self, name):
486        s = getattr(self, name + '_screen', None)
487        panel = self.tabs.ids.panel
488        tab = self.tabs.ids[name + '_tab']
489        panel.switch_to(tab)
490
491    def show_request(self, is_lightning, key):
492        from .uix.dialogs.request_dialog import RequestDialog
493        self.request_popup = RequestDialog('Request', key)
494        self.request_popup.open()
495
496    def show_invoice(self, is_lightning, key):
497        from .uix.dialogs.invoice_dialog import InvoiceDialog
498        invoice = self.wallet.get_invoice(key)
499        if not invoice:
500            return
501        data = invoice.invoice if is_lightning else key
502        self.invoice_popup = InvoiceDialog('Invoice', data, key)
503        self.invoice_popup.open()
504
505    def qr_dialog(self, title, data, show_text=False, text_for_clipboard=None, help_text=None):
506        from .uix.dialogs.qr_dialog import QRDialog
507        def on_qr_failure():
508            popup.dismiss()
509            msg = _('Failed to display QR code.')
510            if text_for_clipboard:
511                msg += '\n' + _('Text copied to clipboard.')
512                self._clipboard.copy(text_for_clipboard)
513            Clock.schedule_once(lambda dt: self.show_info(msg))
514        popup = QRDialog(
515            title, data, show_text,
516            failure_cb=on_qr_failure,
517            text_for_clipboard=text_for_clipboard,
518            help_text=help_text)
519        popup.open()
520
521    def scan_qr(self, on_complete):
522        if platform != 'android':
523            return self.scan_qr_non_android(on_complete)
524        from jnius import autoclass, cast
525        from android import activity
526        PythonActivity = autoclass('org.kivy.android.PythonActivity')
527        SimpleScannerActivity = autoclass("org.electrum.qr.SimpleScannerActivity")
528        Intent = autoclass('android.content.Intent')
529        intent = Intent(PythonActivity.mActivity, SimpleScannerActivity)
530
531        def on_qr_result(requestCode, resultCode, intent):
532            try:
533                if resultCode == -1:  # RESULT_OK:
534                    #  this doesn't work due to some bug in jnius:
535                    # contents = intent.getStringExtra("text")
536                    String = autoclass("java.lang.String")
537                    contents = intent.getStringExtra(String("text"))
538                    on_complete(contents)
539            except Exception as e:  # exc would otherwise get lost
540                send_exception_to_crash_reporter(e)
541            finally:
542                activity.unbind(on_activity_result=on_qr_result)
543        activity.bind(on_activity_result=on_qr_result)
544        PythonActivity.mActivity.startActivityForResult(intent, 0)
545
546    def scan_qr_non_android(self, on_complete):
547        from electrum import qrscanner
548        try:
549            video_dev = self.electrum_config.get_video_device()
550            data = qrscanner.scan_barcode(video_dev)
551            if data is not None:
552                on_complete(data)
553        except UserFacingException as e:
554            self.show_error(e)
555        except BaseException as e:
556            self.logger.exception('camera error')
557            self.show_error(repr(e))
558
559    def do_share(self, data, title):
560        if platform != 'android':
561            return
562        from jnius import autoclass, cast
563        JS = autoclass('java.lang.String')
564        Intent = autoclass('android.content.Intent')
565        sendIntent = Intent()
566        sendIntent.setAction(Intent.ACTION_SEND)
567        sendIntent.setType("text/plain")
568        sendIntent.putExtra(Intent.EXTRA_TEXT, JS(data))
569        PythonActivity = autoclass('org.kivy.android.PythonActivity')
570        currentActivity = cast('android.app.Activity', PythonActivity.mActivity)
571        it = Intent.createChooser(sendIntent, cast('java.lang.CharSequence', JS(title)))
572        currentActivity.startActivity(it)
573
574    def build(self):
575        return Builder.load_file(KIVY_GUI_PATH + '/main.kv')
576
577    def _pause(self):
578        if platform == 'android':
579            # move activity to back
580            from jnius import autoclass
581            python_act = autoclass('org.kivy.android.PythonActivity')
582            mActivity = python_act.mActivity
583            mActivity.moveTaskToBack(True)
584
585    def handle_crash_on_startup(func):
586        def wrapper(self, *args, **kwargs):
587            try:
588                return func(self, *args, **kwargs)
589            except Exception as e:
590                self.logger.exception('crash on startup')
591                from .uix.dialogs.crash_reporter import CrashReporter
592                # show the crash reporter, and when it's closed, shutdown the app
593                cr = CrashReporter(self, exctype=type(e), value=e, tb=e.__traceback__)
594                cr.on_dismiss = lambda: self.stop()
595                Clock.schedule_once(lambda _, cr=cr: cr.open(), 0)
596        return wrapper
597
598    @handle_crash_on_startup
599    def on_start(self):
600        ''' This is the start point of the kivy ui
601        '''
602        import time
603        self.logger.info('Time to on_start: {} <<<<<<<<'.format(time.process_time()))
604        Window.bind(size=self.on_size, on_keyboard=self.on_keyboard)
605        #Window.softinput_mode = 'below_target'
606        self.on_size(Window, Window.size)
607        self.init_ui()
608        crash_reporter.ExceptionHook(self)
609        # init plugins
610        run_hook('init_kivy', self)
611        # fiat currency
612        self.fiat_unit = self.fx.ccy if self.fx.is_enabled() else ''
613        # default tab
614        self.switch_to('history')
615        # bind intent for bitcoin: URI scheme
616        if platform == 'android':
617            from android import activity
618            from jnius import autoclass
619            PythonActivity = autoclass('org.kivy.android.PythonActivity')
620            mactivity = PythonActivity.mActivity
621            self.on_new_intent(mactivity.getIntent())
622            activity.bind(on_new_intent=self.on_new_intent)
623        # connect callbacks
624        if self.network:
625            interests = ['wallet_updated', 'network_updated', 'blockchain_updated',
626                         'status', 'new_transaction', 'verified']
627            util.register_callback(self.on_network_event, interests)
628            util.register_callback(self.on_fee, ['fee'])
629            util.register_callback(self.on_fee_histogram, ['fee_histogram'])
630            util.register_callback(self.on_quotes, ['on_quotes'])
631            util.register_callback(self.on_history, ['on_history'])
632            util.register_callback(self.on_channels, ['channels_updated'])
633            util.register_callback(self.on_channel, ['channel'])
634            util.register_callback(self.on_invoice_status, ['invoice_status'])
635            util.register_callback(self.on_request_status, ['request_status'])
636            util.register_callback(self.on_payment_failed, ['payment_failed'])
637            util.register_callback(self.on_payment_succeeded, ['payment_succeeded'])
638            util.register_callback(self.on_channel_db, ['channel_db'])
639            util.register_callback(self.set_num_peers, ['gossip_peers'])
640            util.register_callback(self.set_unknown_channels, ['unknown_channels'])
641        # load wallet
642        self.load_wallet_by_name(self.electrum_config.get_wallet_path(use_gui_last_wallet=True))
643        # URI passed in config
644        uri = self.electrum_config.get('url')
645        if uri:
646            self.set_URI(uri)
647
648    def on_channel_db(self, event, num_nodes, num_channels, num_policies):
649        self.lightning_gossip_num_nodes = num_nodes
650        self.lightning_gossip_num_channels = num_channels
651
652    def set_num_peers(self, event, num_peers):
653        self.lightning_gossip_num_peers = num_peers
654
655    def set_unknown_channels(self, event, unknown):
656        self.lightning_gossip_num_queries = unknown
657
658    def get_wallet_path(self):
659        if self.wallet:
660            return self.wallet.storage.path
661        else:
662            return ''
663
664    def on_wizard_success(self, storage, db, password):
665        self.password = password
666        if self.electrum_config.get('single_password'):
667            self._use_single_password = update_password_for_directory(self.electrum_config, password, password)
668        self.logger.info(f'use single password: {self._use_single_password}')
669        wallet = Wallet(db, storage, config=self.electrum_config)
670        wallet.start_network(self.daemon.network)
671        self.daemon.add_wallet(wallet)
672        self.load_wallet(wallet)
673
674    def on_wizard_aborted(self):
675        # wizard did not return a wallet; and there is no wallet open atm
676        if not self.wallet:
677            self.stop()
678
679    def load_wallet_by_name(self, path):
680        if not path:
681            return
682        if self.wallet and self.wallet.storage.path == path:
683            return
684        if self.password and self._use_single_password:
685            storage = WalletStorage(path)
686            # call check_password to decrypt
687            storage.check_password(self.password)
688            self.on_open_wallet(self.password, storage)
689            return
690        d = OpenWalletDialog(self, path, self.on_open_wallet)
691        d.open()
692
693    def on_open_wallet(self, password, storage):
694        if not storage.file_exists():
695            wizard = InstallWizard(self.electrum_config, self.plugins)
696            wizard.path = storage.path
697            wizard.run('new')
698        else:
699            assert storage.is_past_initial_decryption()
700            db = WalletDB(storage.read(), manual_upgrades=False)
701            assert not db.requires_upgrade()
702            self.on_wizard_success(storage, db, password)
703
704    def on_stop(self):
705        self.logger.info('on_stop')
706        self.stop_wallet()
707
708    def stop_wallet(self):
709        if self.wallet:
710            self.daemon.stop_wallet(self.wallet.storage.path)
711            self.wallet = None
712
713    def on_keyboard(self, instance, key, keycode, codepoint, modifiers):
714        if key == 27 and self.is_exit is False:
715            self.is_exit = True
716            self.show_info(_('Press again to exit'))
717            return True
718        # override settings button
719        if key in (319, 282): #f1/settings button on android
720            #self.gui.main_gui.toggle_settings(self)
721            return True
722
723    def settings_dialog(self):
724        from .uix.dialogs.settings import SettingsDialog
725        if self._settings_dialog is None:
726            self._settings_dialog = SettingsDialog(self)
727        else:
728            self._settings_dialog.update()
729        self._settings_dialog.open()
730
731    def lightning_open_channel_dialog(self):
732        if not self.wallet.has_lightning():
733            self.show_error(_('Lightning is not enabled for this wallet'))
734            return
735        if not self.wallet.lnworker.channels and not self.wallet.lnworker.channel_backups:
736            warning = _(messages.MSG_LIGHTNING_WARNING)
737            d = Question(_('Do you want to create your first channel?') +
738                         '\n\n' + warning, 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        if self.nfcscanner:
1007            self.nfcscanner.nfc_enable()
1008        if self.resume_dialog is not None:
1009            return
1010        now = time.time()
1011        if self.wallet and self.has_pin_code() and now - self.pause_time > 5*60:
1012            def on_success(x):
1013                self.resume_dialog = None
1014            d = PincodeDialog(
1015                self,
1016                check_password=self.check_pin_code,
1017                on_success=on_success,
1018                on_failure=self.stop)
1019            self.resume_dialog = d
1020            d.open()
1021
1022    def on_size(self, instance, value):
1023        width, height = value
1024        self._orientation = 'landscape' if width > height else 'portrait'
1025        self._ui_mode = 'tablet' if min(width, height) > inch(3.51) else 'phone'
1026
1027    def on_ref_label(self, label, *, show_text_with_qr: bool = True):
1028        if not label.data:
1029            return
1030        self.qr_dialog(label.name, label.data, show_text_with_qr)
1031
1032    def show_error(self, error, width='200dp', pos=None, arrow_pos=None,
1033                   exit=False, icon=f'atlas://{KIVY_GUI_PATH}/theming/atlas/light/error', duration=0,
1034                   modal=False):
1035        ''' Show an error Message Bubble.
1036        '''
1037        self.show_info_bubble(text=error, icon=icon, width=width,
1038            pos=pos or Window.center, arrow_pos=arrow_pos, exit=exit,
1039            duration=duration, modal=modal)
1040
1041    def show_info(self, error, width='200dp', pos=None, arrow_pos=None,
1042                  exit=False, duration=0, modal=False):
1043        ''' Show an Info Message Bubble.
1044        '''
1045        self.show_error(error, icon=f'atlas://{KIVY_GUI_PATH}/theming/atlas/light/important',
1046            duration=duration, modal=modal, exit=exit, pos=pos,
1047            arrow_pos=arrow_pos)
1048
1049    def show_info_bubble(self, text=_('Hello World'), pos=None, duration=0,
1050                         arrow_pos='bottom_mid', width=None, icon='', modal=False, exit=False):
1051        '''Method to show an Information Bubble
1052
1053        .. parameters::
1054            text: Message to be displayed
1055            pos: position for the bubble
1056            duration: duration the bubble remains on screen. 0 = click to hide
1057            width: width of the Bubble
1058            arrow_pos: arrow position for the bubble
1059        '''
1060        text = str(text)  # so that we also handle e.g. Exception
1061        info_bubble = self.info_bubble
1062        if not info_bubble:
1063            info_bubble = self.info_bubble = Factory.InfoBubble()
1064
1065        win = Window
1066        if info_bubble.parent:
1067            win.remove_widget(info_bubble
1068                                 if not info_bubble.modal else
1069                                 info_bubble._modal_view)
1070
1071        if not arrow_pos:
1072            info_bubble.show_arrow = False
1073        else:
1074            info_bubble.show_arrow = True
1075            info_bubble.arrow_pos = arrow_pos
1076        img = info_bubble.ids.img
1077        if text == 'texture':
1078            # icon holds a texture not a source image
1079            # display the texture in full screen
1080            text = ''
1081            img.texture = icon
1082            info_bubble.fs = True
1083            info_bubble.show_arrow = False
1084            img.allow_stretch = True
1085            info_bubble.dim_background = True
1086            info_bubble.background_image = f'atlas://{KIVY_GUI_PATH}/theming/atlas/light/card'
1087        else:
1088            info_bubble.fs = False
1089            info_bubble.icon = icon
1090            #if img.texture and img._coreimage:
1091            #    img.reload()
1092            img.allow_stretch = False
1093            info_bubble.dim_background = False
1094            info_bubble.background_image = 'atlas://data/images/defaulttheme/bubble'
1095        info_bubble.message = text
1096        if not pos:
1097            pos = (win.center[0], win.center[1] - (info_bubble.height/2))
1098        info_bubble.show(pos, duration, width, modal=modal, exit=exit)
1099
1100    def tx_dialog(self, tx):
1101        from .uix.dialogs.tx_dialog import TxDialog
1102        d = TxDialog(self, tx)
1103        d.open()
1104
1105    def show_transaction(self, txid):
1106        tx = self.wallet.db.get_transaction(txid)
1107        if not tx and self.wallet.lnworker:
1108            tx = self.wallet.lnworker.lnwatcher.db.get_transaction(txid)
1109        if tx:
1110            self.tx_dialog(tx)
1111        else:
1112            self.show_error(f'Transaction not found {txid}')
1113
1114    def lightning_tx_dialog(self, tx):
1115        from .uix.dialogs.lightning_tx_dialog import LightningTxDialog
1116        d = LightningTxDialog(self, tx)
1117        d.open()
1118
1119    def sign_tx(self, *args):
1120        threading.Thread(target=self._sign_tx, args=args).start()
1121
1122    def _sign_tx(self, tx, password, on_success, on_failure):
1123        try:
1124            self.wallet.sign_transaction(tx, password)
1125        except InvalidPassword:
1126            Clock.schedule_once(lambda dt: on_failure(_("Invalid PIN")))
1127            return
1128        on_success = run_hook('tc_sign_wrapper', self.wallet, tx, on_success, on_failure) or on_success
1129        Clock.schedule_once(lambda dt: on_success(tx))
1130
1131    def _broadcast_thread(self, tx, on_complete):
1132        status = False
1133        try:
1134            self.network.run_from_another_thread(self.network.broadcast_transaction(tx))
1135        except TxBroadcastError as e:
1136            msg = e.get_message_for_gui()
1137        except BestEffortRequestFailed as e:
1138            msg = repr(e)
1139        else:
1140            status, msg = True, tx.txid()
1141        Clock.schedule_once(lambda dt: on_complete(status, msg))
1142
1143    def broadcast(self, tx):
1144        def on_complete(ok, msg):
1145            if ok:
1146                self.show_info(_('Payment sent.'))
1147                if self.send_screen:
1148                    self.send_screen.do_clear()
1149            else:
1150                msg = msg or ''
1151                self.show_error(msg)
1152
1153        if self.network and self.network.is_connected():
1154            self.show_info(_('Sending'))
1155            threading.Thread(target=self._broadcast_thread, args=(tx, on_complete)).start()
1156        else:
1157            self.show_info(_('Cannot broadcast transaction') + ':\n' + _('Not connected'))
1158
1159    def description_dialog(self, screen):
1160        from .uix.dialogs.label_dialog import LabelDialog
1161        text = screen.message
1162        def callback(text):
1163            screen.message = text
1164        d = LabelDialog(_('Enter description'), text, callback)
1165        d.open()
1166
1167    def amount_dialog(self, screen, show_max):
1168        from .uix.dialogs.amount_dialog import AmountDialog
1169        amount = screen.amount
1170        if amount:
1171            amount, u = str(amount).split()
1172            assert u == self.base_unit
1173        def cb(amount):
1174            if amount == '!':
1175                screen.is_max = True
1176                max_amt = self.get_max_amount()
1177                screen.amount = (max_amt + ' ' + self.base_unit) if max_amt else ''
1178            else:
1179                screen.amount = amount
1180                screen.is_max = False
1181        popup = AmountDialog(show_max, amount, cb)
1182        popup.open()
1183
1184    def addresses_dialog(self):
1185        from .uix.dialogs.addresses import AddressesDialog
1186        if self._addresses_dialog is None:
1187            self._addresses_dialog = AddressesDialog(self)
1188        else:
1189            self._addresses_dialog.update()
1190        self._addresses_dialog.open()
1191
1192    def fee_dialog(self):
1193        from .uix.dialogs.fee_dialog import FeeDialog
1194        fee_dialog = FeeDialog(self, self.electrum_config, self.set_fee_status)
1195        fee_dialog.open()
1196
1197    def set_fee_status(self):
1198        target, tooltip, dyn = self.electrum_config.get_fee_target()
1199        self.fee_status = target
1200
1201    def on_fee(self, event, *arg):
1202        self.set_fee_status()
1203
1204    def protected(self, msg, f, args):
1205        if self.electrum_config.get('pin_code'):
1206            msg += "\n" + _("Enter your PIN code to proceed")
1207            on_success = lambda pw: f(*args, self.password)
1208            d = PincodeDialog(
1209                self,
1210                message = msg,
1211                check_password=self.check_pin_code,
1212                on_success=on_success,
1213                on_failure=lambda: None)
1214            d.open()
1215        else:
1216            d = Question(
1217                msg,
1218                lambda b: f(*args, self.password) if b else None,
1219                yes_str=_("OK"),
1220                no_str=_("Cancel"),
1221                title=_("Confirm action"))
1222            d.open()
1223
1224    def delete_wallet(self):
1225        basename = os.path.basename(self.wallet.storage.path)
1226        d = Question(_('Delete wallet?') + '\n' + basename, self._delete_wallet)
1227        d.open()
1228
1229    def _delete_wallet(self, b):
1230        if b:
1231            basename = self.wallet.basename()
1232            self.protected(_("Are you sure you want to delete wallet {}?").format(basename),
1233                           self.__delete_wallet, ())
1234
1235    def __delete_wallet(self, pw):
1236        wallet_path = self.get_wallet_path()
1237        basename = os.path.basename(wallet_path)
1238        if self.wallet.has_password():
1239            try:
1240                self.wallet.check_password(pw)
1241            except InvalidPassword:
1242                self.show_error("Invalid password")
1243                return
1244        self.stop_wallet()
1245        os.unlink(wallet_path)
1246        self.show_error(_("Wallet removed: {}").format(basename))
1247        new_path = self.electrum_config.get_wallet_path(use_gui_last_wallet=True)
1248        self.load_wallet_by_name(new_path)
1249
1250    def show_seed(self, label):
1251        self.protected(_("Display your seed?"), self._show_seed, (label,))
1252
1253    def _show_seed(self, label, password):
1254        if self.wallet.has_password() and password is None:
1255            return
1256        keystore = self.wallet.keystore
1257        seed = keystore.get_seed(password)
1258        passphrase = keystore.get_passphrase(password)
1259        label.data = seed
1260        if passphrase:
1261            label.data += '\n\n' + _('Passphrase') + ': ' + passphrase
1262
1263    def has_pin_code(self):
1264        return bool(self.electrum_config.get('pin_code'))
1265
1266    def check_pin_code(self, pin):
1267        if pin != self.electrum_config.get('pin_code'):
1268            raise InvalidPassword
1269
1270    def change_password(self, cb):
1271        def on_success(old_password, new_password):
1272            # called if old_password works on self.wallet
1273            self.password = new_password
1274            if self._use_single_password:
1275                path = self.wallet.storage.path
1276                self.stop_wallet()
1277                update_password_for_directory(self.electrum_config, old_password, new_password)
1278                self.load_wallet_by_name(path)
1279                msg = _("Password updated successfully")
1280            else:
1281                self.wallet.update_password(old_password, new_password)
1282                msg = _("Password updated for {}").format(os.path.basename(self.wallet.storage.path))
1283            self.show_info(msg)
1284        on_failure = lambda: self.show_error(_("Password not updated"))
1285        d = ChangePasswordDialog(self, self.wallet, on_success, on_failure)
1286        d.open()
1287
1288    def pin_code_dialog(self, cb):
1289        if self._use_single_password and self.has_pin_code():
1290            def on_choice(choice):
1291                if choice == 0:
1292                    self.change_pin_code(cb)
1293                else:
1294                    self.reset_pin_code(cb)
1295            choices = {0:'Change PIN code', 1:'Reset PIN'}
1296            dialog = ChoiceDialog(
1297                _('PIN Code'), choices, 0,
1298                on_choice,
1299                keep_choice_order=True)
1300            dialog.open()
1301        else:
1302            self.change_pin_code(cb)
1303
1304    def reset_pin_code(self, cb):
1305        on_success = lambda x: self._set_new_pin_code(None, cb)
1306        d = PasswordDialog(self,
1307            basename = self.wallet.basename(),
1308            check_password = self.wallet.check_password,
1309            on_success=on_success,
1310            on_failure=lambda: None,
1311            is_change=False,
1312            has_password=self.wallet.has_password())
1313        d.open()
1314
1315    def _set_new_pin_code(self, new_pin, cb):
1316        self.electrum_config.set_key('pin_code', new_pin)
1317        cb()
1318        self.show_info(_("PIN updated") if new_pin else _('PIN disabled'))
1319
1320    def change_pin_code(self, cb):
1321        on_failure = lambda: self.show_error(_("PIN not updated"))
1322        on_success = lambda old_pin, new_pin: self._set_new_pin_code(new_pin, cb)
1323        d = PincodeDialog(
1324            self,
1325            check_password=self.check_pin_code,
1326            on_success=on_success,
1327            on_failure=on_failure,
1328            is_change=True,
1329            has_password = self.has_pin_code())
1330        d.open()
1331
1332    def save_backup(self):
1333        if platform != 'android':
1334            backup_dir = self.electrum_config.get_backup_dir()
1335            if backup_dir:
1336                self._save_backup(backup_dir)
1337            else:
1338                self.show_error(_("Backup NOT saved. Backup directory not configured."))
1339            return
1340
1341        from android.permissions import request_permissions, Permission
1342        def cb(permissions, grant_results: Sequence[bool]):
1343            if not grant_results or not grant_results[0]:
1344                self.show_error(_("Cannot save backup without STORAGE permission"))
1345                return
1346            # note: Clock.schedule_once is a hack so that we get called on a non-daemon thread
1347            #       (needed for WalletDB.write)
1348            backup_dir = util.android_backup_dir()
1349            Clock.schedule_once(lambda dt: self._save_backup(backup_dir))
1350        request_permissions([Permission.WRITE_EXTERNAL_STORAGE], cb)
1351
1352    def _save_backup(self, backup_dir):
1353        try:
1354            new_path = self.wallet.save_backup(backup_dir)
1355        except Exception as e:
1356            self.logger.exception("Failed to save wallet backup")
1357            self.show_error("Failed to save wallet backup" + '\n' + str(e))
1358            return
1359        self.show_info(_("Backup saved:") + f"\n{new_path}")
1360
1361    def export_private_keys(self, pk_label, addr):
1362        if self.wallet.is_watching_only():
1363            self.show_info(_('This is a watching-only wallet. It does not contain private keys.'))
1364            return
1365        def show_private_key(addr, pk_label, password):
1366            if self.wallet.has_password() and password is None:
1367                return
1368            if not self.wallet.can_export():
1369                return
1370            try:
1371                key = str(self.wallet.export_private_key(addr, password))
1372                pk_label.data = key
1373            except InvalidPassword:
1374                self.show_error("Invalid PIN")
1375                return
1376        self.protected(_("Decrypt your private key?"), show_private_key, (addr, pk_label))
1377
1378    def import_channel_backup(self, encrypted):
1379        d = Question(_('Import Channel Backup?'), lambda b: self._import_channel_backup(b, encrypted))
1380        d.open()
1381
1382    def _import_channel_backup(self, b, encrypted):
1383        if not b:
1384            return
1385        try:
1386            self.wallet.lnworker.import_channel_backup(encrypted)
1387        except Exception as e:
1388            self.logger.exception("failed to import backup")
1389            self.show_error("failed to import backup" + '\n' + str(e))
1390            return
1391        self.lightning_channels_dialog()
1392
1393    def lightning_status(self):
1394        if self.wallet.has_lightning():
1395            if self.wallet.lnworker.has_deterministic_node_id():
1396                status = _('Enabled')
1397            else:
1398                status = _('Enabled, non-recoverable channels')
1399        else:
1400            if self.wallet.can_have_lightning():
1401                status = _('Not enabled')
1402            else:
1403                status = _("Not available for this wallet.")
1404        return status
1405
1406    def on_lightning_status(self, root):
1407        if self.wallet.has_lightning():
1408            if self.wallet.lnworker.has_deterministic_node_id():
1409                pass
1410            else:
1411                if self.wallet.db.get('seed_type') == 'segwit':
1412                    msg = _("Your channels cannot be recovered from seed, because they were created with an old version of Electrum. "
1413                            "This means that you must save a backup of your wallet everytime you create a new channel.\n\n"
1414                            "If you want this wallet to have recoverable channels, you must close your existing channels and restore this wallet from seed")
1415                else:
1416                    msg = _("Your channels cannot be recovered from seed. "
1417                            "This means that you must save a backup of your wallet everytime you create a new channel.\n\n"
1418                            "If you want to have recoverable channels, you must create a new wallet with an Electrum seed")
1419                self.show_info(msg)
1420        elif self.wallet.can_have_lightning():
1421            root.dismiss()
1422            if self.wallet.can_have_deterministic_lightning():
1423                msg = _(
1424                    "Lightning is not enabled because this wallet was created with an old version of Electrum. "
1425                    "Create lightning keys?")
1426            else:
1427                msg = _(
1428                    "Warning: this wallet type does not support channel recovery from seed. "
1429                    "You will need to backup your wallet everytime you create a new wallet. "
1430                    "Create lightning keys?")
1431            d = Question(msg, self._enable_lightning, title=_('Enable Lightning?'))
1432            d.open()
1433
1434    def _enable_lightning(self, b):
1435        if not b:
1436            return
1437        self.wallet.init_lightning(password=self.password)
1438        self.show_info(_('Lightning keys have been initialized.'))
1439