import asyncio
import json
import os
from typing import TYPE_CHECKING, Optional, Dict, Union, Sequence, Tuple, Iterable
from decimal import Decimal
import math
import time

import attr
import aiohttp

import electrum_ecc as ecc
from electrum_ecc import ECPrivkey

import electrum_aionostr as aionostr
from electrum_aionostr.util import to_nip19

from collections import defaultdict


from . import lnutil
from .crypto import sha256, hash_160
from .bitcoin import (script_to_p2wsh, opcodes,
                      construct_witness)
from .transaction import PartialTxInput, PartialTxOutput, PartialTransaction, Transaction, TxInput, TxOutpoint
from .transaction import script_GetOp, match_script_against_template, OPPushDataGeneric, OPPushDataPubkey
from .util import log_exceptions, ignore_exceptions, BelowDustLimit, OldTaskGroup, age
from .lnutil import REDEEM_AFTER_DOUBLE_SPENT_DELAY
from .bitcoin import dust_threshold, DummyAddress
from .logging import Logger
from .lnutil import hex_to_bytes
from .lnaddr import lndecode
from .json_db import StoredObject, stored_in
from . import constants
from .address_synchronizer import TX_HEIGHT_LOCAL
from .i18n import _

from .bitcoin import construct_script
from .crypto import ripemd
from .invoices import Invoice
from .network import TxBroadcastError
from .lnonion import OnionRoutingFailure, OnionFailureCode


if TYPE_CHECKING:
    from .network import Network
    from .wallet import Abstract_Wallet
    from .lnwatcher import LNWalletWatcher
    from .lnworker import LNWallet
    from .lnchannel import Channel
    from .simple_config import SimpleConfig



CLAIM_FEE_SIZE = 136
LOCKUP_FEE_SIZE = 153 # assuming 1 output, 2 outputs

MIN_LOCKTIME_DELTA = 60
LOCKTIME_DELTA_REFUND = 70
MAX_LOCKTIME_DELTA = 100
MIN_FINAL_CLTV_DELTA_FOR_CLIENT = 3 * 144  # note: put in invoice, but is not enforced by receiver in lnpeer.py
assert MIN_LOCKTIME_DELTA <= LOCKTIME_DELTA_REFUND <= MAX_LOCKTIME_DELTA
assert MAX_LOCKTIME_DELTA < lnutil.MIN_FINAL_CLTV_DELTA_ACCEPTED
assert MAX_LOCKTIME_DELTA < lnutil.MIN_FINAL_CLTV_DELTA_FOR_INVOICE
assert MAX_LOCKTIME_DELTA < MIN_FINAL_CLTV_DELTA_FOR_CLIENT


# The script of the reverse swaps has one extra check in it to verify
# that the length of the preimage is 32. This is required because in
# the reverse swaps the preimage is generated by the user and to
# settle the hold invoice, you need a preimage with 32 bytes . If that
# check wasn't there the user could generate a preimage with a
# different length which would still allow for claiming the onchain
# coins but the invoice couldn't be settled

WITNESS_TEMPLATE_REVERSE_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 check_reverse_redeem_script(
    *,
    redeem_script: bytes,
    lockup_address: str,
    payment_hash: bytes,
    locktime: int,
    refund_pubkey: bytes = None,
    claim_pubkey: bytes = None,
) -> None:
    parsed_script = [x for x in script_GetOp(redeem_script)]
    if not match_script_against_template(redeem_script, WITNESS_TEMPLATE_REVERSE_SWAP):
        raise Exception("rswap check failed: scriptcode does not match template")
    if script_to_p2wsh(redeem_script) != lockup_address:
        raise Exception("rswap check failed: inconsistent scriptcode and address")
    if ripemd(payment_hash) != parsed_script[5][1]:
        raise Exception("rswap check failed: our preimage not in script")
    if claim_pubkey and claim_pubkey != parsed_script[7][1]:
        raise Exception("rswap check failed: our pubkey not in script")
    if refund_pubkey and refund_pubkey != parsed_script[13][1]:
        raise Exception("rswap check failed: our pubkey not in script")
    if locktime != int.from_bytes(parsed_script[10][1], byteorder='little'):
        raise Exception("rswap check failed: inconsistent locktime and script")


class SwapServerError(Exception):
    def __str__(self):
        return _("The swap server errored or is unreachable.")

def now():
    return int(time.time())

@attr.s
class SwapFees:
    percentage = attr.ib(type=int)
    normal_fee = attr.ib(type=int)
    lockup_fee = attr.ib(type=int)
    claim_fee = attr.ib(type=int)
    min_amount = attr.ib(type=int)
    max_amount = attr.ib(type=int)

@stored_in('submarine_swaps')
@attr.s
class SwapData(StoredObject):
    is_reverse = attr.ib(type=bool)  # for whoever is running code (PoV of client or server)
    locktime = attr.ib(type=int)
    onchain_amount = attr.ib(type=int)  # in sats
    lightning_amount = attr.ib(type=int)  # in sats
    redeem_script = attr.ib(type=bytes, converter=hex_to_bytes)
    preimage = attr.ib(type=Optional[bytes], converter=hex_to_bytes)
    prepay_hash = attr.ib(type=Optional[bytes], converter=hex_to_bytes)
    privkey = attr.ib(type=bytes, converter=hex_to_bytes)
    lockup_address = attr.ib(type=str)
    receive_address = attr.ib(type=str)
    funding_txid = attr.ib(type=Optional[str])
    spending_txid = attr.ib(type=Optional[str])
    is_redeemed = attr.ib(type=bool)

    _funding_prevout = None  # type: Optional[TxOutpoint]  # for RBF
    _payment_hash = None
    _zeroconf = False

    @property
    def payment_hash(self) -> bytes:
        return self._payment_hash

def create_claim_tx(
        *,
        txin: PartialTxInput,
        witness_script: bytes,
        address: str,
        amount_sat: int,
        locktime: int,
) -> PartialTransaction:
    """Create tx to either claim successful reverse-swap,
    or to get refunded for timed-out forward-swap.
    """
    txin.nsequence = 0xffffffff - 2
    txin.script_sig = b''
    txin.witness_script = witness_script
    txout = PartialTxOutput.from_address_and_value(address, amount_sat)
    tx = PartialTransaction.from_io([txin], [txout], version=2, locktime=locktime)
    return tx


class SwapManager(Logger):

    network: Optional['Network'] = None
    lnwatcher: Optional['LNWalletWatcher'] = None

    def __init__(self, *, wallet: 'Abstract_Wallet', lnworker: 'LNWallet'):
        Logger.__init__(self)
        self.normal_fee = None
        self.lockup_fee = None
        self.claim_fee = None # part of the boltz prococol, not used by Electrum
        self.percentage = None
        self._min_amount = None
        self._max_amount = None

        self.wallet = wallet
        self.config = wallet.config
        self.lnworker = lnworker
        self.config = wallet.config
        self.taskgroup = OldTaskGroup()
        self.dummy_address = DummyAddress.SWAP

        self.swaps = self.wallet.db.get_dict('submarine_swaps')  # type: Dict[str, SwapData]
        self._swaps_by_funding_outpoint = {}  # type: Dict[TxOutpoint, SwapData]
        self._swaps_by_lockup_address = {}  # type: Dict[str, SwapData]
        for payment_hash_hex, swap in self.swaps.items():
            payment_hash = bytes.fromhex(payment_hash_hex)
            swap._payment_hash = payment_hash
            self._add_or_reindex_swap(swap)
            if not swap.is_reverse and not swap.is_redeemed:
                self.lnworker.register_hold_invoice(payment_hash, self.hold_invoice_callback)

        self.prepayments = {}  # type: Dict[bytes, bytes] # fee_rhash -> rhash
        for k, swap in self.swaps.items():
            if swap.prepay_hash is not None:
                self.prepayments[swap.prepay_hash] = bytes.fromhex(k)
        self.is_server = False # overriden by swapserver plugin if enabled
        self.is_initialized = asyncio.Event()

    def start_network(self, network: 'Network'):
        assert network
        if self.network is not None:
            self.logger.info('start_network: already started')
            return
        self.logger.info('start_network: starting main loop')
        self.network = network
        self.lnwatcher = self.lnworker.lnwatcher
        for k, swap in self.swaps.items():
            if swap.is_redeemed:
                continue
            self.add_lnwatcher_callback(swap)
        asyncio.run_coroutine_threadsafe(self.main_loop(), self.network.asyncio_loop)

    @log_exceptions
    async def run_nostr_server(self):
        with NostrTransport(self.config, self, self.lnworker.nostr_keypair) as transport:
            await transport.is_connected.wait()
            self.logger.info(f'nostr is connected')
            while True:
                # todo: publish everytime fees have changed
                self.server_update_pairs()
                await transport.publish_offer(self)
                await asyncio.sleep(600)

    @log_exceptions
    async def main_loop(self):
        tasks = [self.pay_pending_invoices()]
        if self.is_server:
            # nostr and http are not mutually exclusive
            if self.config.SWAPSERVER_PORT:
                tasks.append(self.http_server.run())
            if self.config.NOSTR_RELAYS:
                tasks.append(self.run_nostr_server())

        async with self.taskgroup as group:
            for task in tasks:
                await group.spawn(task)

    async def stop(self):
        await self.taskgroup.cancel_remaining()

    def create_transport(self):
        from .lnutil import generate_random_keypair
        if self.config.SWAPSERVER_URL:
            return HttpTransport(self.config, self)
        else:
            keypair = self.lnworker.nostr_keypair if self.is_server else generate_random_keypair()
            return NostrTransport(self.config, self, keypair)

    async def pay_invoice(self, key):
        self.logger.info(f'trying to pay invoice {key}')
        self.invoices_to_pay[key] = 1000000000000 # lock
        try:
            invoice = self.wallet.get_invoice(key)
            success, log = await self.lnworker.pay_invoice(invoice.lightning_invoice, attempts=10)
        except Exception as e:
            self.logger.info(f'exception paying {key}, will not retry')
            self.invoices_to_pay.pop(key, None)
            return
        if not success:
            self.logger.info(f'failed to pay {key}, will retry in 10 minutes')
            self.invoices_to_pay[key] = now() + 600
        else:
            self.logger.info(f'paid invoice {key}')
            self.invoices_to_pay.pop(key, None)

    async def pay_pending_invoices(self):
        self.invoices_to_pay = {}
        while True:
            await asyncio.sleep(5)
            for key, not_before in list(self.invoices_to_pay.items()):
                if now() < not_before:
                    continue
                await self.taskgroup.spawn(self.pay_invoice(key))

    def cancel_normal_swap(self, swap: SwapData):
        """ we must not have broadcast the funding tx """
        if swap is None:
            return
        if swap.funding_txid is not None:
            self.logger.info(f'cannot cancel swap {swap.payment_hash.hex()}: already funded')
            return
        self._fail_swap(swap, 'user cancelled')

    def _fail_swap(self, swap: SwapData, reason: str):
        self.logger.info(f'failing swap {swap.payment_hash.hex()}: {reason}')
        if not swap.is_reverse and swap.payment_hash in self.lnworker.hold_invoice_callbacks:
            self.lnworker.unregister_hold_invoice(swap.payment_hash)
            payment_secret = self.lnworker.get_payment_secret(swap.payment_hash)
            payment_key = swap.payment_hash + payment_secret
            e = OnionRoutingFailure(code=OnionFailureCode.INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS, data=b'')
            self.lnworker.save_forwarding_failure(payment_key.hex(), failure_message=e)
        self.lnwatcher.remove_callback(swap.lockup_address)
        if swap.funding_txid is None:
            self.swaps.pop(swap.payment_hash.hex())

    @log_exceptions
    async def _claim_swap(self, swap: SwapData) -> None:
        assert self.network
        assert self.lnwatcher
        if not self.lnwatcher.adb.is_up_to_date():
            return
        current_height = self.network.get_local_height()
        remaining_time = swap.locktime - current_height
        txos = self.lnwatcher.adb.get_addr_outputs(swap.lockup_address)

        for txin in txos.values():
            if swap.is_reverse and txin.value_sats() < swap.onchain_amount:
                # amount too low, we must not reveal the preimage
                continue
            break
        else:
            # swap not funded.
            txin = None
            # if it is a normal swap, we might have double spent the funding tx
            # in that case we need to fail the HTLCs
            if remaining_time <= 0:
                self._fail_swap(swap, 'expired')

        if txin:
            # the swap is funded
            # note: swap.funding_txid can change due to RBF, it will get updated here:
            swap.funding_txid = txin.prevout.txid.hex()
            swap._funding_prevout = txin.prevout
            self._add_or_reindex_swap(swap)  # to update _swaps_by_funding_outpoint
            funding_height = self.lnwatcher.adb.get_tx_height(txin.prevout.txid.hex())
            spent_height = txin.spent_height
            should_bump_fee = False
            if spent_height is not None:
                swap.spending_txid = txin.spent_txid
                if spent_height > 0:
                    if current_height - spent_height > REDEEM_AFTER_DOUBLE_SPENT_DELAY:
                        self.logger.info(f'stop watching swap {swap.lockup_address}')
                        self.lnwatcher.remove_callback(swap.lockup_address)
                        swap.is_redeemed = True
                elif spent_height == TX_HEIGHT_LOCAL:
                    if funding_height.conf > 0 or (swap.is_reverse and swap._zeroconf):
                        tx = self.lnwatcher.adb.get_transaction(txin.spent_txid)
                        try:
                            await self.network.broadcast_transaction(tx)
                        except TxBroadcastError:
                            self.logger.info(f'error broadcasting claim tx {txin.spent_txid}')
                    elif funding_height.height == TX_HEIGHT_LOCAL:
                        # the funding tx was double spent.
                        # this will remove both funding and child (spending tx) from adb
                        self.lnwatcher.adb.remove_transaction(swap.funding_txid)
                        swap.funding_txid = None
                        swap.spending_txid = None
                else:
                    # spending tx is in mempool
                    pass

            if not swap.is_reverse:
                if swap.preimage is None and spent_height is not None:
                    # extract the preimage, add it to lnwatcher
                    claim_tx = self.lnwatcher.adb.get_transaction(txin.spent_txid)
                    preimage = claim_tx.inputs()[0].witness_elements()[1]
                    if sha256(preimage) == swap.payment_hash:
                        swap.preimage = preimage
                        self.logger.info(f'found preimage: {preimage.hex()}')
                        self.lnworker.preimages[swap.payment_hash.hex()] = preimage.hex()
                        # note: we must check the payment secret before we broadcast the funding tx
                    else:
                        # this is our refund tx
                        if spent_height > 0:
                            self.logger.info(f'refund tx confirmed: {txin.spent_txid} {spent_height}')
                            self._fail_swap(swap, 'refund tx confirmed')
                            return
                        else:
                            claim_tx.add_info_from_wallet(self.wallet)
                            claim_tx_fee = claim_tx.get_fee()
                            recommended_fee = self.get_claim_fee()
                            if claim_tx_fee * 1.1 < recommended_fee:
                                should_bump_fee = True
                                self.logger.info(f'claim tx fee too low {claim_tx_fee} < {recommended_fee}. we will bump the fee')

                if remaining_time > 0:
                    # too early for refund
                    return
                if swap.preimage:
                    # we have been paid. do not try to get refund.
                    return
            else:
                if swap.preimage is None:
                    swap.preimage = self.lnworker.get_preimage(swap.payment_hash)
                if swap.preimage is None:
                    if funding_height.conf <= 0:
                        return
                    key = swap.payment_hash.hex()
                    if remaining_time <= MIN_LOCKTIME_DELTA:
                        if key in self.invoices_to_pay:
                            # fixme: should consider cltv of ln payment
                            self.logger.info(f'locktime too close {key} {remaining_time}')
                            self.invoices_to_pay.pop(key, None)
                        return
                    if key not in self.invoices_to_pay:
                        self.invoices_to_pay[key] = 0
                    return

                if self.network.config.TEST_SWAPSERVER_REFUND:
                    # for testing: do not create claim tx
                    return

            if spent_height is not None and not should_bump_fee:
                return
            try:
                tx = self._create_and_sign_claim_tx(txin=txin, swap=swap, config=self.wallet.config)
            except BelowDustLimit:
                self.logger.info('utxo value below dust threshold')
                return
            self.logger.info(f'adding claim tx {tx.txid()}')
            self.wallet.adb.add_transaction(tx)
            swap.spending_txid = tx.txid()
            if funding_height.conf > 0 or (swap.is_reverse and swap._zeroconf):
                try:
                    await self.network.broadcast_transaction(tx)
                except TxBroadcastError:
                    self.logger.info(f'error broadcasting claim tx {txin.spent_txid}')

    def get_claim_fee(self):
        return self.get_fee(CLAIM_FEE_SIZE)

    def get_fee(self, size):
        # note: 'size' is in vbytes
        return self._get_fee(size=size, config=self.wallet.config)

    @classmethod
    def _get_fee(cls, *, size, config: 'SimpleConfig'):
        return config.estimate_fee(size, allow_fallback_to_static_rates=True)

    def get_swap(self, payment_hash: bytes) -> Optional[SwapData]:
        # for history
        swap = self.swaps.get(payment_hash.hex())
        if swap:
            return swap
        payment_hash = self.prepayments.get(payment_hash)
        if payment_hash:
            return self.swaps.get(payment_hash.hex())

    def add_lnwatcher_callback(self, swap: SwapData) -> None:
        callback = lambda: self._claim_swap(swap)
        self.lnwatcher.add_callback(swap.lockup_address, callback)

    async def hold_invoice_callback(self, payment_hash: bytes) -> None:
        # note: this assumes the wallet has been unlocked
        key = payment_hash.hex()
        if key in self.swaps:
            swap = self.swaps[key]
            if swap.funding_txid is None:
                password = self.wallet.get_unlocked_password()
                for batch_rbf in [False]:
                    # FIXME: tx batching is disabled, because extra logic is needed to handle
                    # the case where the base tx gets mined.
                    tx = self.create_funding_tx(swap, None, password=password)
                    self.logger.info(f'adding funding_tx {tx.txid()}')
                    self.wallet.adb.add_transaction(tx)
                    try:
                        await self.broadcast_funding_tx(swap, tx)
                    except TxBroadcastError:
                        self.wallet.adb.remove_transaction(tx.txid())
                        continue
                    break

    def create_normal_swap(self, *, lightning_amount_sat: int, payment_hash: bytes, their_pubkey: bytes = None):
        """ server method """
        assert lightning_amount_sat
        locktime = self.network.get_local_height() + LOCKTIME_DELTA_REFUND
        our_privkey = os.urandom(32)
        our_pubkey = ECPrivkey(our_privkey).get_public_key_bytes(compressed=True)
        onchain_amount_sat = self._get_recv_amount(lightning_amount_sat, is_reverse=True) # what the client is going to receive
        redeem_script = construct_script(
            WITNESS_TEMPLATE_REVERSE_SWAP,
            {1:32, 5:ripemd(payment_hash), 7:their_pubkey, 10:locktime, 13:our_pubkey}
        )
        swap, invoice, prepay_invoice = self.add_normal_swap(
            redeem_script=redeem_script,
            locktime=locktime,
            onchain_amount_sat=onchain_amount_sat,
            lightning_amount_sat=lightning_amount_sat,
            payment_hash=payment_hash,
            our_privkey=our_privkey,
            prepay=True,
        )
        self.lnworker.register_hold_invoice(payment_hash, self.hold_invoice_callback)
        return swap, invoice, prepay_invoice

    def add_normal_swap(
            self, *,
            redeem_script: bytes,
            locktime: int,  # onchain
            onchain_amount_sat: int,
            lightning_amount_sat: int,
            payment_hash: bytes,
            our_privkey: bytes,
            prepay: bool,
            channels: Optional[Sequence['Channel']] = None,
            min_final_cltv_expiry_delta: Optional[int] = None,
    ) -> Tuple[SwapData, str, Optional[str]]:
        """creates a hold invoice"""
        if prepay:
            prepay_amount_sat = self.get_claim_fee() * 2
            invoice_amount_sat = lightning_amount_sat - prepay_amount_sat
        else:
            invoice_amount_sat = lightning_amount_sat

        _, invoice = self.lnworker.get_bolt11_invoice(
            payment_hash=payment_hash,
            amount_msat=invoice_amount_sat * 1000,
            message='Submarine swap',
            expiry=300,
            fallback_address=None,
            channels=channels,
            min_final_cltv_expiry_delta=min_final_cltv_expiry_delta,
        )
        # add payment info to lnworker
        self.lnworker.add_payment_info_for_hold_invoice(payment_hash, invoice_amount_sat)

        if prepay:
            prepay_hash = self.lnworker.create_payment_info(amount_msat=prepay_amount_sat*1000)
            _, prepay_invoice = self.lnworker.get_bolt11_invoice(
                payment_hash=prepay_hash,
                amount_msat=prepay_amount_sat * 1000,
                message='Submarine swap mining fees',
                expiry=300,
                fallback_address=None,
                channels=channels,
                min_final_cltv_expiry_delta=min_final_cltv_expiry_delta,
            )
            self.lnworker.bundle_payments([payment_hash, prepay_hash])
            self.prepayments[prepay_hash] = payment_hash
        else:
            prepay_invoice = None
            prepay_hash = None

        lockup_address = script_to_p2wsh(redeem_script)
        receive_address = self.wallet.get_receiving_address()
        swap = SwapData(
            redeem_script=redeem_script,
            locktime = locktime,
            privkey = our_privkey,
            preimage = None,
            prepay_hash = prepay_hash,
            lockup_address = lockup_address,
            onchain_amount = onchain_amount_sat,
            receive_address = receive_address,
            lightning_amount = lightning_amount_sat,
            is_reverse = False,
            is_redeemed = False,
            funding_txid = None,
            spending_txid = None,
        )
        swap._payment_hash = payment_hash
        self._add_or_reindex_swap(swap)
        self.add_lnwatcher_callback(swap)
        return swap, invoice, prepay_invoice

    def create_reverse_swap(self, *, lightning_amount_sat: int, their_pubkey: bytes) -> SwapData:
        """ server method. """
        assert lightning_amount_sat is not None
        locktime = self.network.get_local_height() + LOCKTIME_DELTA_REFUND
        privkey = os.urandom(32)
        our_pubkey = ECPrivkey(privkey).get_public_key_bytes(compressed=True)
        onchain_amount_sat = self._get_send_amount(lightning_amount_sat, is_reverse=False)
        preimage = os.urandom(32)
        payment_hash = sha256(preimage)
        redeem_script = construct_script(
            WITNESS_TEMPLATE_REVERSE_SWAP,
            {1:32, 5:ripemd(payment_hash), 7:our_pubkey, 10:locktime, 13:their_pubkey}
        )
        swap = self.add_reverse_swap(
            redeem_script=redeem_script,
            locktime=locktime,
            privkey=privkey,
            preimage=preimage,
            payment_hash=payment_hash,
            prepay_hash=None,
            onchain_amount_sat=onchain_amount_sat,
            lightning_amount_sat=lightning_amount_sat)
        return swap

    def add_reverse_swap(
        self,
        *,
        redeem_script: bytes,
        locktime: int,  # onchain
        privkey: bytes,
        lightning_amount_sat: int,
        onchain_amount_sat: int,
        preimage: bytes,
        payment_hash: bytes,
        prepay_hash: Optional[bytes] = None,
    ) -> SwapData:
        lockup_address = script_to_p2wsh(redeem_script)
        receive_address = self.wallet.get_receiving_address()
        swap = SwapData(
            redeem_script = redeem_script,
            locktime = locktime,
            privkey = privkey,
            preimage = preimage,
            prepay_hash = prepay_hash,
            lockup_address = lockup_address,
            onchain_amount = onchain_amount_sat,
            receive_address = receive_address,
            lightning_amount = lightning_amount_sat,
            is_reverse = True,
            is_redeemed = False,
            funding_txid = None,
            spending_txid = None,
        )
        if prepay_hash:
            self.prepayments[prepay_hash] = payment_hash
        swap._payment_hash = payment_hash
        self._add_or_reindex_swap(swap)
        self.add_lnwatcher_callback(swap)
        return swap

    def server_add_swap_invoice(self, request):
        invoice = request['invoice']
        invoice = Invoice.from_bech32(invoice)
        key = invoice.rhash
        payment_hash = bytes.fromhex(key)
        assert key in self.swaps
        swap = self.swaps[key]
        assert swap.lightning_amount == int(invoice.get_amount_sat())
        self.wallet.save_invoice(invoice)
        # check that we have the preimage
        assert sha256(swap.preimage) == payment_hash
        assert swap.spending_txid is None
        self.invoices_to_pay[key] = 0
        return {}

    async def normal_swap(
            self,
            *,
            lightning_amount_sat: int,
            expected_onchain_amount_sat: int,
            password,
            tx: PartialTransaction = None,
            channels = None,
    ) -> Optional[str]:
        """send on-chain BTC, receive on Lightning

        Old (removed) flow:
        - User generates an LN invoice with RHASH, and knows preimage.
        - User creates on-chain output locked to RHASH.
        - Server pays LN invoice. User reveals preimage.
        - Server spends the on-chain output using preimage.
        cltv safety requirement: (onchain_locktime > LN_locktime),   otherwise server is vulnerable

        New flow:
         - User requests swap
         - Server creates preimage, sends RHASH to user
         - User creates hold invoice, sends it to server
         - Server sends HTLC, user holds it
         - User creates on-chain output locked to RHASH
         - Server spends the on-chain output using preimage (revealing the preimage)
         - User fulfills HTLC using preimage
        cltv safety requirement: (onchain_locktime < LN_locktime),   otherwise client is vulnerable
        """
        assert self.network
        assert self.lnwatcher
        swap, invoice = await self.request_normal_swap(
            lightning_amount_sat=lightning_amount_sat,
            expected_onchain_amount_sat=expected_onchain_amount_sat,
            channels=channels,
        )
        tx = self.create_funding_tx(swap, tx, password=password)
        return await self.wait_for_htlcs_and_broadcast(swap=swap, invoice=invoice, tx=tx)

    async def request_normal_swap(
            self, transport,
        *,
        lightning_amount_sat: int,
        expected_onchain_amount_sat: int,
        channels: Optional[Sequence['Channel']] = None,
    ) -> Tuple[SwapData, str]:
        await self.is_initialized.wait() # add timeout
        refund_privkey = os.urandom(32)
        refund_pubkey = ECPrivkey(refund_privkey).get_public_key_bytes(compressed=True)
        self.logger.info('requesting preimage hash for swap')
        request_data = {
            "invoiceAmount": lightning_amount_sat,
            "refundPublicKey": refund_pubkey.hex()
        }
        data = await transport.send_request_to_server('createnormalswap', request_data)
        payment_hash = bytes.fromhex(data["preimageHash"])

        zeroconf = data["acceptZeroConf"]
        onchain_amount = data["expectedAmount"]
        locktime = data["timeoutBlockHeight"]
        lockup_address = data["address"]
        redeem_script = bytes.fromhex(data["redeemScript"])
        # verify redeem_script is built with our pubkey and preimage
        check_reverse_redeem_script(
            redeem_script=redeem_script,
            lockup_address=lockup_address,
            payment_hash=payment_hash,
            locktime=locktime,
            refund_pubkey=refund_pubkey,
        )

        # check that onchain_amount is not more than what we estimated
        if onchain_amount > expected_onchain_amount_sat:
            raise Exception(f"fswap check failed: onchain_amount is more than what we estimated: "
                            f"{onchain_amount} > {expected_onchain_amount_sat}")
        # verify that they are not locking up funds for too long
        if locktime - self.network.get_local_height() > MAX_LOCKTIME_DELTA:
            raise Exception("fswap check failed: locktime too far in future")

        swap, invoice, _ = self.add_normal_swap(
            redeem_script=redeem_script,
            locktime=locktime,
            lightning_amount_sat=lightning_amount_sat,
            onchain_amount_sat=onchain_amount,
            payment_hash=payment_hash,
            our_privkey=refund_privkey,
            prepay=False,
            channels=channels,
            # When the client is doing a normal swap, we create a ln-invoice with larger than usual final_cltv_delta.
            # If the user goes offline after broadcasting the funding tx (but before it is mined and
            # the server claims it), they need to come back online before the held ln-htlc expires (see #8940).
            # If the held ln-htlc expires, and the funding tx got confirmed, the server will have claimed the onchain
            # funds, and the ln-htlc will be timed out onchain (and channel force-closed). i.e. the user loses the swap
            # amount. Increasing the final_cltv_delta the user puts in the invoice extends this critical window.
            min_final_cltv_expiry_delta=MIN_FINAL_CLTV_DELTA_FOR_CLIENT,
        )
        return swap, invoice

    async def wait_for_htlcs_and_broadcast(
            self, transport,
        *,
        swap: SwapData,
        invoice: str,
        tx: Transaction,
    ) -> Optional[str]:
        await transport.is_connected.wait()
        payment_hash = swap.payment_hash
        refund_pubkey = ECPrivkey(swap.privkey).get_public_key_bytes(compressed=True)
        async def callback(payment_hash):
            # FIXME what if this raises, e.g. TxBroadcastError?
            #       We will never retry the hold-invoice-callback.
            await self.broadcast_funding_tx(swap, tx)

        self.lnworker.register_hold_invoice(payment_hash, callback)

        # send invoice to server and wait for htlcs
        request_data = {
            "preimageHash": payment_hash.hex(),
            "invoice": invoice,
            "refundPublicKey": refund_pubkey.hex(),
        }
        data = await transport.send_request_to_server('addswapinvoice', request_data)
        # wait for funding tx
        lnaddr = lndecode(invoice)
        while swap.funding_txid is None and not lnaddr.is_expired():
            await asyncio.sleep(0.1)
        return swap.funding_txid

    def create_funding_tx(
        self,
        swap: SwapData,
        tx: Optional[PartialTransaction],
        *,
        password,
    ) -> PartialTransaction:
        # create funding tx
        # note: rbf must not decrease payment
        # this is taken care of in wallet._is_rbf_allowed_to_touch_tx_output
        if tx is None:
            funding_output = PartialTxOutput.from_address_and_value(swap.lockup_address, swap.onchain_amount)
            tx = self.wallet.create_transaction(
                outputs=[funding_output],
                rbf=True,
                password=password,
            )
        else:
            tx.replace_output_address(DummyAddress.SWAP, swap.lockup_address)
            tx.set_rbf(True)
            self.wallet.sign_transaction(tx, password)
        return tx

    @log_exceptions
    async def request_swap_for_tx(self, transport, tx: 'PartialTransaction') -> Optional[Tuple[SwapData, str, PartialTransaction]]:
        for o in tx.outputs():
            if o.address == self.dummy_address:
                change_amount = o.value
                break
        else:
            return
        await self.is_initialized.wait()
        lightning_amount_sat = self.get_recv_amount(change_amount, is_reverse=False)
        swap, invoice = await self.request_normal_swap(
            transport,
            lightning_amount_sat = lightning_amount_sat,
            expected_onchain_amount_sat=change_amount)
        tx.replace_output_address(DummyAddress.SWAP, swap.lockup_address)
        return swap, invoice, tx

    @log_exceptions
    async def broadcast_funding_tx(self, swap: SwapData, tx: Transaction) -> None:
        swap.funding_txid = tx.txid()
        await self.network.broadcast_transaction(tx)

    async def reverse_swap(
            self, transport,
            *,
            lightning_amount_sat: int,
            expected_onchain_amount_sat: int,
            zeroconf: bool=False,
            channels: Optional[Sequence['Channel']] = None,
    ) -> Optional[str]:
        """send on Lightning, receive on-chain

        - User generates preimage, RHASH. Sends RHASH to server.
        - Server creates an LN invoice for RHASH.
        - User pays LN invoice - except server needs to hold the HTLC as preimage is unknown.
            - if the server requested a fee prepayment (using 'minerFeeInvoice'),
              the server will have the preimage for that. The user will send HTLCs for both the main RHASH,
              and for the fee prepayment. Once both MPP sets arrive at the server, the server will fulfill
              the HTLCs for the fee prepayment (before creating the on-chain output).
        - Server creates on-chain output locked to RHASH.
        - User spends on-chain output, revealing preimage.
        - Server fulfills HTLC using preimage.

        Note: expected_onchain_amount_sat is BEFORE deducting the on-chain claim tx fee.
        """
        assert self.network
        assert self.lnwatcher
        privkey = os.urandom(32)
        our_pubkey = ECPrivkey(privkey).get_public_key_bytes(compressed=True)
        preimage = os.urandom(32)
        payment_hash = sha256(preimage)
        request_data = {
            "type": "reversesubmarine",
            "pairId": "BTC/BTC",
            "orderSide": "buy",
            "invoiceAmount": lightning_amount_sat,
            "preimageHash": payment_hash.hex(),
            "claimPublicKey": our_pubkey.hex()
        }
        data = await transport.send_request_to_server('createswap', request_data)
        invoice = data['invoice']
        fee_invoice = data.get('minerFeeInvoice')
        lockup_address = data['lockupAddress']
        redeem_script = bytes.fromhex(data['redeemScript'])
        locktime = data['timeoutBlockHeight']
        onchain_amount = data["onchainAmount"]
        response_id = data['id']
        # verify redeem_script is built with our pubkey and preimage
        check_reverse_redeem_script(
            redeem_script=redeem_script,
            lockup_address=lockup_address,
            payment_hash=payment_hash,
            locktime=locktime,
            refund_pubkey=None,
            claim_pubkey=our_pubkey,
        )
        # check that the onchain amount is what we expected
        if onchain_amount < expected_onchain_amount_sat:
            raise Exception(f"rswap check failed: onchain_amount is less than what we expected: "
                            f"{onchain_amount} < {expected_onchain_amount_sat}")
        # verify that we will have enough time to get our tx confirmed
        if locktime - self.network.get_local_height() <= MIN_LOCKTIME_DELTA:
            raise Exception("rswap check failed: locktime too close")
        # verify invoice payment_hash
        lnaddr = self.lnworker._check_invoice(invoice)
        invoice_amount = int(lnaddr.get_amount_sat())
        if lnaddr.paymenthash != payment_hash:
            raise Exception("rswap check failed: inconsistent RHASH and invoice")
        # check that the lightning amount is what we requested
        if fee_invoice:
            fee_lnaddr = self.lnworker._check_invoice(fee_invoice)
            invoice_amount += fee_lnaddr.get_amount_sat()
            prepay_hash = fee_lnaddr.paymenthash
        else:
            prepay_hash = None
        if int(invoice_amount) != lightning_amount_sat:
            raise Exception(f"rswap check failed: invoice_amount ({invoice_amount}) "
                            f"not what we requested ({lightning_amount_sat})")
        # save swap data to wallet file
        swap = self.add_reverse_swap(
            redeem_script=redeem_script,
            locktime=locktime,
            privkey=privkey,
            preimage=preimage,
            payment_hash=payment_hash,
            prepay_hash=prepay_hash,
            onchain_amount_sat=onchain_amount,
            lightning_amount_sat=lightning_amount_sat)
        swap._zeroconf = zeroconf
        # initiate fee payment.
        if fee_invoice:
            asyncio.ensure_future(self.lnworker.pay_invoice(fee_invoice))
        # we return if we detect funding
        async def wait_for_funding(swap):
            while swap.funding_txid is None:
                await asyncio.sleep(1)
        # initiate main payment
        tasks = [asyncio.create_task(self.lnworker.pay_invoice(invoice, channels=channels)), asyncio.create_task(wait_for_funding(swap))]
        await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)
        return swap.funding_txid

    def _add_or_reindex_swap(self, swap: SwapData) -> None:
        if swap.payment_hash.hex() not in self.swaps:
            self.swaps[swap.payment_hash.hex()] = swap
        if swap._funding_prevout:
            self._swaps_by_funding_outpoint[swap._funding_prevout] = swap
        self._swaps_by_lockup_address[swap.lockup_address] = swap

    def server_update_pairs(self) -> None:
        """ for server """
        self.percentage = float(self.config.SWAPSERVER_FEE_MILLIONTHS) / 10000
        self._min_amount = 20000
        self._max_amount = 10000000
        self.normal_fee = self.get_fee(CLAIM_FEE_SIZE)
        self.lockup_fee = self.get_fee(LOCKUP_FEE_SIZE)
        self.claim_fee = self.get_fee(CLAIM_FEE_SIZE)

    def update_pairs(self, pairs):
        self.logger.info(f'updating fees {pairs}')
        self.normal_fee = pairs.normal_fee
        self.lockup_fee = pairs.lockup_fee
        self.claim_fee = pairs.claim_fee
        self.percentage = pairs.percentage
        self._min_amount = pairs.min_amount
        self._max_amount = pairs.max_amount
        self.is_initialized.set()

    def get_max_amount(self):
        return self._max_amount

    def get_min_amount(self):
        return self._min_amount

    def check_invoice_amount(self, x):
        return x >= self.get_min_amount() and x <= self.get_max_amount()

    def _get_recv_amount(self, send_amount: Optional[int], *, is_reverse: bool) -> Optional[int]:
        """For a given swap direction and amount we send, returns how much we will receive.

        Note: in the reverse direction, the mining fee for the on-chain claim tx is NOT accounted for.
        In the reverse direction, the result matches what the swap server returns as response["onchainAmount"].
        """
        if send_amount is None:
            return
        x = Decimal(send_amount)
        percentage = Decimal(self.percentage)
        if is_reverse:
            if not self.check_invoice_amount(x):
                return
            # see/ref:
            # https://github.com/BoltzExchange/boltz-backend/blob/e7e2d30f42a5bea3665b164feb85f84c64d86658/lib/service/Service.ts#L948
            percentage_fee = math.ceil(percentage * x / 100)
            base_fee = self.lockup_fee
            x -= percentage_fee + base_fee
            x = math.floor(x)
            if x < dust_threshold():
                return
        else:
            x -= self.normal_fee
            percentage_fee = math.ceil(x * percentage / (100 + percentage))
            x -= percentage_fee
            if not self.check_invoice_amount(x):
                return
        x = int(x)
        return x

    def _get_send_amount(self, recv_amount: Optional[int], *, is_reverse: bool) -> Optional[int]:
        """For a given swap direction and amount we want to receive, returns how much we will need to send.

        Note: in the reverse direction, the mining fee for the on-chain claim tx is NOT accounted for.
        In the forward direction, the result matches what the swap server returns as response["expectedAmount"].
        """
        if not recv_amount:
            return
        x = Decimal(recv_amount)
        percentage = Decimal(self.percentage)
        if is_reverse:
            # see/ref:
            # https://github.com/BoltzExchange/boltz-backend/blob/e7e2d30f42a5bea3665b164feb85f84c64d86658/lib/service/Service.ts#L928
            # https://github.com/BoltzExchange/boltz-backend/blob/e7e2d30f42a5bea3665b164feb85f84c64d86658/lib/service/Service.ts#L958
            base_fee = self.lockup_fee
            x += base_fee
            x = math.ceil(x / ((100 - percentage) / 100))
            if not self.check_invoice_amount(x):
                return
        else:
            if not self.check_invoice_amount(x):
                return
            # see/ref:
            # https://github.com/BoltzExchange/boltz-backend/blob/e7e2d30f42a5bea3665b164feb85f84c64d86658/lib/service/Service.ts#L708
            # https://github.com/BoltzExchange/boltz-backend/blob/e7e2d30f42a5bea3665b164feb85f84c64d86658/lib/rates/FeeProvider.ts#L90
            percentage_fee = math.ceil(percentage * x / 100)
            x += percentage_fee + self.normal_fee
        x = int(x)
        return x

    def get_recv_amount(self, send_amount: Optional[int], *, is_reverse: bool) -> Optional[int]:
        # first, add percentage fee
        recv_amount = self._get_recv_amount(send_amount, is_reverse=is_reverse)
        # sanity check calculation can be inverted
        if recv_amount is not None:
            inverted_send_amount = self._get_send_amount(recv_amount, is_reverse=is_reverse)
            # accept off-by ones as amt_rcv = recv_amt(send_amt(amt_rcv)) only up to +-1
            if abs(send_amount - inverted_send_amount) > 1:
                raise Exception(f"calc-invert-sanity-check failed. is_reverse={is_reverse}. "
                                f"send_amount={send_amount} -> recv_amount={recv_amount} -> inverted_send_amount={inverted_send_amount}")
        # second, add on-chain claim tx fee
        if is_reverse and recv_amount is not None:
            recv_amount -= self.get_claim_fee()
        return recv_amount

    def get_send_amount(self, recv_amount: Optional[int], *, is_reverse: bool) -> Optional[int]:
        # first, add on-chain claim tx fee
        if is_reverse and recv_amount is not None:
            recv_amount += self.get_claim_fee()
        # second, add percentage fee
        send_amount = self._get_send_amount(recv_amount, is_reverse=is_reverse)
        # sanity check calculation can be inverted
        if send_amount is not None:
            inverted_recv_amount = self._get_recv_amount(send_amount, is_reverse=is_reverse)
            if recv_amount != inverted_recv_amount:
                raise Exception(f"calc-invert-sanity-check failed. is_reverse={is_reverse}. "
                                f"recv_amount={recv_amount} -> send_amount={send_amount} -> inverted_recv_amount={inverted_recv_amount}")
        return send_amount

    def get_swaps_by_funding_tx(self, tx: Transaction) -> Iterable[SwapData]:
        swaps = []
        for txout_idx, _txo in enumerate(tx.outputs()):
            prevout = TxOutpoint(txid=bytes.fromhex(tx.txid()), out_idx=txout_idx)
            if swap := self._swaps_by_funding_outpoint.get(prevout):
                swaps.append(swap)
        return swaps

    def get_swap_by_claim_tx(self, tx: Transaction) -> Optional[SwapData]:
        # note: we don't batch claim txs atm (batch_rbf cannot combine them
        #       as the inputs do not belong to the wallet)
        if not (len(tx.inputs()) == 1 and len(tx.outputs()) == 1):
            return None
        txin = tx.inputs()[0]
        return self.get_swap_by_claim_txin(txin)

    def get_swap_by_claim_txin(self, txin: TxInput) -> Optional[SwapData]:
        return self._swaps_by_funding_outpoint.get(txin.prevout)

    def is_lockup_address_for_a_swap(self, addr: str) -> bool:
        return bool(self._swaps_by_lockup_address.get(addr))

    def add_txin_info(self, txin: PartialTxInput) -> None:
        """Add some info to a claim txin.
        note: even without signing, this is useful for tx size estimation.
        """
        swap = self.get_swap_by_claim_txin(txin)
        if not swap:
            return
        preimage = swap.preimage if swap.is_reverse else 0
        witness_script = swap.redeem_script
        txin.script_sig = b''
        txin.witness_script = witness_script
        sig_dummy = b'\x00' * 71  # DER-encoded ECDSA sig, with low S and low R
        witness = [sig_dummy, preimage, witness_script]
        txin.witness_sizehint = len(construct_witness(witness))
        txin.nsequence = 0xffffffff - 2

    @classmethod
    def sign_tx(cls, tx: PartialTransaction, swap: SwapData) -> None:
        preimage = swap.preimage if swap.is_reverse else 0
        witness_script = swap.redeem_script
        txin = tx.inputs()[0]
        assert len(tx.inputs()) == 1, f"expected 1 input for swap claim tx. found {len(tx.inputs())}"
        assert txin.prevout.txid.hex() == swap.funding_txid
        txin.script_sig = b''
        txin.witness_script = witness_script
        sig = tx.sign_txin(0, swap.privkey)
        witness = [sig, preimage, witness_script]
        txin.witness = construct_witness(witness)

    @classmethod
    def _create_and_sign_claim_tx(
        cls,
        *,
        txin: PartialTxInput,
        swap: SwapData,
        config: 'SimpleConfig',
    ) -> PartialTransaction:
        # FIXME the mining fee should depend on swap.is_reverse.
        #       the txs are not the same size...
        amount_sat = txin.value_sats() - cls._get_fee(size=CLAIM_FEE_SIZE, config=config)
        if amount_sat < dust_threshold():
            raise BelowDustLimit()
        if swap.is_reverse:  # successful reverse swap
            locktime = 0
            # preimage will be set in sign_tx
        else:  # timing out forward swap
            locktime = swap.locktime
        tx = create_claim_tx(
            txin=txin,
            witness_script=swap.redeem_script,
            address=swap.receive_address,
            amount_sat=amount_sat,
            locktime=locktime,
        )
        cls.sign_tx(tx, swap)
        return tx

    def max_amount_forward_swap(self) -> Optional[int]:
        """ returns None if we cannot swap """
        max_swap_amt_ln = self.get_max_amount()
        if max_swap_amt_ln is None:
            return None
        max_recv_amt_ln = int(self.lnworker.num_sats_can_receive())
        max_amt_ln = int(min(max_swap_amt_ln, max_recv_amt_ln))
        max_amt_oc = self.get_send_amount(max_amt_ln, is_reverse=False) or 0
        min_amt_oc = self.get_send_amount(self.get_min_amount(), is_reverse=False) or 0
        return max_amt_oc if max_amt_oc >= min_amt_oc else None

    def server_create_normal_swap(self, request):
        # normal for client, reverse for server
        #request = await r.json()
        lightning_amount_sat = request['invoiceAmount']
        their_pubkey = bytes.fromhex(request['refundPublicKey'])
        assert len(their_pubkey) == 33
        swap = self.create_reverse_swap(
            lightning_amount_sat=lightning_amount_sat,
            their_pubkey=their_pubkey,
        )
        response = {
            "id": swap.payment_hash.hex(),
            'preimageHash': swap.payment_hash.hex(),
            "acceptZeroConf": False,
            "expectedAmount": swap.onchain_amount,
            "timeoutBlockHeight": swap.locktime,
            "address": swap.lockup_address,
            "redeemScript": swap.redeem_script.hex(),
        }
        return response

    def server_create_swap(self, request):
        # reverse for client, forward for server
        # requesting a normal swap (old protocol) will raise an exception
        #request = await r.json()
        req_type = request['type']
        assert request['pairId'] == 'BTC/BTC'
        if req_type == 'reversesubmarine':
            lightning_amount_sat=request['invoiceAmount']
            payment_hash=bytes.fromhex(request['preimageHash'])
            their_pubkey=bytes.fromhex(request['claimPublicKey'])
            assert len(payment_hash) == 32
            assert len(their_pubkey) == 33
            swap, invoice, prepay_invoice = self.create_normal_swap(
                lightning_amount_sat=lightning_amount_sat,
                payment_hash=payment_hash,
                their_pubkey=their_pubkey
            )
            response = {
                'id': payment_hash.hex(),
                'invoice': invoice,
                'minerFeeInvoice': prepay_invoice,
                'lockupAddress': swap.lockup_address,
                'redeemScript': swap.redeem_script.hex(),
                'timeoutBlockHeight': swap.locktime,
                "onchainAmount": swap.onchain_amount,
            }
        elif req_type == 'submarine':
            raise Exception('Deprecated API. Please upgrade your version of Electrum')
        else:
            raise Exception('unsupported request type:' + req_type)
        return response

    def get_groups_for_onchain_history(self):
        current_height = self.wallet.adb.get_local_height()
        d = {}
        # add info about submarine swaps
        settled_payments = self.lnworker.get_payments(status='settled')
        for payment_hash_hex, swap in self.swaps.items():
            txid = swap.spending_txid if swap.is_reverse else swap.funding_txid
            if txid is None:
                continue
            payment_hash = bytes.fromhex(payment_hash_hex)
            if payment_hash in settled_payments:
                plist = settled_payments[payment_hash]
                info = self.lnworker.get_payment_info(payment_hash)
                direction, amount_msat, fee_msat, timestamp = self.lnworker.get_payment_value(info, plist)
            else:
                amount_msat = 0

            if swap.is_reverse:
                group_label = 'Reverse swap' + ' ' + self.config.format_amount_and_units(swap.lightning_amount)
            else:
                group_label = 'Forward swap' + ' ' + self.config.format_amount_and_units(swap.onchain_amount)

            label = _('Claim transaction') if swap.is_reverse else _('Funding transaction')
            delta = current_height - swap.locktime
            if self.wallet.adb.is_mine(swap.lockup_address):
                tx_height = self.wallet.adb.get_tx_height(swap.funding_txid)
                if swap.is_reverse and tx_height.height <= 0:
                    label += ' (%s)' % _('waiting for funding tx confirmation')
                if not swap.is_reverse and not swap.is_redeemed and swap.spending_txid is None and delta < 0:
                    label += f' (refundable in {-delta} blocks)' # fixme: only if unspent
            d[txid] = {
                'group_id': txid,
                'amount_msat': 0, # must be zero for onchain tx
                'type': 'swap',
                'label': label,
                'group_label': group_label,
            }
            if not swap.is_reverse:
                # if the spending_tx is in the wallet, this will add it
                # to the group (see wallet.get_full_history)
                d[swap.spending_txid] = {
                    'group_id': txid,
                    'amount_msat': 0, # must be zero for onchain tx
                    'type': 'swap',
                    'label': _('Refund transaction'),
                }
        return d

    def get_group_id_for_payment_hash(self, payment_hash):
        # add group_id to swap transactions
        swap = self.get_swap(payment_hash)
        if swap:
            if swap.is_reverse:
                return swap.spending_txid
            else:
                return swap.funding_txid



class HttpTransport(Logger):

    def __init__(self, config, sm):
        Logger.__init__(self)
        self.sm = sm
        self.network = sm.network
        self.api_url = config.SWAPSERVER_URL
        self.config = config
        self.is_connected = asyncio.Event()
        self.is_connected.set()

    def __enter__(self):
        asyncio.run_coroutine_threadsafe(self.get_pairs(), self.network.asyncio_loop)
        return self

    def __exit__(self, ex_type, ex, tb):
        pass

    async def send_request_to_server(self, method, request_data):
        response = await self.network.async_send_http_on_proxy(
            'post' if request_data else 'get',
            self.api_url + '/' + method,
            json=request_data,
            timeout=30)
        return json.loads(response)

    async def get_pairs(self) -> None:
        """Might raise SwapServerError."""
        try:
            response = await self.send_request_to_server('getpairs', None)
        except aiohttp.ClientError as e:
            self.logger.error(f"Swap server errored: {e!r}")
            raise SwapServerError() from e
        assert response.get('htlcFirst') is True
        fees = response['pairs']['BTC/BTC']['fees']
        limits = response['pairs']['BTC/BTC']['limits']
        pairs = SwapFees(
            percentage = fees['percentage'],
            normal_fee = fees['minerFees']['baseAsset']['normal'],
            lockup_fee = fees['minerFees']['baseAsset']['reverse']['lockup'],
            claim_fee = fees['minerFees']['baseAsset']['reverse']['claim'],
            min_amount = limits['minimal'],
            max_amount = limits['maximal'],
        )
        self.sm.update_pairs(pairs)



class NostrTransport(Logger):
    # uses nostr:
    #  - to advertise servers
    #  - for client-server RPCs (using DMs)
    #     (todo: we should use onion messages for that)

    NOSTR_DM = 4
    NOSTR_SWAP_OFFER = 10943
    NOSTR_EVENT_TIMEOUT = 60*60*24
    NOSTR_EVENT_VERSION = 1

    def __init__(self, config, sm, keypair):
        Logger.__init__(self)
        self.config = config
        self.network = sm.network
        self.sm = sm
        self.offers = {}
        self.private_key = keypair.privkey
        self.nostr_private_key = to_nip19('nsec', keypair.privkey.hex())
        self.nostr_pubkey = keypair.pubkey.hex()[2:]
        self.dm_replies = defaultdict(asyncio.Future)  # type: Dict[bytes, asyncio.Future]
        self.relay_manager = aionostr.Manager(self.relays, private_key=self.nostr_private_key)
        self.taskgroup = OldTaskGroup()
        self.is_connected = asyncio.Event()
        self.server_relays = None

    def __enter__(self):
        asyncio.run_coroutine_threadsafe(self.main_loop(), self.network.asyncio_loop)
        return self

    def __exit__(self, ex_type, ex, tb):
        fut = asyncio.run_coroutine_threadsafe(self.stop(), self.network.asyncio_loop)
        fut.result(timeout=5)

    @log_exceptions
    async def main_loop(self):
        self.logger.info(f'starting nostr transport with pubkey: {self.nostr_pubkey}')
        self.logger.info(f'nostr relays: {self.relays}')
        await self.relay_manager.connect()
        connected_relays = self.relay_manager.relays
        self.logger.info(f'connected relays: {[relay.url for relay in connected_relays]}')
        if connected_relays:
            self.is_connected.set()
        if self.sm.is_server:
            tasks = [
                self.check_direct_messages(),
            ]
        else:
            tasks = [
                self.check_direct_messages(),
                self.receive_offers(),
                self.get_pairs(),
            ]
        try:
            async with self.taskgroup as group:
                for task in tasks:
                    await group.spawn(task)
        except Exception as e:
            self.logger.exception("taskgroup died.")
        finally:
            self.logger.info("taskgroup stopped.")

    @log_exceptions
    async def stop(self):
        self.logger.info("shutting down nostr transport")
        self.sm.is_initialized.clear()
        await self.taskgroup.cancel_remaining()
        await self.relay_manager.close()

    @property
    def relays(self):
        return self.network.config.NOSTR_RELAYS.split(',')

    def get_offer(self, pubkey):
        offer = self.offers.get(pubkey)
        return self._parse_offer(offer)

    def _parse_offer(self, offer):
        return SwapFees(
            percentage = offer['percentage_fee'],
            normal_fee = offer['normal_mining_fee'],
            lockup_fee = offer['reverse_mining_fee'],
            claim_fee = offer['claim_mining_fee'],
            min_amount = offer['min_amount'],
            max_amount = offer['max_amount'],
        )

    @ignore_exceptions
    @log_exceptions
    async def publish_offer(self, sm):
        assert self.sm.is_server
        offer = {
            "type": "electrum-swap",
            "version": self.NOSTR_EVENT_VERSION,
            'network': constants.net.NET_NAME,
            'percentage_fee': sm.percentage,
            'normal_mining_fee': sm.normal_fee,
            'reverse_mining_fee': sm.lockup_fee,
            'claim_mining_fee': sm.claim_fee,
            'min_amount': sm._min_amount,
            'max_amount': sm._max_amount,
            'relays': sm.config.NOSTR_RELAYS,
        }
        self.logger.info(f'publishing swap offer..')
        event_id = await aionostr._add_event(
            self.relay_manager,
            kind=self.NOSTR_SWAP_OFFER,
            content=json.dumps(offer),
            private_key=self.nostr_private_key)

    async def send_direct_message(self, pubkey: str, relays, content: str) -> str:
        event_id = await aionostr._add_event(
            self.relay_manager,
            kind=self.NOSTR_DM,
            content=content,
            private_key=self.nostr_private_key,
            direct_message=pubkey)
        return event_id

    @log_exceptions
    async def send_request_to_server(self, method: str, request: dict) -> dict:
        request['method'] = method
        request['relays'] = self.config.NOSTR_RELAYS
        server_pubkey = self.config.SWAPSERVER_NPUB
        event_id = await self.send_direct_message(server_pubkey, self.server_relays, json.dumps(request))
        response = await self.dm_replies[event_id]
        return response

    async def receive_offers(self):
        await self.is_connected.wait()
        query = {"kinds": [self.NOSTR_SWAP_OFFER], "limit":10}
        async for event in self.relay_manager.get_events(query, single_event=False, only_stored=False):
            try:
                content = json.loads(event.content)
            except Exception as e:
                continue
            if content.get('version') != self.NOSTR_EVENT_VERSION:
                continue
            if content.get('network') != constants.net.NET_NAME:
                continue
            # check if this is the most recent event for this pubkey
            pubkey = event.pubkey
            ts = self.offers.get(pubkey, {}).get('timestamp', 0)
            if event.created_at <= ts:
                #print('skipping old event', pubkey[0:10], event.id)
                continue
            content['pubkey'] = pubkey
            content['timestamp'] = event.created_at
            self.offers[pubkey] = content
            # mirror event to other relays
            #await man.add_event(event, check_response=False)

    async def get_pairs(self):
        if self.config.SWAPSERVER_NPUB is None:
            return
        query = {"kinds": [self.NOSTR_SWAP_OFFER], "authors": [self.config.SWAPSERVER_NPUB], "limit":1}
        async for event in self.relay_manager.get_events(query, single_event=True, only_stored=False):
            try:
                content = json.loads(event.content)
            except Exception as e:
                continue
            if content.get('version') != self.NOSTR_EVENT_VERSION:
                continue
            if content.get('network') != constants.net.NET_NAME:
                continue
            # check if this is the most recent event for this pubkey
            pubkey = event.pubkey
            content['pubkey'] = pubkey
            content['timestamp'] = event.created_at
            self.logger.info(f'received offer from {age(event.created_at)}')
            pairs = self._parse_offer(content)
            self.sm.update_pairs(pairs)
            self.server_relays = content['relays'].split(',')

    @log_exceptions
    async def check_direct_messages(self):
        privkey = aionostr.key.PrivateKey(self.private_key)
        query = {"kinds": [self.NOSTR_DM], "limit":0, "#p": [self.nostr_pubkey]}
        async for event in self.relay_manager.get_events(query, single_event=False, only_stored=False):
            try:
                content = privkey.decrypt_message(event.content, event.pubkey)
                content = json.loads(content)
            except Exception:
                continue
            content['event_id'] = event.id
            content['event_pubkey'] = event.pubkey
            if 'reply_to' in content:
                self.dm_replies[content['reply_to']].set_result(content)
            elif self.sm.is_server and 'method' in content:
                await self.handle_request(content)
            else:
                self.logger.info(f'unknown message {content}')

    @log_exceptions
    async def handle_request(self, request):
        assert self.sm.is_server
        # todo: remember event_id of already processed requests
        method = request.pop('method')
        event_id = request.pop('event_id')
        event_pubkey = request.pop('event_pubkey')
        print(f'handle_request: id={event_id} {method} {request}')
        relays = request.pop('relays').split(',')
        if method == 'addswapinvoice':
            r = self.sm.server_add_swap_invoice(request)
        elif method == 'createswap':
            r = self.sm.server_create_swap(request)
        elif method == 'createnormalswap':
            r = self.sm.server_create_normal_swap(request)
        else:
            raise Exception(method)
        r['reply_to'] = event_id
        self.logger.info(f'sending response id={event_id}')
        await self.send_direct_message(event_pubkey, relays, json.dumps(r))
