electrum

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

commit 0e8dba897eeabf9a8b19194233ad2a9a8246a2cc
parent d80b709aa4dbb8a07e04afeae171829245224de4
Author: ThomasV <thomasv@electrum.org>
Date:   Tue, 29 Jan 2019 19:01:04 +0100

lightning:
* store invoices for both directions
* do not store lightning_payments_inflight, lightning_payments_completed in lnworker
* payment history is returned by get_payments method of LNChannel
* command line: lightning history, lightning_invoices
* re-enable push_msat

Diffstat:
Melectrum/commands.py | 77++++++++++++++++++++++++++++++++++-------------------------------------------
Melectrum/gui/qt/channel_details.py | 44++++++++++++++++++++++----------------------
Melectrum/gui/qt/main_window.py | 4+++-
Melectrum/gui/qt/request_list.py | 11+++++++----
Melectrum/gui/qt/util.py | 6------
Melectrum/lnbase.py | 4++--
Melectrum/lnchan.py | 11+++++++++++
Melectrum/lnworker.py | 106++++++++++++++++++++++++++-----------------------------------------------------
Melectrum/util.py | 8++++++++
9 files changed, 121 insertions(+), 150 deletions(-)

diff --git a/electrum/commands.py b/electrum/commands.py @@ -47,6 +47,7 @@ from .wallet import Abstract_Wallet, create_new_wallet, restore_wallet_from_text from .address_synchronizer import TX_HEIGHT_LOCAL from .import lightning from .mnemonic import Mnemonic +from .lnutil import SENT, RECEIVED if TYPE_CHECKING: from .network import Network @@ -108,6 +109,8 @@ class Commands: self.wallet = wallet self.network = network self._callback = callback + if self.wallet: + self.lnworker = self.wallet.lnworker def _run(self, method, args, password_getter, **kwargs): """This wrapper is called from the Qt python console.""" @@ -766,33 +769,33 @@ class Commands: # lightning network commands @command('wpn') def open_channel(self, connection_string, amount, channel_push=0, password=None): - return self.wallet.lnworker.open_channel(connection_string, satoshis(amount), satoshis(channel_push), password) + return self.lnworker.open_channel(connection_string, satoshis(amount), satoshis(channel_push), password) @command('wn') def reestablish_channel(self): - self.wallet.lnworker.reestablish_channel() + self.lnworker.reestablish_channel() @command('wn') def lnpay(self, invoice): - addr, peer, f = self.wallet.lnworker.pay(invoice) + addr, peer, f = self.lnworker.pay(invoice) return f.result() @command('wn') def addinvoice(self, requested_amount, message): # using requested_amount because it is documented in param_descriptions - return self.wallet.lnworker.add_invoice(satoshis(requested_amount), message) + return self.lnworker.add_invoice(satoshis(requested_amount), message) @command('wn') def nodeid(self): - return bh2u(self.wallet.lnworker.node_keypair.pubkey) + return bh2u(self.lnworker.node_keypair.pubkey) @command('w') def listchannels(self): - return list(self.wallet.lnworker.list_channels()) + return list(self.lnworker.list_channels()) @command('wn') def dumpgraph(self): - return list(map(bh2u, self.wallet.lnworker.channel_db.nodes.keys())) + return list(map(bh2u, self.lnworker.channel_db.nodes.keys())) @command('n') def inject_fees(self, fees): @@ -805,47 +808,35 @@ class Commands: self.network.path_finder.blacklist.clear() @command('w') - def listinvoices(self): - report = self.wallet.lnworker._list_invoices() - return '\n'.join(self._format_ln_invoices(report)) - - def _format_ln_invoices(self, report): - from .lnutil import SENT - if report['settled']: - yield 'Settled invoices:' - yield '-----------------' - for date, direction, htlc, preimage in sorted(report['settled']): - # astimezone converts to local time - # replace removes the tz info since we don't need to display it - yield 'Paid at: ' + date.astimezone().replace(tzinfo=None).isoformat(sep=' ', timespec='minutes') - yield 'We paid' if direction == SENT else 'They paid' - yield str(htlc) - yield 'Preimage: ' + (bh2u(preimage) if preimage else 'Not available') # if delete_invoice was called - yield '' - if report['unsettled']: - yield 'Your unsettled invoices:' - yield '------------------------' - for addr, preimage, pay_req in report['unsettled']: - yield pay_req - yield str(addr) - yield 'Preimage: ' + bh2u(preimage) - yield '' - if report['inflight']: - yield 'Outgoing payments in progress:' - yield '------------------------------' - for addr, htlc, direction in report['inflight']: - yield str(addr) - yield str(htlc) - yield '' + def lightning_invoices(self): + from .util import pr_tooltips + out = [] + for payment_hash, (preimage, pay_req, direction, pay_timestamp) in self.lnworker.invoices.items(): + status = pr_tooltips[self.lnworker.get_invoice_status(payment_hash)] + out.append({'payment_hash':payment_hash, 'invoice':pay_req, 'preimage':preimage, 'status':status, 'direction':direction}) + return out + @command('w') + def lightning_history(self): + out = [] + for chan_id, htlc, direction, status in self.lnworker.get_payments().values(): + item = { + 'direction': 'sent' if direction == SENT else 'received', + 'status':status, + 'amout_msat':htlc.amount_msat, + 'payment_hash':bh2u(htlc.payment_hash), + 'chan_id':bh2u(chan_id), + 'htlc_id':htlc.htlc_id, + 'cltv_expiry':htlc.cltv_expiry + } + out.append(item) + return out @command('wn') def closechannel(self, channel_point, force=False): chan_id = bytes(reversed(bfh(channel_point))) - if force: - return self.network.run_from_another_thread(self.wallet.lnworker.force_close_channel(chan_id)) - else: - return self.network.run_from_another_thread(self.wallet.lnworker.close_channel(chan_id)) + coro = self.lnworker.force_close_channel(chan_id) if force else self.lnworker.force_close_channel(chan_id) + return self.network.run_from_another_thread(coro) def eval_bool(x: str) -> bool: if x == 'false': return False diff --git a/electrum/gui/qt/channel_details.py b/electrum/gui/qt/channel_details.py @@ -56,11 +56,8 @@ class ChannelDetailsDialog(QtWidgets.QDialog): parentItem = model.invisibleRootItem() folder_types = {'settled': _('Fulfilled HTLCs'), 'inflight': _('HTLCs in current commitment transaction')} self.folders = {} - self.keyname_rows = {} - invoices = dict(self.window.wallet.lnworker.invoices) - for keyname, i in folder_types.items(): myFont=QtGui.QFont() myFont.setBold(True) @@ -70,23 +67,26 @@ class ChannelDetailsDialog(QtWidgets.QDialog): self.folders[keyname] = folder mapping = {} num = 0 - if keyname == 'inflight': - for lnaddr, i, direction in htlcs[keyname]: - it = self.make_inflight(lnaddr, i, direction) - self.folders[keyname].appendRow(it) - mapping[i.payment_hash] = num - num += 1 - elif keyname == 'settled': - for date, direction, i, preimage in htlcs[keyname]: - it = self.make_htlc_item(i, direction) - hex_pay_hash = bh2u(i.payment_hash) - if hex_pay_hash in invoices: - # if we made the invoice and still have it, we can show more info - invoice = invoices[hex_pay_hash][1] - self.append_lnaddr(it, lndecode(invoice)) - self.folders[keyname].appendRow(it) - mapping[i.payment_hash] = num - num += 1 + + invoices = dict(self.window.wallet.lnworker.invoices) + for pay_hash, item in htlcs.items(): + chan_id, i, direction, status = item + if pay_hash in invoices: + preimage, invoice, direction, timestamp = invoices[pay_hash] + lnaddr = lndecode(invoice) + if status == 'inflight': + it = self.make_inflight(lnaddr, i, direction) + self.folders['inflight'].appendRow(it) + mapping[i.payment_hash] = num + num += 1 + elif status == 'settled': + it = self.make_htlc_item(i, direction) + # if we made the invoice and still have it, we can show more info + if pay_hash in invoices: + self.append_lnaddr(it, lndecode(invoice)) + self.folders['settled'].appendRow(it) + mapping[i.payment_hash] = num + num += 1 self.keyname_rows[keyname] = mapping return model @@ -171,8 +171,8 @@ class ChannelDetailsDialog(QtWidgets.QDialog): # add htlc tree view to vbox (wouldn't scale correctly in QFormLayout) form_layout.addRow(_('Payments (HTLCs):'), None) w = QtWidgets.QTreeView(self) - htlcs = window.wallet.lnworker._list_invoices(chan_id) - w.setModel(self.make_model(htlcs)) + htlc_dict = chan.get_payments() + w.setModel(self.make_model(htlc_dict)) w.header().setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeToContents) vbox.addWidget(w) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py @@ -62,7 +62,7 @@ from electrum.util import (format_time, format_satoshis, format_fee_satoshis, UnknownBaseUnit, DECIMAL_POINT_DEFAULT, UserFacingException, get_new_wallet_name, send_exception_to_crash_reporter, InvalidBitcoinURI, InvoiceError) -from electrum.lnutil import PaymentFailure +from electrum.lnutil import PaymentFailure, SENT, RECEIVED from electrum.transaction import Transaction, TxOutput from electrum.address_synchronizer import AddTransactionException from electrum.wallet import (Multisig_Wallet, CannotBumpFee, Abstract_Wallet, @@ -1941,6 +1941,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): #self.amount_e.textEdited.emit("") self.payto_e.is_lightning = True self.show_send_tab_onchain_fees(False) + # save + self.wallet.lnworker.save_invoice(None, invoice, SENT) def show_send_tab_onchain_fees(self, b: bool): self.feecontrol_fields.setVisible(b) diff --git a/electrum/gui/qt/request_list.py b/electrum/gui/qt/request_list.py @@ -31,7 +31,8 @@ from PyQt5.QtCore import Qt, QItemSelectionModel from electrum.i18n import _ from electrum.util import format_time, age -from electrum.util import PR_UNPAID, PR_EXPIRED, PR_PAID, PR_UNKNOWN, PR_INFLIGHT +from electrum.util import PR_UNPAID, PR_EXPIRED, PR_PAID, PR_UNKNOWN, PR_INFLIGHT, pr_tooltips +from electrum.lnutil import SENT, RECEIVED from electrum.plugin import run_hook from electrum.wallet import InternalAddressCorruption from electrum.bitcoin import COIN @@ -95,7 +96,7 @@ class RequestList(MyTreeView): return req = self.parent.get_request_URI(key) elif request_type == REQUEST_TYPE_LN: - preimage, req = self.wallet.lnworker.invoices.get(key, (None, None)) + preimage, req, direction, pay_timestamp = self.wallet.lnworker.invoices.get(key, (None, None, None)) if req is None: self.update() return @@ -145,7 +146,9 @@ class RequestList(MyTreeView): self.filter() # lightning lnworker = self.wallet.lnworker - for key, (preimage_hex, invoice) in lnworker.invoices.items(): + for key, (preimage_hex, invoice, direction, pay_timestamp) in lnworker.invoices.items(): + if direction == SENT: + continue status = lnworker.get_invoice_status(key) lnaddr = lndecode(invoice, expected_hrp=constants.net.SEGWIT_HRP) amount_sat = lnaddr.amount*COIN if lnaddr.amount else None @@ -181,7 +184,7 @@ class RequestList(MyTreeView): if request_type == REQUEST_TYPE_BITCOIN: req = self.wallet.receive_requests.get(addr) elif request_type == REQUEST_TYPE_LN: - preimage, req = self.wallet.lnworker.invoices.get(addr) + preimage, req, direction, pay_timestamp = self.wallet.lnworker.invoices.get(addr) if req is None: self.update() return diff --git a/electrum/gui/qt/util.py b/electrum/gui/qt/util.py @@ -47,12 +47,6 @@ pr_icons = { PR_INFLIGHT:"lightning.png", } -pr_tooltips = { - PR_UNPAID:_('Pending'), - PR_PAID:_('Paid'), - PR_EXPIRED:_('Expired'), - PR_INFLIGHT:_('Inflight') -} expiration_values = [ (_('1 hour'), 60*60), diff --git a/electrum/lnbase.py b/electrum/lnbase.py @@ -420,7 +420,7 @@ class Peer(PrintError): @log_exceptions async def channel_establishment_flow(self, password: Optional[str], funding_sat: int, push_msat: int, temp_channel_id: bytes) -> Channel: - assert push_msat == 0, "push_msat not supported currently" + #assert push_msat == 0, "push_msat not supported currently" wallet = self.lnworker.wallet # dry run creating funding tx to see if we even have enough funds funding_tx_test = wallet.mktx([TxOutput(bitcoin.TYPE_ADDRESS, wallet.dummy_address(), funding_sat)], @@ -549,7 +549,7 @@ class Peer(PrintError): raise Exception('wrong chain_hash') funding_sat = int.from_bytes(payload['funding_satoshis'], 'big') push_msat = int.from_bytes(payload['push_msat'], 'big') - assert push_msat == 0, "push_msat not supported currently" + #assert push_msat == 0, "push_msat not supported currently" feerate = int.from_bytes(payload['feerate_per_kw'], 'big') temp_chan_id = payload['temporary_channel_id'] diff --git a/electrum/lnchan.py b/electrum/lnchan.py @@ -171,6 +171,17 @@ class Channel(PrintError): self.local_commitment = None self.remote_commitment = None + def get_payments(self): + out = {} + for subject in LOCAL, REMOTE: + log = self.hm.log[subject] + for htlc_id, htlc in log.get('adds', {}).items(): + rhash = bh2u(htlc.payment_hash) + status = 'settled' if htlc_id in log.get('settles',{}) else 'inflight' + direction = SENT if subject is LOCAL else RECEIVED + out[rhash] = (self.channel_id, htlc, direction, status) + return out + def set_local_commitment(self, ctx): ctn = extract_ctn_from_tx_and_chan(ctx, self) assert self.signature_fits(ctx), (self.log[LOCAL]) diff --git a/electrum/lnworker.py b/electrum/lnworker.py @@ -19,6 +19,7 @@ import dns.exception from . import constants from . import keystore +from .util import PR_UNPAID, PR_EXPIRED, PR_PAID, PR_UNKNOWN, PR_INFLIGHT from .keystore import BIP32_KeyStore from .bitcoin import COIN from .transaction import Transaction @@ -66,10 +67,7 @@ class LNWorker(PrintError): def __init__(self, wallet: 'Abstract_Wallet'): self.wallet = wallet - # invoices we are currently trying to pay (might be pending HTLCs on a commitment transaction) - self.invoices = self.wallet.storage.get('lightning_invoices', {}) # type: Dict[str, Tuple[str,str]] # RHASH -> (preimage, invoice) - self.inflight = self.wallet.storage.get('lightning_payments_inflight', {}) # type: Dict[bytes, Tuple[str, Optional[int], str]] - self.completed = self.wallet.storage.get('lightning_payments_completed', {}) + self.invoices = self.wallet.storage.get('lightning_invoices', {}) # type: Dict[str, Tuple[str,str]] # RHASH -> (preimage, invoice, direction, pay_timestamp) self.sweep_address = wallet.get_receiving_address() self.lock = threading.RLock() self.ln_keystore = self._read_ln_keystore() @@ -122,73 +120,34 @@ class LNWorker(PrintError): self.wallet.storage.write() self.print_error('saved lightning gossip timestamp') - def payment_completed(self, chan, direction, htlc, preimage): - assert type(direction) is Direction - key = bh2u(htlc.payment_hash) + def payment_completed(self, chan, direction, htlc, _preimage): chan_id = chan.channel_id + key = bh2u(htlc.payment_hash) + if key not in self.invoices: + return + preimage, invoice, direction, timestamp = self.invoices.get(key) if direction == SENT: - assert htlc.payment_hash not in self.invoices - self.inflight.pop(key) - self.wallet.storage.put('lightning_payments_inflight', self.inflight) - if not preimage: - preimage, _addr = self.get_invoice(htlc.payment_hash) - tupl = (time.time(), direction, json.loads(encoder.encode(htlc)), bh2u(preimage), bh2u(chan_id)) - self.completed[key] = tupl - self.wallet.storage.put('lightning_payments_completed', self.completed) + preimage = _preimage + now = time.time() + self.invoices[key] = preimage, invoice, direction, now + self.wallet.storage.put('lightning_invoices', self.invoices) self.wallet.storage.write() - self.network.trigger_callback('ln_payment_completed', tupl[0], direction, htlc, preimage, chan_id) - - def get_invoice_status(self, key): - from electrum.util import PR_UNPAID, PR_EXPIRED, PR_PAID, PR_UNKNOWN, PR_INFLIGHT - if key in self.completed: - return PR_PAID - elif key in self.inflight: - return PR_INFLIGHT - elif key in self.invoices: - return PR_UNPAID - else: + self.network.trigger_callback('ln_payment_completed', now, direction, htlc, preimage, chan_id) + + def get_invoice_status(self, payment_hash): + if payment_hash not in self.invoices: return PR_UNKNOWN + preimage, _addr, direction, timestamp = self.invoices.get(payment_hash) + if timestamp is None: + return PR_UNPAID + return PR_PAID - def _list_invoices(self, chan_id=None): - invoices = dict(self.invoices) - settled = [] - unsettled = [] - inflight = [] - for date, direction, htlc, hex_preimage, hex_chan_id in self.completed.values(): - direction = Direction(direction) - if chan_id is not None: - if bfh(hex_chan_id) != chan_id: - continue - htlcobj = UpdateAddHtlc(*htlc) - if direction == RECEIVED: - preimage = bfh(invoices.pop(bh2u(htlcobj.payment_hash))[0]) - else: - preimage = bfh(hex_preimage) - # FIXME use fromisoformat when minimum Python is 3.7 - settled.append((datetime.fromtimestamp(date, timezone.utc), direction, htlcobj, preimage)) - for preimage, pay_req in invoices.values(): - addr = lndecode(pay_req, expected_hrp=constants.net.SEGWIT_HRP) - unsettled.append((addr, bfh(preimage), pay_req)) - for pay_req, amount_sat, this_chan_id in self.inflight.values(): - if chan_id is not None and bfh(this_chan_id) != chan_id: - continue - addr = lndecode(pay_req, expected_hrp=constants.net.SEGWIT_HRP) - if amount_sat is not None: - addr.amount = Decimal(amount_sat) / COIN - htlc = self.find_htlc_for_addr(addr, None if chan_id is None else [chan_id]) - if not htlc: - self.print_error('Warning, in-flight HTLC not found in any channel') - inflight.append((addr, htlc, SENT)) - # not adding received htlcs to inflight because they should have been settled - # immediatly and therefore let's not spend time trying to show it in the GUI - return {'settled': settled, 'unsettled': unsettled, 'inflight': inflight} - - def find_htlc_for_addr(self, addr, whitelist=None): - channels = [y for x,y in self.channels.items() if whitelist is None or x in whitelist] - for chan in channels: - for htlc in chan.hm.log[LOCAL]['adds'].values(): - if htlc.payment_hash == addr.paymenthash: - return htlc + def get_payments(self): + # note: with AMP we will have several channels per payment + out = {} + for chan in self.channels.values(): + out.update(chan.get_payments()) + return out def _read_ln_keystore(self) -> BIP32_KeyStore: xprv = self.wallet.storage.get('lightning_privkey2') @@ -447,9 +406,6 @@ class LNWorker(PrintError): break else: assert False, 'Found route with short channel ID we don\'t have: ' + repr(route[0].short_channel_id) - self.inflight[bh2u(addr.paymenthash)] = (invoice, amount_sat, bh2u(chan_id)) - self.wallet.storage.put('lightning_payments_inflight', self.inflight) - self.wallet.storage.write() return addr, peer, self._pay_to_route(route, addr) async def _pay_to_route(self, route, addr): @@ -545,14 +501,20 @@ class LNWorker(PrintError): ('c', MIN_FINAL_CLTV_EXPIRY_FOR_INVOICE)] + routing_hints), self.node_keypair.privkey) - self.invoices[bh2u(RHASH)] = (bh2u(payment_preimage), pay_req) + + self.save_invoice(bh2u(payment_preimage), pay_req, RECEIVED) + return pay_req + + def save_invoice(self, preimage, invoice, direction): + lnaddr = lndecode(invoice, expected_hrp=constants.net.SEGWIT_HRP) + key = bh2u(lnaddr.paymenthash) + self.invoices[key] = preimage, invoice, direction, None self.wallet.storage.put('lightning_invoices', self.invoices) self.wallet.storage.write() - return pay_req def get_invoice(self, payment_hash: bytes) -> Tuple[bytes, LnAddr]: try: - preimage_hex, pay_req = self.invoices[bh2u(payment_hash)] + preimage_hex, pay_req, direction,timestamp = self.invoices[bh2u(payment_hash)] preimage = bfh(preimage_hex) assert sha256(preimage) == payment_hash return preimage, lndecode(pay_req, expected_hrp=constants.net.SEGWIT_HRP) diff --git a/electrum/util.py b/electrum/util.py @@ -80,6 +80,14 @@ PR_UNKNOWN = 2 # sent but not propagated PR_PAID = 3 # send and propagated PR_INFLIGHT = 4 # lightning +pr_tooltips = { + PR_UNPAID:_('Pending'), + PR_PAID:_('Paid'), + PR_UNKNOWN:_('Unknown'), + PR_EXPIRED:_('Expired'), + PR_INFLIGHT:_('Inflight') +} + class UnknownBaseUnit(Exception): pass