Show options if we do not have the liquidity to pay a lightning invoice:
pay onchain, open channel, rebalance. If we do a swap or open a channel, the payment will be scheduled.
This commit is contained in:
@@ -383,7 +383,7 @@ class ChannelsList(MyTreeView):
|
||||
vbox.addLayout(Buttons(OkButton(d)))
|
||||
d.exec_()
|
||||
|
||||
def new_channel_dialog(self):
|
||||
def new_channel_dialog(self, *, amount_sat=None):
|
||||
lnworker = self.parent.wallet.lnworker
|
||||
d = WindowModalDialog(self.parent, _('Open Channel'))
|
||||
vbox = QVBoxLayout(d)
|
||||
@@ -413,6 +413,7 @@ class ChannelsList(MyTreeView):
|
||||
trampoline_combo.setCurrentIndex(1)
|
||||
|
||||
amount_e = BTCAmountEdit(self.parent.get_decimal_point)
|
||||
amount_e.setAmount(amount_sat)
|
||||
# max button
|
||||
def spend_max():
|
||||
amount_e.setFrozen(max_button.isChecked())
|
||||
@@ -481,6 +482,7 @@ class ChannelsList(MyTreeView):
|
||||
if not connect_str or not funding_sat:
|
||||
return
|
||||
self.parent.open_channel(connect_str, funding_sat, 0)
|
||||
return True
|
||||
|
||||
def swap_dialog(self):
|
||||
from .swap_dialog import SwapDialog
|
||||
|
||||
@@ -33,7 +33,7 @@ from PyQt5.QtWidgets import QMenu, QVBoxLayout, QTreeWidget, QTreeWidgetItem, QH
|
||||
|
||||
from electrum.i18n import _
|
||||
from electrum.util import format_time
|
||||
from electrum.invoices import Invoice, PR_UNPAID, PR_PAID, PR_INFLIGHT, PR_FAILED
|
||||
from electrum.invoices import Invoice, PR_UNPAID, PR_PAID, PR_INFLIGHT, PR_FAILED, PR_SCHEDULED
|
||||
from electrum.lnutil import HtlcLog
|
||||
|
||||
from .util import MyTreeView, read_QIcon, MySortModel, pr_icons
|
||||
@@ -161,6 +161,8 @@ class InvoiceList(MyTreeView):
|
||||
status = wallet.get_invoice_status(invoice)
|
||||
if status == PR_UNPAID:
|
||||
menu.addAction(_("Pay") + "...", lambda: self.parent.do_pay_invoice(invoice))
|
||||
if status == PR_SCHEDULED:
|
||||
menu.addAction(_("Cancel") + "...", lambda: self.parent.cancel_scheduled_invoice(key))
|
||||
if status == PR_FAILED:
|
||||
menu.addAction(_("Retry"), lambda: self.parent.do_pay_invoice(invoice))
|
||||
if self.parent.wallet.lnworker:
|
||||
|
||||
@@ -67,7 +67,7 @@ from electrum.util import (format_time,
|
||||
AddTransactionException, BITCOIN_BIP21_URI_SCHEME,
|
||||
InvoiceError, parse_max_spend)
|
||||
from electrum.invoices import PR_DEFAULT_EXPIRATION_WHEN_CREATING, Invoice
|
||||
from electrum.invoices import PR_PAID, PR_FAILED, pr_expiration_values, Invoice
|
||||
from electrum.invoices import PR_PAID, PR_UNPAID, PR_FAILED, PR_SCHEDULED, pr_expiration_values, Invoice
|
||||
from electrum.transaction import (Transaction, PartialTxInput,
|
||||
PartialTransaction, PartialTxOutput)
|
||||
from electrum.wallet import (Multisig_Wallet, CannotBumpFee, Abstract_Wallet,
|
||||
@@ -105,6 +105,7 @@ from .confirm_tx_dialog import ConfirmTxDialog
|
||||
from .transaction_dialog import PreviewTxDialog
|
||||
from .rbf_dialog import BumpFeeDialog, DSCancelDialog
|
||||
from .qrreader import scan_qrcode
|
||||
from .swap_dialog import SwapDialog
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import ElectrumGui
|
||||
@@ -1654,16 +1655,65 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
|
||||
|
||||
return False # no errors
|
||||
|
||||
def pay_lightning_invoice(self, invoice: str, *, amount_msat: Optional[int]):
|
||||
if amount_msat is None:
|
||||
def pay_lightning_invoice(self, invoice: Invoice):
|
||||
amount_sat = invoice.get_amount_sat()
|
||||
key = self.wallet.get_key_for_outgoing_invoice(invoice)
|
||||
if amount_sat is None:
|
||||
raise Exception("missing amount for LN invoice")
|
||||
amount_sat = Decimal(amount_msat) / 1000
|
||||
num_sats_can_send = int(self.wallet.lnworker.num_sats_can_send())
|
||||
if amount_sat > num_sats_can_send:
|
||||
lightning_needed = amount_sat - num_sats_can_send
|
||||
lightning_needed += (lightning_needed // 20) # operational safety margin
|
||||
coins = self.get_coins(nonlocal_only=True)
|
||||
can_pay_onchain = invoice.get_address() and self.wallet.can_pay_onchain(invoice.get_outputs(), coins=coins)
|
||||
can_pay_with_new_channel, channel_funding_sat = self.wallet.can_pay_with_new_channel(amount_sat, coins=coins)
|
||||
can_pay_with_swap, swap_recv_amount_sat = self.wallet.can_pay_with_swap(amount_sat, coins=coins)
|
||||
choices = {}
|
||||
if can_pay_onchain:
|
||||
msg = ''.join([
|
||||
_('Pay this invoice onchain'), '\n',
|
||||
_('Funds will be sent to the invoice fallback address.')
|
||||
])
|
||||
choices[0] = msg
|
||||
if can_pay_with_new_channel:
|
||||
msg = ''.join([
|
||||
_('Open a new channel'), '\n',
|
||||
_('Your payment will be scheduled for when the channel is open.')
|
||||
])
|
||||
choices[1] = msg
|
||||
if can_pay_with_swap:
|
||||
msg = ''.join([
|
||||
_('Rebalance your channels with a submarine swap'), '\n',
|
||||
_('Your payment will be scheduled after the swap is confirmed.')
|
||||
])
|
||||
choices[2] = msg
|
||||
if not choices:
|
||||
raise NotEnoughFunds()
|
||||
msg = _('You cannot pay that invoice using Lightning.')
|
||||
if self.wallet.lnworker.channels:
|
||||
msg += '\n' + _('Your channels can send {}.').format(self.format_amount(num_sats_can_send) + self.base_unit())
|
||||
|
||||
r = self.query_choice(msg, choices)
|
||||
if r is not None:
|
||||
self.save_pending_invoice()
|
||||
if r == 0:
|
||||
self.pay_onchain_dialog(coins, invoice.get_outputs())
|
||||
elif r == 1:
|
||||
if self.channels_list.new_channel_dialog(amount_sat=channel_funding_sat):
|
||||
self.wallet.lnworker.set_invoice_status(key, PR_SCHEDULED)
|
||||
elif r == 2:
|
||||
d = SwapDialog(self, is_reverse=False, recv_amount_sat=swap_recv_amount_sat)
|
||||
if d.run():
|
||||
self.wallet.lnworker.set_invoice_status(key, PR_SCHEDULED)
|
||||
return
|
||||
|
||||
# FIXME this is currently lying to user as we truncate to satoshis
|
||||
msg = _("Pay lightning invoice?") + '\n\n' + _("This will send {}?").format(self.format_amount_and_units(amount_sat))
|
||||
amount_msat = invoice.get_amount_msat()
|
||||
msg = _("Pay lightning invoice?") + '\n\n' + _("This will send {}?").format(self.format_amount_and_units(Decimal(amount_msat)/1000))
|
||||
if not self.question(msg):
|
||||
return
|
||||
self.save_pending_invoice()
|
||||
coro = self.wallet.lnworker.pay_invoice(invoice, amount_msat=amount_msat, attempts=LN_NUM_PAYMENT_ATTEMPTS)
|
||||
coro = self.wallet.lnworker.pay_invoice(invoice.lightning_invoice, amount_msat=amount_msat, attempts=LN_NUM_PAYMENT_ATTEMPTS)
|
||||
self.run_coroutine_from_thread(coro)
|
||||
|
||||
def on_request_status(self, wallet, key, status):
|
||||
@@ -1765,10 +1815,13 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
|
||||
|
||||
def do_pay_invoice(self, invoice: 'Invoice'):
|
||||
if invoice.is_lightning():
|
||||
self.pay_lightning_invoice(invoice.lightning_invoice, amount_msat=invoice.get_amount_msat())
|
||||
self.pay_lightning_invoice(invoice)
|
||||
else:
|
||||
self.pay_onchain_dialog(self.get_coins(), invoice.outputs)
|
||||
|
||||
def cancel_scheduled_invoice(self, key):
|
||||
self.wallet.lnworker.set_invoice_status(key, PR_UNPAID)
|
||||
|
||||
def get_coins(self, *, nonlocal_only=False) -> Sequence[PartialTxInput]:
|
||||
coins = self.get_manually_selected_coins()
|
||||
if coins is not None:
|
||||
@@ -2002,7 +2055,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
|
||||
clayout = ChoicesLayout(msg, choices)
|
||||
vbox = QVBoxLayout(dialog)
|
||||
vbox.addLayout(clayout.layout())
|
||||
vbox.addLayout(Buttons(OkButton(dialog)))
|
||||
vbox.addLayout(Buttons(CancelButton(dialog), OkButton(dialog)))
|
||||
if not dialog.exec_():
|
||||
return None
|
||||
return clayout.selected_index()
|
||||
|
||||
@@ -29,7 +29,7 @@ class SwapDialog(WindowModalDialog):
|
||||
tx: Optional[PartialTransaction]
|
||||
update_signal = pyqtSignal()
|
||||
|
||||
def __init__(self, window: 'ElectrumWindow'):
|
||||
def __init__(self, window: 'ElectrumWindow', is_reverse=True, recv_amount_sat=None):
|
||||
WindowModalDialog.__init__(self, window, _('Submarine Swap'))
|
||||
self.window = window
|
||||
self.config = window.config
|
||||
@@ -37,7 +37,7 @@ class SwapDialog(WindowModalDialog):
|
||||
self.swap_manager = self.lnworker.swap_manager
|
||||
self.network = window.network
|
||||
self.tx = None # for the forward-swap only
|
||||
self.is_reverse = True
|
||||
self.is_reverse = is_reverse
|
||||
vbox = QVBoxLayout(self)
|
||||
self.description_label = WWLabel(self.get_description())
|
||||
self.send_amount_e = BTCAmountEdit(self.window.get_decimal_point)
|
||||
@@ -87,6 +87,8 @@ class SwapDialog(WindowModalDialog):
|
||||
vbox.addLayout(Buttons(CancelButton(self), self.ok_button))
|
||||
self.update_signal.connect(self.update)
|
||||
self.update()
|
||||
if recv_amount_sat:
|
||||
self.recv_amount_e.setAmount(recv_amount_sat)
|
||||
|
||||
def fee_slider_callback(self, dyn, pos, fee_rate):
|
||||
if dyn:
|
||||
|
||||
@@ -28,7 +28,7 @@ from PyQt5.QtWidgets import (QPushButton, QLabel, QMessageBox, QHBoxLayout,
|
||||
|
||||
from electrum.i18n import _, languages
|
||||
from electrum.util import FileImportFailed, FileExportFailed, make_aiohttp_session, resource_path
|
||||
from electrum.invoices import PR_UNPAID, PR_PAID, PR_EXPIRED, PR_INFLIGHT, PR_UNKNOWN, PR_FAILED, PR_ROUTING, PR_UNCONFIRMED
|
||||
from electrum.invoices import PR_UNPAID, PR_PAID, PR_EXPIRED, PR_INFLIGHT, PR_UNKNOWN, PR_FAILED, PR_ROUTING, PR_UNCONFIRMED, PR_SCHEDULED
|
||||
from electrum.logging import Logger
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -56,6 +56,7 @@ pr_icons = {
|
||||
PR_FAILED:"warning.png",
|
||||
PR_ROUTING:"unconfirmed.png",
|
||||
PR_UNCONFIRMED:"unconfirmed.png",
|
||||
PR_SCHEDULED:"unconfirmed.png",
|
||||
}
|
||||
|
||||
|
||||
@@ -396,23 +397,23 @@ class ChoicesLayout(object):
|
||||
msg = ""
|
||||
gb2 = QGroupBox(msg)
|
||||
vbox.addWidget(gb2)
|
||||
|
||||
vbox2 = QVBoxLayout()
|
||||
gb2.setLayout(vbox2)
|
||||
|
||||
self.group = group = QButtonGroup()
|
||||
for i,c in enumerate(choices):
|
||||
if isinstance(choices, list):
|
||||
iterator = enumerate(choices)
|
||||
else:
|
||||
iterator = choices.items()
|
||||
for i, c in iterator:
|
||||
button = QRadioButton(gb2)
|
||||
button.setText(c)
|
||||
vbox2.addWidget(button)
|
||||
group.addButton(button)
|
||||
group.setId(button, i)
|
||||
if i==checked_index:
|
||||
if i == checked_index:
|
||||
button.setChecked(True)
|
||||
|
||||
if on_clicked:
|
||||
group.buttonClicked.connect(partial(on_clicked, self))
|
||||
|
||||
self.vbox = vbox
|
||||
|
||||
def layout(self):
|
||||
|
||||
Reference in New Issue
Block a user