electrum

Electrum Bitcoin wallet
git clone https://git.parazyd.org/electrum
Log | Files | Refs | Submodules

lightning_open_channel.py (8063B)


      1 from typing import TYPE_CHECKING
      2 
      3 from kivy.lang import Builder
      4 from kivy.factory import Factory
      5 
      6 from electrum.gui.kivy.i18n import _
      7 from electrum.lnaddr import lndecode
      8 from electrum.util import bh2u
      9 from electrum.bitcoin import COIN
     10 import electrum.simple_config as config
     11 from electrum.logging import Logger
     12 from electrum.lnutil import ln_dummy_address
     13 
     14 from .label_dialog import LabelDialog
     15 
     16 if TYPE_CHECKING:
     17     from ...main_window import ElectrumWindow
     18 
     19 
     20 Builder.load_string('''
     21 #:import KIVY_GUI_PATH electrum.gui.kivy.KIVY_GUI_PATH
     22 
     23 <LightningOpenChannelDialog@Popup>
     24     use_gossip: False
     25     id: s
     26     name: 'lightning_open_channel'
     27     title: _('Open Lightning Channel')
     28     pubkey: ''
     29     amount: ''
     30     is_max: False
     31     ipport: ''
     32     BoxLayout
     33         spacing: '12dp'
     34         padding: '12dp'
     35         orientation: 'vertical'
     36         SendReceiveBlueBottom:
     37             id: blue_bottom
     38             size_hint: 1, None
     39             height: self.minimum_height
     40             BoxLayout:
     41                 size_hint: 1, None
     42                 height: blue_bottom.item_height
     43                 Image:
     44                     source: f'atlas://{KIVY_GUI_PATH}/theming/light/globe'
     45                     size_hint: None, None
     46                     size: '22dp', '22dp'
     47                     pos_hint: {'center_y': .5}
     48                 BlueButton:
     49                     text: s.pubkey if s.pubkey else (_('Node ID') if root.use_gossip else _('Trampoline node'))
     50                     shorten: True
     51                     on_release: s.suggest_node()
     52             CardSeparator:
     53                 color: blue_bottom.foreground_color
     54             BoxLayout:
     55                 size_hint: 1, None
     56                 height: blue_bottom.item_height
     57                 Image:
     58                     source: f'atlas://{KIVY_GUI_PATH}/theming/light/calculator'
     59                     size_hint: None, None
     60                     size: '22dp', '22dp'
     61                     pos_hint: {'center_y': .5}
     62                 BlueButton:
     63                     text: s.amount if s.amount else _('Amount')
     64                     on_release: app.amount_dialog(s, True)
     65         TopLabel:
     66             text: _('Paste or scan a node ID, a connection string or a lightning invoice.') if root.use_gossip else _('Choose a trampoline node and the amount')
     67         BoxLayout:
     68             size_hint: 1, None
     69             height: '48dp'
     70             IconButton:
     71                 icon: f'atlas://{KIVY_GUI_PATH}/theming/light/copy'
     72                 size_hint: 0.5, None
     73                 height: '48dp'
     74                 on_release: s.do_paste()
     75             IconButton:
     76                 icon: f'atlas://{KIVY_GUI_PATH}/theming/light/camera'
     77                 size_hint: 0.5, None
     78                 height: '48dp'
     79                 on_release: app.scan_qr(on_complete=s.on_qr)
     80             Button:
     81                 text: _('Suggest')
     82                 size_hint: 1, None
     83                 height: '48dp'
     84                 on_release: s.suggest_node()
     85             Button:
     86                 text: _('Clear')
     87                 size_hint: 1, None
     88                 height: '48dp'
     89                 on_release: s.do_clear()
     90         Widget:
     91             size_hint: 1, 1
     92         BoxLayout:
     93             size_hint: 1, None
     94             Widget:
     95                 size_hint: 2, None
     96             Button:
     97                 text: _('Open')
     98                 size_hint: 1, None
     99                 height: '48dp'
    100                 on_release: s.open_channel()
    101                 disabled: not root.pubkey or not root.amount
    102 ''')
    103 
    104 class LightningOpenChannelDialog(Factory.Popup, Logger):
    105     def ipport_dialog(self):
    106         def callback(text):
    107             self.ipport = text
    108         d = LabelDialog(_('IP/port in format:\n[host]:[port]'), self.ipport, callback)
    109         d.open()
    110 
    111     def suggest_node(self):
    112         if self.use_gossip:
    113             suggested = self.app.wallet.lnworker.suggest_peer()
    114             if suggested:
    115                 self.pubkey = suggested.hex()
    116             else:
    117                 _, _, percent = self.app.wallet.network.lngossip.get_sync_progress_estimate()
    118                 if percent is None:
    119                     percent = "??"
    120                 self.pubkey = f"Please wait, graph is updating ({percent}% / 30% done)."
    121         else:
    122             self.trampoline_index += 1
    123             self.trampoline_index = self.trampoline_index % len(self.trampoline_names)
    124             self.pubkey = self.trampoline_names[self.trampoline_index]
    125 
    126     def __init__(self, app, lnaddr=None, msg=None):
    127         Factory.Popup.__init__(self)
    128         Logger.__init__(self)
    129         self.app = app  # type: ElectrumWindow
    130         self.lnaddr = lnaddr
    131         self.msg = msg
    132         self.use_gossip = bool(self.app.network.channel_db)
    133         if not self.use_gossip:
    134             from electrum.lnworker import hardcoded_trampoline_nodes
    135             self.trampolines = hardcoded_trampoline_nodes()
    136             self.trampoline_names = list(self.trampolines.keys())
    137             self.trampoline_index = 0
    138             self.pubkey = ''
    139 
    140     def open(self, *args, **kwargs):
    141         super(LightningOpenChannelDialog, self).open(*args, **kwargs)
    142         if self.lnaddr:
    143             fee = self.app.electrum_config.fee_per_kb()
    144             if not fee:
    145                 fee = config.FEERATE_FALLBACK_STATIC_FEE
    146             self.amount = self.app.format_amount_and_units(self.lnaddr.amount * COIN + fee * 2)  # FIXME magic number?!
    147             self.pubkey = bh2u(self.lnaddr.pubkey.serialize())
    148         if self.msg:
    149             self.app.show_info(self.msg)
    150 
    151     def do_clear(self):
    152         self.pubkey = ''
    153         self.amount = ''
    154 
    155     def do_paste(self):
    156         contents = self.app._clipboard.paste()
    157         if not contents:
    158             self.app.show_info(_("Clipboard is empty"))
    159             return
    160         self.pubkey = contents
    161 
    162     def on_qr(self, conn_str):
    163         self.pubkey = conn_str
    164 
    165     # FIXME "max" button in amount_dialog should enforce LN_MAX_FUNDING_SAT
    166     def open_channel(self):
    167         if not self.pubkey or not self.amount:
    168             self.app.show_info(_('All fields must be filled out'))
    169             return
    170         if self.use_gossip:
    171             conn_str = self.pubkey
    172             if self.ipport:
    173                 conn_str += '@' + self.ipport.strip()
    174         else:
    175             conn_str = str(self.trampolines[self.pubkey])
    176         amount = '!' if self.is_max else self.app.get_amount(self.amount)
    177         self.app.protected('Create a new channel?', self.do_open_channel, (conn_str, amount))
    178         self.dismiss()
    179 
    180     def do_open_channel(self, conn_str, amount, password):
    181         coins = self.app.wallet.get_spendable_coins(None, nonlocal_only=True)
    182         lnworker = self.app.wallet.lnworker
    183         try:
    184             funding_tx = lnworker.mktx_for_open_channel(coins=coins, funding_sat=amount)
    185         except Exception as e:
    186             self.logger.exception("Problem opening channel")
    187             self.app.show_error(_('Problem opening channel: ') + '\n' + repr(e))
    188             return
    189         # read funding_sat from tx; converts '!' to int value
    190         funding_sat = funding_tx.output_value_for_address(ln_dummy_address())
    191         try:
    192             chan, funding_tx = lnworker.open_channel(
    193                 connect_str=conn_str,
    194                 funding_tx=funding_tx,
    195                 funding_sat=funding_sat,
    196                 push_amt_sat=0,
    197                 password=password)
    198         except Exception as e:
    199             self.logger.exception("Problem opening channel")
    200             self.app.show_error(_('Problem opening channel: ') + '\n' + repr(e))
    201             return
    202         n = chan.constraints.funding_txn_minimum_depth
    203         message = '\n'.join([
    204             _('Channel established.'),
    205             _('Remote peer ID') + ':' + chan.node_id.hex(),
    206             _('This channel will be usable after {} confirmations').format(n)
    207         ])
    208         if not funding_tx.is_complete():
    209             message += '\n\n' + _('Please sign and broadcast the funding transaction')
    210         self.app.show_info(message)
    211         if not funding_tx.is_complete():
    212             self.app.tx_dialog(funding_tx)