URI: 
       tsubmarine_swaps: add SwapManager - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
   DIR commit 17ff6ffa0883d5b73845c3800fe5f2e04889e576
   DIR parent b26ad81e69b23cadebd9e2efb8d2861927d94543
  HTML Author: ThomasV <thomasv@electrum.org>
       Date:   Sun, 24 May 2020 13:42:36 +0200
       
       submarine_swaps: add SwapManager
       
       Diffstat:
         M electrum/commands.py                |      15 ++-------------
         M electrum/gui/qt/swap_dialog.py      |       8 ++++----
         M electrum/lnworker.py                |       2 ++
         M electrum/submarine_swaps.py         |     347 +++++++++++++++----------------
       
       4 files changed, 175 insertions(+), 197 deletions(-)
       ---
   DIR diff --git a/electrum/commands.py b/electrum/commands.py
       t@@ -1103,23 +1103,12 @@ class Commands:
        
            @command('wnp')
            async def submarine_swap(self, amount, password=None, wallet: Abstract_Wallet = None):
       -        return await submarine_swaps.normal_swap(satoshis(amount), wallet, self.network, password)
       +        return await wallet.lnworker.swap_manager.normal_swap(satoshis(amount), password)
        
            @command('wn')
            async def reverse_swap(self, amount, wallet: Abstract_Wallet = None):
       -        return await submarine_swaps.reverse_swap(satoshis(amount), wallet, self.network)
       +        return await wallet.lnworker.swap_manager.reverse_swap(satoshis(amount))
        
       -    @command('n')
       -    async def get_pairs(self):
       -        return await submarine_swaps.get_pairs(self.network)
       -
       -    @command('wn')
       -    async def claim_swap(self, key, wallet: Abstract_Wallet = None):
       -        return await submarine_swaps.claim_swap(key, wallet)
       -
       -    @command('wn')
       -    async def refund_swap(self, key, wallet: Abstract_Wallet = None):
       -        return await submarine_swaps.refund_swap(key, wallet)
        
        def eval_bool(x: str) -> bool:
            if x == 'false': return False
   DIR diff --git a/electrum/gui/qt/swap_dialog.py b/electrum/gui/qt/swap_dialog.py
       t@@ -16,7 +16,6 @@ from .util import (MyTreeView, WindowModalDialog, Buttons, OkButton, CancelButto
        from .amountedit import BTCAmountEdit, FreezableLineEdit
        
        
       -from electrum import submarine_swaps as ss
        import asyncio
        from .util import read_QIcon
        
       t@@ -26,6 +25,7 @@ class SwapDialog(WindowModalDialog):
            def __init__(self, window):
                WindowModalDialog.__init__(self, window, _('Submarine Swap'))
                self.window = window
       +        self.swap_manager = self.window.wallet.lnworker.swap_manager
                self.network = window.network
                self.normal_fee = 0
                self.lockup_fee = 0
       t@@ -85,7 +85,7 @@ class SwapDialog(WindowModalDialog):
                self.send_amount_e.follows = False
        
            def get_pairs(self):
       -        fut = asyncio.run_coroutine_threadsafe(ss.get_pairs(self.network), self.network.asyncio_loop)
       +        fut = asyncio.run_coroutine_threadsafe(self.swap_manager.get_pairs(), self.network.asyncio_loop)
                pairs = fut.result()
                print(pairs)
                fees = pairs['pairs']['BTC/BTC']['fees']
       t@@ -125,9 +125,9 @@ class SwapDialog(WindowModalDialog):
                    return
                if self.is_reverse:
                    amount_sat = self.send_amount_e.get_amount()
       -            coro = ss.reverse_swap(amount_sat, self.window.wallet, self.network)
       +            coro = self.swap_manager.reverse_swap(amount_sat)
                else:
                    amount_sat = self.recv_amount_e.get_amount()
                    password = self.window.protect(lambda x: x, [])
       -            coro = ss.normal_swap(amount_sat, self.window.wallet, self.network, password)
       +            coro = self.swap_manager.normal_swap(amount_sat, password)
                asyncio.run_coroutine_threadsafe(coro, self.network.asyncio_loop)
   DIR diff --git a/electrum/lnworker.py b/electrum/lnworker.py
       t@@ -70,6 +70,7 @@ from .crypto import pw_encode_bytes, pw_decode_bytes, PW_HASH_VERSION_LATEST
        from .lnutil import ChannelBackupStorage
        from .lnchannel import ChannelBackup
        from .channel_db import UpdateStatus
       +from .submarine_swaps import SwapManager
        
        if TYPE_CHECKING:
            from .network import Network
       t@@ -556,6 +557,7 @@ class LNWallet(LNWorker):
                self.lnwatcher = LNWalletWatcher(self, network)
                self.lnwatcher.start_network(network)
                self.network = network
       +        self.swap_manager = SwapManager(self.wallet, network)
        
                for chan in self.channels.values():
                    self.lnwatcher.add_channel(chan.funding_outpoint.to_str(), chan.get_funding_address())
   DIR diff --git a/electrum/submarine_swaps.py b/electrum/submarine_swaps.py
       t@@ -6,10 +6,10 @@ from .ecc import ECPrivkey
        from .bitcoin import address_to_script, script_to_p2wsh, redeem_script_to_address, opcodes, p2wsh_nested_script, push_script, is_segwit_address
        from .transaction import TxOutpoint, PartialTxInput, PartialTxOutput, PartialTransaction, construct_witness
        from .transaction import script_GetOp, match_script_against_template, OPPushDataGeneric, OPPushDataPubkey
       -from .transaction import Transaction
        from .util import log_exceptions
        from .bitcoin import dust_threshold
        from typing import TYPE_CHECKING
       +from .logging import Logger
        
        if TYPE_CHECKING:
            from .network import Network
       t@@ -73,182 +73,169 @@ def create_claim_tx(txin, witness_script, preimage, privkey:bytes, address, amou
            return tx
        
        
       -@log_exceptions
       -async def _claim_swap(lnworker, lockup_address, redeem_script, preimage, privkey, address, locktime, is_refund=False):
       -    lnwatcher = lnworker.lnwatcher
       -    utxos = lnwatcher.get_addr_utxo(lockup_address)
       -    delta = lnwatcher.network.get_local_height() - locktime
       -    if is_refund and delta < 0:
       -        print('height not reached for refund', delta, locktime)
       -        return
       -    for txin in list(utxos.values()):
       -        fee = lnwatcher.config.estimate_fee(136, allow_fallback_to_static_rates=True)
       -        amount_sat = txin._trusted_value_sats - fee
       -        if amount_sat < dust_threshold():
       -            print('txo lower than dust threshold')
       -            continue
       -        tx = create_claim_tx(txin, redeem_script, preimage, privkey, address, amount_sat, locktime, is_refund)
       -        await lnwatcher.network.broadcast_transaction(tx)
       -
       -
       -@log_exceptions
       -async def claim_swap(key, wallet):
       -    lnworker = wallet.lnworker
       -    address = wallet.get_unused_address()
       -    swaps = wallet.db.get_dict('submarine_swaps')
       -    data = swaps[key]
       -    onchain_amount = data['onchainAmount']
       -    redeem_script = bytes.fromhex(data['redeemScript'])
       -    locktime = data['timeoutBlockHeight']
       -    lockup_address = data['lockupAddress']
       -    preimage = bytes.fromhex(data['preimage'])
       -    privkey = bytes.fromhex(data['privkey'])
       -    callback = lambda: _claim_swap(lnworker, lockup_address, redeem_script, preimage, privkey, address, locktime, is_refund=False)
       -    lnworker.lnwatcher.add_callback(lockup_address, callback)
       -    return True
       -
       -
       -@log_exceptions
       -async def refund_swap(key, wallet):
       -    lnworker = wallet.lnworker
       -    address = wallet.get_unused_address()
       -    swaps = wallet.db.get_dict('submarine_swaps')
       -    data = swaps[key]
       -    lockup_address = data['address']
       -    redeem_script = bytes.fromhex(data['redeemScript'])
       -    locktime = data['timeoutBlockHeight']
       -    preimage = bytes.fromhex(data['preimage'])
       -    privkey = bytes.fromhex(data['privkey'])
       -    callback = lambda: _claim_swap(lnworker, lockup_address, redeem_script, preimage, privkey, address, locktime, is_refund=True)
       -    lnworker.lnwatcher.add_callback(lockup_address, callback)
       -    return True
       -
       -
       -@log_exceptions
       -async def normal_swap(amount_sat, wallet: 'Abstract_Wallet', network: 'Network', password):
       -    lnworker = wallet.lnworker
       -    privkey = os.urandom(32)
       -    pubkey = ECPrivkey(privkey).get_public_key_bytes(compressed=True)
       -    key = await lnworker._add_request_coro(amount_sat, 'swap', expiry=3600*24)
       -    request = wallet.get_request(key)
       -    invoice = request['invoice']
       -    lnaddr = lnworker._check_invoice(invoice, amount_sat)
       -    payment_hash = lnaddr.paymenthash
       -    preimage = lnworker.get_preimage(payment_hash)
       -    address = wallet.get_unused_address()
       -    request_data = {
       -        "type": "submarine",
       -        "pairId": "BTC/BTC",
       -        "orderSide": "sell",
       -        "invoice": invoice,
       -        "refundPublicKey": pubkey.hex()
       -    }
       -    response = await network._send_http_on_proxy(
       -        'post',
       -        API_URL + '/createswap',
       -        json=request_data,
       -        timeout=30)
       -    data = json.loads(response)
       -    response_id = data["id"]
       -    zeroconf = data["acceptZeroConf"]
       -    onchain_amount = data["expectedAmount"]
       -    locktime = data["timeoutBlockHeight"]
       -    lockup_address = data["address"]
       -    redeem_script = data["redeemScript"]
       -    # verify redeem_script is built with our pubkey and preimage
       -    redeem_script = bytes.fromhex(redeem_script)
       -    parsed_script = [x for x in script_GetOp(redeem_script)]
       -    assert match_script_against_template(redeem_script, WITNESS_TEMPLATE_SWAP)
       -    assert script_to_p2wsh(redeem_script.hex()) == lockup_address
       -    assert hash_160(preimage) == parsed_script[1][1]
       -    assert pubkey == parsed_script[9][1]
       -    # verify that we will have enought time to get our tx confirmed
       -    assert locktime == int.from_bytes(parsed_script[6][1], byteorder='little')
       -    assert locktime - network.get_local_height() == 140
       -    # save swap data in wallet in case we need a refund
       -    data['privkey'] = privkey.hex()
       -    data['preimage'] = preimage.hex()
       -    swaps = wallet.db.get_dict('submarine_swaps')
       -    swaps[response_id] = data
       -    callback = lambda: _claim_swap(lnworker, lockup_address, redeem_script, preimage, privkey, address, locktime, is_refund=True)
       -    lnworker.lnwatcher.add_callback(lockup_address, callback)
       -    outputs = [PartialTxOutput.from_address_and_value(lockup_address, onchain_amount)]
       -    tx = wallet.create_transaction(outputs=outputs, rbf=False, password=password)
       -    await network.broadcast_transaction(tx)
       -    #
       -    attempt = await lnworker.await_payment(payment_hash)
       -    return {
       -        'id':response_id,
       -        'success':attempt.success,
       -    }
       -
       -
       -@log_exceptions
       -async def reverse_swap(amount_sat, wallet: 'Abstract_Wallet', network: 'Network'):
       -    privkey = os.urandom(32)
       -    pubkey = ECPrivkey(privkey).get_public_key_bytes(compressed=True)
       -    preimage = os.urandom(32)
       -    preimage_hash = sha256(preimage)
       -    address = wallet.get_unused_address()
       -    request_data = {
       -        "type": "reversesubmarine",
       -        "pairId": "BTC/BTC",
       -        "orderSide": "buy",
       -        "invoiceAmount": amount_sat,
       -        "preimageHash": preimage_hash.hex(),
       -        "claimPublicKey": pubkey.hex()
       -    }
       -    response = await network._send_http_on_proxy(
       -        'post',
       -        API_URL + '/createswap',
       -        json=request_data,
       -        timeout=30)
       -    data = json.loads(response)
       -    invoice = data['invoice']
       -    lockup_address = data['lockupAddress']
       -    redeem_script = data['redeemScript']
       -    locktime = data['timeoutBlockHeight']
       -    onchain_amount = data["onchainAmount"]
       -    response_id = data['id']
       -    # verify redeem_script is built with our pubkey and preimage
       -    redeem_script = bytes.fromhex(redeem_script)
       -    parsed_script = [x for x in script_GetOp(redeem_script)]
       -    assert match_script_against_template(redeem_script, WITNESS_TEMPLATE_REVERSE_SWAP)
       -    assert script_to_p2wsh(redeem_script.hex()) == lockup_address
       -    assert hash_160(preimage) == parsed_script[5][1]
       -    assert pubkey == parsed_script[7][1]
       -    # verify that we will have enought time to get our tx confirmed
       -    assert locktime == int.from_bytes(parsed_script[10][1], byteorder='little')
       -    assert locktime - network.get_local_height() > 10
       -    # verify invoice preimage_hash
       -    lnworker = wallet.lnworker
       -    lnaddr = lnworker._check_invoice(invoice, amount_sat)
       -    assert lnaddr.paymenthash == preimage_hash
       -    # save swap data in wallet in case payment fails
       -    data['privkey'] = privkey.hex()
       -    data['preimage'] = preimage.hex()
       -    # save data to wallet file
       -    swaps = wallet.db.get_dict('submarine_swaps')
       -    swaps[response_id] = data
       -    # add callback to lnwatcher
       -    callback = lambda: _claim_swap(lnworker, lockup_address, redeem_script, preimage, privkey, address, locktime, is_refund=False)
       -    lnworker.lnwatcher.add_callback(lockup_address, callback)
       -    # initiate payment.
       -    success, log = await lnworker._pay(invoice, attempts=5)
       -    # discard data; this should be done by lnwatcher
       -    if success:
       -        swaps.pop(response_id)
       -    return {
       -        'id':response_id,
       -        'success':success,
       -    }
       -
       -
       -@log_exceptions
       -async def get_pairs(network):
       -    response = await network._send_http_on_proxy(
       -        'get',
       -        API_URL + '/getpairs',
       -        timeout=30)
       -    data = json.loads(response)
       -    return data
       +
       +class SwapManager(Logger):
       +
       +    @log_exceptions
       +    async def _claim_swap(self, lockup_address, redeem_script, preimage, privkey, locktime, is_refund=False):
       +        utxos = self.lnwatcher.get_addr_utxo(lockup_address)
       +        if not utxos:
       +            return
       +        delta = self.network.get_local_height() - locktime
       +        if is_refund and delta < 0:
       +            self.logger.info(f'height not reached for refund {lockup_address} {delta}, {locktime}')
       +            return
       +        for txin in list(utxos.values()):
       +            fee = self.lnwatcher.config.estimate_fee(136, allow_fallback_to_static_rates=True)
       +            amount_sat = txin._trusted_value_sats - fee
       +            if amount_sat < dust_threshold():
       +                self.logger.info('utxo value below dust threshold')
       +                continue
       +            address = self.wallet.get_unused_address()
       +            tx = create_claim_tx(txin, redeem_script, preimage, privkey, address, amount_sat, locktime, is_refund)
       +            await self.network.broadcast_transaction(tx)
       +
       +    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
       +        swaps = self.wallet.db.get_dict('submarine_swaps')
       +        for key, data in swaps.items():
       +            redeem_script = bytes.fromhex(data['redeemScript'])
       +            locktime = data['timeoutBlockHeight']
       +            preimage = bytes.fromhex(data['preimage'])
       +            privkey = bytes.fromhex(data['privkey'])
       +            if data.get('invoice'):
       +                lockup_address = data['lockupAddress']
       +                is_refund = False
       +            else:
       +                lockup_address = data['address']
       +                is_refund = True
       +            self.add_lnwatcher_callback(lockup_address, redeem_script, preimage, privkey, locktime, is_refund)
       +
       +    def add_lnwatcher_callback(self, lockup_address, redeem_script, preimage, privkey, locktime, is_refund):
       +        callback = lambda: self._claim_swap(lockup_address, redeem_script, preimage, privkey, locktime, is_refund=is_refund)
       +        self.lnwatcher.add_callback(lockup_address, callback)
       +
       +    @log_exceptions
       +    async def normal_swap(self, amount_sat, password):
       +        privkey = os.urandom(32)
       +        pubkey = ECPrivkey(privkey).get_public_key_bytes(compressed=True)
       +        key = await self.lnworker._add_request_coro(amount_sat, 'swap', expiry=3600*24)
       +        request = self.wallet.get_request(key)
       +        invoice = request['invoice']
       +        lnaddr = self.lnworker._check_invoice(invoice, amount_sat)
       +        payment_hash = lnaddr.paymenthash
       +        preimage = self.lnworker.get_preimage(payment_hash)
       +        request_data = {
       +            "type": "submarine",
       +            "pairId": "BTC/BTC",
       +            "orderSide": "sell",
       +            "invoice": invoice,
       +            "refundPublicKey": pubkey.hex()
       +        }
       +        response = await self.network._send_http_on_proxy(
       +            'post',
       +            API_URL + '/createswap',
       +            json=request_data,
       +            timeout=30)
       +        data = json.loads(response)
       +        response_id = data["id"]
       +        zeroconf = data["acceptZeroConf"]
       +        onchain_amount = data["expectedAmount"]
       +        locktime = data["timeoutBlockHeight"]
       +        lockup_address = data["address"]
       +        redeem_script = data["redeemScript"]
       +        # verify redeem_script is built with our pubkey and preimage
       +        redeem_script = bytes.fromhex(redeem_script)
       +        parsed_script = [x for x in script_GetOp(redeem_script)]
       +        assert match_script_against_template(redeem_script, WITNESS_TEMPLATE_SWAP)
       +        assert script_to_p2wsh(redeem_script.hex()) == lockup_address
       +        assert hash_160(preimage) == parsed_script[1][1]
       +        assert pubkey == parsed_script[9][1]
       +        # verify that they are not locking up funds for more than a day
       +        assert locktime == int.from_bytes(parsed_script[6][1], byteorder='little')
       +        assert locktime - self.network.get_local_height() < 144
       +        # save swap data in wallet in case we need a refund
       +        data['privkey'] = privkey.hex()
       +        data['preimage'] = preimage.hex()
       +        swaps = self.wallet.db.get_dict('submarine_swaps')
       +        swaps[response_id] = data
       +        self.add_lnwatcher_callback(lockup_address, redeem_script, preimage, privkey, locktime, is_refund=True)
       +        outputs = [PartialTxOutput.from_address_and_value(lockup_address, onchain_amount)]
       +        tx = self.wallet.create_transaction(outputs=outputs, rbf=False, password=password)
       +        await self.network.broadcast_transaction(tx)
       +        #
       +        attempt = await self.lnworker.await_payment(payment_hash)
       +        return {
       +            'id':response_id,
       +            'success':attempt.success,
       +        }
       +
       +    @log_exceptions
       +    async def reverse_swap(self, amount_sat):
       +        privkey = os.urandom(32)
       +        pubkey = ECPrivkey(privkey).get_public_key_bytes(compressed=True)
       +        preimage = os.urandom(32)
       +        preimage_hash = sha256(preimage)
       +        request_data = {
       +            "type": "reversesubmarine",
       +            "pairId": "BTC/BTC",
       +            "orderSide": "buy",
       +            "invoiceAmount": amount_sat,
       +            "preimageHash": preimage_hash.hex(),
       +            "claimPublicKey": pubkey.hex()
       +        }
       +        response = await self.network._send_http_on_proxy(
       +            'post',
       +            API_URL + '/createswap',
       +            json=request_data,
       +            timeout=30)
       +        data = json.loads(response)
       +        invoice = data['invoice']
       +        lockup_address = data['lockupAddress']
       +        redeem_script = data['redeemScript']
       +        locktime = data['timeoutBlockHeight']
       +        onchain_amount = data["onchainAmount"]
       +        response_id = data['id']
       +        # verify redeem_script is built with our pubkey and preimage
       +        redeem_script = bytes.fromhex(redeem_script)
       +        parsed_script = [x for x in script_GetOp(redeem_script)]
       +        assert match_script_against_template(redeem_script, WITNESS_TEMPLATE_REVERSE_SWAP)
       +        assert script_to_p2wsh(redeem_script.hex()) == lockup_address
       +        assert hash_160(preimage) == parsed_script[5][1]
       +        assert pubkey == parsed_script[7][1]
       +        # verify that we will have enought time to get our tx confirmed
       +        assert locktime == int.from_bytes(parsed_script[10][1], byteorder='little')
       +        assert locktime - self.network.get_local_height() > 10
       +        # verify invoice preimage_hash
       +        lnaddr = self.lnworker._check_invoice(invoice, amount_sat)
       +        assert lnaddr.paymenthash == preimage_hash
       +        # save swap data in wallet in case payment fails
       +        data['privkey'] = privkey.hex()
       +        data['preimage'] = preimage.hex()
       +        # save data to wallet file
       +        swaps = self.wallet.db.get_dict('submarine_swaps')
       +        swaps[response_id] = data
       +        # add callback to lnwatcher
       +        self.add_lnwatcher_callback(lockup_address, redeem_script, preimage, privkey, locktime, is_refund=False)
       +        # initiate payment.
       +        success, log = await self.lnworker._pay(invoice, attempts=5)
       +        # discard data; this should be done by lnwatcher
       +        if success:
       +            swaps.pop(response_id)
       +        return {
       +            'id':response_id,
       +            'success':success,
       +        }
       +
       +    @log_exceptions
       +    async def get_pairs(self):
       +        response = await self.network._send_http_on_proxy(
       +            'get',
       +            API_URL + '/getpairs',
       +            timeout=30)
       +        data = json.loads(response)
       +        return data