tsubmarine swaps, initial implementation: - server uses Boltz API (https://docs.boltz.exchange/en/latest/) - reverse swaps only - command-line only - electrum - Electrum Bitcoin wallet
 (HTM) git clone https://git.parazyd.org/electrum
 (DIR) Log
 (DIR) Files
 (DIR) Refs
 (DIR) Submodules
       ---
 (DIR) commit 1e67e55303987aa643018a314c1245be597286ef
 (DIR) parent 368229a4c37bb57bdff188be4e9ec59bde7441ee
 (HTM) Author: ThomasV <thomasv@electrum.org>
       Date:   Tue, 19 May 2020 13:23:44 +0200
       
       submarine swaps, initial implementation:
        - server uses Boltz API (https://docs.boltz.exchange/en/latest/)
        - reverse swaps only
        - command-line only
       
       Diffstat:
         M electrum/commands.py                |      10 ++++++++++
         A electrum/submarine_swaps.py         |     154 +++++++++++++++++++++++++++++++
       
       2 files changed, 164 insertions(+), 0 deletions(-)
       ---
 (DIR) diff --git a/electrum/commands.py b/electrum/commands.py
       t@@ -1100,6 +1100,16 @@ class Commands:
                """ return the local watchtower's ctn of channel. used in regtests """
                return await self.network.local_watchtower.sweepstore.get_ctn(channel_point, None)
        
       +    @command('wn')
       +    async def reverse_swap(self, amount, wallet: Abstract_Wallet = None):
       +        from .submarine_swaps import reverse_swap
       +        amount_sat = satoshis(amount)
       +        return await reverse_swap(amount_sat, wallet, self.network)
       +
       +    @command('wn')
       +    async def claim_swap(self, key, wallet: Abstract_Wallet = None):
       +        from .submarine_swaps import claim_swap
       +        return await claim_swap(key, wallet)
        
        def eval_bool(x: str) -> bool:
            if x == 'false': return False
 (DIR) diff --git a/electrum/submarine_swaps.py b/electrum/submarine_swaps.py
       t@@ -0,0 +1,154 @@
       +import asyncio
       +import json
       +import os
       +from .crypto import sha256, hash_160
       +from .ecc import ECPrivkey
       +from .bitcoin import address_to_script, script_to_p2wsh, opcodes
       +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
       +
       +
       +# todo:
       +#  - integrate with lnwatcher
       +#  - forward swaps
       +
       +API_URL = 'http://ecdsa.org:9001'
       +
       +WITNESS_TEMPLATE_SWAP = [
       +    opcodes.OP_SIZE,
       +    OPPushDataGeneric(None),
       +    opcodes.OP_EQUAL,
       +    opcodes.OP_IF,
       +    opcodes.OP_HASH160,
       +    OPPushDataGeneric(lambda x: x == 20),
       +    opcodes.OP_EQUALVERIFY,
       +    OPPushDataPubkey,
       +    opcodes.OP_ELSE,
       +    opcodes.OP_DROP,
       +    OPPushDataGeneric(None),
       +    opcodes.OP_CHECKLOCKTIMEVERIFY,
       +    opcodes.OP_DROP,
       +    OPPushDataPubkey,
       +    opcodes.OP_ENDIF,
       +    opcodes.OP_CHECKSIG
       +]
       +
       +
       +def create_claim_tx(txid, index, witness_script, preimage, privkey:bytes, amount_sat, address, fee):
       +    pubkey = ECPrivkey(privkey).get_public_key_bytes(compressed=True)
       +    prevout = TxOutpoint(txid=bytes.fromhex(txid), out_idx=index)
       +    txin = PartialTxInput(prevout=prevout)
       +    txin.script_type = 'p2wsh'
       +    txin.script_sig = b''
       +    txin.pubkeys = [pubkey]
       +    txin.num_sig = 1
       +    txin.witness_script = witness_script
       +    txin._trusted_value_sats = amount_sat
       +    txout = PartialTxOutput(scriptpubkey=bytes.fromhex(address_to_script(address)), value=amount_sat - fee)
       +    tx = PartialTransaction.from_io([txin], [txout], version=2)
       +    sig = bytes.fromhex(tx.sign_txin(0, privkey))
       +    witness = construct_witness([sig, preimage, witness_script])
       +    tx.inputs()[0].witness = bytes.fromhex(witness)
       +    assert tx.is_complete()
       +    return tx
       +
       +
       +@log_exceptions
       +async def _claim_swap(lnworker, lockup_address, redeem_script, preimage, privkey, onchain_amount, address):
       +    # add address to lnwatcher
       +    lnwatcher = lnworker.lnwatcher
       +    lnwatcher.add_address(lockup_address)
       +    while True:
       +        h = lnwatcher.get_address_history(lockup_address)
       +        if not h:
       +            await asyncio.sleep(1)
       +            continue
       +        for txid, height in h:
       +            tx = lnwatcher.db.get_transaction(txid)
       +            # find relevant output
       +            for i, o in enumerate(tx.outputs()):
       +                if o.address == lockup_address:
       +                    break
       +            else:
       +                continue
       +            # create claim tx
       +            fee = lnwatcher.config.estimate_fee(136, allow_fallback_to_static_rates=True)
       +            tx = create_claim_tx(txid, i, redeem_script, preimage, privkey, onchain_amount, address, fee)
       +            try:
       +                await lnwatcher.network.broadcast_transaction(tx)
       +                break
       +            except:
       +                continue
       +
       +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'])
       +    lockup_address = data['lockupAddress']
       +    preimage = bytes.fromhex(data['preimage'])
       +    privkey = bytes.fromhex(data['privkey'])
       +    await _claim_swap(lnworker, lockup_address, redeem_script, preimage, privkey, onchain_amount, address)
       +
       +@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']
       +    timeout_block_height = 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_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
       +    cltv = int.from_bytes(parsed_script[10][1], byteorder='little')
       +    assert cltv - 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 to lnwatcher
       +    f = asyncio.ensure_future(_claim_swap(lnworker, lockup_address, redeem_script, preimage, privkey, onchain_amount, address))
       +    # initiate payment.
       +    success, log = await lnworker._pay(invoice, attempts=1)
       +    # discard data; this should be done by lnwatcher
       +    if success:
       +        swaps.pop(response_id)
       +    return {
       +        'id':response_id,
       +        'success':success,
       +    }