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)