Browse Source

Merge pull request #7858 from spesmilo/event_listener

EventListener class to handle callbacks
patch-4
ghost43 3 years ago
committed by GitHub
parent
commit
c3093ded21
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 10
      electrum/address_synchronizer.py
  2. 8
      electrum/daemon.py
  3. 9
      electrum/exchange_rate.py
  4. 105
      electrum/gui/kivy/main_window.py
  5. 10
      electrum/gui/qt/__init__.py
  6. 40
      electrum/gui/qt/channel_details.py
  7. 22
      electrum/gui/qt/lightning_dialog.py
  8. 175
      electrum/gui/qt/main_window.py
  9. 21
      electrum/gui/qt/network_dialog.py
  10. 16
      electrum/gui/qt/settings_dialog.py
  11. 28
      electrum/gui/qt/util.py
  12. 22
      electrum/gui/stdio.py
  13. 15
      electrum/gui/text.py
  14. 33
      electrum/lnwatcher.py
  15. 10
      electrum/lnworker.py
  16. 6
      electrum/tests/test_lnpeer.py
  17. 41
      electrum/util.py
  18. 22
      electrum/wallet.py

10
electrum/address_synchronizer.py

@ -37,6 +37,7 @@ from .verifier import SPV
from .blockchain import hash_header, Blockchain
from .i18n import _
from .logging import Logger
from .util import EventListener, event_listener
if TYPE_CHECKING:
from .network import Network
@ -57,7 +58,7 @@ class HistoryItem(NamedTuple):
balance: int
class AddressSynchronizer(Logger):
class AddressSynchronizer(Logger, EventListener):
""" address database """
network: Optional['Network']
@ -181,9 +182,10 @@ class AddressSynchronizer(Logger):
self.synchronizer = Synchronizer(self)
self.verifier = SPV(self.network, self)
self.asyncio_loop = network.asyncio_loop
util.register_callback(self.on_blockchain_updated, ['blockchain_updated'])
self.register_callbacks()
def on_blockchain_updated(self, event, *args):
@event_listener
def on_event_blockchain_updated(self, *args):
self._get_balance_cache = {} # invalidate cache
async def stop(self):
@ -197,7 +199,7 @@ class AddressSynchronizer(Logger):
finally: # even if we get cancelled
self.synchronizer = None
self.verifier = None
util.unregister_callback(self.on_blockchain_updated)
self.unregister_callbacks()
self.db.put('stored_height', self.get_local_height())
def add_address(self, address):

8
electrum/daemon.py

@ -44,6 +44,7 @@ from .network import Network
from .util import (json_decode, to_bytes, to_string, profiler, standardize_path, constant_time_compare)
from .invoices import PR_PAID, PR_EXPIRED
from .util import log_exceptions, ignore_exceptions, randrange, OldTaskGroup
from .util import EventListener, event_listener
from .wallet import Wallet, Abstract_Wallet
from .storage import WalletStorage
from .wallet_db import WalletDB
@ -356,7 +357,7 @@ class WatchTowerServer(AuthenticatedServer):
return await self.lnwatcher.sweepstore.add_sweep_tx(*args)
class PayServer(Logger):
class PayServer(Logger, EventListener):
def __init__(self, daemon: 'Daemon', netaddress):
Logger.__init__(self)
@ -364,14 +365,15 @@ class PayServer(Logger):
self.daemon = daemon
self.config = daemon.config
self.pending = defaultdict(asyncio.Event)
util.register_callback(self.on_payment, ['request_status'])
self.register_callbacks()
@property
def wallet(self):
# FIXME specify wallet somehow?
return list(self.daemon.get_wallets().values())[0]
async def on_payment(self, evt, wallet, key, status):
@event_listener
async def on_event_request_status(self, wallet, key, status):
if status == PR_PAID:
self.pending[key].set()

9
electrum/exchange_rate.py

@ -17,7 +17,7 @@ from . import util
from .bitcoin import COIN
from .i18n import _
from .util import (ThreadJob, make_dir, log_exceptions, OldTaskGroup,
make_aiohttp_session, resource_path)
make_aiohttp_session, resource_path, EventListener, event_listener)
from .network import Network
from .simple_config import SimpleConfig
from .logging import Logger
@ -496,13 +496,13 @@ def get_exchanges_by_ccy(history=True):
return dictinvert(d)
class FxThread(ThreadJob):
class FxThread(ThreadJob, EventListener):
def __init__(self, config: SimpleConfig, network: Optional[Network]):
ThreadJob.__init__(self)
self.config = config
self.network = network
util.register_callback(self.set_proxy, ['proxy_set'])
self.register_callbacks()
self.ccy = self.get_currency()
self.history_used_spot = False
self.ccy_combo = None
@ -513,7 +513,8 @@ class FxThread(ThreadJob):
self.set_exchange(self.config_exchange())
make_dir(self.cache_dir)
def set_proxy(self, trigger_name, *args):
@event_listener
def on_event_proxy_set(self, *args):
self._trigger.set()
@staticmethod

105
electrum/gui/kivy/main_window.py

@ -19,6 +19,7 @@ from electrum import util
from electrum.util import (profiler, InvalidPassword, send_exception_to_crash_reporter,
format_satoshis, format_satoshis_plain, format_fee_satoshis,
maybe_extract_bolt11_invoice, parse_max_spend)
from electrum.util import EventListener, event_listener
from electrum.invoices import PR_PAID, PR_FAILED, Invoice
from electrum import blockchain
from electrum.network import Network, TxBroadcastError, BestEffortRequestFailed
@ -99,7 +100,7 @@ if TYPE_CHECKING:
from electrum.paymentrequest import PaymentRequest
class ElectrumWindow(App, Logger):
class ElectrumWindow(App, Logger, EventListener):
electrum_config = ObjectProperty(None)
language = StringProperty('en')
@ -252,21 +253,25 @@ class ElectrumWindow(App, Logger):
if self.history_screen:
self.history_screen.update()
def on_quotes(self, d):
@event_listener
def on_event_on_quotes(self):
self.logger.info("on_quotes")
self._trigger_update_status()
self._trigger_update_history()
def on_history(self, d):
@event_listener
def on_event_on_history(self):
self.logger.info("on_history")
if self.wallet:
self.wallet.clear_coin_price_cache()
self._trigger_update_history()
def on_fee_histogram(self, *args):
@event_listener
def on_event_fee_histogram(self, *args):
self._trigger_update_history()
def on_request_status(self, event, wallet, key, status):
@event_listener
def on_event_request_status(self, wallet, key, status):
if wallet != self.wallet:
return
req = self.wallet.receive_requests.get(key)
@ -283,7 +288,8 @@ class ElectrumWindow(App, Logger):
self.show_info(_('Payment Received') + '\n' + key)
self._trigger_update_history()
def on_invoice_status(self, event, wallet, key):
@event_listener
def on_event_invoice_status(self, wallet, key):
if wallet != self.wallet:
return
req = self.wallet.get_invoice(key)
@ -299,14 +305,16 @@ class ElectrumWindow(App, Logger):
if self.invoice_popup and self.invoice_popup.key == key:
self.invoice_popup.update_status()
def on_payment_succeeded(self, event, wallet, key):
@event_listener
def on_event_payment_succeeded(self, wallet, key):
if wallet != self.wallet:
return
description = self.wallet.get_label(key)
self.show_info(_('Payment succeeded') + '\n\n' + description)
self._trigger_update_history()
def on_payment_failed(self, event, wallet, key, reason):
@event_listener
def on_event_payment_failed(self, wallet, key, reason):
if wallet != self.wallet:
return
self.show_info(_('Payment failed') + '\n\n' + reason)
@ -647,25 +655,7 @@ class ElectrumWindow(App, Logger):
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']
util.register_callback(self.on_network_event, interests)
util.register_callback(self.on_fee, ['fee'])
util.register_callback(self.on_fee_histogram, ['fee_histogram'])
util.register_callback(self.on_quotes, ['on_quotes'])
util.register_callback(self.on_history, ['on_history'])
util.register_callback(self.on_channels, ['channels_updated'])
util.register_callback(self.on_channel, ['channel'])
util.register_callback(self.on_invoice_status, ['invoice_status'])
util.register_callback(self.on_request_status, ['request_status'])
util.register_callback(self.on_payment_failed, ['payment_failed'])
util.register_callback(self.on_payment_succeeded, ['payment_succeeded'])
util.register_callback(self.on_channel_db, ['channel_db'])
util.register_callback(self.set_num_peers, ['gossip_peers'])
util.register_callback(self.set_unknown_channels, ['unknown_channels'])
self.register_callbacks()
if self.network and self.electrum_config.get('auto_connect') is None:
self.popup_dialog("first_screen")
# load_wallet_on_start will be called later, after initial network setup is completed
@ -677,14 +667,17 @@ class ElectrumWindow(App, Logger):
if uri:
self.set_URI(uri)
def on_channel_db(self, event, num_nodes, num_channels, num_policies):
@event_listener
def on_event_channel_db(self, 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):
@event_listener
def on_event_gossip_peers(self, num_peers):
self.lightning_gossip_num_peers = num_peers
def set_unknown_channels(self, event, unknown):
@event_listener
def on_event_unknown_channels(self, unknown):
self.lightning_gossip_num_queries = unknown
def get_wallet_path(self):
@ -818,11 +811,13 @@ class ElectrumWindow(App, Logger):
delete_gossip)
d.open()
def on_channel(self, evt, wallet, chan):
@event_listener
def on_event_channel(self, wallet, chan):
if self._channels_dialog:
Clock.schedule_once(lambda dt: self._channels_dialog.update())
def on_channels(self, evt, wallet):
@event_listener
def on_event_channels(self, wallet):
if self._channels_dialog:
Clock.schedule_once(lambda dt: self._channels_dialog.update())
@ -904,23 +899,32 @@ class ElectrumWindow(App, Logger):
self.proxy_config = net_params.proxy or {}
self.update_proxy_str(self.proxy_config)
def on_network_event(self, event, *args):
self.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()
@event_listener
def on_event_network_updated(self):
self._trigger_update_interfaces()
self._trigger_update_status()
@event_listener
def on_event_wallet_updated(self, *args):
self._trigger_update_wallet()
self._trigger_update_status()
@event_listener
def on_event_blockchain_updated(self, *args):
# to update number of confirmations in history
self._trigger_update_wallet()
@event_listener
def on_event_status(self, *args):
self._trigger_update_status()
@event_listener
def on_event_new_transaction(self, *args):
self._trigger_update_wallet()
@event_listener
def on_event_verified(self, *args):
self._trigger_update_wallet()
@profiler
def load_wallet(self, wallet: 'Abstract_Wallet'):
@ -1265,7 +1269,8 @@ class ElectrumWindow(App, Logger):
target, tooltip, dyn = self.electrum_config.get_fee_target()
self.fee_status = target
def on_fee(self, event, *arg):
@event_listener
def on_event_fee(self, *arg):
self.set_fee_status()
def protected(self, msg, f, args):

10
electrum/gui/qt/__init__.py

@ -102,9 +102,6 @@ class QElectrumApplication(QApplication):
alias_received_signal = pyqtSignal()
class QNetworkUpdatedSignalObject(QObject):
network_updated_signal = pyqtSignal(str, object)
class ElectrumGui(BaseElectrumGui, Logger):
@ -142,7 +139,6 @@ class ElectrumGui(BaseElectrumGui, Logger):
self.network_dialog = None
self.lightning_dialog = None
self.watchtower_dialog = None
self.network_updated_signal_obj = QNetworkUpdatedSignalObject()
self._num_wizards_in_progress = 0
self._num_wizards_lock = threading.Lock()
self.dark_icon = self.config.get("dark_icon", False)
@ -251,7 +247,6 @@ class ElectrumGui(BaseElectrumGui, Logger):
self.network_dialog.close()
self.network_dialog.clean_up()
self.network_dialog = None
self.network_updated_signal_obj = None
if self.lightning_dialog:
self.lightning_dialog.close()
self.lightning_dialog = None
@ -298,14 +293,13 @@ class ElectrumGui(BaseElectrumGui, Logger):
def show_network_dialog(self):
if self.network_dialog:
self.network_dialog.on_update()
self.network_dialog.on_event_network_updated()
self.network_dialog.show()
self.network_dialog.raise_()
return
self.network_dialog = NetworkDialog(
network=self.daemon.network,
config=self.config,
network_updated_signal_obj=self.network_updated_signal_obj)
config=self.config)
self.network_dialog.show()
def _create_window_for_wallet(self, wallet):

40
electrum/gui/qt/channel_details.py

@ -5,7 +5,7 @@ import PyQt5.QtWidgets as QtWidgets
import PyQt5.QtCore as QtCore
from PyQt5.QtWidgets import QLabel, QLineEdit, QHBoxLayout
from electrum import util
from electrum.util import EventListener
from electrum.i18n import _
from electrum.util import bh2u, format_time
from electrum.lnutil import format_short_channel_id, LOCAL, REMOTE, UpdateAddHtlc, Direction
@ -15,6 +15,7 @@ from electrum.bitcoin import COIN
from electrum.wallet import Abstract_Wallet
from .util import Buttons, CloseButton, ButtonsLineEdit, MessageBoxMixin, WWLabel
from .util import QtEventListener, qt_event_listener
if TYPE_CHECKING:
from .main_window import ElectrumWindow
@ -35,7 +36,8 @@ class LinkedLabel(QtWidgets.QLabel):
self.linkActivated.connect(on_clicked)
class ChannelDetailsDialog(QtWidgets.QDialog, MessageBoxMixin):
class ChannelDetailsDialog(QtWidgets.QDialog, MessageBoxMixin, QtEventListener):
def make_htlc_item(self, i: UpdateAddHtlc, direction: Direction) -> HTLCItem:
it = HTLCItem(_('Sent HTLC with ID {}' if Direction.SENT == direction else 'Received HTLC with ID {}').format(i.htlc_id))
it.appendRow([HTLCItem(_('Amount')),HTLCItem(self.format_msat(i.amount_msat))])
@ -83,35 +85,28 @@ class ChannelDetailsDialog(QtWidgets.QDialog, MessageBoxMixin):
dest_mapping = self.keyname_rows[to]
dest_mapping[payment_hash] = len(dest_mapping)
htlc_fulfilled = QtCore.pyqtSignal(str, bytes, Channel, int)
htlc_failed = QtCore.pyqtSignal(str, bytes, Channel, int)
htlc_added = QtCore.pyqtSignal(str, Channel, UpdateAddHtlc, Direction)
state_changed = QtCore.pyqtSignal(str, Abstract_Wallet, AbstractChannel)
@QtCore.pyqtSlot(str, Abstract_Wallet, AbstractChannel)
def do_state_changed(self, wallet, chan):
if wallet != self.wallet:
return
@qt_event_listener
def on_event_channel(self, wallet, chan):
if chan == self.chan:
self.update()
@QtCore.pyqtSlot(str, Channel, UpdateAddHtlc, Direction)
def on_htlc_added(self, evtname, chan, htlc, direction):
@qt_event_listener
def on_event_htlc_added(self, chan, htlc, direction):
if chan != self.chan:
return
mapping = self.keyname_rows['inflight']
mapping[htlc.payment_hash] = len(mapping)
self.folders['inflight'].appendRow(self.make_htlc_item(htlc, direction))
@QtCore.pyqtSlot(str, bytes, Channel, int)
def on_htlc_fulfilled(self, evtname, payment_hash, chan, htlc_id):
@qt_event_listener
def on_event_htlc_fulfilled(self, payment_hash, chan, htlc_id):
if chan.channel_id != self.chan.channel_id:
return
self.move('inflight', 'settled', payment_hash)
self.update()
@QtCore.pyqtSlot(str, bytes, Channel, int)
def on_htlc_failed(self, evtname, payment_hash, chan, htlc_id):
@qt_event_listener
def on_event_htlc_failed(self, payment_hash, chan, htlc_id):
if chan.channel_id != self.chan.channel_id:
return
self.move('inflight', 'failed', payment_hash)
@ -143,17 +138,8 @@ class ChannelDetailsDialog(QtWidgets.QDialog, MessageBoxMixin):
self.format_msat = lambda msat: window.format_amount_and_units(msat / 1000)
self.format_sat = lambda sat: window.format_amount_and_units(sat)
# connect signals with slots
self.htlc_fulfilled.connect(self.on_htlc_fulfilled)
self.htlc_failed.connect(self.on_htlc_failed)
self.state_changed.connect(self.do_state_changed)
self.htlc_added.connect(self.on_htlc_added)
# register callbacks for updating
util.register_callback(self.htlc_fulfilled.emit, ['htlc_fulfilled'])
util.register_callback(self.htlc_failed.emit, ['htlc_failed'])
util.register_callback(self.htlc_added.emit, ['htlc_added'])
util.register_callback(self.state_changed.emit, ['channel'])
self.register_callbacks()
# set attributes of QDialog
self.setWindowTitle(_('Channel Details'))

22
electrum/gui/qt/lightning_dialog.py

@ -27,16 +27,16 @@ from typing import TYPE_CHECKING
from PyQt5.QtWidgets import (QDialog, QLabel, QVBoxLayout, QPushButton)
from electrum import util
from electrum.i18n import _
from .util import Buttons
from .util import QtEventListener, qt_event_listener
if TYPE_CHECKING:
from . import ElectrumGui
class LightningDialog(QDialog):
class LightningDialog(QDialog, QtEventListener):
def __init__(self, gui_object: 'ElectrumGui'):
QDialog.__init__(self)
@ -59,24 +59,25 @@ class LightningDialog(QDialog):
b = QPushButton(_('Close'))
b.clicked.connect(self.close)
vbox.addLayout(Buttons(b))
util.register_callback(self.on_channel_db, ['channel_db'])
util.register_callback(self.set_num_peers, ['gossip_peers'])
util.register_callback(self.set_unknown_channels, ['unknown_channels'])
self.register_callbacks()
self.network.channel_db.update_counts() # trigger callback
if self.network.lngossip:
self.set_num_peers('', self.network.lngossip.num_peers())
self.set_unknown_channels('', len(self.network.lngossip.unknown_ids))
self.on_event_gossip_peers(self.network.lngossip.num_peers())
self.on_event_unknown_channels(len(self.network.lngossip.unknown_ids))
else:
self.num_peers.setText(_('Lightning gossip not active.'))
def on_channel_db(self, event, num_nodes, num_channels, num_policies):
@qt_event_listener
def on_event_channel_db(self, num_nodes, num_channels, num_policies):
self.num_nodes.setText(_('{} nodes').format(num_nodes))
self.num_channels.setText(_('{} channels').format(num_channels))
def set_num_peers(self, event, num_peers):
@qt_event_listener
def on_event_gossip_peers(self, num_peers):
self.num_peers.setText(_('Connected to {} peers').format(num_peers))
def set_unknown_channels(self, event, unknown):
@qt_event_listener
def on_event_unknown_channels(self, unknown):
self.status.setText(_('Requesting {} channels...').format(unknown) if unknown else '')
def is_hidden(self):
@ -93,5 +94,6 @@ class LightningDialog(QDialog):
self.raise_()
def closeEvent(self, event):
self.unregister_callbacks()
self.gui_object.lightning_dialog = None
event.accept()

175
electrum/gui/qt/main_window.py

@ -193,12 +193,12 @@ def protected(func):
return func(self, *args, **kwargs)
return request_password
from .util import QtEventListener, qt_event_listener, event_listener
class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
payment_request_ok_signal = pyqtSignal()
payment_request_error_signal = pyqtSignal()
network_signal = pyqtSignal(str, object)
#ln_payment_attempt_signal = pyqtSignal(str)
computing_privkeys_signal = pyqtSignal()
show_privkeys_signal = pyqtSignal()
@ -208,7 +208,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
def __init__(self, gui_object: 'ElectrumGui', wallet: Abstract_Wallet):
QMainWindow.__init__(self)
self.gui_object = gui_object
self.config = config = gui_object.config # type: SimpleConfig
self.gui_thread = gui_object.gui_thread
@ -314,21 +313,9 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
self.history_list.setFocus(True)
# network callbacks
if self.network:
self.network_signal.connect(self.on_network_qt)
interests = ['wallet_updated', 'network_updated', 'blockchain_updated',
'new_transaction', 'status',
'banner', 'verified', 'fee', 'fee_histogram', 'on_quotes',
'on_history', 'channel', 'channels_updated',
'payment_failed', 'payment_succeeded',
'invoice_status', 'request_status', 'ln_gossip_sync_progress',
'cert_mismatch', 'gossip_db_loaded']
# To avoid leaking references to "self" that prevent the
# window from being GC-ed when closed, callbacks should be
# methods of this class only, and specifically not be
# partials, lambdas or methods of subobjects. Hence...
util.register_callback(self.on_network, interests)
# set initial message
self.register_callbacks()
# banner may already be there
if self.network and self.network.banner:
self.console.showMessage(self.network.banner)
# update fee slider in case we missed the callback
@ -463,74 +450,75 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
pass # see #4418
self.show_error(repr(e))
def on_network(self, event, *args):
# Handle in GUI thread
self.network_signal.emit(event, args)
@event_listener
def on_event_wallet_updated(self, wallet):
if wallet == self.wallet:
self.need_update.set()
@event_listener
def on_event_new_transaction(self, wallet, tx):
if wallet == self.wallet:
self.tx_notification_queue.put(tx)
def on_network_qt(self, event, args=None):
# Handle a network message in the GUI thread
# note: all windows get events from all wallets!
if event == 'wallet_updated':
wallet = args[0]
if wallet == self.wallet:
self.need_update.set()
elif event == 'network_updated':
self.gui_object.network_updated_signal_obj.network_updated_signal \
.emit(event, args)
self.network_signal.emit('status', None)
elif event == 'blockchain_updated':
# to update number of confirmations in history
self.refresh_tabs()
elif event == 'new_transaction':
wallet, tx = args
if wallet == self.wallet:
self.tx_notification_queue.put(tx)
elif event == 'on_quotes':
self.on_fx_quotes()
elif event == 'on_history':
self.on_fx_history()
elif event == 'gossip_db_loaded':
self.channels_list.gossip_db_loaded.emit(*args)
elif event == 'channels_updated':
wallet = args[0]
if wallet == self.wallet:
self.channels_list.update_rows.emit(*args)
elif event == 'channel':
wallet = args[0]
if wallet == self.wallet:
self.channels_list.update_single_row.emit(*args)
self.update_status()
elif event == 'request_status':
self.on_request_status(*args)
elif event == 'invoice_status':
self.on_invoice_status(*args)
elif event == 'payment_succeeded':
# sent by lnworker, redundant with invoice_status
wallet = args[0]
if wallet == self.wallet:
self.on_payment_succeeded(*args)
elif event == 'payment_failed':
wallet = args[0]
if wallet == self.wallet:
self.on_payment_failed(*args)
elif event == 'status':
@qt_event_listener
def on_event_status(self):
self.update_status()
@qt_event_listener
def on_event_network_updated(self, *args):
self.update_status()
@qt_event_listener
def on_event_blockchain_updated(self, *args):
# update the number of confirmations in history
self.refresh_tabs()
@qt_event_listener
def on_event_on_quotes(self, *args):
self.on_fx_quotes()
@qt_event_listener
def on_event_on_history(self, *args):
self.on_fx_history()
@qt_event_listener
def on_event_gossip_db_loaded(self, *args):
self.channels_list.gossip_db_loaded.emit(*args)
@qt_event_listener
def on_event_channels_updated(self, *args):
wallet = args[0]
if wallet == self.wallet:
self.channels_list.update_rows.emit(*args)
@qt_event_listener
def on_event_channel(self, *args):
wallet = args[0]
if wallet == self.wallet:
self.channels_list.update_single_row.emit(*args)
self.update_status()
elif event == 'banner':
self.console.showMessage(args[0])
elif event == 'verified':
wallet, tx_hash, tx_mined_status = args
if wallet == self.wallet:
self.history_model.update_tx_mined_status(tx_hash, tx_mined_status)
elif event == 'fee':
pass
elif event == 'fee_histogram':
self.history_model.on_fee_histogram()
elif event == 'ln_gossip_sync_progress':
self.update_lightning_icon()
elif event == 'cert_mismatch':
self.show_cert_mismatch_error()
else:
self.logger.info(f"unexpected network event: {event} {args}")
@qt_event_listener
def on_event_banner(self, *args):
self.console.showMessage(args[0])
@qt_event_listener
def on_event_verified(self, *args):
wallet, tx_hash, tx_mined_status = args
if wallet == self.wallet:
self.history_model.update_tx_mined_status(tx_hash, tx_mined_status)
@qt_event_listener
def on_event_fee_histogram(self, *args):
self.history_model.on_fee_histogram()
@qt_event_listener
def on_event_ln_gossip_sync_progress(self, *args):
self.update_lightning_icon()
@qt_event_listener
def on_event_cert_mismatch(self, *args):
self.show_cert_mismatch_error()
def close_wallet(self):
if self.wallet:
@ -1821,7 +1809,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
d = SwapDialog(self, is_reverse=is_reverse, recv_amount_sat=recv_amount_sat, channels=channels)
return d.run()
def on_request_status(self, wallet, key, status):
@qt_event_listener
def on_event_request_status(self, wallet, key, status):
if wallet != self.wallet:
return
req = self.wallet.receive_requests.get(key)
@ -1840,7 +1829,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
else:
self.request_list.refresh_item(key)
def on_invoice_status(self, wallet, key):
@qt_event_listener
def on_event_invoice_status(self, wallet, key):
if wallet != self.wallet:
return
invoice = self.wallet.get_invoice(key)
@ -1852,12 +1842,19 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
else:
self.invoice_list.refresh_item(key)
def on_payment_succeeded(self, wallet, key):
@qt_event_listener
def on_event_payment_succeeded(self, wallet, key):
# sent by lnworker, redundant with invoice_status
if wallet != self.wallet:
return
description = self.wallet.get_label(key)
self.notify(_('Payment sent') + '\n\n' + description)
self.need_update.set()
def on_payment_failed(self, wallet, key, reason):
@qt_event_listener
def on_event_payment_failed(self, wallet, key, reason):
if wallet != self.wallet:
return
invoice = self.wallet.get_invoice(key)
if invoice and invoice.is_lightning() and invoice.get_address():
if self.question(_('Payment failed') + '\n\n' + reason + '\n\n'+ 'Fallback to onchain payment?'):
@ -3497,7 +3494,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
self.thread = None
for fut in self._coroutines_scheduled.keys():
fut.cancel()
util.unregister_callback(self.on_network)
self.unregister_callbacks()
self.config.set_key("is_maximized", self.isMaximized())
if not self.isMaximized():
g = self.geometry()

21
electrum/gui/qt/network_dialog.py

@ -43,6 +43,7 @@ from electrum.logging import get_logger
from .util import (Buttons, CloseButton, HelpButton, read_QIcon, char_width_in_lineedit,
PasswordLineEdit)
from .util import QtEventListener, qt_event_listener
if TYPE_CHECKING:
from electrum.simple_config import SimpleConfig
@ -53,27 +54,20 @@ _logger = get_logger(__name__)
protocol_names = ['TCP', 'SSL']
protocol_letters = 'ts'
class NetworkDialog(QDialog):
def __init__(self, *, network: Network, config: 'SimpleConfig', network_updated_signal_obj):
class NetworkDialog(QDialog, QtEventListener):
def __init__(self, *, network: Network, config: 'SimpleConfig'):
QDialog.__init__(self)
self.setWindowTitle(_('Network'))
self.setMinimumSize(500, 500)
self.nlayout = NetworkChoiceLayout(network, config)
self.network_updated_signal_obj = network_updated_signal_obj
vbox = QVBoxLayout(self)
vbox.addLayout(self.nlayout.layout())
vbox.addLayout(Buttons(CloseButton(self)))
self.network_updated_signal_obj.network_updated_signal.connect(
self.on_update)
util.register_callback(self.on_network, ['network_updated'])
self.register_callbacks()
self._cleaned_up = False
def on_network(self, event, *args):
signal_obj = self.network_updated_signal_obj
if signal_obj:
signal_obj.network_updated_signal.emit(event, args)
def on_update(self):
@qt_event_listener
def on_event_network_updated(self):
self.nlayout.update()
def clean_up(self):
@ -81,8 +75,7 @@ class NetworkDialog(QDialog):
return
self._cleaned_up = True
self.nlayout.clean_up()
self.network_updated_signal_obj.network_updated_signal.disconnect()
self.network_updated_signal_obj = None
self.unregister_callbacks()
class NodesListWidget(QTreeWidget):

16
electrum/gui/qt/settings_dialog.py

@ -34,12 +34,12 @@ from PyQt5.QtWidgets import (QComboBox, QTabWidget, QDialog,
from electrum.i18n import _, languages
from electrum import util, coinchooser, paymentrequest
from electrum.util import base_units_list
from electrum.util import base_units_list, event_listener
from electrum.gui import messages
from .util import (ColorScheme, WindowModalDialog, HelpLabel, Buttons,
CloseButton)
CloseButton, QtEventListener)
if TYPE_CHECKING:
@ -47,7 +47,7 @@ if TYPE_CHECKING:
from .main_window import ElectrumWindow
class SettingsDialog(QDialog):
class SettingsDialog(QDialog, QtEventListener):
def __init__(self, window: 'ElectrumWindow', config: 'SimpleConfig'):
QDialog.__init__(self)
@ -60,7 +60,7 @@ class SettingsDialog(QDialog):
self.fx = window.fx
self.wallet = window.wallet
util.register_callback(self.on_network_callback, ['alias_received'])
self.register_callbacks()
self.app.alias_received_signal.connect(self.set_alias_color)
vbox = QVBoxLayout()
@ -559,10 +559,10 @@ class SettingsDialog(QDialog):
vbox.addStretch(1)
vbox.addLayout(Buttons(CloseButton(self)))
self.setLayout(vbox)
def on_network_callback(self, cb):
if cb == 'alias_received':
self.app.alias_received_signal.emit()
@event_listener
def on_event_alias_received(self):
self.app.alias_received_signal.emit()
def set_alias_color(self):
if not self.config.get('alias'):

28
electrum/gui/qt/util.py

@ -8,7 +8,7 @@ import traceback
import os
import webbrowser
from decimal import Decimal
from functools import partial, lru_cache
from functools import partial, lru_cache, wraps
from typing import (NamedTuple, Callable, Optional, TYPE_CHECKING, Union, List, Dict, Any,
Sequence, Iterable)
@ -28,6 +28,7 @@ from PyQt5.QtWidgets import (QPushButton, QLabel, QMessageBox, QHBoxLayout,
from electrum.i18n import _, languages
from electrum.util import FileImportFailed, FileExportFailed, make_aiohttp_session, resource_path
from electrum.util import EventListener, event_listener
from electrum.invoices import PR_UNPAID, PR_PAID, PR_EXPIRED, PR_INFLIGHT, PR_UNKNOWN, PR_FAILED, PR_ROUTING, PR_UNCONFIRMED
from electrum.logging import Logger
from electrum.qrreader import MissingQrDetectionLib
@ -1503,6 +1504,31 @@ class VTabWidget(QtWidgets.QTabWidget):
return super().resizeEvent(e)
class QtEventListener(EventListener):
qt_callback_signal = QtCore.pyqtSignal(tuple)
def register_callbacks(self):
self.qt_callback_signal.connect(self.on_qt_callback_signal)
EventListener.register_callbacks(self)
def unregister_callbacks(self):
self.qt_callback_signal.disconnect()
EventListener.unregister_callbacks(self)
def on_qt_callback_signal(self, args):
func = args[0]
return func(self, *args[1:])
# decorator for members of the QtEventListener class
def qt_event_listener(func):
func = event_listener(func)
@wraps(func)
def decorator(self, *args):
self.qt_callback_signal.emit( (func,) + args)
return decorator
if __name__ == "__main__":
app = QApplication([])
t = WaitingDialog(None, 'testing ...', lambda: [time.sleep(1)], lambda x: QMessageBox.information(None, 'done', "done"))

22
electrum/gui/stdio.py

@ -9,7 +9,7 @@ from electrum import util
from electrum import WalletStorage, Wallet
from electrum.wallet import Abstract_Wallet
from electrum.wallet_db import WalletDB
from electrum.util import format_satoshis
from electrum.util import format_satoshis, EventListener, event_listener
from electrum.bitcoin import is_address, COIN
from electrum.transaction import PartialTxOutput
from electrum.network import TxBroadcastError, BestEffortRequestFailed
@ -20,7 +20,7 @@ _ = lambda x:x # i18n
# written by rofl0r, with some bits stolen from the text gui (ncurses)
class ElectrumGui(BaseElectrumGui):
class ElectrumGui(BaseElectrumGui, EventListener):
def __init__(self, *, config, daemon, plugins):
BaseElectrumGui.__init__(self, config=config, daemon=daemon, plugins=plugins)
@ -47,7 +47,7 @@ class ElectrumGui(BaseElectrumGui):
self.wallet.start_network(self.network)
self.contacts = self.wallet.contacts
util.register_callback(self.on_network, ['wallet_updated', 'network_updated', 'banner'])
self.register_callbacks()
self.commands = [_("[h] - displays this help text"), \
_("[i] - display transaction history"), \
_("[o] - enter payment order"), \
@ -59,11 +59,17 @@ class ElectrumGui(BaseElectrumGui):
_("[q] - quit")]
self.num_commands = len(self.commands)
def on_network(self, event, *args):
if event in ['wallet_updated', 'network_updated']:
self.updated()
elif event == 'banner':
self.print_banner()
@event_listener
def on_event_wallet_updated(self, wallet):
self.updated()
@event_listener
def on_event_network_updated(self):
self.updated()
@event_listener
def on_event_banner(self):
self.print_banner()
def main_command(self):
self.print_balance()

15
electrum/gui/text.py

@ -12,6 +12,7 @@ import electrum
from electrum.gui import BaseElectrumGui
from electrum import util
from electrum.util import format_satoshis
from electrum.util import EventListener, event_listener
from electrum.bitcoin import is_address, COIN
from electrum.transaction import PartialTxOutput
from electrum.wallet import Wallet, Abstract_Wallet
@ -29,7 +30,7 @@ if TYPE_CHECKING:
_ = lambda x:x # i18n
class ElectrumGui(BaseElectrumGui):
class ElectrumGui(BaseElectrumGui, EventListener):
def __init__(self, *, config: 'SimpleConfig', daemon: 'Daemon', plugins: 'Plugins'):
BaseElectrumGui.__init__(self, config=config, daemon=daemon, plugins=plugins)
@ -74,12 +75,20 @@ class ElectrumGui(BaseElectrumGui):
self.history = None
self.txid = []
util.register_callback(self.update, ['wallet_updated', 'network_updated'])
self.register_callbacks()
self.tab_names = [_("History"), _("Send"), _("Receive"), _("Addresses"), _("Contacts"), _("Banner")]
self.num_tabs = len(self.tab_names)
@event_listener
def on_event_wallet_updated(self, wallet):
self.update()
@event_listener
def on_event_network_updated(self):
self.update()
def set_cursor(self, x):
try:
curses.curs_set(x)
@ -101,7 +110,7 @@ class ElectrumGui(BaseElectrumGui):
self.set_cursor(0)
return s
def update(self, event, *args):
def update(self):
self.update_history()
if self.tab == 0:
self.print_history()

33
electrum/lnwatcher.py

@ -135,8 +135,9 @@ class SweepStore(SqlDB):
return [(r[0], r[1]) for r in c.fetchall()]
from .util import EventListener, event_listener
class LNWatcher(Logger):
class LNWatcher(Logger, EventListener):
LOGGING_SHORTCUT = 'W'
@ -147,23 +148,12 @@ class LNWatcher(Logger):
self.config = network.config
self.callbacks = {} # address -> lambda: coroutine
self.network = network
util.register_callback(self.on_fee, ['fee'])
util.register_callback(self.on_blockchain_updated, ['blockchain_updated'])
util.register_callback(self.on_network_updated, ['network_updated'])
util.register_callback(self.on_adb_added_verified_tx, ['adb_added_verified_tx'])
util.register_callback(self.on_adb_set_up_to_date, ['adb_set_up_to_date'])
self.register_callbacks()
# status gets populated when we run
self.channel_status = {}
async def stop(self):
util.unregister_callback(self.on_fee)
util.unregister_callback(self.on_blockchain_updated)
util.unregister_callback(self.on_network_updated)
util.unregister_callback(self.on_adb_added_verified_tx)
util.unregister_callback(self.on_adb_set_up_to_date)
self.unregister_callbacks()
def get_channel_status(self, outpoint):
return self.channel_status.get(outpoint, 'unknown')
@ -185,21 +175,26 @@ class LNWatcher(Logger):
self.adb.add_address(address)
self.callbacks[address] = callback
async def on_fee(self, event, *args):
@event_listener
async def on_event_fee(self, *args):
await self.trigger_callbacks()
async def on_network_updated(self, event, *args):
@event_listener
async def on_event_network_updated(self, *args):
await self.trigger_callbacks()
async def on_blockchain_updated(self, event, *args):
@event_listener
async def on_event_blockchain_updated(self, *args):
await self.trigger_callbacks()
async def on_adb_added_verified_tx(self, event, adb, tx_hash):
@event_listener
async def on_event_adb_added_verified_tx(self, adb, tx_hash):
if adb != self.adb:
return
await self.trigger_callbacks()
async def on_adb_set_up_to_date(self, event, adb):
@event_listener
async def on_event_adb_set_up_to_date(self, adb):
if adb != self.adb:
return
await self.trigger_callbacks()

10
electrum/lnworker.py

@ -31,6 +31,7 @@ from . import keystore
from .util import profiler, chunks, OldTaskGroup
from .invoices import Invoice, PR_UNPAID, PR_EXPIRED, PR_PAID, PR_INFLIGHT, PR_FAILED, PR_ROUTING, LN_EXPIRY_NEVER
from .util import NetworkRetryManager, JsonRPCClient, NotEnoughFunds
from .util import EventListener, event_listener
from .lnutil import LN_MAX_FUNDING_SAT
from .keystore import BIP32_KeyStore
from .bitcoin import COIN
@ -192,7 +193,7 @@ LNGOSSIP_FEATURES = BASE_FEATURES\
| LnFeatures.GOSSIP_QUERIES_REQ\
class LNWorker(Logger, NetworkRetryManager[LNPeerAddr]):
class LNWorker(Logger, EventListener, NetworkRetryManager[LNPeerAddr]):
INITIAL_TRAMPOLINE_FEE_LEVEL = 1 # only used for trampoline payments. set to 0 in tests.
@ -216,7 +217,7 @@ class LNWorker(Logger, NetworkRetryManager[LNPeerAddr]):
self.config = None # type: Optional[SimpleConfig]
self.stopping_soon = False # whether we are being shut down
util.register_callback(self.on_proxy_changed, ['proxy_set'])
self.register_callbacks()
@property
def channel_db(self):
@ -340,7 +341,7 @@ class LNWorker(Logger, NetworkRetryManager[LNPeerAddr]):
async def stop(self):
if self.listen_server:
self.listen_server.close()
util.unregister_callback(self.on_proxy_changed)
self.unregister_callbacks()
await self.taskgroup.cancel_remaining()
def _add_peers_from_config(self):
@ -477,7 +478,8 @@ class LNWorker(Logger, NetworkRetryManager[LNPeerAddr]):
choice = random.choice(addr_list)
return choice
def on_proxy_changed(self, event, *args):
@event_listener
def on_event_proxy_set(self, *args):
for peer in self.peers.values():
peer.close_and_cleanup()
self._clear_addr_retry_times()

6
electrum/tests/test_lnpeer.py

@ -22,7 +22,7 @@ from electrum.ecc import ECPrivkey
from electrum import simple_config, lnutil
from electrum.lnaddr import lnencode, LnAddr, lndecode
from electrum.bitcoin import COIN, sha256
from electrum.util import bh2u, NetworkRetryManager, bfh, OldTaskGroup
from electrum.util import bh2u, NetworkRetryManager, bfh, OldTaskGroup, EventListener
from electrum.lnpeer import Peer
from electrum.lnutil import LNPeerAddr, Keypair, privkey_to_pubkey
from electrum.lnutil import PaymentFailure, LnFeatures, HTLCOwner
@ -124,7 +124,7 @@ class MockWallet:
return True
class MockLNWallet(Logger, NetworkRetryManager[LNPeerAddr]):
class MockLNWallet(Logger, EventListener, NetworkRetryManager[LNPeerAddr]):
MPP_EXPIRY = 2 # HTLC timestamps are cast to int, so this cannot be 1
PAYMENT_TIMEOUT = 120
TIMEOUT_SHUTDOWN_FAIL_PENDING_HTLCS = 0
@ -255,7 +255,7 @@ class MockLNWallet(Logger, NetworkRetryManager[LNPeerAddr]):
handle_error_code_from_failed_htlc = LNWallet.handle_error_code_from_failed_htlc
is_trampoline_peer = LNWallet.is_trampoline_peer
wait_for_received_pending_htlcs_to_get_removed = LNWallet.wait_for_received_pending_htlcs_to_get_removed
on_proxy_changed = LNWallet.on_proxy_changed
#on_event_proxy_set = LNWallet.on_event_proxy_set
_decode_channel_update_msg = LNWallet._decode_channel_update_msg
_handle_chanupd_from_failed_htlc = LNWallet._handle_chanupd_from_failed_htlc
_on_maybe_forwarded_htlc_resolved = LNWallet._on_maybe_forwarded_htlc_resolved

41
electrum/util.py

@ -1594,10 +1594,10 @@ class CallbackManager:
self.callbacks = defaultdict(list) # note: needs self.callback_lock
self.asyncio_loop = None
def register_callback(self, callback, events):
def register_callback(self, func, events):
with self.callback_lock:
for event in events:
self.callbacks[event].append(callback)
self.callbacks[event].append(func)
def unregister_callback(self, callback):
with self.callback_lock:
@ -1618,13 +1618,13 @@ class CallbackManager:
for callback in callbacks:
# FIXME: if callback throws, we will lose the traceback
if asyncio.iscoroutinefunction(callback):
asyncio.run_coroutine_threadsafe(callback(event, *args), self.asyncio_loop)
asyncio.run_coroutine_threadsafe(callback(*args), self.asyncio_loop)
elif get_running_loop() == self.asyncio_loop:
# run callback immediately, so that it is guaranteed
# to have been executed when this method returns
callback(event, *args)
callback(*args)
else:
self.asyncio_loop.call_soon_threadsafe(callback, event, *args)
self.asyncio_loop.call_soon_threadsafe(callback, *args)
callback_mgr = CallbackManager()
@ -1633,6 +1633,37 @@ register_callback = callback_mgr.register_callback
unregister_callback = callback_mgr.unregister_callback
class EventListener:
def _list_callbacks(self):
for method_name in dir(self):
# Fixme: getattr executes the code of methods decorated by @property.
# This if why we shield with startswith
if not method_name.startswith('on_event_'):
continue
method = getattr(self, method_name)
if not getattr(method, '_is_event_listener', False):
continue
assert callable(method)
yield method_name[len('on_event_'):], method
def register_callbacks(self):
for name, method in self._list_callbacks():
_logger.info(f'registering callback {method}')
register_callback(method, [name])
def unregister_callbacks(self):
for name, method in self._list_callbacks():
_logger.info(f'unregistering callback {method}')
unregister_callback(method)
def event_listener(func):
assert func.__name__.startswith('on_event_')
func._is_event_listener = True
return func
_NetAddrType = TypeVar("_NetAddrType")

22
electrum/wallet.py

@ -83,6 +83,7 @@ from .logging import get_logger, Logger
from .lnworker import LNWallet
from .paymentrequest import PaymentRequest
from .util import read_json_file, write_json_file, UserFacingException
from .util import EventListener, event_listener
if TYPE_CHECKING:
from .network import Network
@ -267,7 +268,7 @@ class TxWalletDetails(NamedTuple):
is_lightning_funding_tx: bool
class Abstract_Wallet(ABC, Logger):
class Abstract_Wallet(ABC, Logger, EventListener):
"""
Wallet classes are created to handle various address generation methods.
Completion states (watching-only, single account, no seed, etc) are handled inside classes.
@ -282,6 +283,7 @@ class Abstract_Wallet(ABC, Logger):
lnworker: Optional['LNWallet']
def __init__(self, db: WalletDB, storage: Optional[WalletStorage], *, config: SimpleConfig):
if not db.is_ready_to_be_used_by_wallet():
raise Exception("storage not ready to be used by Abstract_Wallet")
@ -332,10 +334,7 @@ class Abstract_Wallet(ABC, Logger):
self.lnworker = None
self.load_keystore()
self.test_addresses_sanity()
# callbacks
util.register_callback(self.on_adb_added_tx, ['adb_added_tx'])
util.register_callback(self.on_adb_added_verified_tx, ['adb_added_verified_tx'])
util.register_callback(self.on_adb_removed_verified_tx, ['adb_removed_verified_tx'])
self.register_callbacks()
@ignore_exceptions # don't kill outer taskgroup
async def main_loop(self):
@ -422,9 +421,7 @@ class Abstract_Wallet(ABC, Logger):
async def stop(self):
"""Stop all networking and save DB to disk."""
util.unregister_callback(self.on_adb_added_tx)
util.unregister_callback(self.on_adb_added_verified_tx)
util.unregister_callback(self.on_adb_removed_verified_tx)
self.unregister_callbacks()
try:
async with ignore_after(5):
if self.network:
@ -454,7 +451,8 @@ class Abstract_Wallet(ABC, Logger):
if status_changed:
self.logger.info(f'set_up_to_date: {up_to_date}')
def on_adb_added_tx(self, event, adb, tx_hash, notify_GUI):
@event_listener
def on_event_adb_added_tx(self, adb, tx_hash, notify_GUI):
if self.adb != adb:
return
tx = self.db.get_transaction(tx_hash)
@ -471,14 +469,16 @@ class Abstract_Wallet(ABC, Logger):
if notify_GUI:
util.trigger_callback('new_transaction', self, tx)
def on_adb_added_verified_tx(self, event, adb, tx_hash):
@event_listener
def on_event_adb_added_verified_tx(self, adb, tx_hash):
if adb != self.adb:
return
self._update_request_statuses_touched_by_tx(tx_hash)
tx_mined_status = self.adb.get_tx_height(tx_hash)
util.trigger_callback('verified', self, tx_hash, tx_mined_status)
def on_adb_removed_verified_tx(self, event, adb, tx_hash):
@event_listener
def on_event_adb_removed_verified_tx(self, adb, tx_hash):
if adb != self.adb:
return
self._update_request_statuses_touched_by_tx(tx_hash)

Loading…
Cancel
Save