mirror of
https://github.com/oxen-io/oxen-observer.git
synced 2023-12-14 09:22:54 +01:00
Initial commit
Basic working prototype of most of the first pages (templates brought in from the old explorer but updated for jinja).
This commit is contained in:
commit
4b27018965
11 changed files with 855 additions and 0 deletions
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
mainnet.sock
|
||||
testnet.sock
|
||||
devnet.sock
|
||||
pylokimq.cpython-*.so
|
||||
__pycache__
|
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
[submodule "pylokimq"]
|
||||
path = pylokimq
|
||||
url = https://github.com/majestrate/pylokimq.git
|
242
app.py
Normal file
242
app.py
Normal file
|
@ -0,0 +1,242 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import flask
|
||||
import pylokimq
|
||||
from datetime import datetime, timedelta
|
||||
import babel.dates
|
||||
import json
|
||||
import sys
|
||||
import statistics
|
||||
|
||||
app = flask.Flask(__name__)
|
||||
# DEBUG:
|
||||
app.config['TEMPLATES_AUTO_RELOAD'] = True
|
||||
app.jinja_env.auto_reload = True
|
||||
# end DEBUG
|
||||
|
||||
lmq = pylokimq.LokiMQ(pylokimq.LogLevel.warn)
|
||||
lmq.start()
|
||||
lokid = lmq.connect_remote('ipc://./mainnet.sock')
|
||||
#lokid = lmq.connect_remote('ipc://./testnet.sock')
|
||||
#lokid = lmq.connect_remote('ipc://./devnet.sock')
|
||||
|
||||
cached = {}
|
||||
cached_args = {}
|
||||
cache_expiry = {}
|
||||
|
||||
class FutureJSON():
|
||||
def __init__(self, endpoint, cache_seconds=3, *, args=[], fail_okay=False, timeout=10):
|
||||
self.endpoint = endpoint
|
||||
self.fail_okay = fail_okay
|
||||
if self.endpoint in cached and cached_args[self.endpoint] == args and cache_expiry[self.endpoint] >= datetime.now():
|
||||
self.json = cached[self.endpoint]
|
||||
self.args = None
|
||||
self.future = None
|
||||
else:
|
||||
self.json = None
|
||||
self.args = args
|
||||
self.future = lmq.request_future(lokid, self.endpoint, self.args, timeout=timeout)
|
||||
self.cache_seconds = cache_seconds
|
||||
|
||||
def get(self):
|
||||
"""If the result is already available, returns it immediately (and can safely be called multiple times.
|
||||
Otherwise waits for the result, parses as json, and caches it. Returns None if the request fails"""
|
||||
if self.json is None and self.future is not None:
|
||||
try:
|
||||
result = self.future.get()
|
||||
if result[0] != b'200':
|
||||
raise RuntimeError("Request failed: got {}".format(result))
|
||||
self.json = json.loads(result[1])
|
||||
cached[self.endpoint] = self.json
|
||||
cached_args[self.endpoint] = self.args
|
||||
cache_expiry[self.endpoint] = datetime.now() + timedelta(seconds=self.cache_seconds)
|
||||
except RuntimeError as e:
|
||||
if not self.fail_okay:
|
||||
print("Something getting wrong: {}".format(e), file=sys.stderr)
|
||||
self.future = None
|
||||
pass
|
||||
|
||||
return self.json
|
||||
|
||||
|
||||
@app.template_filter('format_datetime')
|
||||
def format_datetime(value, format='long'):
|
||||
return babel.dates.format_datetime(value, format, tzinfo=babel.dates.get_timezone('UTC'))
|
||||
|
||||
@app.template_filter('from_timestamp')
|
||||
def from_timestamp(value):
|
||||
return datetime.fromtimestamp(value)
|
||||
|
||||
@app.template_filter('ago')
|
||||
def datetime_ago(value):
|
||||
delta = datetime.now() - value
|
||||
disp=''
|
||||
if delta.days < 0:
|
||||
delta = -delta
|
||||
disp += '-'
|
||||
if delta.days > 0:
|
||||
disp += '{}d '.format(delta.days)
|
||||
disp += '{:2d}:{:02d}:{:02d}'.format(delta.seconds // 3600, delta.seconds // 60 % 60, delta.seconds % 60)
|
||||
return disp
|
||||
|
||||
@app.template_filter('round')
|
||||
def filter_round(value):
|
||||
return ("{:.0f}" if value >= 100 or isinstance(value, int) else "{:.1f}" if value >= 10 else "{:.2f}").format(value)
|
||||
|
||||
si_suffix = ['', 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']
|
||||
@app.template_filter('si')
|
||||
def format_si(value):
|
||||
i = 0
|
||||
while value >= 1000 and i < len(si_suffix) - 1:
|
||||
value /= 1000
|
||||
i += 1
|
||||
return filter_round(value) + '{}'.format(si_suffix[i])
|
||||
|
||||
@app.template_filter('loki')
|
||||
def format_loki(atomic, tag=True, fixed=False, decimals=9):
|
||||
"""Formats an atomic current value as a human currency value.
|
||||
tag - if False then don't append " LOKI"
|
||||
fixed - if True then don't strip insignificant trailing 0's and '.'
|
||||
decimals - at how many decimal we should round; the default is full precision
|
||||
"""
|
||||
disp = "{{:.{}f}}".format(decimals).format(atomic * 1e-9)
|
||||
if not fixed:
|
||||
disp = disp.rstrip('0').rstrip('.')
|
||||
if tag:
|
||||
disp += ' LOKI'
|
||||
return disp
|
||||
|
||||
@app.after_request
|
||||
def add_global_headers(response):
|
||||
if 'Cache-Control' not in response.headers:
|
||||
response.headers['Cache-Control'] = 'no-store'
|
||||
return response
|
||||
|
||||
@app.route('/style.css')
|
||||
def css():
|
||||
return flask.send_from_directory('static', 'style.css')
|
||||
|
||||
@app.route('/')
|
||||
@app.route('/page/<int:page>')
|
||||
@app.route('/page/<int:page>/<int:per_page>')
|
||||
@app.route('/range/<int:first>/<int:last>')
|
||||
@app.route('/autorefresh/<int:refresh>')
|
||||
def main(refresh=None, page=0, per_page=None, first=None, last=None):
|
||||
info = FutureJSON('rpc.get_info', 1)
|
||||
stake = FutureJSON('rpc.get_staking_requirement', 10)
|
||||
base_fee = FutureJSON('rpc.get_fee_estimate', 10)
|
||||
hfinfo = FutureJSON('rpc.hard_fork_info', 10)
|
||||
mempool = FutureJSON('rpc.get_transaction_pool', 5)
|
||||
# This call is slow the first time it gets called in lokid but will be fast after that, so call
|
||||
# it with a very short timeout. It's also an admin-only command, so will always fail if we're
|
||||
# using a restricted RPC interface.
|
||||
coinbase = FutureJSON('admin.get_coinbase_tx_sum', 10, timeout=1, fail_okay=True,
|
||||
args=[json.dumps({"height":0, "count":2**31-1}).encode()])
|
||||
server = dict(
|
||||
timestamp=datetime.utcnow(),
|
||||
)
|
||||
|
||||
config = dict(
|
||||
# FIXME: make these configurable
|
||||
pusher=True,
|
||||
key_image_checker=True,
|
||||
output_key_checker=True,
|
||||
autorefresh_option=True,
|
||||
mainnet_url='',
|
||||
testnet_url='',
|
||||
devnet_url='',
|
||||
blocks_per_page=20
|
||||
)
|
||||
|
||||
custom_per_page = ''
|
||||
if per_page is None or per_page <= 0 or per_page > 100:
|
||||
per_page = config['blocks_per_page']
|
||||
else:
|
||||
custom_per_page = '/{}'.format(per_page)
|
||||
|
||||
# We have some chained request dependencies here and below, so get() them as needed; all other
|
||||
# non-dependent requests should already have a future initiated above so that they can
|
||||
# potentially run in parallel.
|
||||
height = info.get()['height']
|
||||
|
||||
# Permalinked block range:
|
||||
if first is not None and last is not None and 0 <= first <= last and last <= first + 99:
|
||||
start_height, end_height = first, last
|
||||
if end_height - start_height + 1 != per_page:
|
||||
per_page = end_height - start_height + 1;
|
||||
custom_per_page = '/{}'.format(per_page)
|
||||
# We generally can't get a perfect page number because our range (e.g. 5-14) won't line up
|
||||
# with pages (e.g. 10-19, 0-19), so just get as close as we can. Next/Prev page won't be
|
||||
# quite right, but they'll be within half a page.
|
||||
page = round((height - 1 - end_height) / per_page)
|
||||
else:
|
||||
end_height = max(0, height - per_page*page - 1)
|
||||
start_height = max(0, end_height - per_page + 1)
|
||||
|
||||
blocks = FutureJSON('rpc.get_block_headers_range', args=[json.dumps({
|
||||
'start_height': start_height,
|
||||
'end_height': end_height,
|
||||
'get_tx_hashes': True,
|
||||
}).encode()]).get()['headers']
|
||||
|
||||
# If 'txs' is already there then it is probably left over from our cached previous call through
|
||||
# here.
|
||||
if blocks and 'txs' not in blocks[0]:
|
||||
txids = []
|
||||
for b in blocks:
|
||||
b['txs'] = []
|
||||
txids.append(b['miner_tx_hash'])
|
||||
if 'tx_hashes' in b:
|
||||
txids += b['tx_hashes']
|
||||
txs = FutureJSON('rpc.get_transactions', args=[json.dumps({
|
||||
"txs_hashes": txids,
|
||||
"decode_as_json": True,
|
||||
"tx_extra": True,
|
||||
"prune": True,
|
||||
}).encode()]).get()
|
||||
txs = txs['txs']
|
||||
i = 0
|
||||
for tx in txs:
|
||||
# TXs should come back in the same order so we can just skip ahead one when the block
|
||||
# height changes rather than needing to search for the block
|
||||
if blocks[i]['height'] != tx['block_height']:
|
||||
i += 1
|
||||
while i < len(blocks) and blocks[i]['height'] != tx['block_height']:
|
||||
print("Something getting wrong: missing txes?", file=sys.stderr)
|
||||
i += 1
|
||||
if i >= len(blocks):
|
||||
print("Something getting wrong: have leftover txes")
|
||||
break
|
||||
tx['info'] = json.loads(tx['as_json'])
|
||||
blocks[i]['txs'].append(tx)
|
||||
|
||||
|
||||
#txes = FutureJSON('rpc.get_transactions');
|
||||
|
||||
|
||||
# mempool RPC return values are about as nasty as can be. For each mempool tx, we get back
|
||||
# *both* binary+hex encoded values and JSON-encoded values slammed into a string, which means we
|
||||
# have to invoke an *extra* JSON parser for each tx. This is terrible.
|
||||
mp = mempool.get()
|
||||
if 'transactions' in mp:
|
||||
for tx in mp['transactions']:
|
||||
tx['info'] = json.loads(tx["tx_json"])
|
||||
else:
|
||||
mp['transactions'] = []
|
||||
|
||||
return flask.render_template('index.html',
|
||||
info=info.get(),
|
||||
stake=stake.get(),
|
||||
fees=base_fee.get(),
|
||||
emission=coinbase.get(),
|
||||
hf=hfinfo.get(),
|
||||
blocks=blocks,
|
||||
block_size_median=statistics.median(b['block_size'] for b in blocks),
|
||||
page=page,
|
||||
per_page=per_page,
|
||||
custom_per_page=custom_per_page,
|
||||
mempool=mp,
|
||||
server=server,
|
||||
config=config,
|
||||
refresh=refresh,
|
||||
)
|
1
pylokimq
Submodule
1
pylokimq
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit 32de27c2fe8ff9ccb86031afaf900734ea16508a
|
233
static/style.css
Normal file
233
static/style.css
Normal file
|
@ -0,0 +1,233 @@
|
|||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-size: 80%;
|
||||
background-color: #1a1a1a;
|
||||
font-family: sans-serif;
|
||||
color: white;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
text-align: left;
|
||||
font-weight: normal;
|
||||
margin-bottom: 0.2em;
|
||||
}
|
||||
|
||||
.general_info {
|
||||
font-size: 12px;
|
||||
margin-top: 5px;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
.nowrap-spans {
|
||||
text-indent: -2em;
|
||||
padding-left: 2em;
|
||||
}
|
||||
.nowrap-spans>span {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.Subtitle {
|
||||
font-size: 1.0em;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
margin: 0;
|
||||
margin-top: 0.5em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.PaginationControl,
|
||||
.PageButton {
|
||||
background-color: #008522;
|
||||
min-height: 1.5em;
|
||||
padding: 5px;
|
||||
padding-top: 8px;
|
||||
margin-right: 1em;
|
||||
font-weight: bold;
|
||||
font-size: 1em;
|
||||
color: white;
|
||||
border: none;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
.PageButton.disabled {
|
||||
background-color: #808080;
|
||||
}
|
||||
|
||||
.PaginationControl select {
|
||||
font-size: inherit;
|
||||
background-color: #1a1a1a;
|
||||
color: #008522;
|
||||
border: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.PageButton:hover {
|
||||
background-color: #006a1b;
|
||||
}
|
||||
.PageButton.disabled:hover {
|
||||
background-color: #707070;
|
||||
}
|
||||
|
||||
.Wrapper {
|
||||
width: 80%;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.Header {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.Header > a:link {
|
||||
text-decoration: none;
|
||||
color: white; !important
|
||||
}
|
||||
|
||||
.Table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
table thead tr,
|
||||
.TableHeader {
|
||||
font-weight: bold;
|
||||
background-color: #008522;
|
||||
}
|
||||
|
||||
.TitleDivider {
|
||||
height: 2px;
|
||||
width: 100%;
|
||||
background-color: #008522;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.LinkNoUnderline {
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
.LinkNoUnderline:hover {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.center {
|
||||
margin: auto;
|
||||
width: 96%;
|
||||
/*border: 1px solid #73AD21;
|
||||
padding: 10px;*/
|
||||
}
|
||||
|
||||
tr, li, #pages, .info {
|
||||
height: 2em;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
#pages {
|
||||
margin-top: 1em;
|
||||
margin-bottom: 1em;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
td {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
a:link, a:visited {
|
||||
text-decoration: underline;
|
||||
color: white;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
color: #78be20;
|
||||
}
|
||||
|
||||
form {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.style-1 input[type="text"] {
|
||||
padding: 2px;
|
||||
border: solid 1px rgba(255, 255, 255, 0.25);
|
||||
background-color: #1a1a1a;
|
||||
height: 2em;
|
||||
width: 80%;
|
||||
font-size: 1em;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.style-1 input[type="text"]:focus,
|
||||
.style-1 input[type="text"].focus {
|
||||
border: solid 1px #008522;
|
||||
}
|
||||
h1, h2, h3, h4, h5, h6 { color: white; }
|
||||
|
||||
.tabs {
|
||||
position: relative;
|
||||
min-height: 220px; /* This part sucks */
|
||||
clear: both;
|
||||
margin: 25px 0;
|
||||
}
|
||||
|
||||
.tab {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.tab label {
|
||||
background: rgba(0, 133, 34, 0.5);
|
||||
padding: 10px;
|
||||
margin-right: 2px;
|
||||
margin-left: -1px;
|
||||
position: relative;
|
||||
left: 1px;
|
||||
}
|
||||
|
||||
.tab [type=radio] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.content {
|
||||
position: absolute;
|
||||
top: 28px;
|
||||
left: 0;
|
||||
background: #1a1a1a;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
padding-top: 20px;
|
||||
padding-right: 20px;
|
||||
padding-bottom: 20px;
|
||||
border-top: 2px solid #008522;
|
||||
}
|
||||
|
||||
[type=radio]:checked ~ label {
|
||||
background: #008522 ;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
[type=radio]:checked ~ label ~ .content {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
input#toggle-1[type=checkbox] {
|
||||
position: absolute;
|
||||
/*top: -9999px;*/
|
||||
left: -9999px;
|
||||
|
||||
}
|
||||
label#show-decoded-inputs {
|
||||
/*-webkit-appearance: push-button;*/
|
||||
/*-moz-appearance: button;*/
|
||||
/*margin: 60px 0 10px 0;*/
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
}
|
||||
|
||||
div#decoded-inputs{
|
||||
}
|
||||
|
||||
/* Toggled State */
|
||||
input#toggle-1[type=checkbox]:checked ~ div#decoded-inputs {
|
||||
display: block;
|
||||
}
|
||||
|
||||
span.icon {
|
||||
cursor: help;
|
||||
}
|
54
templates/_basic.html
Normal file
54
templates/_basic.html
Normal file
|
@ -0,0 +1,54 @@
|
|||
{# Basic content template: provides an overridable header/content/footer #}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
{% block head %}
|
||||
<title>{% block title %}{% endblock %}Loki{{' TESTNET' if info and info.testnet else ' DEVNET' if info and info.devnet else ''}}
|
||||
Blockchain Explorer</title>
|
||||
<link rel="stylesheet" type="text/css" href="/style.css">
|
||||
{% if refresh %}
|
||||
<meta http-equiv="refresh" content="{{refresh}}">
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="header" class="Wrapper">
|
||||
{% block header %}
|
||||
<div>
|
||||
<h1 class="Header"><a href="/">Loki
|
||||
{%if info and info.testnet%}
|
||||
<span style="color:#ff6b62">Testnet</span>
|
||||
{%elif info and info.devnet%}
|
||||
<span style="color:#af5bd2">Devnet</span>
|
||||
{%endif%}
|
||||
Blockchain Explorer</a></h1>
|
||||
<form action="/search" method="get" style="width: 100%; margin-top:15px;" class="style-1">
|
||||
<input type="text" name="value" size="120" placeholder="Block Height, Block Hash, Transaction Hash">
|
||||
<input type="submit" class="PageButton" value="Search">
|
||||
</form>
|
||||
<form action="/search_service_node" method="get" style="width: 100%; margin-top:15px; margin-bottom: 1em;" class="style-1">
|
||||
<input type="text" name="value" size="120" placeholder="Service Node Public Key">
|
||||
<input type="submit" class="PageButton" value="Search">
|
||||
</form>
|
||||
<div class="TitleDivider" style="margin-bottom: 1em"></div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
</div>
|
||||
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
|
||||
<div id="footer" class="Wrapper">
|
||||
{% block footer %}
|
||||
<div class="TitleDivider" style="margin-top: 1em" ></div>
|
||||
<p style="margin-top:10px">
|
||||
<a href="https://github.com/Doy-lee/onion-loki-blockchain-explorer/tree/loki">Source Code</a>
|
||||
| Explorer Version (api): {{git_branch_name}}-{{last_git_commit_date}}-{{last_git_commit_hash}} ({{api}})
|
||||
| Loki Version: {{loki_version_full}}
|
||||
</p>
|
||||
{% endblock %}
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
31
templates/include/block_page_controls.html
Normal file
31
templates/include/block_page_controls.html
Normal file
|
@ -0,0 +1,31 @@
|
|||
<div id="pages" class="center" style="text-align: center;">
|
||||
{% set top_page = (info.height - 1) // per_page %}
|
||||
{% if page > 0 %}
|
||||
<a href="/page/{{page - 1}}{{custom_per_page}}">
|
||||
<div class="PageButton">Prev</div>
|
||||
</a>
|
||||
{% else %}
|
||||
<div class="PageButton disabled">Prev</div>
|
||||
{% endif %}
|
||||
<div class="PageButton">
|
||||
Current Page: <a title="Return to current blocks" href="/{%if custom_per_page%}page/0{{custom_per_page}}{%endif%}">{{page}}</a>/<a href="/page/{{top_page}}{{custom_per_page}}">{{top_page}}</a>
|
||||
</div>
|
||||
|
||||
{%if page < top_page%}
|
||||
<a href="/page/{{page + 1}}{{custom_per_page}}">
|
||||
<div class="PageButton">Next</div>
|
||||
</a>
|
||||
{% else %}
|
||||
<div class="PageButton disabled">Next</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="PaginationControl">
|
||||
{% set per_page_options = [5, 10, 20, 25, 50, 100] %}
|
||||
{% if not per_page in per_page_options %}
|
||||
{{ '' if per_page_options.append(per_page) }}
|
||||
{% endif %}
|
||||
Showing <select onchange="window.location.href = '/page/0/' + this.value">
|
||||
{% for p in per_page_options | sort %}<option{%if p == per_page%} selected="selected"{%endif%}>{{p}}</option>{% endfor %}
|
||||
</select> blocks/page
|
||||
</div>
|
||||
</div>
|
43
templates/include/mempool.html
Normal file
43
templates/include/mempool.html
Normal file
|
@ -0,0 +1,43 @@
|
|||
{# Lists mempool transactions. mempool_limit can be set to limit the number of shown transactions; defaults to 25. Set explicitly to none to show all. #}
|
||||
{% if not mempool_limit is defined %}{% set mempool_limit = 25 %}
|
||||
{% elif mempool_limit is none %}{% set mempool_limit = mempool.transactions|length %}
|
||||
{% endif %}
|
||||
<div class="Wrapper">
|
||||
<h2 style="margin-bottom: 0px"> Transaction Pool</h2>
|
||||
|
||||
<h4 class="Subtitle">{{mempool.transactions|length}} transactions,
|
||||
{{mempool.transactions|sum(attribute='blob_size') | si}}B</h4>
|
||||
<div class="TitleDivider"></div>
|
||||
|
||||
<table style="width:100%">
|
||||
<thead>
|
||||
<tr>
|
||||
<td title="How long ago the transaction was received by this node">Age [h:m:s]</td>
|
||||
<td>Type</td>
|
||||
<td>Transaction Hash</td>
|
||||
<td>Fee/Per kB</td>
|
||||
<td>In/Out</td>
|
||||
<td>TX Size</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for tx in mempool.transactions[:mempool_limit] %}
|
||||
<tr>
|
||||
<td title="{{tx.receive_time | from_timestamp | format_datetime}}">{{tx.receive_time | from_timestamp | ago}}</td>
|
||||
<td class=TXType>{{tx_type_symbol}}</td>
|
||||
<td><a href="/tx/{{tx.id_hash}}">{{tx.id_hash}}</a></td>
|
||||
<td>{{tx.info.vin | length}}/{{tx.info.vout | length}}</td>
|
||||
<td>{{tx.blob_size | si}}B</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{% if mempool.transactions|length > mempool_limit %}
|
||||
<div class="center" style="text-align: center; margin-bottom: 10px">
|
||||
<a href="/txpool">Only {{mempool_limit}}/{{len(mempool.transactions)}} transactions shown. Click here to see all of them</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div> <!-- Wrapper -->
|
||||
|
13
templates/include/tx_fee.html
Normal file
13
templates/include/tx_fee.html
Normal file
|
@ -0,0 +1,13 @@
|
|||
{% macro display(tx, show_zero=false) -%}
|
||||
{% set fee =
|
||||
(tx.info.rct_signatures.txnFee if 'rct_signatures' in tx.info and 'txnFee' in tx.info.rct_signatures else 0) +
|
||||
(tx.extra.burn_amount if 'burn_amount' in tx.info else 0)
|
||||
-%}
|
||||
{% if fee > 0 or show_zero -%}
|
||||
{{ fee | loki(tag=False, fixed=True, decimals=4) }}
|
||||
{%- if 'burn_amount' in tx.extra %}
|
||||
<span class="icon" title="= {{(fee - tx.extra.burn_amount) | loki}} TX fee
|
||||
+ {{tx.extra.burn_amount | loki}} burned">🔥</span>
|
||||
{%-endif-%}
|
||||
{%endif-%}
|
||||
{% endmacro %}
|
33
templates/include/tx_type_symbol.html
Normal file
33
templates/include/tx_type_symbol.html
Normal file
|
@ -0,0 +1,33 @@
|
|||
{% macro display(tx) -%}
|
||||
{% if tx.info.version >= 4 -%}
|
||||
{% if tx.info.type == 1 and 'sn_state_change' in tx.extra -%}
|
||||
{% if tx.extra.sn_state_change.type == 'decom' -%}
|
||||
<span class="icon" title="Service Node decommission">👎</span>
|
||||
{% elif tx.extra.sn_state_change.type == 'recom' -%}
|
||||
<span class="icon" title="Service Node recommission">👍</span>
|
||||
{% elif tx.extra.sn_state_change.type == 'dereg' -%}
|
||||
<span class="icon" title="Service Node deregistration">🚫</span>
|
||||
{% elif tx.extra.sn_state_change.type == 'ip' -%}
|
||||
<span class="icon" title="Service Node IP change penalty">📋</span>
|
||||
{% else -%}
|
||||
<span class="icon" title="Unknown state change transaction">❓</span><!-- Either a bug or a malformed transaction -->
|
||||
{% endif -%}
|
||||
{% elif tx.info.type == 2 -%}
|
||||
<span class="icon" title="Service Node stake unlock — {{tx.extra.sn_pubkey}}">🔓</span>
|
||||
{% elif tx.info.type == 4 and 'lns' in tx.extra -%}
|
||||
{% if 'buy' in tx.extra.lns -%}
|
||||
<span class="icon" title="Loki Name Service Buying">🎫</span>
|
||||
{% elif 'update' in tx.extra.lns -%}
|
||||
<span class="icon" title="Loki Name Service Updating">💾</span>
|
||||
{% endif -%}
|
||||
{% elif 'sn_registration' in tx.extra -%}
|
||||
<span class="icon" title="Service Node registration ({{tx.extra.sn_registration.fee / 10000}}% fee)
|
||||
{{tx.extra.sn_pubkey}}
|
||||
{%-for c in tx.extra.sn_registration.contributors%}
|
||||
{{c.wallet | truncate(15)}} ({{c.portion / 10000}}% stake)
|
||||
{%-endfor%}">🏁</span>
|
||||
{% elif 'sn_contributor' in tx.extra -%}
|
||||
<span class="icon" title="Service Node contribution – {{tx.extra.sn_pubkey}} / {{tx.extra.sn_contributor}}">⚑</span>
|
||||
{% endif -%}
|
||||
{% endif -%}
|
||||
{% endmacro %}
|
197
templates/index.html
Normal file
197
templates/index.html
Normal file
|
@ -0,0 +1,197 @@
|
|||
{% extends "_basic.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="Wrapper">
|
||||
|
||||
<div class="nowrap-spans">
|
||||
<span>Server Time: {{ server.timestamp | format_datetime }}</span>
|
||||
<span>| <a href="/txpool">Transaction Pool</a></span>
|
||||
{% if config.pusher %}
|
||||
<span>| <a href="/rawtx">Transaction pusher </a></span>
|
||||
{% endif %}
|
||||
{% if config.key_image_checker %}
|
||||
<span>| <a href="/rawkeyimgs">Key images checker</a></span>
|
||||
{% endif %}
|
||||
{% if config.output_key_checker %}
|
||||
<span>| <a href="/rawoutputkeys">Output keys checker</a></span>
|
||||
{% endif %}
|
||||
{% if config.autorefresh_option %}
|
||||
<span>|
|
||||
{% if refresh %}
|
||||
<a href="/">Autorefresh is ON ({{refresh}} s)</a>
|
||||
{% else %}
|
||||
<a href="/autorefresh/10">Autorefresh is OFF</a>
|
||||
{% endif %}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if config.testnet_url and not info.testnet %}
|
||||
<span>| <a href="{{config.testnet_url}}">Go to testnet explorer</a></span>
|
||||
{% endif %}
|
||||
{% if config.devnet_url and not info.devnet %}
|
||||
<span>| <a href="{{config.devnet_url}}">Go to devnet explorer</a></span>
|
||||
{% endif %}
|
||||
{% if config.mainnet_url and not info.mainnet %}
|
||||
<span>| <a href="{{config.mainnet_url}}">Go to mainnet explorer</a></span>
|
||||
{% endif %}
|
||||
{% if info.testnet %}
|
||||
<span>| This is <span style="color:#ff6b62; font-weight: bold">TESTNET</span> blockchain</span>
|
||||
{% elif info.devnet %}
|
||||
<span>| This is <span style="color:#af5bd2; font-weight: bold">DEVNET</span> blockchain</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
|
||||
{% if info %}
|
||||
<h3 class="general_info nowrap-spans">
|
||||
<span>Hard fork: v{{hf.version}}</span>
|
||||
<span>| Network difficulty: {{info.difficulty}}</span>
|
||||
<span>| Hash rate: ~{{(info.difficulty / info.target) | si }}H/s</span>
|
||||
<span>| Staking requirement: {{stake.staking_requirement | loki}}</span>
|
||||
<span title="{{(2500 * fees.fee_per_byte + 2*fees.fee_per_output) | loki}} for a typical simple transaction (~2.5kB, 2 outputs)">|
|
||||
Base fee:
|
||||
{{fees.fee_per_output | loki}}/output + {{(fees.fee_per_byte * 1000) | loki}}/kB
|
||||
</span>
|
||||
<span title="{{(2500 * fees.blink_fee_per_byte + 2*fees.blink_fee_per_output) | loki}} for a typical simple blink transaction (~2.5kB, 2 outputs)">|
|
||||
Blink fee:
|
||||
{{fees.blink_fee_per_output | loki}}/output + {{(fees.blink_fee_per_byte * 1000) | loki}}/kB
|
||||
</span>
|
||||
<span title="{{(info.block_size_limit / 2) | si}}B soft limit, {{info.block_size_limit | si}}B hard limit. Blocks may include TXes up to the soft limit without penalty and incur increasing reward penalties as they approach the hard limit.">|
|
||||
Block size limit:
|
||||
{{(info.block_size_limit / 2) | si}}B/{{info.block_size_limit | si}}B
|
||||
</span>
|
||||
<span>| Blockchain size: {{info.database_size | si}}B</span>
|
||||
</h3>
|
||||
{% endif %}
|
||||
|
||||
<code>{{emission}}</code>
|
||||
{% if emission %}
|
||||
<h4 class="nowrap-spans">
|
||||
<span><span style="font-weight: bold">Circulating Supply*</span>:
|
||||
{% if emission.status == 'BUSY' %}
|
||||
(still calculating...)</span>
|
||||
{% elif emission.status == 'OK' %}
|
||||
{{(emission.emission_amount - emission.burn_amount) | loki}}</span>
|
||||
<span>(Coinbase: {{emission.emission_amount | loki}}</span>
|
||||
<span>| Fees: {{emission.fee_amount | loki}}</span>
|
||||
<span>| Burned: {{emission.burn_amount | loki}}).</span>
|
||||
{%endif%}
|
||||
<p style="padding: 0px; margin-top: 2px; font-size: 0.9em">
|
||||
* — Circulating supply may exclude any currently, publicised locked tokens, otherwise it is equal to the Coinbase minus burned coins.
|
||||
Fees includes paid transaction fees less any portion of the fee that was burned.
|
||||
</p>
|
||||
</h4>
|
||||
{% endif %}
|
||||
|
||||
|
||||
<h4 style="font-weight: bold; margin-bottom: 2px">TX Types Legend</h4>
|
||||
<h4 class="tx-type-legend nowrap-spans" style="margin-top: 0">
|
||||
<span>Service Node - Registration: 🏁</span>
|
||||
<span>| Contribution: ⚑</span>
|
||||
<span>| Recommission: 👍</span>
|
||||
<span>| Decommission: 👎</span>
|
||||
<span>| Deregister: 🚫</span>
|
||||
<span>| IP Change Penalty: 📋</span>
|
||||
<span>| Stake Unlock: 🔓</span>
|
||||
<span>| Loki Name System Buy: 🎫</span>
|
||||
<span>| LNS Update: 💾</span>
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
{% include 'include/mempool.html' %}
|
||||
|
||||
<div class="Wrapper">
|
||||
<h2>Transactions in
|
||||
{% if page == 0 %}
|
||||
the Last {{blocks|length}} Blocks
|
||||
{% else %}
|
||||
Blocks {{blocks[0].height}}–{{blocks[-1].height}}
|
||||
{% endif %}
|
||||
<a class="LinkNoUnderline" href="/range/{{blocks[0].height}}/{{blocks[-1].height}}" title="Permanent link to this block range">🔗</a>
|
||||
</h2>
|
||||
|
||||
{% set block_sizes = blocks | map(attribute='block_size') | sort %}
|
||||
{%if block_sizes|count > 0%}
|
||||
<h4 class="Subtitle">(Min. / Median / Average / Max. size of these blocks:
|
||||
{{block_sizes[0] | si}}B /
|
||||
{{(block_sizes[(block_sizes|count-1)//2]/2 + block_sizes[(block_sizes|count)//2]/2) | si}}B /
|
||||
{{(block_sizes|sum / block_sizes|count) | si}}B /
|
||||
{{block_sizes[-1] | si}}B)
|
||||
</h4>
|
||||
{%endif%}
|
||||
<div class="TitleDivider"></div>
|
||||
|
||||
{% include 'include/block_page_controls.html' %}
|
||||
|
||||
<table class="Table">
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Height</td>
|
||||
<td>Age [h:m:s]</td>
|
||||
<td>Size</td>
|
||||
<td>Type</td>
|
||||
<td>Transaction Hash</td>
|
||||
<td>Fee</td>
|
||||
<td>Outputs</td>
|
||||
<td>In/Out</td>
|
||||
<td>TX Size</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% import 'include/tx_type_symbol.html' as symbol %}
|
||||
{% import 'include/tx_fee.html' as fee %}
|
||||
{% for b in blocks | reverse %}
|
||||
<tr class="block">
|
||||
<td><a href="/block/{{b.height}}">{{b.height}}</a></td>
|
||||
<td title="{{b.timestamp | from_timestamp | format_datetime}}">{{b.timestamp | from_timestamp | ago}}</td>
|
||||
<td>{{b.block_size | si}}</td>
|
||||
<td>{{symbol.display(b.txs[0])}}</td>
|
||||
<td><a href="/tx/{{b.miner_tx_hash}}">{{b.miner_tx_hash}}</a></td>
|
||||
<td>{{fee.display(b.txs[0])}}</td>
|
||||
<td>{{b.reward | loki(tag=False, fixed=True, decimals=2)}}</td>
|
||||
<td>{{b.txs[0].info.vin | length}}/{{b.txs[0].info.vout | length}}</td>
|
||||
<td>{{b.txs[0].size | si}}</td>
|
||||
</tr>
|
||||
{% for tx in b.txs[1:] %}
|
||||
<tr class="tx">
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td>{{symbol.display(tx)}}</td>
|
||||
<td><a href="/tx/{{tx.tx_hash}}">{{tx.tx_hash}}</a></td>
|
||||
<td>{{fee.display(tx)}}</td>
|
||||
<td></td>
|
||||
<td>{{tx.info.vin | length}}/{{tx.info.vout | length}}</td>
|
||||
<td>{{tx.size | si}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{% include 'include/block_page_controls.html' %}
|
||||
</div>
|
||||
|
||||
{{service_node_summary}}
|
||||
<div class="Wrapper">
|
||||
<p> <a class="" href="/service_nodes">Click here to see the service node list (at most only 10 are shown here)</a> </p>
|
||||
</div>
|
||||
|
||||
{{quorum_state_summary}}
|
||||
<div class="Wrapper">
|
||||
<p> Note: The quorum shown here is the currently active voting height
|
||||
which is not necessarily the latest quorum. Quorums can only be voted on
|
||||
after a number of blocks have transpired.</p>
|
||||
<p> <a class="" href="/quorums">Click here to see the last 1hrs worth of the stored quorum states</a> </p>
|
||||
</div>
|
||||
|
||||
{% if show_cache_times %}
|
||||
<div class="center">
|
||||
<h6 style="margin-top: 1px;color:#949490">
|
||||
Tx details construction time: {{construction_time_total}} s
|
||||
<br/>
|
||||
includes {{construction_time_cached}} s from block cache ({{cache_hits}} hits)
|
||||
and {{construction_time_non_cached}} s from non cache ({{cache_misses}} misses)
|
||||
</h6>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
Loading…
Reference in a new issue