Example #1
0
def execute(payload: dict):
    my_id = plugin.rpc.getinfo().get('id')
    peer_id = peer_from_scid(plugin, payload, payload['scid'], my_id)
    get_channel(plugin, payload, peer_id, payload['scid']) # ensures or raises error
    test_or_set_chunks(plugin, payload, my_id)
    plugin.log("%s  %s  %d%%  %d chunks" % (payload['command'], payload['scid'], payload['percentage'], payload['chunks']))

    # iterate of chunks, default just one
    for chunk in range(payload['chunks']):
        # we discover remaining capacities for each chunk,
        # as fees from previous chunks affect reserves
        spendable, receivable = spendable_from_scid(plugin, payload)
        total = spendable + receivable
        amount = total * 0.01 * payload['percentage'] / payload['chunks']

        # if capacity exceeds, limit amount to full or empty channel
        if payload['command'] == "drain" and amount > spendable:
            amount = spendable
        if payload['command'] == "fill" and amount > receivable:
            amount = receivable

        result = False
        try:
            # we need to try with different HTLC_FEE values
            # until we dont get capacity error on first hop
            htlc_fee = HTLC_FEE_NUL
            htlc_stp = HTLC_FEE_STP

            while htlc_fee < HTLC_FEE_MAX and result is False:
                # When getting close to 100% we need to account for HTLC commitment fee
                if payload['command'] == 'drain' and spendable - amount <= htlc_fee:
                    if amount < htlc_fee:
                        raise RpcError(payload['command'], payload, {'message': 'channel too low to cover fees'})
                    amount -= htlc_fee
                plugin.log("Trying... chunk:%s/%s  spendable:%s  receivable:%s  htlc_fee:%s =>  amount:%s" % (chunk+1, payload['chunks'], spendable, receivable, htlc_fee, amount))

                try:
                    result = try_for_htlc_fee(plugin, payload, my_id, peer_id, amount, chunk, spendable)
                except Exception as err:
                    if "htlc_fee unknown" in str(err):
                        if htlc_fee == HTLC_FEE_NUL:
                            htlc_fee = HTLC_FEE_MIN - HTLC_FEE_STP
                        htlc_fee += htlc_stp
                        htlc_stp *= 1.1  # exponential increase steps
                        plugin.log("Retrying with additional HTLC onchain fees: %s" % htlc_fee)
                        continue
                    if "htlc_fee is" in str(err):
                        htlc_fee = Millisatoshi(str(err)[12:])
                        plugin.log("Retrying with exact HTLC onchain fees: %s" % htlc_fee)
                        continue
                    raise err

            # If result is still false, we tried allowed htlc_fee range unsuccessfully
            if result is False:
                raise RpcError(payload['command'], payload, {'message': 'Cannot determine required htlc commitment fees.'})

        except Exception as e:
            return cleanup(plugin, payload, e)

    return cleanup(plugin, payload)
Example #2
0
def get_channel(plugin, payload, peer_id, scid):
    peer = plugin.rpc.listpeers(peer_id).get('peers')[0]
    channel = next(c for c in peer['channels'] if 'short_channel_id' in c and c['short_channel_id'] == scid)
    if channel['state'] != "CHANNELD_NORMAL":
        raise RpcError(payload['command'], payload, {'message': 'Channel %s not in state CHANNELD_NORMAL, but: %s' % (scid, channel['state']) })
    if not peer['connected']:
        raise RpcError(payload['command'], payload, {'message': 'Channel %s peer is not connected.' % scid})
    return channel
Example #3
0
def calc_optimal_amount(out_ours, out_total, in_ours, in_total, payload):
    out_ours, out_total = int(out_ours), int(out_total)
    in_ours, in_total = int(in_ours), int(in_total)

    in_theirs = in_total - in_ours
    vo = int(out_ours - (out_total / 2))
    vi = int((in_total / 2) - in_ours)

    # cases where one option can be eliminated because it exceeds other capacity
    if vo > in_theirs and vi > 0 and vi < out_ours:
        return Millisatoshi(vi)
    if vi > out_ours and vo > 0 and vo < in_theirs:
        return Millisatoshi(vo)

    # cases where one channel is still capable to bring other to balance
    if vo < 0 and vi > 0 and vi < out_ours:
        return Millisatoshi(vi)
    if vi < 0 and vo > 0 and vo < in_theirs:
        return Millisatoshi(vo)

    # when both options are possible take the one with least effort
    if vo > 0 and vo < in_theirs and vi > 0 and vi < out_ours:
        return Millisatoshi(min(vi, vo))

    raise RpcError(
        "rebalance", payload,
        {'message': 'rebalancing these channels will make things worse'})
Example #4
0
def setup_routing_fees(plugin, payload, route, amount, substractfees: bool=False):
    delay = int(plugin.get_option('cltv-final'))

    amount_iter = amount
    for r in reversed(route):
        r['msatoshi'] = amount_iter.millisatoshis
        r['amount_msat'] = amount_iter
        r['delay'] = delay
        channels = plugin.rpc.listchannels(r['channel'])
        ch = next(c for c in channels.get('channels') if c['destination'] == r['id'])
        fee = Millisatoshi(ch['base_fee_millisatoshi'])
        # BOLT #7 requires fee >= fee_base_msat + ( amount_to_forward * fee_proportional_millionths / 1000000 )
        fee += (amount_iter * ch['fee_per_millionth'] + 10**6 - 1) // 10**6    # integer math trick to round up
        amount_iter += fee
        delay += ch['delay']

    # amounts have to be calculated the other way when being fee substracted
    # we took the upper loop as well for the delay parameter
    if substractfees:
        amount_iter = amount
        first = True
        for r in route:
            channels = plugin.rpc.listchannels(r['channel'])
            ch = next(c for c in channels.get('channels') if c['destination'] == r['id'])
            if not first:
                fee = Millisatoshi(ch['base_fee_millisatoshi'])
                # BOLT #7 requires fee >= fee_base_msat + ( amount_to_forward * fee_proportional_millionths / 1000000 )
                fee += (amount_iter * ch['fee_per_millionth'] + 10**6 - 1) // 10**6    # integer math trick to round up
                if fee > amount_iter:
                    raise RpcError(payload['command'], payload, {'message': 'cannot cover fees to %s %s' % (payload['command'], amount)})
                amount_iter -= fee
            first = False
            r['msatoshi'] = amount_iter.millisatoshis
            r['amount_msat'] = amount_iter
Example #5
0
def peer_from_scid(plugin, short_channel_id, my_node_id, payload):
    channels = plugin.rpc.listchannels(short_channel_id).get('channels')
    for ch in channels:
        if ch['source'] == my_node_id:
            return ch['destination']
    raise RpcError(
        "rebalance", payload,
        {'message': 'Cannot find peer for channel: ' + short_channel_id})
Example #6
0
def read_params(command: str, scid: str, percentage: float,
        chunks: int, maxfeepercent: float, retry_for: int, exemptfee: Millisatoshi):

    # check parameters
    if command != 'drain' and command != 'fill' and command != 'setbalance':
        raise RpcError(command, {}, {'message': 'Invalid command. Must be "drain", "fill" or "setbalance"'})
    percentage = float(percentage)
    if percentage < 0 or percentage > 100 or command != 'setbalance' and percentage == 0.0:
        raise RpcError(command, {}, {'message': 'Percentage must be between 0 and 100'})
    if chunks < 0:
        raise RpcError(command, {}, {'message': 'Negative chunks do not make sense. Try a positive value or use 0 (default) for auto-detection.'})

    # forge operation payload
    payload = {
        "command" : command,
        "scid": scid,
        "percentage": percentage,
        "chunks": chunks,
        "maxfeepercent": maxfeepercent,
        "retry_for": retry_for,
        "exemptfee": exemptfee,
        "labels" : [],
        "success_msg" : [],
    }

    # translate a 'setbalance' into respective drain or fill
    if command == 'setbalance':
        spendable, receivable = spendable_from_scid(plugin, payload)
        total = spendable + receivable
        target = total * 0.01 * payload['percentage']
        if target == spendable:
            raise RpcError(payload['command'], payload, {'message': 'target already reached, nothing to do.'})
        if spendable > target:
            payload['command'] = 'drain'
            amount = spendable - target
        else:
            payload['command'] = 'fill'
            amount = target - spendable
        payload['percentage'] = 100.0 * int(amount) / int(total)
        if payload['percentage'] == 0.0:
            raise RpcError(command, payload, {'message': 'target already reached, nothing to do.'})

    return payload
Example #7
0
def cleanup(plugin, label, payload, success_msg, error=None):
    try:
        plugin.rpc.delinvoice(label, 'unpaid')
    except RpcError as e:
        # race condition: waitsendpay timed out, but invoice get paid
        if 'status is paid' in e.error.get('message', ""):
            return success_msg
    if error is None:
        error = RpcError("rebalance", payload, {'message': 'Rebalance failed'})
    raise error
Example #8
0
def init(configuration, options, plugin):
    if not plugin.get_option('dblog-file'):
        raise RpcError("No dblog-file specified")
    plugin.conn = sqlite3.connect(plugin.get_option('dblog-file'),
                                  isolation_level=None)
    plugin.log("replaying pre-init data:")
    for c in plugin.sqlite_pre_init_cmds:
        plugin.conn.execute(c)
        plugin.log("{}".format(c))
    plugin.initted = True
    plugin.log("initialized {}".format(configuration))
Example #9
0
def cleanup(plugin, payload, error=None):
    # delete all invoices and count how many went through
    successful_chunks = 0
    for label in payload['labels']:
        try:
            plugin.rpc.delinvoice(label, 'unpaid')
        except RpcError as e:
            # race condition: waitsendpay timed out, but invoice got paid
            if 'status is paid' in e.error.get('message', ""):
                successful_chunks += 1

    if successful_chunks == payload['chunks']:
        return payload['success_msg']
    if successful_chunks > 0:
        payload['success_msg'] += ['Partially completed %d/%d chunks. Error: %s' % (successful_chunks, payload['chunks'], str(error))]
        return payload['success_msg']
    if error is None:
        error = RpcError(payload['command'], payload, {'message': 'Command failed, no chunk succeeded.'})
    raise error
Example #10
0
def setup_routing_fees(plugin, route, msatoshi, payload):
    delay = int(plugin.get_option('cltv-final'))
    for r in reversed(route):
        r['msatoshi'] = r['amount_msat'] = msatoshi
        r['delay'] = delay
        channels = plugin.rpc.listchannels(r['channel'])
        for ch in channels.get('channels'):
            if ch['destination'] == r['id']:
                fee = Millisatoshi(ch['base_fee_millisatoshi'])
                fee += msatoshi * ch['fee_per_millionth'] // 1000000
                if ch['source'] == payload['nodeid']:
                    if fee <= payload['msatoshi']:
                        fee = payload['msatoshi']
                    else:
                        raise RpcError(
                            "sendinvoiceless", payload,
                            {'message': 'Insufficient sending amount'})
                msatoshi += fee
                delay += ch['delay']
                r['direction'] = int(ch['channel_flags']) % 2
Example #11
0
def try_for_htlc_fee(plugin, payload, my_id, peer_id, amount, chunk, spendable_before):
    start_ts = int(time.time())
    label = payload['command'] + "-" + str(uuid.uuid4())
    payload['labels'] += [label]
    description = "%s %s %s%s [%d/%d]" % (payload['command'], payload['scid'], payload['percentage'], '%', chunk+1, payload['chunks'])
    invoice = plugin.rpc.invoice("any", label, description, payload['retry_for'] + 60)
    payment_hash = invoice['payment_hash']
    plugin.log("Invoice payment_hash: %s" % payment_hash)

    # exclude selected channel to prevent unwanted shortcuts
    excludes = [payload['scid']+'/0', payload['scid']+'/1']
    mychannels = plugin.rpc.listchannels(source=my_id).get('channels')
    # exclude local channels known to have too little capacity.
    # getroute currently does not do this.
    for channel in mychannels:
        if channel['short_channel_id'] == payload['scid']:
            continue  # already added few lines above
        spend, recv = spendable_from_scid(plugin, payload, channel['short_channel_id'])
        if payload['command'] == 'drain' and recv < amount:
            excludes += [channel['short_channel_id']+'/0', channel['short_channel_id']+'/1']
        if payload['command'] == 'fill' and spend < amount:
            excludes += [channel['short_channel_id']+'/0', channel['short_channel_id']+'/1']

    while int(time.time()) - start_ts < payload['retry_for']:
        if payload['command'] == 'drain':
            r = plugin.rpc.getroute(my_id, amount, riskfactor=0,
                    cltv=9, fromid=peer_id, fuzzpercent=0, exclude=excludes)
            route_out = {'id': peer_id, 'channel': payload['scid'], 'direction': int(my_id >= peer_id)}
            route = [route_out] + r['route']
            setup_routing_fees(plugin, payload, route, amount, True)
        if payload['command'] == 'fill':
            r = plugin.rpc.getroute(peer_id, amount, riskfactor=0,
                    cltv=9, fromid=my_id, fuzzpercent=0, exclude=excludes)
            route_in = {'id': my_id, 'channel': payload['scid'], 'direction': int(peer_id >= my_id)}
            route = r['route'] + [route_in]
            setup_routing_fees(plugin, payload, route, amount , False)

        fees = route[0]['amount_msat'] - route[-1]['amount_msat']

        # check fee and exclude worst channel the next time
        # NOTE: the int(msat) casts are just a workaround for outdated pylightning versions
        if fees > payload['exemptfee'] and int(fees) > int(amount) * payload['maxfeepercent'] / 100:
            worst_channel_id = find_worst_channel(route)
            if worst_channel_id is None:
                raise RpcError(payload['command'], payload, {'message': 'Insufficient fee'})
            excludes += [worst_channel_id + '/0', worst_channel_id + '/1']
            continue

        plugin.log("[%d/%d] Sending over %d hops to %s %s using %s fees" % (chunk+1, payload['chunks'], len(route), payload['command'], amount, fees))
        for r in route:
            plugin.log("    - %s  %14s  %s" % (r['id'], r['channel'], r['amount_msat']))

        try:
            plugin.rpc.sendpay(route, payment_hash, label)
            result = plugin.rpc.waitsendpay(payment_hash, payload['retry_for'] + start_ts - int(time.time()))
            if result.get('status') == 'complete':
                payload['success_msg'] += ["%dmsat sent over %d hops to %s %dmsat [%d/%d]" % (amount + fees, len(route), payload['command'], amount, chunk+1, payload['chunks'])]
                # we need to wait for gossipd to update to new state,
                # so remaining amounts will be calculated correctly for the next chunk
                spendable, _ = spendable_from_scid(plugin, payload)
                while spendable == spendable_before:
                    time.sleep(0.5)
                    spendable, _ = spendable_from_scid(plugin, payload)
                return True
            return False

        except RpcError as e:
            erring_message = e.error.get('message', '')
            erring_channel = e.error.get('data', {}).get('erring_channel')
            erring_index = e.error.get('data', {}).get('erring_index')
            erring_direction = e.error.get('data', {}).get('erring_direction')

            # detect exceeding of HTLC commitment fee
            if 'Capacity exceeded' in erring_message and erring_index == 0:
                match = HTLC_FEE_PAT.search(erring_message)
                if match:  # new servers tell htlc_fee via exception (#2691)
                    raise ValueError("htlc_fee is %s" % match.group(1))
                raise ValueError("htlc_fee unknown")

            if erring_channel == payload['scid']:
                raise RpcError(payload['command'], payload, {'message': 'Error with selected channel: %s' % erring_message})

            plugin.log("RpcError: " + str(e))
            if erring_channel is not None and erring_direction is not None:
                excludes.append(erring_channel + '/' + str(erring_direction))
Example #12
0
def test_or_set_chunks(plugin, payload, my_id):
    scid = payload['scid']
    cmd = payload['command']
    spendable, receivable = spendable_from_scid(plugin, payload)
    total = spendable + receivable
    amount = total * 0.01 * payload['percentage']

    # if capacity exceeds, limit amount to full or empty channel
    if cmd == "drain" and amount > spendable:
        amount = spendable
    if cmd == "fill" and amount > receivable:
        amount = receivable

    # get all spendable/receivables for our channels
    channels = {}
    for channel in plugin.rpc.listchannels(source=my_id).get('channels'):
        if channel['short_channel_id'] == scid:
            continue
        spend, recv = spendable_from_scid(plugin, payload, channel['short_channel_id'])
        channels[channel['short_channel_id']] = {
            'spendable' : spend,
            'receivable' : recv,
        }

    # test if selected chunks fit into other channel capacities
    if payload['chunks'] >= 1:
        chunks = payload['chunks']
        chunksize = amount / chunks
        fit = 0
        for i in channels:
            channel = channels[i]
            if cmd == "drain":
                fit += int(channel['receivable']) // int(chunksize)
            if cmd == "fill":
                fit += int(channel['spendable']) // int(chunksize)
        if fit >= chunks:
            return
        if cmd == "drain":
            raise RpcError(payload['command'], payload, {'message': 'Selected chunks (%d) will not fit incoming channel capacities.' % chunks})
        if cmd == "fill":
            raise RpcError(payload['command'], payload, {'message': 'Selected chunks (%d) will not fit outgoing channel capacities.' % chunks})


    # if chunks is 0 -> auto detect from 1 to 16 (max) chunks until amounts fit
    else:
        chunks = 0
        while chunks < 16:
            chunks += 1
            chunksize = amount / chunks
            fit = 0
            for i in channels:
                channel = channels[i]
                if cmd == "drain" and int(channel['receivable']) > 0:
                    fit += int(channel['receivable']) // int(chunksize)
                if cmd == "fill" and int(channel['spendable']) > 0:
                    fit += int(channel['spendable']) // int(chunksize)
                if fit >= chunks:
                    payload['chunks'] = chunks
                    return

        if cmd == "drain":
            raise RpcError(payload['command'], payload, {'message': 'Cannot detect required chunks to perform operation. Incoming capacity problem.'})
        if cmd == "fill":
            raise RpcError(payload['command'], payload, {'message': 'Cannot detect required chunks to perform operation. Outgoing capacity problem.'})
Example #13
0
def peer_from_scid(plugin, payload, short_channel_id, my_id):
    channels = plugin.rpc.listchannels(short_channel_id).get('channels')
    try:
        return next(c for c in channels if c['source'] == my_id)['destination']
    except StopIteration:
        raise RpcError(payload['command'], payload, {'message': 'Cannot find peer for channel: ' + short_channel_id})
Example #14
0
def rebalance(plugin,
              outgoing_scid,
              incoming_scid,
              msatoshi: Millisatoshi = None,
              maxfeepercent: float = 0.5,
              retry_for: int = 60,
              exemptfee: Millisatoshi = Millisatoshi(5000)):
    """Rebalancing channel liquidity with circular payments.

    This tool helps to move some msatoshis between your channels.
    """
    if msatoshi:
        msatoshi = Millisatoshi(msatoshi)
    maxfeepercent = float(maxfeepercent)
    retry_for = int(retry_for)
    exemptfee = Millisatoshi(exemptfee)
    payload = {
        "outgoing_scid": outgoing_scid,
        "incoming_scid": incoming_scid,
        "msatoshi": msatoshi,
        "maxfeepercent": maxfeepercent,
        "retry_for": retry_for,
        "exemptfee": exemptfee
    }
    my_node_id = plugin.rpc.getinfo().get('id')
    outgoing_node_id = peer_from_scid(plugin, outgoing_scid, my_node_id,
                                      payload)
    incoming_node_id = peer_from_scid(plugin, incoming_scid, my_node_id,
                                      payload)
    get_channel(plugin, payload, outgoing_node_id, outgoing_scid, True)
    get_channel(plugin, payload, incoming_node_id, incoming_scid, True)
    out_ours, out_total = amounts_from_scid(plugin, outgoing_scid)
    in_ours, in_total = amounts_from_scid(plugin, incoming_scid)
    plugin.log("Outgoing node: %s, channel: %s" %
               (outgoing_node_id, outgoing_scid))
    plugin.log("Incoming node: %s, channel: %s" %
               (incoming_node_id, incoming_scid))

    # If amount was not given, calculate a suitable 50/50 rebalance amount
    if msatoshi is None:
        msatoshi = calc_optimal_amount(out_ours, out_total, in_ours, in_total,
                                       payload)
        plugin.log("Estimating optimal amount %s" % msatoshi)

    # Check requested amounts are selected channels
    if msatoshi > out_ours or msatoshi > in_total - in_ours:
        raise RpcError("rebalance", payload,
                       {'message': 'Channel capacities too low'})

    route_out = {'id': outgoing_node_id, 'channel': outgoing_scid}
    route_in = {'id': my_node_id, 'channel': incoming_scid}
    start_ts = int(time.time())
    label = "Rebalance-" + str(uuid.uuid4())
    description = "%s to %s" % (outgoing_scid, incoming_scid)
    invoice = plugin.rpc.invoice(msatoshi, label, description, retry_for + 60)
    payment_hash = invoice['payment_hash']
    plugin.log("Invoice payment_hash: %s" % payment_hash)
    success_msg = ""
    try:
        excludes = []
        # excude all own channels to prevent unwanted shortcuts [out,mid,in]
        mychannels = plugin.rpc.listchannels(source=my_node_id)['channels']
        for channel in mychannels:
            excludes += [
                channel['short_channel_id'] + '/0',
                channel['short_channel_id'] + '/1'
            ]

        while int(time.time()) - start_ts < retry_for:
            r = plugin.rpc.getroute(incoming_node_id,
                                    msatoshi,
                                    riskfactor=1,
                                    cltv=9,
                                    fromid=outgoing_node_id,
                                    exclude=excludes)
            route_mid = r['route']
            route = [route_out] + route_mid + [route_in]
            setup_routing_fees(plugin, route, msatoshi)
            fees = route[0]['amount_msat'] - msatoshi

            # check fee and exclude worst channel the next time
            # NOTE: the int(msat) casts are just a workaround for outdated pylightning versions
            if fees > exemptfee and int(
                    fees) > int(msatoshi) * maxfeepercent / 100:
                worst_channel_id = find_worst_channel(route)
                if worst_channel_id is None:
                    raise RpcError("rebalance", payload,
                                   {'message': 'Insufficient fee'})
                excludes += [worst_channel_id + '/0', worst_channel_id + '/1']
                continue

            success_msg = "%d msat sent over %d hops to rebalance %d msat" % (
                msatoshi + fees, len(route), msatoshi)
            plugin.log("Sending %s over %d hops to rebalance %s" %
                       (msatoshi + fees, len(route), msatoshi))
            for r in route:
                plugin.log("    - %s  %14s  %s" %
                           (r['id'], r['channel'], r['amount_msat']))

            try:
                plugin.rpc.sendpay(route, payment_hash)
                plugin.rpc.waitsendpay(payment_hash,
                                       retry_for + start_ts - int(time.time()))
                return success_msg

            except RpcError as e:
                plugin.log("RpcError: " + str(e))
                erring_channel = e.error.get('data', {}).get('erring_channel')
                if erring_channel == incoming_scid:
                    raise RpcError("rebalance", payload,
                                   {'message': 'Error with incoming channel'})
                if erring_channel == outgoing_scid:
                    raise RpcError("rebalance", payload,
                                   {'message': 'Error with outgoing channel'})
                erring_direction = e.error.get('data',
                                               {}).get('erring_direction')
                if erring_channel is not None and erring_direction is not None:
                    excludes.append(erring_channel + '/' +
                                    str(erring_direction))

    except Exception as e:
        plugin.log("Exception: " + str(e))
        return cleanup(plugin, label, payload, success_msg, e)
    return cleanup(plugin, label, payload, success_msg)
Example #15
0
def sendinvoiceless(plugin,
                    nodeid,
                    msatoshi: Millisatoshi,
                    maxfeepercent: float = 0.5,
                    retry_for: int = 60,
                    exemptfee: Millisatoshi = Millisatoshi(5000)):
    """Send invoiceless payments with circular routes.
    This tool sends some msatoshis without needing to have an invoice from the receiving node.
    """
    msatoshi = Millisatoshi(msatoshi)
    maxfeepercent = float(maxfeepercent)
    retry_for = int(retry_for)
    exemptfee = Millisatoshi(exemptfee)
    payload = {
        "nodeid": nodeid,
        "msatoshi": msatoshi,
        "maxfeepercent": maxfeepercent,
        "retry_for": retry_for,
        "exemptfee": exemptfee
    }
    myid = plugin.rpc.getinfo().get('id')
    label = "InvoicelessChange-" + str(uuid.uuid4())
    description = "Sending %s to %s" % (msatoshi, nodeid)
    change = Millisatoshi(1000)
    invoice = plugin.rpc.invoice(change, label, description, retry_for + 60)
    payment_hash = invoice['payment_hash']
    plugin.log("Invoice payment_hash: %s" % payment_hash)
    success_msg = ""
    try:
        excludes = []
        start_ts = int(time.time())
        while int(time.time()) - start_ts < retry_for:
            forth = plugin.rpc.getroute(nodeid,
                                        msatoshi + change,
                                        riskfactor=10,
                                        exclude=excludes)
            back = plugin.rpc.getroute(myid,
                                       change,
                                       riskfactor=10,
                                       fromid=nodeid,
                                       exclude=excludes)
            route = forth['route'] + back['route']
            setup_routing_fees(plugin, route, change, payload)
            fees = route[0]['amount_msat'] - route[-1]['amount_msat'] - msatoshi

            # check fee and exclude worst channel the next time
            # NOTE: the int(msat) casts are just a workaround for outdated pylightning versions
            if fees > exemptfee and int(
                    fees) > int(msatoshi) * maxfeepercent / 100:
                worst_channel = find_worst_channel(route, nodeid)
                if worst_channel is None:
                    raise RpcError("sendinvoiceless", payload,
                                   {'message': 'Insufficient fee'})
                excludes.append(worst_channel)
                continue

            success_msg = "%d msat delivered with %d msat fee over %d hops" % (
                msatoshi, fees, len(route))
            plugin.log("Sending %s over %d hops to send %s and return %s" %
                       (route[0]['msatoshi'], len(route), msatoshi, change))
            for r in route:
                plugin.log("    - %s  %14s  %s" %
                           (r['id'], r['channel'], r['amount_msat']))

            try:
                plugin.rpc.sendpay(route, payment_hash)
                plugin.rpc.waitsendpay(payment_hash,
                                       retry_for + start_ts - int(time.time()))
                return success_msg

            except RpcError as e:
                plugin.log("RpcError: " + str(e))
                erring_channel = e.error.get('data', {}).get('erring_channel')
                erring_direction = e.error.get('data',
                                               {}).get('erring_direction')
                if erring_channel is not None and erring_direction is not None:
                    excludes.append(erring_channel + '/' +
                                    str(erring_direction))

    except Exception as e:
        plugin.log("Exception: " + str(e))
        return cleanup(plugin, label, payload, success_msg, e)
    return cleanup(plugin, label, payload, success_msg)
Example #16
0
def sendinvoiceless(plugin,
                    nodeid,
                    msatoshi: Millisatoshi,
                    maxfeepercent="0.5",
                    retry_for=60,
                    exemptfee: Millisatoshi = Millisatoshi(5000)):
    """Invoiceless payment with circular routes.

    This tool sends some msatoshis without needing to have an invoice from the receiving node.

    """
    payload = {
        "nodeid": nodeid,
        "msatoshi": msatoshi,
        "maxfeepercent": maxfeepercent,
        "retry_for": retry_for,
        "exemptfee": exemptfee
    }
    myid = plugin.rpc.getinfo().get('id')
    label = "InvoicelessChange-" + str(uuid.uuid4())
    description = "Sending %s to %s" % (msatoshi, nodeid)
    change = Millisatoshi(1000)
    invoice = plugin.rpc.invoice(change, label, description,
                                 int(retry_for) + 60)
    payment_hash = invoice['payment_hash']
    plugin.log("Invoice payment_hash: %s" % payment_hash)
    success_msg = ""
    try:
        excludes = []
        start_ts = int(time.time())
        while int(time.time()) - start_ts < int(retry_for):
            forth = plugin.rpc.getroute(nodeid,
                                        msatoshi + change,
                                        riskfactor=10,
                                        exclude=excludes)
            back = plugin.rpc.getroute(myid,
                                       change,
                                       riskfactor=10,
                                       fromid=nodeid,
                                       exclude=excludes)
            route = forth['route'] + back['route']
            setup_routing_fees(plugin, route, change, payload)
            fees = route[0]['msatoshi'] - route[-1]['msatoshi'] - msatoshi
            # Next line would be correct, but must be fixed to work around #2601 - cleanup when merged
            # if fees > exemptfee and fees > msatoshi * float(maxfeepercent) / 100:
            if fees > exemptfee and int(
                    fees) > int(msatoshi) * float(maxfeepercent) / 100:
                worst_channel = find_worst_channel(route, nodeid)
                if worst_channel is None:
                    raise RpcError("sendinvoiceless", payload,
                                   {'message': 'Insufficient fee'})
                excludes.append(worst_channel)
                continue
            try:
                plugin.log(
                    "Sending %s over %d hops to deliver %s and bring back %s" %
                    (route[0]['msatoshi'], len(route), msatoshi, change))
                for r in route:
                    plugin.log("Node: %s, channel: %13s, %s" %
                               (r['id'], r['channel'], r['msatoshi']))
                success_msg = "%d msat delivered with %d msat fee over %d hops" % (
                    msatoshi, fees, len(route))
                plugin.rpc.sendpay(route, payment_hash)
                plugin.rpc.waitsendpay(
                    payment_hash,
                    int(retry_for) + start_ts - int(time.time()))
                return success_msg
            except RpcError as e:
                plugin.log("RpcError: " + str(e))
                erring_channel = e.error.get('data', {}).get('erring_channel')
                erring_direction = e.error.get('data',
                                               {}).get('erring_direction')
                if erring_channel is not None and erring_direction is not None:
                    excludes.append(erring_channel + '/' +
                                    str(erring_direction))
    except Exception as e:
        plugin.log("Exception: " + str(e))
        return sendinvoiceless_fail(plugin, label, payload, success_msg, e)
    return sendinvoiceless_fail(plugin, label, payload, success_msg)