qt+android: add lightning URI support
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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])
|
||||
|
||||
|
||||
Reference in New Issue
Block a user