mirror of
https://github.com/oxen-io/oxen-observer.git
synced 2023-12-14 09:22:54 +01:00
Add service node details page
This commit is contained in:
parent
752f19784b
commit
81d9dfdef7
53
observer.py
53
observer.py
|
@ -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(),
|
|
||||||
)
|
)
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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
167
templates/sn.html
Normal 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%}
|
Loading…
Reference in a new issue