Rename directory 'archive' to 'Unmaintained'

This commit is contained in:
fmhoeger
2024-02-01 15:40:46 -06:00
committed by mergify[bot]
parent 7cbfcaf025
commit e538e3d559
82 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,63 @@
# Summary plugin
This plugin is a little hack to show a summary of your node, including
fiat amounts. If you have pylightning 0.0.7.1 or above, you get nice linegraphs,
otherwise normal ASCII.
## Installation
For general plugin installation instructions see the repos main
[README.md](https://github.com/lightningd/plugins/blob/master/README.md#Installation)
## Options:
* --summary-currency: Currency ticker to look up on bitaverage (default: `USD`)
* --summary-currency-prefix: Prefix when printing currency (default: `USD $`)
## Example Usage
Unfortunately the python plugin framework doesn't pretty-print, nor does
lightning-cli, so best viewed with -H:
```
$ lightning-cli -H summary
network=TESTNET
my_address=031a3478d481b92e3c28810228252898c5f0d82fc4d07f5210c4f34d4aba56b769@165.227.30.200
num_utxos=5
utxo_amount=1.20119332000btc (USD $4473.84)
num_channels=29
num_connected=2
num_gossipers=1
avail_out=0.27095103btc (USD $1009.16)
avail_in=2.05851379btc (USD $7666.93)
fees_collected=0.00000012341btc (USD $0.00)
channels_key=P=private O=offline
channels= ├────────────╢ (O):02ac05912f89e43b88de3472e8c3003b
├───────────╢ (O):02dd4cef0192611bc34cd1c3a0a7eb0f
╟────────────┤ (PO):02a13878947a133d7c96e70303a9bf27
║ (O):033e2db012833d997e3c
╟┤ (O):Kenny_Loggins
╟──────────────────────┤(O):DeutscheTestnetBank
╟─────────────────────┤ (O):BlueLagoon1
╟──────────────────────┤(O):0270dd38e8af9a64b4a483ab12b6aeb1
╟┤ (O):btctest.lnetwork.tokyo
╟─┤ (O):microbet.fun
╟──────────────────────┤(PO):02fcab6e34a2ad21be2a752ab96d13f5
╟──────────────────────┤(O):htlc.me
╟───┤ (O):02229ea9a7a4f9bf8bf25ce225079aed
╟─────────────────────┤ (O):025d5b572a94235cfcbdc429181b2b88
╟────────────┤ (PO):03c56de3a84336b4a939777ace9ecbef
╟────────┤ (O):LiteStrikeBTClnd
╟────────────────┤ (PO):037c9cf1cde4414c59407d547b7eac08
║ (O):03490a74e4def9125a84aee2d84e8cfe
├─────────┼─────────┤ (O):aranguren.org
║ (PO):03cc6603e1f6df535dd8b423284f2c09
║ (O):cyclopes
╟─────────────────────┤ (PO):02b73a2160863e925e9fa978b0ddc56b
╟───┤ (O):lnd-testnet.ignios.net
╟─┤ (PO):0327a104108173d4a4f34ab2cbc3084c
╟─┤ :dwarf
║ (PO):028133777757ce281658804dd82f5758
╟────────────┤ (PO):02db62ffff5c35be74e7f856bba136db
╟┤ (PO):03015ac044f5fa9768ededf6fed9c0ff
╟──────────────────────┤:0270685ca81a8e4d4d01

View File

View File

@@ -0,0 +1,4 @@
pyln-client>=0.12.1
requests>=2.10.0
requests[socks]>=2.10.0
packaging>=14.1

370
Unmaintained/summary/summary.py Executable file
View File

@@ -0,0 +1,370 @@
#!/usr/bin/env python3
from pyln.client import Plugin, Millisatoshi
from packaging import version
from collections import namedtuple
from operator import attrgetter
from summary_avail import trace_availability, addpeer
import pyln.client
import requests
import threading
import time
import pickle
import sys
plugin = Plugin(autopatch=True)
datastore_key = ['summary', 'avail']
Channel = namedtuple('Channel', ['total', 'ours', 'theirs', 'pid', 'private', 'connected', 'scid', 'avail', 'base', 'ppm'])
Charset = namedtuple('Charset', ['double_left', 'left', 'bar', 'mid', 'right', 'double_right', 'empty'])
draw_boxch = Charset('', '', '', '', '', '', '')
draw_ascii = Charset('#', '[', '-', '+', ']', '#', '|')
summary_description = "Gets summary information about this node.\n"\
"Pass a list of scids to the {exclude} parameter to exclude some channels from the outputs.\n"\
"Sort the result by using the {sortkey} parameter that can be one of 'total', 'ours', 'theirs', 'scid' (default), 'avail', 'base', 'ppm'."
class PeerThread(threading.Thread):
def __init__(self):
super().__init__()
self.daemon = True
def run(self):
# delay initial execution, so peers have a chance to connect on startup
time.sleep(plugin.avail_interval)
while True:
try:
rpcpeers = plugin.rpc.listpeers()
trace_availability(plugin, rpcpeers)
write_datastore(plugin)
plugin.log("[PeerThread] Peerstate wrote to datastore. "
"Sleeping now...", 'debug')
time.sleep(plugin.avail_interval)
except Exception as ex:
plugin.log("[PeerThread] " + str(ex), 'warn')
class PriceThread(threading.Thread):
def __init__(self, proxies):
super().__init__()
self.daemon = True
self.proxies = proxies
def run(self):
while True:
try:
# NOTE: Bitstamp has a DNS/Proxy issues that can return 404
# Workaround: retry up to 5 times with a delay
for _ in range(5):
r = requests.get('https://www.bitstamp.net/api/v2/ticker/btc{}'.format(plugin.currency.lower()), proxies=self.proxies)
if not r.status_code == 200:
time.sleep(1)
continue
break
plugin.fiat_per_btc = float(r.json()['last'])
except Exception as ex:
plugin.log("[PriceThread] " + str(ex), 'warn')
# Six hours is more than often enough for polling
time.sleep(6 * 3600)
def to_fiatstr(msat: Millisatoshi):
return "{}{:.2f}".format(plugin.currency_prefix,
int(msat) / 10**11 * plugin.fiat_per_btc)
# appends an output table header that explains fields and capacity
def append_header(table, max_msat):
short_str = Millisatoshi(max_msat).to_approx_str()
draw = plugin.draw
table.append("%c%-13sOUT/OURS %c IN/THEIRS%12s%c SCID FLAG BASE PPM AVAIL ALIAS"
% (draw.left, short_str, draw.mid, short_str, draw.right))
@plugin.method("summary", long_desc=summary_description)
def summary(plugin, exclude='', sortkey=None, ascii=None):
"""Gets summary information about this node."""
# Sets ascii mode for this and future requests (if requested)
if ascii is not None:
if ascii:
plugin.draw = draw_ascii
else:
plugin.draw = draw_boxch
reply = {}
info = plugin.rpc.getinfo()
funds = plugin.rpc.listfunds()
peers = plugin.rpc.listpeers()['peers']
# Make it stand out if we're not on mainnet.
if info['network'] != 'bitcoin':
reply['network'] = info['network'].upper()
if hasattr(plugin, 'my_address') and plugin.my_address:
reply['my_address'] = plugin.my_address
else:
reply['warning_no_address'] = "NO PUBLIC ADDRESSES"
utxos = [int(f['amount_msat']) for f in funds['outputs']
if f['status'] == 'confirmed']
reply['num_utxos'] = len(utxos)
utxo_amount = Millisatoshi(sum(utxos))
reply['utxo_amount'] = utxo_amount.to_btc_str()
avail_out = Millisatoshi(0)
avail_in = Millisatoshi(0)
chans = []
reply['num_channels'] = 0
reply['num_connected'] = 0
reply['num_gossipers'] = 0
for p in peers:
pid = p['id']
channels = []
if 'channels' in p:
channels = p['channels']
elif 'num_channels' in p and p['num_channels'] > 0:
channels = plugin.rpc.listpeerchannels(pid)['channels']
addpeer(plugin, p)
active_channel = False
for c in channels:
if c['state'] != 'CHANNELD_NORMAL':
continue
active_channel = True
if c['short_channel_id'] in exclude:
continue
if p['connected']:
reply['num_connected'] += 1
if c['our_reserve_msat'] < c['to_us_msat']:
to_us = c['to_us_msat'] - c['our_reserve_msat']
else:
to_us = Millisatoshi(0)
avail_out += to_us
# We have to derive amount to them
to_them = c['total_msat'] - c['to_us_msat']
if c['their_reserve_msat'] < to_them:
to_them = to_them - c['their_reserve_msat']
else:
to_them = Millisatoshi(0)
avail_in += to_them
reply['num_channels'] += 1
chans.append(Channel(
c['total_msat'],
to_us, to_them,
pid,
c['private'],
p['connected'],
c['short_channel_id'],
plugin.persist['p'][pid]['a'],
Millisatoshi(c['fee_base_msat']),
c['fee_proportional_millionths'],
))
if not active_channel and p['connected']:
reply['num_gossipers'] += 1
reply['avail_out'] = avail_out.to_btc_str()
reply['avail_in'] = avail_in.to_btc_str()
reply['fees_collected'] = Millisatoshi(info['fees_collected_msat']).to_btc_str()
if plugin.fiat_per_btc > 0:
reply['utxo_amount'] += ' ({})'.format(to_fiatstr(utxo_amount))
reply['avail_out'] += ' ({})'.format(to_fiatstr(avail_out))
reply['avail_in'] += ' ({})'.format(to_fiatstr(avail_in))
reply['fees_collected'] += ' ({})'.format(to_fiatstr(info['fees_collected_msat']))
if len(chans) > 0:
if sortkey is None or sortkey.lower() not in Channel._fields:
sortkey = plugin.sortkey
chans = sorted(chans, key=attrgetter(sortkey.lower()))
reply['channels_flags'] = 'P:private O:offline'
reply['channels'] = ["\n"]
biggest = max(max(int(c.ours), int(c.theirs)) for c in chans)
append_header(reply['channels'], biggest)
for c in chans:
# Create simple line graph, 47 chars wide.
our_len = int(round(int(c.ours) / biggest * 23))
their_len = int(round(int(c.theirs) / biggest * 23))
# We put midpoint in the middle.
draw = plugin.draw
mid = draw.mid
if our_len == 0:
left = "{:>23}".format('')
mid = draw.double_left
else:
left = "{:>23}".format(draw.left + draw.bar * (our_len - 1))
if their_len == 0:
right = "{:23}".format('')
# Both 0 is a special case.
if our_len == 0:
mid = draw.empty
else:
mid = draw.double_right
else:
right = "{:23}".format(draw.bar * (their_len - 1) + draw.right)
s = left + mid + right
# output short channel id, so things can be copyNpasted easily
s += " {:14} ".format(c.scid)
extra = ''
if c.private:
extra += 'P'
else:
extra += '_'
if not c.connected:
extra += 'O'
else:
extra += '_'
s += '[{}] '.format(extra)
# append fees
s += ' {:4}'.format(c.base.millisatoshis)
s += ' {:5} '.format(c.ppm)
# append 24hr availability
s += '{:4.0%} '.format(c.avail)
# append alias or id
node = plugin.rpc.listnodes(c.pid)['nodes']
if len(node) != 0 and 'alias' in node[0]:
s += node[0]['alias']
else:
s += c.pid[0:32]
reply['channels'].append(s)
# Make modern lightning-cli format this human-readble by default!
reply['format-hint'] = 'simple'
return reply
def new_datastore():
return {'p': {}, 'r': 0, 'v': 1} # see summary_avail.py for structure
def check_datastore(obj):
if 'v' in obj and type(obj['v']) is int and obj['v'] == 1:
return True
return False
def load_datastore(plugin):
entries = plugin.rpc.listdatastore(key=datastore_key)['datastore']
if len(entries) == 0:
plugin.log(f"Creating a new datastore '{datastore_key}'", 'debug')
return new_datastore()
persist = pickle.loads(bytearray.fromhex(entries[0]["hex"]))
if not check_datastore(persist):
plugin.log(f"Dismissing old datastore '{datastore_key}'", 'debug')
return new_datastore()
plugin.log(f"Reopened datastore '{datastore_key}' with {persist['r']} "
f"runs and {len(persist['p'])} entries", 'debug')
return persist
def write_datastore(plugin):
hexstr = pickle.dumps(plugin.persist).hex()
plugin.rpc.datastore(key=datastore_key, hex=hexstr, mode="create-or-replace")
@plugin.init()
def init(options, configuration, plugin):
plugin.sortkey = options['summary-sortkey']
if plugin.sortkey not in Channel._fields:
plugin.sortkey = 'scid' # default to 'scid' on unknown keys
plugin.currency = options['summary-currency']
plugin.currency_prefix = options['summary-currency-prefix']
plugin.fiat_per_btc = 0
plugin.avail_interval = float(options['summary-availability-interval'])
plugin.avail_window = 60 * 60 * int(options['summary-availability-window'])
plugin.persist = load_datastore(plugin)
plugin.draw = draw_ascii
# __version__ was introduced in 0.0.7.1, with utf8 passthrough support.
if hasattr(pyln.client, "__version__") and version.parse(pyln.client.__version__) >= version.parse("0.0.7.1"):
plugin.draw = draw_boxch
if options.get('summary-ascii'):
plugin.draw = draw_ascii
info = plugin.rpc.getinfo()
config = plugin.rpc.listconfigs()
if 'always-use-proxy' in config and config['always-use-proxy']:
paddr = config['proxy']
# Default port in 9050
if ':' not in paddr:
paddr += ':9050'
proxies = {'https': 'socks5h://' + paddr,
'http': 'socks5h://' + paddr}
else:
proxies = None
# Measure availability
PeerThread().start()
# Try to grab conversion price
PriceThread(proxies).start()
# Prefer IPv4, otherwise take any to give out address.
best_address = None
for a in info['address']:
if best_address is None:
best_address = a
elif a['type'] == 'ipv4' and best_address['type'] != 'ipv4':
best_address = a
if best_address:
plugin.my_address = info['id'] + '@' + best_address['address']
if best_address['port'] != 9735:
plugin.my_address += ':' + str(best_address['port'])
else:
plugin.my_address = None
plugin.log("Plugin summary.py initialized")
@plugin.subscribe("shutdown")
def on_rpc_command_callback(plugin, **kwargs):
# FIXME: Writing datastore does not work on exit, as daemon is already lost.
# plugin.log("Writing out datastore before shutting down")
# write_datastore(plugin)
sys.exit()
plugin.add_option(
'summary-currency',
'USD',
'What currency should I look up on btcaverage?'
)
plugin.add_option(
'summary-currency-prefix',
'USD $',
'What prefix to use for currency'
)
plugin.add_option(
'summary-availability-interval',
300,
'How often in seconds the availability should be calculated.'
)
plugin.add_option(
'summary-availability-window',
72,
'How many hours the availability should be averaged over.'
)
plugin.add_option(
'summary-sortkey',
'scid',
'Sort the channels list by a namedtuple key, defaults to "scid".'
)
plugin.add_option(
'summary-ascii',
False,
'If ascii mode should be enabled by default',
'flag'
)
plugin.run()

View File

@@ -0,0 +1,42 @@
# This is the persist object structure:
#
# {
# "p": { # peerstate
# "PEER_ID" : { # the peers id
# "c": True, # connected or not
# "a": 1.0 # the availability value
# }
# },
# "r": 123, # the number of runs
# "v": 1 # version
# }
# ensure an rpc peer is added
def addpeer(p, rpcpeer):
pid = rpcpeer['id']
if pid not in p.persist['p']:
p.persist['p'][pid] = {
'c': rpcpeer['connected'],
'a': 1.0 if rpcpeer['connected'] else 0.0
}
# exponetially smooth online/offline states of peers
def trace_availability(p, rpcpeers):
p.persist['r'] += 1
leadwin = max(min(p.avail_window, p.persist['r'] * p.avail_interval), p.avail_interval)
samples = leadwin / p.avail_interval
alpha = 1.0 / samples
beta = 1.0 - alpha
for rpcpeer in rpcpeers['peers']:
pid = rpcpeer['id']
addpeer(p, rpcpeer)
if rpcpeer['connected']:
p.persist['p'][pid]['c'] = True
p.persist['p'][pid]['a'] = 1.0 * alpha + p.persist['p'][pid]['a'] * beta
else:
p.persist['p'][pid]['c'] = False
p.persist['p'][pid]['a'] = 0.0 * alpha + p.persist['p'][pid]['a'] * beta

View File

@@ -0,0 +1,295 @@
import subprocess
import unittest
import re
import os
from pyln.client import Plugin
from pyln.testing.fixtures import * # noqa: F401,F403
from pyln.testing.utils import wait_for
from .summary_avail import trace_availability
pluginopt = {'plugin': os.path.join(os.path.dirname(__file__), "summary.py")}
# returns a test plugin stub
def get_stub():
plugin = Plugin()
plugin.avail_interval = 60
plugin.avail_window = 3600
plugin.persist = {}
plugin.persist['p'] = {}
plugin.persist['r'] = 0
plugin.persist['v'] = 1
return plugin
def test_summary_peer_thread(node_factory):
# Set a low PeerThread interval so we can test quickly.
opts = {'summary-availability-interval': 0.5}
opts.update(pluginopt)
l1, l2 = node_factory.line_graph(2, opts=opts)
l2id = l2.info['id']
# when
s1 = l1.rpc.summary()
l2.stop() # we stop l2 and wait for l1 to see that
l1.daemon.wait_for_log(f".*{l2id}.*Peer connection lost.*")
wait_for(lambda: l1.rpc.listpeers(l2id)['peers'][0]['connected'] is False)
l1.daemon.wait_for_log("Peerstate wrote to datastore")
s2 = l1.rpc.summary()
# then
avail1 = int(re.search(' ([0-9]*)% ', s1['channels'][2]).group(1))
avail2 = int(re.search(' ([0-9]*)% ', s2['channels'][2]).group(1))
assert(avail1 == 100)
assert(avail2 > 0 and avail2 < avail1)
# tests the 72hr exponential availibility tracing
# tests base algo and peerstate tracing
def test_summary_avail_101():
# given
plugin = get_stub()
rpcpeers = {
'peers': [
{'id': '1', 'connected': True},
{'id': '2', 'connected': False},
{'id': '3', 'connected': True},
]
}
# when
for i in range(100):
trace_availability(plugin, rpcpeers)
# then
assert(plugin.persist['p']['1']['a'] == 1.0)
assert(plugin.persist['p']['2']['a'] == 0.0)
assert(plugin.persist['p']['3']['a'] == 1.0)
assert(plugin.persist['p']['1']['c'] is True)
assert(plugin.persist['p']['2']['c'] is False)
assert(plugin.persist['p']['3']['c'] is True)
# tests for 50% downtime
def test_summary_avail_50():
# given
plugin = get_stub()
rpcpeers_on = {
'peers': [
{'id': '1', 'connected': True},
]
}
rpcpeers_off = {
'peers': [
{'id': '1', 'connected': False},
]
}
# when
for i in range(30):
trace_availability(plugin, rpcpeers_on)
for i in range(30):
trace_availability(plugin, rpcpeers_off)
# then
assert(round(plugin.persist['p']['1']['a'], 3) == 0.5)
# tests for 2/3 downtime
def test_summary_avail_33():
# given
plugin = get_stub()
rpcpeers_on = {
'peers': [
{'id': '1', 'connected': True},
]
}
rpcpeers_off = {
'peers': [
{'id': '1', 'connected': False},
]
}
# when
for i in range(20):
trace_availability(plugin, rpcpeers_on)
for i in range(40):
trace_availability(plugin, rpcpeers_off)
# then
assert(round(plugin.persist['p']['1']['a'], 3) == 0.333)
# tests for 1/3 downtime
def test_summary_avail_66():
# given
plugin = get_stub()
rpcpeers_on = {
'peers': [
{'id': '1', 'connected': True},
]
}
rpcpeers_off = {
'peers': [
{'id': '1', 'connected': False},
]
}
# when
for i in range(40):
trace_availability(plugin, rpcpeers_on)
for i in range(20):
trace_availability(plugin, rpcpeers_off)
# then
assert(round(plugin.persist['p']['1']['a'], 3) == 0.667)
# checks the leading window is smaller if interval count is low
# when a node just started
def test_summary_avail_leadwin():
# given
plugin = get_stub()
rpcpeers_on = {
'peers': [
{'id': '1', 'connected': True},
]
}
rpcpeers_off = {
'peers': [
{'id': '1', 'connected': False},
]
}
# when
trace_availability(plugin, rpcpeers_on)
trace_availability(plugin, rpcpeers_on)
trace_availability(plugin, rpcpeers_off)
# then
assert(round(plugin.persist['p']['1']['a'], 3) == 0.667)
# checks whether the peerstate is persistent
def test_summary_persist(node_factory):
# Set a low PeerThread interval so we can test quickly.
opts = {'summary-availability-interval': 0.5, 'may_reconnect': True}
opts.update(pluginopt)
l1, l2 = node_factory.line_graph(2, opts=opts)
# when
l1.daemon.logsearch_start = 0
l1.daemon.wait_for_log("Creating a new datastore")
l1.daemon.wait_for_log("Peerstate wrote to datastore")
s1 = l1.rpc.summary()
l2.stop()
l1.restart()
assert l1.daemon.is_in_log("Reopened datastore")
l1.daemon.logsearch_start = len(l1.daemon.logs)
l1.daemon.wait_for_log("Peerstate wrote to datastore")
s2 = l1.rpc.summary()
# then
avail1 = int(re.search(' ([0-9]*)% ', s1['channels'][2]).group(1))
avail2 = int(re.search(' ([0-9]*)% ', s2['channels'][2]).group(1))
assert(avail1 == 100)
assert(0 < avail2 < 100)
def test_summary_start(node_factory):
# given
l1 = node_factory.get_node(options=pluginopt)
l2 = node_factory.get_node(options=pluginopt)
l1.connect(l2)
# when
s = l1.rpc.summary()
# then
expected = {
'format-hint': 'simple',
'network': 'REGTEST',
'num_channels': 0,
'num_connected': 0,
'num_gossipers': 1,
'num_utxos': 0,
'warning_no_address': 'NO PUBLIC ADDRESSES'
}
for k, v in expected.items():
assert(s[k] == v)
def test_summary_ascii(node_factory):
# given
l1, l2 = node_factory.line_graph(2, opts=pluginopt)
l3, l5 = node_factory.line_graph(2, opts={**pluginopt, 'summary-ascii': None})
# when
s1 = l1.rpc.summary()
s2 = l1.rpc.summary(ascii=True)
s3 = l1.rpc.summary() # remembers last calls ascii setting
s4 = l1.rpc.summary(ascii=False)
s5 = l1.rpc.summary()
s6 = l3.rpc.summary()
# then
assert "├─────" in s1['channels'][-1]
assert "[-----" in s2['channels'][-1]
assert "[-----" in s3['channels'][-1]
assert "├─────" in s4['channels'][-1]
assert "├─────" in s5['channels'][-1]
assert "[-----" in s6['channels'][-1]
def test_summary_opts(directory):
opts = ['--summary-currency', '--summary-currency-prefix']
help_out = subprocess.check_output([
'lightningd',
'--lightning-dir={}'.format(directory),
'--help'
]).decode('utf-8')
for o in opts:
assert(o not in help_out)
help_out = subprocess.check_output([
'lightningd',
'--lightning-dir={}'.format(directory),
'--plugin={}'.format(pluginopt['plugin']),
'--help'
]).decode('utf-8')
for o in opts:
assert(o in help_out)
def test_summary_exclude(node_factory):
l1, l2 = node_factory.line_graph(2, opts=pluginopt)
s = l1.rpc.summary()
expected = {
'format-hint': 'simple',
'network': 'REGTEST',
'num_channels': 1,
'num_connected': 1,
'num_gossipers': 0,
'num_utxos': 1,
'warning_no_address': 'NO PUBLIC ADDRESSES'
}
for k, v in expected.items():
assert(s[k] == v)
scid = l1.rpc.listchannels()['channels'][0]['short_channel_id']
s = l1.rpc.summary(exclude=scid)
expected = {
'format-hint': 'simple',
'network': 'REGTEST',
'num_channels': 0,
'num_connected': 0,
'num_gossipers': 0,
'num_utxos': 1,
'warning_no_address': 'NO PUBLIC ADDRESSES'
}
for k, v in expected.items():
assert(s[k] == v)