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