mirror of
https://github.com/LBRYFoundation/LBRY-Vault.git
synced 2025-08-23 17:47:31 +00:00
The intended behaviour is that when the user launches the app, the wallet we try to open is the wallet most recently opened by the user. The old code in 'on_stop' in practice only got called if the user exited via repeatedly pressing the system 'back' button. related #5952
1217 lines
48 KiB
Python
1217 lines
48 KiB
Python
import re
|
|
import os
|
|
import sys
|
|
import time
|
|
import datetime
|
|
import traceback
|
|
from decimal import Decimal
|
|
import threading
|
|
import asyncio
|
|
from typing import TYPE_CHECKING, Optional, Union, Callable
|
|
|
|
from electrum.storage import WalletStorage, StorageReadWriteError
|
|
from electrum.wallet_db import WalletDB
|
|
from electrum.wallet import Wallet, InternalAddressCorruption, Abstract_Wallet
|
|
from electrum.plugin import run_hook
|
|
from electrum.util import (profiler, InvalidPassword, send_exception_to_crash_reporter,
|
|
format_satoshis, format_satoshis_plain, format_fee_satoshis,
|
|
PR_PAID, PR_FAILED, maybe_extract_bolt11_invoice)
|
|
from electrum import blockchain
|
|
from electrum.network import Network, TxBroadcastError, BestEffortRequestFailed
|
|
from .i18n import _
|
|
|
|
from kivy.app import App
|
|
from kivy.core.window import Window
|
|
from kivy.logger import Logger
|
|
from kivy.utils import platform
|
|
from kivy.properties import (OptionProperty, AliasProperty, ObjectProperty,
|
|
StringProperty, ListProperty, BooleanProperty, NumericProperty)
|
|
from kivy.cache import Cache
|
|
from kivy.clock import Clock
|
|
from kivy.factory import Factory
|
|
from kivy.metrics import inch
|
|
from kivy.lang import Builder
|
|
|
|
## lazy imports for factory so that widgets can be used in kv
|
|
#Factory.register('InstallWizard', module='electrum.gui.kivy.uix.dialogs.installwizard')
|
|
#Factory.register('InfoBubble', module='electrum.gui.kivy.uix.dialogs')
|
|
#Factory.register('OutputList', module='electrum.gui.kivy.uix.dialogs')
|
|
#Factory.register('OutputItem', module='electrum.gui.kivy.uix.dialogs')
|
|
|
|
from .uix.dialogs.installwizard import InstallWizard
|
|
from .uix.dialogs import InfoBubble, crash_reporter
|
|
from .uix.dialogs import OutputList, OutputItem
|
|
from .uix.dialogs import TopLabel, RefLabel
|
|
from .uix.dialogs.question import Question
|
|
|
|
#from kivy.core.window import Window
|
|
#Window.softinput_mode = 'below_target'
|
|
|
|
# delayed imports: for startup speed on android
|
|
notification = app = ref = None
|
|
util = False
|
|
|
|
# register widget cache for keeping memory down timeout to forever to cache
|
|
# the data
|
|
Cache.register('electrum_widgets', timeout=0)
|
|
|
|
from kivy.uix.screenmanager import Screen
|
|
from kivy.uix.tabbedpanel import TabbedPanel
|
|
from kivy.uix.label import Label
|
|
from kivy.core.clipboard import Clipboard
|
|
|
|
Factory.register('TabbedCarousel', module='electrum.gui.kivy.uix.screens')
|
|
|
|
# Register fonts without this you won't be able to use bold/italic...
|
|
# inside markup.
|
|
from kivy.core.text import Label
|
|
Label.register('Roboto',
|
|
'electrum/gui/kivy/data/fonts/Roboto.ttf',
|
|
'electrum/gui/kivy/data/fonts/Roboto.ttf',
|
|
'electrum/gui/kivy/data/fonts/Roboto-Bold.ttf',
|
|
'electrum/gui/kivy/data/fonts/Roboto-Bold.ttf')
|
|
|
|
|
|
from electrum.util import (base_units, NoDynamicFeeEstimates, decimal_point_to_base_unit_name,
|
|
base_unit_name_to_decimal_point, NotEnoughFunds, UnknownBaseUnit,
|
|
DECIMAL_POINT_DEFAULT)
|
|
|
|
from .uix.dialogs.lightning_open_channel import LightningOpenChannelDialog
|
|
from .uix.dialogs.lightning_channels import LightningChannelsDialog
|
|
|
|
if TYPE_CHECKING:
|
|
from . import ElectrumGui
|
|
from electrum.simple_config import SimpleConfig
|
|
from electrum.plugin import Plugins
|
|
from electrum.paymentrequest import PaymentRequest
|
|
|
|
|
|
class ElectrumWindow(App):
|
|
|
|
electrum_config = ObjectProperty(None)
|
|
language = StringProperty('en')
|
|
|
|
# properties might be updated by the network
|
|
num_blocks = NumericProperty(0)
|
|
num_nodes = NumericProperty(0)
|
|
server_host = StringProperty('')
|
|
server_port = StringProperty('')
|
|
num_chains = NumericProperty(0)
|
|
blockchain_name = StringProperty('')
|
|
fee_status = StringProperty('Fee')
|
|
balance = StringProperty('')
|
|
fiat_balance = StringProperty('')
|
|
is_fiat = BooleanProperty(False)
|
|
blockchain_forkpoint = NumericProperty(0)
|
|
|
|
lightning_gossip_num_peers = NumericProperty(0)
|
|
lightning_gossip_num_nodes = NumericProperty(0)
|
|
lightning_gossip_num_channels = NumericProperty(0)
|
|
lightning_gossip_num_queries = NumericProperty(0)
|
|
|
|
auto_connect = BooleanProperty(False)
|
|
def on_auto_connect(self, instance, x):
|
|
net_params = self.network.get_parameters()
|
|
net_params = net_params._replace(auto_connect=self.auto_connect)
|
|
self.network.run_from_another_thread(self.network.set_parameters(net_params))
|
|
def toggle_auto_connect(self, x):
|
|
self.auto_connect = not self.auto_connect
|
|
|
|
oneserver = BooleanProperty(False)
|
|
def on_oneserver(self, instance, x):
|
|
net_params = self.network.get_parameters()
|
|
net_params = net_params._replace(oneserver=self.oneserver)
|
|
self.network.run_from_another_thread(self.network.set_parameters(net_params))
|
|
def toggle_oneserver(self, x):
|
|
self.oneserver = not self.oneserver
|
|
|
|
proxy_str = StringProperty('')
|
|
def update_proxy_str(self, proxy: dict):
|
|
mode = proxy.get('mode')
|
|
host = proxy.get('host')
|
|
port = proxy.get('port')
|
|
self.proxy_str = (host + ':' + port) if mode else _('None')
|
|
|
|
def choose_server_dialog(self, popup):
|
|
from .uix.dialogs.choice_dialog import ChoiceDialog
|
|
protocol = 's'
|
|
def cb2(host):
|
|
from electrum import constants
|
|
pp = servers.get(host, constants.net.DEFAULT_PORTS)
|
|
port = pp.get(protocol, '')
|
|
popup.ids.host.text = host
|
|
popup.ids.port.text = port
|
|
servers = self.network.get_servers()
|
|
ChoiceDialog(_('Choose a server'), sorted(servers), popup.ids.host.text, cb2).open()
|
|
|
|
def choose_blockchain_dialog(self, dt):
|
|
from .uix.dialogs.choice_dialog import ChoiceDialog
|
|
chains = self.network.get_blockchains()
|
|
def cb(name):
|
|
with blockchain.blockchains_lock: blockchain_items = list(blockchain.blockchains.items())
|
|
for chain_id, b in blockchain_items:
|
|
if name == b.get_name():
|
|
self.network.run_from_another_thread(self.network.follow_chain_given_id(chain_id))
|
|
chain_objects = [blockchain.blockchains.get(chain_id) for chain_id in chains]
|
|
chain_objects = filter(lambda b: b is not None, chain_objects)
|
|
names = [b.get_name() for b in chain_objects]
|
|
if len(names) > 1:
|
|
cur_chain = self.network.blockchain().get_name()
|
|
ChoiceDialog(_('Choose your chain'), names, cur_chain, cb).open()
|
|
|
|
use_rbf = BooleanProperty(False)
|
|
def on_use_rbf(self, instance, x):
|
|
self.electrum_config.set_key('use_rbf', self.use_rbf, True)
|
|
|
|
use_change = BooleanProperty(False)
|
|
def on_use_change(self, instance, x):
|
|
if self.wallet:
|
|
self.wallet.use_change = self.use_change
|
|
self.wallet.db.put('use_change', self.use_change)
|
|
self.wallet.save_db()
|
|
|
|
use_unconfirmed = BooleanProperty(False)
|
|
def on_use_unconfirmed(self, instance, x):
|
|
self.electrum_config.set_key('confirmed_only', not self.use_unconfirmed, True)
|
|
|
|
def set_URI(self, uri):
|
|
self.switch_to('send')
|
|
self.send_screen.set_URI(uri)
|
|
|
|
def set_ln_invoice(self, invoice):
|
|
self.switch_to('send')
|
|
self.send_screen.set_ln_invoice(invoice)
|
|
|
|
def on_new_intent(self, intent):
|
|
data = intent.getDataString()
|
|
if intent.getScheme() == 'bitcoin':
|
|
self.set_URI(data)
|
|
elif intent.getScheme() == 'lightning':
|
|
self.set_ln_invoice(data)
|
|
|
|
def on_language(self, instance, language):
|
|
Logger.info('language: {}'.format(language))
|
|
_.switch_lang(language)
|
|
|
|
def update_history(self, *dt):
|
|
if self.history_screen:
|
|
self.history_screen.update()
|
|
|
|
def on_quotes(self, d):
|
|
Logger.info("on_quotes")
|
|
self._trigger_update_status()
|
|
self._trigger_update_history()
|
|
|
|
def on_history(self, d):
|
|
Logger.info("on_history")
|
|
if self.wallet:
|
|
self.wallet.clear_coin_price_cache()
|
|
self._trigger_update_history()
|
|
|
|
def on_fee_histogram(self, *args):
|
|
self._trigger_update_history()
|
|
|
|
def on_request_status(self, event, key, status):
|
|
if key not in self.wallet.receive_requests:
|
|
return
|
|
self.update_tab('receive')
|
|
if self.request_popup and self.request_popup.key == key:
|
|
self.request_popup.set_status(status)
|
|
if status == PR_PAID:
|
|
self.show_info(_('Payment Received') + '\n' + key)
|
|
self._trigger_update_history()
|
|
|
|
def on_invoice_status(self, event, key, status):
|
|
# todo: update single item
|
|
self.update_tab('send')
|
|
if self.invoice_popup and self.invoice_popup.key == key:
|
|
self.invoice_popup.set_status(status)
|
|
if status == PR_PAID:
|
|
self.show_info(_('Payment was sent'))
|
|
self._trigger_update_history()
|
|
elif status == PR_FAILED:
|
|
self.show_info(_('Payment failed'))
|
|
|
|
def _get_bu(self):
|
|
decimal_point = self.electrum_config.get('decimal_point', DECIMAL_POINT_DEFAULT)
|
|
try:
|
|
return decimal_point_to_base_unit_name(decimal_point)
|
|
except UnknownBaseUnit:
|
|
return decimal_point_to_base_unit_name(DECIMAL_POINT_DEFAULT)
|
|
|
|
def _set_bu(self, value):
|
|
assert value in base_units.keys()
|
|
decimal_point = base_unit_name_to_decimal_point(value)
|
|
self.electrum_config.set_key('decimal_point', decimal_point, True)
|
|
self._trigger_update_status()
|
|
self._trigger_update_history()
|
|
|
|
wallet_name = StringProperty(_('No Wallet'))
|
|
base_unit = AliasProperty(_get_bu, _set_bu)
|
|
fiat_unit = StringProperty('')
|
|
|
|
def on_fiat_unit(self, a, b):
|
|
self._trigger_update_history()
|
|
|
|
def decimal_point(self):
|
|
return base_units[self.base_unit]
|
|
|
|
def btc_to_fiat(self, amount_str):
|
|
if not amount_str:
|
|
return ''
|
|
if not self.fx.is_enabled():
|
|
return ''
|
|
rate = self.fx.exchange_rate()
|
|
if rate.is_nan():
|
|
return ''
|
|
fiat_amount = self.get_amount(amount_str + ' ' + self.base_unit) * rate / pow(10, 8)
|
|
return "{:.2f}".format(fiat_amount).rstrip('0').rstrip('.')
|
|
|
|
def fiat_to_btc(self, fiat_amount):
|
|
if not fiat_amount:
|
|
return ''
|
|
rate = self.fx.exchange_rate()
|
|
if rate.is_nan():
|
|
return ''
|
|
satoshis = int(pow(10,8) * Decimal(fiat_amount) / Decimal(rate))
|
|
return format_satoshis_plain(satoshis, self.decimal_point())
|
|
|
|
def get_amount(self, amount_str):
|
|
a, u = amount_str.split()
|
|
assert u == self.base_unit
|
|
try:
|
|
x = Decimal(a)
|
|
except:
|
|
return None
|
|
p = pow(10, self.decimal_point())
|
|
return int(p * x)
|
|
|
|
|
|
_orientation = OptionProperty('landscape',
|
|
options=('landscape', 'portrait'))
|
|
|
|
def _get_orientation(self):
|
|
return self._orientation
|
|
|
|
orientation = AliasProperty(_get_orientation,
|
|
None,
|
|
bind=('_orientation',))
|
|
'''Tries to ascertain the kind of device the app is running on.
|
|
Cane be one of `tablet` or `phone`.
|
|
|
|
:data:`orientation` is a read only `AliasProperty` Defaults to 'landscape'
|
|
'''
|
|
|
|
_ui_mode = OptionProperty('phone', options=('tablet', 'phone'))
|
|
|
|
def _get_ui_mode(self):
|
|
return self._ui_mode
|
|
|
|
ui_mode = AliasProperty(_get_ui_mode,
|
|
None,
|
|
bind=('_ui_mode',))
|
|
'''Defines tries to ascertain the kind of device the app is running on.
|
|
Cane be one of `tablet` or `phone`.
|
|
|
|
:data:`ui_mode` is a read only `AliasProperty` Defaults to 'phone'
|
|
'''
|
|
|
|
def __init__(self, **kwargs):
|
|
# initialize variables
|
|
self._clipboard = Clipboard
|
|
self.info_bubble = None
|
|
self.nfcscanner = None
|
|
self.tabs = None
|
|
self.is_exit = False
|
|
self.wallet = None # type: Optional[Abstract_Wallet]
|
|
self.pause_time = 0
|
|
self.asyncio_loop = asyncio.get_event_loop()
|
|
|
|
App.__init__(self)#, **kwargs)
|
|
|
|
self.electrum_config = config = kwargs.get('config', None) # type: SimpleConfig
|
|
self.language = config.get('language', 'en')
|
|
self.network = network = kwargs.get('network', None) # type: Network
|
|
if self.network:
|
|
self.num_blocks = self.network.get_local_height()
|
|
self.num_nodes = len(self.network.get_interfaces())
|
|
net_params = self.network.get_parameters()
|
|
self.server_host = net_params.host
|
|
self.server_port = net_params.port
|
|
self.auto_connect = net_params.auto_connect
|
|
self.oneserver = net_params.oneserver
|
|
self.proxy_config = net_params.proxy if net_params.proxy else {}
|
|
self.update_proxy_str(self.proxy_config)
|
|
|
|
self.plugins = kwargs.get('plugins', None) # type: Plugins
|
|
self.gui_object = kwargs.get('gui_object', None) # type: ElectrumGui
|
|
self.daemon = self.gui_object.daemon
|
|
self.fx = self.daemon.fx
|
|
self.use_rbf = config.get('use_rbf', True)
|
|
self.use_unconfirmed = not config.get('confirmed_only', False)
|
|
|
|
# create triggers so as to minimize updating a max of 2 times a sec
|
|
self._trigger_update_wallet = Clock.create_trigger(self.update_wallet, .5)
|
|
self._trigger_update_status = Clock.create_trigger(self.update_status, .5)
|
|
self._trigger_update_history = Clock.create_trigger(self.update_history, .5)
|
|
self._trigger_update_interfaces = Clock.create_trigger(self.update_interfaces, .5)
|
|
|
|
self._periodic_update_status_during_sync = Clock.schedule_interval(self.update_wallet_synchronizing_progress, .5)
|
|
|
|
# cached dialogs
|
|
self._settings_dialog = None
|
|
self._password_dialog = None
|
|
self._channels_dialog = None
|
|
self._addresses_dialog = None
|
|
self.fee_status = self.electrum_config.get_fee_status()
|
|
self.request_popup = None
|
|
|
|
def on_pr(self, pr: 'PaymentRequest'):
|
|
if not self.wallet:
|
|
self.show_error(_('No wallet loaded.'))
|
|
return
|
|
if pr.verify(self.wallet.contacts):
|
|
key = pr.get_id()
|
|
invoice = self.wallet.get_invoice(key) # FIXME wrong key...
|
|
if invoice and invoice['status'] == PR_PAID:
|
|
self.show_error("invoice already paid")
|
|
self.send_screen.do_clear()
|
|
elif pr.has_expired():
|
|
self.show_error(_('Payment request has expired'))
|
|
else:
|
|
self.switch_to('send')
|
|
self.send_screen.set_request(pr)
|
|
else:
|
|
self.show_error("invoice error:" + pr.error)
|
|
self.send_screen.do_clear()
|
|
|
|
def on_qr(self, data):
|
|
from electrum.bitcoin import is_address
|
|
data = data.strip()
|
|
if is_address(data):
|
|
self.set_URI(data)
|
|
return
|
|
if data.startswith('bitcoin:'):
|
|
self.set_URI(data)
|
|
return
|
|
bolt11_invoice = maybe_extract_bolt11_invoice(data)
|
|
if bolt11_invoice is not None:
|
|
self.set_ln_invoice(bolt11_invoice)
|
|
return
|
|
# try to decode transaction
|
|
from electrum.transaction import tx_from_any
|
|
try:
|
|
tx = tx_from_any(data)
|
|
except:
|
|
tx = None
|
|
if tx:
|
|
self.tx_dialog(tx)
|
|
return
|
|
# show error
|
|
self.show_error("Unable to decode QR data")
|
|
|
|
def update_tab(self, name):
|
|
s = getattr(self, name + '_screen', None)
|
|
if s:
|
|
s.update()
|
|
|
|
@profiler
|
|
def update_tabs(self):
|
|
for tab in ['invoices', 'send', 'history', 'receive', 'address']:
|
|
self.update_tab(tab)
|
|
|
|
def switch_to(self, name):
|
|
s = getattr(self, name + '_screen', None)
|
|
if s is None:
|
|
s = self.tabs.ids[name + '_screen']
|
|
s.load_screen()
|
|
panel = self.tabs.ids.panel
|
|
tab = self.tabs.ids[name + '_tab']
|
|
panel.switch_to(tab)
|
|
|
|
def show_request(self, is_lightning, key):
|
|
from .uix.dialogs.request_dialog import RequestDialog
|
|
request = self.wallet.get_request(key)
|
|
data = request['invoice'] if is_lightning else request['URI']
|
|
self.request_popup = RequestDialog('Request', data, key, is_lightning=is_lightning)
|
|
self.request_popup.set_status(request['status'])
|
|
self.request_popup.open()
|
|
|
|
def show_invoice(self, is_lightning, key):
|
|
from .uix.dialogs.invoice_dialog import InvoiceDialog
|
|
invoice = self.wallet.get_invoice(key)
|
|
if not invoice:
|
|
return
|
|
status = invoice['status']
|
|
data = invoice['invoice'] if is_lightning else key
|
|
self.invoice_popup = InvoiceDialog('Invoice', data, key)
|
|
self.invoice_popup.set_status(status)
|
|
self.invoice_popup.open()
|
|
|
|
def qr_dialog(self, title, data, show_text=False, text_for_clipboard=None):
|
|
from .uix.dialogs.qr_dialog import QRDialog
|
|
def on_qr_failure():
|
|
popup.dismiss()
|
|
msg = _('Failed to display QR code.')
|
|
if text_for_clipboard:
|
|
msg += '\n' + _('Text copied to clipboard.')
|
|
self._clipboard.copy(text_for_clipboard)
|
|
Clock.schedule_once(lambda dt: self.show_info(msg))
|
|
popup = QRDialog(title, data, show_text, failure_cb=on_qr_failure,
|
|
text_for_clipboard=text_for_clipboard)
|
|
popup.open()
|
|
|
|
def scan_qr(self, on_complete):
|
|
if platform != 'android':
|
|
return
|
|
from jnius import autoclass, cast
|
|
from android import activity
|
|
PythonActivity = autoclass('org.kivy.android.PythonActivity')
|
|
SimpleScannerActivity = autoclass("org.electrum.qr.SimpleScannerActivity")
|
|
Intent = autoclass('android.content.Intent')
|
|
intent = Intent(PythonActivity.mActivity, SimpleScannerActivity)
|
|
|
|
def on_qr_result(requestCode, resultCode, intent):
|
|
try:
|
|
if resultCode == -1: # RESULT_OK:
|
|
# this doesn't work due to some bug in jnius:
|
|
# contents = intent.getStringExtra("text")
|
|
String = autoclass("java.lang.String")
|
|
contents = intent.getStringExtra(String("text"))
|
|
on_complete(contents)
|
|
except Exception as e: # exc would otherwise get lost
|
|
send_exception_to_crash_reporter(e)
|
|
finally:
|
|
activity.unbind(on_activity_result=on_qr_result)
|
|
activity.bind(on_activity_result=on_qr_result)
|
|
PythonActivity.mActivity.startActivityForResult(intent, 0)
|
|
|
|
def do_share(self, data, title):
|
|
if platform != 'android':
|
|
return
|
|
from jnius import autoclass, cast
|
|
JS = autoclass('java.lang.String')
|
|
Intent = autoclass('android.content.Intent')
|
|
sendIntent = Intent()
|
|
sendIntent.setAction(Intent.ACTION_SEND)
|
|
sendIntent.setType("text/plain")
|
|
sendIntent.putExtra(Intent.EXTRA_TEXT, JS(data))
|
|
PythonActivity = autoclass('org.kivy.android.PythonActivity')
|
|
currentActivity = cast('android.app.Activity', PythonActivity.mActivity)
|
|
it = Intent.createChooser(sendIntent, cast('java.lang.CharSequence', JS(title)))
|
|
currentActivity.startActivity(it)
|
|
|
|
def build(self):
|
|
return Builder.load_file('electrum/gui/kivy/main.kv')
|
|
|
|
def _pause(self):
|
|
if platform == 'android':
|
|
# move activity to back
|
|
from jnius import autoclass
|
|
python_act = autoclass('org.kivy.android.PythonActivity')
|
|
mActivity = python_act.mActivity
|
|
mActivity.moveTaskToBack(True)
|
|
|
|
def handle_crash_on_startup(func):
|
|
def wrapper(self, *args, **kwargs):
|
|
try:
|
|
return func(self, *args, **kwargs)
|
|
except Exception as e:
|
|
from .uix.dialogs.crash_reporter import CrashReporter
|
|
# show the crash reporter, and when it's closed, shutdown the app
|
|
cr = CrashReporter(self, exctype=type(e), value=e, tb=e.__traceback__)
|
|
cr.on_dismiss = lambda: self.stop()
|
|
Clock.schedule_once(lambda _, cr=cr: cr.open(), 0)
|
|
return wrapper
|
|
|
|
@handle_crash_on_startup
|
|
def on_start(self):
|
|
''' This is the start point of the kivy ui
|
|
'''
|
|
import time
|
|
Logger.info('Time to on_start: {} <<<<<<<<'.format(time.process_time()))
|
|
Window.bind(size=self.on_size, on_keyboard=self.on_keyboard)
|
|
Window.bind(on_key_down=self.on_key_down)
|
|
#Window.softinput_mode = 'below_target'
|
|
self.on_size(Window, Window.size)
|
|
self.init_ui()
|
|
crash_reporter.ExceptionHook(self)
|
|
# init plugins
|
|
run_hook('init_kivy', self)
|
|
# fiat currency
|
|
self.fiat_unit = self.fx.ccy if self.fx.is_enabled() else ''
|
|
# default tab
|
|
self.switch_to('history')
|
|
# bind intent for bitcoin: URI scheme
|
|
if platform == 'android':
|
|
from android import activity
|
|
from jnius import autoclass
|
|
PythonActivity = autoclass('org.kivy.android.PythonActivity')
|
|
mactivity = PythonActivity.mActivity
|
|
self.on_new_intent(mactivity.getIntent())
|
|
activity.bind(on_new_intent=self.on_new_intent)
|
|
# connect callbacks
|
|
if self.network:
|
|
interests = ['wallet_updated', 'network_updated', 'blockchain_updated',
|
|
'status', 'new_transaction', 'verified']
|
|
self.network.register_callback(self.on_network_event, interests)
|
|
self.network.register_callback(self.on_fee, ['fee'])
|
|
self.network.register_callback(self.on_fee_histogram, ['fee_histogram'])
|
|
self.network.register_callback(self.on_quotes, ['on_quotes'])
|
|
self.network.register_callback(self.on_history, ['on_history'])
|
|
self.network.register_callback(self.on_channels, ['channels_updated'])
|
|
self.network.register_callback(self.on_channel, ['channel'])
|
|
self.network.register_callback(self.on_invoice_status, ['invoice_status'])
|
|
self.network.register_callback(self.on_request_status, ['request_status'])
|
|
self.network.register_callback(self.on_channel_db, ['channel_db'])
|
|
self.network.register_callback(self.set_num_peers, ['gossip_peers'])
|
|
self.network.register_callback(self.set_unknown_channels, ['unknown_channels'])
|
|
# load wallet
|
|
self.load_wallet_by_name(self.electrum_config.get_wallet_path(use_gui_last_wallet=True))
|
|
# URI passed in config
|
|
uri = self.electrum_config.get('url')
|
|
if uri:
|
|
self.set_URI(uri)
|
|
|
|
def on_channel_db(self, event, num_nodes, num_channels, num_policies):
|
|
self.lightning_gossip_num_nodes = num_nodes
|
|
self.lightning_gossip_num_channels = num_channels
|
|
|
|
def set_num_peers(self, event, num_peers):
|
|
self.lightning_gossip_num_peers = num_peers
|
|
|
|
def set_unknown_channels(self, event, unknown):
|
|
self.lightning_gossip_num_queries = unknown
|
|
|
|
def get_wallet_path(self):
|
|
if self.wallet:
|
|
return self.wallet.storage.path
|
|
else:
|
|
return ''
|
|
|
|
def on_wizard_complete(self, wizard, storage, db):
|
|
if storage:
|
|
wallet = Wallet(db, storage, config=self.electrum_config)
|
|
wallet.start_network(self.daemon.network)
|
|
self.daemon.add_wallet(wallet)
|
|
self.load_wallet(wallet)
|
|
elif not self.wallet:
|
|
# wizard did not return a wallet; and there is no wallet open atm
|
|
# try to open last saved wallet (potentially start wizard again)
|
|
self.load_wallet_by_name(self.electrum_config.get_wallet_path(use_gui_last_wallet=True),
|
|
ask_if_wizard=True)
|
|
|
|
def _on_decrypted_storage(self, storage: WalletStorage):
|
|
assert storage.is_past_initial_decryption()
|
|
db = WalletDB(storage.read(), manual_upgrades=False)
|
|
if db.requires_upgrade():
|
|
wizard = Factory.InstallWizard(self.electrum_config, self.plugins)
|
|
wizard.path = storage.path
|
|
wizard.bind(on_wizard_complete=self.on_wizard_complete)
|
|
wizard.upgrade_storage(storage, db)
|
|
else:
|
|
self.on_wizard_complete(None, storage, db)
|
|
|
|
def load_wallet_by_name(self, path, ask_if_wizard=False):
|
|
if not path:
|
|
return
|
|
if self.wallet and self.wallet.storage.path == path:
|
|
return
|
|
wallet = self.daemon.load_wallet(path, None)
|
|
if wallet:
|
|
if platform == 'android' and wallet.has_password():
|
|
self.password_dialog(wallet=wallet, msg=_('Enter PIN code'),
|
|
on_success=lambda x: self.load_wallet(wallet), on_failure=self.stop)
|
|
else:
|
|
self.load_wallet(wallet)
|
|
else:
|
|
def launch_wizard():
|
|
storage = WalletStorage(path)
|
|
if not storage.file_exists():
|
|
wizard = Factory.InstallWizard(self.electrum_config, self.plugins)
|
|
wizard.path = path
|
|
wizard.bind(on_wizard_complete=self.on_wizard_complete)
|
|
wizard.run('new')
|
|
else:
|
|
if storage.is_encrypted():
|
|
if not storage.is_encrypted_with_user_pw():
|
|
raise Exception("Kivy GUI does not support this type of encrypted wallet files.")
|
|
def on_password(pw):
|
|
storage.decrypt(pw)
|
|
self._on_decrypted_storage(storage)
|
|
self.password_dialog(wallet=storage, msg=_('Enter PIN code'),
|
|
on_success=on_password, on_failure=self.stop)
|
|
return
|
|
self._on_decrypted_storage(storage)
|
|
if not ask_if_wizard:
|
|
launch_wizard()
|
|
else:
|
|
def handle_answer(b: bool):
|
|
if b:
|
|
launch_wizard()
|
|
else:
|
|
try: os.unlink(path)
|
|
except FileNotFoundError: pass
|
|
self.stop()
|
|
d = Question(_('Do you want to launch the wizard again?'), handle_answer)
|
|
d.open()
|
|
|
|
def on_stop(self):
|
|
Logger.info('on_stop')
|
|
self.stop_wallet()
|
|
|
|
def stop_wallet(self):
|
|
if self.wallet:
|
|
self.daemon.stop_wallet(self.wallet.storage.path)
|
|
self.wallet = None
|
|
|
|
def on_key_down(self, instance, key, keycode, codepoint, modifiers):
|
|
if 'ctrl' in modifiers:
|
|
# q=24 w=25
|
|
if keycode in (24, 25):
|
|
self.stop()
|
|
elif keycode == 27:
|
|
# r=27
|
|
# force update wallet
|
|
self.update_wallet()
|
|
elif keycode == 112:
|
|
# pageup
|
|
#TODO move to next tab
|
|
pass
|
|
elif keycode == 117:
|
|
# pagedown
|
|
#TODO move to prev tab
|
|
pass
|
|
#TODO: alt+tab_number to activate the particular tab
|
|
|
|
def on_keyboard(self, instance, key, keycode, codepoint, modifiers):
|
|
if key == 27 and self.is_exit is False:
|
|
self.is_exit = True
|
|
self.show_info(_('Press again to exit'))
|
|
return True
|
|
# override settings button
|
|
if key in (319, 282): #f1/settings button on android
|
|
#self.gui.main_gui.toggle_settings(self)
|
|
return True
|
|
|
|
def settings_dialog(self):
|
|
from .uix.dialogs.settings import SettingsDialog
|
|
if self._settings_dialog is None:
|
|
self._settings_dialog = SettingsDialog(self)
|
|
self._settings_dialog.update()
|
|
self._settings_dialog.open()
|
|
|
|
def lightning_open_channel_dialog(self):
|
|
d = LightningOpenChannelDialog(self)
|
|
d.open()
|
|
|
|
def lightning_channels_dialog(self):
|
|
if not self.wallet.has_lightning():
|
|
self.show_error('Lightning not enabled on this wallet')
|
|
return
|
|
if self._channels_dialog is None:
|
|
self._channels_dialog = LightningChannelsDialog(self)
|
|
self._channels_dialog.open()
|
|
|
|
def on_channel(self, evt, chan):
|
|
if self._channels_dialog:
|
|
Clock.schedule_once(lambda dt: self._channels_dialog.update())
|
|
|
|
def on_channels(self, evt, wallet):
|
|
if self._channels_dialog:
|
|
Clock.schedule_once(lambda dt: self._channels_dialog.update())
|
|
|
|
def popup_dialog(self, name):
|
|
if name == 'settings':
|
|
self.settings_dialog()
|
|
elif name == 'wallets':
|
|
from .uix.dialogs.wallets import WalletDialog
|
|
d = WalletDialog()
|
|
d.open()
|
|
elif name == 'status':
|
|
popup = Builder.load_file('electrum/gui/kivy/uix/ui_screens/'+name+'.kv')
|
|
master_public_keys_layout = popup.ids.master_public_keys
|
|
for xpub in self.wallet.get_master_public_keys()[1:]:
|
|
master_public_keys_layout.add_widget(TopLabel(text=_('Master Public Key')))
|
|
ref = RefLabel()
|
|
ref.name = _('Master Public Key')
|
|
ref.data = xpub
|
|
master_public_keys_layout.add_widget(ref)
|
|
popup.open()
|
|
elif name.endswith("_dialog"):
|
|
getattr(self, name)()
|
|
else:
|
|
popup = Builder.load_file('electrum/gui/kivy/uix/ui_screens/'+name+'.kv')
|
|
popup.open()
|
|
|
|
@profiler
|
|
def init_ui(self):
|
|
''' Initialize The Ux part of electrum. This function performs the basic
|
|
tasks of setting up the ui.
|
|
'''
|
|
#from weakref import ref
|
|
|
|
self.funds_error = False
|
|
# setup UX
|
|
self.screens = {}
|
|
|
|
#setup lazy imports for mainscreen
|
|
Factory.register('AnimatedPopup',
|
|
module='electrum.gui.kivy.uix.dialogs')
|
|
Factory.register('QRCodeWidget',
|
|
module='electrum.gui.kivy.uix.qrcodewidget')
|
|
|
|
# preload widgets. Remove this if you want to load the widgets on demand
|
|
#Cache.append('electrum_widgets', 'AnimatedPopup', Factory.AnimatedPopup())
|
|
#Cache.append('electrum_widgets', 'QRCodeWidget', Factory.QRCodeWidget())
|
|
|
|
# load and focus the ui
|
|
self.root.manager = self.root.ids['manager']
|
|
|
|
self.history_screen = None
|
|
self.contacts_screen = None
|
|
self.send_screen = None
|
|
self.invoices_screen = None
|
|
self.receive_screen = None
|
|
self.requests_screen = None
|
|
self.address_screen = None
|
|
self.icon = "electrum/gui/icons/electrum.png"
|
|
self.tabs = self.root.ids['tabs']
|
|
|
|
def update_interfaces(self, dt):
|
|
net_params = self.network.get_parameters()
|
|
self.num_nodes = len(self.network.get_interfaces())
|
|
self.num_chains = len(self.network.get_blockchains())
|
|
chain = self.network.blockchain()
|
|
self.blockchain_forkpoint = chain.get_max_forkpoint()
|
|
self.blockchain_name = chain.get_name()
|
|
interface = self.network.interface
|
|
if interface:
|
|
self.server_host = interface.host
|
|
else:
|
|
self.server_host = str(net_params.host) + ' (connecting...)'
|
|
self.proxy_config = net_params.proxy or {}
|
|
self.update_proxy_str(self.proxy_config)
|
|
|
|
def on_network_event(self, event, *args):
|
|
Logger.info('network event: '+ event)
|
|
if event == 'network_updated':
|
|
self._trigger_update_interfaces()
|
|
self._trigger_update_status()
|
|
elif event == 'wallet_updated':
|
|
self._trigger_update_wallet()
|
|
self._trigger_update_status()
|
|
elif event == 'blockchain_updated':
|
|
# to update number of confirmations in history
|
|
self._trigger_update_wallet()
|
|
elif event == 'status':
|
|
self._trigger_update_status()
|
|
elif event == 'new_transaction':
|
|
self._trigger_update_wallet()
|
|
elif event == 'verified':
|
|
self._trigger_update_wallet()
|
|
|
|
@profiler
|
|
def load_wallet(self, wallet: 'Abstract_Wallet'):
|
|
if self.wallet:
|
|
self.stop_wallet()
|
|
self.wallet = wallet
|
|
self.wallet_name = wallet.basename()
|
|
self.update_wallet()
|
|
# Once GUI has been initialized check if we want to announce something
|
|
# since the callback has been called before the GUI was initialized
|
|
if self.receive_screen:
|
|
self.receive_screen.clear()
|
|
self.update_tabs()
|
|
run_hook('load_wallet', wallet, self)
|
|
try:
|
|
wallet.try_detecting_internal_addresses_corruption()
|
|
except InternalAddressCorruption as e:
|
|
self.show_error(str(e))
|
|
send_exception_to_crash_reporter(e)
|
|
return
|
|
self.use_change = self.wallet.use_change
|
|
self.electrum_config.save_last_wallet(wallet)
|
|
|
|
def update_status(self, *dt):
|
|
if not self.wallet:
|
|
return
|
|
if self.network is None or not self.network.is_connected():
|
|
status = _("Offline")
|
|
elif self.network.is_connected():
|
|
self.num_blocks = self.network.get_local_height()
|
|
server_height = self.network.get_server_height()
|
|
server_lag = self.num_blocks - server_height
|
|
if not self.wallet.up_to_date or server_height == 0:
|
|
num_sent, num_answered = self.wallet.get_history_sync_state_details()
|
|
status = ("{} [size=18dp]({}/{})[/size]"
|
|
.format(_("Synchronizing..."), num_answered, num_sent))
|
|
elif server_lag > 1:
|
|
status = _("Server is lagging ({} blocks)").format(server_lag)
|
|
else:
|
|
status = ''
|
|
else:
|
|
status = _("Disconnected")
|
|
if status:
|
|
self.balance = status
|
|
self.fiat_balance = status
|
|
else:
|
|
c, u, x = self.wallet.get_balance()
|
|
text = self.format_amount(c+x+u)
|
|
self.balance = str(text.strip()) + ' [size=22dp]%s[/size]'% self.base_unit
|
|
self.fiat_balance = self.fx.format_amount(c+u+x) + ' [size=22dp]%s[/size]'% self.fx.ccy
|
|
|
|
def update_wallet_synchronizing_progress(self, *dt):
|
|
if not self.wallet:
|
|
return
|
|
if not self.wallet.up_to_date:
|
|
self._trigger_update_status()
|
|
|
|
def get_max_amount(self):
|
|
from electrum.transaction import PartialTxOutput
|
|
if run_hook('abort_send', self):
|
|
return ''
|
|
inputs = self.wallet.get_spendable_coins(None)
|
|
if not inputs:
|
|
return ''
|
|
addr = None
|
|
if self.send_screen:
|
|
addr = str(self.send_screen.screen.address)
|
|
if not addr:
|
|
addr = self.wallet.dummy_address()
|
|
outputs = [PartialTxOutput.from_address_and_value(addr, '!')]
|
|
try:
|
|
tx = self.wallet.make_unsigned_transaction(coins=inputs, outputs=outputs)
|
|
except NoDynamicFeeEstimates as e:
|
|
Clock.schedule_once(lambda dt, bound_e=e: self.show_error(str(bound_e)))
|
|
return ''
|
|
except NotEnoughFunds:
|
|
return ''
|
|
except InternalAddressCorruption as e:
|
|
self.show_error(str(e))
|
|
send_exception_to_crash_reporter(e)
|
|
return ''
|
|
amount = tx.output_value()
|
|
__, x_fee_amount = run_hook('get_tx_extra_fee', self.wallet, tx) or (None, 0)
|
|
amount_after_all_fees = amount - x_fee_amount
|
|
return format_satoshis_plain(amount_after_all_fees, self.decimal_point())
|
|
|
|
def format_amount(self, x, is_diff=False, whitespaces=False):
|
|
return format_satoshis(x, 0, self.decimal_point(), is_diff=is_diff, whitespaces=whitespaces)
|
|
|
|
def format_amount_and_units(self, x):
|
|
return format_satoshis_plain(x, self.decimal_point()) + ' ' + self.base_unit
|
|
|
|
def format_fee_rate(self, fee_rate):
|
|
# fee_rate is in sat/kB
|
|
return format_fee_satoshis(fee_rate/1000) + ' sat/byte'
|
|
|
|
#@profiler
|
|
def update_wallet(self, *dt):
|
|
self._trigger_update_status()
|
|
if self.wallet and (self.wallet.up_to_date or not self.network or not self.network.is_connected()):
|
|
self.update_tabs()
|
|
|
|
def notify(self, message):
|
|
try:
|
|
global notification, os
|
|
if not notification:
|
|
from plyer import notification
|
|
icon = (os.path.dirname(os.path.realpath(__file__))
|
|
+ '/../../' + self.icon)
|
|
notification.notify('Electrum', message,
|
|
app_icon=icon, app_name='Electrum')
|
|
except ImportError:
|
|
Logger.Error('Notification: needs plyer; `sudo python3 -m pip install plyer`')
|
|
|
|
def on_pause(self):
|
|
self.pause_time = time.time()
|
|
# pause nfc
|
|
if self.nfcscanner:
|
|
self.nfcscanner.nfc_disable()
|
|
return True
|
|
|
|
def on_resume(self):
|
|
now = time.time()
|
|
if self.wallet and self.wallet.has_password() and now - self.pause_time > 60:
|
|
self.password_dialog(wallet=self.wallet, msg=_('Enter PIN'), on_success=None, on_failure=self.stop)
|
|
if self.nfcscanner:
|
|
self.nfcscanner.nfc_enable()
|
|
|
|
def on_size(self, instance, value):
|
|
width, height = value
|
|
self._orientation = 'landscape' if width > height else 'portrait'
|
|
self._ui_mode = 'tablet' if min(width, height) > inch(3.51) else 'phone'
|
|
|
|
def on_ref_label(self, label):
|
|
if not label.data:
|
|
return
|
|
self.qr_dialog(label.name, label.data, True)
|
|
|
|
def show_error(self, error, width='200dp', pos=None, arrow_pos=None,
|
|
exit=False, icon='atlas://electrum/gui/kivy/theming/light/error', duration=0,
|
|
modal=False):
|
|
''' Show an error Message Bubble.
|
|
'''
|
|
self.show_info_bubble( text=error, icon=icon, width=width,
|
|
pos=pos or Window.center, arrow_pos=arrow_pos, exit=exit,
|
|
duration=duration, modal=modal)
|
|
|
|
def show_info(self, error, width='200dp', pos=None, arrow_pos=None,
|
|
exit=False, duration=0, modal=False):
|
|
''' Show an Info Message Bubble.
|
|
'''
|
|
self.show_error(error, icon='atlas://electrum/gui/kivy/theming/light/important',
|
|
duration=duration, modal=modal, exit=exit, pos=pos,
|
|
arrow_pos=arrow_pos)
|
|
|
|
def show_info_bubble(self, text=_('Hello World'), pos=None, duration=0,
|
|
arrow_pos='bottom_mid', width=None, icon='', modal=False, exit=False):
|
|
'''Method to show an Information Bubble
|
|
|
|
.. parameters::
|
|
text: Message to be displayed
|
|
pos: position for the bubble
|
|
duration: duration the bubble remains on screen. 0 = click to hide
|
|
width: width of the Bubble
|
|
arrow_pos: arrow position for the bubble
|
|
'''
|
|
info_bubble = self.info_bubble
|
|
if not info_bubble:
|
|
info_bubble = self.info_bubble = Factory.InfoBubble()
|
|
|
|
win = Window
|
|
if info_bubble.parent:
|
|
win.remove_widget(info_bubble
|
|
if not info_bubble.modal else
|
|
info_bubble._modal_view)
|
|
|
|
if not arrow_pos:
|
|
info_bubble.show_arrow = False
|
|
else:
|
|
info_bubble.show_arrow = True
|
|
info_bubble.arrow_pos = arrow_pos
|
|
img = info_bubble.ids.img
|
|
if text == 'texture':
|
|
# icon holds a texture not a source image
|
|
# display the texture in full screen
|
|
text = ''
|
|
img.texture = icon
|
|
info_bubble.fs = True
|
|
info_bubble.show_arrow = False
|
|
img.allow_stretch = True
|
|
info_bubble.dim_background = True
|
|
info_bubble.background_image = 'atlas://electrum/gui/kivy/theming/light/card'
|
|
else:
|
|
info_bubble.fs = False
|
|
info_bubble.icon = icon
|
|
#if img.texture and img._coreimage:
|
|
# img.reload()
|
|
img.allow_stretch = False
|
|
info_bubble.dim_background = False
|
|
info_bubble.background_image = 'atlas://data/images/defaulttheme/bubble'
|
|
info_bubble.message = text
|
|
if not pos:
|
|
pos = (win.center[0], win.center[1] - (info_bubble.height/2))
|
|
info_bubble.show(pos, duration, width, modal=modal, exit=exit)
|
|
|
|
def tx_dialog(self, tx):
|
|
from .uix.dialogs.tx_dialog import TxDialog
|
|
d = TxDialog(self, tx)
|
|
d.open()
|
|
|
|
def sign_tx(self, *args):
|
|
threading.Thread(target=self._sign_tx, args=args).start()
|
|
|
|
def _sign_tx(self, tx, password, on_success, on_failure):
|
|
try:
|
|
self.wallet.sign_transaction(tx, password)
|
|
except InvalidPassword:
|
|
Clock.schedule_once(lambda dt: on_failure(_("Invalid PIN")))
|
|
return
|
|
on_success = run_hook('tc_sign_wrapper', self.wallet, tx, on_success, on_failure) or on_success
|
|
Clock.schedule_once(lambda dt: on_success(tx))
|
|
|
|
def _broadcast_thread(self, tx, on_complete):
|
|
status = False
|
|
try:
|
|
self.network.run_from_another_thread(self.network.broadcast_transaction(tx))
|
|
except TxBroadcastError as e:
|
|
msg = e.get_message_for_gui()
|
|
except BestEffortRequestFailed as e:
|
|
msg = repr(e)
|
|
else:
|
|
status, msg = True, tx.txid()
|
|
Clock.schedule_once(lambda dt: on_complete(status, msg))
|
|
|
|
def broadcast(self, tx):
|
|
def on_complete(ok, msg):
|
|
if ok:
|
|
self.show_info(_('Payment sent.'))
|
|
if self.send_screen:
|
|
self.send_screen.do_clear()
|
|
else:
|
|
msg = msg or ''
|
|
self.show_error(msg)
|
|
|
|
if self.network and self.network.is_connected():
|
|
self.show_info(_('Sending'))
|
|
threading.Thread(target=self._broadcast_thread, args=(tx, on_complete)).start()
|
|
else:
|
|
self.show_info(_('Cannot broadcast transaction') + ':\n' + _('Not connected'))
|
|
|
|
def description_dialog(self, screen):
|
|
from .uix.dialogs.label_dialog import LabelDialog
|
|
text = screen.message
|
|
def callback(text):
|
|
screen.message = text
|
|
d = LabelDialog(_('Enter description'), text, callback)
|
|
d.open()
|
|
|
|
def amount_dialog(self, screen, show_max):
|
|
from .uix.dialogs.amount_dialog import AmountDialog
|
|
amount = screen.amount
|
|
if amount:
|
|
amount, u = str(amount).split()
|
|
assert u == self.base_unit
|
|
def cb(amount):
|
|
screen.amount = amount
|
|
popup = AmountDialog(show_max, amount, cb)
|
|
popup.open()
|
|
|
|
def addresses_dialog(self):
|
|
from .uix.dialogs.addresses import AddressesDialog
|
|
if self._addresses_dialog is None:
|
|
self._addresses_dialog = AddressesDialog(self)
|
|
self._addresses_dialog.update()
|
|
self._addresses_dialog.open()
|
|
|
|
def fee_dialog(self, label, dt):
|
|
from .uix.dialogs.fee_dialog import FeeDialog
|
|
def cb():
|
|
self.fee_status = self.electrum_config.get_fee_status()
|
|
fee_dialog = FeeDialog(self, self.electrum_config, cb)
|
|
fee_dialog.open()
|
|
|
|
def on_fee(self, event, *arg):
|
|
self.fee_status = self.electrum_config.get_fee_status()
|
|
|
|
def protected(self, msg, f, args):
|
|
if self.wallet.has_password():
|
|
on_success = lambda pw: f(*(args + (pw,)))
|
|
self.password_dialog(wallet=self.wallet, msg=msg, on_success=on_success, on_failure=lambda: None)
|
|
else:
|
|
f(*(args + (None,)))
|
|
|
|
def toggle_lightning(self):
|
|
if self.wallet.has_lightning():
|
|
if not bool(self.wallet.lnworker.channels):
|
|
warning = _('This will delete your lightning private keys')
|
|
d = Question(_('Disable Lightning?') + '\n\n' + warning, self._disable_lightning)
|
|
d.open()
|
|
else:
|
|
self.show_info('This wallet has channels')
|
|
else:
|
|
warning1 = _("Lightning support in Electrum is experimental. Do not put large amounts in lightning channels.")
|
|
warning2 = _("Funds stored in lightning channels are not recoverable from your seed. You must backup your wallet file everytime you create a new channel.")
|
|
d = Question(_('Enable Lightning?') + '\n\n' + warning1 + '\n\n' + warning2, self._enable_lightning)
|
|
d.open()
|
|
|
|
def _enable_lightning(self, b):
|
|
if not b:
|
|
return
|
|
wallet_path = self.get_wallet_path()
|
|
self.wallet.init_lightning()
|
|
self.show_info(_('Lightning keys have been initialized.'))
|
|
self.stop_wallet()
|
|
self.load_wallet_by_name(wallet_path)
|
|
|
|
def _disable_lightning(self, b):
|
|
if not b:
|
|
return
|
|
wallet_path = self.get_wallet_path()
|
|
self.wallet.remove_lightning()
|
|
self.show_info(_('Lightning keys have been removed.'))
|
|
self.stop_wallet()
|
|
self.load_wallet_by_name(wallet_path)
|
|
|
|
def delete_wallet(self):
|
|
basename = os.path.basename(self.wallet.storage.path)
|
|
d = Question(_('Delete wallet?') + '\n' + basename, self._delete_wallet)
|
|
d.open()
|
|
|
|
def _delete_wallet(self, b):
|
|
if b:
|
|
basename = self.wallet.basename()
|
|
self.protected(_("Enter your PIN code to confirm deletion of {}").format(basename), self.__delete_wallet, ())
|
|
|
|
def __delete_wallet(self, pw):
|
|
wallet_path = self.get_wallet_path()
|
|
basename = os.path.basename(wallet_path)
|
|
if self.wallet.has_password():
|
|
try:
|
|
self.wallet.check_password(pw)
|
|
except:
|
|
self.show_error("Invalid PIN")
|
|
return
|
|
self.stop_wallet()
|
|
os.unlink(wallet_path)
|
|
self.show_error(_("Wallet removed: {}").format(basename))
|
|
new_path = self.electrum_config.get_wallet_path(use_gui_last_wallet=True)
|
|
self.load_wallet_by_name(new_path)
|
|
|
|
def show_seed(self, label):
|
|
self.protected(_("Enter your PIN code in order to decrypt your seed"), self._show_seed, (label,))
|
|
|
|
def _show_seed(self, label, password):
|
|
if self.wallet.has_password() and password is None:
|
|
return
|
|
keystore = self.wallet.keystore
|
|
try:
|
|
seed = keystore.get_seed(password)
|
|
passphrase = keystore.get_passphrase(password)
|
|
except:
|
|
self.show_error("Invalid PIN")
|
|
return
|
|
label.data = seed
|
|
if passphrase:
|
|
label.data += '\n\n' + _('Passphrase') + ': ' + passphrase
|
|
|
|
def password_dialog(self, *, wallet: Union[Abstract_Wallet, WalletStorage],
|
|
msg: str, on_success: Callable = None, on_failure: Callable = None):
|
|
from .uix.dialogs.password_dialog import PasswordDialog
|
|
if self._password_dialog is None:
|
|
self._password_dialog = PasswordDialog()
|
|
self._password_dialog.init(self, wallet=wallet, msg=msg,
|
|
on_success=on_success, on_failure=on_failure)
|
|
self._password_dialog.open()
|
|
|
|
def change_password(self, cb):
|
|
from .uix.dialogs.password_dialog import PasswordDialog
|
|
if self._password_dialog is None:
|
|
self._password_dialog = PasswordDialog()
|
|
message = _("Changing PIN code.") + '\n' + _("Enter your current PIN:")
|
|
def on_success(old_password, new_password):
|
|
self.wallet.update_password(old_password, new_password)
|
|
self.show_info(_("Your PIN code was updated"))
|
|
on_failure = lambda: self.show_error(_("PIN codes do not match"))
|
|
self._password_dialog.init(self, wallet=self.wallet, msg=message,
|
|
on_success=on_success, on_failure=on_failure, is_change=1)
|
|
self._password_dialog.open()
|
|
|
|
def export_private_keys(self, pk_label, addr):
|
|
if self.wallet.is_watching_only():
|
|
self.show_info(_('This is a watching-only wallet. It does not contain private keys.'))
|
|
return
|
|
def show_private_key(addr, pk_label, password):
|
|
if self.wallet.has_password() and password is None:
|
|
return
|
|
if not self.wallet.can_export():
|
|
return
|
|
try:
|
|
key = str(self.wallet.export_private_key(addr, password))
|
|
pk_label.data = key
|
|
except InvalidPassword:
|
|
self.show_error("Invalid PIN")
|
|
return
|
|
self.protected(_("Enter your PIN code in order to decrypt your private key"), show_private_key, (addr, pk_label))
|