diff --git a/contrib/android/bitcoin_intent.xml b/contrib/android/bitcoin_intent.xml index 87553b2c826f..3592e45eb764 100644 --- a/contrib/android/bitcoin_intent.xml +++ b/contrib/android/bitcoin_intent.xml @@ -5,4 +5,6 @@ + + diff --git a/contrib/build-wine/electrum.nsi b/contrib/build-wine/electrum.nsi index 6aa4ce22be72..23111d5e4306 100644 --- a/contrib/build-wine/electrum.nsi +++ b/contrib/build-wine/electrum.nsi @@ -189,7 +189,7 @@ Section CreateShortCut "$SMPROGRAMS\${PRODUCT_NAME}\${PRODUCT_NAME} Testnet.lnk" "$INSTDIR\electrum-${PRODUCT_VERSION}.exe" "--testnet" "$INSTDIR\electrum-${PRODUCT_VERSION}.exe" 0 - ;Links bitcoin: and lightning: URIs to Electrum + ;Links bitcoin:, lightning: and lnurl LUD-17 URIs to Electrum WriteRegStr HKCU "Software\Classes\bitcoin" "" "URL:bitcoin Protocol" WriteRegStr HKCU "Software\Classes\bitcoin" "URL Protocol" "" WriteRegStr HKCU "Software\Classes\bitcoin" "DefaultIcon" "$\"$INSTDIR\electrum.ico, 0$\"" @@ -198,6 +198,14 @@ Section WriteRegStr HKCU "Software\Classes\lightning" "URL Protocol" "" WriteRegStr HKCU "Software\Classes\lightning" "DefaultIcon" "$\"$INSTDIR\electrum.ico, 0$\"" WriteRegStr HKCU "Software\Classes\lightning\shell\open\command" "" "$\"$INSTDIR\electrum-${PRODUCT_VERSION}.exe$\" $\"%1$\"" + WriteRegStr HKCU "Software\Classes\lnurlp" "" "URL:lnurlp Protocol" + WriteRegStr HKCU "Software\Classes\lnurlp" "URL Protocol" "" + WriteRegStr HKCU "Software\Classes\lnurlp" "DefaultIcon" "$\"$INSTDIR\electrum.ico, 0$\"" + WriteRegStr HKCU "Software\Classes\lnurlp\shell\open\command" "" "$\"$INSTDIR\electrum-${PRODUCT_VERSION}.exe$\" $\"%1$\"" + WriteRegStr HKCU "Software\Classes\lnurlw" "" "URL:lnurlw Protocol" + WriteRegStr HKCU "Software\Classes\lnurlw" "URL Protocol" "" + WriteRegStr HKCU "Software\Classes\lnurlw" "DefaultIcon" "$\"$INSTDIR\electrum.ico, 0$\"" + WriteRegStr HKCU "Software\Classes\lnurlw\shell\open\command" "" "$\"$INSTDIR\electrum-${PRODUCT_VERSION}.exe$\" $\"%1$\"" ;Adds an uninstaller possibility to Windows Uninstall or change a program section WriteRegStr HKCU "${PRODUCT_UNINST_KEY}" "DisplayName" "$(^Name)" @@ -229,6 +237,9 @@ Section "Uninstall" RMDir "$SMPROGRAMS\${PRODUCT_NAME}" DeleteRegKey HKCU "Software\Classes\bitcoin" + DeleteRegKey HKCU "Software\Classes\lightning" + DeleteRegKey HKCU "Software\Classes\lnurlp" + DeleteRegKey HKCU "Software\Classes\lnurlw" DeleteRegKey HKCU "Software\${PRODUCT_NAME}" DeleteRegKey HKCU "${PRODUCT_UNINST_KEY}" SectionEnd diff --git a/contrib/osx/pyinstaller.spec b/contrib/osx/pyinstaller.spec index 5cd612eda437..6adb5513c9fd 100644 --- a/contrib/osx/pyinstaller.spec +++ b/contrib/osx/pyinstaller.spec @@ -135,7 +135,7 @@ app = BUNDLE( 'CFBundleURLTypes': [{ 'CFBundleURLName': 'bitcoin', - 'CFBundleURLSchemes': ['bitcoin', 'lightning', ], + 'CFBundleURLSchemes': ['bitcoin', 'lightning', 'lnurlp', 'lnurlw', ], }], 'LSMinimumSystemVersion': '11', 'NSCameraUsageDescription': 'Electrum would like to access the camera to scan for QR codes', diff --git a/electrum.desktop b/electrum.desktop index 7434829a560f..8d8d9edbd3ce 100644 --- a/electrum.desktop +++ b/electrum.desktop @@ -15,7 +15,7 @@ StartupNotify=true StartupWMClass=electrum Terminal=false Type=Application -MimeType=x-scheme-handler/bitcoin;x-scheme-handler/lightning; +MimeType=x-scheme-handler/bitcoin;x-scheme-handler/lightning;x-scheme-handler/lnurlp;x-scheme-handler/lnurlw; Actions=Testnet; Keywords=crypto;currency;BTC diff --git a/electrum/gui/qml/qeapp.py b/electrum/gui/qml/qeapp.py index e3971561a087..8bf7cc70db3c 100644 --- a/electrum/gui/qml/qeapp.py +++ b/electrum/gui/qml/qeapp.py @@ -23,6 +23,7 @@ from electrum.plugin import run_hook from electrum.gui.common_qt.util import get_font_id from electrum.util import profiler +from electrum.lnurl import SUPPORTED_LNURL_SCHEMES from .qeconfig import QEConfig from .qedaemon import QEDaemon @@ -235,7 +236,9 @@ def on_new_intent(self, intent): data = str(intent.getDataString()) self.logger.debug(f'received intent: {repr(data)}') scheme = str(intent.getScheme()).lower() - if scheme == BITCOIN_BIP21_URI_SCHEME or scheme == LIGHTNING_URI_SCHEME: + if scheme == BITCOIN_BIP21_URI_SCHEME \ + or scheme == LIGHTNING_URI_SCHEME \ + or scheme in SUPPORTED_LNURL_SCHEMES: self.uriReceived.emit(data) def startup_finished(self): diff --git a/electrum/lnurl.py b/electrum/lnurl.py index a95bf8bcc633..e08f254d6047 100644 --- a/electrum/lnurl.py +++ b/electrum/lnurl.py @@ -21,6 +21,9 @@ _logger = get_logger(__name__) +SUPPORTED_LNURL_SCHEMES = ('lnurlp', 'lnurlw') + + class LNURLError(Exception): pass class UntrustedLNURLError(LNURLError): diff --git a/electrum/payment_identifier.py b/electrum/payment_identifier.py index 5883aadfb669..198bb68a19d7 100644 --- a/electrum/payment_identifier.py +++ b/electrum/payment_identifier.py @@ -16,7 +16,7 @@ from .transaction import PartialTxOutput from .lnurl import (decode_lnurl, request_lnurl, callback_lnurl, LNURLError, lightning_address_to_url, try_resolve_lnurlpay, LNURL6Data, - LNURL3Data, LNURLData) + LNURL3Data, LNURLData, SUPPORTED_LNURL_SCHEMES) from .bitcoin import opcodes, construct_script from .lnaddr import LnInvoiceException from .lnutil import IncompatibleOrInsaneFeatures @@ -45,9 +45,25 @@ def remove_uri_prefix(data: str, *, prefix: str) -> str: return data +def maybe_extract_url_from_lud_17_uri(data: str) -> Optional[str]: + """https://github.com/lnurl/luds/blob/luds/17.md""" + data = data.strip() + try: + parsed = urllib.parse.urlsplit(data) + except ValueError: + return None + if parsed.scheme not in SUPPORTED_LNURL_SCHEMES: + return None + if not (host := parsed.hostname) or not parsed.path: + return None + is_onion = host.endswith('.onion') + url_scheme = 'http' if is_onion else 'https' + return urllib.parse.urlunsplit(parsed._replace(scheme=url_scheme)) + + 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_DOMAIN = r'\b([A-Za-z0-9-]+\.)+[A-Z|a-z]{2,7}\b' +RE_EMAIL = r'\b[A-Za-z0-9._%+-]+@([A-Za-z0-9-]+\.)+[A-Za-z]{2,7}\b' +RE_DOMAIN = r'\b([A-Za-z0-9-]+\.)+[A-Za-z]{2,7}\b' RE_SCRIPT_FN = r'script\((.*)\)' @@ -98,6 +114,7 @@ class PaymentIdentifier(Logger): * openalias * bip21 URI * lightning-URI (containing bolt11 or lnurl) + * lnurl-URI (lud17 lnurlw/lnurlp URI) * bolt11 invoice * lnurl * lightning address @@ -228,6 +245,10 @@ def parse(self, text: str): self.logger.debug(f'Exception cause {e.args!r}') return self.set_state(PaymentIdentifierState.AVAILABLE) + elif lnurl_url := maybe_extract_url_from_lud_17_uri(text): + self._type = PaymentIdentifierType.LNURL + self.lnurl = lnurl_url + self.set_state(PaymentIdentifierState.NEED_RESOLVE) elif text.lower().startswith(BITCOIN_BIP21_URI_SCHEME + ':'): try: out = parse_bip21_URI(text) diff --git a/tests/test_payment_identifier.py b/tests/test_payment_identifier.py index 046f5ba11b73..f19fb6baaeaf 100644 --- a/tests/test_payment_identifier.py +++ b/tests/test_payment_identifier.py @@ -184,6 +184,17 @@ def test_lnurl_basic(self): self.assertEqual(PaymentIdentifierType.LNURL, pi.type) self.assertTrue(pi.need_resolve()) + # test with lud17 prefix + unsupported_lud_17_lnurl_c = f"lnurlc://service.io/?q=3fc3645b439ce8e7" + pi = PaymentIdentifier(None, unsupported_lud_17_lnurl_c) + self.assertFalse(pi.is_valid()) + + valid_lud_17_lnurl_w = f"lnurlw://service.io/?q=3fc3645b439ce8e7" + pi = PaymentIdentifier(None, valid_lud_17_lnurl_w) + self.assertTrue(pi.is_valid()) + self.assertEqual(PaymentIdentifierType.LNURL, pi.type) + self.assertTrue(pi.need_resolve()) + @patch('electrum.payment_identifier.request_lnurl') def test_lnurl_pay_resolve(self, mock_request_lnurl): """Test LNURL-pay (LNURL6) with mocked resolve"""