URI: 
       tlightning: kivy: open channel button in invoice - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
   DIR commit afa5797099cff6e448f07596a6c501a9e67fe208
   DIR parent 1ab03e8b2a5745b8aa999ceea14b11ff4b0b97ff
  HTML Author: Janus <ysangkok@gmail.com>
       Date:   Wed, 28 Mar 2018 15:41:51 +0200
       
       lightning: kivy: open channel button in invoice
       
       Diffstat:
         M gui/kivy/uix/dialogs/lightning_pay… |      17 +++++++++++++++--
         A lib/lightning_payencode/FORKED      |       1 +
         A lib/lightning_payencode/bech32.py   |     123 +++++++++++++++++++++++++++++++
         A lib/lightning_payencode/lnaddr.py   |     385 +++++++++++++++++++++++++++++++
       
       4 files changed, 524 insertions(+), 2 deletions(-)
       ---
   DIR diff --git a/gui/kivy/uix/dialogs/lightning_payer.py b/gui/kivy/uix/dialogs/lightning_payer.py
       t@@ -1,7 +1,9 @@
       +import binascii
        from kivy.lang import Builder
        from kivy.factory import Factory
        from electrum_gui.kivy.i18n import _
        import electrum.lightning as lightning
       +from electrum.lightning_payencode.lnaddr import lndecode
        
        Builder.load_string('''
        <LightningPayerDialog@Popup>
       t@@ -34,6 +36,11 @@ Builder.load_string('''
                Button:
                    size_hint: 1, None
                    height: '48dp'
       +            text: _('Open channel to pubkey in invoice')
       +            on_release: s.do_open_channel()
       +        Button:
       +            size_hint: 1, None
       +            height: '48dp'
                    text: _('Pay pasted/scanned invoice')
                    on_release: s.do_pay()
        ''')
       t@@ -63,7 +70,13 @@ class LightningPayerDialog(Factory.Popup):
                self.invoice_data = contents
            def do_clear(self):
                self.invoice_data = ""
       +    def do_open_channel(self):
       +        compressed_pubkey_bytes = lndecode(self.invoice_data).pubkey.serialize()
       +        hexpubkey = binascii.hexlify(compressed_pubkey_bytes).decode("ascii")
       +        local_amt = 100000
       +        push_amt = 0
       +        lightning.lightningCall(self.app.wallet.network.lightningrpc, "openchannel")(hexpubkey, local_amt, push_amt)
            def do_pay(self):
                lightning.lightningCall(self.app.wallet.network.lightningrpc, "sendpayment")("--pay_req=" + self.invoice_data)
       -    def on_lightning_qr(self):
       -        self.app.show_info("Lightning Invoice QR scanning not implemented") #TODO
       +    def on_lightning_qr(self, data):
       +        self.invoice_data = str(data)
   DIR diff --git a/lib/lightning_payencode/FORKED b/lib/lightning_payencode/FORKED
       t@@ -0,0 +1 @@
       +This was forked from https://github.com/rustyrussell/lightning-payencode/tree/acc16ec13a3fa1dc16c07af6ec67c261bd8aff23
   DIR diff --git a/lib/lightning_payencode/bech32.py b/lib/lightning_payencode/bech32.py
       t@@ -0,0 +1,123 @@
       +# Copyright (c) 2017 Pieter Wuille
       +#
       +# Permission is hereby granted, free of charge, to any person obtaining a copy
       +# of this software and associated documentation files (the "Software"), to deal
       +# in the Software without restriction, including without limitation the rights
       +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
       +# copies of the Software, and to permit persons to whom the Software is
       +# furnished to do so, subject to the following conditions:
       +#
       +# The above copyright notice and this permission notice shall be included in
       +# all copies or substantial portions of the Software.
       +#
       +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
       +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
       +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
       +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
       +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
       +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
       +# THE SOFTWARE.
       +
       +"""Reference implementation for Bech32 and segwit addresses."""
       +
       +
       +CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
       +
       +
       +def bech32_polymod(values):
       +    """Internal function that computes the Bech32 checksum."""
       +    generator = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3]
       +    chk = 1
       +    for value in values:
       +        top = chk >> 25
       +        chk = (chk & 0x1ffffff) << 5 ^ value
       +        for i in range(5):
       +            chk ^= generator[i] if ((top >> i) & 1) else 0
       +    return chk
       +
       +
       +def bech32_hrp_expand(hrp):
       +    """Expand the HRP into values for checksum computation."""
       +    return [ord(x) >> 5 for x in hrp] + [0] + [ord(x) & 31 for x in hrp]
       +
       +
       +def bech32_verify_checksum(hrp, data):
       +    """Verify a checksum given HRP and converted data characters."""
       +    return bech32_polymod(bech32_hrp_expand(hrp) + data) == 1
       +
       +
       +def bech32_create_checksum(hrp, data):
       +    """Compute the checksum values given HRP and data."""
       +    values = bech32_hrp_expand(hrp) + data
       +    polymod = bech32_polymod(values + [0, 0, 0, 0, 0, 0]) ^ 1
       +    return [(polymod >> 5 * (5 - i)) & 31 for i in range(6)]
       +
       +
       +def bech32_encode(hrp, data):
       +    """Compute a Bech32 string given HRP and data values."""
       +    combined = data + bech32_create_checksum(hrp, data)
       +    return hrp + '1' + ''.join([CHARSET[d] for d in combined])
       +
       +
       +def bech32_decode(bech):
       +    """Validate a Bech32 string, and determine HRP and data."""
       +    if ((any(ord(x) < 33 or ord(x) > 126 for x in bech)) or
       +            (bech.lower() != bech and bech.upper() != bech)):
       +        return (None, None)
       +    bech = bech.lower()
       +    pos = bech.rfind('1')
       +    if pos < 1 or pos + 7 > len(bech): #or len(bech) > 90:
       +        return (None, None)
       +    if not all(x in CHARSET for x in bech[pos+1:]):
       +        return (None, None)
       +    hrp = bech[:pos]
       +    data = [CHARSET.find(x) for x in bech[pos+1:]]
       +    if not bech32_verify_checksum(hrp, data):
       +        return (None, None)
       +    return (hrp, data[:-6])
       +
       +
       +def convertbits(data, frombits, tobits, pad=True):
       +    """General power-of-2 base conversion."""
       +    acc = 0
       +    bits = 0
       +    ret = []
       +    maxv = (1 << tobits) - 1
       +    max_acc = (1 << (frombits + tobits - 1)) - 1
       +    for value in data:
       +        if value < 0 or (value >> frombits):
       +            return None
       +        acc = ((acc << frombits) | value) & max_acc
       +        bits += frombits
       +        while bits >= tobits:
       +            bits -= tobits
       +            ret.append((acc >> bits) & maxv)
       +    if pad:
       +        if bits:
       +            ret.append((acc << (tobits - bits)) & maxv)
       +    elif bits >= frombits or ((acc << (tobits - bits)) & maxv):
       +        return None
       +    return ret
       +
       +
       +def decode(hrp, addr):
       +    """Decode a segwit address."""
       +    hrpgot, data = bech32_decode(addr)
       +    if hrpgot != hrp:
       +        return (None, None)
       +    decoded = convertbits(data[1:], 5, 8, False)
       +    if decoded is None or len(decoded) < 2 or len(decoded) > 40:
       +        return (None, None)
       +    if data[0] > 16:
       +        return (None, None)
       +    if data[0] == 0 and len(decoded) != 20 and len(decoded) != 32:
       +        return (None, None)
       +    return (data[0], decoded)
       +
       +
       +def encode(hrp, witver, witprog):
       +    """Encode a segwit address."""
       +    ret = bech32_encode(hrp, [witver] + convertbits(witprog, 8, 5))
       +    assert decode(hrp, ret) is not (None, None)
       +    return ret
       +
   DIR diff --git a/lib/lightning_payencode/lnaddr.py b/lib/lightning_payencode/lnaddr.py
       t@@ -0,0 +1,385 @@
       +#! /usr/bin/env python3
       +import traceback
       +import ecdsa.curves
       +from ..bitcoin import MyVerifyingKey, GetPubKey
       +from .bech32 import bech32_encode, bech32_decode, CHARSET
       +from binascii import hexlify, unhexlify
       +from bitstring import BitArray
       +from decimal import Decimal
       +
       +import bitstring
       +import hashlib
       +import math
       +import re
       +import sys
       +import time
       +
       +
       +# 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
       +    return str(amount) + unit
       +
       +def unshorten_amount(amount):
       +    """ 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 ValueError("Invalid amount '{}'".format(amount))
       +
       +    if unit in units.keys():
       +        return Decimal(amount[:-1]) / units[unit]
       +    else:
       +        return Decimal(amount)
       +
       +# Bech32 spits out array of 5-bit values.  Shim here.
       +def u5_to_bitarray(arr):
       +    ret = bitstring.BitArray()
       +    for a in arr:
       +        ret += bitstring.pack("uint:5", a)
       +    return ret
       +
       +def bitarray_to_u5(barr):
       +    assert barr.len % 5 == 0
       +    ret = []
       +    s = bitstring.ConstBitStream(barr)
       +    while s.pos != s.len:
       +        ret.append(s.read(5).uint)
       +    return ret
       +
       +def encode_fallback(fallback, currency):
       +    """ Encode all supported fallback addresses.
       +    """
       +    if currency == 'bc' or currency == 'tb':
       +        fbhrp, witness = bech32_decode(fallback)
       +        if fbhrp:
       +            if fbhrp != currency:
       +                raise ValueError("Not a bech32 address for this currency")
       +            wver = witness[0]
       +            if wver > 16:
       +                raise ValueError("Invalid witness version {}".format(witness[0]))
       +            wprog = u5_to_bitarray(witness[1:])
       +        else:
       +            addr = base58.b58decode_check(fallback)
       +            if is_p2pkh(currency, addr[0]):
       +                wver = 17
       +            elif is_p2sh(currency, addr[0]):
       +                wver = 18
       +            else:
       +                raise ValueError("Unknown address type for {}".format(currency))
       +            wprog = addr[1:]
       +        return tagged('f', bitstring.pack("uint:5", wver) + wprog)
       +    else:
       +        raise NotImplementedError("Support for currency {} not implemented".format(currency))
       +
       +def parse_fallback(fallback, currency):
       +    return None # this function disabled by Janus to avoid base58 dependency
       +    if currency == 'bc' or currency == 'tb':
       +        wver = fallback[0:5].uint
       +        if wver == 17:
       +            addr=base58.b58encode_check(bytes([base58_prefix_map[currency][0]])
       +                                        + fallback[5:].tobytes())
       +        elif wver == 18:
       +            addr=base58.b58encode_check(bytes([base58_prefix_map[currency][1]])
       +                                        + fallback[5:].tobytes())
       +        elif wver <= 16:
       +            addr=bech32_encode(currency, bitarray_to_u5(fallback))
       +        else:
       +            return None
       +    else:
       +        addr=fallback.tobytes()
       +    return addr
       +
       +
       +# Map of classical and witness address prefixes
       +base58_prefix_map = {
       +    'bc' : (0, 5),
       +    'tb' : (111, 196)
       +}
       +
       +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
       +def tagged(char, l):
       +    # Tagged fields need to be zero-padded to 5 bits.
       +    while l.len % 5 != 0:
       +        l.append('0b0')
       +    return bitstring.pack("uint:5, uint:5, uint:5",
       +                          CHARSET.find(char),
       +                          (l.len / 5) / 32, (l.len / 5) % 32) + l
       +
       +# Tagged field containing bytes
       +def tagged_bytes(char, l):
       +    return tagged(char, bitstring.BitArray(l))
       +
       +# Discard trailing bits, convert to bytes.
       +def trim_to_bytes(barr):
       +    # Adds a byte if necessary.
       +    b = barr.tobytes()
       +    if barr.len % 8 != 0:
       +        return b[:-1]
       +    return b
       +
       +# Try to pull out tagged data: returns tag, tagged data and remainder.
       +def pull_tagged(stream):
       +    tag = stream.read(5).uint
       +    length = stream.read(5).uint * 32 + stream.read(5).uint
       +    return (CHARSET[tag], stream.read(length * 5), stream)
       +
       +def lnencode(addr, privkey):
       +    if addr.amount:
       +        amount = Decimal(str(addr.amount))
       +        # We can only send down to millisatoshi.
       +        if amount * 10**12 % 10:
       +            raise ValueError("Cannot encode {}: too many decimal places".format(
       +                addr.amount))
       +
       +        amount = addr.currency + shorten_amount(amount)
       +    else:
       +        amount = addr.currency if addr.currency else ''
       +
       +    hrp = 'ln' + amount
       +
       +    # Start with the timestamp
       +    data = bitstring.pack('uint:35', addr.date)
       +
       +    # Payment hash
       +    data += tagged_bytes('p', addr.paymenthash)
       +    tags_set = set()
       +
       +    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'):
       +            if k in tags_set:
       +                raise ValueError("Duplicate '{}' tag".format(k))
       +
       +        if k == 'r':
       +            route = bitstring.BitArray()
       +            for step in v:
       +                pubkey, channel, feebase, feerate, cltv = step
       +                route.append(bitstring.BitArray(pubkey) + bitstring.BitArray(channel) + bitstring.pack('intbe:32', feebase) + bitstring.pack('intbe:32', feerate) + bitstring.pack('intbe:16', cltv))
       +            data += tagged('r', route)
       +        elif k == 'f':
       +            data += encode_fallback(v, addr.currency)
       +        elif k == 'd':
       +            data += tagged_bytes('d', v.encode())
       +        elif k == 'x':
       +            # Get minimal length by trimming leading 5 bits at a time.
       +            expirybits = bitstring.pack('intbe:64', v)[4:64]
       +            while expirybits.startswith('0b00000'):
       +                expirybits = expirybits[5:]
       +            data += tagged('x', expirybits)
       +        elif k == 'h':
       +            data += tagged_bytes('h', hashlib.sha256(v.encode('utf-8')).digest())
       +        elif k == 'n':
       +            data += tagged_bytes('n', v)
       +        else:
       +            # FIXME: Support unknown tags?
       +            raise ValueError("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 not 'd' in tags_set and not 'h' in tags_set:
       +        raise ValueError("Must include either 'd' or 'h'")
       +
       +    # We actually sign the hrp, then data (padded to 8 bits with zeroes).
       +    privkey = secp256k1.PrivateKey(bytes(unhexlify(privkey)))
       +    sig = privkey.ecdsa_sign_recoverable(bytearray([ord(c) for c in hrp]) + data.tobytes())
       +    # This doesn't actually serialize, but returns a pair of values :(
       +    sig, recid = privkey.ecdsa_recoverable_serialize(sig)
       +    data += bytes(sig) + bytes([recid])
       +
       +    return bech32_encode(hrp, bitarray_to_u5(data))
       +
       +class LnAddr(object):
       +    def __init__(self, paymenthash=None, amount=None, currency='bc', tags=None, date=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.signature = None
       +        self.pubkey = None
       +        self.currency = currency
       +        self.amount = amount
       +
       +    def __str__(self):
       +        return "LnAddr[{}, amount={}{} tags=[{}]]".format(
       +            hexlify(self.pubkey.serialize()).decode('utf-8'),
       +            self.amount, self.currency,
       +            ", ".join([k + '=' + str(v) for k, v in self.tags])
       +        )
       +
       +def lndecode(a, verbose=False):
       +    hrp, data = bech32_decode(a)
       +    if not hrp:
       +        raise ValueError("Bad bech32 checksum")
       +
       +    # BOLT #11:
       +    #
       +    # A reader MUST fail if it does not understand the `prefix`.
       +    if not hrp.startswith('ln'):
       +        raise ValueError("Does not start with ln")
       +
       +    data = u5_to_bitarray(data);
       +
       +    # Final signature 65 bytes, split it off.
       +    if len(data) < 65*8:
       +        raise ValueError("Too short to contain signature")
       +    sigdecoded = data[-65*8:].tobytes()
       +    data = bitstring.ConstBitStream(data[:-65*8])
       +
       +    addr = LnAddr()
       +    addr.pubkey = None
       +
       +    m = re.search("[^\d]+", hrp[2:])
       +    if m:
       +        addr.currency = m.group(0)
       +        amountstr = hrp[2+m.end():]
       +        # 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 = data.read(35).uint
       +
       +    while data.pos != data.len:
       +        tag, tagdata, data = pull_tagged(data)
       +
       +        # 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) / 5
       +
       +        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)
       +            route=[]
       +            s = bitstring.ConstBitStream(tagdata)
       +            while s.pos + 264 + 64 + 32 + 32 + 16 < s.len:
       +                route.append((s.read(264).tobytes(),
       +                              s.read(64).tobytes(),
       +                              s.read(32).intbe,
       +                              s.read(32).intbe,
       +                              s.read(16).intbe))
       +            addr.tags.append(('r',route))
       +        elif tag == 'f':
       +            fallback = parse_fallback(tagdata, addr.currency)
       +            if fallback:
       +                addr.tags.append(('f', fallback))
       +            else:
       +                # Incorrect version.
       +                addr.unknown_tags.append((tag, tagdata))
       +                continue
       +
       +        elif tag == 'd':
       +            addr.tags.append(('d', trim_to_bytes(tagdata).decode('utf-8')))
       +
       +        elif tag == 'h':
       +            if data_length != 52:
       +                addr.unknown_tags.append((tag, tagdata))
       +                continue
       +            addr.tags.append(('h', trim_to_bytes(tagdata)))
       +
       +        elif tag == 'x':
       +            addr.tags.append(('x', tagdata.uint))
       +
       +        elif tag == 'p':
       +            if data_length != 52:
       +                addr.unknown_tags.append((tag, tagdata))
       +                continue
       +            addr.paymenthash = trim_to_bytes(tagdata)
       +
       +        elif tag == 'n':
       +            if data_length != 53:
       +                addr.unknown_tags.append((tag, tagdata))
       +                continue
       +            addr.pubkey = secp256k1.PublicKey(flags=secp256k1.ALL_FLAGS)
       +            addr.pubkey.deserialize(trim_to_bytes(tagdata))
       +        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]))
       +        print('hex of data for signing: {}'
       +              .format(hexlify(bytearray([ord(c) for c in hrp])
       +                              + data.tobytes())))
       +        print('SHA256 of above: {}'.format(hashlib.sha256(bytearray([ord(c) for c in hrp]) + data.tobytes()).hexdigest()))
       +
       +    # BOLT #11:
       +    #
       +    # A reader MUST check that the `signature` is valid (see the `n` tagged
       +    # field specified below).
       +    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.
       +        addr.signature = addr.pubkey.ecdsa_deserialize_compact(sigdecoded[0:64])
       +        if not addr.pubkey.ecdsa_verify(bytearray([ord(c) for c in hrp]) + data.tobytes(), addr.signature):
       +            raise ValueError('Invalid signature')
       +    else: # Recover pubkey from signature.
       +        addr.pubkey = SerializableKey(MyVerifyingKey.from_signature(sigdecoded[:64], sigdecoded[64], hashlib.sha256(bytearray([ord(c) for c in hrp]) + data.tobytes()).digest(), curve = ecdsa.curves.SECP256k1))
       +
       +    return addr
       +
       +class SerializableKey:
       +    def __init__(self, pubkey):
       +        self.pubkey = pubkey
       +    def serialize(self):
       +        return GetPubKey(self.pubkey.pubkey, True)