channel_details.py (9518B)
1 from typing import TYPE_CHECKING 2 3 import PyQt5.QtGui as QtGui 4 import PyQt5.QtWidgets as QtWidgets 5 import PyQt5.QtCore as QtCore 6 from PyQt5.QtWidgets import QLabel, QLineEdit 7 8 from electrum import util 9 from electrum.i18n import _ 10 from electrum.util import bh2u, format_time 11 from electrum.lnutil import format_short_channel_id, LOCAL, REMOTE, UpdateAddHtlc, Direction 12 from electrum.lnchannel import htlcsum, Channel, AbstractChannel 13 from electrum.lnaddr import LnAddr, lndecode 14 from electrum.bitcoin import COIN 15 from electrum.wallet import Abstract_Wallet 16 17 from .util import Buttons, CloseButton, ButtonsLineEdit 18 19 if TYPE_CHECKING: 20 from .main_window import ElectrumWindow 21 22 class HTLCItem(QtGui.QStandardItem): 23 def __init__(self, *args, **kwargs): 24 super().__init__(*args, **kwargs) 25 self.setEditable(False) 26 27 class SelectableLabel(QtWidgets.QLabel): 28 def __init__(self, text=''): 29 super().__init__(text) 30 self.setTextInteractionFlags(QtCore.Qt.TextSelectableByMouse) 31 32 class LinkedLabel(QtWidgets.QLabel): 33 def __init__(self, text, on_clicked): 34 super().__init__(text) 35 self.linkActivated.connect(on_clicked) 36 37 class ChannelDetailsDialog(QtWidgets.QDialog): 38 def make_htlc_item(self, i: UpdateAddHtlc, direction: Direction) -> HTLCItem: 39 it = HTLCItem(_('Sent HTLC with ID {}' if Direction.SENT == direction else 'Received HTLC with ID {}').format(i.htlc_id)) 40 it.appendRow([HTLCItem(_('Amount')),HTLCItem(self.format_msat(i.amount_msat))]) 41 it.appendRow([HTLCItem(_('CLTV expiry')),HTLCItem(str(i.cltv_expiry))]) 42 it.appendRow([HTLCItem(_('Payment hash')),HTLCItem(bh2u(i.payment_hash))]) 43 return it 44 45 def make_model(self, htlcs) -> QtGui.QStandardItemModel: 46 model = QtGui.QStandardItemModel(0, 2) 47 model.setHorizontalHeaderLabels(['HTLC', 'Property value']) 48 parentItem = model.invisibleRootItem() 49 folder_types = { 50 'settled': _('Fulfilled HTLCs'), 51 'inflight': _('HTLCs in current commitment transaction'), 52 'failed': _('Failed HTLCs'), 53 } 54 self.folders = {} 55 self.keyname_rows = {} 56 57 for keyname, i in folder_types.items(): 58 myFont=QtGui.QFont() 59 myFont.setBold(True) 60 folder = HTLCItem(i) 61 folder.setFont(myFont) 62 parentItem.appendRow(folder) 63 self.folders[keyname] = folder 64 mapping = {} 65 num = 0 66 for item in htlcs: 67 pay_hash, chan_id, i, direction, status = item 68 if status != keyname: 69 continue 70 it = self.make_htlc_item(i, direction) 71 self.folders[keyname].appendRow(it) 72 mapping[i.payment_hash] = num 73 num += 1 74 self.keyname_rows[keyname] = mapping 75 return model 76 77 def move(self, fro: str, to: str, payment_hash: bytes): 78 assert fro != to 79 row_idx = self.keyname_rows[fro].pop(payment_hash) 80 row = self.folders[fro].takeRow(row_idx) 81 self.folders[to].appendRow(row) 82 dest_mapping = self.keyname_rows[to] 83 dest_mapping[payment_hash] = len(dest_mapping) 84 85 htlc_fulfilled = QtCore.pyqtSignal(str, bytes, bytes) 86 htlc_failed = QtCore.pyqtSignal(str, bytes, bytes) 87 htlc_added = QtCore.pyqtSignal(str, Channel, UpdateAddHtlc, Direction) 88 state_changed = QtCore.pyqtSignal(str, Abstract_Wallet, AbstractChannel) 89 90 @QtCore.pyqtSlot(str, Abstract_Wallet, AbstractChannel) 91 def do_state_changed(self, wallet, chan): 92 if wallet != self.wallet: 93 return 94 if chan == self.chan: 95 self.update() 96 97 @QtCore.pyqtSlot(str, Channel, UpdateAddHtlc, Direction) 98 def on_htlc_added(self, evtname, chan, htlc, direction): 99 if chan != self.chan: 100 return 101 mapping = self.keyname_rows['inflight'] 102 mapping[htlc.payment_hash] = len(mapping) 103 self.folders['inflight'].appendRow(self.make_htlc_item(htlc, direction)) 104 105 @QtCore.pyqtSlot(str, bytes, bytes) 106 def on_htlc_fulfilled(self, evtname, payment_hash, chan_id): 107 if chan_id != self.chan.channel_id: 108 return 109 self.move('inflight', 'settled', payment_hash) 110 self.update() 111 112 @QtCore.pyqtSlot(str, bytes, bytes) 113 def on_htlc_failed(self, evtname, payment_hash, chan_id): 114 if chan_id != self.chan.channel_id: 115 return 116 self.move('inflight', 'failed', payment_hash) 117 self.update() 118 119 def update(self): 120 self.can_send_label.setText(self.format_msat(self.chan.available_to_spend(LOCAL))) 121 self.can_receive_label.setText(self.format_msat(self.chan.available_to_spend(REMOTE))) 122 self.sent_label.setText(self.format_msat(self.chan.total_msat(Direction.SENT))) 123 self.received_label.setText(self.format_msat(self.chan.total_msat(Direction.RECEIVED))) 124 125 @QtCore.pyqtSlot(str) 126 def show_tx(self, link_text: str): 127 funding_tx = self.wallet.db.get_transaction(self.chan.funding_outpoint.txid) 128 self.window.show_transaction(funding_tx, tx_desc=_('Funding Transaction')) 129 130 def __init__(self, window: 'ElectrumWindow', chan_id: bytes): 131 super().__init__(window) 132 133 # initialize instance fields 134 self.window = window 135 self.wallet = window.wallet 136 chan = self.chan = window.wallet.lnworker.channels[chan_id] 137 self.format_msat = lambda msat: window.format_amount_and_units(msat / 1000) 138 139 # connect signals with slots 140 self.htlc_fulfilled.connect(self.on_htlc_fulfilled) 141 self.htlc_failed.connect(self.on_htlc_failed) 142 self.state_changed.connect(self.do_state_changed) 143 self.htlc_added.connect(self.on_htlc_added) 144 145 # register callbacks for updating 146 util.register_callback(self.htlc_fulfilled.emit, ['htlc_fulfilled']) 147 util.register_callback(self.htlc_failed.emit, ['htlc_failed']) 148 util.register_callback(self.htlc_added.emit, ['htlc_added']) 149 util.register_callback(self.state_changed.emit, ['channel']) 150 151 # set attributes of QDialog 152 self.setWindowTitle(_('Channel Details')) 153 self.setMinimumSize(800, 400) 154 155 # add layouts 156 vbox = QtWidgets.QVBoxLayout(self) 157 vbox.addWidget(QLabel(_('Remote Node ID:'))) 158 remote_id_e = ButtonsLineEdit(bh2u(chan.node_id)) 159 remote_id_e.addCopyButton(self.window.app) 160 remote_id_e.setReadOnly(True) 161 vbox.addWidget(remote_id_e) 162 funding_label_text = f'<a href=click_destination>{chan.funding_outpoint.txid}</a>:{chan.funding_outpoint.output_index}' 163 vbox.addWidget(QLabel(_('Funding Outpoint:'))) 164 vbox.addWidget(LinkedLabel(funding_label_text, self.show_tx)) 165 166 form_layout = QtWidgets.QFormLayout(None) 167 # add form content 168 form_layout.addRow(_('Channel ID:'), SelectableLabel(f"{chan.channel_id.hex()} (Short: {chan.short_channel_id})")) 169 form_layout.addRow(_('State:'), SelectableLabel(chan.get_state_for_GUI())) 170 self.initiator = 'Local' if chan.constraints.is_initiator else 'Remote' 171 form_layout.addRow(_('Initiator:'), SelectableLabel(self.initiator)) 172 self.capacity = self.window.format_amount_and_units(chan.get_capacity()) 173 form_layout.addRow(_('Capacity:'), SelectableLabel(self.capacity)) 174 self.can_send_label = SelectableLabel() 175 self.can_receive_label = SelectableLabel() 176 form_layout.addRow(_('Can send:'), self.can_send_label) 177 form_layout.addRow(_('Can receive:'), self.can_receive_label) 178 self.received_label = SelectableLabel() 179 form_layout.addRow(_('Received:'), self.received_label) 180 self.sent_label = SelectableLabel() 181 form_layout.addRow(_('Sent:'), self.sent_label) 182 #self.htlc_minimum_msat = SelectableLabel(str(chan.config[REMOTE].htlc_minimum_msat)) 183 #form_layout.addRow(_('Minimum HTLC value accepted by peer (mSAT):'), self.htlc_minimum_msat) 184 #self.max_htlcs = SelectableLabel(str(chan.config[REMOTE].max_accepted_htlcs)) 185 #form_layout.addRow(_('Maximum number of concurrent HTLCs accepted by peer:'), self.max_htlcs) 186 #self.max_htlc_value = SelectableLabel(self.window.format_amount_and_units(chan.config[REMOTE].max_htlc_value_in_flight_msat / 1000)) 187 #form_layout.addRow(_('Maximum value of in-flight HTLCs accepted by peer:'), self.max_htlc_value) 188 self.dust_limit = SelectableLabel(self.window.format_amount_and_units(chan.config[REMOTE].dust_limit_sat)) 189 form_layout.addRow(_('Remote dust limit:'), self.dust_limit) 190 self.remote_reserve = self.window.format_amount_and_units(chan.config[REMOTE].reserve_sat) 191 form_layout.addRow(_('Remote reserve:'), SelectableLabel(self.remote_reserve)) 192 vbox.addLayout(form_layout) 193 194 # add htlc tree view to vbox (wouldn't scale correctly in QFormLayout) 195 vbox.addWidget(QLabel(_('Payments (HTLCs):'))) 196 w = QtWidgets.QTreeView(self) 197 htlc_dict = chan.get_payments() 198 htlc_list = [] 199 for rhash, _list in htlc_dict.items(): 200 for _tuple in _list: 201 htlc_list.append((rhash.hex(),) + _tuple) 202 w.setModel(self.make_model(htlc_list)) 203 w.header().setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeToContents) 204 vbox.addWidget(w) 205 vbox.addLayout(Buttons(CloseButton(self))) 206 # initialize sent/received fields 207 self.update()