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
       +