electrum

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

swap_dialog.py (12371B)


      1 from typing import TYPE_CHECKING, Optional
      2 
      3 from PyQt5.QtWidgets import QLabel, QVBoxLayout, QGridLayout, QPushButton
      4 
      5 from electrum.i18n import _
      6 from electrum.util import NotEnoughFunds, NoDynamicFeeEstimates
      7 from electrum.lnutil import ln_dummy_address
      8 from electrum.transaction import PartialTxOutput, PartialTransaction
      9 
     10 from .util import (WindowModalDialog, Buttons, OkButton, CancelButton,
     11                    EnterButton, ColorScheme, WWLabel, read_QIcon, IconLabel)
     12 from .amountedit import BTCAmountEdit
     13 from .fee_slider import FeeSlider, FeeComboBox
     14 
     15 if TYPE_CHECKING:
     16     from .main_window import ElectrumWindow
     17 
     18 CANNOT_RECEIVE_WARNING = """
     19 The requested amount is higher than what you can receive in your currently open channels.
     20 If you continue, your funds will be locked until the remote server can find a path to pay you.
     21 If the swap cannot be performed after 24h, you will be refunded.
     22 Do you want to continue?
     23 """
     24 
     25 
     26 class SwapDialog(WindowModalDialog):
     27 
     28     tx: Optional[PartialTransaction]
     29 
     30     def __init__(self, window: 'ElectrumWindow'):
     31         WindowModalDialog.__init__(self, window, _('Submarine Swap'))
     32         self.window = window
     33         self.config = window.config
     34         self.lnworker = self.window.wallet.lnworker
     35         self.swap_manager = self.lnworker.swap_manager
     36         self.network = window.network
     37         self.tx = None  # for the forward-swap only
     38         self.is_reverse = True
     39         vbox = QVBoxLayout(self)
     40         self.description_label = WWLabel(self.get_description())
     41         self.send_amount_e = BTCAmountEdit(self.window.get_decimal_point)
     42         self.recv_amount_e = BTCAmountEdit(self.window.get_decimal_point)
     43         self.max_button = EnterButton(_("Max"), self.spend_max)
     44         self.max_button.setFixedWidth(100)
     45         self.max_button.setCheckable(True)
     46         self.toggle_button = QPushButton(u'\U000021c4')
     47         # send_follows is used to know whether the send amount field / receive
     48         # amount field should be adjusted after the fee slider was moved
     49         self.send_follows = False
     50         self.send_amount_e.follows = False
     51         self.recv_amount_e.follows = False
     52         self.toggle_button.clicked.connect(self.toggle_direction)
     53         # textChanged is triggered for both user and automatic action
     54         self.send_amount_e.textChanged.connect(self.on_send_edited)
     55         self.recv_amount_e.textChanged.connect(self.on_recv_edited)
     56         # textEdited is triggered only for user editing of the fields
     57         self.send_amount_e.textEdited.connect(self.uncheck_max)
     58         self.recv_amount_e.textEdited.connect(self.uncheck_max)
     59         fee_slider = FeeSlider(self.window, self.config, self.fee_slider_callback)
     60         fee_combo = FeeComboBox(fee_slider)
     61         fee_slider.update()
     62         self.fee_label = QLabel()
     63         self.server_fee_label = QLabel()
     64         vbox.addWidget(self.description_label)
     65         h = QGridLayout()
     66         self.send_label = IconLabel(text=_('You send')+':')
     67         self.recv_label = IconLabel(text=_('You receive')+':')
     68         h.addWidget(self.send_label, 1, 0)
     69         h.addWidget(self.send_amount_e, 1, 1)
     70         h.addWidget(self.max_button, 1, 2)
     71         h.addWidget(self.toggle_button, 1, 3)
     72         h.addWidget(self.recv_label, 2, 0)
     73         h.addWidget(self.recv_amount_e, 2, 1)
     74         h.addWidget(QLabel(_('Server fee')+':'), 4, 0)
     75         h.addWidget(self.server_fee_label, 4, 1, 1, 2)
     76         h.addWidget(QLabel(_('Mining fee')+':'), 5, 0)
     77         h.addWidget(self.fee_label, 5, 1, 1, 2)
     78         h.addWidget(fee_slider, 6, 1)
     79         h.addWidget(fee_combo, 6, 2)
     80         vbox.addLayout(h)
     81         vbox.addStretch(1)
     82         self.ok_button = OkButton(self)
     83         self.ok_button.setDefault(True)
     84         self.ok_button.setEnabled(False)
     85         vbox.addLayout(Buttons(CancelButton(self), self.ok_button))
     86         self.update()
     87 
     88     def fee_slider_callback(self, dyn, pos, fee_rate):
     89         if dyn:
     90             if self.config.use_mempool_fees():
     91                 self.config.set_key('depth_level', pos, False)
     92             else:
     93                 self.config.set_key('fee_level', pos, False)
     94         else:
     95             self.config.set_key('fee_per_kb', fee_rate, False)
     96         if self.send_follows:
     97             self.on_recv_edited()
     98         else:
     99             self.on_send_edited()
    100         self.update()
    101 
    102     def toggle_direction(self):
    103         self.is_reverse = not self.is_reverse
    104         self.send_amount_e.setAmount(None)
    105         self.recv_amount_e.setAmount(None)
    106         self.max_button.setChecked(False)
    107         self.update()
    108 
    109     def spend_max(self):
    110         if self.max_button.isChecked():
    111             if self.is_reverse:
    112                 self._spend_max_reverse_swap()
    113             else:
    114                 self._spend_max_forward_swap()
    115         else:
    116             self.send_amount_e.setAmount(None)
    117         self.update_fee()
    118         self.update_ok_button()
    119 
    120     def uncheck_max(self):
    121         self.max_button.setChecked(False)
    122         self.update()
    123 
    124     def _spend_max_forward_swap(self):
    125         self._update_tx('!')
    126         if self.tx:
    127             amount = self.tx.output_value_for_address(ln_dummy_address())
    128             max_swap_amt = self.swap_manager.get_max_amount()
    129             max_recv_amt = int(self.lnworker.num_sats_can_receive())
    130             max_amt = min(max_swap_amt, max_recv_amt)
    131             if amount > max_amt:
    132                 amount = max_amt
    133                 self._update_tx(amount)
    134             if self.tx:
    135                 amount = self.tx.output_value_for_address(ln_dummy_address())
    136                 assert amount <= max_amt
    137                 self.send_amount_e.setAmount(amount)
    138 
    139     def _spend_max_reverse_swap(self):
    140         amount = min(self.lnworker.num_sats_can_send(), self.swap_manager.get_max_amount())
    141         self.send_amount_e.setAmount(amount)
    142 
    143     def on_send_edited(self):
    144         if self.send_amount_e.follows:
    145             return
    146         self.send_amount_e.setStyleSheet(ColorScheme.DEFAULT.as_stylesheet())
    147         send_amount = self.send_amount_e.get_amount()
    148         recv_amount = self.swap_manager.get_recv_amount(send_amount, self.is_reverse)
    149         if self.is_reverse and send_amount and send_amount > self.lnworker.num_sats_can_send():
    150             # cannot send this much on lightning
    151             recv_amount = None
    152         if (not self.is_reverse) and recv_amount and recv_amount > self.lnworker.num_sats_can_receive():
    153             # cannot receive this much on lightning
    154             recv_amount = None
    155         self.recv_amount_e.follows = True
    156         self.recv_amount_e.setAmount(recv_amount)
    157         self.recv_amount_e.setStyleSheet(ColorScheme.BLUE.as_stylesheet())
    158         self.recv_amount_e.follows = False
    159         self.send_follows = False
    160         self._update_tx(send_amount)
    161         self.update_fee()
    162         self.update_ok_button()
    163 
    164     def on_recv_edited(self):
    165         if self.recv_amount_e.follows:
    166             return
    167         self.recv_amount_e.setStyleSheet(ColorScheme.DEFAULT.as_stylesheet())
    168         recv_amount = self.recv_amount_e.get_amount()
    169         send_amount = self.swap_manager.get_send_amount(recv_amount, self.is_reverse)
    170         if self.is_reverse and send_amount and send_amount > self.lnworker.num_sats_can_send():
    171             send_amount = None
    172         self.send_amount_e.follows = True
    173         self.send_amount_e.setAmount(send_amount)
    174         self.send_amount_e.setStyleSheet(ColorScheme.BLUE.as_stylesheet())
    175         self.send_amount_e.follows = False
    176         self.send_follows = True
    177         self._update_tx(send_amount)
    178         self.update_fee()
    179         self.update_ok_button()
    180 
    181     def update(self):
    182         from .util import IconLabel
    183         sm = self.swap_manager
    184         send_icon = read_QIcon("lightning.png" if self.is_reverse else "bitcoin.png")
    185         self.send_label.setIcon(send_icon)
    186         recv_icon = read_QIcon("lightning.png" if not self.is_reverse else "bitcoin.png")
    187         self.recv_label.setIcon(recv_icon)
    188         self.description_label.setText(self.get_description())
    189         self.description_label.repaint()  # macOS hack for #6269
    190         server_mining_fee = sm.lockup_fee if self.is_reverse else sm.normal_fee
    191         server_fee_str = '%.2f'%sm.percentage + '%  +  '  + self.window.format_amount(server_mining_fee) + ' ' + self.window.base_unit()
    192         self.server_fee_label.setText(server_fee_str)
    193         self.server_fee_label.repaint()  # macOS hack for #6269
    194         self.update_tx()
    195         self.update_fee()
    196         self.update_ok_button()
    197 
    198     def update_fee(self):
    199         """Updates self.fee_label. No other side-effects."""
    200         if self.is_reverse:
    201             sm = self.swap_manager
    202             fee = sm.get_claim_fee()
    203         else:
    204             fee = self.tx.get_fee() if self.tx else None
    205         fee_text = self.window.format_amount(fee) + ' ' + self.window.base_unit() if fee else ''
    206         self.fee_label.setText(fee_text)
    207         self.fee_label.repaint()  # macOS hack for #6269
    208 
    209     def run(self):
    210         if not self.network:
    211             self.window.show_error(_("You are offline."))
    212             return
    213         self.window.run_coroutine_from_thread(self.swap_manager.get_pairs(), lambda x: self.update())
    214         if not self.exec_():
    215             return
    216         if self.is_reverse:
    217             lightning_amount = self.send_amount_e.get_amount()
    218             onchain_amount = self.recv_amount_e.get_amount()
    219             if lightning_amount is None or onchain_amount is None:
    220                 return
    221             coro = self.swap_manager.reverse_swap(
    222                 lightning_amount_sat=lightning_amount,
    223                 expected_onchain_amount_sat=onchain_amount + self.swap_manager.get_claim_fee(),
    224             )
    225             self.window.run_coroutine_from_thread(coro)
    226         else:
    227             lightning_amount = self.recv_amount_e.get_amount()
    228             onchain_amount = self.send_amount_e.get_amount()
    229             if lightning_amount is None or onchain_amount is None:
    230                 return
    231             if lightning_amount > self.lnworker.num_sats_can_receive():
    232                 if not self.window.question(CANNOT_RECEIVE_WARNING):
    233                     return
    234             self.window.protect(self.do_normal_swap, (lightning_amount, onchain_amount))
    235 
    236     def update_tx(self):
    237         if self.is_reverse:
    238             return
    239         is_max = self.max_button.isChecked()
    240         if is_max:
    241             self._spend_max_forward_swap()
    242         else:
    243             onchain_amount = self.send_amount_e.get_amount()
    244             self._update_tx(onchain_amount)
    245 
    246     def _update_tx(self, onchain_amount):
    247         """Updates self.tx. No other side-effects."""
    248         if self.is_reverse:
    249             return
    250         if onchain_amount is None:
    251             self.tx = None
    252             return
    253         outputs = [PartialTxOutput.from_address_and_value(ln_dummy_address(), onchain_amount)]
    254         coins = self.window.get_coins()
    255         try:
    256             self.tx = self.window.wallet.make_unsigned_transaction(
    257                 coins=coins,
    258                 outputs=outputs)
    259         except (NotEnoughFunds, NoDynamicFeeEstimates) as e:
    260             self.tx = None
    261 
    262     def update_ok_button(self):
    263         """Updates self.ok_button. No other side-effects."""
    264         send_amount = self.send_amount_e.get_amount()
    265         recv_amount = self.recv_amount_e.get_amount()
    266         self.ok_button.setEnabled(
    267             (send_amount is not None)
    268             and (recv_amount is not None)
    269             and (self.tx is not None or self.is_reverse)
    270         )
    271 
    272     def do_normal_swap(self, lightning_amount, onchain_amount, password):
    273         tx = self.tx
    274         assert tx
    275         coro = self.swap_manager.normal_swap(
    276             lightning_amount_sat=lightning_amount,
    277             expected_onchain_amount_sat=onchain_amount,
    278             password=password,
    279             tx=tx,
    280         )
    281         self.window.run_coroutine_from_thread(coro)
    282 
    283     def get_description(self):
    284         onchain_funds = "onchain funds"
    285         lightning_funds = "lightning funds"
    286 
    287         return "Swap {fromType} for {toType}. This will increase your {capacityType} capacity. This service is powered by the Boltz backend.".format(
    288             fromType=lightning_funds if self.is_reverse else onchain_funds,
    289             toType=onchain_funds if self.is_reverse else lightning_funds,
    290             capacityType="receiving" if self.is_reverse else "sending",
    291         )