From b6e1e527c2618802fc3c51176199873db302e0c2 Mon Sep 17 00:00:00 2001 From: f321x Date: Fri, 2 May 2025 16:12:55 +0200 Subject: [PATCH] contacts: support lightning addressses as contact address lightning addresses are useful and widely adopted, so it should be possible to save them as payment identifier for a contact. --- electrum/contacts.py | 18 +++++++++++------- electrum/gui/qt/contact_list.py | 4 ++++ electrum/gui/qt/main_window.py | 15 ++++++++++----- electrum/gui/qt/send_tab.py | 2 +- electrum/payment_identifier.py | 5 +---- 5 files changed, 27 insertions(+), 17 deletions(-) diff --git a/electrum/contacts.py b/electrum/contacts.py index c7b06cca9..bbcf13038 100644 --- a/electrum/contacts.py +++ b/electrum/contacts.py @@ -29,7 +29,7 @@ from dns.exception import DNSException from . import bitcoin from . import dnssec -from .util import read_json_file, write_json_file, to_string +from .util import read_json_file, write_json_file, to_string, is_valid_email from .logging import Logger, get_logger from .util import trigger_callback @@ -83,6 +83,7 @@ class Contacts(dict, Logger): res = dict.pop(self, key) self.save() return res + return None def resolve(self, k): if bitcoin.is_address(k): @@ -90,11 +91,12 @@ class Contacts(dict, Logger): 'address': k, 'type': 'address' } - if k in self.keys(): - _type, addr = self[k] - if _type == 'address': + for address, (_type, label) in self.items(): + if k.casefold() != label.casefold(): + continue + if _type in ('address', 'lnaddress'): return { - 'address': addr, + 'address': address, 'type': 'contact' } if openalias := self.resolve_openalias(k): @@ -159,6 +161,8 @@ class Contacts(dict, Logger): if not address: continue return address, name, validated + return None + return None @staticmethod def find_regex(haystack, needle): @@ -172,11 +176,11 @@ class Contacts(dict, Logger): for k, v in list(data.items()): if k == 'contacts': return self._validate(v) - if not bitcoin.is_address(k): + if not (bitcoin.is_address(k) or is_valid_email(k)): data.pop(k) else: _type, _ = v - if _type != 'address': + if _type not in ('address', 'lnaddress'): data.pop(k) return data diff --git a/electrum/gui/qt/contact_list.py b/electrum/gui/qt/contact_list.py index 652be2baa..0e3142377 100644 --- a/electrum/gui/qt/contact_list.py +++ b/electrum/gui/qt/contact_list.py @@ -34,6 +34,7 @@ from electrum.i18n import _ from electrum.bitcoin import is_address from electrum.util import block_explorer_URL from electrum.plugin import run_hook +from electrum.gui.qt.util import read_QIcon from .util import webopen from .my_treeview import MyTreeView @@ -118,6 +119,9 @@ class ContactList(MyTreeView): items[self.Columns.NAME].setEditable(contact_type != 'openalias') items[self.Columns.ADDRESS].setEditable(False) items[self.Columns.NAME].setData(key, self.ROLE_CONTACT_KEY) + items[self.Columns.NAME].setIcon( + read_QIcon("lightning" if contact_type == 'lnaddress' else "bitcoin") + ) row_count = self.model().rowCount() self.model().insertRow(row_count, items) if key == current_key: diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index e70b0da20..26e2a5562 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -54,8 +54,10 @@ from electrum.bitcoin import COIN, is_address, DummyAddress from electrum.plugin import run_hook from electrum.i18n import _ from electrum.util import (format_time, UserCancelled, profiler, bfh, InvalidPassword, - UserFacingException, get_new_wallet_name, send_exception_to_crash_reporter, - AddTransactionException, os_chmod, UI_UNIT_NAME_TXSIZE_VBYTES, ChoiceItem) + UserFacingException, get_new_wallet_name, + send_exception_to_crash_reporter, + AddTransactionException, os_chmod, UI_UNIT_NAME_TXSIZE_VBYTES, + is_valid_email, ChoiceItem) from electrum.bip21 import BITCOIN_BIP21_URI_SCHEME from electrum.payment_identifier import PaymentIdentifier from electrum.invoices import PR_PAID, Invoice @@ -1614,11 +1616,12 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): self.send_tab.payto_contacts(labels) def set_contact(self, label, address): - if not is_address(address): + if not (is_address(address) or is_valid_email(address)): # email = lightning address self.show_error(_('Invalid Address')) self.contact_list.update() # Displays original unchanged value return False - self.contacts[address] = ('address', label) + address_type = 'address' if is_address(address) else 'lnaddress' + self.contacts[address] = (address_type, label) self.contact_list.update() self.history_list.update() self.update_completions() @@ -2008,7 +2011,9 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): line1.setFixedWidth(32 * char_width_in_lineedit()) line2 = QLineEdit() line2.setFixedWidth(32 * char_width_in_lineedit()) - grid.addWidget(QLabel(_("Address")), 1, 0) + address_label = QLabel(_("Address")) + address_label.setToolTip(_("Bitcoin- or Lightning address")) + grid.addWidget(address_label, 1, 0) grid.addWidget(line1, 1, 1) grid.addWidget(QLabel(_("Name")), 2, 0) grid.addWidget(line2, 2, 1) diff --git a/electrum/gui/qt/send_tab.py b/electrum/gui/qt/send_tab.py index 2817784bf..e9ee9ef83 100644 --- a/electrum/gui/qt/send_tab.py +++ b/electrum/gui/qt/send_tab.py @@ -801,7 +801,7 @@ class SendTab(QWidget, MessageBoxMixin, Logger): def payto_contacts(self, labels): paytos = [self.window.get_contact_payto(label) for label in labels] self.window.show_send_tab() - self.payto_e.do_clear() + self.do_clear() if len(paytos) == 1: self.logger.debug('payto_e setText 1') self.payto_e.setText(paytos[0]) diff --git a/electrum/payment_identifier.py b/electrum/payment_identifier.py index 36044b7ba..88ce2bd73 100644 --- a/electrum/payment_identifier.py +++ b/electrum/payment_identifier.py @@ -285,7 +285,7 @@ class PaymentIdentifier(Logger): 'label': contact['name'] } self.set_state(PaymentIdentifierState.AVAILABLE) - elif contact['type'] == 'openalias': + elif contact['type'] in ('openalias', 'lnaddress'): self._type = PaymentIdentifierType.EMAILLIKE self.emaillike = contact['address'] self.set_state(PaymentIdentifierState.NEED_RESOLVE) @@ -649,9 +649,6 @@ class PaymentIdentifier(Logger): return pubkey, amount, description async def resolve_openalias(self, key: str) -> Optional[dict]: - # TODO: below check needed? we already matched RE_EMAIL/RE_DOMAIN - # if not (('.' in key) and ('<' not in key) and (' ' not in key)): - # return None parts = key.split(sep=',') # assuming single line if parts and len(parts) > 0 and bitcoin.is_address(parts[0]): return None