tswaps: move fee logic to swap_manager, fix command line - electrum - Electrum Bitcoin wallet (HTM) git clone https://git.parazyd.org/electrum (DIR) Log (DIR) Files (DIR) Refs (DIR) Submodules --- (DIR) commit 5fa09970b65ed0480a7040621c73afab3cabcd77 (DIR) parent 3874f7ec77bdd49f3dac02078e7b8a468efb5f88 (HTM) Author: ThomasV <thomasv@electrum.org> Date: Thu, 28 May 2020 13:11:32 +0200 swaps: move fee logic to swap_manager, fix command line Diffstat: M electrum/commands.py | 56 ++++++++++++++++++++++++++++--- M electrum/gui/qt/swap_dialog.py | 68 ++++--------------------------- M electrum/submarine_swaps.py | 86 +++++++++++++++++++++---------- 3 files changed, 119 insertions(+), 91 deletions(-) --- (DIR) diff --git a/electrum/commands.py b/electrum/commands.py t@@ -79,6 +79,8 @@ def satoshis(amount): # satoshi conversion must not be performed by the parser return int(COIN*Decimal(amount)) if amount not in ['!', None] else amount +def format_satoshis(x): + return str(Decimal(x)/COIN) if x is not None else None def json_normalize(x): # note: The return value of commands, when going through the JSON-RPC interface, t@@ -1102,12 +1104,56 @@ class Commands: return await self.network.local_watchtower.sweepstore.get_ctn(channel_point, None) @command('wnp') - async def submarine_swap(self, amount, password=None, wallet: Abstract_Wallet = None): - return await wallet.lnworker.swap_manager.normal_swap(satoshis(amount), password) + async def normal_swap(self, onchain_amount, lightning_amount, password=None, wallet: Abstract_Wallet = None): + """ + Normal submarine swap: send on-chain BTC, receive on Lightning + Note that your funds will be locked for 24h if you do not have enough incoming capacity. + """ + sm = wallet.lnworker.swap_manager + if lightning_amount == 'dryrun': + await sm.get_pairs() + onchain_amount_sat = satoshis(onchain_amount) + lightning_amount_sat = sm.get_recv_amount(onchain_amount_sat, is_reverse=False) + txid = None + elif onchain_amount == 'dryrun': + await sm.get_pairs() + lightning_amount_sat = satoshis(lightning_amount) + onchain_amount_sat = sm.get_send_amount(lightning_amount_sat, is_reverse=False) + txid = None + else: + lightning_amount_sat = satoshis(lightning_amount) + onchain_amount_sat = satoshis(onchain_amount) + txid = await wallet.lnworker.swap_manager.normal_swap(lightning_amount_sat, onchain_amount_sat, password) + return { + 'txid': txid, + 'lightning_amount': format_satoshis(lightning_amount_sat), + 'onchain_amount': format_satoshis(onchain_amount_sat), + } @command('wn') - async def reverse_swap(self, amount, wallet: Abstract_Wallet = None): - return await wallet.lnworker.swap_manager.reverse_swap(satoshis(amount)) + async def reverse_swap(self, lightning_amount, onchain_amount, wallet: Abstract_Wallet = None): + """Reverse submarine swap: send on Lightning, receive on-chain + """ + sm = wallet.lnworker.swap_manager + if onchain_amount == 'dryrun': + await sm.get_pairs() + lightning_amount_sat = satoshis(lightning_amount) + onchain_amount_sat = sm.get_recv_amount(lightning_amount_sat, is_reverse=True) + success = None + elif lightning_amount == 'dryrun': + await sm.get_pairs() + onchain_amount_sat = satoshis(onchain_amount) + lightning_amount_sat = sm.get_send_amount(onchain_amount_sat, is_reverse=True) + success = None + else: + lightning_amount_sat = satoshis(lightning_amount) + onchain_amount_sat = satoshis(onchain_amount) + success = await wallet.lnworker.swap_manager.reverse_swap(lightning_amount_sat, onchain_amount_sat) + return { + 'success': success, + 'lightning_amount': format_satoshis(lightning_amount_sat), + 'onchain_amount': format_satoshis(onchain_amount_sat), + } def eval_bool(x: str) -> bool: t@@ -1135,6 +1181,8 @@ param_descriptions = { 'requested_amount': 'Requested amount (in BTC).', 'outputs': 'list of ["address", amount]', 'redeem_script': 'redeem script (hexadecimal)', + 'lightning_amount': "Amount sent or received in a submarine swap. Set it to 'dryrun' to receive a value", + 'onchain_amount': "Amount sent or received in a submarine swap. Set it to 'dryrun' to receive a value", } command_options = { (DIR) diff --git a/electrum/gui/qt/swap_dialog.py b/electrum/gui/qt/swap_dialog.py t@@ -29,12 +29,6 @@ class SwapDialog(WindowModalDialog): self.config = window.config self.swap_manager = self.window.wallet.lnworker.swap_manager self.network = window.network - self.normal_fee = 0 - self.lockup_fee = 0 - self.claim_fee = self.swap_manager.get_tx_fee() - self.percentage = 0 - self.min_amount = 0 - self.max_amount = 0 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.')) self.send_amount_e = BTCAmountEdit(self.window.get_decimal_point) t@@ -82,8 +76,6 @@ class SwapDialog(WindowModalDialog): self.config.set_key('fee_level', pos, False) else: self.config.set_key('fee_per_kb', fee_rate, False) - # read claim_fee from config - self.claim_fee = self.swap_manager.get_tx_fee() if self.send_follows: self.on_recv_edited() else: t@@ -102,7 +94,7 @@ class SwapDialog(WindowModalDialog): self.send_amount_e.setStyleSheet(ColorScheme.DEFAULT.as_stylesheet()) amount = self.send_amount_e.get_amount() self.recv_amount_e.follows = True - self.recv_amount_e.setAmount(self.get_recv_amount(amount)) + self.recv_amount_e.setAmount(self.swap_manager.get_recv_amount(amount, self.is_reverse)) self.recv_amount_e.setStyleSheet(ColorScheme.BLUE.as_stylesheet()) self.recv_amount_e.follows = False self.send_follows = False t@@ -113,72 +105,26 @@ class SwapDialog(WindowModalDialog): self.recv_amount_e.setStyleSheet(ColorScheme.DEFAULT.as_stylesheet()) amount = self.recv_amount_e.get_amount() self.send_amount_e.follows = True - self.send_amount_e.setAmount(self.get_send_amount(amount)) + self.send_amount_e.setAmount(self.swap_manager.get_send_amount(amount, self.is_reverse)) self.send_amount_e.setStyleSheet(ColorScheme.BLUE.as_stylesheet()) self.send_amount_e.follows = False self.send_follows = True - def on_pairs(self, pairs): - fees = pairs['pairs']['BTC/BTC']['fees'] - self.percentage = fees['percentage'] - self.normal_fee = fees['minerFees']['baseAsset']['normal'] - self.lockup_fee = fees['minerFees']['baseAsset']['reverse']['lockup'] - #self.claim_fee = fees['minerFees']['baseAsset']['reverse']['claim'] - limits = pairs['pairs']['BTC/BTC']['limits'] - self.min_amount = limits['minimal'] - self.max_amount = limits['maximal'] - self.update() - def update(self): + sm = self.swap_manager 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")) - fee = self.lockup_fee + self.claim_fee if self.is_reverse else self.normal_fee + fee = sm.lockup_fee + sm.get_claim_fee() if self.is_reverse else sm.normal_fee self.fee_label.setText(self.window.format_amount(fee) + ' ' + self.window.base_unit()) - self.percentage_label.setText('%.2f'%self.percentage + '%') - - def set_minimum(self): - self.send_amount_e.setAmount(self.min_amount) - - def set_maximum(self): - self.send_amount_e.setAmount(self.max_amount) - - def get_recv_amount(self, send_amount): - if send_amount is None: - return - if send_amount < self.min_amount or send_amount > self.max_amount: - return - x = send_amount - if self.is_reverse: - x = int(x * (100 - self.percentage) / 100) - x -= self.lockup_fee - x -= self.claim_fee - else: - x -= self.normal_fee - x = int(x * (100 - self.percentage) / 100) - if x < 0: - return - return x - - def get_send_amount(self, recv_amount): - if not recv_amount: - return - x = recv_amount - if self.is_reverse: - x += self.lockup_fee - x += self.claim_fee - x = int(x * 100 / (100 - self.percentage)) + 1 - else: - x = int(x * 100 / (100 - self.percentage)) + 1 - x += self.normal_fee - return x + self.percentage_label.setText('%.2f'%sm.percentage + '%') def run(self): - self.window.run_coroutine_from_thread(self.swap_manager.get_pairs(), self.on_pairs) + self.window.run_coroutine_from_thread(self.swap_manager.get_pairs(), lambda x: self.update()) if not self.exec_(): return if self.is_reverse: lightning_amount = self.send_amount_e.get_amount() - onchain_amount = self.recv_amount_e.get_amount() + self.claim_fee + onchain_amount = self.recv_amount_e.get_amount() + self.swap_manager.get_claim_fee() coro = self.swap_manager.reverse_swap(lightning_amount, onchain_amount) self.window.run_coroutine_from_thread(coro) else: (DIR) diff --git a/electrum/submarine_swaps.py b/electrum/submarine_swaps.py t@@ -96,6 +96,23 @@ def create_claim_tx(txin, witness_script, preimage, privkey:bytes, address, amou class SwapManager(Logger): + def __init__(self, wallet: 'Abstract_Wallet', network:'Network'): + Logger.__init__(self) + self.normal_fee = 0 + self.lockup_fee = 0 + self.percentage = 0 + self.min_amount = 0 + self.max_amount = 0 + self.network = network + self.wallet = wallet + self.lnworker = wallet.lnworker + self.lnwatcher = self.wallet.lnworker.lnwatcher + self.swaps = self.wallet.db.get_dict('submarine_swaps') + for swap in self.swaps.values(): + if swap.is_redeemed: + continue + self.add_lnwatcher_callback(swap) + @log_exceptions async def _claim_swap(self, swap): if not self.lnwatcher.is_up_to_date(): t@@ -117,7 +134,7 @@ class SwapManager(Logger): self.lnwatcher.remove_callback(swap.lockup_address) swap.is_redeemed = True continue - amount_sat = txin._trusted_value_sats - self.get_tx_fee() + amount_sat = txin._trusted_value_sats - self.get_claim_fee() if amount_sat < dust_threshold(): self.logger.info('utxo value below dust threshold') continue t@@ -128,21 +145,9 @@ class SwapManager(Logger): # save txid swap.spending_txid = tx.txid() - def get_tx_fee(self): + def get_claim_fee(self): return self.lnwatcher.config.estimate_fee(136, allow_fallback_to_static_rates=True) - def __init__(self, wallet: 'Abstract_Wallet', network:'Network'): - Logger.__init__(self) - self.network = network - self.wallet = wallet - self.lnworker = wallet.lnworker - self.lnwatcher = self.wallet.lnworker.lnwatcher - self.swaps = self.wallet.db.get_dict('submarine_swaps') - for swap in self.swaps.values(): - if swap.is_redeemed: - continue - self.add_lnwatcher_callback(swap) - def get_swap(self, payment_hash): return self.swaps.get(payment_hash.hex()) t@@ -211,12 +216,7 @@ class SwapManager(Logger): self.swaps[payment_hash.hex()] = swap self.add_lnwatcher_callback(swap) await self.network.broadcast_transaction(tx) - # - attempt = await self.lnworker.await_payment(payment_hash) - return { - 'id':response_id, - 'success':attempt.success, - } + return tx.txid() @log_exceptions async def reverse_swap(self, amount_sat, expected_amount): t@@ -278,10 +278,7 @@ class SwapManager(Logger): self.add_lnwatcher_callback(swap) # initiate payment. success, log = await self.lnworker._pay(invoice, attempts=10) - return { - 'id':response_id, - 'success':success, - } + return success @log_exceptions async def get_pairs(self): t@@ -289,5 +286,42 @@ class SwapManager(Logger): 'get', API_URL + '/getpairs', timeout=30) - data = json.loads(response) - return data + pairs = json.loads(response) + fees = pairs['pairs']['BTC/BTC']['fees'] + self.percentage = fees['percentage'] + self.normal_fee = fees['minerFees']['baseAsset']['normal'] + self.lockup_fee = fees['minerFees']['baseAsset']['reverse']['lockup'] + limits = pairs['pairs']['BTC/BTC']['limits'] + self.min_amount = limits['minimal'] + self.max_amount = limits['maximal'] + + def get_recv_amount(self, send_amount, is_reverse): + if send_amount is None: + return + if send_amount < self.min_amount or send_amount > self.max_amount: + return + x = send_amount + if is_reverse: + x = int(x * (100 - self.percentage) / 100) + x -= self.lockup_fee + x -= self.get_claim_fee() + else: + x -= self.normal_fee + x = int(x * (100 - self.percentage) / 100) + if x < 0: + return + return x + + def get_send_amount(self, recv_amount, is_reverse): + if not recv_amount: + return + x = recv_amount + if is_reverse: + x += self.lockup_fee + x += self.get_claim_fee() + x = int(x * 100 / (100 - self.percentage)) + 1 + else: + x = int(x * 100 / (100 - self.percentage)) + 1 + x += self.normal_fee + return x +