electrum

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

commit 6d9ef2969030d9dac8111f36e1044621bcb15840
parent 669b84fbd68a8ea56d91053dd7a39143e4b1e310
Author: ThomasV <thomasv@electrum.org>
Date:   Thu, 23 May 2019 12:37:24 +0200

redo LNWorker pay:
 - wait until htlc has been fulfilled
 - raise if htlc is not fulfilled
 - return boolean success
 - try multiple paths in GUI

Diffstat:
Melectrum/commands.py | 3+--
Melectrum/gui/qt/main_window.py | 18+++++-------------
Melectrum/lnpeer.py | 4++--
Melectrum/lnworker.py | 21++++++++++++---------
Melectrum/tests/regtest/regtest.sh | 2+-
Melectrum/tests/test_lnpeer.py | 18+++---------------
6 files changed, 24 insertions(+), 42 deletions(-)

diff --git a/electrum/commands.py b/electrum/commands.py @@ -781,8 +781,7 @@ class Commands: @command('wn') def lnpay(self, invoice): - addr, peer, f = self.lnworker.pay(invoice) - return f.result() + return self.lnworker.pay(invoice, timeout=10) @command('wn') def addinvoice(self, requested_amount, message): diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py @@ -1675,33 +1675,25 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): def pay_lightning_invoice(self, invoice): amount = self.amount_e.get_amount() - LN_NUM_PAYMENT_ATTEMPTS = 1 # TODO increase - + LN_NUM_PAYMENT_ATTEMPTS = 3 def on_success(result): self.logger.info(f'ln payment success. {result}') self.do_clear() def on_failure(exc_info): type_, e, traceback = exc_info if isinstance(e, PaymentFailure): - self.show_error(_('Payment failed. Tried {} times:\n{}') - .format(LN_NUM_PAYMENT_ATTEMPTS, e)) + self.show_error(_('Payment failed. {}').format(e)) elif isinstance(e, InvoiceError): self.show_error(_('InvoiceError: {}').format(e)) else: raise e def task(): - failure_list = [] for i in range(LN_NUM_PAYMENT_ATTEMPTS): - try: - addr, peer, future = self.wallet.lnworker.pay(invoice, amount_sat=amount) - future.result() + success = self.wallet.lnworker.pay(invoice, amount_sat=amount, timeout=30) + if success: break - except PaymentFailure as e: - failure_list.append(e) - # try again else: - msg = '\n'.join(str(e) for e in failure_list) - raise PaymentFailure(msg) + raise PaymentFailure('Failed after {i} attempts') msg = _('Sending lightning payment...') WaitingDialog(self, msg, task, on_success, on_failure) diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py @@ -973,7 +973,7 @@ class Peer(Logger): @log_exceptions async def _on_update_fail_htlc(self, chan, htlc_id, local_ctn): await self.await_local(chan, local_ctn) - self.network.trigger_callback('ln_message', self.lnworker, 'Payment failed', htlc_id) + self.lnworker.pending_payments[(chan.short_channel_id, htlc_id)].set_result(False) def _handle_error_code_from_failed_htlc(self, error_reason, route: List['RouteEdge'], channel_id, htlc_id): chan = self.channels[channel_id] @@ -1117,7 +1117,7 @@ class Peer(Logger): @log_exceptions async def _on_update_fulfill_htlc(self, chan, htlc_id, preimage, local_ctn): await self.await_local(chan, local_ctn) - self.network.trigger_callback('ln_message', self.lnworker, 'Payment sent', htlc_id) + self.lnworker.pending_payments[(chan.short_channel_id, htlc_id)].set_result(True) self.payment_preimages[sha256(preimage)].put_nowait(preimage) def on_update_fail_malformed_htlc(self, payload): diff --git a/electrum/lnworker.py b/electrum/lnworker.py @@ -314,6 +314,7 @@ class LNWallet(LNWorker): c.set_local_commitment(c.current_commitment(LOCAL)) # timestamps of opening and closing transactions self.channel_timestamps = self.storage.get('lightning_channel_timestamps', {}) + self.pending_payments = defaultdict(asyncio.Future) def start_network(self, network: 'Network'): self.network = network @@ -641,14 +642,15 @@ class LNWallet(LNWorker): chan = f.result(timeout) return chan.funding_outpoint.to_str() - def pay(self, invoice, amount_sat=None): + def pay(self, invoice, amount_sat=None, timeout=10): """ - This is not merged with _pay so that we can run the test with - one thread only. + Can be called from other threads + Raises timeout exception if htlc is not fulfilled """ - addr, peer, coro = self.network.run_from_another_thread(self._pay(invoice, amount_sat)) - fut = asyncio.run_coroutine_threadsafe(coro, self.network.asyncio_loop) - return addr, peer, fut + fut = asyncio.run_coroutine_threadsafe( + self._pay(invoice, amount_sat), + self.network.asyncio_loop) + return fut.result(timeout=timeout) def get_channel_by_short_id(self, short_channel_id): with self.lock: @@ -656,15 +658,14 @@ class LNWallet(LNWorker): if chan.short_channel_id == short_channel_id: return chan - async def _pay(self, invoice, amount_sat=None, same_thread=False): + async def _pay(self, invoice, amount_sat=None): addr = self._check_invoice(invoice, amount_sat) self.save_invoice(addr.paymenthash, invoice, SENT, is_paid=False) self.wallet.set_label(bh2u(addr.paymenthash), addr.get_description()) route = await self._create_route_from_invoice(decoded_invoice=addr) - peer = self.peers[route[0].node_id] if not self.get_channel_by_short_id(route[0].short_channel_id): assert False, 'Found route with short channel ID we don\'t have: ' + repr(route[0].short_channel_id) - return addr, peer, self._pay_to_route(route, addr, invoice) + return await self._pay_to_route(route, addr, invoice) async def _pay_to_route(self, route, addr, pay_req): short_channel_id = route[0].short_channel_id @@ -674,6 +675,8 @@ class LNWallet(LNWorker): peer = self.peers[route[0].node_id] 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) + success = await self.pending_payments[(short_channel_id, htlc.htlc_id)] + return success @staticmethod def _check_invoice(invoice, amount_sat=None): diff --git a/electrum/tests/regtest/regtest.sh b/electrum/tests/regtest/regtest.sh @@ -101,7 +101,7 @@ if [[ $1 == "redeem_htlcs" ]]; then sleep 10 # alice pays bob invoice=$($bob addinvoice 0.05 "test") - $alice lnpay $invoice + $alice lnpay $invoice || true sleep 1 settled=$($alice list_channels | jq '.[] | .local_htlcs | .settles | length') if [[ "$settled" != "0" ]]; then diff --git a/electrum/tests/test_lnpeer.py b/electrum/tests/test_lnpeer.py @@ -90,6 +90,7 @@ class MockLNWallet: self.inflight = {} self.wallet = MockWallet() self.localfeatures = LnLocalFeatures(0) + self.pending_payments = defaultdict(asyncio.Future) @property def lock(self): @@ -216,25 +217,12 @@ class TestPeer(SequentialTestCase): w2.invoices[bh2u(RHASH)] = (pay_req, True, False) return pay_req - @staticmethod - def prepare_ln_message_future(w2 # receiver - ): - fut = asyncio.Future() - def evt_set(event, _lnwallet, msg, _htlc_id): - fut.set_result(msg) - w2.network.register_callback(evt_set, ['ln_message']) - return fut - def test_payment(self): p1, p2, w1, w2, _q1, _q2 = self.prepare_peers() pay_req = self.prepare_invoice(w2) - fut = self.prepare_ln_message_future(w2) - async def pay(): - addr, peer, coro = await LNWallet._pay(w1, pay_req, same_thread=True) - await coro - print("HTLC ADDED") - self.assertEqual(await fut, 'Payment received') + result = await LNWallet._pay(w1, pay_req) + self.assertEqual(result, True) gath.cancel() gath = asyncio.gather(pay(), p1._message_loop(), p2._message_loop()) async def f():