diff --git a/summary/README.md b/summary/README.md new file mode 100644 index 0000000..50ab3f6 --- /dev/null +++ b/summary/README.md @@ -0,0 +1,57 @@ +# 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. + +## 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) +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 diff --git a/summary/requirements.txt b/summary/requirements.txt new file mode 100644 index 0000000..0b40f7d --- /dev/null +++ b/summary/requirements.txt @@ -0,0 +1,3 @@ +pylightning>=0.0.6 +requests>=2.0.0 +packaging>=14.1 diff --git a/summary/summary.py b/summary/summary.py new file mode 100755 index 0000000..0b1c877 --- /dev/null +++ b/summary/summary.py @@ -0,0 +1,202 @@ +#!/usr/bin/env python3 +from lightning import Plugin, Millisatoshi +from packaging import version +from collections import namedtuple +import lightning +import json +import requests +import threading +import time + +plugin = Plugin(autopatch=True) + +have_utf8 = False + +# __version__ was introduced in 0.0.7.1, with utf8 passthrough support. +try: + if version.parse(lightning.__version__) >= version.parse("0.0.7.1"): + have_utf8 = True +except Exception: + pass + +Charset = namedtuple('Charset', ['double_left', 'left', 'bar', 'mid', 'right', 'double_right', 'empty']) +if have_utf8: + draw = Charset('╟', '├', '─', '┼', '┤', '╢', '║') +else: + draw = Charset('#', '[', '-', '/', ']', '#', '|') + + +class PriceThread(threading.Thread): + def __init__(self): + super().__init__() + self.daemon = True + self.start() + + def run(self): + try: + r = requests.get('https://apiv2.bitcoinaverage.com/convert/global' + '?from=BTC&to={}&amount=1'.format(plugin.currency)) + plugin.fiat_per_btc = json.loads(r.content)['price'] + except Exception: + pass + # 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) + + +@plugin.method("summary") +def summary(plugin): + """Gets summary information about this node.""" + + reply = {} + info = plugin.rpc.getinfo() + funds = plugin.rpc.listfunds() + peers = plugin.rpc.listpeers() + + # Make it stand out if we're not on mainnet. + if info['network'] != 'bitcoin': + reply['network'] = info['network'].upper() + + if not plugin.my_address: + reply['warning_no_address'] = "NO PUBLIC ADDRESSES" + else: + reply['my_address'] = plugin.my_address + + + 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['peers']: + active_channel = False + for c in p['channels']: + if c['state'] != 'CHANNELD_NORMAL': + continue + active_channel = True + 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((c['total_msat'], to_us, to_them, p['id'], c['private'], p['connected'])) + + 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() + + if plugin.fiat_per_btc: + reply['utxo_amount'] += ' ({})'.format(to_fiatstr(utxo_amount)) + reply['avail_out'] += ' ({})'.format(to_fiatstr(avail_out)) + reply['avail_in'] += ' ({})'.format(to_fiatstr(avail_in)) + + if chans != []: + reply['channels_key'] = 'P=private O=offline' + reply['channels'] = [] + biggest = max(max(int(c[1]), int(c[2])) for c in chans) + for c in chans: + # Create simple line graph, 47 chars wide. + our_len = int((int(c[1]) / biggest * 23)) + their_len = int((int(c[2]) / biggest * 23)) + divided = False + + # We put midpoint in the middle. + 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 + + extra = '' + if c[4]: + extra += 'P' + if not c[5]: + extra += 'O' + if extra != '': + s += '({})'.format(extra) + + node = plugin.rpc.listnodes(c[3])['nodes'] + if len(node) != 0: + s += ':' + node[0]['alias'] + else: + s += ':' + c[3][0:32] + reply['channels'].append(s) + + return reply + + +@plugin.init() +def init(options, configuration, plugin): + plugin.currency = options['summary-currency'] + plugin.currency_prefix = options['summary-currency-prefix'] + info = plugin.rpc.getinfo() + + # Try to grab conversion price + PriceThread() + + # 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.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.run()