mirror of
https://github.com/aljazceru/plugins.git
synced 2025-12-22 15:44:20 +01:00
invoiceless payment plugin
- sends payment without an invoice from the receiving node - uses circular payments: takes the money to the target node, pays in the form of routing fee, and brings some change back - no plugin/update required on the receiving side
This commit is contained in:
committed by
Christian Decker
parent
799eb51528
commit
9815483491
23
sendinvoiceless/README.md
Normal file
23
sendinvoiceless/README.md
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# Invoiceless payment plugin
|
||||||
|
|
||||||
|
This plugin sends some msatoshis without needing to have an invoice from the receiving node. It uses circular
|
||||||
|
payment: takes the money to the receiving node, pays in the form of routing fee, and brings some change back to
|
||||||
|
close the circle.
|
||||||
|
|
||||||
|
The plugin can be started with `lightningd` by adding the following `--plugin` option
|
||||||
|
(adjusting the path to wherever the plugins are actually stored):
|
||||||
|
|
||||||
|
```
|
||||||
|
lightningd --plugin=/path/to/plugin/sendinvoiceless.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Once the plugin is active you can send payment by running:
|
||||||
|
`lightning-cli sendinvoiceless nodeid msatoshi [maxfeepercent] [retry_for] [exemptfee]`
|
||||||
|
|
||||||
|
The `nodeid` is the identifier of the receiving node. The `maxfeepercent` limits the money paid in fees and
|
||||||
|
defaults to 0.5. The maxfeepercent' is a percentage of the amount that is to be paid. The `exemptfee` option can
|
||||||
|
be used for tiny payments which would be dominated by the fee leveraged by forwarding nodes. Setting exemptfee
|
||||||
|
allows the maxfeepercent check to be skipped on fees that are smaller than exemptfee (default: 5000 millisatoshi).
|
||||||
|
|
||||||
|
The command will keep finding routes and retrying the payment until it succeeds, or the given `retry_for` seconds
|
||||||
|
pass. retry_for defaults to 60 seconds and can only be an integer.
|
||||||
119
sendinvoiceless/sendinvoiceless.py
Executable file
119
sendinvoiceless/sendinvoiceless.py
Executable file
@@ -0,0 +1,119 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
from lightning import Plugin, Millisatoshi, RpcError
|
||||||
|
import time
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
plugin = Plugin()
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
def find_worst_channel(route, nodeid):
|
||||||
|
worst = None
|
||||||
|
worst_val = Millisatoshi(0)
|
||||||
|
for i in range(1, len(route)):
|
||||||
|
if route[i - 1]['id'] == nodeid:
|
||||||
|
continue
|
||||||
|
val = route[i - 1]['msatoshi'] - route[i]['msatoshi']
|
||||||
|
if val > worst_val:
|
||||||
|
worst = route[i]['channel'] + '/' + str(route[i]['direction'])
|
||||||
|
worst_val = val
|
||||||
|
return worst
|
||||||
|
|
||||||
|
|
||||||
|
def sendinvoiceless_fail(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("sendinvoiceless", payload, {'message': 'Sending failed'})
|
||||||
|
raise error
|
||||||
|
|
||||||
|
|
||||||
|
@plugin.method("sendinvoiceless")
|
||||||
|
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
|
||||||
|
if fees > exemptfee and fees > 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)
|
||||||
|
|
||||||
|
|
||||||
|
@plugin.init()
|
||||||
|
def init(options, configuration, plugin):
|
||||||
|
plugin.options['cltv-final']['value'] = plugin.rpc.listconfigs().get('cltv-final')
|
||||||
|
plugin.log("Plugin sendinvoiceless.py initialized")
|
||||||
|
|
||||||
|
|
||||||
|
plugin.add_option('cltv-final', 10, 'Number of blocks for final CheckLockTimeVerify expiry')
|
||||||
|
plugin.run()
|
||||||
Reference in New Issue
Block a user