tmain_window.py - electrum - Electrum Bitcoin wallet
HTML git clone https://git.parazyd.org/electrum
DIR Log
DIR Files
DIR Refs
DIR Submodules
---
tmain_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()