import json
import logging
import socket
import warnings
from decimal import Decimal
from math import floor, log10

__version__ = "0.7.3"


class RpcError(ValueError):
    def __init__(self, method, payload, error):
        super(ValueError, self).__init__("RPC call failed: method: {}, payload: {}, error: {}"
                                         .format(method, payload, error))

        self.method = method
        self.payload = payload
        self.error = error


class Millisatoshi:
    """
    A subtype to represent thousandths of a satoshi.

    Many JSON API fields are expressed in millisatoshis: these automatically get
    turned into Millisatoshi types.  Converts to and from int.
    """
    def __init__(self, v):
        """
        Takes either a string ending in 'msat', 'sat', 'btc' or an integer.
        """
        if isinstance(v, str):
            if v.endswith("msat"):
                self.millisatoshis = int(v[0:-4])
            elif v.endswith("sat"):
                self.millisatoshis = Decimal(v[0:-3]) * 1000
            elif v.endswith("btc"):
                self.millisatoshis = Decimal(v[0:-3]) * 1000 * 10**8
            else:
                raise TypeError("Millisatoshi must be string with msat/sat/btc suffix or int")
            if self.millisatoshis != int(self.millisatoshis):
                raise ValueError("Millisatoshi must be a whole number")
            self.millisatoshis = int(self.millisatoshis)
        elif isinstance(v, Millisatoshi):
            self.millisatoshis = v.millisatoshis
        elif int(v) == v:
            self.millisatoshis = int(v)
        else:
            raise TypeError("Millisatoshi must be string with msat/sat/btc suffix or int")

        if self.millisatoshis < 0:
            raise ValueError("Millisatoshi must be >= 0")

    def __repr__(self):
        """
        Appends the 'msat' as expected for this type.
        """
        return str(self.millisatoshis) + "msat"

    def to_satoshi(self):
        """
        Return a Decimal representing the number of satoshis
        """
        return Decimal(self.millisatoshis) / 1000

    def to_btc(self):
        """
        Return a Decimal representing the number of bitcoin
        """
        return Decimal(self.millisatoshis) / 1000 / 10**8

    def to_satoshi_str(self):
        """
        Return a string of form 1234sat or 1234.567sat.
        """
        if self.millisatoshis % 1000:
            return '{:.3f}sat'.format(self.to_satoshi())
        else:
            return '{:.0f}sat'.format(self.to_satoshi())

    def to_btc_str(self):
        """
        Return a string of form 12.34567890btc or 12.34567890123btc.
        """
        if self.millisatoshis % 1000:
            return '{:.11f}btc'.format(self.to_btc())
        else:
            return '{:.8f}btc'.format(self.to_btc())

    def to_approx_str(self, digits: int = 3):
        """Returns the shortmost string using common units representation.

        Rounds to significant `digits`. Default: 3
        """
        round_to_n = lambda x, n: round(x, -int(floor(log10(x))) + (n - 1))
        result = None

        # we try to increase digits to check if we did loose out on precision
        # without gaining a shorter string, since this is a rarely used UI
        # function, performance is not an issue. Adds at least one iteration.
        while True:
            # first round everything down to effective digits
            amount_rounded = round_to_n(self.millisatoshis, digits)
            # try different units and take shortest resulting normalized string
            amounts_str = [
                "%gbtc" % (amount_rounded / 1000 / 10**8),
                "%gsat" % (amount_rounded / 1000),
                "%gmsat" % (amount_rounded),
            ]
            test_result = min(amounts_str, key=len)

            # check result and do another run if necessary
            if test_result == result:
                return result
            elif not result or len(test_result) <= len(result):
                digits = digits + 1
                result = test_result
            else:
                return result

    def to_json(self):
        return self.__repr__()

    def __int__(self):
        return self.millisatoshis

    def __lt__(self, other):
        return self.millisatoshis < other.millisatoshis

    def __le__(self, other):
        return self.millisatoshis <= other.millisatoshis

    def __eq__(self, other):
        return self.millisatoshis == other.millisatoshis

    def __gt__(self, other):
        return self.millisatoshis > other.millisatoshis

    def __ge__(self, other):
        return self.millisatoshis >= other.millisatoshis

    def __add__(self, other):
        return Millisatoshi(int(self) + int(other))

    def __sub__(self, other):
        return Millisatoshi(int(self) - int(other))

    def __mul__(self, other):
        return Millisatoshi(int(int(self) * other))

    def __truediv__(self, other):
        return Millisatoshi(int(int(self) / other))

    def __floordiv__(self, other):
        return Millisatoshi(int(self) // other)

    def __mod__(self, other):
        return Millisatoshi(int(self) % other)

    def __radd__(self, other):
        return Millisatoshi(int(self) + int(other))


class UnixDomainSocketRpc(object):
    def __init__(self, socket_path, executor=None, logger=logging, encoder_cls=json.JSONEncoder, decoder=json.JSONDecoder()):
        self.socket_path = socket_path
        self.encoder_cls = encoder_cls
        self.decoder = decoder
        self.executor = executor
        self.logger = logger

        self.next_id = 0

    def _writeobj(self, sock, obj):
        s = json.dumps(obj, cls=self.encoder_cls)
        sock.sendall(bytearray(s, 'UTF-8'))

    def _readobj(self, sock, buff=b''):
        """Read a JSON object, starting with buff; returns object and any buffer left over"""
        while True:
            parts = buff.split(b'\n\n', 1)
            if len(parts) == 1:
                # Didn't read enough.
                b = sock.recv(max(1024, len(buff)))
                buff += b
                if len(b) == 0:
                    return {'error': 'Connection to RPC server lost.'}, buff
            else:
                buff = parts[1]
                obj, _ = self.decoder.raw_decode(parts[0].decode("UTF-8"))
                return obj, buff

    def __getattr__(self, name):
        """Intercept any call that is not explicitly defined and call @call

        We might still want to define the actual methods in the subclasses for
        documentation purposes.
        """
        name = name.replace('_', '-')

        def wrapper(*args, **kwargs):
            if len(args) != 0 and len(kwargs) != 0:
                raise RpcError("Cannot mix positional and non-positional arguments")
            elif len(args) != 0:
                return self.call(name, payload=args)
            else:
                return self.call(name, payload=kwargs)
        return wrapper

    def call(self, method, payload=None):
        self.logger.debug("Calling %s with payload %r", method, payload)

        if payload is None:
            payload = {}
        # Filter out arguments that are None
        if isinstance(payload, dict):
            payload = {k: v for k, v in payload.items() if v is not None}

        # FIXME: we open a new socket for every readobj call...
        sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
        sock.connect(self.socket_path)
        self._writeobj(sock, {
            "method": method,
            "params": payload,
            "id": self.next_id,
        })
        self.next_id += 1
        resp, _ = self._readobj(sock)
        sock.close()

        self.logger.debug("Received response for %s call: %r", method, resp)
        if not isinstance(resp, dict):
            raise ValueError("Malformed response, response is not a dictionary %s." % resp)
        elif "error" in resp:
            raise RpcError(method, payload, resp['error'])
        elif "result" not in resp:
            raise ValueError("Malformed response, \"result\" missing.")
        return resp["result"]


class LightningRpc(UnixDomainSocketRpc):
    """
    RPC client for the `lightningd` daemon.

    This RPC client connects to the `lightningd` daemon through a unix
    domain socket and passes calls through. Since some of the calls
    are blocking, the corresponding python methods include an `async`
    keyword argument. If `async` is set to true then the method
    returns a future immediately, instead of blocking indefinitely.

    This implementation is thread safe in that it locks the socket
    between calls, but it does not (yet) support concurrent calls.
    """

    class LightningJSONEncoder(json.JSONEncoder):
        def default(self, o):
            try:
                return o.to_json()
            except NameError:
                pass
            return json.JSONEncoder.default(self, o)

    class LightningJSONDecoder(json.JSONDecoder):
        def __init__(self, *, object_hook=None, parse_float=None, parse_int=None, parse_constant=None, strict=True, object_pairs_hook=None):
            self.object_hook_next = object_hook
            super().__init__(object_hook=self.millisatoshi_hook, parse_float=parse_float, parse_int=parse_int, parse_constant=parse_constant, strict=strict, object_pairs_hook=object_pairs_hook)

        @staticmethod
        def replace_amounts(obj):
            """
            Recursively replace _msat fields with appropriate values with Millisatoshi.
            """
            if isinstance(obj, dict):
                for k, v in obj.items():
                    if k.endswith('msat'):
                        if isinstance(v, str) and v.endswith('msat'):
                            obj[k] = Millisatoshi(v)
                        # Special case for array of msat values
                        elif isinstance(v, list) and all(isinstance(e, str) and e.endswith('msat') for e in v):
                            obj[k] = [Millisatoshi(e) for e in v]
                    else:
                        obj[k] = LightningRpc.LightningJSONDecoder.replace_amounts(v)
            elif isinstance(obj, list):
                obj = [LightningRpc.LightningJSONDecoder.replace_amounts(e) for e in obj]

            return obj

        def millisatoshi_hook(self, obj):
            obj = LightningRpc.LightningJSONDecoder.replace_amounts(obj)
            if self.object_hook_next:
                obj = self.object_hook_next(obj)
            return obj

    def __init__(self, socket_path, executor=None, logger=logging):
        super().__init__(socket_path, executor, logging, self.LightningJSONEncoder, self.LightningJSONDecoder())

    def autocleaninvoice(self, cycle_seconds=None, expired_by=None):
        """
        Sets up automatic cleaning of expired invoices. {cycle_seconds} sets
        the cleaning frequency in seconds (defaults to 3600) and {expired_by}
        sets the minimum time an invoice should have been expired for to be
        cleaned in seconds (defaults to 86400).
        """
        payload = {
            "cycle_seconds": cycle_seconds,
            "expired_by": expired_by
        }
        return self.call("autocleaninvoice", payload)

    def check(self, command_to_check, **kwargs):
        """
        Checks if a command is valid without running it.
        """
        payload = {"command_to_check": command_to_check}
        payload.update({k: v for k, v in kwargs.items()})
        return self.call("check", payload)

    def _deprecated_close(self, peer_id, force=None, timeout=None):
        warnings.warn("close now takes unilateraltimeout arg: expect removal"
                      " in early 2020",
                      DeprecationWarning)
        payload = {
            "id": peer_id,
            "force": force,
            "timeout": timeout
        }
        return self.call("close", payload)

    def close(self, peer_id, *args, **kwargs):
        """
        Close the channel with peer {id}, forcing a unilateral
        close after {unilateraltimeout} seconds if non-zero, and
        the to-local output will be sent to {destination}.

        Deprecated usage has {force} and {timeout} args.
        """

        if 'force' in kwargs or 'timeout' in kwargs:
            return self._deprecated_close(peer_id, *args, **kwargs)

        # Single arg is ambigious.
        if len(args) >= 1:
            if isinstance(args[0], bool):
                return self._deprecated_close(peer_id, *args, **kwargs)
            if len(args) == 2:
                if args[0] is None and isinstance(args[1], int):
                    return self._deprecated_close(peer_id, *args, **kwargs)

        def _close(peer_id, unilateraltimeout=None, destination=None):
            payload = {
                "id": peer_id,
                "unilateraltimeout": unilateraltimeout,
                "destination": destination
            }
            return self.call("close", payload)

        return _close(peer_id, *args, **kwargs)

    def connect(self, peer_id, host=None, port=None):
        """
        Connect to {peer_id} at {host} and {port}
        """
        payload = {
            "id": peer_id,
            "host": host,
            "port": port
        }
        return self.call("connect", payload)

    def decodepay(self, bolt11, description=None):
        """
        Decode {bolt11}, using {description} if necessary
        """
        payload = {
            "bolt11": bolt11,
            "description": description
        }
        return self.call("decodepay", payload)

    def delexpiredinvoice(self, maxexpirytime=None):
        """
        Delete all invoices that have expired on or before the given {maxexpirytime}
        """
        payload = {
            "maxexpirytime": maxexpirytime
        }
        return self.call("delexpiredinvoice", payload)

    def delinvoice(self, label, status):
        """
        Delete unpaid invoice {label} with {status}
        """
        payload = {
            "label": label,
            "status": status
        }
        return self.call("delinvoice", payload)

    def dev_crash(self):
        """
        Crash lightningd by calling fatal()
        """
        payload = {
            "subcommand": "crash"
        }
        return self.call("dev", payload)

    def dev_fail(self, peer_id):
        """
        Fail with peer {peer_id}
        """
        payload = {
            "id": peer_id
        }
        return self.call("dev-fail", payload)

    def dev_forget_channel(self, peerid, force=False):
        """ Forget the channel with id=peerid
        """
        return self.call(
            "dev-forget-channel",
            payload={"id": peerid, "force": force}
        )

    def dev_memdump(self):
        """
        Show memory objects currently in use
        """
        return self.call("dev-memdump")

    def dev_memleak(self):
        """
        Show unreferenced memory objects
        """
        return self.call("dev-memleak")

    def dev_pay(self, bolt11, msatoshi=None, label=None, riskfactor=None,
                description=None, maxfeepercent=None, retry_for=None,
                maxdelay=None, exemptfee=None, use_shadow=True):
        """
        A developer version of `pay`, with the possibility to deactivate
        shadow routing (used for testing).
        """
        payload = {
            "bolt11": bolt11,
            "msatoshi": msatoshi,
            "label": label,
            "riskfactor": riskfactor,
            "maxfeepercent": maxfeepercent,
            "retry_for": retry_for,
            "maxdelay": maxdelay,
            "exemptfee": exemptfee,
            "use_shadow": use_shadow,
            # Deprecated.
            "description": description,
        }
        return self.call("pay", payload)

    def dev_reenable_commit(self, peer_id):
        """
        Re-enable the commit timer on peer {id}
        """
        payload = {
            "id": peer_id
        }
        return self.call("dev-reenable-commit", payload)

    def dev_rescan_outputs(self):
        """
        Synchronize the state of our funds with bitcoind
        """
        return self.call("dev-rescan-outputs")

    def dev_rhash(self, secret):
        """
        Show SHA256 of {secret}
        """
        payload = {
            "subcommand": "rhash",
            "secret": secret
        }
        return self.call("dev", payload)

    def dev_sign_last_tx(self, peer_id):
        """
        Sign and show the last commitment transaction with peer {id}
        """
        payload = {
            "id": peer_id
        }
        return self.call("dev-sign-last-tx", payload)

    def dev_slowcmd(self, msec=None):
        """
        Torture test for slow commands, optional {msec}
        """
        payload = {
            "subcommand": "slowcmd",
            "msec": msec
        }
        return self.call("dev", payload)

    def disconnect(self, peer_id, force=False):
        """
        Disconnect from peer with {peer_id}, optional {force} even if has active channel
        """
        payload = {
            "id": peer_id,
            "force": force,
        }
        return self.call("disconnect", payload)

    def feerates(self, style, urgent=None, normal=None, slow=None):
        """
        Supply feerate estimates manually.
        """
        payload = {
            "style": style,
            "urgent": urgent,
            "normal": normal,
            "slow": slow
        }
        return self.call("feerates", payload)

    def _deprecated_fundchannel(self, node_id, satoshi, feerate=None, announce=True, minconf=None, utxos=None):
        warnings.warn("fundchannel: the 'satoshi' field is renamed 'amount' : expect removal"
                      " in Mid-2020",
                      DeprecationWarning)

        payload = {
            "id": node_id,
            "satoshi": satoshi,
            "feerate": feerate,
            "announce": announce,
            "minconf": minconf,
            "utxos": utxos
        }
        return self.call("fundchannel", payload)

    def fundchannel(self, node_id, *args, **kwargs):
        """
        Fund channel with {id} using {amount} satoshis with feerate
        of {feerate} (uses default feerate if unset).
        If {announce} is False, don't send channel announcements.
        Only select outputs with {minconf} confirmations.
        If {utxos} is specified (as a list of 'txid:vout' strings),
        fund a channel from these specifics utxos.
        """

        if 'satoshi' in kwargs:
            return self._deprecated_fundchannel(node_id, *args, **kwargs)

        def _fundchannel(node_id, amount, feerate=None, announce=True, minconf=None, utxos=None):
            payload = {
                "id": node_id,
                "amount": amount,
                "feerate": feerate,
                "announce": announce,
                "minconf": minconf,
                "utxos": utxos
            }
            return self.call("fundchannel", payload)

        return _fundchannel(node_id, *args, **kwargs)

    def _deprecated_fundchannel_start(self, node_id, satoshi, feerate=None, announce=True):
        warnings.warn("fundchannel_start: the 'satoshi' field is renamed 'amount' : expect removal"
                      " in Mid-2020",
                      DeprecationWarning)

        payload = {
            "id": node_id,
            "satoshi": satoshi,
            "feerate": feerate,
            "announce": announce,
        }
        return self.call("fundchannel_start", payload)

    def fundchannel_start(self, node_id, *args, **kwargs):
        """
        Start channel funding with {id} for {amount} satoshis
        with feerate of {feerate} (uses default feerate if unset).
        If {announce} is False, don't send channel announcements.
        Returns a Bech32 {funding_address} for an external wallet
        to create a funding transaction for. Requires a call to
        'fundchannel_complete' to complete channel establishment
        with peer.
        """

        if 'satoshi' in kwargs:
            return self._deprecated_fundchannel_start(node_id, *args, **kwargs)

        def _fundchannel_start(node_id, amount, feerate=None, announce=True, close_to=None):
            payload = {
                "id": node_id,
                "amount": amount,
                "feerate": feerate,
                "announce": announce,
                "close_to": close_to,
            }
            return self.call("fundchannel_start", payload)

        return _fundchannel_start(node_id, *args, **kwargs)

    def fundchannel_cancel(self, node_id):
        """
        Cancel a 'started' fundchannel with node {id}.
        """
        payload = {
            "id": node_id,
        }
        return self.call("fundchannel_cancel", payload)

    def fundchannel_complete(self, node_id, funding_txid, funding_txout):
        """
        Complete channel establishment with {id}, using {funding_txid} at {funding_txout}
        """
        payload = {
            "id": node_id,
            "txid": funding_txid,
            "txout": funding_txout,
        }
        return self.call("fundchannel_complete", payload)

    def getinfo(self):
        """
        Show information about this node
        """
        return self.call("getinfo")

    def getlog(self, level=None):
        """
        Show logs, with optional log {level} (info|unusual|debug|io)
        """
        payload = {
            "level": level
        }
        return self.call("getlog", payload)

    def getpeer(self, peer_id, level=None):
        """
        Show peer with {peer_id}, if {level} is set, include {log}s
        """
        payload = {
            "id": peer_id,
            "level": level
        }
        res = self.call("listpeers", payload)
        return res.get("peers") and res["peers"][0] or None

    def getroute(self, node_id, msatoshi, riskfactor, cltv=9, fromid=None, fuzzpercent=None, exclude=[], maxhops=20):
        """
        Show route to {id} for {msatoshi}, using {riskfactor} and optional
        {cltv} (default 9). If specified search from {fromid} otherwise use
        this node as source. Randomize the route with up to {fuzzpercent}
        (0.0 -> 100.0, default 5.0). {exclude} is an optional array of
        scid/direction or node-id to exclude. Limit the number of hops in the
        route to {maxhops}.
        """
        payload = {
            "id": node_id,
            "msatoshi": msatoshi,
            "riskfactor": riskfactor,
            "cltv": cltv,
            "fromid": fromid,
            "fuzzpercent": fuzzpercent,
            "exclude": exclude,
            "maxhops": maxhops
        }
        return self.call("getroute", payload)

    def help(self, command=None):
        """
        Show available commands, or just {command} if supplied.
        """
        payload = {
            "command": command,
        }
        return self.call("help", payload)

    def invoice(self, msatoshi, label, description, expiry=None, fallbacks=None, preimage=None, exposeprivatechannels=None):
        """
        Create an invoice for {msatoshi} with {label} and {description} with
        optional {expiry} seconds (default 1 week)
        """
        payload = {
            "msatoshi": msatoshi,
            "label": label,
            "description": description,
            "expiry": expiry,
            "fallbacks": fallbacks,
            "preimage": preimage,
            "exposeprivatechannels": exposeprivatechannels
        }
        return self.call("invoice", payload)

    def listchannels(self, short_channel_id=None, source=None):
        """
        Show all known channels, accept optional {short_channel_id} or {source}
        """
        payload = {
            "short_channel_id": short_channel_id,
            "source": source
        }
        return self.call("listchannels", payload)

    def listconfigs(self, config=None):
        """List this node's config
        """
        payload = {
            "config": config
        }
        return self.call("listconfigs", payload)

    def listforwards(self):
        """List all forwarded payments and their information
        """
        return self.call("listforwards")

    def listfunds(self):
        """
        Show funds available for opening channels
        """
        return self.call("listfunds")

    def listtransactions(self):
        """
        Show wallet history
        """
        return self.call("listtransactions")

    def listinvoices(self, label=None):
        """
        Show invoice {label} (or all, if no {label))
        """
        payload = {
            "label": label
        }
        return self.call("listinvoices", payload)

    def listnodes(self, node_id=None):
        """
        Show all nodes in our local network view, filter on node {id}
        if provided
        """
        payload = {
            "id": node_id
        }
        return self.call("listnodes", payload)

    def listpayments(self, bolt11=None, payment_hash=None):
        """
        Show outgoing payments, regarding {bolt11} or {payment_hash} if set
        Can only specify one of {bolt11} or {payment_hash}
        """
        assert not (bolt11 and payment_hash)
        payload = {
            "bolt11": bolt11,
            "payment_hash": payment_hash
        }
        return self.call("listpayments", payload)

    def listpeers(self, peerid=None, level=None):
        """
        Show current peers, if {level} is set, include {log}s"
        """
        payload = {
            "id": peerid,
            "level": level,
        }
        return self.call("listpeers", payload)

    def listsendpays(self, bolt11=None, payment_hash=None):
        """Show all sendpays results, or only for `bolt11` or `payment_hash`"""
        payload = {
            "bolt11": bolt11,
            "payment_hash": payment_hash
        }
        return self.call("listsendpays", payload)

    def newaddr(self, addresstype=None):
        """Get a new address of type {addresstype} of the internal wallet.
        """
        return self.call("newaddr", {"addresstype": addresstype})

    def pay(self, bolt11, msatoshi=None, label=None, riskfactor=None,
            description=None, maxfeepercent=None, retry_for=None,
            maxdelay=None, exemptfee=None):
        """
        Send payment specified by {bolt11} with {msatoshi}
        (ignored if {bolt11} has an amount), optional {label}
        and {riskfactor} (default 1.0)
        """
        payload = {
            "bolt11": bolt11,
            "msatoshi": msatoshi,
            "label": label,
            "riskfactor": riskfactor,
            "maxfeepercent": maxfeepercent,
            "retry_for": retry_for,
            "maxdelay": maxdelay,
            "exemptfee": exemptfee,
            # Deprecated.
            "description": description,
        }
        return self.call("pay", payload)

    def paystatus(self, bolt11=None):
        """Detail status of attempts to pay {bolt11} or any"""
        payload = {
            "bolt11": bolt11
        }
        return self.call("paystatus", payload)

    def ping(self, peer_id, length=128, pongbytes=128):
        """
        Send {peer_id} a ping of length {len} asking for {pongbytes}"
        """
        payload = {
            "id": peer_id,
            "len": length,
            "pongbytes": pongbytes
        }
        return self.call("ping", payload)

    def plugin_start(self, plugin):
        """
        Adds a plugin to lightningd.
        """
        payload = {
            "subcommand": "start",
            "plugin": plugin
        }
        return self.call("plugin", payload)

    def plugin_startdir(self, directory):
        """
        Adds all plugins from a directory to lightningd.
        """
        payload = {
            "subcommand": "startdir",
            "directory": directory
        }
        return self.call("plugin", payload)

    def plugin_stop(self, plugin):
        """
        Stops a lightningd plugin, will fail if plugin is not dynamic.
        """
        payload = {
            "subcommand": "stop",
            "plugin": plugin
        }
        return self.call("plugin", payload)

    def plugin_list(self):
        """
        Lists all plugins lightningd knows about.
        """
        payload = {
            "subcommand": "list"
        }
        return self.call("plugin", payload)

    def plugin_rescan(self):
        payload = {
            "subcommand": "rescan"
        }
        return self.call("plugin", payload)

    def _deprecated_sendpay(self, route, payment_hash, description, msatoshi=None):
        warnings.warn("sendpay: the 'description' field is renamed 'label' : expect removal"
                      " in early-2020",
                      DeprecationWarning)
        payload = {
            "route": route,
            "payment_hash": payment_hash,
            "label": description,
            "msatoshi": msatoshi,
        }
        return self.call("sendpay", payload)

    def sendpay(self, route, payment_hash, *args, **kwargs):
        """
        Send along {route} in return for preimage of {payment_hash}
        """

        if 'description' in kwargs:
            return self._deprecated_sendpay(route, payment_hash, *args, **kwargs)

        def _sendpay(route, payment_hash, label=None, msatoshi=None):
            payload = {
                "route": route,
                "payment_hash": payment_hash,
                "label": label,
                "msatoshi": msatoshi,
            }
            return self.call("sendpay", payload)

        return _sendpay(route, payment_hash, *args, **kwargs)

    def setchannelfee(self, id, base=None, ppm=None):
        """
        Set routing fees for a channel/peer {id} (or 'all'). {base} is a value in millisatoshi
        that is added as base fee to any routed payment. {ppm} is a value added proportionally
        per-millionths to any routed payment volume in satoshi.
        """
        payload = {
            "id": id,
            "base": base,
            "ppm": ppm
        }
        return self.call("setchannelfee", payload)

    def stop(self):
        """
        Shut down the lightningd process
        """
        return self.call("stop")

    def waitanyinvoice(self, lastpay_index=None):
        """
        Wait for the next invoice to be paid, after {lastpay_index}
        (if supplied)
        """
        payload = {
            "lastpay_index": lastpay_index
        }
        return self.call("waitanyinvoice", payload)

    def waitinvoice(self, label):
        """
        Wait for an incoming payment matching the invoice with {label}
        """
        payload = {
            "label": label
        }
        return self.call("waitinvoice", payload)

    def waitsendpay(self, payment_hash, timeout=None):
        """
        Wait for payment for preimage of {payment_hash} to complete
        """
        payload = {
            "payment_hash": payment_hash,
            "timeout": timeout
        }
        return self.call("waitsendpay", payload)

    def withdraw(self, destination, satoshi, feerate=None, minconf=None, utxos=None):
        """
        Send to {destination} address {satoshi} (or "all")
        amount via Bitcoin transaction. Only select outputs
        with {minconf} confirmations
        """
        payload = {
            "destination": destination,
            "satoshi": satoshi,
            "feerate": feerate,
            "minconf": minconf,
            "utxos": utxos,
        }

        return self.call("withdraw", payload)

    def _deprecated_txprepare(self, destination, satoshi, feerate=None, minconf=None):
        warnings.warn("txprepare now takes output arg: expect removal"
                      " in Mid-2020",
                      DeprecationWarning)
        payload = {
            "destination": destination,
            "satoshi": satoshi,
            "feerate": feerate,
            "minconf": minconf,
        }
        return self.call("txprepare", payload)

    def txprepare(self, *args, **kwargs):
        """
        Prepare a bitcoin transaction which sends to [outputs].
        The format of output is like [{address1: amount1},
        {address2: amount2}], or [{address: "all"}]).
        Only select outputs with {minconf} confirmations.

        Outputs will be reserved until you call txdiscard or txsend, or
        lightningd restarts.
        """
        if 'destination' in kwargs or 'satoshi' in kwargs:
            return self._deprecated_txprepare(*args, **kwargs)

        if not isinstance(args[0], list):
            return self._deprecated_txprepare(*args, **kwargs)

        def _txprepare(outputs, feerate=None, minconf=None, utxos=None):
            payload = {
                "outputs": outputs,
                "feerate": feerate,
                "minconf": minconf,
                "utxos": utxos,
            }
            return self.call("txprepare", payload)

        return _txprepare(*args, **kwargs)

    def txdiscard(self, txid):
        """
        Cancel a bitcoin transaction returned from txprepare.  The outputs
        it was spending are released for other use.
        """
        payload = {
            "txid": txid
        }
        return self.call("txdiscard", payload)

    def txsend(self, txid):
        """
        Sign and broadcast a bitcoin transaction returned from txprepare.
        """
        payload = {
            "txid": txid
        }
        return self.call("txsend", payload)

    def signmessage(self, message):
        """
        Sign a message with this node's secret key.
        """
        payload = {
            "message": message
        }
        return self.call("signmessage", payload)

    def checkmessage(self, message, zbase, pubkey=None):
        """
        Check if a message was signed (with a specific key).
        Use returned field ['verified'] to get result.
        """
        payload = {
            "message": message,
            "zbase": zbase,
            "pubkey": pubkey,
        }
        return self.call("checkmessage", payload)
