From 7c1c3d2d6d07bc530e9454fa7e0eb1ccd0c7829c Mon Sep 17 00:00:00 2001 From: ThomasV Date: Thu, 31 May 2018 12:38:02 +0200 Subject: [PATCH] lightning GUI: use existing receive and send tabs with lightning invoices --- electrum/gui/qt/main_window.py | 133 ++++++++++++++++-------------- electrum/gui/qt/paytoedit.py | 9 +- electrum/gui/qt/request_list.py | 75 +++++++++++------ gui/qt/lightning_channels_list.py | 22 ++--- icons.qrc | 1 + icons/lightning.png | Bin 0 -> 446 bytes lib/lnworker.py | 32 ++++++- 7 files changed, 165 insertions(+), 107 deletions(-) create mode 100644 icons/lightning.png diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index e59e55da1..11c2fbc20 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -73,7 +73,6 @@ from .transaction_dialog import show_transaction from .fee_slider import FeeSlider from .util import * from .installwizard import WIF_HELP_TEXT -from .lightning_invoice_list import LightningInvoiceList from .lightning_channels_list import LightningChannelsList @@ -162,11 +161,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): tabs.addTab(self.send_tab, QIcon(":icons/tab_send.png"), _('Send')) tabs.addTab(self.receive_tab, QIcon(":icons/tab_receive.png"), _('Receive')) if config.get("lnbase", False): - self.lightning_invoices_tab = self.create_lightning_invoices_tab(wallet) - tabs.addTab(self.lightning_invoices_tab, _("Lightning Invoices")) - self.lightning_channels_tab = self.create_lightning_channels_tab(wallet) - tabs.addTab(self.lightning_channels_tab, _("Lightning Channels")) + tabs.addTab(self.lightning_channels_tab, QIcon(":icons/lightning.png"), _("Channels")) def add_optional_tab(tabs, tab, icon, description, name): tab.tab_icon = icon @@ -809,10 +805,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): self.invoice_list.update() self.update_completions() - def create_lightning_invoices_tab(self, wallet): - self.lightning_invoice_list = LightningInvoiceList(self, wallet.lnworker) - return self.lightning_invoice_list - def create_lightning_channels_tab(self, wallet): self.lightning_channels_list = LightningChannelsList(self, wallet.lnworker) return self.lightning_channels_list @@ -842,16 +834,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): grid.setSpacing(8) grid.setColumnStretch(3, 1) - self.receive_address_e = ButtonsLineEdit() - self.receive_address_e.addCopyButton(self.app) - self.receive_address_e.setReadOnly(True) - msg = _('Bitcoin address where the payment should be received. Note that each payment request uses a different Bitcoin address.') - self.receive_address_label = HelpLabel(_('Receiving address'), msg) - self.receive_address_e.textChanged.connect(self.update_receive_qr) - self.receive_address_e.setFocusPolicy(Qt.ClickFocus) - grid.addWidget(self.receive_address_label, 0, 0) - grid.addWidget(self.receive_address_e, 0, 1, 1, -1) - self.receive_message_e = QLineEdit() grid.addWidget(QLabel(_('Description')), 1, 0) grid.addWidget(self.receive_message_e, 1, 1, 1, -1) @@ -886,23 +868,30 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): self.expires_label.hide() grid.addWidget(self.expires_label, 3, 1) - self.save_request_button = QPushButton(_('Save')) - self.save_request_button.clicked.connect(self.save_payment_request) + self.receive_type = QComboBox() + self.receive_type.addItems([_('Bitcoin address'), _('Lightning')]) + grid.addWidget(QLabel(_('Type')), 4, 0) + grid.addWidget(self.receive_type, 4, 1) - self.new_request_button = QPushButton(_('New')) - self.new_request_button.clicked.connect(self.new_payment_request) + self.save_request_button = QPushButton(_('Create')) + self.save_request_button.clicked.connect(self.create_invoice) + + self.receive_buttons = buttons = QHBoxLayout() + buttons.addWidget(self.save_request_button) + buttons.addStretch(1) + grid.addLayout(buttons, 4, 2, 1, 2) + + self.receive_address_e = ButtonsTextEdit() + self.receive_address_e.addCopyButton(self.app) + self.receive_address_e.setReadOnly(True) + self.receive_address_e.textChanged.connect(self.update_receive_qr) + self.receive_address_e.setFocusPolicy(Qt.ClickFocus) self.receive_qr = QRCodeWidget(fixedSize=200) self.receive_qr.mouseReleaseEvent = lambda x: self.toggle_qr_window() self.receive_qr.enterEvent = lambda x: self.app.setOverrideCursor(QCursor(Qt.PointingHandCursor)) self.receive_qr.leaveEvent = lambda x: self.app.setOverrideCursor(QCursor(Qt.ArrowCursor)) - self.receive_buttons = buttons = QHBoxLayout() - buttons.addStretch(1) - buttons.addWidget(self.save_request_button) - buttons.addWidget(self.new_request_button) - grid.addLayout(buttons, 4, 1, 1, 2) - self.receive_requests_label = QLabel(_('Requests')) from .request_list import RequestList @@ -913,14 +902,19 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): vbox_g.addLayout(grid) vbox_g.addStretch() + hbox_r = QHBoxLayout() + hbox_r.addWidget(self.receive_qr) + hbox_r.addWidget(self.receive_address_e) + hbox = QHBoxLayout() hbox.addLayout(vbox_g) - hbox.addWidget(self.receive_qr) + hbox.addLayout(hbox_r) w = QWidget() w.searchable_list = self.request_list vbox = QVBoxLayout(w) vbox.addLayout(hbox) + vbox.addStretch(1) vbox.addWidget(self.receive_requests_label) vbox.addWidget(self.request_list) @@ -939,8 +933,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): message = self.wallet.labels.get(addr, '') amount = req['amount'] URI = util.create_URI(addr, amount, message) - if req.get('time'): - URI += "&time=%d"%req.get('time') + #if req.get('time'): + # URI += "&time=%d"%req.get('time') if req.get('exp'): URI += "&exp=%d"%req.get('exp') if req.get('name') and req.get('sig'): @@ -971,15 +965,34 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): else: return - def save_payment_request(self): - addr = str(self.receive_address_e.text()) + def create_invoice(self): amount = self.receive_amount_e.get_amount() message = self.receive_message_e.text() - if not message and not amount: - self.show_error(_('No message or amount')) - return False i = self.expires_combo.currentIndex() expiration = list(map(lambda x: x[1], expiration_values))[i] + if self.receive_type.currentIndex() == 1: + self.create_lightning_request(amount, message, expiration) + else: + self.create_bitcoin_request(amount, message, expiration) + self.request_list.update() + + def create_lightning_request(self, amount, message, expiration): + req = self.wallet.lnworker.add_invoice(amount) + + def create_bitcoin_request(self, amount, message, expiration): + addr = self.wallet.get_unused_address() + if addr is None: + if not self.wallet.is_deterministic(): + msg = [ + _('No more addresses in your wallet.'), + _('You are using a non-deterministic wallet, which cannot create new addresses.'), + _('If you want to create new addresses, use a deterministic wallet instead.') + ] + self.show_message(' '.join(msg)) + return + if not self.question(_("Warning: The next address will not be recovered automatically if you restore your wallet from seed; you may need to add it manually.\n\nThis occurs because you have too many unused addresses in your wallet. To avoid this situation, use the existing addresses first.\n\nCreate anyway?")): + return + addr = self.wallet.create_new_address(False) req = self.wallet.make_payment_request(addr, amount, message, expiration) try: self.wallet.add_payment_request(req, self.config) @@ -990,7 +1003,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): self.sign_payment_request(addr) self.save_request_button.setEnabled(False) finally: - self.request_list.update() self.address_list.update() def view_and_paste(self, title, msg, data): @@ -1016,26 +1028,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): self.show_message(_("Request saved successfully")) self.saved = True - def new_payment_request(self): - addr = self.wallet.get_unused_address() - if addr is None: - if not self.wallet.is_deterministic(): - msg = [ - _('No more addresses in your wallet.'), - _('You are using a non-deterministic wallet, which cannot create new addresses.'), - _('If you want to create new addresses, use a deterministic wallet instead.') - ] - self.show_message(' '.join(msg)) - return - if not self.question(_("Warning: The next address will not be recovered automatically if you restore your wallet from seed; you may need to add it manually.\n\nThis occurs because you have too many unused addresses in your wallet. To avoid this situation, use the existing addresses first.\n\nCreate anyway?")): - return - addr = self.wallet.create_new_address(False) - self.set_receive_address(addr) - self.expires_label.hide() - self.expires_combo.show() - self.new_request_button.setEnabled(False) - self.receive_message_e.setFocus(1) - def set_receive_address(self, addr): self.receive_address_e.setText(addr) self.receive_message_e.setText('') @@ -1078,14 +1070,10 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): self.new_request_button.setEnabled(True) def update_receive_qr(self): - addr = str(self.receive_address_e.text()) - amount = self.receive_amount_e.get_amount() - message = self.receive_message_e.text() - self.save_request_button.setEnabled((amount is not None) or (message != "")) - uri = util.create_URI(addr, amount, message) + uri = str(self.receive_address_e.text()) self.receive_qr.setData(uri) if self.qr_window and self.qr_window.isVisible(): - self.qr_window.set_content(addr, amount, message, uri) + self.qr_window.set_content(uri, '', '', uri) def set_feerounding_text(self, num_satoshis_added): self.feerounding_text = (_('Additional {} satoshis are going to be added.') @@ -1764,6 +1752,23 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): else: self.payment_request_error_signal.emit() + def parse_lightning_invoice(self, invoice): + from electrum.lightning_payencode.lnaddr import lndecode + lnaddr = lndecode(invoice, expected_hrp=constants.net.SEGWIT_HRP) + pubkey = bh2u(lnaddr.pubkey.serialize()) + for k,v in lnaddr.tags: + if k == 'd': + description = v + break + else: + description = '' + self.payto_e.setFrozen(True) + self.payto_e.setGreen() + self.payto_e.setText(pubkey) + self.message_e.setText(description) + self.amount_e.setAmount(lnaddr.amount) + #self.amount_e.textEdited.emit("") + def pay_to_URI(self, URI): if not URI: return diff --git a/electrum/gui/qt/paytoedit.py b/electrum/gui/qt/paytoedit.py index 6235b85cc..2b6bfce7b 100644 --- a/electrum/gui/qt/paytoedit.py +++ b/electrum/gui/qt/paytoedit.py @@ -58,10 +58,8 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit, PrintError): self.errors = [] self.is_pr = False self.is_alias = False - self.scan_f = win.pay_to_URI self.update_size() self.payto_address = None - self.previous_payto = '' def setFrozen(self, b): @@ -129,7 +127,10 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit, PrintError): if len(lines) == 1: data = lines[0] if data.startswith("bitcoin:"): - self.scan_f(data) + self.win.pay_to_URI(data) + return + if data.startswith("ln"): + self.win.parse_lightning_invoice(data) return try: self.payto_address = self.parse_output(data) @@ -203,7 +204,7 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit, PrintError): def qr_input(self): data = super(PayToEdit,self).qr_input() if data.startswith("bitcoin:"): - self.scan_f(data) + self.win.pay_to_URI(data) # TODO: update fee def resolve(self): diff --git a/electrum/gui/qt/request_list.py b/electrum/gui/qt/request_list.py index 19ec59703..ed786e726 100644 --- a/electrum/gui/qt/request_list.py +++ b/electrum/gui/qt/request_list.py @@ -25,7 +25,7 @@ from PyQt5.QtGui import * from PyQt5.QtCore import * -from PyQt5.QtWidgets import QTreeWidgetItem, QMenu +from PyQt5.QtWidgets import QTreeWidgetItem, QMenu, QHeaderView from electrum.i18n import _ from electrum.util import format_time, age @@ -35,38 +35,47 @@ from electrum.paymentrequest import PR_UNKNOWN from .util import MyTreeWidget, pr_tooltips, pr_icons -class RequestList(MyTreeWidget): - filter_columns = [0, 1, 2, 3, 4] # Date, Account, Address, Description, Amount +class RequestList(MyTreeWidget): + filter_columns = [0, 2, 3, 4] # Date, Address, Description, Amount def __init__(self, parent): - MyTreeWidget.__init__(self, parent, self.create_menu, [_('Date'), _('Address'), '', _('Description'), _('Amount'), _('Status')], 3) + MyTreeWidget.__init__(self, parent, self.create_menu, [_('Date'), _('Type'), _('Address'), _('Description'), _('Amount'), _('Status')], 3) self.currentItemChanged.connect(self.item_changed) self.itemClicked.connect(self.item_changed) self.setSortingEnabled(True) self.setColumnWidth(0, 180) - self.hideColumn(1) + self.setColumnWidth(2, 250) + + def update_headers(self, headers): + self.setColumnCount(len(headers)) + self.setHeaderLabels(headers) + self.header().setStretchLastSection(False) + for col in range(len(headers)): + if col in [2]: continue + sm = QHeaderView.Stretch if col == self.stretch_column else QHeaderView.ResizeToContents + self.header().setSectionResizeMode(col, sm) def item_changed(self, item): if item is None: return if not item.isSelected(): return - addr = str(item.text(1)) - req = self.wallet.receive_requests.get(addr) - if req is None: - self.update() - return - expires = age(req['time'] + req['exp']) if req.get('exp') else _('Never') - amount = req['amount'] - message = self.wallet.labels.get(addr, '') + addr = str(item.text(2)) self.parent.receive_address_e.setText(addr) - self.parent.receive_message_e.setText(message) - self.parent.receive_amount_e.setAmount(amount) - self.parent.expires_combo.hide() - self.parent.expires_label.show() - self.parent.expires_label.setText(expires) - self.parent.new_request_button.setEnabled(True) + #req = self.wallet.receive_requests.get(addr) + #if req is None: + # self.update() + # return + #expires = age(req['time'] + req['exp']) if req.get('exp') else _('Never') + #amount = req['amount'] + #message = self.wallet.labels.get(addr, '') + #self.parent.receive_message_e.setText(message) + #self.parent.receive_amount_e.setAmount(amount) + #self.parent.expires_combo.hide() + #self.parent.expires_label.show() + #self.parent.expires_label.setText(expires) + #self.parent.new_request_button.setEnabled(True) def on_update(self): self.wallet = self.parent.wallet @@ -79,12 +88,12 @@ class RequestList(MyTreeWidget): self.parent.expires_combo.show() # update the receive address if necessary - current_address = self.parent.receive_address_e.text() + #current_address = self.parent.receive_address_e.text() domain = self.wallet.get_receiving_addresses() - addr = self.wallet.get_unused_address() - if not current_address in domain and addr: - self.parent.set_receive_address(addr) - self.parent.new_request_button.setEnabled(addr != current_address) + #addr = self.wallet.get_unused_address() + #if not current_address in domain and addr: + # self.parent.set_receive_address(addr) + #self.parent.new_request_button.setEnabled(addr != current_address) # clear the list and fill it again self.clear() @@ -101,13 +110,29 @@ class RequestList(MyTreeWidget): signature = req.get('sig') requestor = req.get('name', '') amount_str = self.parent.format_amount(amount) if amount else "" - item = QTreeWidgetItem([date, address, '', message, amount_str, pr_tooltips.get(status,'')]) + URI = self.parent.get_request_URI(address) + item = QTreeWidgetItem([date, '', URI, message, amount_str, pr_tooltips.get(status,'')]) if signature is not None: item.setIcon(2, self.icon_cache.get(":icons/seal.png")) item.setToolTip(2, 'signed by '+ requestor) if status is not PR_UNKNOWN: item.setIcon(6, self.icon_cache.get(pr_icons.get(status))) self.addTopLevelItem(item) + # lightning + for k, r in self.wallet.lnworker.invoices.items(): + from electrum.lightning_payencode.lnaddr import lndecode + import electrum.constants as constants + lnaddr = lndecode(r, expected_hrp=constants.net.SEGWIT_HRP) + amount_str = self.parent.format_amount(lnaddr.amount*100000000) + for k,v in lnaddr.tags: + if k == 'd': + description = v + break + else: + description = '' + item = QTreeWidgetItem([date, '', r, description, amount_str, '']) + item.setIcon(1, QIcon(":icons/lightning.png")) + self.addTopLevelItem(item) def create_menu(self, position): diff --git a/gui/qt/lightning_channels_list.py b/gui/qt/lightning_channels_list.py index 7b75321e3..09e0d4812 100644 --- a/gui/qt/lightning_channels_list.py +++ b/gui/qt/lightning_channels_list.py @@ -47,6 +47,14 @@ class LightningChannelsList(QtWidgets.QWidget): assert local_amt >= push_amt obj = self.lnworker.open_channel(node_id, local_amt, push_amt, password) + def create_menu(self, position): + menu = QtWidgets.QMenu() + cur = self._tv.currentItem() + def close(): + print("closechannel result", lnworker.close_channel_from_other_thread(cur.di)) + menu.addAction("Close channel", close) + menu.exec_(self._tv.viewport().mapToGlobal(position)) + @QtCore.pyqtSlot(dict) def do_update_single_row(self, new): try: @@ -60,14 +68,6 @@ class LightningChannelsList(QtWidgets.QWidget): except KeyError: obj[k] = v - def create_menu(self, position): - menu = QtWidgets.QMenu() - cur = self._tv.currentItem() - def close(): - print("closechannel result", lnworker.close_channel_from_other_thread(cur.di)) - menu.addAction("Close channel", close) - menu.exec_(self._tv.viewport().mapToGlobal(position)) - @QtCore.pyqtSlot(dict) def do_update_rows(self, obj): self._tv.clear() @@ -82,9 +82,8 @@ class LightningChannelsList(QtWidgets.QWidget): self.update_single_row.connect(self.do_update_single_row) self.lnworker = lnworker - - #lnworker.subscribe_channel_list_updates_from_other_thread(self.update_rows.emit) - #lnworker.subscribe_single_channel_update_from_other_thread(self.update_single_row.emit) + lnworker.register_callback(self.update_rows.emit, ['channels_updated']) + lnworker.register_callback(self.update_single_row.emit, ['channel_updated']) self._tv=QtWidgets.QTreeWidget(self) self._tv.setHeaderLabels([mapping[i] for i in range(len(mapping))]) @@ -122,3 +121,4 @@ class LightningChannelsList(QtWidgets.QWidget): l.addWidget(self._tv) self.resize(2500,1000) + lnworker.on_channels_updated() diff --git a/icons.qrc b/icons.qrc index 19c298adc..67db4fe5b 100644 --- a/icons.qrc +++ b/icons.qrc @@ -22,6 +22,7 @@ icons/key.png icons/ledger.png icons/ledger_unpaired.png + icons/lightning.png icons/lock.png icons/microphone.png icons/network.png diff --git a/icons/lightning.png b/icons/lightning.png new file mode 100644 index 0000000000000000000000000000000000000000..797d3ca9381688d75a8f2f7e02b9fb33a52ff6b3 GIT binary patch literal 446 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY3?!3`olAj~L4Z$)t9yaZ|24+MBabr|Re^f> zOM?7@8Qu#d%r`jSpm4w6VgLGq<8_ngGB7aedAc};RNPuS-CO9e0?*MLg+{f^+W)t| zZLX9qXYArTkm2_A^p$6pO1lbA8QV!Ld-HLrM9SwMmw1Y*{zx2KA?V|=^PPZ}u1ZUB z;X>c{CiDKb^H1@f@cD(}o*PEGd4gKuca%)N9dJ5zv~?A`a8Q}vl9)%VZl+E)H>P%R z6(ugb%*?GRE^ei?uqlw$=~Qd$wm);bQ^lW`tc(0LKX9)4{!6Tv0%ISXa4T(`^>o(p zY_nCLf4FVxu2l)&bzrJah0cXD2b{l%pFRHI&+P0;+UK`Ud@EMHF;XsbU6PMSI9E&F z|6M;beLQY%oKRG_@cAw4^-sLQHTsG==k;g_2I;9S2~2g83RPPY6VmbD+4<(tf+^l6 zPo3WLlr3GPy11)_FB!<=>D5}SX}w58++{J>OYfccV(dcV8Q1r900Wi5)78&qol`;+ E0H3$V5&!@I literal 0 HcmV?d00001 diff --git a/lib/lnworker.py b/lib/lnworker.py index 701e98bb2..c8fa43f09 100644 --- a/lib/lnworker.py +++ b/lib/lnworker.py @@ -8,7 +8,8 @@ import os from decimal import Decimal import binascii import asyncio - +import threading +from collections import defaultdict from . import constants from .bitcoin import sha256, COIN @@ -109,6 +110,8 @@ class LNWorker(PrintError): self.channel_state = {chan.channel_id: "OPENING" for chan in self.channels} for host, port, pubkey in peer_list: self.add_peer(host, int(port), pubkey) + + self.callbacks = defaultdict(list) # wait until we see confirmations self.network.register_callback(self.on_network_update, ['updated', 'verified']) # thread safe self.on_network_update('updated') # shortcut (don't block) if funding tx locked and verified @@ -119,6 +122,7 @@ class LNWorker(PrintError): peer = Peer(host, int(port), node_id, self.privkey, self.network, self.channel_db, self.path_finder, self.channel_state, channels, self.invoices, request_initial_sync=True) self.network.futures.append(asyncio.run_coroutine_threadsafe(peer.main_loop(), asyncio.get_event_loop())) self.peers[node_id] = peer + self.lock = threading.Lock() def save_channel(self, openchannel): if openchannel.channel_id not in self.channel_state: @@ -127,6 +131,7 @@ class LNWorker(PrintError): dumped = serialize_channels(self.channels) self.wallet.storage.put("channels", dumped) self.wallet.storage.write() + self.trigger_callback('channel_updated', {"chan_id": openchannel.channel_id}) def save_short_chan_id(self, chan): """ @@ -176,6 +181,11 @@ class LNWorker(PrintError): openingchannel = await peer.channel_establishment_flow(self.wallet, self.config, password, amount_sat, push_sat * 1000, temp_channel_id=os.urandom(32)) self.print_error("SAVING OPENING CHANNEL") self.save_channel(openingchannel) + self.on_channels_updated() + + def on_channels_updated(self): + std_chan = [{"chan_id": chan.channel_id} for chan in self.channels] + self.trigger_callback('channels_updated', {'channels':std_chan}) def open_channel(self, node_id, local_amt_sat, push_amt_sat, pw): coro = self._open_channel_coroutine(node_id, local_amt_sat, push_amt_sat, None if pw == "" else pw) @@ -199,8 +209,8 @@ class LNWorker(PrintError): def add_invoice(self, amount_sat, message='one cup of coffee'): is_open = lambda chan: self.channel_state[chan] == "OPEN" # TODO doesn't account for fees!!! - if not any(openchannel.remote_state.amount_msat >= amount_sat * 1000 for openchannel in self.channels if is_open(chan)): - return "Not making invoice, no channel has enough balance" + #if not any(openchannel.remote_state.amount_msat >= amount_sat * 1000 for openchannel in self.channels if is_open(chan)): + # return "Not making invoice, no channel has enough balance" payment_preimage = os.urandom(32) RHASH = sha256(payment_preimage) pay_req = lnencode(LnAddr(RHASH, amount_sat/Decimal(COIN), tags=[('d', message)]), self.privkey) @@ -213,3 +223,19 @@ class LNWorker(PrintError): def list_channels(self): return serialize_channels(self.channels) + + def register_callback(self, callback, events): + with self.lock: + for event in events: + self.callbacks[event].append(callback) + + def unregister_callback(self, callback): + with self.lock: + for callbacks in self.callbacks.values(): + if callback in callbacks: + callbacks.remove(callback) + + def trigger_callback(self, event, *args): + with self.lock: + callbacks = self.callbacks[event][:] + [callback(*args) for callback in callbacks]