mirror of
https://github.com/LBRYFoundation/LBRY-Vault.git
synced 2025-08-23 17:47:31 +00:00
channel details with list of htlcs
This commit is contained in:
parent
9d32031ca2
commit
e3409d32ef
10 changed files with 195 additions and 28 deletions
|
@ -294,7 +294,7 @@ class SendScreen(CScreen):
|
||||||
fut = asyncio.run_coroutine_threadsafe(coro, self.app.network.asyncio_loop)
|
fut = asyncio.run_coroutine_threadsafe(coro, self.app.network.asyncio_loop)
|
||||||
fut.add_done_callback(self.ln_payment_result)
|
fut.add_done_callback(self.ln_payment_result)
|
||||||
|
|
||||||
def payment_completed_async_thread(self, event, direction, htlc, preimage):
|
def payment_completed_async_thread(self, event, date, direction, htlc, preimage, chan_id):
|
||||||
Clock.schedule_once(lambda dt: self.payment_completed(direction, htlc, preimage))
|
Clock.schedule_once(lambda dt: self.payment_completed(direction, htlc, preimage))
|
||||||
|
|
||||||
def payment_completed(self, direction, htlc, preimage):
|
def payment_completed(self, direction, htlc, preimage):
|
||||||
|
|
141
electrum/gui/qt/channel_details.py
Normal file
141
electrum/gui/qt/channel_details.py
Normal file
|
@ -0,0 +1,141 @@
|
||||||
|
from typing import Optional, TYPE_CHECKING
|
||||||
|
|
||||||
|
import PyQt5.QtGui as QtGui
|
||||||
|
import PyQt5.QtWidgets as QtWidgets
|
||||||
|
import PyQt5.QtCore as QtCore
|
||||||
|
|
||||||
|
from electrum.i18n import _
|
||||||
|
from electrum.lnchan import UpdateAddHtlc
|
||||||
|
from electrum.util import bh2u, format_time
|
||||||
|
from electrum.lnchan import HTLCOwner
|
||||||
|
from electrum.lnaddr import LnAddr, lndecode
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .main_window import ElectrumWindow
|
||||||
|
|
||||||
|
class HTLCItem(QtGui.QStandardItem):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.setEditable(False)
|
||||||
|
|
||||||
|
class ChannelDetailsDialog(QtWidgets.QDialog):
|
||||||
|
|
||||||
|
def make_inflight(self, lnaddr, i: UpdateAddHtlc):
|
||||||
|
it = HTLCItem(_('HTLC with ID ') + str(i.htlc_id))
|
||||||
|
it.appendRow([HTLCItem(_('Amount')),HTLCItem(self.format(i.amount_msat))])
|
||||||
|
it.appendRow([HTLCItem(_('CLTV expiry')),HTLCItem(str(i.cltv_expiry))])
|
||||||
|
it.appendRow([HTLCItem(_('Payment hash')),HTLCItem(bh2u(i.payment_hash))])
|
||||||
|
invoice = HTLCItem(_('Invoice'))
|
||||||
|
invoice.appendRow([HTLCItem(_('Remote node public key')), HTLCItem(bh2u(lnaddr.pubkey.serialize()))])
|
||||||
|
invoice.appendRow([HTLCItem(_('Amount in BTC')), HTLCItem(str(lnaddr.amount))])
|
||||||
|
invoice.appendRow([HTLCItem(_('Description')), HTLCItem(dict(lnaddr.tags).get('d', _('N/A')))])
|
||||||
|
invoice.appendRow([HTLCItem(_('Date')), HTLCItem(format_time(lnaddr.date))])
|
||||||
|
it.appendRow([invoice])
|
||||||
|
return it
|
||||||
|
|
||||||
|
def make_model(self, htlcs):
|
||||||
|
model = QtGui.QStandardItemModel(0, 2)
|
||||||
|
model.setHorizontalHeaderLabels(['HTLC', 'Property value'])
|
||||||
|
parentItem = model.invisibleRootItem()
|
||||||
|
folder_types = {'settled': _('Fulfilled HTLCs'), 'inflight': _('HTLCs in current commitment transaction')}
|
||||||
|
self.folders = {}
|
||||||
|
|
||||||
|
self.keyname_rows = {}
|
||||||
|
|
||||||
|
for keyname, i in folder_types.items():
|
||||||
|
myFont=QtGui.QFont()
|
||||||
|
myFont.setBold(True)
|
||||||
|
folder = HTLCItem(i)
|
||||||
|
folder.setFont(myFont)
|
||||||
|
parentItem.appendRow(folder)
|
||||||
|
self.folders[keyname] = folder
|
||||||
|
mapping = {}
|
||||||
|
num = 0
|
||||||
|
if keyname == 'inflight':
|
||||||
|
for lnaddr, i in htlcs[keyname]:
|
||||||
|
it = self.make_inflight(lnaddr, i)
|
||||||
|
self.folders[keyname].appendRow(it)
|
||||||
|
mapping[i.payment_hash] = num
|
||||||
|
num += 1
|
||||||
|
elif keyname == 'settled':
|
||||||
|
for date, direction, i, preimage in htlcs[keyname]:
|
||||||
|
it = HTLCItem(_('HTLC with ID ') + str(i.htlc_id))
|
||||||
|
it.appendRow([HTLCItem(_('Amount')),HTLCItem(self.format(i.amount_msat))])
|
||||||
|
it.appendRow([HTLCItem(_('CLTV expiry')),HTLCItem(str(i.cltv_expiry))])
|
||||||
|
it.appendRow([HTLCItem(_('Payment hash')),HTLCItem(bh2u(i.payment_hash))])
|
||||||
|
# NOTE no invoices because user can delete invoices after settlement
|
||||||
|
self.folders[keyname].appendRow(it)
|
||||||
|
mapping[i.payment_hash] = num
|
||||||
|
num += 1
|
||||||
|
|
||||||
|
self.keyname_rows[keyname] = mapping
|
||||||
|
return model
|
||||||
|
|
||||||
|
def move(self, fro: str, to: str, payment_hash: bytes):
|
||||||
|
assert fro != to
|
||||||
|
row_idx = self.keyname_rows[fro].pop(payment_hash)
|
||||||
|
row = self.folders[fro].takeRow(row_idx)
|
||||||
|
self.folders[to].appendRow(row)
|
||||||
|
dest_mapping = self.keyname_rows[to]
|
||||||
|
dest_mapping[payment_hash] = len(dest_mapping)
|
||||||
|
|
||||||
|
ln_payment_completed = QtCore.pyqtSignal(str, float, HTLCOwner, UpdateAddHtlc, bytes, bytes)
|
||||||
|
htlc_added = QtCore.pyqtSignal(str, UpdateAddHtlc, LnAddr, HTLCOwner)
|
||||||
|
|
||||||
|
@QtCore.pyqtSlot(str, UpdateAddHtlc, LnAddr, HTLCOwner)
|
||||||
|
def do_htlc_added(self, evtname, htlc, lnaddr, direction):
|
||||||
|
mapping = self.keyname_rows['inflight']
|
||||||
|
mapping[htlc.payment_hash] = len(mapping)
|
||||||
|
self.folders['inflight'].appendRow(self.make_inflight(lnaddr, htlc))
|
||||||
|
|
||||||
|
@QtCore.pyqtSlot(str, float, HTLCOwner, UpdateAddHtlc, bytes, bytes)
|
||||||
|
def do_ln_payment_completed(self, evtname, date, direction, htlc, preimage, chan_id):
|
||||||
|
self.move('inflight', 'settled', htlc.payment_hash)
|
||||||
|
|
||||||
|
def __init__(self, window: Optional['ElectrumWindow'], chan_id: bytes):
|
||||||
|
super().__init__(window)
|
||||||
|
self.window = window
|
||||||
|
assert type(window).__name__ in ['NoneType', 'ElectrumWindow']
|
||||||
|
self.ln_payment_completed.connect(self.do_ln_payment_completed)
|
||||||
|
self.htlc_added.connect(self.do_htlc_added)
|
||||||
|
if not window:
|
||||||
|
self.format = str
|
||||||
|
htlcs = {
|
||||||
|
'settled':
|
||||||
|
[
|
||||||
|
],
|
||||||
|
'inflight':
|
||||||
|
[
|
||||||
|
(lndecode("lnbcrt100n1pdl9c2vpp5z6ztyjy8an80te3u6l0fxuhjzt9pfa27a27uqap3xt8nv6dq47esdqgw3jhxapncqzy3rzjq2j0zgr9slpsefhaem0rq9w3kgjx6mjfd9tp7pe8yw23jqydcdtrsqqrc5qqqqgqqqqqqqlgqqqqqqgqjq5v97p0f0ftkwzmpxhjj6magd5ars465krljcp5z28j3nxl8d0kqjkzf6acjerxdu3yvtus75kakx3yvyus6c68hdwm2hpunusr47w3gpee4hgp"), UpdateAddHtlc(amount_msat=10001, payment_hash=b"\x01"*32, cltv_expiry=500, htlc_id=1)),
|
||||||
|
(lndecode('lnbcrt22m1pdl9kc7pp5qw903tar0e3ar4mu4h8m3zratj0sddqhfftpsjgcx0jsekzk43dsdqqcqzy3a6ev4vh6lt62xrzlq5l23g59pv0g3tur6drnduhczqg8smqlm75nklwx8r0mm535e4x8uq6tzqw7j7tvy70qaapfnt3e9n6rltvcs7cppzmqys'), UpdateAddHtlc(amount_msat=10002, payment_hash=b"\x02"*32, cltv_expiry=501, htlc_id=2)),
|
||||||
|
(lndecode('lnbcrt1u1pdl9k6tpp58la47qfxz6mvtgjmnmkl8xe8vcrkhluxrldlhv3dgdlla6tr3mvqdqgw3jhxapncqzy3rzjq2j0zgr9slpsefhaem0rq9w3kgjx6mjfd9tp7pe8yw23jqydcdtrsqqrc5qqqqgqqqqqqqlgqqqqqqgqjqavsdk9qdjwgfdywhlqtuzn5atkhzt9sgjz6tfll67wc34rh80mqzjme3meqyutrj0p7tvxczeuag956h6fv0356ezstgpfgqy47d7vsq7vhx6l'), UpdateAddHtlc(amount_msat=10003, payment_hash=b"\x03"*32, cltv_expiry=502, htlc_id=3)),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
htlcs = self.window.wallet.lnworker._list_invoices(chan_id)
|
||||||
|
self.format = lambda msat: self.window.format_amount_and_units(msat / 1000)
|
||||||
|
self.window.network.register_callback(self.ln_payment_completed.emit, ['ln_payment_completed'])
|
||||||
|
self.window.network.register_callback(self.htlc_added.emit, ['htlc_added'])
|
||||||
|
self.setWindowTitle(_('Channel Details'))
|
||||||
|
self.setMinimumSize(800, 400)
|
||||||
|
vbox = QtWidgets.QVBoxLayout(self)
|
||||||
|
w = QtWidgets.QTreeView(self)
|
||||||
|
w.setModel(self.make_model(htlcs))
|
||||||
|
#w.header().setStretchLastSection(False)
|
||||||
|
w.header().setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeToContents)
|
||||||
|
vbox.addWidget(w)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import sys
|
||||||
|
app = QtWidgets.QApplication(sys.argv)
|
||||||
|
d = ChannelDetailsDialog(None, b"\x01"*32)
|
||||||
|
d.show()
|
||||||
|
|
||||||
|
timer = QtCore.QTimer()
|
||||||
|
timer.setSingleShot(True)
|
||||||
|
def tick():
|
||||||
|
d.move('inflight', 'settled', b'\x02' * 32)
|
||||||
|
timer.timeout.connect(tick)
|
||||||
|
timer.start(3000)
|
||||||
|
|
||||||
|
sys.exit(app.exec_())
|
|
@ -11,6 +11,7 @@ from electrum.lnutil import LOCAL, REMOTE, ConnStringFormatError
|
||||||
|
|
||||||
from .util import MyTreeWidget, SortableTreeWidgetItem, WindowModalDialog, Buttons, OkButton, CancelButton
|
from .util import MyTreeWidget, SortableTreeWidgetItem, WindowModalDialog, Buttons, OkButton, CancelButton
|
||||||
from .amountedit import BTCAmountEdit
|
from .amountedit import BTCAmountEdit
|
||||||
|
from .channel_details import ChannelDetailsDialog
|
||||||
|
|
||||||
class ChannelsList(MyTreeWidget):
|
class ChannelsList(MyTreeWidget):
|
||||||
update_rows = QtCore.pyqtSignal()
|
update_rows = QtCore.pyqtSignal()
|
||||||
|
@ -62,10 +63,15 @@ class ChannelsList(MyTreeWidget):
|
||||||
coro = lnworker.force_close_channel(channel_id)
|
coro = lnworker.force_close_channel(channel_id)
|
||||||
return network.run_from_another_thread(coro)
|
return network.run_from_another_thread(coro)
|
||||||
WaitingDialog(self, 'please wait..', task, on_success, on_failure)
|
WaitingDialog(self, 'please wait..', task, on_success, on_failure)
|
||||||
|
menu.addAction(_("Details..."), lambda: self.details(channel_id))
|
||||||
menu.addAction(_("Close channel"), close)
|
menu.addAction(_("Close channel"), close)
|
||||||
menu.addAction(_("Force-close channel"), force_close)
|
menu.addAction(_("Force-close channel"), force_close)
|
||||||
menu.exec_(self.viewport().mapToGlobal(position))
|
menu.exec_(self.viewport().mapToGlobal(position))
|
||||||
|
|
||||||
|
def details(self, channel_id):
|
||||||
|
assert self.parent.wallet
|
||||||
|
ChannelDetailsDialog(self.parent, channel_id).show()
|
||||||
|
|
||||||
@QtCore.pyqtSlot(Channel)
|
@QtCore.pyqtSlot(Channel)
|
||||||
def do_update_single_row(self, chan):
|
def do_update_single_row(self, chan):
|
||||||
for i in range(self.topLevelItemCount()):
|
for i in range(self.topLevelItemCount()):
|
||||||
|
|
|
@ -396,7 +396,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
|
||||||
self.require_fee_update = True
|
self.require_fee_update = True
|
||||||
self.history_model.on_fee_histogram()
|
self.history_model.on_fee_histogram()
|
||||||
elif event == 'ln_message':
|
elif event == 'ln_message':
|
||||||
lnworker, message = args
|
lnworker, message, htlc_id = args
|
||||||
if lnworker == self.wallet.lnworker:
|
if lnworker == self.wallet.lnworker:
|
||||||
self.show_message(message)
|
self.show_message(message)
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -250,7 +250,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'),
|
hexlify(self.pubkey.serialize()).decode('utf-8') if self.pubkey else None,
|
||||||
self.amount, self.currency,
|
self.amount, self.currency,
|
||||||
", ".join([k + '=' + str(v) for k, v in self.tags])
|
", ".join([k + '=' + str(v) for k, v in self.tags])
|
||||||
)
|
)
|
||||||
|
|
|
@ -25,8 +25,8 @@ from .util import PrintError, bh2u, print_error, bfh, log_exceptions, list_enabl
|
||||||
from .transaction import Transaction, TxOutput
|
from .transaction import Transaction, TxOutput
|
||||||
from .lnonion import (new_onion_packet, decode_onion_error, OnionFailureCode, calc_hops_data_for_payment,
|
from .lnonion import (new_onion_packet, decode_onion_error, OnionFailureCode, calc_hops_data_for_payment,
|
||||||
process_onion_packet, OnionPacket, construct_onion_error, OnionRoutingFailureMessage)
|
process_onion_packet, OnionPacket, construct_onion_error, OnionRoutingFailureMessage)
|
||||||
from .lnchan import Channel, RevokeAndAck, htlcsum
|
from .lnchan import Channel, RevokeAndAck, htlcsum, UpdateAddHtlc
|
||||||
from .lnutil import (Outpoint, LocalConfig,
|
from .lnutil import (Outpoint, LocalConfig, RECEIVED,
|
||||||
RemoteConfig, OnlyPubkeyKeypair, ChannelConstraints, RevocationStore,
|
RemoteConfig, OnlyPubkeyKeypair, ChannelConstraints, RevocationStore,
|
||||||
funding_output_script, get_per_commitment_secret_from_seed,
|
funding_output_script, get_per_commitment_secret_from_seed,
|
||||||
secret_to_pubkey, LNPeerAddr, PaymentFailure, LnLocalFeatures,
|
secret_to_pubkey, LNPeerAddr, PaymentFailure, LnLocalFeatures,
|
||||||
|
@ -896,7 +896,7 @@ class Peer(PrintError):
|
||||||
self.revoke(chan)
|
self.revoke(chan)
|
||||||
self.send_commitment(chan) # htlc will be removed
|
self.send_commitment(chan) # htlc will be removed
|
||||||
await self.receive_revoke(chan)
|
await self.receive_revoke(chan)
|
||||||
self.network.trigger_callback('ln_message', self.lnworker, 'Payment failed')
|
self.network.trigger_callback('ln_message', self.lnworker, 'Payment failed', htlc_id)
|
||||||
|
|
||||||
async def _handle_error_code_from_failed_htlc(self, error_reason, route: List['RouteEdge'], channel_id, htlc_id):
|
async def _handle_error_code_from_failed_htlc(self, error_reason, route: List['RouteEdge'], channel_id, htlc_id):
|
||||||
chan = self.channels[channel_id]
|
chan = self.channels[channel_id]
|
||||||
|
@ -969,6 +969,7 @@ class Peer(PrintError):
|
||||||
self.attempted_route[(chan.channel_id, htlc_id)] = route
|
self.attempted_route[(chan.channel_id, htlc_id)] = route
|
||||||
self.print_error(f"starting payment. route: {route}")
|
self.print_error(f"starting payment. route: {route}")
|
||||||
await self.update_channel(chan, "update_add_htlc", channel_id=chan.channel_id, id=htlc_id, cltv_expiry=cltv, amount_msat=amount_msat, payment_hash=payment_hash, onion_routing_packet=onion.to_bytes())
|
await self.update_channel(chan, "update_add_htlc", channel_id=chan.channel_id, id=htlc_id, cltv_expiry=cltv, amount_msat=amount_msat, payment_hash=payment_hash, onion_routing_packet=onion.to_bytes())
|
||||||
|
return UpdateAddHtlc(**htlc, htlc_id=htlc_id)
|
||||||
|
|
||||||
async def receive_revoke(self, chan: Channel):
|
async def receive_revoke(self, chan: Channel):
|
||||||
revoke_and_ack_msg = await self.revoke_and_ack[chan.channel_id].get()
|
revoke_and_ack_msg = await self.revoke_and_ack[chan.channel_id].get()
|
||||||
|
@ -1007,7 +1008,7 @@ class Peer(PrintError):
|
||||||
self.revoke(chan)
|
self.revoke(chan)
|
||||||
self.send_commitment(chan) # htlc will be removed
|
self.send_commitment(chan) # htlc will be removed
|
||||||
await self.receive_revoke(chan)
|
await self.receive_revoke(chan)
|
||||||
self.network.trigger_callback('ln_message', self.lnworker, 'Payment sent')
|
self.network.trigger_callback('ln_message', self.lnworker, 'Payment sent', htlc_id)
|
||||||
|
|
||||||
# used in lightning-integration
|
# used in lightning-integration
|
||||||
self.payment_preimages[sha256(preimage)].put_nowait(preimage)
|
self.payment_preimages[sha256(preimage)].put_nowait(preimage)
|
||||||
|
@ -1034,7 +1035,7 @@ class Peer(PrintError):
|
||||||
pass # TODO fail the channel
|
pass # TODO fail the channel
|
||||||
# add htlc
|
# add htlc
|
||||||
htlc = {'amount_msat': amount_msat_htlc, 'payment_hash':payment_hash, 'cltv_expiry':cltv_expiry}
|
htlc = {'amount_msat': amount_msat_htlc, 'payment_hash':payment_hash, 'cltv_expiry':cltv_expiry}
|
||||||
chan.receive_htlc(htlc)
|
htlc_id = chan.receive_htlc(htlc)
|
||||||
await self.receive_commitment(chan)
|
await self.receive_commitment(chan)
|
||||||
self.revoke(chan)
|
self.revoke(chan)
|
||||||
self.send_commitment(chan)
|
self.send_commitment(chan)
|
||||||
|
@ -1074,6 +1075,7 @@ class Peer(PrintError):
|
||||||
data=amount_msat_htlc.to_bytes(8, byteorder="big"))
|
data=amount_msat_htlc.to_bytes(8, byteorder="big"))
|
||||||
await self.fail_htlc(chan, htlc_id, onion_packet, reason)
|
await self.fail_htlc(chan, htlc_id, onion_packet, reason)
|
||||||
return
|
return
|
||||||
|
self.network.trigger_callback('htlc_added', UpdateAddHtlc(**htlc, htlc_id=htlc_id), invoice, RECEIVED)
|
||||||
# settle htlc
|
# settle htlc
|
||||||
await self.settle_htlc(chan, htlc_id, preimage)
|
await self.settle_htlc(chan, htlc_id, preimage)
|
||||||
|
|
||||||
|
@ -1083,7 +1085,7 @@ class Peer(PrintError):
|
||||||
channel_id=chan.channel_id,
|
channel_id=chan.channel_id,
|
||||||
id=htlc_id,
|
id=htlc_id,
|
||||||
payment_preimage=preimage)
|
payment_preimage=preimage)
|
||||||
self.network.trigger_callback('ln_message', self.lnworker, 'Payment received')
|
self.network.trigger_callback('ln_message', self.lnworker, 'Payment received', htlc_id)
|
||||||
|
|
||||||
async def fail_htlc(self, chan: Channel, htlc_id: int, onion_packet: OnionPacket,
|
async def fail_htlc(self, chan: Channel, htlc_id: int, onion_packet: OnionPacket,
|
||||||
reason: OnionRoutingFailureMessage):
|
reason: OnionRoutingFailureMessage):
|
||||||
|
|
|
@ -149,7 +149,7 @@ class Channel(PrintError):
|
||||||
def __init__(self, state, name = None, payment_completed : Optional[Callable[[HTLCOwner, UpdateAddHtlc, bytes], None]] = None):
|
def __init__(self, state, name = None, payment_completed : Optional[Callable[[HTLCOwner, UpdateAddHtlc, bytes], None]] = None):
|
||||||
self.preimages = {}
|
self.preimages = {}
|
||||||
if not payment_completed:
|
if not payment_completed:
|
||||||
payment_completed = lambda x, y, z: None
|
payment_completed = lambda this, x, y, z: None
|
||||||
self.payment_completed = payment_completed
|
self.payment_completed = payment_completed
|
||||||
assert 'local_state' not in state
|
assert 'local_state' not in state
|
||||||
self.config = {}
|
self.config = {}
|
||||||
|
@ -505,7 +505,7 @@ class Channel(PrintError):
|
||||||
preimage = self.preimages.pop(htlc_id)
|
preimage = self.preimages.pop(htlc_id)
|
||||||
else:
|
else:
|
||||||
preimage = None
|
preimage = None
|
||||||
self.payment_completed(subject, htlc, preimage)
|
self.payment_completed(self, subject, htlc, preimage)
|
||||||
self.log[subject].settles.clear()
|
self.log[subject].settles.clear()
|
||||||
|
|
||||||
return old_amount - htlcsum(self.htlcs(subject, False))
|
return old_amount - htlcsum(self.htlcs(subject, False))
|
||||||
|
|
|
@ -595,7 +595,7 @@ def extract_nodeid(connect_contents: str) -> Tuple[bytes, str]:
|
||||||
raise ConnStringFormatError(_('At least a hostname must be supplied after the at symbol.'))
|
raise ConnStringFormatError(_('At least a hostname must be supplied after the at symbol.'))
|
||||||
try:
|
try:
|
||||||
node_id = bfh(nodeid_hex)
|
node_id = bfh(nodeid_hex)
|
||||||
assert len(node_id) == 33
|
assert len(node_id) == 33, len(node_id)
|
||||||
except:
|
except:
|
||||||
raise ConnStringFormatError(_('Invalid node ID, must be 33 bytes and hexadecimal'))
|
raise ConnStringFormatError(_('Invalid node ID, must be 33 bytes and hexadecimal'))
|
||||||
return node_id, rest
|
return node_id, rest
|
||||||
|
|
|
@ -12,6 +12,7 @@ import threading
|
||||||
import socket
|
import socket
|
||||||
import json
|
import json
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
import dns.resolver
|
import dns.resolver
|
||||||
import dns.exception
|
import dns.exception
|
||||||
|
@ -62,7 +63,7 @@ class LNWorker(PrintError):
|
||||||
def __init__(self, wallet: 'Abstract_Wallet', network: 'Network'):
|
def __init__(self, wallet: 'Abstract_Wallet', network: 'Network'):
|
||||||
self.wallet = wallet
|
self.wallet = wallet
|
||||||
# invoices we are currently trying to pay (might be pending HTLCs on a commitment transaction)
|
# invoices we are currently trying to pay (might be pending HTLCs on a commitment transaction)
|
||||||
self.paying = self.wallet.storage.get('lightning_payments_inflight', {}) # type: Dict[bytes, Tuple[str, Optional[int]]]
|
self.paying = self.wallet.storage.get('lightning_payments_inflight', {}) # type: Dict[bytes, Tuple[str, Optional[int], bytes]]
|
||||||
self.sweep_address = wallet.get_receiving_address()
|
self.sweep_address = wallet.get_receiving_address()
|
||||||
self.network = network
|
self.network = network
|
||||||
self.channel_db = self.network.channel_db
|
self.channel_db = self.network.channel_db
|
||||||
|
@ -71,9 +72,11 @@ class LNWorker(PrintError):
|
||||||
self.node_keypair = generate_keypair(self.ln_keystore, LnKeyFamily.NODE_KEY, 0)
|
self.node_keypair = generate_keypair(self.ln_keystore, LnKeyFamily.NODE_KEY, 0)
|
||||||
self.config = network.config
|
self.config = network.config
|
||||||
self.peers = {} # type: Dict[bytes, Peer] # pubkey -> Peer
|
self.peers = {} # type: Dict[bytes, Peer] # pubkey -> Peer
|
||||||
channels_map = map(lambda x: Channel(x, payment_completed=self.payment_completed), wallet.storage.get("channels", []))
|
self.channels = {} # type: Dict[bytes, Channel]
|
||||||
self.channels = {x.channel_id: x for x in channels_map} # type: Dict[bytes, Channel]
|
for x in wallet.storage.get("channels", []):
|
||||||
for c in self.channels.values():
|
c = Channel(x, payment_completed=self.payment_completed)
|
||||||
|
self.channels[c.channel_id] = c
|
||||||
|
|
||||||
c.lnwatcher = network.lnwatcher
|
c.lnwatcher = network.lnwatcher
|
||||||
c.sweep_address = self.sweep_address
|
c.sweep_address = self.sweep_address
|
||||||
self.invoices = wallet.storage.get('lightning_invoices', {}) # type: Dict[str, Tuple[str,str]] # RHASH -> (preimage, invoice)
|
self.invoices = wallet.storage.get('lightning_invoices', {}) # type: Dict[str, Tuple[str,str]] # RHASH -> (preimage, invoice)
|
||||||
|
@ -86,7 +89,8 @@ class LNWorker(PrintError):
|
||||||
self.network.register_callback(self.on_channel_txo, ['channel_txo'])
|
self.network.register_callback(self.on_channel_txo, ['channel_txo'])
|
||||||
asyncio.run_coroutine_threadsafe(self.network.main_taskgroup.spawn(self.main_loop()), self.network.asyncio_loop)
|
asyncio.run_coroutine_threadsafe(self.network.main_taskgroup.spawn(self.main_loop()), self.network.asyncio_loop)
|
||||||
|
|
||||||
def payment_completed(self, direction, htlc, preimage):
|
def payment_completed(self, chan, direction, htlc, preimage):
|
||||||
|
chan_id = chan.channel_id
|
||||||
if direction == SENT:
|
if direction == SENT:
|
||||||
assert htlc.payment_hash not in self.invoices
|
assert htlc.payment_hash not in self.invoices
|
||||||
self.paying.pop(bh2u(htlc.payment_hash))
|
self.paying.pop(bh2u(htlc.payment_hash))
|
||||||
|
@ -94,10 +98,11 @@ class LNWorker(PrintError):
|
||||||
l = self.wallet.storage.get('lightning_payments_completed', [])
|
l = self.wallet.storage.get('lightning_payments_completed', [])
|
||||||
if not preimage:
|
if not preimage:
|
||||||
preimage, _addr = self.get_invoice(htlc.payment_hash)
|
preimage, _addr = self.get_invoice(htlc.payment_hash)
|
||||||
l.append((time.time(), direction, json.loads(encoder.encode(htlc)), bh2u(preimage)))
|
tupl = (time.time(), direction, json.loads(encoder.encode(htlc)), bh2u(preimage), bh2u(chan_id))
|
||||||
|
l.append(tupl)
|
||||||
self.wallet.storage.put('lightning_payments_completed', l)
|
self.wallet.storage.put('lightning_payments_completed', l)
|
||||||
self.wallet.storage.write()
|
self.wallet.storage.write()
|
||||||
self.network.trigger_callback('ln_payment_completed', direction, htlc, preimage)
|
self.network.trigger_callback('ln_payment_completed', tupl[0], direction, htlc, preimage, chan_id)
|
||||||
|
|
||||||
def list_invoices(self):
|
def list_invoices(self):
|
||||||
report = self._list_invoices()
|
report = self._list_invoices()
|
||||||
|
@ -128,13 +133,16 @@ class LNWorker(PrintError):
|
||||||
yield str(htlc)
|
yield str(htlc)
|
||||||
yield ''
|
yield ''
|
||||||
|
|
||||||
def _list_invoices(self):
|
def _list_invoices(self, chan_id=None):
|
||||||
invoices = dict(self.invoices)
|
invoices = dict(self.invoices)
|
||||||
completed = self.wallet.storage.get('lightning_payments_completed', [])
|
completed = self.wallet.storage.get('lightning_payments_completed', [])
|
||||||
settled = []
|
settled = []
|
||||||
unsettled = []
|
unsettled = []
|
||||||
inflight = []
|
inflight = []
|
||||||
for date, direction, htlc, hex_preimage in completed:
|
for date, direction, htlc, hex_preimage, hex_chan_id in completed:
|
||||||
|
if chan_id is not None:
|
||||||
|
if bfh(hex_chan_id) != chan_id:
|
||||||
|
continue
|
||||||
htlcobj = UpdateAddHtlc(*htlc)
|
htlcobj = UpdateAddHtlc(*htlc)
|
||||||
if direction == RECEIVED:
|
if direction == RECEIVED:
|
||||||
preimage = bfh(invoices.pop(bh2u(htlcobj.payment_hash))[0])
|
preimage = bfh(invoices.pop(bh2u(htlcobj.payment_hash))[0])
|
||||||
|
@ -145,18 +153,21 @@ class LNWorker(PrintError):
|
||||||
for preimage, pay_req in invoices.values():
|
for preimage, pay_req in invoices.values():
|
||||||
addr = lndecode(pay_req, expected_hrp=constants.net.SEGWIT_HRP)
|
addr = lndecode(pay_req, expected_hrp=constants.net.SEGWIT_HRP)
|
||||||
unsettled.append((addr, bfh(preimage), pay_req))
|
unsettled.append((addr, bfh(preimage), pay_req))
|
||||||
for pay_req, amount_sat in self.paying.values():
|
for pay_req, amount_sat, this_chan_id in self.paying.values():
|
||||||
|
if chan_id is not None and this_chan_id != chan_id:
|
||||||
|
continue
|
||||||
addr = lndecode(pay_req, expected_hrp=constants.net.SEGWIT_HRP)
|
addr = lndecode(pay_req, expected_hrp=constants.net.SEGWIT_HRP)
|
||||||
if amount_sat is not None:
|
if amount_sat is not None:
|
||||||
addr.amount = Decimal(amount_sat) / COIN
|
addr.amount = Decimal(amount_sat) / COIN
|
||||||
htlc = self.find_htlc_for_addr(addr)
|
htlc = self.find_htlc_for_addr(addr, None if chan_id is None else [chan_id])
|
||||||
if not htlc:
|
if not htlc:
|
||||||
self.print_error('Warning, in flight HTLC not found in any channel')
|
self.print_error('Warning, in flight HTLC not found in any channel')
|
||||||
inflight.append((addr, htlc))
|
inflight.append((addr, htlc))
|
||||||
return {'settled': settled, 'unsettled': unsettled, 'inflight': inflight}
|
return {'settled': settled, 'unsettled': unsettled, 'inflight': inflight}
|
||||||
|
|
||||||
def find_htlc_for_addr(self, addr):
|
def find_htlc_for_addr(self, addr, whitelist=None):
|
||||||
for chan in self.channels.values():
|
channels = [y for x,y in self.channels.items() if x in whitelist or whitelist is None]
|
||||||
|
for chan in channels:
|
||||||
for htlc in chan.log[LOCAL].adds.values():
|
for htlc in chan.log[LOCAL].adds.values():
|
||||||
if htlc.payment_hash == addr.paymenthash:
|
if htlc.payment_hash == addr.paymenthash:
|
||||||
return htlc
|
return htlc
|
||||||
|
@ -362,7 +373,13 @@ class LNWorker(PrintError):
|
||||||
addr = self._check_invoice(invoice, amount_sat)
|
addr = self._check_invoice(invoice, amount_sat)
|
||||||
route = self._create_route_from_invoice(decoded_invoice=addr)
|
route = self._create_route_from_invoice(decoded_invoice=addr)
|
||||||
peer = self.peers[route[0].node_id]
|
peer = self.peers[route[0].node_id]
|
||||||
self.paying[bh2u(addr.paymenthash)] = (invoice, amount_sat)
|
for chan in self.channels.values():
|
||||||
|
if chan.short_channel_id == route[0].short_channel_id:
|
||||||
|
chan_id = chan.channel_id
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
assert False, 'Found route with short channel ID we don\'t have: ' + repr(route[0].short_channel_id)
|
||||||
|
self.paying[bh2u(addr.paymenthash)] = (invoice, amount_sat, chan_id)
|
||||||
self.wallet.storage.put('lightning_payments_inflight', self.paying)
|
self.wallet.storage.put('lightning_payments_inflight', self.paying)
|
||||||
self.wallet.storage.write()
|
self.wallet.storage.write()
|
||||||
return addr, peer, self._pay_to_route(route, addr)
|
return addr, peer, self._pay_to_route(route, addr)
|
||||||
|
@ -377,7 +394,8 @@ class LNWorker(PrintError):
|
||||||
else:
|
else:
|
||||||
raise Exception("PathFinder returned path with short_channel_id {} that is not in channel list".format(bh2u(short_channel_id)))
|
raise Exception("PathFinder returned path with short_channel_id {} that is not in channel list".format(bh2u(short_channel_id)))
|
||||||
peer = self.peers[route[0].node_id]
|
peer = self.peers[route[0].node_id]
|
||||||
return await peer.pay(route, chan, int(addr.amount * COIN * 1000), addr.paymenthash, addr.get_min_final_cltv_expiry())
|
htlc = await peer.pay(route, chan, int(addr.amount * COIN * 1000), addr.paymenthash, addr.get_min_final_cltv_expiry())
|
||||||
|
self.network.trigger_callback('htlc_added', htlc, addr, SENT)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _check_invoice(invoice, amount_sat=None):
|
def _check_invoice(invoice, amount_sat=None):
|
||||||
|
|
|
@ -195,7 +195,7 @@ class TestPeer(unittest.TestCase):
|
||||||
def prepare_ln_message_future(w2 # receiver
|
def prepare_ln_message_future(w2 # receiver
|
||||||
):
|
):
|
||||||
fut = asyncio.Future()
|
fut = asyncio.Future()
|
||||||
def evt_set(event, _lnworker, msg):
|
def evt_set(event, _lnworker, msg, _htlc_id):
|
||||||
fut.set_result(msg)
|
fut.set_result(msg)
|
||||||
w2.network.register_callback(evt_set, ['ln_message'])
|
w2.network.register_callback(evt_set, ['ln_message'])
|
||||||
return fut
|
return fut
|
||||||
|
|
Loading…
Add table
Reference in a new issue