Browse Source

Submarine swaps:

- improve gui
 - allow coin selection
 - allow spending 'max'
bip39-recovery
ThomasV 5 years ago
parent
commit
540dd73f3b
  1. 62
      electrum/gui/qt/swap_dialog.py
  2. 22
      electrum/submarine_swaps.py

62
electrum/gui/qt/swap_dialog.py

@ -9,7 +9,9 @@ from electrum.i18n import _
from electrum.lnchannel import AbstractChannel, PeerState from electrum.lnchannel import AbstractChannel, PeerState
from electrum.wallet import Abstract_Wallet from electrum.wallet import Abstract_Wallet
from electrum.lnutil import LOCAL, REMOTE, format_short_channel_id, LN_MAX_FUNDING_SAT from electrum.lnutil import LOCAL, REMOTE, format_short_channel_id, LN_MAX_FUNDING_SAT
from electrum.lnutil import ln_dummy_address
from electrum.lnworker import LNWallet from electrum.lnworker import LNWallet
from electrum.transaction import PartialTxOutput
from .util import (MyTreeView, WindowModalDialog, Buttons, OkButton, CancelButton, from .util import (MyTreeView, WindowModalDialog, Buttons, OkButton, CancelButton,
EnterButton, WaitingDialog, MONOSPACE_FONT, ColorScheme) EnterButton, WaitingDialog, MONOSPACE_FONT, ColorScheme)
@ -39,6 +41,7 @@ class SwapDialog(WindowModalDialog):
vbox = QVBoxLayout(self) vbox = QVBoxLayout(self)
vbox.addWidget(WWLabel('Swap lightning funds for on-chain funds if you need to increase your receiving capacity. This service is powered by the Boltz backend.')) vbox.addWidget(WWLabel('Swap lightning funds for on-chain funds if you need to increase your receiving capacity. This service is powered by the Boltz backend.'))
self.send_amount_e = BTCAmountEdit(self.window.get_decimal_point) self.send_amount_e = BTCAmountEdit(self.window.get_decimal_point)
self.send_amount_e.shortcut.connect(self.spend_max)
self.recv_amount_e = BTCAmountEdit(self.window.get_decimal_point) self.recv_amount_e = BTCAmountEdit(self.window.get_decimal_point)
self.send_button = QPushButton('') self.send_button = QPushButton('')
self.recv_button = QPushButton('') self.recv_button = QPushButton('')
@ -54,17 +57,17 @@ class SwapDialog(WindowModalDialog):
fee_combo = FeeComboBox(fee_slider) fee_combo = FeeComboBox(fee_slider)
fee_slider.update() fee_slider.update()
self.fee_label = QLabel() self.fee_label = QLabel()
self.percentage_label = QLabel() self.server_fee_label = QLabel()
h = QGridLayout() h = QGridLayout()
h.addWidget(QLabel(_('You send')+':'), 2, 0) h.addWidget(QLabel(_('You send')+':'), 1, 0)
h.addWidget(self.send_amount_e, 2, 1) h.addWidget(self.send_amount_e, 1, 1)
h.addWidget(self.send_button, 2, 2) h.addWidget(self.send_button, 1, 2)
h.addWidget(QLabel(_('You receive')+':'), 3, 0) h.addWidget(QLabel(_('You receive')+':'), 2, 0)
h.addWidget(self.recv_amount_e, 3, 1) h.addWidget(self.recv_amount_e, 2, 1)
h.addWidget(self.recv_button, 3, 2) h.addWidget(self.recv_button, 2, 2)
h.addWidget(QLabel(_('Swap fee')+':'), 4, 0) h.addWidget(QLabel(_('Server fee')+':'), 4, 0)
h.addWidget(self.percentage_label, 4, 1) h.addWidget(self.server_fee_label, 4, 1)
h.addWidget(QLabel(_('Mining fees')+':'), 5, 0) h.addWidget(QLabel(_('Mining fee')+':'), 5, 0)
h.addWidget(self.fee_label, 5, 1) h.addWidget(self.fee_label, 5, 1)
h.addWidget(fee_slider, 6, 1) h.addWidget(fee_slider, 6, 1)
h.addWidget(fee_combo, 6, 2) h.addWidget(fee_combo, 6, 2)
@ -95,6 +98,13 @@ class SwapDialog(WindowModalDialog):
self.recv_amount_e.setAmount(None) self.recv_amount_e.setAmount(None)
self.update() self.update()
def spend_max(self):
if not self.is_reverse:
self.update_tx('!')
if self.tx:
txo = self.tx.outputs()[0]
self.send_amount_e.setAmount(txo.value)
def on_send_edited(self): def on_send_edited(self):
if self.send_amount_e.follows: if self.send_amount_e.follows:
return return
@ -108,6 +118,7 @@ class SwapDialog(WindowModalDialog):
self.recv_amount_e.setStyleSheet(ColorScheme.BLUE.as_stylesheet()) self.recv_amount_e.setStyleSheet(ColorScheme.BLUE.as_stylesheet())
self.recv_amount_e.follows = False self.recv_amount_e.follows = False
self.send_follows = False self.send_follows = False
self.update_fee()
def on_recv_edited(self): def on_recv_edited(self):
if self.recv_amount_e.follows: if self.recv_amount_e.follows:
@ -122,14 +133,27 @@ class SwapDialog(WindowModalDialog):
self.send_amount_e.setStyleSheet(ColorScheme.BLUE.as_stylesheet()) self.send_amount_e.setStyleSheet(ColorScheme.BLUE.as_stylesheet())
self.send_amount_e.follows = False self.send_amount_e.follows = False
self.send_follows = True self.send_follows = True
self.update_fee()
def update(self): def update(self):
sm = self.swap_manager sm = self.swap_manager
self.send_button.setIcon(read_QIcon("lightning.png" if self.is_reverse else "bitcoin.png")) self.send_button.setIcon(read_QIcon("lightning.png" if self.is_reverse else "bitcoin.png"))
self.recv_button.setIcon(read_QIcon("lightning.png" if not self.is_reverse else "bitcoin.png")) self.recv_button.setIcon(read_QIcon("lightning.png" if not self.is_reverse else "bitcoin.png"))
fee = sm.lockup_fee + sm.get_claim_fee() if self.is_reverse else sm.normal_fee server_mining_fee = sm.lockup_fee if self.is_reverse else sm.normal_fee
self.fee_label.setText(self.window.format_amount(fee) + ' ' + self.window.base_unit()) server_fee_str = '%.2f'%sm.percentage + '% + ' + self.window.format_amount(server_mining_fee) + ' ' + self.window.base_unit()
self.percentage_label.setText('%.2f'%sm.percentage + '%') self.server_fee_label.setText(server_fee_str)
self.update_fee()
def update_fee(self):
if self.is_reverse:
sm = self.swap_manager
fee = sm.get_claim_fee()
else:
onchain_amount = self.send_amount_e.get_amount()
self.update_tx(onchain_amount)
fee = self.tx.get_fee() if self.tx else None
fee_text = self.window.format_amount(fee) + ' ' + self.window.base_unit() if fee else ''
self.fee_label.setText(fee_text)
def run(self): def run(self):
self.window.run_coroutine_from_thread(self.swap_manager.get_pairs(), lambda x: self.update()) self.window.run_coroutine_from_thread(self.swap_manager.get_pairs(), lambda x: self.update())
@ -152,6 +176,16 @@ class SwapDialog(WindowModalDialog):
return return
self.window.protect(self.do_normal_swap, (lightning_amount, onchain_amount)) self.window.protect(self.do_normal_swap, (lightning_amount, onchain_amount))
def update_tx(self, onchain_amount):
if onchain_amount is None:
self.tx = None
return
outputs = [PartialTxOutput.from_address_and_value(ln_dummy_address(), onchain_amount)]
coins = self.window.get_coins()
self.tx = self.window.wallet.make_unsigned_transaction(
coins=coins,
outputs=outputs)
def do_normal_swap(self, lightning_amount, onchain_amount, password): def do_normal_swap(self, lightning_amount, onchain_amount, password):
coro = self.swap_manager.normal_swap(lightning_amount, onchain_amount, password) coro = self.swap_manager.normal_swap(lightning_amount, onchain_amount, password, tx=self.tx)
self.window.run_coroutine_from_thread(coro) self.window.run_coroutine_from_thread(coro)

22
electrum/submarine_swaps.py

@ -10,7 +10,7 @@ from .bitcoin import address_to_script, script_to_p2wsh, redeem_script_to_addres
from .transaction import TxOutpoint, PartialTxInput, PartialTxOutput, PartialTransaction, construct_witness from .transaction import TxOutpoint, PartialTxInput, PartialTxOutput, PartialTransaction, construct_witness
from .transaction import script_GetOp, match_script_against_template, OPPushDataGeneric, OPPushDataPubkey from .transaction import script_GetOp, match_script_against_template, OPPushDataGeneric, OPPushDataPubkey
from .util import log_exceptions from .util import log_exceptions
from .lnutil import REDEEM_AFTER_DOUBLE_SPENT_DELAY from .lnutil import REDEEM_AFTER_DOUBLE_SPENT_DELAY, ln_dummy_address
from .bitcoin import dust_threshold from .bitcoin import dust_threshold
from .logging import Logger from .logging import Logger
from .lnutil import hex_to_bytes from .lnutil import hex_to_bytes
@ -143,7 +143,10 @@ class SwapManager(Logger):
tx = create_claim_tx(txin, swap.redeem_script, preimage, swap.privkey, address, amount_sat, swap.locktime) tx = create_claim_tx(txin, swap.redeem_script, preimage, swap.privkey, address, amount_sat, swap.locktime)
await self.network.broadcast_transaction(tx) await self.network.broadcast_transaction(tx)
# save txid # save txid
if swap.is_reverse:
swap.spending_txid = tx.txid() swap.spending_txid = tx.txid()
else:
self.wallet.setlabel(tx.txid(), 'Swap refund')
def get_claim_fee(self): def get_claim_fee(self):
return self.lnwatcher.config.estimate_fee(136, allow_fallback_to_static_rates=True) return self.lnwatcher.config.estimate_fee(136, allow_fallback_to_static_rates=True)
@ -156,12 +159,12 @@ class SwapManager(Logger):
self.lnwatcher.add_callback(swap.lockup_address, callback) self.lnwatcher.add_callback(swap.lockup_address, callback)
@log_exceptions @log_exceptions
async def normal_swap(self, lightning_amount, expected_onchain_amount, password): async def normal_swap(self, lightning_amount, expected_onchain_amount, password, *, tx=None):
privkey = os.urandom(32) privkey = os.urandom(32)
pubkey = ECPrivkey(privkey).get_public_key_bytes(compressed=True) pubkey = ECPrivkey(privkey).get_public_key_bytes(compressed=True)
key = await self.lnworker._add_request_coro(lightning_amount, 'swap', expiry=3600*24) key = await self.lnworker._add_request_coro(lightning_amount, 'swap', expiry=3600*24)
request = self.wallet.get_request(key) request = self.wallet.get_request(key)
invoice = request['invoice'] invoice = request.invoice
lnaddr = self.lnworker._check_invoice(invoice, lightning_amount) lnaddr = self.lnworker._check_invoice(invoice, lightning_amount)
payment_hash = lnaddr.paymenthash payment_hash = lnaddr.paymenthash
preimage = self.lnworker.get_preimage(payment_hash) preimage = self.lnworker.get_preimage(payment_hash)
@ -192,13 +195,20 @@ class SwapManager(Logger):
assert hash_160(preimage) == parsed_script[1][1] assert hash_160(preimage) == parsed_script[1][1]
assert pubkey == parsed_script[9][1] assert pubkey == parsed_script[9][1]
assert locktime == int.from_bytes(parsed_script[6][1], byteorder='little') assert locktime == int.from_bytes(parsed_script[6][1], byteorder='little')
# check that onchain_amount is what was announced # check that onchain_amount is not more than what we estimated
assert onchain_amount <= expected_onchain_amount, (onchain_amount, expected_onchain_amount) assert onchain_amount <= expected_onchain_amount, (onchain_amount, expected_onchain_amount)
# verify that they are not locking up funds for more than a day # verify that they are not locking up funds for more than a day
assert locktime - self.network.get_local_height() < 144 assert locktime - self.network.get_local_height() < 144
# create funding tx # create funding tx
outputs = [PartialTxOutput.from_address_and_value(lockup_address, onchain_amount)] funding_output = PartialTxOutput.from_address_and_value(lockup_address, expected_onchain_amount)
tx = self.wallet.create_transaction(outputs=outputs, rbf=False, password=password) if tx is None:
tx = self.wallet.create_transaction(outputs=[funding_output], rbf=False, password=password)
else:
dummy_output = PartialTxOutput.from_address_and_value(ln_dummy_address(), expected_onchain_amount)
tx.outputs().remove(dummy_output)
tx.add_outputs([funding_output])
tx.set_rbf(False)
self.wallet.sign_transaction(tx, password)
# save swap data in wallet in case we need a refund # save swap data in wallet in case we need a refund
swap = SwapData( swap = SwapData(
redeem_script = redeem_script, redeem_script = redeem_script,

Loading…
Cancel
Save