diff --git a/contrib/build-wine/electrum.nsi b/contrib/build-wine/electrum.nsi index 1946c7749..12779238a 100644 --- a/contrib/build-wine/electrum.nsi +++ b/contrib/build-wine/electrum.nsi @@ -2,7 +2,7 @@ ;Include Modern UI !include "TextFunc.nsh" ;Needed for the $GetSize function. I know, doesn't sound logical, it isn't. !include "MUI2.nsh" - + ;-------------------------------- ;Variables @@ -29,31 +29,31 @@ ;Specifies whether or not the installer will perform a CRC on itself before allowing an install CRCCheck on - + ;Sets whether or not the details of the install are shown. Can be 'hide' (the default) to hide the details by default, allowing the user to view them, or 'show' to show them by default, or 'nevershow', to prevent the user from ever seeing them. ShowInstDetails show - + ;Sets whether or not the details of the uninstall are shown. Can be 'hide' (the default) to hide the details by default, allowing the user to view them, or 'show' to show them by default, or 'nevershow', to prevent the user from ever seeing them. ShowUninstDetails show - + ;Sets the colors to use for the install info screen (the default is 00FF00 000000. Use the form RRGGBB (in hexadecimal, as in HTML, only minus the leading '#', since # can be used for comments). Note that if "/windows" is specified as the only parameter, the default windows colors will be used. InstallColors /windows - + ;This command sets the compression algorithm used to compress files/data in the installer. (http://nsis.sourceforge.net/Reference/SetCompressor) SetCompressor /SOLID lzma - + ;Sets the dictionary size in megabytes (MB) used by the LZMA compressor (default is 8 MB). SetCompressorDictSize 64 - + ;Sets the text that is shown (by default it is 'Nullsoft Install System vX.XX') in the bottom of the install window. Setting this to an empty string ("") uses the default; to set the string to blank, use " " (a space). - BrandingText "${PRODUCT_NAME} Installer v${PRODUCT_VERSION}" - + BrandingText "${PRODUCT_NAME} Installer v${PRODUCT_VERSION}" + ;Sets what the titlebars of the installer will display. By default, it is 'Name Setup', where Name is specified with the Name command. You can, however, override it with 'MyApp Installer' or whatever. If you specify an empty string (""), the default will be used (you can however specify " " to achieve a blank string) Caption "${PRODUCT_NAME}" ;Adds the Product Version on top of the Version Tab in the Properties of the file. VIProductVersion 1.0.0.0 - + ;VIAddVersionKey - Adds a field in the Version Tab of the File Properties. This can either be a field provided by the system or a user defined field. VIAddVersionKey ProductName "${PRODUCT_NAME} Installer" VIAddVersionKey Comments "The installer for ${PRODUCT_NAME}" @@ -63,7 +63,7 @@ VIAddVersionKey FileVersion ${PRODUCT_VERSION} VIAddVersionKey ProductVersion ${PRODUCT_VERSION} VIAddVersionKey InternalName "${PRODUCT_NAME} Installer" - VIAddVersionKey LegalTrademarks "${PRODUCT_NAME} is a trademark of ${PRODUCT_PUBLISHER}" + VIAddVersionKey LegalTrademarks "${PRODUCT_NAME} is a trademark of ${PRODUCT_PUBLISHER}" VIAddVersionKey OriginalFilename "${PRODUCT_NAME}.exe" ;-------------------------------- @@ -71,9 +71,9 @@ !define MUI_ABORTWARNING !define MUI_ABORTWARNING_TEXT "Are you sure you wish to abort the installation of ${PRODUCT_NAME}?" - + !define MUI_ICON "c:\electrum\electrum\gui\icons\electrum.ico" - + ;-------------------------------- ;Pages @@ -108,7 +108,7 @@ Section RMDir /r "$INSTDIR\*.*" Delete "$DESKTOP\${PRODUCT_NAME}.lnk" Delete "$SMPROGRAMS\${PRODUCT_NAME}\*.*" - + ;Files to pack into the installer File /r "dist\electrum\*.*" File "c:\electrum\electrum\gui\icons\electrum.ico" @@ -132,11 +132,15 @@ Section CreateShortCut "$SMPROGRAMS\${PRODUCT_NAME}\${PRODUCT_NAME} Testnet.lnk" "$INSTDIR\electrum-${PRODUCT_VERSION}.exe" "--testnet" "$INSTDIR\electrum-${PRODUCT_VERSION}.exe" 0 - ;Links bitcoin: URI's to Electrum + ;Links bitcoin: and lightning: 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$\"" WriteRegStr HKCU "Software\Classes\bitcoin\shell\open\command" "" "$\"$INSTDIR\electrum-${PRODUCT_VERSION}.exe$\" $\"%1$\"" + WriteRegStr HKCU "Software\Classes\lightning" "" "URL:lightning Protocol" + 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$\"" ;Adds an uninstaller possibility to Windows Uninstall or change a program section WriteRegStr HKCU "${PRODUCT_UNINST_KEY}" "DisplayName" "$(^Name)" @@ -166,7 +170,7 @@ Section "Uninstall" Delete "$DESKTOP\${PRODUCT_NAME}.lnk" Delete "$SMPROGRAMS\${PRODUCT_NAME}\*.*" RMDir "$SMPROGRAMS\${PRODUCT_NAME}" - + DeleteRegKey HKCU "Software\Classes\bitcoin" DeleteRegKey HKCU "Software\${PRODUCT_NAME}" DeleteRegKey HKCU "${PRODUCT_UNINST_KEY}" diff --git a/electrum.desktop b/electrum.desktop index e40106716..1593591f7 100644 --- a/electrum.desktop +++ b/electrum.desktop @@ -14,7 +14,7 @@ StartupNotify=true StartupWMClass=electrum Terminal=false Type=Application -MimeType=x-scheme-handler/bitcoin; +MimeType=x-scheme-handler/bitcoin;x-scheme-handler/lightning; Actions=Testnet; [Desktop Action Testnet] diff --git a/electrum/gui/kivy/uix/screens.py b/electrum/gui/kivy/uix/screens.py index 091dcd0c2..0ed6f5eea 100644 --- a/electrum/gui/kivy/uix/screens.py +++ b/electrum/gui/kivy/uix/screens.py @@ -17,7 +17,7 @@ from electrum import bitcoin, constants from electrum.transaction import tx_from_any, PartialTxOutput from electrum.util import (parse_URI, InvalidBitcoinURI, TxMinedInfo, maybe_extract_bolt11_invoice, InvoiceError, format_time) -from electrum.lnaddr import lndecode +from electrum.lnaddr import lndecode, LnInvoiceException from electrum.logging import Logger from .dialogs.confirm_tx_dialog import ConfirmTxDialog @@ -170,6 +170,15 @@ class SendScreen(CScreen, Logger): def set_URI(self, text: str): if not self.app.wallet: return + # interpret as lighting URI + bolt11_invoice = maybe_extract_bolt11_invoice(text) + if bolt11_invoice: + self.set_ln_invoice(bolt11_invoice) + # interpret as BIP21 URI + else: + self.set_bip21(text) + + def set_bip21(self, text: str): try: uri = parse_URI(text, self.app.on_pr, loop=self.app.asyncio_loop) except InvalidBitcoinURI as e: @@ -188,8 +197,8 @@ class SendScreen(CScreen, Logger): try: invoice = str(invoice).lower() lnaddr = lndecode(invoice) - except Exception as e: - self.app.show_info(invoice + _(" is not a valid Lightning invoice: ") + repr(e)) # repr because str(Exception()) == '' + except LnInvoiceException as e: + self.app.show_info(_("Invoice is not a valid Lightning invoice: ") + repr(e)) # repr because str(Exception()) == '' return self.address = invoice self.message = dict(lnaddr.tags).get('d', None) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 0868fecb9..bc2902995 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -79,7 +79,7 @@ from electrum.exchange_rate import FxThread from electrum.simple_config import SimpleConfig from electrum.logging import Logger from electrum.lnutil import ln_dummy_address, extract_nodeid, ConnStringFormatError -from electrum.lnaddr import lndecode, LnDecodeException +from electrum.lnaddr import lndecode, LnInvoiceException from .exception_window import Exception_Hook from .amountedit import AmountEdit, BTCAmountEdit, FreezableLineEdit, FeerateEdit, SizedFreezableLineEdit @@ -1962,12 +1962,15 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): else: self.payment_request_error_signal.emit() - def parse_lightning_invoice(self, invoice): + def set_ln_invoice(self, invoice: str): """Parse ln invoice, and prepare the send tab for it.""" try: lnaddr = lndecode(invoice) - except Exception as e: - raise LnDecodeException(e) from e + except LnInvoiceException as e: + self.show_error(_("Error parsing Lightning invoice") + f":\n{e}") + return + + self.payto_e.lightning_invoice = invoice pubkey = bh2u(lnaddr.pubkey.serialize()) for k,v in lnaddr.tags: if k == 'd': @@ -1980,22 +1983,18 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): self.message_e.setText(description) if lnaddr.get_amount_sat() is not None: self.amount_e.setAmount(lnaddr.get_amount_sat()) - #self.amount_e.textEdited.emit("") self.set_onchain(False) def set_onchain(self, b): self._is_onchain = b self.max_button.setEnabled(b) - def pay_to_URI(self, URI): - if not URI: - return + def set_bip21(self, text: str): try: - out = util.parse_URI(URI, self.on_pr) + out = util.parse_URI(text, self.on_pr) except InvalidBitcoinURI as e: self.show_error(_("Error parsing URI") + f":\n{e}") return - self.show_send_tab() self.payto_URI = out r = out.get('r') sig = out.get('sig') @@ -2016,8 +2015,19 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): self.message_e.setText(message) if amount: self.amount_e.setAmount(amount) - self.amount_e.textEdited.emit("") + def pay_to_URI(self, text: str): + if not text: + return + # first interpret as lightning invoice + bolt11_invoice = maybe_extract_bolt11_invoice(text) + if bolt11_invoice: + self.set_ln_invoice(bolt11_invoice) + else: + self.set_bip21(text) + # update fiat amount + self.amount_e.textEdited.emit("") + self.show_send_tab() def do_clear(self): self.max_button.setChecked(False) diff --git a/electrum/gui/qt/paytoedit.py b/electrum/gui/qt/paytoedit.py index b3b60bdf6..183534dd4 100644 --- a/electrum/gui/qt/paytoedit.py +++ b/electrum/gui/qt/paytoedit.py @@ -166,12 +166,7 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit, Logger): # try LN invoice bolt11_invoice = maybe_extract_bolt11_invoice(data) if bolt11_invoice is not None: - try: - self.win.parse_lightning_invoice(bolt11_invoice) - except LnDecodeException as e: - self.errors.append(PayToLineError(line_content=data, exc=e)) - else: - self.lightning_invoice = bolt11_invoice + self.win.set_ln_invoice(bolt11_invoice) return # try "address, amount" on-chain format try: diff --git a/electrum/lnaddr.py b/electrum/lnaddr.py index 2a8cf7433..4c297a3e5 100644 --- a/electrum/lnaddr.py +++ b/electrum/lnaddr.py @@ -23,6 +23,11 @@ if TYPE_CHECKING: from .lnutil import LnFeatures +class LnInvoiceException(Exception): pass +class LnDecodeException(LnInvoiceException): pass +class LnEncodeException(LnInvoiceException): pass + + # BOLT #11: # # A writer MUST encode `amount` as a positive decimal integer with no @@ -63,7 +68,7 @@ def unshorten_amount(amount) -> Decimal: # A reader SHOULD fail if `amount` contains a non-digit, or is followed by # anything except a `multiplier` in the table above. if not re.fullmatch("\\d+[pnum]?", str(amount)): - raise ValueError("Invalid amount '{}'".format(amount)) + raise LnDecodeException("Invalid amount '{}'".format(amount)) if unit in units.keys(): return Decimal(amount[:-1]) / units[unit] @@ -99,7 +104,7 @@ def encode_fallback(fallback: str, net: Type[AbstractNet]): elif addrtype == net.ADDRTYPE_P2SH: wver = 18 else: - raise ValueError(f"Unknown address type {addrtype} for {net}") + raise LnEncodeException(f"Unknown address type {addrtype} for {net}") wprog = addr return tagged('f', bitstring.pack("uint:5", wver) + wprog) @@ -193,7 +198,7 @@ def lnencode(addr: 'LnAddr', privkey) -> str: # A writer MUST NOT include more than one `d`, `h`, `n` or `x` fields, if k in ('d', 'h', 'n', 'x', 'p', 's'): if k in tags_set: - raise ValueError("Duplicate '{}' tag".format(k)) + raise LnEncodeException("Duplicate '{}' tag".format(k)) if k == 'r': route = bitstring.BitArray() @@ -230,7 +235,7 @@ def lnencode(addr: 'LnAddr', privkey) -> str: data += tagged('9', feature_bits) else: # FIXME: Support unknown tags? - raise ValueError("Unknown tag {}".format(k)) + raise LnEncodeException("Unknown tag {}".format(k)) tags_set.add(k) @@ -275,16 +280,16 @@ class LnAddr(object): @amount.setter def amount(self, value): if not (isinstance(value, Decimal) or value is None): - raise ValueError(f"amount must be Decimal or None, not {value!r}") + raise LnInvoiceException(f"amount must be Decimal or None, not {value!r}") if value is None: self._amount = None return assert isinstance(value, Decimal) if value.is_nan() or not (0 <= value <= TOTAL_COIN_SUPPLY_LIMIT_IN_BTC): - raise ValueError(f"amount is out-of-bounds: {value!r} BTC") + raise LnInvoiceException(f"amount is out-of-bounds: {value!r} BTC") if value * 10**12 % 10: # max resolution is millisatoshi - raise ValueError(f"Cannot encode {value!r}: too many decimal places") + raise LnInvoiceException(f"Cannot encode {value!r}: too many decimal places") self._amount = value def get_amount_sat(self) -> Optional[Decimal]: @@ -344,8 +349,6 @@ class LnAddr(object): return now > self.get_expiry() + self.date -class LnDecodeException(Exception): pass - class SerializableKey: def __init__(self, pubkey): self.pubkey = pubkey @@ -359,24 +362,24 @@ def lndecode(invoice: str, *, verbose=False, net=None) -> LnAddr: hrp = decoded_bech32.hrp data = decoded_bech32.data if decoded_bech32.encoding is None: - raise ValueError("Bad bech32 checksum") + raise LnDecodeException("Bad bech32 checksum") if decoded_bech32.encoding != segwit_addr.Encoding.BECH32: - raise ValueError("Bad bech32 encoding: must be using vanilla BECH32") + raise LnDecodeException("Bad bech32 encoding: must be using vanilla BECH32") # BOLT #11: # # A reader MUST fail if it does not understand the `prefix`. if not hrp.startswith('ln'): - raise ValueError("Does not start with ln") + raise LnDecodeException("Does not start with ln") if not hrp[2:].startswith(net.BOLT11_HRP): - raise ValueError(f"Wrong Lightning invoice HRP {hrp[2:]}, should be {net.BOLT11_HRP}") + raise LnDecodeException(f"Wrong Lightning invoice HRP {hrp[2:]}, should be {net.BOLT11_HRP}") data = u5_to_bitarray(data) # Final signature 65 bytes, split it off. if len(data) < 65*8: - raise ValueError("Too short to contain signature") + raise LnDecodeException("Too short to contain signature") sigdecoded = data[-65*8:].tobytes() data = bitstring.ConstBitStream(data[:-65*8]) diff --git a/run_electrum b/run_electrum index 0b593704c..1d51cec82 100755 --- a/run_electrum +++ b/run_electrum @@ -88,7 +88,7 @@ from electrum.wallet_db import WalletDB from electrum.wallet import Wallet from electrum.storage import WalletStorage from electrum.util import print_msg, print_stderr, json_encode, json_decode, UserCancelled -from electrum.util import InvalidPassword, BITCOIN_BIP21_URI_SCHEME +from electrum.util import InvalidPassword, BITCOIN_BIP21_URI_SCHEME, LIGHTNING_URI_SCHEME from electrum.commands import get_parser, known_commands, Commands, config_variables from electrum import daemon from electrum import keystore @@ -341,10 +341,12 @@ def main(): # check uri uri = config_options.get('url') - if uri: - if not uri.lower().startswith(BITCOIN_BIP21_URI_SCHEME + ':'): - print_stderr('unknown command:', uri) - sys.exit(1) + if uri and not ( + uri.lower().startswith(BITCOIN_BIP21_URI_SCHEME + ':') or + uri.lower().startswith(LIGHTNING_URI_SCHEME + ':') + ): + print_stderr('unknown command:', uri) + sys.exit(1) config = SimpleConfig(config_options)