URI: 
       tswap_dialog.py - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
       tswap_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         )