electrum

Electrum Bitcoin wallet
git clone https://git.parazyd.org/electrum
Log | Files | Refs | Submodules

commit 92244041081db96b92925c9e76b117035e241011
parent 73325831b7075f461e106f36099865a3fedadb72
Author: ThomasV <thomasv@electrum.org>
Date:   Tue, 14 Apr 2020 16:12:47 +0200

Move callback manager out of Network class

Diffstat:
Melectrum/address_synchronizer.py | 8++++----
Melectrum/channel_db.py | 6+++---
Melectrum/daemon.py | 3++-
Melectrum/exchange_rate.py | 10++++------
Melectrum/gui/kivy/main_window.py | 30+++++++++++++++---------------
Melectrum/gui/qt/channel_details.py | 9+++++----
Melectrum/gui/qt/lightning_dialog.py | 7++++---
Melectrum/gui/qt/main_window.py | 9++++-----
Melectrum/gui/qt/network_dialog.py | 4++--
Melectrum/gui/stdio.py | 3++-
Melectrum/gui/text.py | 4++--
Melectrum/interface.py | 6+++---
Melectrum/lnchannel.py | 8+++-----
Melectrum/lnpeer.py | 8++++----
Melectrum/lnwatcher.py | 8+++++---
Melectrum/lnworker.py | 60++++++++++++++++++++++++++++++------------------------------
Melectrum/network.py | 47++++++++++++-----------------------------------
Melectrum/synchronizer.py | 5+++--
Melectrum/util.py | 40+++++++++++++++++++++++++++++++++++++++-
19 files changed, 146 insertions(+), 129 deletions(-)

diff --git a/electrum/address_synchronizer.py b/electrum/address_synchronizer.py @@ -28,7 +28,7 @@ import itertools from collections import defaultdict from typing import TYPE_CHECKING, Dict, Optional, Set, Tuple, NamedTuple, Sequence, List -from . import bitcoin +from . import bitcoin, util from .bitcoin import COINBASE_MATURITY from .util import profiler, bfh, TxMinedInfo from .transaction import Transaction, TxOutput, TxInput, PartialTxInput, TxOutpoint, PartialTransaction @@ -161,7 +161,7 @@ class AddressSynchronizer(Logger): if self.network is not None: self.synchronizer = Synchronizer(self) self.verifier = SPV(self.network, self) - self.network.register_callback(self.on_blockchain_updated, ['blockchain_updated']) + util.register_callback(self.on_blockchain_updated, ['blockchain_updated']) def on_blockchain_updated(self, event, *args): self._get_addr_balance_cache = {} # invalidate cache @@ -174,7 +174,7 @@ class AddressSynchronizer(Logger): if self.verifier: asyncio.run_coroutine_threadsafe(self.verifier.stop(), self.network.asyncio_loop) self.verifier = None - self.network.unregister_callback(self.on_blockchain_updated) + util.unregister_callback(self.on_blockchain_updated) self.db.put('stored_height', self.get_local_height()) def add_address(self, address): @@ -546,7 +546,7 @@ class AddressSynchronizer(Logger): self.unverified_tx.pop(tx_hash, None) self.db.add_verified_tx(tx_hash, info) tx_mined_status = self.get_tx_height(tx_hash) - self.network.trigger_callback('verified', self, tx_hash, tx_mined_status) + util.trigger_callback('verified', self, tx_hash, tx_mined_status) def get_unverified_txs(self): '''Returns a map from tx hash to transaction height''' diff --git a/electrum/channel_db.py b/electrum/channel_db.py @@ -35,7 +35,7 @@ import threading from .sql_db import SqlDB, sql -from . import constants +from . import constants, util from .util import bh2u, profiler, get_headers_dir, bfh, is_ip_address, list_enabled_bits from .logging import Logger from .lnutil import (LNPeerAddr, format_short_channel_id, ShortChannelID, @@ -269,8 +269,8 @@ class ChannelDB(SqlDB): self.num_nodes = len(self._nodes) self.num_channels = len(self._channels) self.num_policies = len(self._policies) - self.network.trigger_callback('channel_db', self.num_nodes, self.num_channels, self.num_policies) - self.network.trigger_callback('ln_gossip_sync_progress') + util.trigger_callback('channel_db', self.num_nodes, self.num_channels, self.num_policies) + util.trigger_callback('ln_gossip_sync_progress') def get_channel_ids(self): with self.lock: diff --git a/electrum/daemon.py b/electrum/daemon.py @@ -41,6 +41,7 @@ from jsonrpcserver import response from jsonrpcclient.clients.aiohttp_client import AiohttpClient from aiorpcx import TaskGroup +from . import util from .network import Network from .util import (json_decode, to_bytes, to_string, profiler, standardize_path, constant_time_compare) from .util import PR_PAID, PR_EXPIRED, get_request_status @@ -181,7 +182,7 @@ class PayServer(Logger): self.daemon = daemon self.config = daemon.config self.pending = defaultdict(asyncio.Event) - self.daemon.network.register_callback(self.on_payment, ['payment_received']) + util.register_callback(self.on_payment, ['payment_received']) async def on_payment(self, evt, wallet, key, status): if status == PR_PAID: diff --git a/electrum/exchange_rate.py b/electrum/exchange_rate.py @@ -12,6 +12,7 @@ from typing import Sequence, Optional from aiorpcx.curio import timeout_after, TaskTimeout, TaskGroup +from . import util from .bitcoin import COIN from .i18n import _ from .util import (ThreadJob, make_dir, log_exceptions, @@ -456,8 +457,7 @@ class FxThread(ThreadJob): ThreadJob.__init__(self) self.config = config self.network = network - if self.network: - self.network.register_callback(self.set_proxy, ['proxy_set']) + util.register_callback(self.set_proxy, ['proxy_set']) self.ccy = self.get_currency() self.history_used_spot = False self.ccy_combo = None @@ -567,12 +567,10 @@ class FxThread(ThreadJob): self.exchange.read_historical_rates(self.ccy, self.cache_dir) def on_quotes(self): - if self.network: - self.network.trigger_callback('on_quotes') + util.trigger_callback('on_quotes') def on_history(self): - if self.network: - self.network.trigger_callback('on_history') + util.trigger_callback('on_history') def exchange_rate(self) -> Decimal: """Returns the exchange rate as a Decimal""" diff --git a/electrum/gui/kivy/main_window.py b/electrum/gui/kivy/main_window.py @@ -13,6 +13,7 @@ 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 import util 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) @@ -50,7 +51,6 @@ from .uix.dialogs.question import Question # 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 @@ -565,20 +565,20 @@ class ElectrumWindow(App): 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_payment_failed, ['payment_failed']) - self.network.register_callback(self.on_payment_succeeded, ['payment_succeeded']) - 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']) + 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']) # load wallet self.load_wallet_by_name(self.electrum_config.get_wallet_path(use_gui_last_wallet=True)) # URI passed in config diff --git a/electrum/gui/qt/channel_details.py b/electrum/gui/qt/channel_details.py @@ -5,6 +5,7 @@ import PyQt5.QtWidgets as QtWidgets import PyQt5.QtCore as QtCore from PyQt5.QtWidgets import QLabel, QLineEdit +from electrum import util from electrum.i18n import _ from electrum.util import bh2u, format_time from electrum.lnutil import format_short_channel_id, LOCAL, REMOTE, UpdateAddHtlc, Direction @@ -132,10 +133,10 @@ class ChannelDetailsDialog(QtWidgets.QDialog): self.htlc_added.connect(self.do_htlc_added) # register callbacks for updating - window.network.register_callback(self.ln_payment_completed.emit, ['ln_payment_completed']) - window.network.register_callback(self.ln_payment_failed.emit, ['ln_payment_failed']) - window.network.register_callback(self.htlc_added.emit, ['htlc_added']) - window.network.register_callback(self.state_changed.emit, ['channel']) + util.register_callback(self.ln_payment_completed.emit, ['ln_payment_completed']) + util.register_callback(self.ln_payment_failed.emit, ['ln_payment_failed']) + util.register_callback(self.htlc_added.emit, ['htlc_added']) + util.register_callback(self.state_changed.emit, ['channel']) # set attributes of QDialog self.setWindowTitle(_('Channel Details')) diff --git a/electrum/gui/qt/lightning_dialog.py b/electrum/gui/qt/lightning_dialog.py @@ -27,6 +27,7 @@ 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 @@ -58,9 +59,9 @@ class LightningDialog(QDialog): b = QPushButton(_('Close')) b.clicked.connect(self.close) vbox.addLayout(Buttons(b)) - 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']) + 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.network.channel_db.update_counts() # trigger callback self.set_num_peers('', self.network.lngossip.num_peers()) self.set_unknown_channels('', len(self.network.lngossip.unknown_ids)) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py @@ -272,7 +272,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): # 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... - self.network.register_callback(self.on_network, interests) + util.register_callback(self.on_network, interests) # set initial message self.console.showMessage(self.network.banner) @@ -466,8 +466,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): def load_wallet(self, wallet): wallet.thread = TaskThread(self, self.on_error) self.update_recently_visited(wallet.storage.path) - if wallet.lnworker and wallet.network: - wallet.network.trigger_callback('channels_updated', wallet) + if wallet.lnworker: + util.trigger_callback('channels_updated', wallet) self.need_update.set() # Once GUI has been initialized check if we want to announce something since the callback has been called before the GUI was initialized # update menus @@ -2889,8 +2889,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): def clean_up(self): self.wallet.thread.stop() - if self.network: - self.network.unregister_callback(self.on_network) + util.unregister_callback(self.on_network) self.config.set_key("is_maximized", self.isMaximized()) if not self.isMaximized(): g = self.geometry() diff --git a/electrum/gui/qt/network_dialog.py b/electrum/gui/qt/network_dialog.py @@ -35,7 +35,7 @@ from PyQt5.QtWidgets import (QTreeWidget, QTreeWidgetItem, QMenu, QGridLayout, Q from PyQt5.QtGui import QFontMetrics from electrum.i18n import _ -from electrum import constants, blockchain +from electrum import constants, blockchain, util from electrum.interface import serialize_server, deserialize_server from electrum.network import Network from electrum.logging import get_logger @@ -61,7 +61,7 @@ class NetworkDialog(QDialog): vbox.addLayout(Buttons(CloseButton(self))) self.network_updated_signal_obj.network_updated_signal.connect( self.on_update) - network.register_callback(self.on_network, ['network_updated']) + util.register_callback(self.on_network, ['network_updated']) def on_network(self, event, *args): self.network_updated_signal_obj.network_updated_signal.emit(event, args) diff --git a/electrum/gui/stdio.py b/electrum/gui/stdio.py @@ -3,6 +3,7 @@ import getpass import datetime import logging +from electrum import util from electrum import WalletStorage, Wallet from electrum.util import format_satoshis from electrum.bitcoin import is_address, COIN @@ -43,7 +44,7 @@ class ElectrumGui: self.wallet.start_network(self.network) self.contacts = self.wallet.contacts - self.network.register_callback(self.on_network, ['wallet_updated', 'network_updated', 'banner']) + util.register_callback(self.on_network, ['wallet_updated', 'network_updated', 'banner']) self.commands = [_("[h] - displays this help text"), \ _("[i] - display transaction history"), \ _("[o] - enter payment order"), \ diff --git a/electrum/gui/text.py b/electrum/gui/text.py @@ -8,6 +8,7 @@ import getpass import logging import electrum +from electrum import util from electrum.util import format_satoshis from electrum.bitcoin import is_address, COIN from electrum.transaction import PartialTxOutput @@ -65,8 +66,7 @@ class ElectrumGui: self.str_fee = "" self.history = None - if self.network: - self.network.register_callback(self.update, ['wallet_updated', 'network_updated']) + util.register_callback(self.update, ['wallet_updated', 'network_updated']) self.tab_names = [_("History"), _("Send"), _("Receive"), _("Addresses"), _("Contacts"), _("Banner")] self.num_tabs = len(self.tab_names) diff --git a/electrum/interface.py b/electrum/interface.py @@ -548,7 +548,7 @@ class Interface(Logger): raise GracefulDisconnect('server tip below max checkpoint') self._mark_ready() await self._process_header_at_tip() - self.network.trigger_callback('network_updated') + util.trigger_callback('network_updated') await self.network.switch_unwanted_fork_interface() await self.network.switch_lagging_interface() @@ -563,7 +563,7 @@ class Interface(Logger): # in the simple case, height == self.tip+1 if height <= self.tip: await self.sync_until(height) - self.network.trigger_callback('blockchain_updated') + util.trigger_callback('blockchain_updated') async def sync_until(self, height, next_height=None): if next_height is None: @@ -578,7 +578,7 @@ class Interface(Logger): raise GracefulDisconnect('server chain conflicts with checkpoints or genesis') last, height = await self.step(height) continue - self.network.trigger_callback('network_updated') + util.trigger_callback('network_updated') height = (height // 2016 * 2016) + num_headers assert height <= next_height+1, (height, self.tip) last = 'catchup' diff --git a/electrum/lnchannel.py b/electrum/lnchannel.py @@ -33,7 +33,7 @@ from aiorpcx import NetAddress import attr from . import ecc -from . import constants +from . import constants, util from .util import bfh, bh2u, chunks, TxMinedInfo from .bitcoin import redeem_script_to_address from .crypto import sha256, sha256d @@ -679,16 +679,14 @@ class Channel(AbstractChannel): def set_frozen_for_sending(self, b: bool) -> None: self.storage['frozen_for_sending'] = bool(b) - if self.lnworker: - self.lnworker.network.trigger_callback('channel', self) + util.trigger_callback('channel', self) def is_frozen_for_receiving(self) -> bool: return self.storage.get('frozen_for_receiving', False) def set_frozen_for_receiving(self, b: bool) -> None: self.storage['frozen_for_receiving'] = bool(b) - if self.lnworker: - self.lnworker.network.trigger_callback('channel', self) + util.trigger_callback('channel', self) def _assert_can_add_htlc(self, *, htlc_proposer: HTLCOwner, amount_msat: int) -> None: """Raises PaymentFailure if the htlc_proposer cannot add this new HTLC. diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py @@ -19,7 +19,7 @@ from datetime import datetime import aiorpcx from .crypto import sha256, sha256d -from . import bitcoin +from . import bitcoin, util from . import ecc from .ecc import sig_string_from_r_and_s, get_r_and_s_from_sig_string, der_sig_from_sig_string from . import constants @@ -744,7 +744,7 @@ class Peer(Logger): f'already in peer_state {chan.peer_state}') return chan.peer_state = PeerState.REESTABLISHING - self.network.trigger_callback('channel', chan) + util.trigger_callback('channel', chan) # BOLT-02: "A node [...] upon disconnection [...] MUST reverse any uncommitted updates sent by the other side" chan.hm.discard_unsigned_remote_updates() # ctns @@ -891,7 +891,7 @@ class Peer(Logger): # checks done if chan.is_funded() and chan.config[LOCAL].funding_locked_received: self.mark_open(chan) - self.network.trigger_callback('channel', chan) + util.trigger_callback('channel', chan) if chan.get_state() == ChannelState.CLOSING: await self.send_shutdown(chan) @@ -979,7 +979,7 @@ class Peer(Logger): return assert chan.config[LOCAL].funding_locked_received chan.set_state(ChannelState.OPEN) - self.network.trigger_callback('channel', chan) + util.trigger_callback('channel', chan) # peer may have sent us a channel update for the incoming direction previously pending_channel_update = self.orphan_channel_updates.get(chan.short_channel_id) if pending_channel_update: diff --git a/electrum/lnwatcher.py b/electrum/lnwatcher.py @@ -8,6 +8,7 @@ import asyncio from enum import IntEnum, auto from typing import NamedTuple, Dict +from . import util from .sql_db import SqlDB, sql from .wallet_db import WalletDB from .util import bh2u, bfh, log_exceptions, ignore_exceptions, TxMinedInfo @@ -139,8 +140,9 @@ class LNWatcher(AddressSynchronizer): self.config = network.config self.channels = {} self.network = network - self.network.register_callback(self.on_network_update, - ['network_updated', 'blockchain_updated', 'verified', 'wallet_updated', 'fee']) + util.register_callback( + self.on_network_update, + ['network_updated', 'blockchain_updated', 'verified', 'wallet_updated', 'fee']) # status gets populated when we run self.channel_status = {} @@ -420,4 +422,4 @@ class LNWalletWatcher(LNWatcher): tx_was_added = False if tx_was_added: self.logger.info(f'added future tx: {name}. prevout: {prevout}') - self.network.trigger_callback('wallet_updated', self.lnworker.wallet) + util.trigger_callback('wallet_updated', self.lnworker.wallet) diff --git a/electrum/lnworker.py b/electrum/lnworker.py @@ -21,7 +21,7 @@ import dns.resolver import dns.exception from aiorpcx import run_in_thread -from . import constants +from . import constants, util from . import keystore from .util import profiler from .util import PR_UNPAID, PR_EXPIRED, PR_PAID, PR_INFLIGHT, PR_FAILED, PR_ROUTING @@ -367,7 +367,7 @@ class LNGossip(LNWorker): max_age = 14*24*3600 LOGGING_SHORTCUT = 'g' - def __init__(self, network): + def __init__(self): seed = os.urandom(32) node = BIP32Node.from_rootseed(seed, xtype='standard') xprv = node.to_xprv() @@ -393,16 +393,16 @@ class LNGossip(LNWorker): known = self.channel_db.get_channel_ids() new = set(ids) - set(known) self.unknown_ids.update(new) - self.network.trigger_callback('unknown_channels', len(self.unknown_ids)) - self.network.trigger_callback('gossip_peers', self.num_peers()) - self.network.trigger_callback('ln_gossip_sync_progress') + util.trigger_callback('unknown_channels', len(self.unknown_ids)) + util.trigger_callback('gossip_peers', self.num_peers()) + util.trigger_callback('ln_gossip_sync_progress') def get_ids_to_query(self): N = 500 l = list(self.unknown_ids) self.unknown_ids = set(l[N:]) - self.network.trigger_callback('unknown_channels', len(self.unknown_ids)) - self.network.trigger_callback('ln_gossip_sync_progress') + util.trigger_callback('unknown_channels', len(self.unknown_ids)) + util.trigger_callback('ln_gossip_sync_progress') return l[0:N] def get_sync_progress_estimate(self) -> Tuple[Optional[int], Optional[int]]: @@ -514,7 +514,7 @@ class LNWallet(LNWorker): def peer_closed(self, peer): for chan in self.channels_for_peer(peer.pubkey).values(): chan.peer_state = PeerState.DISCONNECTED - self.network.trigger_callback('channel', chan) + util.trigger_callback('channel', chan) super().peer_closed(peer) def get_settled_payments(self): @@ -645,14 +645,14 @@ class LNWallet(LNWorker): def channel_state_changed(self, chan): self.save_channel(chan) - self.network.trigger_callback('channel', chan) + util.trigger_callback('channel', chan) def save_channel(self, chan): assert type(chan) is Channel if chan.config[REMOTE].next_per_commitment_point == chan.config[REMOTE].current_per_commitment_point: raise Exception("Tried to save channel with next_point == current_point, this should not happen") self.wallet.save_db() - self.network.trigger_callback('channel', chan) + util.trigger_callback('channel', chan) def channel_by_txo(self, txo): with self.lock: @@ -703,7 +703,7 @@ class LNWallet(LNWorker): funding_sat=funding_sat, push_msat=push_sat * 1000, temp_channel_id=os.urandom(32)) - self.network.trigger_callback('channels_updated', self.wallet) + util.trigger_callback('channels_updated', self.wallet) self.wallet.add_transaction(funding_tx) # save tx as local into the wallet self.wallet.set_label(funding_tx.txid(), _('Open channel')) if funding_tx.is_complete(): @@ -804,10 +804,10 @@ class LNWallet(LNWorker): # note: path-finding runs in a separate thread so that we don't block the asyncio loop # graph updates might occur during the computation self.set_invoice_status(key, PR_ROUTING) - self.network.trigger_callback('invoice_status', key) + util.trigger_callback('invoice_status', key) route = await run_in_thread(self._create_route_from_invoice, lnaddr) self.set_invoice_status(key, PR_INFLIGHT) - self.network.trigger_callback('invoice_status', key) + util.trigger_callback('invoice_status', key) payment_attempt_log = await self._pay_to_route(route, lnaddr) except Exception as e: log.append(PaymentAttemptLog(success=False, exception=e)) @@ -820,11 +820,11 @@ class LNWallet(LNWorker): break else: reason = _('Failed after {} attempts').format(attempts) - self.network.trigger_callback('invoice_status', key) + util.trigger_callback('invoice_status', key) if success: - self.network.trigger_callback('payment_succeeded', key) + util.trigger_callback('payment_succeeded', key) else: - self.network.trigger_callback('payment_failed', key, reason) + util.trigger_callback('payment_failed', key, reason) return success async def _pay_to_route(self, route: LNPaymentRoute, lnaddr: LnAddr) -> PaymentAttemptLog: @@ -840,7 +840,7 @@ class LNWallet(LNWorker): payment_hash=lnaddr.paymenthash, min_final_cltv_expiry=lnaddr.get_min_final_cltv_expiry(), payment_secret=lnaddr.payment_secret) - self.network.trigger_callback('htlc_added', htlc, lnaddr, SENT) + util.trigger_callback('htlc_added', htlc, lnaddr, SENT) payment_attempt = await self.await_payment(lnaddr.paymenthash) if payment_attempt.success: failure_log = None @@ -1139,9 +1139,9 @@ class LNWallet(LNWorker): f.set_result(payment_attempt) else: chan.logger.info('received unexpected payment_failed, probably from previous session') - self.network.trigger_callback('invoice_status', key) - self.network.trigger_callback('payment_failed', key, '') - self.network.trigger_callback('ln_payment_failed', payment_hash, chan.channel_id) + util.trigger_callback('invoice_status', key) + util.trigger_callback('payment_failed', key, '') + util.trigger_callback('ln_payment_failed', payment_hash, chan.channel_id) def payment_sent(self, chan, payment_hash: bytes): self.set_payment_status(payment_hash, PR_PAID) @@ -1155,14 +1155,14 @@ class LNWallet(LNWorker): f.set_result(payment_attempt) else: chan.logger.info('received unexpected payment_sent, probably from previous session') - self.network.trigger_callback('invoice_status', key) - self.network.trigger_callback('payment_succeeded', key) - self.network.trigger_callback('ln_payment_completed', payment_hash, chan.channel_id) + util.trigger_callback('invoice_status', key) + util.trigger_callback('payment_succeeded', key) + util.trigger_callback('ln_payment_completed', payment_hash, chan.channel_id) def payment_received(self, chan, payment_hash: bytes): self.set_payment_status(payment_hash, PR_PAID) - self.network.trigger_callback('request_status', payment_hash.hex(), PR_PAID) - self.network.trigger_callback('ln_payment_completed', payment_hash, chan.channel_id) + util.trigger_callback('request_status', payment_hash.hex(), PR_PAID) + util.trigger_callback('ln_payment_completed', payment_hash, chan.channel_id) async def _calc_routing_hints_for_invoice(self, amount_sat): """calculate routing hints (BOLT-11 'r' field)""" @@ -1251,8 +1251,8 @@ class LNWallet(LNWorker): self.channels.pop(chan_id) self.db.get('channels').pop(chan_id.hex()) - self.network.trigger_callback('channels_updated', self.wallet) - self.network.trigger_callback('wallet_updated', self.wallet) + util.trigger_callback('channels_updated', self.wallet) + util.trigger_callback('wallet_updated', self.wallet) @ignore_exceptions @log_exceptions @@ -1355,7 +1355,7 @@ class LNBackups(Logger): self.channel_backups[bfh(channel_id)] = ChannelBackup(cb, sweep_address=self.sweep_address, lnworker=self) def channel_state_changed(self, chan): - self.network.trigger_callback('channel', chan) + util.trigger_callback('channel', chan) def peer_closed(self, chan): pass @@ -1389,7 +1389,7 @@ class LNBackups(Logger): d[channel_id] = cb_storage self.channel_backups[bfh(channel_id)] = cb = ChannelBackup(cb_storage, sweep_address=self.sweep_address, lnworker=self) self.wallet.save_db() - self.network.trigger_callback('channels_updated', self.wallet) + util.trigger_callback('channels_updated', self.wallet) self.lnwatcher.add_channel(cb.funding_outpoint.to_str(), cb.get_funding_address()) def remove_channel_backup(self, channel_id): @@ -1399,7 +1399,7 @@ class LNBackups(Logger): d.pop(channel_id.hex()) self.channel_backups.pop(channel_id) self.wallet.save_db() - self.network.trigger_callback('channels_updated', self.wallet) + util.trigger_callback('channels_updated', self.wallet) @log_exceptions async def request_force_close(self, channel_id): diff --git a/electrum/network.py b/electrum/network.py @@ -278,7 +278,6 @@ class Network(Logger): # locks self.restart_lock = asyncio.Lock() self.bhi_lock = asyncio.Lock() - self.callback_lock = threading.Lock() self.recent_servers_lock = threading.RLock() # <- re-entrant self.interfaces_lock = threading.Lock() # for mutating/iterating self.interfaces @@ -288,8 +287,6 @@ class Network(Logger): self.banner = '' self.donation_address = '' self.relay_fee = None # type: Optional[int] - # callbacks set by the GUI - self.callbacks = defaultdict(list) # note: needs self.callback_lock dir_path = os.path.join(self.config.path, 'certs') util.make_dir(dir_path) @@ -332,7 +329,7 @@ class Network(Logger): from . import channel_db self.channel_db = channel_db.ChannelDB(self) self.path_finder = lnrouter.LNPathFinder(self.channel_db) - self.lngossip = lnworker.LNGossip(self) + self.lngossip = lnworker.LNGossip() self.lngossip.start_network(self) def run_from_another_thread(self, coro, *, timeout=None): @@ -350,27 +347,6 @@ class Network(Logger): return func(self, *args, **kwargs) return func_wrapper - def register_callback(self, callback, events): - with self.callback_lock: - for event in events: - self.callbacks[event].append(callback) - - def unregister_callback(self, callback): - with self.callback_lock: - for callbacks in self.callbacks.values(): - if callback in callbacks: - callbacks.remove(callback) - - def trigger_callback(self, event, *args): - with self.callback_lock: - callbacks = self.callbacks[event][:] - 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) - else: - self.asyncio_loop.call_soon_threadsafe(callback, event, *args) - def _read_recent_servers(self): if not self.config.path: return [] @@ -481,9 +457,9 @@ class Network(Logger): def notify(self, key): if key in ['status', 'updated']: - self.trigger_callback(key) + util.trigger_callback(key) else: - self.trigger_callback(key, self.get_status_value(key)) + util.trigger_callback(key, self.get_status_value(key)) def get_parameters(self) -> NetworkParameters: host, port, protocol = deserialize_server(self.default_server) @@ -574,7 +550,7 @@ class Network(Logger): self.proxy = proxy dns_hacks.configure_dns_depending_on_proxy(bool(proxy)) self.logger.info(f'setting proxy {proxy}') - self.trigger_callback('proxy_set', self.proxy) + util.trigger_callback('proxy_set', self.proxy) @log_exceptions async def set_parameters(self, net_params: NetworkParameters): @@ -700,12 +676,13 @@ class Network(Logger): blockchain_updated = i.blockchain != self.blockchain() self.interface = i await i.taskgroup.spawn(self._request_server_info(i)) - self.trigger_callback('default_server_changed') + util.trigger_callback('default_server_changed') self.default_server_changed_event.set() self.default_server_changed_event.clear() self._set_status('connected') - self.trigger_callback('network_updated') - if blockchain_updated: self.trigger_callback('blockchain_updated') + util.trigger_callback('network_updated') + if blockchain_updated: + util.trigger_callback('blockchain_updated') async def _close_interface(self, interface: Interface): if interface: @@ -734,7 +711,7 @@ class Network(Logger): if server == self.default_server: self._set_status('disconnected') await self._close_interface(interface) - self.trigger_callback('network_updated') + util.trigger_callback('network_updated') def get_network_timeout_seconds(self, request_type=NetworkTimeout.Generic) -> int: if self.oneserver and not self.auto_connect: @@ -767,7 +744,7 @@ class Network(Logger): await self.switch_to_interface(server) self._add_recent_server(server) - self.trigger_callback('network_updated') + util.trigger_callback('network_updated') def check_interface_against_healthy_spread_of_connected_servers(self, iface_to_check) -> bool: # main interface is exempt. this makes switching servers easier @@ -1152,7 +1129,7 @@ class Network(Logger): self.logger.info("taskgroup stopped.") asyncio.run_coroutine_threadsafe(main(), self.asyncio_loop) - self.trigger_callback('network_updated') + util.trigger_callback('network_updated') def start(self, jobs: Iterable = None): """Schedule starting the network, along with the given job co-routines. @@ -1176,7 +1153,7 @@ class Network(Logger): self.connecting.clear() self.server_queue = None if not full_shutdown: - self.trigger_callback('network_updated') + util.trigger_callback('network_updated') def stop(self): assert self._loop_thread != threading.current_thread(), 'must not be called from network thread' diff --git a/electrum/synchronizer.py b/electrum/synchronizer.py @@ -30,6 +30,7 @@ import logging from aiorpcx import TaskGroup, run_in_thread, RPCError +from . import util from .transaction import Transaction, PartialTransaction from .util import bh2u, make_aiohttp_session, NetworkJobOnDefaultServer from .bitcoin import address_to_scripthash, is_address @@ -227,7 +228,7 @@ class Synchronizer(SynchronizerBase): self.wallet.receive_tx_callback(tx_hash, tx, tx_height) self.logger.info(f"received tx {tx_hash} height: {tx_height} bytes: {len(raw_tx)}") # callbacks - self.wallet.network.trigger_callback('new_transaction', self.wallet, tx) + util.trigger_callback('new_transaction', self.wallet, tx) async def main(self): self.wallet.set_up_to_date(False) @@ -252,7 +253,7 @@ class Synchronizer(SynchronizerBase): if up_to_date: self._reset_request_counters() self.wallet.set_up_to_date(up_to_date) - self.wallet.network.trigger_callback('wallet_updated', self.wallet) + util.trigger_callback('wallet_updated', self.wallet) class Notifier(SynchronizerBase): diff --git a/electrum/util.py b/electrum/util.py @@ -1130,7 +1130,7 @@ class NetworkJobOnDefaultServer(Logger): self._restart_lock = asyncio.Lock() self._reset() asyncio.run_coroutine_threadsafe(self._restart(), network.asyncio_loop) - network.register_callback(self._restart, ['default_server_changed']) + register_callback(self._restart, ['default_server_changed']) def _reset(self): """Initialise fields. Called every time the underlying @@ -1304,3 +1304,41 @@ def randrange(bound: int) -> int: """Return a random integer k such that 1 <= k < bound, uniformly distributed across that range.""" return ecdsa.util.randrange(bound) + + +class CallbackManager: + # callbacks set by the GUI + def __init__(self): + self.callback_lock = threading.Lock() + self.callbacks = defaultdict(list) # note: needs self.callback_lock + self.asyncio_loop = None + + def register_callback(self, callback, events): + with self.callback_lock: + for event in events: + self.callbacks[event].append(callback) + + def unregister_callback(self, callback): + with self.callback_lock: + for callbacks in self.callbacks.values(): + if callback in callbacks: + callbacks.remove(callback) + + def trigger_callback(self, event, *args): + if self.asyncio_loop is None: + self.asyncio_loop = asyncio.get_event_loop() + assert self.asyncio_loop.is_running(), "event loop not running" + with self.callback_lock: + callbacks = self.callbacks[event][:] + 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) + else: + self.asyncio_loop.call_soon_threadsafe(callback, event, *args) + + +callback_mgr = CallbackManager() +trigger_callback = callback_mgr.trigger_callback +register_callback = callback_mgr.register_callback +unregister_callback = callback_mgr.unregister_callback