Browse Source

Merge pull request #7301 from bitromortac/2105-lightning-uri

qt+android: add lightning URI support
patch-4
ghost43 4 years ago
committed by GitHub
parent
commit
9b774d3a5d
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      contrib/android/bitcoin_intent.xml
  2. 6
      contrib/build-wine/electrum.nsi
  3. 2
      contrib/osx/make_osx
  4. 2
      electrum.desktop
  5. 4
      electrum/gui/kivy/main_window.py
  6. 15
      electrum/gui/kivy/uix/screens.py
  7. 32
      electrum/gui/qt/main_window.py
  8. 7
      electrum/gui/qt/paytoedit.py
  9. 35
      electrum/lnaddr.py
  10. 3
      electrum/tests/test_bolt11.py
  11. 8
      run_electrum

1
contrib/android/bitcoin_intent.xml

@ -4,4 +4,5 @@
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="bitcoin" />
<data android:scheme="lightning" />
</intent-filter>

6
contrib/build-wine/electrum.nsi

@ -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)"

2
contrib/osx/make_osx

@ -179,7 +179,7 @@ pyinstaller --noconfirm --ascii --clean --name $VERSION contrib/osx/osx.spec ||
info "Adding bitcoin URI types to Info.plist"
plutil -insert 'CFBundleURLTypes' \
-xml '<array><dict> <key>CFBundleURLName</key> <string>bitcoin</string> <key>CFBundleURLSchemes</key> <array><string>bitcoin</string></array> </dict></array>' \
-xml '<array><dict> <key>CFBundleURLName</key> <string>bitcoin</string> <key>CFBundleURLSchemes</key> <array><string>bitcoin</string><string>lightning</string></array> </dict></array>' \
-- dist/$PACKAGE.app/Contents/Info.plist \
|| fail "Could not add keys to Info.plist. Make sure the program 'plutil' exists and is installed."

2
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]

4
electrum/gui/kivy/main_window.py

@ -229,10 +229,8 @@ class ElectrumWindow(App, Logger):
def on_new_intent(self, intent):
data = str(intent.getDataString())
scheme = str(intent.getScheme()).lower()
if scheme == BITCOIN_BIP21_URI_SCHEME:
if scheme == BITCOIN_BIP21_URI_SCHEME or scheme == LIGHTNING_URI_SCHEME:
self.set_URI(data)
elif scheme == LIGHTNING_URI_SCHEME:
self.set_ln_invoice(data)
def on_language(self, instance, language):
self.logger.info('language: {}'.format(language))

15
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)

32
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)

7
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:

35
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
@ -32,12 +37,14 @@ def shorten_amount(amount):
"""
# Convert to pico initially
amount = int(amount * 10**12)
units = ['p', 'n', 'u', 'm', '']
units = ['p', 'n', 'u', 'm']
for unit in units:
if amount % 1000 == 0:
amount //= 1000
else:
break
else:
unit = ''
return str(amount) + unit
def unshorten_amount(amount) -> Decimal:
@ -61,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]
@ -97,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)
@ -191,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()
@ -228,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)
@ -273,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]:
@ -342,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
@ -357,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])

3
electrum/tests/test_bolt11.py

@ -28,10 +28,11 @@ class TestBolt11(ElectrumTestCase):
Decimal(123)/10**6: '123u',
Decimal(123)/1000: '123m',
Decimal(3): '3',
Decimal(1000): '1000',
}
for i, o in tests.items():
assert shorten_amount(i) == o
self.assertEqual(shorten_amount(i), o)
assert unshorten_amount(shorten_amount(i)) == i
@staticmethod

8
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,8 +341,10 @@ def main():
# check uri
uri = config_options.get('url')
if uri:
if not uri.lower().startswith(BITCOIN_BIP21_URI_SCHEME + ':'):
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)

Loading…
Cancel
Save