Add service node details page

This commit is contained in:
Jason Rhinelander 2020-08-31 13:51:28 -03:00
parent 752f19784b
commit 81d9dfdef7
4 changed files with 222 additions and 6 deletions

View file

@ -6,6 +6,7 @@ import babel.dates
import json import json
import sys import sys
import statistics import statistics
from base64 import b32encode, b16decode
from werkzeug.routing import BaseConverter from werkzeug.routing import BaseConverter
from pygments import highlight from pygments import highlight
from pygments.lexers import JsonLexer 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): def bytes_to_hex(b):
return "".join("{:02x}".format(x) for x in 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 @app.after_request
def add_global_headers(response): def add_global_headers(response):
if 'Cache-Control' not in response.headers: if 'Cache-Control' not in response.headers:
@ -171,10 +180,14 @@ def get_sns(sns_future, info_future):
awaiting_sns.append(sn) awaiting_sns.append(sn)
return awaiting_sns, active_sns, inactive_sns return awaiting_sns, active_sns, inactive_sns
@app.context_processor
def template_globals(): def template_globals():
return { return {
'config': conf, '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, custom_per_page=custom_per_page,
mempool=mp, mempool=mp,
refresh=refresh, refresh=refresh,
**template_globals(),
) )
@app.route('/service_nodes') @app.route('/service_nodes')
@ -308,10 +320,43 @@ def sns():
active_sns=active, active_sns=active,
awaiting_sns=awaiting, awaiting_sns=awaiting,
inactive_sns=inactive, inactive_sns=inactive,
**template_globals(),
) )
@app.route('/sn/<hex64:pubkey>')
@app.route('/service_node/<hex64:pubkey>') # 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/<hex64:txid>') @app.route('/tx/<hex64:txid>')
@app.route('/tx/<hex64:txid>/<int:more_details>') @app.route('/tx/<hex64:txid>/<int:more_details>')
def show_tx(txid, more_details=False): def show_tx(txid, more_details=False):
@ -329,7 +374,6 @@ def show_tx(txid, more_details=False):
info=info.get(), info=info.get(),
type='tx', type='tx',
id=txid, id=txid,
**template_globals(),
) )
tx = txs['txs'][0] tx = txs['txs'][0]
if 'info' not in tx: if 'info' not in tx:
@ -389,5 +433,4 @@ def show_tx(txid, more_details=False):
koffset_info=koffset_info, koffset_info=koffset_info,
block_info=block_info, block_info=block_info,
**more_details, **more_details,
**template_globals(),
) )

View file

@ -27,9 +27,15 @@ h1, h2, h3, h4, h5, h6 {
.info_list>span>label { .info_list>span>label {
font-weight: normal; font-weight: normal;
} }
.info_list>span>label.omg {
font-weight: bold;
}
p>label, span>label, h3>label, h4>label, td>label, .info-item>label { p>label, span>label, h3>label, h4>label, td>label, .info-item>label {
color: #30a532; color: #30a532;
} }
label.warning {
color: red;
}
.nowrap-spans { .nowrap-spans {
text-indent: -2em; text-indent: -2em;
padding-left: 2em; padding-left: 2em;

View file

@ -2,7 +2,7 @@
{%-set portions_base = 2**64 - 4-%} {%-set portions_base = 2**64 - 4-%}
<td><a href="/service_node/{{sn.service_node_pubkey}}">{{sn.service_node_pubkey}}</a></td> <td><a href="/sn/{{sn.service_node_pubkey}}">{{sn.service_node_pubkey}}</a></td>
<td title=" <td title="
{%-for c in sn.contributors%}{%for lc in c.locked_contributions%}{{c.address | truncate(15)}} ({{lc.amount | loki(decimals=0)}} = {{(lc.amount / sn.staking_requirement * 100) | round(1) | chop0}}%) {%-for c in sn.contributors%}{%for lc in c.locked_contributions%}{{c.address | truncate(15)}} ({{lc.amount | loki(decimals=0)}} = {{(lc.amount / sn.staking_requirement * 100) | round(1) | chop0}}%)
{%endfor%}{%endfor%}"><span class="icon">{{sn.contributors | length}}/4</span></td> {%endfor%}{%endfor%}"><span class="icon">{{sn.contributors | length}}/4</span></td>

167
templates/sn.html Normal file
View file

@ -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 %}
<div class="sn-details Wrapper">
<h2>Service Node Details</h2>
<div class="TitleUnderliner"></div>
<h4 style="margin:5px"><label>Service Node Public Key:</label> {{sn.service_node_pubkey}}</h4>
{%if sn.pubkey_ed25519 %}
{%if sn.pubkey_ed25519 != sn.service_node_pubkey%}
<h4 style="margin:5px"><label>Service Node Auxiliary Pubkey:</label> {{sn.pubkey_ed25519}}</h4>
{%endif%}
<h4 style="margin:5px"><label>Lokinet Address:</label> {{sn.pubkey_ed25519 | base32z}}.snode</h4>
{%endif%}
<h4 style="margin:5px"><label>Operator Address:</label> {{sn.operator_address}}</h4>
<h2>Metadata</h2>
<div class="TitleUnderliner"></div>
<h4 class="info_list nowrap-spans">
<span><label>Registration height:</label> <a href="/block/{{sn.registration_height}}">{{sn.registration_height}}</a>
{%if sn.registration_hf_version < hf.version%}
(hardfork v{{sn.registration_hf_version}})
{%endif%}
</span>
<span title="TX index: {{sn.last_reward_transaction_index}}">
{%if sn.funded%}
{%if sn.active%}
<label>Active since block:</label>
{%else%}{# decommissioned #}
<label>Decommissioned since block:</label>
{%endif%}
{%else%}
<label>Last contribution block:</label>
{%endif%}
<a href="/block/{{sn.state_height}}">{{sn.state_height}}</a>
</span>
<span><label>Staking requirement:</label> {{sn.staking_requirement | loki}}</span>
{%if not solo_node%}
<span><label>Operator fee:</label> {{(sn.portions_for_operator / portions_base * 100) | round(3) | chop0}}%</span>
{%endif%}
<span><label>Total contributed:</label>
{%if sn.total_contributed >= sn.staking_requirement%}100%
{%else%}
{{sn.total_contributed | loki}} ({{(sn.total_contributed / sn.staking_requirement) * 100 | round(2) | chop0}}%)
{%endif%}
</span>
{%if sn.total_reserved != sn.total_contributed%}
<span><label>Total reserved:</label> {{sn.total_reserved | loki}}</span>
{%endif%}
<span>
<label>{%if decommed %}Remaining{%else%}Allowed{%endif%} downtime:</label>
{%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%}
<span class="note">(Note: ≥ 60 blocks required)</span>
{%endif%}
{%else%}
None
{%endif%}
</span>
<span>
<label class="omg{%if server.timestamp - sn.last_uptime_proof > 3900%} warning{%endif%}">Last uptime proof:</label>
{%if sn.last_uptime_proof == 0%}
<span>Not received</span>
{%else%}
<span title="{{sn.last_uptime_proof|from_timestamp|format_datetime}}">{{sn.last_uptime_proof | from_timestamp | ago}} ago</span>
{%endif%}
</span>
</h4>
<h2>Service Node Status</h2>
<div class="TitleDivider"></div>
{%if sn.active %}
<p class="sn-active">Registered, staked, and active on the network since block {{sn.state_height}}.</p>
{%elif sn.funded%}
<p class="sn-decomm"><span class="omg">Decommissioned</span>: this service node is
registered and staked, but is currently <span class="omg">decommissioned</span> (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 <span class="omg">deregistration</span>.
{%else%}
The decommission time has expired; service node <span class="omg">deregistration</span> is imminent.
{%endif%}
</p>
{%else%}
<p class="sn-awaiting">Awaiting registration. This service node has <span class="loki required">{{(sn.staking_requirement - total_contributed) | loki}}</span>
remaining to be contributed.
{%if sn.num_open_spots > 0%}
The minimum required stake contribution is <span class="loki required">{{((sn.staking_requirement - sn.total_reserved) / sn.num_open_spots) | loki}}</span>.
{%endif%}
</p>
{%endif%}
<p class="sn-expiration{%if sn.requested_unlock_height > 0%} unlocking{%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%}
</p>
<h2>{{sn.contributors|length}} Contributor{%if sn.contributors|length > 1%}s{%endif%}</h2>
<div class="TitleDivider"></div>
<table style="width:100%">
<thead>
<tr>
<td>Contributor</td>
<td>Amount</td>
<td>Reserved</td>
</tr>
</thead>
<tbody>
{%for c in sn.contributors%}
<tr>
<td>{{c.address}}</td>
<td>{{c.amount | loki}}
{%-if c.locked_contributions and c.locked_contributions|length > 1%}
({{c.locked_contributions|length}} contributions)
{%endif-%}
</td>
<td>{{c.reserved | loki}}</td>
</tr>
{%endfor%}
</tbody>
</table>
{#FIXME:#}
{%if pending_stakes and pending_stakes|length > 0%}
<h2>{{pending_stakes|length}} pending mempool contribution(s)</h2>
<div class="TitleDivider"></div>
<table class="pending-mempool-stakes">
<tr class="TableHeader">
<td>Contributor</td>
<td class="tx">TX</td>
<td>Amount</td>
</tr>
{%for s in pending_stakes%}
<tr>
<td>{{s.address}}</td>
<td class="tx"><a href="/tx/{{s.txid}}">{{s.txid}}</a></td>
<td>{{s.amount}}</td>
</tr>
{%endfor%}
</table>
{%endif%}
</div>
{%endblock%}