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