From 81d9dfdef750aa5aee53caa79d8fc92689c9a376 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Mon, 31 Aug 2020 13:51:28 -0300 Subject: [PATCH] Add service node details page --- observer.py | 53 ++++++++++- static/style.css | 6 ++ templates/include/sn_kcf.html | 2 +- templates/sn.html | 167 ++++++++++++++++++++++++++++++++++ 4 files changed, 222 insertions(+), 6 deletions(-) create mode 100644 templates/sn.html diff --git a/observer.py b/observer.py index 4535407..136d7e6 100644 --- a/observer.py +++ b/observer.py @@ -6,6 +6,7 @@ import babel.dates import json import sys import statistics +from base64 import b32encode, b16decode from werkzeug.routing import BaseConverter from pygments import highlight from pygments.lexers import JsonLexer @@ -131,6 +132,14 @@ def format_loki(atomic, tag=True, fixed=False, decimals=9, zero=None): def bytes_to_hex(b): return "".join("{:02x}".format(x) for x in b) +@app.template_filter('base32z') +def base32z(hex): + return b32encode(b16decode(hex, casefold=True)).translate( + bytes.maketrans( + b'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567', + b'ybndrfg8ejkmcpqxot1uwisza345h769')).decode().rstrip('=') + + @app.after_request def add_global_headers(response): if 'Cache-Control' not in response.headers: @@ -171,10 +180,14 @@ def get_sns(sns_future, info_future): awaiting_sns.append(sn) return awaiting_sns, active_sns, inactive_sns +@app.context_processor def template_globals(): return { 'config': conf, - 'server': { 'datetime': datetime.utcnow() } + 'server': { + 'datetime': datetime.utcnow(), + 'timestamp': datetime.utcnow().timestamp(), + }, } @@ -294,7 +307,6 @@ def main(refresh=None, page=0, per_page=None, first=None, last=None): custom_per_page=custom_per_page, mempool=mp, refresh=refresh, - **template_globals(), ) @app.route('/service_nodes') @@ -308,10 +320,43 @@ def sns(): active_sns=active, awaiting_sns=awaiting, inactive_sns=inactive, - **template_globals(), ) +@app.route('/sn/') +@app.route('/service_node/') # For backwards compatibility with old explorer URLs +def show_sn(pubkey): + lmq, lokid = lmq_connection() + info = FutureJSON(lmq, lokid, 'rpc.get_info', 1) + hfinfo = FutureJSON(lmq, lokid, 'rpc.hard_fork_info', 10) + sn = FutureJSON(lmq, lokid, 'rpc.get_service_nodes', 5, cache_key='single', args={ + "service_node_pubkeys": [pubkey]}).get() + + if 'service_node_states' not in sn or not sn['service_node_states']: + return flask.render_template('not_found.html', + info=info.get(), + hf=hfinfo.get(), + type='sn', + id=pubkey, + ) + + sn = sn['service_node_states'][0] + # These are a bit non-trivial to properly calculate: + + # Number of staked contributions + sn['num_contributions'] = sum(len(x["locked_contributions"]) for x in sn["contributors"]) + # Number of unfilled, reserved contribution spots: + sn['num_reserved_spots'] = sum(x["amount"] < x["reserved"] for x in sn["contributors"]) + # Available open contribution spots: + sn['num_open_spots'] = 0 if sn['total_reserved'] >= sn['staking_requirement'] else max(0, 4 - sn['num_contributions'] - sn['num_reserved_spots']) + + return flask.render_template('sn.html', + info=info.get(), + hf=hfinfo.get(), + sn=sn, + ) + + @app.route('/tx/') @app.route('/tx//') def show_tx(txid, more_details=False): @@ -329,7 +374,6 @@ def show_tx(txid, more_details=False): info=info.get(), type='tx', id=txid, - **template_globals(), ) tx = txs['txs'][0] if 'info' not in tx: @@ -389,5 +433,4 @@ def show_tx(txid, more_details=False): koffset_info=koffset_info, block_info=block_info, **more_details, - **template_globals(), ) diff --git a/static/style.css b/static/style.css index 23dc5ec..57a33e4 100644 --- a/static/style.css +++ b/static/style.css @@ -27,9 +27,15 @@ h1, h2, h3, h4, h5, h6 { .info_list>span>label { font-weight: normal; } +.info_list>span>label.omg { + font-weight: bold; +} p>label, span>label, h3>label, h4>label, td>label, .info-item>label { color: #30a532; } +label.warning { + color: red; +} .nowrap-spans { text-indent: -2em; padding-left: 2em; diff --git a/templates/include/sn_kcf.html b/templates/include/sn_kcf.html index d4af1ce..e0a5c2b 100644 --- a/templates/include/sn_kcf.html +++ b/templates/include/sn_kcf.html @@ -2,7 +2,7 @@ {%-set portions_base = 2**64 - 4-%} -{{sn.service_node_pubkey}} +{{sn.service_node_pubkey}} {{sn.contributors | length}}/4 diff --git a/templates/sn.html b/templates/sn.html new file mode 100644 index 0000000..c5e595d --- /dev/null +++ b/templates/sn.html @@ -0,0 +1,167 @@ +{% extends "_basic.html" %} + +{% block content %} + +{%-set portions_base = 2**64 - 4 %} +{%-set solo_node = sn.portions_for_operator == portions_base %} +{%-set decommed = sn.funded and not sn.active %} + +
+

Service Node Details

+
+

{{sn.service_node_pubkey}}

+ {%if sn.pubkey_ed25519 %} + {%if sn.pubkey_ed25519 != sn.service_node_pubkey%} +

{{sn.pubkey_ed25519}}

+ {%endif%} +

{{sn.pubkey_ed25519 | base32z}}.snode

+ {%endif%} +

{{sn.operator_address}}

+ +

Metadata

+
+

+ + {{sn.registration_height}} + {%if sn.registration_hf_version < hf.version%} + (hardfork v{{sn.registration_hf_version}}) + {%endif%} + + + + {%if sn.funded%} + {%if sn.active%} + + {%else%}{# decommissioned #} + + {%endif%} + {%else%} + + {%endif%} + {{sn.state_height}} + + + {{sn.staking_requirement | loki}} + + {%if not solo_node%} + {{(sn.portions_for_operator / portions_base * 100) | round(3) | chop0}}% + {%endif%} + + + {%if sn.total_contributed >= sn.staking_requirement%}100% + {%else%} + {{sn.total_contributed | loki}} ({{(sn.total_contributed / sn.staking_requirement) * 100 | round(2) | chop0}}%) + {%endif%} + + + {%if sn.total_reserved != sn.total_contributed%} + {{sn.total_reserved | loki}} + {%endif%} + + + + {%if sn.earned_downtime_blocks > 0%} + {{ (sn.earned_downtime_blocks * 120) | reltime(in_ago=false) }} ({{sn.earned_downtime_blocks}} blocks) + {%if not decommed and sn.earned_downtime_blocks < 60%} + (Note: ≥ 60 blocks required) + {%endif%} + {%else%} + None + {%endif%} + + + + + {%if sn.last_uptime_proof == 0%} + Not received + {%else%} + {{sn.last_uptime_proof | from_timestamp | ago}} ago + {%endif%} + + +

+ +

Service Node Status

+
+ {%if sn.active %} +

Registered, staked, and active on the network since block {{sn.state_height}}.

+ {%elif sn.funded%} +

Decommissioned: this service node is + registered and staked, but is currently decommissioned (since block + {{sn.state_height}}) for failing to meet service node requirements. + {%if sn.earned_downtime_blocks > 0%} + If it does not return to active duty within {{sn.earned_downtime_blocks}} blocks (about + {{(sn.earned_downtime_blocks * 120) | reltime(in_ago=false)}}) it will face deregistration. + {%else%} + The decommission time has expired; service node deregistration is imminent. + {%endif%} +

+ {%else%} +

Awaiting registration. This service node has {{(sn.staking_requirement - total_contributed) | loki}} + remaining to be contributed. + {%if sn.num_open_spots > 0%} + The minimum required stake contribution is {{((sn.staking_requirement - sn.total_reserved) / sn.num_open_spots) | loki}}. + {%endif%} +

+ {%endif%} + +

+ {%if sn.requested_unlock_height > 0%} + This service node is scheduled to expire at block {{sn.requested_unlock_height}}, in approximately + {{(sn.requested_unlock_height - info.height + 1) * 120 | reltime(in_ago=false) }} + ({{(server.datetime + ((sn.requested_unlock_height - info.height + 1) * 120)|from_timestamp) | format_datetime('short')}} UTC, est.) + {%else%} + This service node is staking infinitely: no unlock has been initiated by any of its contributors. + {%endif%} +

+ +

{{sn.contributors|length}} Contributor{%if sn.contributors|length > 1%}s{%endif%}

+
+ + + + + + + + + + + + {%for c in sn.contributors%} + + + + + + {%endfor%} + +
ContributorAmountReserved
{{c.address}}{{c.amount | loki}} + {%-if c.locked_contributions and c.locked_contributions|length > 1%} + ({{c.locked_contributions|length}} contributions) + {%endif-%} + {{c.reserved | loki}}
+ + {#FIXME:#} + {%if pending_stakes and pending_stakes|length > 0%} +

{{pending_stakes|length}} pending mempool contribution(s)

+
+ + + + + + + + + {%for s in pending_stakes%} + + + + + + {%endfor%} +
ContributorTXAmount
{{s.address}}{{s.txid}}{{s.amount}}
+ {%endif%} +
+{%endblock%}