Browse Source

redo LNWorker pay:

- wait until htlc has been fulfilled
 - raise if htlc is not fulfilled
 - return boolean success
 - try multiple paths in GUI
regtest_lnd
ThomasV 6 years ago
committed by SomberNight
parent
commit
71d4f63921
No known key found for this signature in database GPG Key ID: B33B5F232C6271E9
  1. 3
      electrum/commands.py
  2. 18
      electrum/gui/qt/main_window.py
  3. 4
      electrum/lnpeer.py
  4. 21
      electrum/lnworker.py
  5. 2
      electrum/tests/regtest/regtest.sh
  6. 18
      electrum/tests/test_lnpeer.py

3
electrum/commands.py

@ -785,8 +785,7 @@ class Commands:
@command('wn') @command('wn')
def lnpay(self, invoice): def lnpay(self, invoice):
addr, peer, f = self.lnworker.pay(invoice) return self.lnworker.pay(invoice, timeout=10)
return f.result()
@command('wn') @command('wn')
def addinvoice(self, requested_amount, message): def addinvoice(self, requested_amount, message):

18
electrum/gui/qt/main_window.py

@ -1674,33 +1674,25 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
def pay_lightning_invoice(self, invoice): def pay_lightning_invoice(self, invoice):
amount = self.amount_e.get_amount() amount = self.amount_e.get_amount()
LN_NUM_PAYMENT_ATTEMPTS = 1 # TODO increase LN_NUM_PAYMENT_ATTEMPTS = 3
def on_success(result): def on_success(result):
self.logger.info(f'ln payment success. {result}') self.logger.info(f'ln payment success. {result}')
self.do_clear() self.do_clear()
def on_failure(exc_info): def on_failure(exc_info):
type_, e, traceback = exc_info type_, e, traceback = exc_info
if isinstance(e, PaymentFailure): if isinstance(e, PaymentFailure):
self.show_error(_('Payment failed. Tried {} times:\n{}') self.show_error(_('Payment failed. {}').format(e))
.format(LN_NUM_PAYMENT_ATTEMPTS, e))
elif isinstance(e, InvoiceError): elif isinstance(e, InvoiceError):
self.show_error(_('InvoiceError: {}').format(e)) self.show_error(_('InvoiceError: {}').format(e))
else: else:
raise e raise e
def task(): def task():
failure_list = []
for i in range(LN_NUM_PAYMENT_ATTEMPTS): for i in range(LN_NUM_PAYMENT_ATTEMPTS):
try: success = self.wallet.lnworker.pay(invoice, amount_sat=amount, timeout=30)
addr, peer, future = self.wallet.lnworker.pay(invoice, amount_sat=amount) if success:
future.result()
break break
except PaymentFailure as e:
failure_list.append(e)
# try again
else: else:
msg = '\n'.join(str(e) for e in failure_list) raise PaymentFailure('Failed after {i} attempts')
raise PaymentFailure(msg)
msg = _('Sending lightning payment...') msg = _('Sending lightning payment...')
WaitingDialog(self, msg, task, on_success, on_failure) WaitingDialog(self, msg, task, on_success, on_failure)

4
electrum/lnpeer.py

@ -973,7 +973,7 @@ class Peer(Logger):
@log_exceptions @log_exceptions
async def _on_update_fail_htlc(self, chan, htlc_id, local_ctn): async def _on_update_fail_htlc(self, chan, htlc_id, local_ctn):
await self.await_local(chan, 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): 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]
@ -1117,7 +1117,7 @@ class Peer(Logger):
@log_exceptions @log_exceptions
async def _on_update_fulfill_htlc(self, chan, htlc_id, preimage, local_ctn): async def _on_update_fulfill_htlc(self, chan, htlc_id, preimage, local_ctn):
await self.await_local(chan, 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) self.payment_preimages[sha256(preimage)].put_nowait(preimage)
def on_update_fail_malformed_htlc(self, payload): def on_update_fail_malformed_htlc(self, payload):

21
electrum/lnworker.py

@ -314,6 +314,7 @@ class LNWallet(LNWorker):
c.set_local_commitment(c.current_commitment(LOCAL)) c.set_local_commitment(c.current_commitment(LOCAL))
# timestamps of opening and closing transactions # timestamps of opening and closing transactions
self.channel_timestamps = self.storage.get('lightning_channel_timestamps', {}) self.channel_timestamps = self.storage.get('lightning_channel_timestamps', {})
self.pending_payments = defaultdict(asyncio.Future)
def start_network(self, network: 'Network'): def start_network(self, network: 'Network'):
self.network = network self.network = network
@ -641,14 +642,15 @@ class LNWallet(LNWorker):
chan = f.result(timeout) chan = f.result(timeout)
return chan.funding_outpoint.to_str() 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 Can be called from other threads
one thread only. 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(
fut = asyncio.run_coroutine_threadsafe(coro, self.network.asyncio_loop) self._pay(invoice, amount_sat),
return addr, peer, fut self.network.asyncio_loop)
return fut.result(timeout=timeout)
def get_channel_by_short_id(self, short_channel_id): def get_channel_by_short_id(self, short_channel_id):
with self.lock: with self.lock:
@ -656,15 +658,14 @@ class LNWallet(LNWorker):
if chan.short_channel_id == short_channel_id: if chan.short_channel_id == short_channel_id:
return chan 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) addr = self._check_invoice(invoice, amount_sat)
self.save_invoice(addr.paymenthash, invoice, SENT, is_paid=False) self.save_invoice(addr.paymenthash, invoice, SENT, is_paid=False)
self.wallet.set_label(bh2u(addr.paymenthash), addr.get_description()) self.wallet.set_label(bh2u(addr.paymenthash), addr.get_description())
route = await self._create_route_from_invoice(decoded_invoice=addr) 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): 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) 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): async def _pay_to_route(self, route, addr, pay_req):
short_channel_id = route[0].short_channel_id short_channel_id = route[0].short_channel_id
@ -674,6 +675,8 @@ class LNWallet(LNWorker):
peer = self.peers[route[0].node_id] 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()) 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) self.network.trigger_callback('htlc_added', htlc, addr, SENT)
success = await self.pending_payments[(short_channel_id, htlc.htlc_id)]
return success
@staticmethod @staticmethod
def _check_invoice(invoice, amount_sat=None): def _check_invoice(invoice, amount_sat=None):

2
electrum/tests/regtest/regtest.sh

@ -101,7 +101,7 @@ if [[ $1 == "redeem_htlcs" ]]; then
sleep 10 sleep 10
# alice pays bob # alice pays bob
invoice=$($bob addinvoice 0.05 "test") invoice=$($bob addinvoice 0.05 "test")
$alice lnpay $invoice $alice lnpay $invoice || true
sleep 1 sleep 1
settled=$($alice list_channels | jq '.[] | .local_htlcs | .settles | length') settled=$($alice list_channels | jq '.[] | .local_htlcs | .settles | length')
if [[ "$settled" != "0" ]]; then if [[ "$settled" != "0" ]]; then

18
electrum/tests/test_lnpeer.py

@ -90,6 +90,7 @@ class MockLNWallet:
self.inflight = {} self.inflight = {}
self.wallet = MockWallet() self.wallet = MockWallet()
self.localfeatures = LnLocalFeatures(0) self.localfeatures = LnLocalFeatures(0)
self.pending_payments = defaultdict(asyncio.Future)
@property @property
def lock(self): def lock(self):
@ -216,25 +217,12 @@ class TestPeer(SequentialTestCase):
w2.invoices[bh2u(RHASH)] = (pay_req, True, False) w2.invoices[bh2u(RHASH)] = (pay_req, True, False)
return pay_req 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): def test_payment(self):
p1, p2, w1, w2, _q1, _q2 = self.prepare_peers() p1, p2, w1, w2, _q1, _q2 = self.prepare_peers()
pay_req = self.prepare_invoice(w2) pay_req = self.prepare_invoice(w2)
fut = self.prepare_ln_message_future(w2)
async def pay(): async def pay():
addr, peer, coro = await LNWallet._pay(w1, pay_req, same_thread=True) result = await LNWallet._pay(w1, pay_req)
await coro self.assertEqual(result, True)
print("HTLC ADDED")
self.assertEqual(await fut, 'Payment received')
gath.cancel() gath.cancel()
gath = asyncio.gather(pay(), p1._message_loop(), p2._message_loop()) gath = asyncio.gather(pay(), p1._message_loop(), p2._message_loop())
async def f(): async def f():

Loading…
Cancel
Save