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 )