Browse Source

payment_identifier: add DOMAINLIKE payment identifier type, support domainlike -> openalias

master
Sander van Grieken 3 years ago
parent
commit
fbb37d6fae
  1. 2
      electrum/gui/qt/paytoedit.py
  2. 8
      electrum/gui/qt/send_tab.py
  3. 40
      electrum/payment_identifier.py

2
electrum/gui/qt/paytoedit.py

@ -216,7 +216,7 @@ class PayToEdit(QObject, Logger, GenericInputHandler):
# pushback timer if timer active or PI needs resolving # pushback timer if timer active or PI needs resolving
pi = PaymentIdentifier(self.send_tab.wallet, self.text_edit.toPlainText()) pi = PaymentIdentifier(self.send_tab.wallet, self.text_edit.toPlainText())
if pi.need_resolve() or self.edit_timer.isActive(): if not pi.is_valid() or pi.need_resolve() or self.edit_timer.isActive():
self.edit_timer.start() self.edit_timer.start()
else: else:
self.set_payment_identifier(self.text_edit.toPlainText()) self.set_payment_identifier(self.text_edit.toPlainText())

8
electrum/gui/qt/send_tab.py

@ -363,7 +363,7 @@ class SendTab(QWidget, MessageBoxMixin, Logger):
if pi.is_multiline(): if pi.is_multiline():
self.lock_fields(lock_recipient=False, lock_amount=True, lock_max=True, lock_description=False) self.lock_fields(lock_recipient=False, lock_amount=True, lock_max=True, lock_description=False)
self.set_field_style(self.payto_e, True if not pi.is_valid() else None, False) self.set_field_validated(self.payto_e, validated=pi.is_valid()) # TODO: validated used differently here than openalias
self.save_button.setEnabled(pi.is_valid()) self.save_button.setEnabled(pi.is_valid())
self.send_button.setEnabled(pi.is_valid()) self.send_button.setEnabled(pi.is_valid())
self.payto_e.setToolTip(pi.get_error() if not pi.is_valid() else '') self.payto_e.setToolTip(pi.get_error() if not pi.is_valid() else '')
@ -378,11 +378,13 @@ class SendTab(QWidget, MessageBoxMixin, Logger):
return return
lock_recipient = pi.type != PaymentIdentifierType.SPK \ lock_recipient = pi.type != PaymentIdentifierType.SPK \
and not (pi.type == PaymentIdentifierType.EMAILLIKE and pi.state in [PaymentIdentifierState.NOT_FOUND,PaymentIdentifierState.NEED_RESOLVE]) and not (pi.type in [PaymentIdentifierType.EMAILLIKE, PaymentIdentifierType.DOMAINLIKE] \
and pi.state in [PaymentIdentifierState.NOT_FOUND, PaymentIdentifierState.NEED_RESOLVE])
lock_amount = pi.is_amount_locked() lock_amount = pi.is_amount_locked()
lock_max = lock_amount \ lock_max = lock_amount \
or pi.type in [PaymentIdentifierType.BOLT11, PaymentIdentifierType.LNURLP, or pi.type in [PaymentIdentifierType.BOLT11, PaymentIdentifierType.LNURLP,
PaymentIdentifierType.LNADDR, PaymentIdentifierType.EMAILLIKE] PaymentIdentifierType.LNADDR, PaymentIdentifierType.EMAILLIKE,
PaymentIdentifierType.DOMAINLIKE]
self.lock_fields(lock_recipient=lock_recipient, self.lock_fields(lock_recipient=lock_recipient,
lock_amount=lock_amount, lock_amount=lock_amount,

40
electrum/payment_identifier.py

@ -162,7 +162,8 @@ def is_uri(data: str) -> bool:
RE_ALIAS = r'(.*?)\s*\<([0-9A-Za-z]{1,})\>' RE_ALIAS = r'(.*?)\s*\<([0-9A-Za-z]{1,})\>'
RE_EMAIL = r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,7}\b' RE_EMAIL = r'\b[A-Za-z0-9._%+-]+@([A-Za-z0-9-]+\.)+[A-Z|a-z]{2,7}\b'
RE_DOMAIN = r'\b([A-Za-z0-9-]+\.)+[A-Z|a-z]{2,7}\b'
class PaymentIdentifierState(IntEnum): class PaymentIdentifierState(IntEnum):
@ -194,6 +195,7 @@ class PaymentIdentifierType(IntEnum):
EMAILLIKE = 7 EMAILLIKE = 7
OPENALIAS = 8 OPENALIAS = 8
LNADDR = 9 LNADDR = 9
DOMAINLIKE = 10
class FieldsForGUI(NamedTuple): class FieldsForGUI(NamedTuple):
@ -235,6 +237,7 @@ class PaymentIdentifier(Logger):
self.spk = None self.spk = None
# #
self.emaillike = None self.emaillike = None
self.domainlike = None
self.openalias_data = None self.openalias_data = None
# #
self.bip70 = None self.bip70 = None
@ -284,9 +287,7 @@ class PaymentIdentifier(Logger):
return self.is_multiline() and self._is_max return self.is_multiline() and self._is_max
def is_amount_locked(self): def is_amount_locked(self):
if self._type == PaymentIdentifierType.SPK: if self._type == PaymentIdentifierType.BIP21:
return False
elif self._type == PaymentIdentifierType.BIP21:
return bool(self.bip21.get('amount')) return bool(self.bip21.get('amount'))
elif self._type == PaymentIdentifierType.BIP70: elif self._type == PaymentIdentifierType.BIP70:
return True # TODO always given? return True # TODO always given?
@ -303,9 +304,7 @@ class PaymentIdentifier(Logger):
return True return True
elif self._type == PaymentIdentifierType.MULTILINE: elif self._type == PaymentIdentifierType.MULTILINE:
return True return True
elif self._type == PaymentIdentifierType.EMAILLIKE: else:
return False
elif self._type == PaymentIdentifierType.OPENALIAS:
return False return False
def is_error(self) -> bool: def is_error(self) -> bool:
@ -384,6 +383,10 @@ class PaymentIdentifier(Logger):
self._type = PaymentIdentifierType.EMAILLIKE self._type = PaymentIdentifierType.EMAILLIKE
self.emaillike = text self.emaillike = text
self.set_state(PaymentIdentifierState.NEED_RESOLVE) self.set_state(PaymentIdentifierState.NEED_RESOLVE)
elif re.match(RE_DOMAIN, text):
self._type = PaymentIdentifierType.DOMAINLIKE
self.domainlike = text
self.set_state(PaymentIdentifierState.NEED_RESOLVE)
elif self.error is None: elif self.error is None:
truncated_text = f"{text[:100]}..." if len(text) > 100 else text truncated_text = f"{text[:100]}..." if len(text) > 100 else text
self.error = f"Unknown payment identifier:\n{truncated_text}" self.error = f"Unknown payment identifier:\n{truncated_text}"
@ -397,7 +400,7 @@ class PaymentIdentifier(Logger):
@log_exceptions @log_exceptions
async def _do_resolve(self, *, on_finished=None): async def _do_resolve(self, *, on_finished=None):
try: try:
if self.emaillike: if self.emaillike or self.domainlike:
# TODO: parallel lookup? # TODO: parallel lookup?
data = await self.resolve_openalias() data = await self.resolve_openalias()
if data: if data:
@ -405,11 +408,12 @@ class PaymentIdentifier(Logger):
self.logger.debug(f'OA: {data!r}') self.logger.debug(f'OA: {data!r}')
name = data.get('name') name = data.get('name')
address = data.get('address') address = data.get('address')
self.contacts[self.emaillike] = ('openalias', name) key = self.emaillike if self.emaillike else self.domainlike
self.contacts[key] = ('openalias', name)
if not data.get('validated'): if not data.get('validated'):
self.warning = _( self.warning = _(
'WARNING: the alias "{}" could not be validated via an additional ' 'WARNING: the alias "{}" could not be validated via an additional '
'security check, DNSSEC, and thus may not be correct.').format(self.emaillike) 'security check, DNSSEC, and thus may not be correct.').format(key)
try: try:
assert bitcoin.is_address(address) assert bitcoin.is_address(address)
scriptpubkey = bytes.fromhex(bitcoin.address_to_script(address)) scriptpubkey = bytes.fromhex(bitcoin.address_to_script(address))
@ -419,7 +423,7 @@ class PaymentIdentifier(Logger):
except Exception as e: except Exception as e:
self.error = str(e) self.error = str(e)
self.set_state(PaymentIdentifierState.NOT_FOUND) self.set_state(PaymentIdentifierState.NOT_FOUND)
else: elif self.emaillike:
lnurl = lightning_address_to_url(self.emaillike) lnurl = lightning_address_to_url(self.emaillike)
try: try:
data = await request_lnurl(lnurl) data = await request_lnurl(lnurl)
@ -433,6 +437,8 @@ class PaymentIdentifier(Logger):
# NOTE: any other exception is swallowed here (e.g. DNS error) # NOTE: any other exception is swallowed here (e.g. DNS error)
# as the user may be typing and we have an incomplete emaillike # as the user may be typing and we have an incomplete emaillike
self.set_state(PaymentIdentifierState.NOT_FOUND) self.set_state(PaymentIdentifierState.NOT_FOUND)
else:
self.set_state(PaymentIdentifierState.NOT_FOUND)
elif self.bip70: elif self.bip70:
from . import paymentrequest from . import paymentrequest
data = await paymentrequest.get_payment_request(self.bip70) data = await paymentrequest.get_payment_request(self.bip70)
@ -627,14 +633,16 @@ class PaymentIdentifier(Logger):
validated = None validated = None
comment = None comment = None
if self.emaillike and self.openalias_data: if (self.emaillike or self.domainlike) and self.openalias_data:
key = self.emaillike if self.emaillike else self.domainlike
address = self.openalias_data.get('address') address = self.openalias_data.get('address')
name = self.openalias_data.get('name') name = self.openalias_data.get('name')
recipient = self.emaillike + ' <' + address + '>' description = name
recipient = key + ' <' + address + '>'
validated = self.openalias_data.get('validated') validated = self.openalias_data.get('validated')
if not validated: if not validated:
self.warning = _('WARNING: the alias "{}" could not be validated via an additional ' self.warning = _('WARNING: the alias "{}" could not be validated via an additional '
'security check, DNSSEC, and thus may not be correct.').format(self.emaillike) 'security check, DNSSEC, and thus may not be correct.').format(key)
elif self.bolt11 and self.wallet.has_lightning(): elif self.bolt11 and self.wallet.has_lightning():
recipient, amount, description = self._get_bolt11_fields(self.bolt11) recipient, amount, description = self._get_bolt11_fields(self.bolt11)
@ -689,8 +697,8 @@ class PaymentIdentifier(Logger):
return pubkey, amount, description return pubkey, amount, description
async def resolve_openalias(self) -> Optional[dict]: async def resolve_openalias(self) -> Optional[dict]:
key = self.emaillike key = self.emaillike if self.emaillike else self.domainlike
# TODO: below check needed? we already matched RE_EMAIL # TODO: below check needed? we already matched RE_EMAIL/RE_DOMAIN
# if not (('.' in key) and ('<' not in key) and (' ' not in key)): # if not (('.' in key) and ('<' not in key) and (' ' not in key)):
# return None # return None
parts = key.split(sep=',') # assuming single line parts = key.split(sep=',') # assuming single line

Loading…
Cancel
Save