What changed, and why it matters
This commit is a large but purely cosmetic rename in the Electrum Bitcoin wallet. It renames the internal module `lnaddr.py` to `bolt11.py` and renames classes/functions from names like `LnAddr` and `lndecode` to `BOLT11Addr` and `decode_bolt11_invoice`. The goal is to avoid confusion with the newer BOLT 12 Lightning invoice format. The actual code logic is unchanged, apart from removing the obsolete `object` base class from one class. There is no security fix or vulnerability introduced here.
No security action needed. Treat as routine maintenance. Reviewers may optionally verify that the rename did not accidentally alter any logic by spot-checking that the new `bolt11.py` matches the deleted `lnaddr.py` except for identifiers and the `object` base class removal.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The change is a pure refactor: electrum/lnaddr.py is deleted and its 593-line contents are copied into the new electrum/bolt11.py with identifiers renamed (LnAddr → BOLT11Addr, lndecode → decode_bolt11_invoice, lnencode → encode_bolt11_invoice, exception classes prefixed with BOLT11). All call sites across 15 files are updated to import from electrum.bolt11 and use the new names. The only non-rename change is dropping class LnAddr(object): in favor of class BOLT11Addr:, which is a Python 2 cleanup with no behavioral effect. No algorithmic, cryptographic, or parsing changes are present.
Changed components
electrum/bolt11.py (new module, renamed from lnaddr.py)electrum/lnaddr.py (deleted)electrum/bip21.pyelectrum/gui/qt/channel_details.pyelectrum/gui/qt/main_window.pyelectrum/gui/text.pyelectrum/invoices.pyelectrum/lnurl.pyelectrum/lnworker.pyelectrum/payment_identifier.pyelectrum/plugins/watchtower/server.pyelectrum/submarine_swaps.pyelectrum/wallet_db.pytests/test_bolt11.pytests/test_commands.pytests/test_lnpeer.pyInspect captured patch +681 / −681
diff --git a/electrum/bip21.py b/electrum/bip21.py
index c7f3ecf..3db495e 100644
--- a/electrum/bip21.py
+++ b/electrum/bip21.py
@@ -7,7 +7,7 @@ from typing import Optional
from . import bitcoin
from .util import format_satoshis_plain
from .bitcoin import COIN, TOTAL_COIN_SUPPLY_LIMIT_IN_BTC
-from .lnaddr import lndecode, LnDecodeException
+from .bolt11 import decode_bolt11_invoice, BOLT11DecodeException
# note: when checking against these, use .lower() to support case-insensitivity
BITCOIN_BIP21_URI_SCHEME = 'bitcoin'
@@ -93,8 +93,8 @@ def parse_bip21_URI(uri: str) -> dict:
raise InvalidBitcoinURI(f"failed to parse 'sig' field: {repr(e)}") from e
if 'lightning' in out:
try:
- lnaddr = lndecode(out['lightning'])
- except LnDecodeException as e:
+ lnaddr = decode_bolt11_invoice(out['lightning'])
+ except BOLT11DecodeException as e:
raise InvalidBitcoinURI(f"Failed to decode 'lightning' field: {e!r}") from e
amount_sat = out.get('amount')
if amount_sat:
diff --git a/electrum/bolt11.py b/electrum/bolt11.py
new file mode 100644
index 0000000..0d910a9
--- /dev/null
+++ b/electrum/bolt11.py
@@ -0,0 +1,593 @@
+#! /usr/bin/env python3
+# This was forked from https://github.com/rustyrussell/lightning-payencode/tree/acc16ec13a3fa1dc16c07af6ec67c261bd8aff23
+
+import io
+import re
+import time
+from hashlib import sha256
+from binascii import hexlify
+from decimal import Decimal
+from typing import Optional, TYPE_CHECKING, Type, Dict, Any, Sequence, Tuple
+import random
+
+import electrum_ecc as ecc
+
+from .bitcoin import hash160_to_b58_address, b58_address_to_hash160, TOTAL_COIN_SUPPLY_LIMIT_IN_BTC
+from .segwit_addr import bech32_encode, bech32_decode, CHARSET, CHARSET_INVERSE, convertbits
+from . import segwit_addr
+from . import constants
+from .constants import AbstractNet
+from .bitcoin import COIN
+
+if TYPE_CHECKING:
+ from .lnutil import LnFeatures
+
+
+class BOLT11InvoiceException(Exception): pass
+class BOLT11DecodeException(BOLT11InvoiceException): pass
+class BOLT11EncodeException(BOLT11InvoiceException): pass
+
+
+# BOLT #11:
+#
+# A writer MUST encode `amount` as a positive decimal integer with no
+# leading zeroes, SHOULD use the shortest representation possible.
+def shorten_amount(amount):
+ """ Given an amount in bitcoin, shorten it
+ """
+ # Convert to pico initially
+ amount = int(amount * 10**12)
+ 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:
+ """ Given a shortened amount, convert it into a decimal
+ """
+ # BOLT #11:
+ # The following `multiplier` letters are defined:
+ #
+ #* `m` (milli): multiply by 0.001
+ #* `u` (micro): multiply by 0.000001
+ #* `n` (nano): multiply by 0.000000001
+ #* `p` (pico): multiply by 0.000000000001
+ units = {
+ 'p': 10**12,
+ 'n': 10**9,
+ 'u': 10**6,
+ 'm': 10**3,
+ }
+ unit = str(amount)[-1]
+ # BOLT #11:
+ # 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 BOLT11DecodeException("Invalid amount '{}'".format(amount))
+
+ if unit in units.keys():
+ return Decimal(amount[:-1]) / units[unit]
+ else:
+ return Decimal(amount)
+
+
+def encode_fallback_addr(fallback: str, net: Type[AbstractNet]) -> Sequence[int]:
+ """Encode all supported fallback addresses."""
+ wver, wprog_ints = segwit_addr.decode_segwit_address(net.SEGWIT_HRP, fallback)
+ if wver is not None:
+ wprog = bytes(wprog_ints)
+ else:
+ addrtype, addr = b58_address_to_hash160(fallback)
+ if addrtype == net.ADDRTYPE_P2PKH:
+ wver = 17
+ elif addrtype == net.ADDRTYPE_P2SH:
+ wver = 18
+ else:
+ raise BOLT11EncodeException(f"Unknown address type {addrtype} for {net}")
+ wprog = addr
+ data5 = convertbits(wprog, 8, 5)
+ assert data5 is not None
+ return tagged5('f', [wver] + list(data5))
+
+
+def parse_fallback_addr(data5: Sequence[int], net: Type[AbstractNet]) -> Optional[str]:
+ wver = data5[0]
+ data8 = bytes(convertbits(data5[1:], 5, 8, False))
+ if wver == 17:
+ addr = hash160_to_b58_address(data8, net.ADDRTYPE_P2PKH)
+ elif wver == 18:
+ addr = hash160_to_b58_address(data8, net.ADDRTYPE_P2SH)
+ elif wver <= 16:
+ addr = segwit_addr.encode_segwit_address(net.SEGWIT_HRP, wver, data8)
+ else:
+ return None
+ return addr
+
+
+def tagged5(char: str, data5: Sequence[int]) -> Sequence[int]:
+ assert len(data5) < (1 << 10)
+ return [CHARSET_INVERSE[char], len(data5) >> 5, len(data5) & 31] + data5
+
+
+def tagged8(char: str, data8: Sequence[int]) -> Sequence[int]:
+ return tagged5(char, convertbits(data8, 8, 5))
+
+
+def int_to_data5(val: int, *, bit_len: int = None) -> Sequence[int]:
+ """Represent big-endian number with as many 0-31 values as it takes.
+ If `bit_len` is set, use exactly bit_len//5 values (left-padded with zeroes).
+ """
+ if bit_len is not None:
+ assert bit_len % 5 == 0, bit_len
+ if val.bit_length() > bit_len:
+ raise ValueError(f"{val=} too big for {bit_len=!r}")
+ ret = []
+ while val != 0:
+ ret.append(val % 32)
+ val //= 32
+ if bit_len is not None:
+ ret.extend([0] * (len(ret) - bit_len // 5))
+ ret.reverse()
+ return ret
+
+
+def int_from_data5(data5: Sequence[int]) -> int:
+ total = 0
+ for v in data5:
+ total = 32 * total + v
+ return total
+
+
+def pull_tagged(data5: bytearray) -> Tuple[str, Sequence[int]]:
+ """Try to pull out tagged data: returns tag, tagged data. Mutates data in-place."""
+ if len(data5) < 3:
+ raise ValueError("Truncated field")
+ length = data5[1] * 32 + data5[2]
+ if length > len(data5) - 3:
+ raise ValueError(
+ "Truncated {} field: expected {} values".format(CHARSET[data5[0]], length))
+ ret = (CHARSET[data5[0]], data5[3:3+length])
+ del data5[:3 + length] # much faster than: data5=data5[offset:]
+ return ret
+
+
+def encode_bolt11_invoice(addr: 'BOLT11Addr', privkey) -> str:
+ if addr.amount:
+ amount = addr.net.BOLT11_HRP + shorten_amount(addr.amount)
+ else:
+ amount = addr.net.BOLT11_HRP if addr.net else ''
+
+ hrp = 'ln' + amount
+
+ # Start with the timestamp
+ data5 = int_to_data5(addr.date, bit_len=35)
+
+ tags_set = set()
+
+ # Payment hash
+ assert addr.paymenthash is not None
+ data5 += tagged8('p', addr.paymenthash)
+ tags_set.add('p')
+
+ if addr.payment_secret is not None:
+ data5 += tagged8('s', addr.payment_secret)
+ tags_set.add('s')
+
+ for k, v in addr.tags:
+
+ # BOLT #11:
+ #
+ # A writer MUST NOT include more than one `d`, `h`, `n` or `x` fields,
+ if k in ('d', 'h', 'n', 'x', 'p', 's', '9'):
+ if k in tags_set:
+ raise BOLT11EncodeException("Duplicate '{}' tag".format(k))
+
+ if k == 'r':
+ route = bytearray()
+ for step in v:
+ pubkey, scid, feebase, feerate, cltv = step
+ route += pubkey
+ route += scid
+ route += int.to_bytes(feebase, length=4, byteorder="big", signed=False)
+ route += int.to_bytes(feerate, length=4, byteorder="big", signed=False)
+ route += int.to_bytes(cltv, length=2, byteorder="big", signed=False)
+ data5 += tagged8('r', route)
+ elif k == 't':
+ pubkey, feebase, feerate, cltv = v
+ route = bytearray()
+ route += pubkey
+ route += int.to_bytes(feebase, length=4, byteorder="big", signed=False)
+ route += int.to_bytes(feerate, length=4, byteorder="big", signed=False)
+ route += int.to_bytes(cltv, length=2, byteorder="big", signed=False)
+ data5 += tagged8('t', route)
+ elif k == 'f':
+ if v is not None:
+ data5 += encode_fallback_addr(v, addr.net)
+ elif k == 'd':
+ # truncate to max length: 1024*5 bits = 639 bytes
+ data5 += tagged8('d', v.encode()[0:639])
+ elif k == 'x':
+ expirybits = int_to_data5(v)
+ data5 += tagged5('x', expirybits)
+ elif k == 'h':
+ data5 += tagged8('h', sha256(v.encode('utf-8')).digest())
+ elif k == 'n':
+ data5 += tagged8('n', v)
+ elif k == 'c':
+ finalcltvbits = int_to_data5(v)
+ data5 += tagged5('c', finalcltvbits)
+ elif k == '9':
+ if v == 0:
+ continue
+ feature_bits = int_to_data5(v)
+ data5 += tagged5('9', feature_bits)
+ else:
+ # FIXME: Support unknown tags?
+ raise BOLT11EncodeException("Unknown tag {}".format(k))
+
+ tags_set.add(k)
+
+ # BOLT #11:
+ #
+ # A writer MUST include either a `d` or `h` field, and MUST NOT include
+ # both.
+ if 'd' in tags_set and 'h' in tags_set:
+ raise ValueError("Cannot include both 'd' and 'h'")
+ if 'd' not in tags_set and 'h' not in tags_set:
+ raise ValueError("Must include either 'd' or 'h'")
+
+ # We actually sign the hrp, then data (padded to 8 bits with zeroes).
+ msg = hrp.encode("ascii") + bytes(convertbits(data5, 5, 8))
+ msg32 = sha256(msg).digest()
+ privkey = ecc.ECPrivkey(privkey)
+ sig = privkey.ecdsa_sign_recoverable(msg32, is_compressed=False)
+ recovery_flag = bytes([sig[0] - 27])
+ sig = bytes(sig[1:]) + recovery_flag
+ sig = bytes(convertbits(sig, 8, 5, False))
+ data5 += sig
+
+ return bech32_encode(segwit_addr.Encoding.BECH32, hrp, data5)
+
+
+class BOLT11Addr:
+ def __init__(self, *, paymenthash: bytes = None, amount=None, net: Type[AbstractNet] = None, tags=None, date=None,
+ payment_secret: bytes = None):
+ self.date = int(time.time()) if not date else int(date)
+ self.tags = [] if not tags else tags
+ self.unknown_tags = []
+ self.paymenthash = paymenthash
+ self.payment_secret = payment_secret
+ self.signature = None
+ self.pubkey = None
+ self.net = constants.net if net is None else net # type: Type[AbstractNet]
+ self._amount = amount # type: Optional[Decimal] # in bitcoins
+
+ @property
+ def amount(self) -> Optional[Decimal]:
+ return self._amount
+
+ @amount.setter
+ def amount(self, value):
+ if not (isinstance(value, Decimal) or value is None):
+ raise BOLT11InvoiceException(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 BOLT11InvoiceException(f"amount is out-of-bounds: {value!r} BTC")
+ if value * 10**12 % 10:
+ # max resolution is millisatoshi
+ raise BOLT11InvoiceException(f"Cannot encode {value!r}: too many decimal places")
+ self._amount = value
+
+ def get_amount_sat(self) -> Optional[Decimal]:
+ # note that this has msat resolution potentially
+ if self.amount is None:
+ return None
+ return self.amount * COIN
+
+ def get_routing_info(self, tag):
+ # note: tag will be 't' for trampoline
+ r_tags = list(filter(lambda x: x[0] == tag, self.tags))
+ # strip the tag type, it's implicitly 'r' now
+ r_tags = list(map(lambda x: x[1], r_tags))
+ # if there are multiple hints, we will use the first one that works,
+ # from a random permutation
+ random.shuffle(r_tags)
+ return r_tags
+
+ @staticmethod
+ def format_bolt11_routing_info_as_human_readable(r_tags, *, has_explicit_r_tagtype: bool = False):
+ """Converts the node-id bytes->hex, and the SCID bytes->"AAAxBBBxCC", e.g. for logging."""
+ from .util import format_short_id
+ r_tags2 = []
+ for r_tag in r_tags:
+ if has_explicit_r_tagtype:
+ (tagtype, path) = r_tag
+ assert tagtype == "r", f"found unexpected {tagtype=}"
+ else:
+ path = r_tag
+ path2 = [
+ (edge[0].hex(), format_short_id(edge[1]), edge[2], edge[3], edge[4])
+ for edge in path]
+ r_tag2 = (tagtype, path2) if has_explicit_r_tagtype else path2
+ r_tags2.append(r_tag2)
+ return r_tags2
+
+ def get_amount_msat(self) -> Optional[int]:
+ if self.amount is None:
+ return None
+ return int(self.amount * COIN * 1000)
+
+ def get_features(self) -> 'LnFeatures':
+ from .lnutil import LnFeatures
+ return LnFeatures(self.get_tag('9') or 0)
+
+ def validate_and_compare_features(self, myfeatures: 'LnFeatures') -> None:
+ """Raises IncompatibleOrInsaneFeatures.
+
+ note: these checks are not done by the parser (in lndecode), as then when we started requiring a new feature,
+ old saved already paid invoices could no longer be parsed.
+ """
+ from .lnutil import validate_features, ln_compare_features
+ invoice_features = self.get_features()
+ validate_features(invoice_features)
+ ln_compare_features(myfeatures.for_invoice(), invoice_features)
+
+ def __str__(self):
+ return "BOLT11Addr[{}, amount={}{} tags=[{}]]".format(
+ hexlify(self.pubkey.serialize()).decode('utf-8') if self.pubkey else None,
+ self.amount, self.net.BOLT11_HRP,
+ ", ".join([k + '=' + str(v) for k, v in self.tags])
+ )
+
+ def get_min_final_cltv_delta(self) -> int:
+ cltv = self.get_tag('c')
+ if cltv is None:
+ return 18
+ return int(cltv)
+
+ def get_tag(self, tag):
+ for k, v in self.tags:
+ if k == tag:
+ return v
+ return None
+
+ def get_description(self) -> str:
+ return self.get_tag('d') or ''
+
+ def get_fallback_address(self) -> str:
+ return self.get_tag('f') or ''
+
+ def get_expiry(self) -> int:
+ exp = self.get_tag('x')
+ if exp is None:
+ exp = 3600
+ return int(exp)
+
+ def is_expired(self) -> bool:
+ now = time.time()
+ # BOLT-11 does not specify what expiration of '0' means.
+ # we treat it as 0 seconds here (instead of never)
+ return now > self.get_expiry() + self.date
+
+ def to_debug_json(self) -> Dict[str, Any]:
+ d = {
+ 'pubkey': self.pubkey.serialize().hex(),
+ 'amount_BTC': str(self.amount),
+ 'rhash': self.paymenthash.hex(),
+ 'payment_secret': self.payment_secret.hex() if self.payment_secret else None,
+ 'description': self.get_description(),
+ 'exp': self.get_expiry(),
+ 'time': self.date,
+ 'min_final_cltv_delta': self.get_min_final_cltv_delta(),
+ 'features': self.get_features().get_names(),
+ 'tags': self.tags,
+ 'unknown_tags': self.unknown_tags,
+ }
+ if ln_routing_info := self.get_routing_info('r'):
+ d['r_tags'] = self.format_bolt11_routing_info_as_human_readable(ln_routing_info)
+ return d
+
+
+class SerializableKey:
+ def __init__(self, pubkey):
+ self.pubkey = pubkey
+ def serialize(self):
+ return self.pubkey.get_public_key_bytes(True)
+
+
+def decode_bolt11_invoice(invoice: str, *, verbose=False, net=None) -> BOLT11Addr:
+ """Parses a string into an LnAddr object.
+ Can raise LnDecodeException or IncompatibleOrInsaneFeatures.
+ """
+ if net is None:
+ net = constants.net
+ decoded_bech32 = bech32_decode(invoice, ignore_long_length=True)
+ hrp = decoded_bech32.hrp
+ data5 = decoded_bech32.data # "5" as in list of 5-bit integers
+ if decoded_bech32.encoding is None:
+ raise BOLT11DecodeException("Bad bech32 checksum")
+ if decoded_bech32.encoding != segwit_addr.Encoding.BECH32:
+ raise BOLT11DecodeException("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 BOLT11DecodeException("Does not start with ln")
+
+ if not hrp[2:].startswith(net.BOLT11_HRP):
+ raise BOLT11DecodeException(f"Wrong Lightning invoice HRP {hrp[2:]}, should be {net.BOLT11_HRP}")
+
+ # Final signature 65 bytes, split it off.
+ if len(data5) < 65*8//5:
+ raise BOLT11DecodeException("Too short to contain signature")
+ sigdecoded = bytes(convertbits(data5[-65*8//5:], 5, 8, False))
+ data5 = data5[:-65*8//5]
+ data5_remaining = bytearray(data5) # note: bytearray is faster than list of ints
+
+ addr = BOLT11Addr()
+ addr.pubkey = None
+ addr.net = net
+
+ amountstr = hrp[2+len(net.BOLT11_HRP):]
+ # BOLT #11:
+ #
+ # A reader SHOULD indicate if amount is unspecified, otherwise it MUST
+ # multiply `amount` by the `multiplier` value (if any) to derive the
+ # amount required for payment.
+ if amountstr != '':
+ addr.amount = unshorten_amount(amountstr)
+
+ addr.date = int_from_data5(data5_remaining[:7])
+ data5_remaining = data5_remaining[7:]
+
+ while data5_remaining:
+ tag, tagdata = pull_tagged(data5_remaining) # mutates arg
+
+ # BOLT #11:
+ #
+ # A reader MUST skip over unknown fields, an `f` field with unknown
+ # `version`, or a `p`, `h`, or `n` field which does not have
+ # `data_length` 52, 52, or 53 respectively.
+ data_length = len(tagdata)
+
+ if tag == 'r':
+ # BOLT #11:
+ #
+ # * `r` (3): `data_length` variable. One or more entries
+ # containing extra routing information for a private route;
+ # there may be more than one `r` field, too.
+ # * `pubkey` (264 bits)
+ # * `short_channel_id` (64 bits)
+ # * `feebase` (32 bits, big-endian)
+ # * `feerate` (32 bits, big-endian)
+ # * `cltv_expiry_delta` (16 bits, big-endian)
+ tagdata = convertbits(tagdata, 5, 8, False)
+ if not tagdata:
+ continue
+ route = []
+ with io.BytesIO(bytes(tagdata)) as s:
+ while True:
+ pubkey = s.read(33)
+ scid = s.read(8)
+ feebase = s.read(4)
+ feerate = s.read(4)
+ cltv = s.read(2)
+ if len(cltv) != 2:
+ break # EOF
+ feebase = int.from_bytes(feebase, byteorder="big")
+ feerate = int.from_bytes(feerate, byteorder="big")
+ cltv = int.from_bytes(cltv, byteorder="big")
+ route.append((pubkey, scid, feebase, feerate, cltv))
+ if route:
+ addr.tags.append(('r',route))
+ elif tag == 't':
+ tagdata = convertbits(tagdata, 5, 8, False)
+ if not tagdata:
+ continue
+ route = []
+ with io.BytesIO(bytes(tagdata)) as s:
+ pubkey = s.read(33)
+ feebase = s.read(4)
+ feerate = s.read(4)
+ cltv = s.read(2)
+ if len(cltv) == 2: # no EOF
+ feebase = int.from_bytes(feebase, byteorder="big")
+ feerate = int.from_bytes(feerate, byteorder="big")
+ cltv = int.from_bytes(cltv, byteorder="big")
+ route.append((pubkey, feebase, feerate, cltv))
+ addr.tags.append(('t', route))
+ elif tag == 'f':
+ fallback = parse_fallback_addr(tagdata, addr.net)
+ if fallback:
+ addr.tags.append(('f', fallback))
+ else:
+ # Incorrect version.
+ addr.unknown_tags.append((tag, tagdata))
+ continue
+
+ elif tag == 'd':
+ addr.tags.append(('d', bytes(convertbits(tagdata, 5, 8, False)).decode('utf-8')))
+
+ elif tag == 'h':
+ if data_length != 52:
+ addr.unknown_tags.append((tag, tagdata))
+ continue
+ addr.tags.append(('h', bytes(convertbits(tagdata, 5, 8, False))))
+
+ elif tag == 'x':
+ addr.tags.append(('x', int_from_data5(tagdata)))
+
+ elif tag == 'p':
+ if data_length != 52:
+ addr.unknown_tags.append((tag, tagdata))
+ continue
+ addr.paymenthash = bytes(convertbits(tagdata, 5, 8, False))
+
+ elif tag == 's':
+ if data_length != 52:
+ addr.unknown_tags.append((tag, tagdata))
+ continue
+ addr.payment_secret = bytes(convertbits(tagdata, 5, 8, False))
+
+ elif tag == 'n':
+ if data_length != 53:
+ addr.unknown_tags.append((tag, tagdata))
+ continue
+ pubkeybytes = bytes(convertbits(tagdata, 5, 8, False))
+ addr.pubkey = pubkeybytes
+
+ elif tag == 'c':
+ addr.tags.append(('c', int_from_data5(tagdata)))
+
+ elif tag == '9':
+ features = int_from_data5(tagdata)
+ addr.tags.append(('9', features))
+ # note: The features are not validated here in the parser,
+ # instead, validation is done just before we try paying the invoice (in lnworker._check_bolt11_invoice).
+ # Context: invoice parsing happens when opening a wallet. If there was a backwards-incompatible
+ # change to a feature, and we raised, some existing wallets could not be opened. Such a change
+ # can happen to features not-yet-merged-to-BOLTs (e.g. trampoline feature bit was moved and reused).
+ else:
+ addr.unknown_tags.append((tag, tagdata))
+
+ if verbose:
+ print('hex of signature data (32 byte r, 32 byte s): {}'
+ .format(hexlify(sigdecoded[0:64])))
+ print('recovery flag: {}'.format(sigdecoded[64]))
+ data8 = bytes(convertbits(data5, 5, 8, True))
+ print('hex of data for signing: {}'
+ .format(hexlify(hrp.encode("ascii") + data8)))
+ print('SHA256 of above: {}'.format(sha256(hrp.encode("ascii") + data8).hexdigest()))
+
+ # BOLT #11:
+ #
+ # A reader MUST check that the `signature` is valid (see the `n` tagged
+ # field specified below).
+ addr.signature = sigdecoded[:65]
+ hrp_hash = sha256(hrp.encode("ascii") + bytes(convertbits(data5, 5, 8, True))).digest()
+ if addr.pubkey: # Specified by `n`
+ # BOLT #11:
+ #
+ # A reader MUST use the `n` field to validate the signature instead of
+ # performing signature recovery if a valid `n` field is provided.
+ if not ecc.ECPubkey(addr.pubkey).ecdsa_verify(sigdecoded[:64], hrp_hash):
+ raise BOLT11DecodeException("bad signature")
+ pubkey_copy = addr.pubkey
+
+ class WrappedBytesKey:
+ serialize = lambda: pubkey_copy
+
+ addr.pubkey = WrappedBytesKey
+ else: # Recover pubkey from signature.
+ addr.pubkey = SerializableKey(ecc.ECPubkey.from_ecdsa_sig64(sigdecoded[:64], sigdecoded[64], hrp_hash))
+
+ return addr
diff --git a/electrum/gui/qt/channel_details.py b/electrum/gui/qt/channel_details.py
index 0bc1652..3369dd1 100644
--- a/electrum/gui/qt/channel_details.py
+++ b/electrum/gui/qt/channel_details.py
@@ -10,7 +10,7 @@ from electrum.i18n import _
from electrum.util import format_time
from electrum.lnutil import format_short_channel_id, LOCAL, REMOTE, UpdateAddHtlc, Direction
from electrum.lnchannel import htlcsum, Channel, AbstractChannel, HTLCWithStatus
-from electrum.lnaddr import LnAddr, lndecode
+from electrum.bolt11 import BOLT11Addr, decode_bolt11_invoice
from electrum.bitcoin import COIN
from electrum.wallet import Abstract_Wallet
diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py
index d6bb67b..6e1c35d 100644
--- a/electrum/gui/qt/main_window.py
+++ b/electrum/gui/qt/main_window.py
@@ -73,7 +73,7 @@ from electrum.exchange_rate import FxThread
from electrum.simple_config import SimpleConfig
from electrum.logging import Logger
from electrum.lntransport import extract_nodeid, ConnStringFormatError
-from electrum.lnaddr import lndecode, LnAddr
+from electrum.bolt11 import decode_bolt11_invoice, BOLT11Addr
from electrum.submarine_swaps import SwapServerTransport, NostrTransport
from electrum.fee_policy import FeePolicy
@@ -1678,7 +1678,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
def show_lightning_invoice(self, invoice: Invoice):
from electrum.util import format_short_id
- lnaddr = lndecode(invoice.lightning_invoice)
+ lnaddr = decode_bolt11_invoice(invoice.lightning_invoice)
d = WindowModalDialog(self, _("Lightning Invoice"))
vbox = QVBoxLayout(d)
grid = QGridLayout()
@@ -1713,7 +1713,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
grid.addWidget(QLabel(_('Text') + ':'), 8, 0)
grid.addWidget(invoice_e, 8, 1)
r_tags = lnaddr.get_routing_info('r')
- r_tags = '\n'.join(repr(r) for r in LnAddr.format_bolt11_routing_info_as_human_readable(r_tags))
+ r_tags = '\n'.join(repr(r) for r in BOLT11Addr.format_bolt11_routing_info_as_human_readable(r_tags))
routing_e = QTextEdit(str(r_tags))
routing_e.setReadOnly(True)
grid.addWidget(QLabel(_("Routing Hints") + ':'), 9, 0)
diff --git a/electrum/gui/text.py b/electrum/gui/text.py
index 09fc64c..1c86193 100644
--- a/electrum/gui/text.py
+++ b/electrum/gui/text.py
@@ -50,9 +50,9 @@ def parse_bip21(text):
def parse_bolt11(text):
- from electrum.lnaddr import lndecode
+ from electrum.bolt11 import decode_bolt11_invoice
try:
- return lndecode(text)
+ return decode_bolt11_invoice(text)
except Exception:
return
diff --git a/electrum/invoices.py b/electrum/invoices.py
index 8f20166..c15f329 100644
--- a/electrum/invoices.py
+++ b/electrum/invoices.py
@@ -9,7 +9,7 @@ from .i18n import _
from .util import age, InvoiceError, format_satoshis
from .bip21 import create_bip21_uri
from .lnutil import hex_to_bytes
-from .lnaddr import lndecode, LnAddr
+from .bolt11 import decode_bolt11_invoice, BOLT11Addr
from . import constants
from .bitcoin import COIN, TOTAL_COIN_SUPPLY_LIMIT_IN_BTC
from .bitcoin import address_to_script
@@ -213,7 +213,7 @@ class BaseInvoice(StoredObject):
Might raise InvoiceError.
"""
try:
- lnaddr = lndecode(invoice)
+ lnaddr = decode_bolt11_invoice(invoice)
except Exception as e:
raise InvoiceError(e) from e
amount_msat = lnaddr.get_amount_msat()
@@ -275,9 +275,9 @@ class Invoice(BaseInvoice):
return address
@property
- def _lnaddr(self) -> LnAddr:
+ def _lnaddr(self) -> BOLT11Addr:
if self.__lnaddr is None:
- self.__lnaddr = lndecode(self.lightning_invoice)
+ self.__lnaddr = decode_bolt11_invoice(self.lightning_invoice)
return self.__lnaddr
@property
@@ -288,7 +288,7 @@ class Invoice(BaseInvoice):
@lightning_invoice.validator
def _validate_invoice_str(self, attribute, value):
if value is not None:
- lnaddr = lndecode(value) # this checks the str can be decoded
+ lnaddr = decode_bolt11_invoice(value) # this checks the str can be decoded
self.__lnaddr = lnaddr # save it, just to avoid having to recompute later
def can_be_paid_onchain(self) -> bool:
diff --git a/electrum/lnaddr.py b/electrum/lnaddr.py
deleted file mode 100644
index 0316768..0000000
--- a/electrum/lnaddr.py
+++ /dev/null
@@ -1,593 +0,0 @@
-#! /usr/bin/env python3
-# This was forked from https://github.com/rustyrussell/lightning-payencode/tree/acc16ec13a3fa1dc16c07af6ec67c261bd8aff23
-
-import io
-import re
-import time
-from hashlib import sha256
-from binascii import hexlify
-from decimal import Decimal
-from typing import Optional, TYPE_CHECKING, Type, Dict, Any, Sequence, Tuple
-import random
-
-import electrum_ecc as ecc
-
-from .bitcoin import hash160_to_b58_address, b58_address_to_hash160, TOTAL_COIN_SUPPLY_LIMIT_IN_BTC
-from .segwit_addr import bech32_encode, bech32_decode, CHARSET, CHARSET_INVERSE, convertbits
-from . import segwit_addr
-from . import constants
-from .constants import AbstractNet
-from .bitcoin import COIN
-
-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
-# leading zeroes, SHOULD use the shortest representation possible.
-def shorten_amount(amount):
- """ Given an amount in bitcoin, shorten it
- """
- # Convert to pico initially
- amount = int(amount * 10**12)
- 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:
- """ Given a shortened amount, convert it into a decimal
- """
- # BOLT #11:
- # The following `multiplier` letters are defined:
- #
- #* `m` (milli): multiply by 0.001
- #* `u` (micro): multiply by 0.000001
- #* `n` (nano): multiply by 0.000000001
- #* `p` (pico): multiply by 0.000000000001
- units = {
- 'p': 10**12,
- 'n': 10**9,
- 'u': 10**6,
- 'm': 10**3,
- }
- unit = str(amount)[-1]
- # BOLT #11:
- # 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 LnDecodeException("Invalid amount '{}'".format(amount))
-
- if unit in units.keys():
- return Decimal(amount[:-1]) / units[unit]
- else:
- return Decimal(amount)
-
-
-def encode_fallback_addr(fallback: str, net: Type[AbstractNet]) -> Sequence[int]:
- """Encode all supported fallback addresses."""
- wver, wprog_ints = segwit_addr.decode_segwit_address(net.SEGWIT_HRP, fallback)
- if wver is not None:
- wprog = bytes(wprog_ints)
- else:
- addrtype, addr = b58_address_to_hash160(fallback)
- if addrtype == net.ADDRTYPE_P2PKH:
- wver = 17
- elif addrtype == net.ADDRTYPE_P2SH:
- wver = 18
- else:
- raise LnEncodeException(f"Unknown address type {addrtype} for {net}")
- wprog = addr
- data5 = convertbits(wprog, 8, 5)
- assert data5 is not None
- return tagged5('f', [wver] + list(data5))
-
-
-def parse_fallback_addr(data5: Sequence[int], net: Type[AbstractNet]) -> Optional[str]:
- wver = data5[0]
- data8 = bytes(convertbits(data5[1:], 5, 8, False))
- if wver == 17:
- addr = hash160_to_b58_address(data8, net.ADDRTYPE_P2PKH)
- elif wver == 18:
- addr = hash160_to_b58_address(data8, net.ADDRTYPE_P2SH)
- elif wver <= 16:
- addr = segwit_addr.encode_segwit_address(net.SEGWIT_HRP, wver, data8)
- else:
- return None
- return addr
-
-
-def tagged5(char: str, data5: Sequence[int]) -> Sequence[int]:
- assert len(data5) < (1 << 10)
- return [CHARSET_INVERSE[char], len(data5) >> 5, len(data5) & 31] + data5
-
-
-def tagged8(char: str, data8: Sequence[int]) -> Sequence[int]:
- return tagged5(char, convertbits(data8, 8, 5))
-
-
-def int_to_data5(val: int, *, bit_len: int = None) -> Sequence[int]:
- """Represent big-endian number with as many 0-31 values as it takes.
- If `bit_len` is set, use exactly bit_len//5 values (left-padded with zeroes).
- """
- if bit_len is not None:
- assert bit_len % 5 == 0, bit_len
- if val.bit_length() > bit_len:
- raise ValueError(f"{val=} too big for {bit_len=!r}")
- ret = []
- while val != 0:
- ret.append(val % 32)
- val //= 32
- if bit_len is not None:
- ret.extend([0] * (len(ret) - bit_len // 5))
- ret.reverse()
- return ret
-
-
-def int_from_data5(data5: Sequence[int]) -> int:
- total = 0
- for v in data5:
- total = 32 * total + v
- return total
-
-
-def pull_tagged(data5: bytearray) -> Tuple[str, Sequence[int]]:
- """Try to pull out tagged data: returns tag, tagged data. Mutates data in-place."""
- if len(data5) < 3:
- raise ValueError("Truncated field")
- length = data5[1] * 32 + data5[2]
- if length > len(data5) - 3:
- raise ValueError(
- "Truncated {} field: expected {} values".format(CHARSET[data5[0]], length))
- ret = (CHARSET[data5[0]], data5[3:3+length])
- del data5[:3 + length] # much faster than: data5=data5[offset:]
- return ret
-
-
-def lnencode(addr: 'LnAddr', privkey) -> str:
- if addr.amount:
- amount = addr.net.BOLT11_HRP + shorten_amount(addr.amount)
- else:
- amount = addr.net.BOLT11_HRP if addr.net else ''
-
- hrp = 'ln' + amount
-
- # Start with the timestamp
- data5 = int_to_data5(addr.date, bit_len=35)
-
- tags_set = set()
-
- # Payment hash
- assert addr.paymenthash is not None
- data5 += tagged8('p', addr.paymenthash)
- tags_set.add('p')
-
- if addr.payment_secret is not None:
- data5 += tagged8('s', addr.payment_secret)
- tags_set.add('s')
-
- for k, v in addr.tags:
-
- # BOLT #11:
- #
- # A writer MUST NOT include more than one `d`, `h`, `n` or `x` fields,
- if k in ('d', 'h', 'n', 'x', 'p', 's', '9'):
- if k in tags_set:
- raise LnEncodeException("Duplicate '{}' tag".format(k))
-
- if k == 'r':
- route = bytearray()
- for step in v:
- pubkey, scid, feebase, feerate, cltv = step
- route += pubkey
- route += scid
- route += int.to_bytes(feebase, length=4, byteorder="big", signed=False)
- route += int.to_bytes(feerate, length=4, byteorder="big", signed=False)
- route += int.to_bytes(cltv, length=2, byteorder="big", signed=False)
- data5 += tagged8('r', route)
- elif k == 't':
- pubkey, feebase, feerate, cltv = v
- route = bytearray()
- route += pubkey
- route += int.to_bytes(feebase, length=4, byteorder="big", signed=False)
- route += int.to_bytes(feerate, length=4, byteorder="big", signed=False)
- route += int.to_bytes(cltv, length=2, byteorder="big", signed=False)
- data5 += tagged8('t', route)
- elif k == 'f':
- if v is not None:
- data5 += encode_fallback_addr(v, addr.net)
- elif k == 'd':
- # truncate to max length: 1024*5 bits = 639 bytes
- data5 += tagged8('d', v.encode()[0:639])
- elif k == 'x':
- expirybits = int_to_data5(v)
- data5 += tagged5('x', expirybits)
- elif k == 'h':
- data5 += tagged8('h', sha256(v.encode('utf-8')).digest())
- elif k == 'n':
- data5 += tagged8('n', v)
- elif k == 'c':
- finalcltvbits = int_to_data5(v)
- data5 += tagged5('c', finalcltvbits)
- elif k == '9':
- if v == 0:
- continue
- feature_bits = int_to_data5(v)
- data5 += tagged5('9', feature_bits)
- else:
- # FIXME: Support unknown tags?
- raise LnEncodeException("Unknown tag {}".format(k))
-
- tags_set.add(k)
-
- # BOLT #11:
- #
- # A writer MUST include either a `d` or `h` field, and MUST NOT include
- # both.
- if 'd' in tags_set and 'h' in tags_set:
- raise ValueError("Cannot include both 'd' and 'h'")
- if 'd' not in tags_set and 'h' not in tags_set:
- raise ValueError("Must include either 'd' or 'h'")
-
- # We actually sign the hrp, then data (padded to 8 bits with zeroes).
- msg = hrp.encode("ascii") + bytes(convertbits(data5, 5, 8))
- msg32 = sha256(msg).digest()
- privkey = ecc.ECPrivkey(privkey)
- sig = privkey.ecdsa_sign_recoverable(msg32, is_compressed=False)
- recovery_flag = bytes([sig[0] - 27])
- sig = bytes(sig[1:]) + recovery_flag
- sig = bytes(convertbits(sig, 8, 5, False))
- data5 += sig
-
- return bech32_encode(segwit_addr.Encoding.BECH32, hrp, data5)
-
-
-class LnAddr(object):
- def __init__(self, *, paymenthash: bytes = None, amount=None, net: Type[AbstractNet] = None, tags=None, date=None,
- payment_secret: bytes = None):
- self.date = int(time.time()) if not date else int(date)
- self.tags = [] if not tags else tags
- self.unknown_tags = []
- self.paymenthash = paymenthash
- self.payment_secret = payment_secret
- self.signature = None
- self.pubkey = None
- self.net = constants.net if net is None else net # type: Type[AbstractNet]
- self._amount = amount # type: Optional[Decimal] # in bitcoins
-
- @property
- def amount(self) -> Optional[Decimal]:
- return self._amount
-
- @amount.setter
- def amount(self, value):
- if not (isinstance(value, Decimal) or value is None):
- 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 LnInvoiceException(f"amount is out-of-bounds: {value!r} BTC")
- if value * 10**12 % 10:
- # max resolution is millisatoshi
- raise LnInvoiceException(f"Cannot encode {value!r}: too many decimal places")
- self._amount = value
-
- def get_amount_sat(self) -> Optional[Decimal]:
- # note that this has msat resolution potentially
- if self.amount is None:
- return None
- return self.amount * COIN
-
- def get_routing_info(self, tag):
- # note: tag will be 't' for trampoline
- r_tags = list(filter(lambda x: x[0] == tag, self.tags))
- # strip the tag type, it's implicitly 'r' now
- r_tags = list(map(lambda x: x[1], r_tags))
- # if there are multiple hints, we will use the first one that works,
- # from a random permutation
- random.shuffle(r_tags)
- return r_tags
-
- @staticmethod
- def format_bolt11_routing_info_as_human_readable(r_tags, *, has_explicit_r_tagtype: bool = False):
- """Converts the node-id bytes->hex, and the SCID bytes->"AAAxBBBxCC", e.g. for logging."""
- from .util import format_short_id
- r_tags2 = []
- for r_tag in r_tags:
- if has_explicit_r_tagtype:
- (tagtype, path) = r_tag
- assert tagtype == "r", f"found unexpected {tagtype=}"
- else:
- path = r_tag
- path2 = [
- (edge[0].hex(), format_short_id(edge[1]), edge[2], edge[3], edge[4])
- for edge in path]
- r_tag2 = (tagtype, path2) if has_explicit_r_tagtype else path2
- r_tags2.append(r_tag2)
- return r_tags2
-
- def get_amount_msat(self) -> Optional[int]:
- if self.amount is None:
- return None
- return int(self.amount * COIN * 1000)
-
- def get_features(self) -> 'LnFeatures':
- from .lnutil import LnFeatures
- return LnFeatures(self.get_tag('9') or 0)
-
- def validate_and_compare_features(self, myfeatures: 'LnFeatures') -> None:
- """Raises IncompatibleOrInsaneFeatures.
-
- note: these checks are not done by the parser (in lndecode), as then when we started requiring a new feature,
- old saved already paid invoices could no longer be parsed.
- """
- from .lnutil import validate_features, ln_compare_features
- invoice_features = self.get_features()
- validate_features(invoice_features)
- ln_compare_features(myfeatures.for_invoice(), invoice_features)
-
- def __str__(self):
- return "LnAddr[{}, amount={}{} tags=[{}]]".format(
- hexlify(self.pubkey.serialize()).decode('utf-8') if self.pubkey else None,
- self.amount, self.net.BOLT11_HRP,
- ", ".join([k + '=' + str(v) for k, v in self.tags])
- )
-
- def get_min_final_cltv_delta(self) -> int:
- cltv = self.get_tag('c')
- if cltv is None:
- return 18
- return int(cltv)
-
- def get_tag(self, tag):
- for k, v in self.tags:
- if k == tag:
- return v
- return None
-
- def get_description(self) -> str:
- return self.get_tag('d') or ''
-
- def get_fallback_address(self) -> str:
- return self.get_tag('f') or ''
-
- def get_expiry(self) -> int:
- exp = self.get_tag('x')
- if exp is None:
- exp = 3600
- return int(exp)
-
- def is_expired(self) -> bool:
- now = time.time()
- # BOLT-11 does not specify what expiration of '0' means.
- # we treat it as 0 seconds here (instead of never)
- return now > self.get_expiry() + self.date
-
- def to_debug_json(self) -> Dict[str, Any]:
- d = {
- 'pubkey': self.pubkey.serialize().hex(),
- 'amount_BTC': str(self.amount),
- 'rhash': self.paymenthash.hex(),
- 'payment_secret': self.payment_secret.hex() if self.payment_secret else None,
- 'description': self.get_description(),
- 'exp': self.get_expiry(),
- 'time': self.date,
- 'min_final_cltv_delta': self.get_min_final_cltv_delta(),
- 'features': self.get_features().get_names(),
- 'tags': self.tags,
- 'unknown_tags': self.unknown_tags,
- }
- if ln_routing_info := self.get_routing_info('r'):
- d['r_tags'] = self.format_bolt11_routing_info_as_human_readable(ln_routing_info)
- return d
-
-
-class SerializableKey:
- def __init__(self, pubkey):
- self.pubkey = pubkey
- def serialize(self):
- return self.pubkey.get_public_key_bytes(True)
-
-
-def lndecode(invoice: str, *, verbose=False, net=None) -> LnAddr:
- """Parses a string into an LnAddr object.
- Can raise LnDecodeException or IncompatibleOrInsaneFeatures.
- """
- if net is None:
- net = constants.net
- decoded_bech32 = bech32_decode(invoice, ignore_long_length=True)
- hrp = decoded_bech32.hrp
- data5 = decoded_bech32.data # "5" as in list of 5-bit integers
- if decoded_bech32.encoding is None:
- raise LnDecodeException("Bad bech32 checksum")
- if decoded_bech32.encoding != segwit_addr.Encoding.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 LnDecodeException("Does not start with ln")
-
- if not hrp[2:].startswith(net.BOLT11_HRP):
- raise LnDecodeException(f"Wrong Lightning invoice HRP {hrp[2:]}, should be {net.BOLT11_HRP}")
-
- # Final signature 65 bytes, split it off.
- if len(data5) < 65*8//5:
- raise LnDecodeException("Too short to contain signature")
- sigdecoded = bytes(convertbits(data5[-65*8//5:], 5, 8, False))
- data5 = data5[:-65*8//5]
- data5_remaining = bytearray(data5) # note: bytearray is faster than list of ints
-
- addr = LnAddr()
- addr.pubkey = None
- addr.net = net
-
- amountstr = hrp[2+len(net.BOLT11_HRP):]
- # BOLT #11:
- #
- # A reader SHOULD indicate if amount is unspecified, otherwise it MUST
- # multiply `amount` by the `multiplier` value (if any) to derive the
- # amount required for payment.
- if amountstr != '':
- addr.amount = unshorten_amount(amountstr)
-
- addr.date = int_from_data5(data5_remaining[:7])
- data5_remaining = data5_remaining[7:]
-
- while data5_remaining:
- tag, tagdata = pull_tagged(data5_remaining) # mutates arg
-
- # BOLT #11:
- #
- # A reader MUST skip over unknown fields, an `f` field with unknown
- # `version`, or a `p`, `h`, or `n` field which does not have
- # `data_length` 52, 52, or 53 respectively.
- data_length = len(tagdata)
-
- if tag == 'r':
- # BOLT #11:
- #
- # * `r` (3): `data_length` variable. One or more entries
- # containing extra routing information for a private route;
- # there may be more than one `r` field, too.
- # * `pubkey` (264 bits)
- # * `short_channel_id` (64 bits)
- # * `feebase` (32 bits, big-endian)
- # * `feerate` (32 bits, big-endian)
- # * `cltv_expiry_delta` (16 bits, big-endian)
- tagdata = convertbits(tagdata, 5, 8, False)
- if not tagdata:
- continue
- route = []
- with io.BytesIO(bytes(tagdata)) as s:
- while True:
- pubkey = s.read(33)
- scid = s.read(8)
- feebase = s.read(4)
- feerate = s.read(4)
- cltv = s.read(2)
- if len(cltv) != 2:
- break # EOF
- feebase = int.from_bytes(feebase, byteorder="big")
- feerate = int.from_bytes(feerate, byteorder="big")
- cltv = int.from_bytes(cltv, byteorder="big")
- route.append((pubkey, scid, feebase, feerate, cltv))
- if route:
- addr.tags.append(('r',route))
- elif tag == 't':
- tagdata = convertbits(tagdata, 5, 8, False)
- if not tagdata:
- continue
- route = []
- with io.BytesIO(bytes(tagdata)) as s:
- pubkey = s.read(33)
- feebase = s.read(4)
- feerate = s.read(4)
- cltv = s.read(2)
- if len(cltv) == 2: # no EOF
- feebase = int.from_bytes(feebase, byteorder="big")
- feerate = int.from_bytes(feerate, byteorder="big")
- cltv = int.from_bytes(cltv, byteorder="big")
- route.append((pubkey, feebase, feerate, cltv))
- addr.tags.append(('t', route))
- elif tag == 'f':
- fallback = parse_fallback_addr(tagdata, addr.net)
- if fallback:
- addr.tags.append(('f', fallback))
- else:
- # Incorrect version.
- addr.unknown_tags.append((tag, tagdata))
- continue
-
- elif tag == 'd':
- addr.tags.append(('d', bytes(convertbits(tagdata, 5, 8, False)).decode('utf-8')))
-
- elif tag == 'h':
- if data_length != 52:
- addr.unknown_tags.append((tag, tagdata))
- continue
- addr.tags.append(('h', bytes(convertbits(tagdata, 5, 8, False))))
-
- elif tag == 'x':
- addr.tags.append(('x', int_from_data5(tagdata)))
-
- elif tag == 'p':
- if data_length != 52:
- addr.unknown_tags.append((tag, tagdata))
- continue
- addr.paymenthash = bytes(convertbits(tagdata, 5, 8, False))
-
- elif tag == 's':
- if data_length != 52:
- addr.unknown_tags.append((tag, tagdata))
- continue
- addr.payment_secret = bytes(convertbits(tagdata, 5, 8, False))
-
- elif tag == 'n':
- if data_length != 53:
- addr.unknown_tags.append((tag, tagdata))
- continue
- pubkeybytes = bytes(convertbits(tagdata, 5, 8, False))
- addr.pubkey = pubkeybytes
-
- elif tag == 'c':
- addr.tags.append(('c', int_from_data5(tagdata)))
-
- elif tag == '9':
- features = int_from_data5(tagdata)
- addr.tags.append(('9', features))
- # note: The features are not validated here in the parser,
- # instead, validation is done just before we try paying the invoice (in lnworker._check_bolt11_invoice).
- # Context: invoice parsing happens when opening a wallet. If there was a backwards-incompatible
- # change to a feature, and we raised, some existing wallets could not be opened. Such a change
- # can happen to features not-yet-merged-to-BOLTs (e.g. trampoline feature bit was moved and reused).
- else:
- addr.unknown_tags.append((tag, tagdata))
-
- if verbose:
- print('hex of signature data (32 byte r, 32 byte s): {}'
- .format(hexlify(sigdecoded[0:64])))
- print('recovery flag: {}'.format(sigdecoded[64]))
- data8 = bytes(convertbits(data5, 5, 8, True))
- print('hex of data for signing: {}'
- .format(hexlify(hrp.encode("ascii") + data8)))
- print('SHA256 of above: {}'.format(sha256(hrp.encode("ascii") + data8).hexdigest()))
-
- # BOLT #11:
- #
- # A reader MUST check that the `signature` is valid (see the `n` tagged
- # field specified below).
- addr.signature = sigdecoded[:65]
- hrp_hash = sha256(hrp.encode("ascii") + bytes(convertbits(data5, 5, 8, True))).digest()
- if addr.pubkey: # Specified by `n`
- # BOLT #11:
- #
- # A reader MUST use the `n` field to validate the signature instead of
- # performing signature recovery if a valid `n` field is provided.
- if not ecc.ECPubkey(addr.pubkey).ecdsa_verify(sigdecoded[:64], hrp_hash):
- raise LnDecodeException("bad signature")
- pubkey_copy = addr.pubkey
-
- class WrappedBytesKey:
- serialize = lambda: pubkey_copy
-
- addr.pubkey = WrappedBytesKey
- else: # Recover pubkey from signature.
- addr.pubkey = SerializableKey(ecc.ECPubkey.from_ecdsa_sig64(sigdecoded[:64], sigdecoded[64], hrp_hash))
-
- return addr
diff --git a/electrum/lnurl.py b/electrum/lnurl.py
index e08f254..603ff20 100644
--- a/electrum/lnurl.py
+++ b/electrum/lnurl.py
@@ -12,7 +12,7 @@ import aiohttp.client_exceptions
from electrum import segwit_addr, util
from electrum.segwit_addr import bech32_decode, Encoding, convertbits, bech32_encode
-from electrum.lnaddr import LnDecodeException, LnEncodeException
+from electrum.bolt11 import BOLT11DecodeException, BOLT11EncodeException
from electrum.network import Network
from electrum.logging import get_logger
from electrum.i18n import _
@@ -47,11 +47,11 @@ def decode_lnurl(lnurl: str) -> str:
hrp = decoded_bech32.hrp
data = decoded_bech32.data
if decoded_bech32.encoding is None:
- raise LnDecodeException("Bad bech32 checksum")
+ raise BOLT11DecodeException("Bad bech32 checksum")
if decoded_bech32.encoding != Encoding.BECH32:
- raise LnDecodeException("Bad bech32 encoding: must be using vanilla BECH32")
+ raise BOLT11DecodeException("Bad bech32 encoding: must be using vanilla BECH32")
if not hrp.startswith("lnurl"):
- raise LnDecodeException("Does not start with lnurl")
+ raise BOLT11DecodeException("Does not start with lnurl")
data = convertbits(data, 5, 8, False)
url = bytes(data).decode("utf-8")
return url
@@ -62,7 +62,7 @@ def encode_lnurl(url: str) -> str:
try:
url = url.encode("utf-8")
except UnicodeError as e:
- raise LnEncodeException("invalid url") from e
+ raise BOLT11EncodeException("invalid url") from e
bech32_data = convertbits(url, 8, 5, True)
assert bech32_data
lnurl = bech32_encode(
diff --git a/electrum/lnworker.py b/electrum/lnworker.py
index e73fb51..79421e6 100644
--- a/electrum/lnworker.py
+++ b/electrum/lnworker.py
@@ -64,7 +64,7 @@ from .lntransport import (
ConnStringFormatError
)
from .lnpeer import Peer, LN_P2P_NETWORK_TIMEOUT
-from .lnaddr import lnencode, LnAddr, lndecode
+from .bolt11 import encode_bolt11_invoice, BOLT11Addr, decode_bolt11_invoice
from .lnchannel import Channel, AbstractChannel, ChannelState, PeerState, HTLCWithStatus, ChannelBackup
from .lnrater import LNRater
from .lnutil import (
@@ -123,7 +123,7 @@ class PaymentInfo:
- Historically, we used to store "bolt11, direction, status", but deserializing bolt11 was too slow.
(even deserializing just once - all bolt11 during wallet-open - was slow)
- - note: the deserialization code in lnaddr.py has been significantly sped up since
+ - note: the deserialization code in bolt11.py has been significantly sped up since
- For incoming payments, for unpaid requests, ~every time the user displays the unpaid bolt11,
we get a chance to display a new bolt11, with same payment_hash/amount, but with updated
routing_hints (channels might get closed/opened, or just liquidity changed drastically).
@@ -1930,7 +1930,7 @@ class LNWallet(Logger):
f"pay_to_node starting session for RHASH={payment_hash.hex()}. "
f"using_trampoline={self.uses_trampoline()}. "
f"invoice_features={paysession.invoice_features.get_names()}. "
- f"r_tags={LnAddr.format_bolt11_routing_info_as_human_readable(r_tags)}. "
+ f"r_tags={BOLT11Addr.format_bolt11_routing_info_as_human_readable(r_tags)}. "
f"{amount_to_pay=} msat. {budget=}")
if not self.uses_trampoline():
self.logger.info(
@@ -2219,11 +2219,11 @@ class LNWallet(Logger):
except Exception:
return None
- def _check_bolt11_invoice(self, bolt11_invoice: str, *, amount_msat: int = None) -> LnAddr:
+ def _check_bolt11_invoice(self, bolt11_invoice: str, *, amount_msat: int = None) -> BOLT11Addr:
"""Parses and validates a bolt11 invoice str into a LnAddr.
Includes pre-payment checks external to the parser.
"""
- addr = lndecode(bolt11_invoice)
+ addr = decode_bolt11_invoice(bolt11_invoice)
if addr.is_expired():
raise InvoiceError(_("This invoice has expired"))
# check amount
@@ -2563,7 +2563,7 @@ class LNWallet(Logger):
message: str,
fallback_address: Optional[str],
channels: Optional[Sequence[Channel]] = None,
- ) -> Tuple[LnAddr, str]:
+ ) -> Tuple[BOLT11Addr, str]:
amount_msat = payment_info.amount_msat
pair = self._bolt11_cache.get(payment_info.payment_hash)
if pair:
@@ -2580,12 +2580,12 @@ class LNWallet(Logger):
# TODO: make invoice_features dynamic depending on available trampoline channels
only_trampoline=payment_info.invoice_features.supports(LnFeatures.OPTION_TRAMPOLINE_ROUTING_OPT_ELECTRUM),
)
- formatted_r_hints = LnAddr.format_bolt11_routing_info_as_human_readable(routing_hints, has_explicit_r_tagtype=True)
+ formatted_r_hints = BOLT11Addr.format_bolt11_routing_info_as_human_readable(routing_hints, has_explicit_r_tagtype=True)
self.logger.info(f"creating bolt11 invoice with routing_hints: {formatted_r_hints}, sat: {(amount_msat or 0) // 1000}")
payment_secret = self.get_payment_secret(payment_info.payment_hash)
amount_btc = amount_msat/Decimal(COIN*1000) if amount_msat else None
min_final_cltv_delta = payment_info.min_final_cltv_delta + MIN_FINAL_CLTV_DELTA_BUFFER_INVOICE
- lnaddr = LnAddr(
+ lnaddr = BOLT11Addr(
paymenthash=payment_info.payment_hash,
amount=amount_btc,
tags=[
@@ -2597,7 +2597,7 @@ class LNWallet(Logger):
] + routing_hints,
date=timestamp,
payment_secret=payment_secret)
- invoice = lnencode(lnaddr, self.node_keypair.privkey)
+ invoice = encode_bolt11_invoice(lnaddr, self.node_keypair.privkey)
pair = lnaddr, invoice
self._bolt11_cache[payment_info.payment_hash] = pair
return pair
@@ -3971,7 +3971,7 @@ class LNWallet(Logger):
invoice_features = payload["invoice_features"]["invoice_features"]
invoice_routing_info = payload["invoice_routing_info"]["invoice_routing_info"]
r_tags = decode_routing_info(invoice_routing_info)
- self.logger.info(f'r_tags {LnAddr.format_bolt11_routing_info_as_human_readable(r_tags)}')
+ self.logger.info(f'r_tags {BOLT11Addr.format_bolt11_routing_info_as_human_readable(r_tags)}')
# TODO legacy mpp payment, use total_msat from trampoline onion
else:
self.logger.info('forward_trampoline: end-to-end')
diff --git a/electrum/payment_identifier.py b/electrum/payment_identifier.py
index 198bb68..7fe8a2f 100644
--- a/electrum/payment_identifier.py
+++ b/electrum/payment_identifier.py
@@ -18,7 +18,7 @@ from .lnurl import (decode_lnurl, request_lnurl, callback_lnurl, LNURLError,
lightning_address_to_url, try_resolve_lnurlpay, LNURL6Data,
LNURL3Data, LNURLData, SUPPORTED_LNURL_SCHEMES)
from .bitcoin import opcodes, construct_script
-from .lnaddr import LnInvoiceException
+from .bolt11 import BOLT11InvoiceException
from .lnutil import IncompatibleOrInsaneFeatures
from .bip21 import parse_bip21_URI, InvalidBitcoinURI, LIGHTNING_URI_SCHEME, BITCOIN_BIP21_URI_SCHEME
from .segwit_addr import bech32_decode
@@ -520,7 +520,7 @@ class PaymentIdentifier(Logger):
error = _("Error parsing Lightning invoice") + f":\n{e!r}"
if e.args and len(e.args):
arg = e.args[0]
- if isinstance(arg, LnInvoiceException):
+ if isinstance(arg, BOLT11InvoiceException):
error = _("Error parsing Lightning invoice") + f":\n{e}"
elif isinstance(arg, IncompatibleOrInsaneFeatures):
error = _("Invoice requires unknown or incompatible Lightning feature") + f":\n{e!r}"
diff --git a/electrum/plugins/watchtower/server.py b/electrum/plugins/watchtower/server.py
index e184753..2f526f8 100644
--- a/electrum/plugins/watchtower/server.py
+++ b/electrum/plugins/watchtower/server.py
@@ -8,7 +8,7 @@ from aiohttp import web
from electrum.util import log_exceptions, ignore_exceptions
from electrum.logging import Logger
from electrum.util import EventListener
-from electrum.lnaddr import lndecode
+from electrum.bolt11 import decode_bolt11_invoice
from electrum.daemon import AuthenticatedServer
diff --git a/electrum/submarine_swaps.py b/electrum/submarine_swaps.py
index ef55aa6..37d8d2a 100644
--- a/electrum/submarine_swaps.py
+++ b/electrum/submarine_swaps.py
@@ -38,7 +38,7 @@ from .util import (
)
from . import lnutil
from .lnutil import hex_to_bytes, REDEEM_AFTER_DOUBLE_SPENT_DELAY, Keypair
-from .lnaddr import lndecode
+from .bolt11 import decode_bolt11_invoice
from .json_db import StoredObject, stored_in
from . import constants
from .address_synchronizer import (TX_HEIGHT_LOCAL, TX_HEIGHT_FUTURE, TX_HEIGHT_UNCONFIRMED,
@@ -997,7 +997,7 @@ class SwapManager(Logger):
}
await transport.send_request_to_server('addswapinvoice', request_data)
# wait for funding tx
- lnaddr = lndecode(invoice)
+ lnaddr = decode_bolt11_invoice(invoice)
while swap.funding_txid is None and not lnaddr.is_expired():
await asyncio.sleep(0.1)
return swap.funding_txid
diff --git a/electrum/wallet_db.py b/electrum/wallet_db.py
index a5d146a..d00883b 100644
--- a/electrum/wallet_db.py
+++ b/electrum/wallet_db.py
@@ -905,7 +905,7 @@ class WalletDBUpgrader(Logger):
self.data['seed_version'] = 44
def _convert_version_45(self):
- from .lnaddr import lndecode
+ from .bolt11 import decode_bolt11_invoice
if not self._is_upgrade_method_needed(44, 44):
return
swaps = self.data.get('submarine_swaps', {})
@@ -921,7 +921,7 @@ class WalletDBUpgrader(Logger):
outputs = item['outputs'] if not is_lightning else None
bip70 = item['bip70'] if not is_lightning else None
if is_lightning:
- lnaddr = lndecode(item['invoice'])
+ lnaddr = decode_bolt11_invoice(item['invoice'])
amount_msat = lnaddr.get_amount_msat()
timestamp = lnaddr.date
exp_delay = lnaddr.get_expiry()
@@ -974,7 +974,7 @@ class WalletDBUpgrader(Logger):
self.data['seed_version'] = 46
def _convert_version_47(self):
- from .lnaddr import lndecode
+ from .bolt11 import decode_bolt11_invoice
if not self._is_upgrade_method_needed(46, 46):
return
# recalc keys of requests
@@ -982,7 +982,7 @@ class WalletDBUpgrader(Logger):
for key, item in list(requests.items()):
lnaddr = item.get('lightning_invoice')
if lnaddr:
- lnaddr = lndecode(lnaddr)
+ lnaddr = decode_bolt11_invoice(lnaddr)
rhash = lnaddr.paymenthash.hex()
if key != rhash:
requests[rhash] = item
@@ -1023,7 +1023,7 @@ class WalletDBUpgrader(Logger):
self.data['seed_version'] = 50
def _convert_version_51(self):
- from .lnaddr import lndecode
+ from .bolt11 import decode_bolt11_invoice
if not self._is_upgrade_method_needed(50, 50):
return
requests = self.data.get('payment_requests', {})
@@ -1032,7 +1032,7 @@ class WalletDBUpgrader(Logger):
if lightning_invoice is None:
payment_hash = None
else:
- lnaddr = lndecode(lightning_invoice)
+ lnaddr = decode_bolt11_invoice(lightning_invoice)
payment_hash = lnaddr.paymenthash.hex()
item['payment_hash'] = payment_hash
self.data['seed_version'] = 51
diff --git a/tests/test_bolt11.py b/tests/test_bolt11.py
index a900545..01453a8 100644
--- a/tests/test_bolt11.py
+++ b/tests/test_bolt11.py
@@ -4,7 +4,7 @@ from binascii import unhexlify, hexlify
import pprint
import unittest
-from electrum.lnaddr import shorten_amount, unshorten_amount, LnAddr, lnencode, lndecode
+from electrum.bolt11 import shorten_amount, unshorten_amount, BOLT11Addr, encode_bolt11_invoice, decode_bolt11_invoice
from electrum.segwit_addr import bech32_encode, bech32_decode
from electrum import segwit_addr
from electrum.lnutil import UnknownEvenFeatureBits, LnFeatures, IncompatibleLightningFeatures
@@ -67,106 +67,106 @@ class TestBolt11(ElectrumTestCase):
timestamp = 1615922274
tests = [
- (LnAddr(date=timestamp, paymenthash=RHASH, payment_secret=PAYMENT_SECRET, tags=[('d', ''), ('9', 33282)]),
+ (BOLT11Addr(date=timestamp, paymenthash=RHASH, payment_secret=PAYMENT_SECRET, tags=[('d', ''), ('9', 33282)]),
"lnbc1ps9zprzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygsdqq9qypqszpyrpe4tym8d3q87d43cgdhhlsrt78epu7u99mkzttmt2wtsx0304rrw50addkryfrd3vn3zy467vxwlmf4uz7yvntuwjr2hqjl9lw5cqwtp2dy"),
- (LnAddr(date=timestamp, paymenthash=RHASH, payment_secret=PAYMENT_SECRET, amount=Decimal('0.001'), tags=[('d', '1 cup coffee'), ('x', 60), ('9', 0x28200)]),
+ (BOLT11Addr(date=timestamp, paymenthash=RHASH, payment_secret=PAYMENT_SECRET, amount=Decimal('0.001'), tags=[('d', '1 cup coffee'), ('x', 60), ('9', 0x28200)]),
"lnbc1m1ps9zprzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygsdq5xysxxatsyp3k7enxv4jsxqzpu9qy9qsqw8l2pulslacwjt86vle3sgfdmcct5v34gtcpfnujsf6ufqa7v7jzdpddnwgte82wkscdlwfwucrgn8z36rv9hzk5mukltteh0yqephqpk5vegu"),
- (LnAddr(date=timestamp, paymenthash=RHASH, payment_secret=PAYMENT_SECRET, amount=Decimal('1'), tags=[('h', longdescription), ('9', 0x28200)]),
+ (BOLT11Addr(date=timestamp, paymenthash=RHASH, payment_secret=PAYMENT_SECRET, amount=Decimal('1'), tags=[('h', longdescription), ('9', 0x28200)]),
"lnbc11ps9zprzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygshp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqs9qy9qsq0jnua6dc4p984aeafs6ss7tjjj7553ympvg82qrjq0zgdqgtdvt5wlwkvw4ds5sn96nazp6ct9ts37tcw708kzkk4p8znahpsgp9tnspnycsf7"),
- (LnAddr(date=timestamp, paymenthash=RHASH, payment_secret=PAYMENT_SECRET, net=constants.BitcoinTestnet, tags=[('f', 'mk2QpYatsKicvFVuTAQLBryyccRXMUaGHP'), ('h', longdescription), ('9', 0x28200)]),
+ (BOLT11Addr(date=timestamp, paymenthash=RHASH, payment_secret=PAYMENT_SECRET, net=constants.BitcoinTestnet, tags=[('f', 'mk2QpYatsKicvFVuTAQLBryyccRXMUaGHP'), ('h', longdescription), ('9', 0x28200)]),
"lntb1ps9zprzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygsfpp3x9et2e20v6pu37c5d9vax37wxq72un98hp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqs9qy9qsqy5826t0z3sn29z396pmr4kv73lcx0v7y6vas6h3pysmqllmzwgm5ps2t468gm4psj52usjy6y4xcry4k84n2zggs6f9agwg95454v6gqrwmh4f"),
- (LnAddr(date=timestamp, paymenthash=RHASH, payment_secret=PAYMENT_SECRET, amount=24, tags=[
+ (BOLT11Addr(date=timestamp, paymenthash=RHASH, payment_secret=PAYMENT_SECRET, amount=24, tags=[
('r', [(unhexlify('029e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255'), unhexlify('0102030405060708'), 1, 20, 3),
(unhexlify('039e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255'), unhexlify('030405060708090a'), 2, 30, 4)]),
('f', '1RustyRX2oai4EYYDpQGWvEL62BBGqN9T'),
('h', longdescription),
('9', 0x28200)]),
"lnbc241ps9zprzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygsr9yq20q82gphp2nflc7jtzrcazrra7wwgzxqc8u7754cdlpfrmccae92qgzqvzq2ps8pqqqqqqpqqqqq9qqqvpeuqafqxu92d8lr6fvg0r5gv0heeeqgcrqlnm6jhphu9y00rrhy4grqszsvpcgpy9qqqqqqgqqqqq7qqzqfpp3qjmp7lwpagxun9pygexvgpjdc4jdj85fhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqs9qy9qsqfnk063vsrgjx7l6td6v42skuxql7epn5tmrl4qte2e78nqnsjlgjg3sgkxreqex5fw4c9chnvtc2hykqnyxr84zwfr8f3d9q3h0nfdgqenlzvj"),
- (LnAddr(date=timestamp, paymenthash=RHASH, payment_secret=PAYMENT_SECRET, amount=24, tags=[('f', '3EktnHQD7RiAE6uzMj2ZifT9YgRrkSgzQX'), ('h', longdescription), ('9', 0x28200)]),
+ (BOLT11Addr(date=timestamp, paymenthash=RHASH, payment_secret=PAYMENT_SECRET, amount=24, tags=[('f', '3EktnHQD7RiAE6uzMj2ZifT9YgRrkSgzQX'), ('h', longdescription), ('9', 0x28200)]),
"lnbc241ps9zprzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygsfppj3a24vwu6r8ejrss3axul8rxldph2q7z9hp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqs9qy9qsqqf6z4r7ruzr5txm5ln4netwa2f4x233tud7jy8gxrynyx07rxt7qm92yk2krlgwr7d8jknglur75sujeyapmda5nf3femrk2mep8a2cp4hlvup"),
- (LnAddr(date=timestamp, paymenthash=RHASH, payment_secret=PAYMENT_SECRET, amount=24, tags=[('f', 'bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4'), ('h', longdescription), ('9', 0x28200)]),
+ (BOLT11Addr(date=timestamp, paymenthash=RHASH, payment_secret=PAYMENT_SECRET, amount=24, tags=[('f', 'bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4'), ('h', longdescription), ('9', 0x28200)]),
"lnbc241ps9zprzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygsfppqw508d6qejxtdg4y5r3zarvary0c5xw7khp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqs9qy9qsqy4wp73jma5uktd9y7yha56f98n2k0hxgnvp2qdcury00dapps3k3urgfy8tvv8jzwcafpy576msk5xx2hladf06m3s5mgx5msn4elfqqaaqjhk"),
- (LnAddr(date=timestamp, paymenthash=RHASH, payment_secret=PAYMENT_SECRET, amount=24, tags=[('f', 'bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3'), ('h', longdescription), ('9', 0x28200)]),
+ (BOLT11Addr(date=timestamp, paymenthash=RHASH, payment_secret=PAYMENT_SECRET, amount=24, tags=[('f', 'bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3'), ('h', longdescription), ('9', 0x28200)]),
"lnbc241ps9zprzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygsfp4qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqs9qy9qsqgt4gg9uktlpgnnuvczazusp5uwjv78na305ucsw06c8uk58e5stjqj9sz7fgavw0z688alt364js72mc9mg8yumhpes2dsmq5k9nr5qqddykxy"),
- (LnAddr(date=timestamp, paymenthash=RHASH, payment_secret=PAYMENT_SECRET, amount=24, tags=[('n', PUBKEY), ('h', longdescription), ('9', 0x28200)]),
+ (BOLT11Addr(date=timestamp, paymenthash=RHASH, payment_secret=PAYMENT_SECRET, amount=24, tags=[('n', PUBKEY), ('h', longdescription), ('9', 0x28200)]),
"lnbc241ps9zprzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygsnp4q0n326hr8v9zprg8gsvezcch06gfaqqhde2aj730yg0durunfhv66hp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqs9qy9qsq2y235rxw7v0gkn2t9ehc742tm3p22q2yjjykq4d85ze6g62yk60navxqz0ga96sqrszju8nlfajthem4gngxvyz4hwy39j4nqm8kv0qq9znxs7"),
- (LnAddr(date=timestamp, paymenthash=RHASH, payment_secret=PAYMENT_SECRET, amount=24, tags=[('h', longdescription), ('9', 2 + (1 << 9) + (1 << 15))]),
+ (BOLT11Addr(date=timestamp, paymenthash=RHASH, payment_secret=PAYMENT_SECRET, amount=24, tags=[('h', longdescription), ('9', 2 + (1 << 9) + (1 << 15))]),
"lnbc241ps9zprzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygshp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqs9qypqszrwfgrl5k3rt4q4mclc8t00p2tcjsf9pmpcq6lu5zhmampyvk43fk30eqpdm8t5qmdpzan25aqxqaqdzmy0smrtduazjcxx975vz78ccpx0qhev"),
- (LnAddr(date=timestamp, paymenthash=RHASH, payment_secret=PAYMENT_SECRET, amount=24, tags=[('h', longdescription), ('9', 10 + (1 << 8) + (1 << 15))]),
+ (BOLT11Addr(date=timestamp, paymenthash=RHASH, payment_secret=PAYMENT_SECRET, amount=24, tags=[('h', longdescription), ('9', 10 + (1 << 8) + (1 << 15))]),
"lnbc241ps9zprzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygshp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqs9qypqg2wans8f6vkfd3l7zjv547hlc7wd7eqyxfwhtdudnkkgrpk6p9ffykwrvdtwm0aakaxujurdxgd7cllnfypmj22cvy7z333udg6zncgacqzmd2z9"),
- (LnAddr(date=timestamp, paymenthash=RHASH, payment_secret=PAYMENT_SECRET, amount=24, tags=[('h', longdescription), ('9', 10 + (1 << 9) + (1 << 15))]),
+ (BOLT11Addr(date=timestamp, paymenthash=RHASH, payment_secret=PAYMENT_SECRET, amount=24, tags=[('h', longdescription), ('9', 10 + (1 << 9) + (1 << 15))]),
"lnbc241ps9zprzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygshp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqs9qypqs2dr525u5f4kjxdv0hq5c822qwxrtttjl4u586yl84x0kvvx66gz9ygy76005s5sjwgr7fp55ccsae47vpl4gqvwhc3exps964g743j5gqwtt68t"),
- (LnAddr(date=timestamp, paymenthash=RHASH, payment_secret=PAYMENT_SECRET, amount=24, tags=[('h', longdescription), ('9', 10 + (1 << 9) + (1 << 14))]),
+ (BOLT11Addr(date=timestamp, paymenthash=RHASH, payment_secret=PAYMENT_SECRET, amount=24, tags=[('h', longdescription), ('9', 10 + (1 << 9) + (1 << 14))]),
"lnbc241ps9zprzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygshp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqs9qrss2f8kr98446xls02yndup2ynwjh46u8kdeuuncexx2hnets0j0064nyq25gkd6jnttldzt5qqtszum5dufvuvryxt204w2p24557udxgcp0nlwtw"),
]
# Some old tests follow that do not have payment_secret. Note that if the parser raised due to the lack of features/payment_secret,
# old wallets that have these invoices saved (as paid/expired), could not be opened (though we could do a db upgrade and delete them).
tests.extend([
- (LnAddr(date=timestamp, paymenthash=RHASH, tags=[('d', '')]),
+ (BOLT11Addr(date=timestamp, paymenthash=RHASH, tags=[('d', '')]),
"lnbc1ps9zprzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdqqd9n3kwjjwglnfne5p4rvkze998m3xcxrc8kunl5khkchlaqhwhlyztuuwkrglv47mqg96mcqjjx70hh9luaj4te0u4ww6aclxwve3fqpkmdxlj"),
- (LnAddr(date=timestamp, paymenthash=RHASH, amount=Decimal('0.001'), tags=[('d', '1 cup coffee'), ('x', 60)]),
+ (BOLT11Addr(date=timestamp, paymenthash=RHASH, amount=Decimal('0.001'), tags=[('d', '1 cup coffee'), ('x', 60)]),
"lnbc1m1ps9zprzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpu9rflz25dx0qw6kdg05u0c5hdc30yq6ga6ew4pz86n244va45nchns9zrs3wjxznsqnt37hz7pswvc56wvuhxcjyd6k3lqf4ujynyxuspmvr078"),
- (LnAddr(date=timestamp, paymenthash=RHASH, amount=Decimal('1'), tags=[('h', longdescription)]),
+ (BOLT11Addr(date=timestamp, paymenthash=RHASH, amount=Decimal('1'), tags=[('h', longdescription)]),
"lnbc11ps9zprzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqs2qjafckq94q3js6lvqz2kmenn9ysjejyj8fm4hlx0xtqhaxfzlxjappkgp0hmm40dnuan4v3jy83lqjup2n0fdzgysg049y9l9uc98qq07kfd3"),
- (LnAddr(date=timestamp, paymenthash=RHASH, net=constants.BitcoinTestnet, tags=[('f', 'mk2QpYatsKicvFVuTAQLBryyccRXMUaGHP'), ('h', longdescription)]),
+ (BOLT11Addr(date=timestamp, paymenthash=RHASH, net=constants.BitcoinTestnet, tags=[('f', 'mk2QpYatsKicvFVuTAQLBryyccRXMUaGHP'), ('h', longdescription)]),
"lntb1ps9zprzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqfpp3x9et2e20v6pu37c5d9vax37wxq72un98hp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsr9zktgu78k8p9t8555ve37qwfvqn6ga37fnfwhgexmf20nzdpmuhwvuv7zra3xrh8y2ggxxuemqfsgka9x7uzsrcx8rfv85c8pmhq9gq4sampn"),
])
# Roundtrip
for lnaddr1, invoice_str1 in tests:
- invoice_str2 = lnencode(lnaddr1, PRIVKEY)
+ invoice_str2 = encode_bolt11_invoice(lnaddr1, PRIVKEY)
self.assertEqual(invoice_str1, invoice_str2)
- lnaddr2 = lndecode(invoice_str2, net=lnaddr1.net)
+ lnaddr2 = decode_bolt11_invoice(invoice_str2, net=lnaddr1.net)
self.compare(lnaddr1, lnaddr2)
def test_n_decoding(self):
# We flip the signature recovery bit, which would normally give a different
# pubkey.
_, hrp, data = bech32_decode(
- lnencode(LnAddr(paymenthash=RHASH, payment_secret=PAYMENT_SECRET, amount=24, tags=[('d', ''), ('9', 33282)]), PRIVKEY),
+ encode_bolt11_invoice(BOLT11Addr(paymenthash=RHASH, payment_secret=PAYMENT_SECRET, amount=24, tags=[('d', ''), ('9', 33282)]), PRIVKEY),
ignore_long_length=True)
data[-1] ^= 1
- lnaddr = lndecode(bech32_encode(segwit_addr.Encoding.BECH32, hrp, data), verbose=True)
+ lnaddr = decode_bolt11_invoice(bech32_encode(segwit_addr.Encoding.BECH32, hrp, data), verbose=True)
self.assertNotEqual(lnaddr.pubkey.serialize(), PUBKEY)
# But not if we supply expliciy `n` specifier!
_, hrp, data = bech32_decode(
- lnencode(LnAddr(paymenthash=RHASH, payment_secret=PAYMENT_SECRET, amount=24, tags=[('d', ''), ('n', PUBKEY), ('9', 33282)]), PRIVKEY),
+ encode_bolt11_invoice(BOLT11Addr(paymenthash=RHASH, payment_secret=PAYMENT_SECRET, amount=24, tags=[('d', ''), ('n', PUBKEY), ('9', 33282)]), PRIVKEY),
ignore_long_length=True)
data[-1] ^= 1
- lnaddr = lndecode(bech32_encode(segwit_addr.Encoding.BECH32, hrp, data), verbose=True)
+ lnaddr = decode_bolt11_invoice(bech32_encode(segwit_addr.Encoding.BECH32, hrp, data), verbose=True)
self.assertEqual(lnaddr.pubkey.serialize(), PUBKEY)
def test_min_final_cltv_expiry_decoding(self):
- lnaddr = lndecode("lnsb500u1pdsgyf3pp5nmrqejdsdgs4n9ukgxcp2kcq265yhrxd4k5dyue58rxtp5y83s3qsp5qyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqsdqqcqzys9qypqsqp2h6a5xeytuc3fad2ed4gxvhd593lwjdna3dxsyeem0qkzjx6guk44jend0xq4zzvp6f3fy07wnmxezazzsxgmvqee8shxjuqu2eu0qpnvc95x",
- net=constants.BitcoinSimnet)
+ lnaddr = decode_bolt11_invoice("lnsb500u1pdsgyf3pp5nmrqejdsdgs4n9ukgxcp2kcq265yhrxd4k5dyue58rxtp5y83s3qsp5qyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqsdqqcqzys9qypqsqp2h6a5xeytuc3fad2ed4gxvhd593lwjdna3dxsyeem0qkzjx6guk44jend0xq4zzvp6f3fy07wnmxezazzsxgmvqee8shxjuqu2eu0qpnvc95x",
+ net=constants.BitcoinSimnet)
self.assertEqual(144, lnaddr.get_min_final_cltv_delta())
- lnaddr = lndecode("lntb15u1p0m6lzupp5zqjthgvaad9mewmdjuehwddyze9d8zyxcc43zhaddeegt37sndgsdq4xysyymr0vd4kzcmrd9hx7cqp7xqrrss9qy9qsqsp5vlhcs24hwm747w8f3uau2tlrdkvjaglffnsstwyamj84cxuhrn2s8tut3jqumepu42azyyjpgqa4w9w03204zp9h4clk499y2umstl6s29hqyj8vv4as6zt5567ux7l3f66m8pjhk65zjaq2esezk7ll2kcpljewkg",
- net=constants.BitcoinTestnet)
+ lnaddr = decode_bolt11_invoice("lntb15u1p0m6lzupp5zqjthgvaad9mewmdjuehwddyze9d8zyxcc43zhaddeegt37sndgsdq4xysyymr0vd4kzcmrd9hx7cqp7xqrrss9qy9qsqsp5vlhcs24hwm747w8f3uau2tlrdkvjaglffnsstwyamj84cxuhrn2s8tut3jqumepu42azyyjpgqa4w9w03204zp9h4clk499y2umstl6s29hqyj8vv4as6zt5567ux7l3f66m8pjhk65zjaq2esezk7ll2kcpljewkg",
+ net=constants.BitcoinTestnet)
self.assertEqual(30, lnaddr.get_min_final_cltv_delta())
def test_min_final_cltv_expiry_roundtrip(self):
for cltv in (1, 15, 16, 31, 32, 33, 150, 511, 512, 513, 1023, 1024, 1025):
- lnaddr = LnAddr(
+ lnaddr = BOLT11Addr(
paymenthash=RHASH, payment_secret=b"\x01"*32, amount=Decimal('0.001'), tags=[('d', '1 cup coffee'), ('x', 60), ('c', cltv), ('9', 33282)])
self.assertEqual(cltv, lnaddr.get_min_final_cltv_delta())
- invoice = lnencode(lnaddr, PRIVKEY)
- self.assertEqual(cltv, lndecode(invoice).get_min_final_cltv_delta())
+ invoice = encode_bolt11_invoice(lnaddr, PRIVKEY)
+ self.assertEqual(cltv, decode_bolt11_invoice(invoice).get_min_final_cltv_delta())
def test_features(self):
- lnaddr = lndecode("lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygsdq5vdhkven9v5sxyetpdees9qypqsztrz5v3jfnxskfv7g8chmyzyrfhf2vupcavuq5rce96kyt6g0zh337h206awccwp335zarqrud4wccgdn39vur44d8um4hmgv06aj0sgpdrv73z")
+ lnaddr = decode_bolt11_invoice("lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygsdq5vdhkven9v5sxyetpdees9qypqsztrz5v3jfnxskfv7g8chmyzyrfhf2vupcavuq5rce96kyt6g0zh337h206awccwp335zarqrud4wccgdn39vur44d8um4hmgv06aj0sgpdrv73z")
self.assertEqual(33282, lnaddr.get_tag('9'))
self.assertEqual(LnFeatures(33282), lnaddr.get_features())
def test_payment_secret(self):
- lnaddr = lndecode("lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygsdq5vdhkven9v5sxyetpdees9q5sqqqqqqqqqqqqqqqpqsqvvh7ut50r00p3pg34ea68k7zfw64f8yx9jcdk35lh5ft8qdr8g4r0xzsdcrmcy9hex8un8d8yraewvhqc9l0sh8l0e0yvmtxde2z0hgpzsje5l")
+ lnaddr = decode_bolt11_invoice("lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygsdq5vdhkven9v5sxyetpdees9q5sqqqqqqqqqqqqqqqpqsqvvh7ut50r00p3pg34ea68k7zfw64f8yx9jcdk35lh5ft8qdr8g4r0xzsdcrmcy9hex8un8d8yraewvhqc9l0sh8l0e0yvmtxde2z0hgpzsje5l")
self.assertEqual((1 << 9) + (1 << 15) + (1 << 99), lnaddr.get_tag('9'))
self.assertEqual(b"\x11" * 32, lnaddr.payment_secret)
def test_validate_and_compare_features(self):
- lnaddr = lndecode("lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygsdq5vdhkven9v5sxyetpdees9q5sqqqqqqqqqqqqqqqpqsqvvh7ut50r00p3pg34ea68k7zfw64f8yx9jcdk35lh5ft8qdr8g4r0xzsdcrmcy9hex8un8d8yraewvhqc9l0sh8l0e0yvmtxde2z0hgpzsje5l")
+ lnaddr = decode_bolt11_invoice("lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygsdq5vdhkven9v5sxyetpdees9q5sqqqqqqqqqqqqqqqpqsqvvh7ut50r00p3pg34ea68k7zfw64f8yx9jcdk35lh5ft8qdr8g4r0xzsdcrmcy9hex8un8d8yraewvhqc9l0sh8l0e0yvmtxde2z0hgpzsje5l")
lnaddr.validate_and_compare_features(LnFeatures((1 << 8) + (1 << 14) + (1 << 15)))
with self.assertRaises(IncompatibleLightningFeatures):
lnaddr.validate_and_compare_features(LnFeatures((1 << 8) + (1 << 14) + (1 << 16)))
@@ -188,7 +188,7 @@ class TestBolt11(ElectrumTestCase):
'16000000x0x717', 0, 1, 40),]
),
],
- LnAddr.format_bolt11_routing_info_as_human_readable(r_tags_expl, has_explicit_r_tagtype=True))
+ BOLT11Addr.format_bolt11_routing_info_as_human_readable(r_tags_expl, has_explicit_r_tagtype=True))
r_tags_impl = [
[(bfh('029e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255'), bfh('0102030405060708'), 1, 20, 3),
@@ -204,9 +204,9 @@ class TestBolt11(ElectrumTestCase):
[('038863cf8ab91046230f561cd5b386cbff8309fa02e3f0c3ed161a3aeb64a643b9',
'16000000x0x717', 0, 1, 40),],
],
- LnAddr.format_bolt11_routing_info_as_human_readable(r_tags_impl, has_explicit_r_tagtype=False))
+ BOLT11Addr.format_bolt11_routing_info_as_human_readable(r_tags_impl, has_explicit_r_tagtype=False))
for has_explicit_r_tagtype in (False, True):
self.assertEqual(
[],
- LnAddr.format_bolt11_routing_info_as_human_readable([], has_explicit_r_tagtype=has_explicit_r_tagtype))
+ BOLT11Addr.format_bolt11_routing_info_as_human_readable([], has_explicit_r_tagtype=has_explicit_r_tagtype))
diff --git a/tests/test_commands.py b/tests/test_commands.py
index 69b833b..f294c6d 100644
--- a/tests/test_commands.py
+++ b/tests/test_commands.py
@@ -20,7 +20,7 @@ from electrum.submarine_swaps import SwapOffer, SwapFees, NostrTransport
from electrum.transaction import Transaction, TxOutput, tx_from_any
from electrum.util import UserFacingException, NotEnoughFunds
from electrum.crypto import sha256
-from electrum.lnaddr import lndecode
+from electrum.bolt11 import decode_bolt11_invoice
from electrum.daemon import Daemon
from electrum import json_db
@@ -508,7 +508,7 @@ class TestCommandsTestnet(ElectrumTestCase):
expiry=3500,
wallet=wallet,
)
- invoice = lndecode(invoice=result['invoice'])
+ invoice = decode_bolt11_invoice(invoice=result['invoice'])
assert invoice.paymenthash.hex() == payment_hash
assert wallet.lnworker.get_payment_info(bytes.fromhex(payment_hash), direction=RECEIVED)
assert payment_hash in wallet.lnworker.dont_expire_htlcs
diff --git a/tests/test_lnpeer.py b/tests/test_lnpeer.py
index 6dc07e5..cf06af5 100644
--- a/tests/test_lnpeer.py
+++ b/tests/test_lnpeer.py
@@ -29,7 +29,7 @@ from electrum import constants
from electrum import bip32
from electrum.network import Network, ProxySettings
from electrum import simple_config, lnutil
-from electrum.lnaddr import lnencode, LnAddr, lndecode
+from electrum.bolt11 import encode_bolt11_invoice, BOLT11Addr, decode_bolt11_invoice
from electrum.bitcoin import COIN, sha256
from electrum.transaction import Transaction
from electrum.util import NetworkRetryManager, bfh, OldTaskGroup, EventListener, InvoiceError
@@ -179,7 +179,7 @@ class MockLNWallet(LNWallet):
self.channel_db.stop()
await self.channel_db.stopped_event.wait()
- async def create_routes_from_invoice(self, amount_msat: int, decoded_invoice: LnAddr, *, full_path=None):
+ async def create_routes_from_invoice(self, amount_msat: int, decoded_invoice: BOLT11Addr, *, full_path=None):
paysession = PaySession(
payment_hash=decoded_invoice.paymenthash,
payment_secret=decoded_invoice.payment_secret,
@@ -419,7 +419,7 @@ class TestPeer(ElectrumTestCase):
invoice_features: LnFeatures = None,
min_final_cltv_delta: int = None,
expiry: int = None,
- ) -> Tuple[LnAddr, Invoice]:
+ ) -> Tuple[BOLT11Addr, Invoice]:
amount_btc = amount_msat/Decimal(COIN*1000)
if payment_preimage is None and not payment_hash:
payment_preimage = os.urandom(32)
@@ -450,7 +450,7 @@ class TestPeer(ElectrumTestCase):
invoice_features=invoice_features,
)
w2.save_payment_info(info)
- lnaddr1 = LnAddr(
+ lnaddr1 = BOLT11Addr(
paymenthash=payment_hash,
amount=amount_btc,
tags=[
@@ -461,8 +461,8 @@ class TestPeer(ElectrumTestCase):
] + routing_hints,
payment_secret=payment_secret,
)
- invoice = lnencode(lnaddr1, w2.node_keypair.privkey)
- lnaddr2 = lndecode(invoice) # unlike lnaddr1, this now has a pubkey set
+ invoice = encode_bolt11_invoice(lnaddr1, w2.node_keypair.privkey)
+ lnaddr2 = decode_bolt11_invoice(invoice) # unlike lnaddr1, this now has a pubkey set
return lnaddr2, Invoice.from_bech32(invoice)
async def _activate_trampoline(self, w: MockLNWallet):
@@ -1052,7 +1052,7 @@ class TestPeerDirect(TestPeer):
self.assertEqual(PR_UNPAID, w2.get_payment_status(lnaddr.paymenthash, direction=RECEIVED))
assert lnaddr.get_min_final_cltv_delta() == 400 # what the receiver expects
lnaddr.tags = [tag for tag in lnaddr.tags if tag[0] != 'c'] + [['c', 144]]
- b11 = lnencode(lnaddr, w2.node_keypair.privkey)
+ b11 = encode_bolt11_invoice(lnaddr, w2.node_keypair.privkey)
pay_req = Invoice.from_bech32(b11)
assert pay_req._lnaddr.get_min_final_cltv_delta() == 144 # what w1 will use to pay
result, log = await w1.pay_invoice(pay_req)
@@ -1095,7 +1095,7 @@ class TestPeerDirect(TestPeer):
# create lightning invoice in the past, so it is expired
with mock.patch('time.time', return_value=int(time.time()) - 10000):
lnaddr, _pay_req = self.prepare_invoice(w2, expiry=3600)
- b11 = lnencode(lnaddr, w2.node_keypair.privkey)
+ b11 = encode_bolt11_invoice(lnaddr, w2.node_keypair.privkey)
pay_req = Invoice.from_bech32(b11)
async def try_pay_expired_invoice(pay_req: Invoice, w1=w1):
@@ -2044,7 +2044,7 @@ class TestPeerDirect(TestPeer):
}
# create 10 invoices (10 pending htlc sets with 1 htlc each)
- invoices = [] # type: list[tuple[LnAddr, Invoice]]
+ invoices = [] # type: list[tuple[BOLT11Addr, Invoice]]
for i in range(10):
lnaddr, pay_req = self.prepare_invoice(bob_w)
# prevent bob from settling so that htlc switch will have to iterate through all pending htlcs
Why this scored 15/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.