Browse Source

lnaddr: clean-up SEGWIT_HRP vs BOLT11_HRP confusion

With signet, SEGWIT_HRP != BOLT11_HRP, so the previous "currency" string
became a flawed concept. Instead we pass around net objects now.
patch-4
SomberNight 4 years ago
parent
commit
57e52da77f
No known key found for this signature in database GPG Key ID: B33B5F232C6271E9
  1. 16
      electrum/constants.py
  2. 2
      electrum/gui/kivy/uix/screens.py
  3. 4
      electrum/gui/qt/main_window.py
  4. 114
      electrum/lnaddr.py
  5. 2
      electrum/lnworker.py
  6. 9
      electrum/tests/test_bolt11.py
  7. 10
      electrum/util.py

16
electrum/constants.py

@ -26,7 +26,7 @@
import os import os
import json import json
from .util import inv_dict from .util import inv_dict, all_subclasses
from . import bitcoin from . import bitcoin
@ -47,7 +47,17 @@ BIP39_WALLET_FORMATS = read_json('bip39_wallet_formats.json', [])
class AbstractNet: class AbstractNet:
BLOCK_HEIGHT_FIRST_LIGHTNING_CHANNELS = 0 NET_NAME: str
TESTNET: bool
WIF_PREFIX: int
ADDRTYPE_P2PKH: int
ADDRTYPE_P2SH: int
SEGWIT_HRP: str
BOLT11_HRP: str
GENESIS: str
BLOCK_HEIGHT_FIRST_LIGHTNING_CHANNELS: int = 0
BIP44_COIN_TYPE: int
LN_REALM_BYTE: int
@classmethod @classmethod
def max_checkpoint(cls) -> int: def max_checkpoint(cls) -> int:
@ -171,6 +181,8 @@ class BitcoinSignet(BitcoinTestnet):
LN_DNS_SEEDS = [] LN_DNS_SEEDS = []
NETS_LIST = tuple(all_subclasses(AbstractNet))
# don't import net directly, import the module instead (so that net is singleton) # don't import net directly, import the module instead (so that net is singleton)
net = BitcoinMainnet net = BitcoinMainnet

2
electrum/gui/kivy/uix/screens.py

@ -187,7 +187,7 @@ class SendScreen(CScreen, Logger):
def set_ln_invoice(self, invoice: str): def set_ln_invoice(self, invoice: str):
try: try:
invoice = str(invoice).lower() invoice = str(invoice).lower()
lnaddr = lndecode(invoice, expected_hrp=constants.net.SEGWIT_HRP) lnaddr = lndecode(invoice)
except Exception as e: except Exception as e:
self.app.show_info(invoice + _(" is not a valid Lightning invoice: ") + repr(e)) # repr because str(Exception()) == '' self.app.show_info(invoice + _(" is not a valid Lightning invoice: ") + repr(e)) # repr because str(Exception()) == ''
return return

4
electrum/gui/qt/main_window.py

@ -1969,7 +1969,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
def parse_lightning_invoice(self, invoice): def parse_lightning_invoice(self, invoice):
"""Parse ln invoice, and prepare the send tab for it.""" """Parse ln invoice, and prepare the send tab for it."""
try: try:
lnaddr = lndecode(invoice, expected_hrp=constants.net.SEGWIT_HRP) lnaddr = lndecode(invoice)
except Exception as e: except Exception as e:
raise LnDecodeException(e) from e raise LnDecodeException(e) from e
pubkey = bh2u(lnaddr.pubkey.serialize()) pubkey = bh2u(lnaddr.pubkey.serialize())
@ -2180,7 +2180,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
d.exec_() d.exec_()
def show_lightning_invoice(self, invoice: LNInvoice): def show_lightning_invoice(self, invoice: LNInvoice):
lnaddr = lndecode(invoice.invoice, expected_hrp=constants.net.SEGWIT_HRP) lnaddr = lndecode(invoice.invoice)
d = WindowModalDialog(self, _("Lightning Invoice")) d = WindowModalDialog(self, _("Lightning Invoice"))
vbox = QVBoxLayout(d) vbox = QVBoxLayout(d)
grid = QGridLayout() grid = QGridLayout()

114
electrum/lnaddr.py

@ -6,7 +6,7 @@ import time
from hashlib import sha256 from hashlib import sha256
from binascii import hexlify from binascii import hexlify
from decimal import Decimal from decimal import Decimal
from typing import Optional, TYPE_CHECKING from typing import Optional, TYPE_CHECKING, Type
import random import random
import bitstring import bitstring
@ -15,6 +15,7 @@ from .bitcoin import hash160_to_b58_address, b58_address_to_hash160, TOTAL_COIN_
from .segwit_addr import bech32_encode, bech32_decode, CHARSET from .segwit_addr import bech32_encode, bech32_decode, CHARSET
from . import segwit_addr from . import segwit_addr
from . import constants from . import constants
from .constants import AbstractNet
from . import ecc from . import ecc
from .bitcoin import COIN from .bitcoin import COIN
@ -83,57 +84,42 @@ def bitarray_to_u5(barr):
return ret return ret
def encode_fallback(fallback: str, currency): def encode_fallback(fallback: str, net: Type[AbstractNet]):
""" Encode all supported fallback addresses. """ Encode all supported fallback addresses.
""" """
if currency in [constants.BitcoinMainnet.SEGWIT_HRP, constants.BitcoinTestnet.SEGWIT_HRP]: wver, wprog_ints = segwit_addr.decode_segwit_address(net.SEGWIT_HRP, fallback)
wver, wprog_ints = segwit_addr.decode_segwit_address(currency, fallback) if wver is not None:
if wver is not None: wprog = bytes(wprog_ints)
wprog = bytes(wprog_ints)
else:
addrtype, addr = b58_address_to_hash160(fallback)
if is_p2pkh(currency, addrtype):
wver = 17
elif is_p2sh(currency, addrtype):
wver = 18
else:
raise ValueError("Unknown address type for {}".format(currency))
wprog = addr
return tagged('f', bitstring.pack("uint:5", wver) + wprog)
else: else:
raise NotImplementedError("Support for currency {} not implemented".format(currency)) addrtype, addr = b58_address_to_hash160(fallback)
if addrtype == net.ADDRTYPE_P2PKH:
wver = 17
def parse_fallback(fallback, currency): elif addrtype == net.ADDRTYPE_P2SH:
if currency in [constants.BitcoinMainnet.SEGWIT_HRP, constants.BitcoinTestnet.SEGWIT_HRP]: wver = 18
wver = fallback[0:5].uint
if wver == 17:
addr=hash160_to_b58_address(fallback[5:].tobytes(), base58_prefix_map[currency][0])
elif wver == 18:
addr=hash160_to_b58_address(fallback[5:].tobytes(), base58_prefix_map[currency][1])
elif wver <= 16:
witprog = fallback[5:] # cut witver
witprog = witprog[:len(witprog) // 8 * 8] # can only be full bytes
witprog = witprog.tobytes()
addr = segwit_addr.encode_segwit_address(currency, wver, witprog)
else: else:
return None raise ValueError(f"Unknown address type {addrtype} for {net}")
wprog = addr
return tagged('f', bitstring.pack("uint:5", wver) + wprog)
def parse_fallback(fallback, net: Type[AbstractNet]):
wver = fallback[0:5].uint
if wver == 17:
addr = hash160_to_b58_address(fallback[5:].tobytes(), net.ADDRTYPE_P2PKH)
elif wver == 18:
addr = hash160_to_b58_address(fallback[5:].tobytes(), net.ADDRTYPE_P2SH)
elif wver <= 16:
witprog = fallback[5:] # cut witver
witprog = witprog[:len(witprog) // 8 * 8] # can only be full bytes
witprog = witprog.tobytes()
addr = segwit_addr.encode_segwit_address(net.SEGWIT_HRP, wver, witprog)
else: else:
addr=fallback.tobytes() return None
return addr return addr
# Map of classical and witness address prefixes BOLT11_HRP_INV_DICT = {net.BOLT11_HRP: net for net in constants.NETS_LIST}
base58_prefix_map = {
constants.BitcoinMainnet.SEGWIT_HRP : (constants.BitcoinMainnet.ADDRTYPE_P2PKH, constants.BitcoinMainnet.ADDRTYPE_P2SH),
constants.BitcoinTestnet.SEGWIT_HRP : (constants.BitcoinTestnet.ADDRTYPE_P2PKH, constants.BitcoinTestnet.ADDRTYPE_P2SH)
}
def is_p2pkh(currency, prefix):
return prefix == base58_prefix_map[currency][0]
def is_p2sh(currency, prefix):
return prefix == base58_prefix_map[currency][1]
# Tagged field containing BitArray # Tagged field containing BitArray
def tagged(char, l): def tagged(char, l):
@ -178,13 +164,10 @@ def pull_tagged(stream):
return (CHARSET[tag], stream.read(length * 5), stream) return (CHARSET[tag], stream.read(length * 5), stream)
def lnencode(addr: 'LnAddr', privkey) -> str: def lnencode(addr: 'LnAddr', privkey) -> str:
# see https://github.com/lightningnetwork/lightning-rfc/pull/844
if constants.net.NET_NAME == "signet":
addr.currency = constants.net.BOLT11_HRP
if addr.amount: if addr.amount:
amount = addr.currency + shorten_amount(addr.amount) amount = addr.net.BOLT11_HRP + shorten_amount(addr.amount)
else: else:
amount = addr.currency if addr.currency else '' amount = addr.net.BOLT11_HRP if addr.net else ''
hrp = 'ln' + amount hrp = 'ln' + amount
@ -221,7 +204,7 @@ def lnencode(addr: 'LnAddr', privkey) -> str:
route = bitstring.BitArray(pubkey) + bitstring.pack('intbe:32', feebase) + bitstring.pack('intbe:32', feerate) + bitstring.pack('intbe:16', cltv) route = bitstring.BitArray(pubkey) + bitstring.pack('intbe:32', feebase) + bitstring.pack('intbe:32', feerate) + bitstring.pack('intbe:16', cltv)
data += tagged('t', route) data += tagged('t', route)
elif k == 'f': elif k == 'f':
data += encode_fallback(v, addr.currency) data += encode_fallback(v, addr.net)
elif k == 'd': elif k == 'd':
# truncate to max length: 1024*5 bits = 639 bytes # truncate to max length: 1024*5 bits = 639 bytes
data += tagged_bytes('d', v.encode()[0:639]) data += tagged_bytes('d', v.encode()[0:639])
@ -270,7 +253,7 @@ def lnencode(addr: 'LnAddr', privkey) -> str:
class LnAddr(object): class LnAddr(object):
def __init__(self, *, paymenthash: bytes = None, amount=None, currency=None, tags=None, date=None, def __init__(self, *, paymenthash: bytes = None, amount=None, net: Type[AbstractNet] = None, tags=None, date=None,
payment_secret: bytes = None): payment_secret: bytes = None):
self.date = int(time.time()) if not date else int(date) self.date = int(time.time()) if not date else int(date)
self.tags = [] if not tags else tags self.tags = [] if not tags else tags
@ -279,7 +262,7 @@ class LnAddr(object):
self.payment_secret = payment_secret self.payment_secret = payment_secret
self.signature = None self.signature = None
self.pubkey = None self.pubkey = None
self.currency = constants.net.SEGWIT_HRP if currency is None else currency self.net = constants.net if net is None else net # type: Type[AbstractNet]
self._amount = amount # type: Optional[Decimal] # in bitcoins self._amount = amount # type: Optional[Decimal] # in bitcoins
self._min_final_cltv_expiry = 18 self._min_final_cltv_expiry = 18
@ -330,7 +313,7 @@ class LnAddr(object):
def __str__(self): def __str__(self):
return "LnAddr[{}, amount={}{} tags=[{}]]".format( return "LnAddr[{}, amount={}{} tags=[{}]]".format(
hexlify(self.pubkey.serialize()).decode('utf-8') if self.pubkey else None, hexlify(self.pubkey.serialize()).decode('utf-8') if self.pubkey else None,
self.amount, self.currency, self.amount, self.net.BOLT11_HRP,
", ".join([k + '=' + str(v) for k, v in self.tags]) ", ".join([k + '=' + str(v) for k, v in self.tags])
) )
@ -367,12 +350,9 @@ class SerializableKey:
def serialize(self): def serialize(self):
return self.pubkey.get_public_key_bytes(True) return self.pubkey.get_public_key_bytes(True)
def lndecode(invoice: str, *, verbose=False, expected_hrp=None) -> LnAddr: def lndecode(invoice: str, *, verbose=False, net=None) -> LnAddr:
if expected_hrp is None: if net is None:
expected_hrp = constants.net.SEGWIT_HRP net = constants.net
# see https://github.com/lightningnetwork/lightning-rfc/pull/844
if constants.net.NET_NAME == "signet":
expected_hrp = constants.net.BOLT11_HRP
decoded_bech32 = bech32_decode(invoice, ignore_long_length=True) decoded_bech32 = bech32_decode(invoice, ignore_long_length=True)
hrp = decoded_bech32.hrp hrp = decoded_bech32.hrp
data = decoded_bech32.data data = decoded_bech32.data
@ -387,8 +367,8 @@ def lndecode(invoice: str, *, verbose=False, expected_hrp=None) -> LnAddr:
if not hrp.startswith('ln'): if not hrp.startswith('ln'):
raise ValueError("Does not start with ln") raise ValueError("Does not start with ln")
if not hrp[2:].startswith(expected_hrp): if not hrp[2:].startswith(net.BOLT11_HRP):
raise ValueError("Wrong Lightning invoice HRP " + hrp[2:] + ", should be " + expected_hrp) raise ValueError(f"Wrong Lightning invoice HRP {hrp[2:]}, should be {net.BOLT11_HRP}")
data = u5_to_bitarray(data) data = u5_to_bitarray(data)
@ -403,7 +383,7 @@ def lndecode(invoice: str, *, verbose=False, expected_hrp=None) -> LnAddr:
m = re.search("[^\\d]+", hrp[2:]) m = re.search("[^\\d]+", hrp[2:])
if m: if m:
addr.currency = m.group(0) addr.net = BOLT11_HRP_INV_DICT[m.group(0)]
amountstr = hrp[2+m.end():] amountstr = hrp[2+m.end():]
# BOLT #11: # BOLT #11:
# #
@ -453,7 +433,7 @@ def lndecode(invoice: str, *, verbose=False, expected_hrp=None) -> LnAddr:
s.read(16).uintbe) s.read(16).uintbe)
addr.tags.append(('t', e)) addr.tags.append(('t', e))
elif tag == 'f': elif tag == 'f':
fallback = parse_fallback(tagdata, addr.currency) fallback = parse_fallback(tagdata, addr.net)
if fallback: if fallback:
addr.tags.append(('f', fallback)) addr.tags.append(('f', fallback))
else: else:
@ -532,13 +512,3 @@ def lndecode(invoice: str, *, verbose=False, expected_hrp=None) -> LnAddr:
addr.pubkey = SerializableKey(ecc.ECPubkey.from_sig_string(sigdecoded[:64], sigdecoded[64], hrp_hash)) addr.pubkey = SerializableKey(ecc.ECPubkey.from_sig_string(sigdecoded[:64], sigdecoded[64], hrp_hash))
return addr return addr
if __name__ == '__main__':
# run using
# python3 -m electrum.lnaddr <invoice> <expected hrp>
# python3 -m electrum.lnaddr lntb1n1pdlcakepp5e7rn0knl0gm46qqp9eqdsza2c942d8pjqnwa5903n39zu28sgk3sdq423jhxapqv3hkuct5d9hkucqp2rzjqwyx8nu2hygyvgc02cwdtvuxe0lcxz06qt3lpsldzcdr46my5epmj9vk9sqqqlcqqqqqqqlgqqqqqqgqjqdhnmkgahfaynuhe9md8k49xhxuatnv6jckfmsjq8maxta2l0trh5sdrqlyjlwutdnpd5gwmdnyytsl9q0dj6g08jacvthtpeg383k0sq542rz2 tb1n
import sys
print(lndecode(sys.argv[1], expected_hrp=sys.argv[2]))

2
electrum/lnworker.py

@ -1398,7 +1398,7 @@ class LNWallet(LNWorker):
@staticmethod @staticmethod
def _check_invoice(invoice: str, *, amount_msat: int = None) -> LnAddr: def _check_invoice(invoice: str, *, amount_msat: int = None) -> LnAddr:
addr = lndecode(invoice, expected_hrp=constants.net.SEGWIT_HRP) addr = lndecode(invoice)
if addr.is_expired(): if addr.is_expired():
raise InvoiceError(_("This invoice has expired")) raise InvoiceError(_("This invoice has expired"))
if amount_msat: # replace amt in invoice. main usecase is paying zero amt invoices if amount_msat: # replace amt in invoice. main usecase is paying zero amt invoices

9
electrum/tests/test_bolt11.py

@ -8,6 +8,7 @@ from electrum.lnaddr import shorten_amount, unshorten_amount, LnAddr, lnencode,
from electrum.segwit_addr import bech32_encode, bech32_decode from electrum.segwit_addr import bech32_encode, bech32_decode
from electrum import segwit_addr from electrum import segwit_addr
from electrum.lnutil import UnknownEvenFeatureBits, derive_payment_secret_from_payment_preimage, LnFeatures from electrum.lnutil import UnknownEvenFeatureBits, derive_payment_secret_from_payment_preimage, LnFeatures
from electrum import constants
from . import ElectrumTestCase from . import ElectrumTestCase
@ -69,7 +70,7 @@ class TestBolt11(ElectrumTestCase):
"lnbc1m1ps9zprzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpu9rflz25dx0qw6kdg05u0c5hdc30yq6ga6ew4pz86n244va45nchns9zrs3wjxznsqnt37hz7pswvc56wvuhxcjyd6k3lqf4ujynyxuspmvr078"), "lnbc1m1ps9zprzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpu9rflz25dx0qw6kdg05u0c5hdc30yq6ga6ew4pz86n244va45nchns9zrs3wjxznsqnt37hz7pswvc56wvuhxcjyd6k3lqf4ujynyxuspmvr078"),
(LnAddr(date=timestamp, paymenthash=RHASH, amount=Decimal('1'), tags=[('h', longdescription)]), (LnAddr(date=timestamp, paymenthash=RHASH, amount=Decimal('1'), tags=[('h', longdescription)]),
"lnbc11ps9zprzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqs2qjafckq94q3js6lvqz2kmenn9ysjejyj8fm4hlx0xtqhaxfzlxjappkgp0hmm40dnuan4v3jy83lqjup2n0fdzgysg049y9l9uc98qq07kfd3"), "lnbc11ps9zprzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqs2qjafckq94q3js6lvqz2kmenn9ysjejyj8fm4hlx0xtqhaxfzlxjappkgp0hmm40dnuan4v3jy83lqjup2n0fdzgysg049y9l9uc98qq07kfd3"),
(LnAddr(date=timestamp, paymenthash=RHASH, currency='tb', tags=[('f', 'mk2QpYatsKicvFVuTAQLBryyccRXMUaGHP'), ('h', longdescription)]), (LnAddr(date=timestamp, paymenthash=RHASH, net=constants.BitcoinTestnet, tags=[('f', 'mk2QpYatsKicvFVuTAQLBryyccRXMUaGHP'), ('h', longdescription)]),
"lntb1ps9zprzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqfpp3x9et2e20v6pu37c5d9vax37wxq72un98hp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsr9zktgu78k8p9t8555ve37qwfvqn6ga37fnfwhgexmf20nzdpmuhwvuv7zra3xrh8y2ggxxuemqfsgka9x7uzsrcx8rfv85c8pmhq9gq4sampn"), "lntb1ps9zprzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqfpp3x9et2e20v6pu37c5d9vax37wxq72un98hp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsr9zktgu78k8p9t8555ve37qwfvqn6ga37fnfwhgexmf20nzdpmuhwvuv7zra3xrh8y2ggxxuemqfsgka9x7uzsrcx8rfv85c8pmhq9gq4sampn"),
(LnAddr(date=timestamp, paymenthash=RHASH, amount=24, tags=[ (LnAddr(date=timestamp, paymenthash=RHASH, amount=24, tags=[
('r', [(unhexlify('029e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255'), unhexlify('0102030405060708'), 1, 20, 3), ('r', [(unhexlify('029e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255'), unhexlify('0102030405060708'), 1, 20, 3),
@ -109,7 +110,7 @@ class TestBolt11(ElectrumTestCase):
for lnaddr1, invoice_str1 in tests: for lnaddr1, invoice_str1 in tests:
invoice_str2 = lnencode(lnaddr1, PRIVKEY) invoice_str2 = lnencode(lnaddr1, PRIVKEY)
self.assertEqual(invoice_str1, invoice_str2) self.assertEqual(invoice_str1, invoice_str2)
lnaddr2 = lndecode(invoice_str2, expected_hrp=lnaddr1.currency) lnaddr2 = lndecode(invoice_str2, net=lnaddr1.net)
self.compare(lnaddr1, lnaddr2) self.compare(lnaddr1, lnaddr2)
def test_n_decoding(self): def test_n_decoding(self):
@ -134,11 +135,11 @@ class TestBolt11(ElectrumTestCase):
def test_min_final_cltv_expiry_decoding(self): def test_min_final_cltv_expiry_decoding(self):
lnaddr = lndecode("lnsb500u1pdsgyf3pp5nmrqejdsdgs4n9ukgxcp2kcq265yhrxd4k5dyue58rxtp5y83s3qdqqcqzystrggccm9yvkr5yqx83jxll0qjpmgfg9ywmcd8g33msfgmqgyfyvqhku80qmqm8q6v35zvck2y5ccxsz5avtrauz8hgjj3uahppyq20qp6dvwxe", lnaddr = lndecode("lnsb500u1pdsgyf3pp5nmrqejdsdgs4n9ukgxcp2kcq265yhrxd4k5dyue58rxtp5y83s3qdqqcqzystrggccm9yvkr5yqx83jxll0qjpmgfg9ywmcd8g33msfgmqgyfyvqhku80qmqm8q6v35zvck2y5ccxsz5avtrauz8hgjj3uahppyq20qp6dvwxe",
expected_hrp="sb") net=constants.BitcoinSimnet)
self.assertEqual(144, lnaddr.get_min_final_cltv_expiry()) self.assertEqual(144, lnaddr.get_min_final_cltv_expiry())
lnaddr = lndecode("lntb15u1p0m6lzupp5zqjthgvaad9mewmdjuehwddyze9d8zyxcc43zhaddeegt37sndgsdq4xysyymr0vd4kzcmrd9hx7cqp7xqrrss9qy9qsqsp5vlhcs24hwm747w8f3uau2tlrdkvjaglffnsstwyamj84cxuhrn2s8tut3jqumepu42azyyjpgqa4w9w03204zp9h4clk499y2umstl6s29hqyj8vv4as6zt5567ux7l3f66m8pjhk65zjaq2esezk7ll2kcpljewkg", lnaddr = lndecode("lntb15u1p0m6lzupp5zqjthgvaad9mewmdjuehwddyze9d8zyxcc43zhaddeegt37sndgsdq4xysyymr0vd4kzcmrd9hx7cqp7xqrrss9qy9qsqsp5vlhcs24hwm747w8f3uau2tlrdkvjaglffnsstwyamj84cxuhrn2s8tut3jqumepu42azyyjpgqa4w9w03204zp9h4clk499y2umstl6s29hqyj8vv4as6zt5567ux7l3f66m8pjhk65zjaq2esezk7ll2kcpljewkg",
expected_hrp="tb") net=constants.BitcoinTestnet)
self.assertEqual(30, lnaddr.get_min_final_cltv_expiry()) self.assertEqual(30, lnaddr.get_min_final_cltv_expiry())
def test_min_final_cltv_expiry_roundtrip(self): def test_min_final_cltv_expiry_roundtrip(self):

10
electrum/util.py

@ -24,7 +24,7 @@ import binascii
import os, sys, re, json import os, sys, re, json
from collections import defaultdict, OrderedDict from collections import defaultdict, OrderedDict
from typing import (NamedTuple, Union, TYPE_CHECKING, Tuple, Optional, Callable, Any, from typing import (NamedTuple, Union, TYPE_CHECKING, Tuple, Optional, Callable, Any,
Sequence, Dict, Generic, TypeVar, List, Iterable) Sequence, Dict, Generic, TypeVar, List, Iterable, Set)
from datetime import datetime from datetime import datetime
import decimal import decimal
from decimal import Decimal from decimal import Decimal
@ -72,6 +72,14 @@ def inv_dict(d):
return {v: k for k, v in d.items()} return {v: k for k, v in d.items()}
def all_subclasses(cls) -> Set:
"""Return all (transitive) subclasses of cls."""
res = set(cls.__subclasses__())
for sub in res.copy():
res |= all_subclasses(sub)
return res
ca_path = certifi.where() ca_path = certifi.where()

Loading…
Cancel
Save