Spruce up block display; DRY tx fetching code
- Make block metadata use the same style as other new pages - Abstract tx fetching code - make /block/latest redirect to top height - Add "Show raw details" for blocks - Make searching by block height properly redirect
This commit is contained in:
parent
a7cf7f8769
commit
33b362db96
192
observer.py
192
observer.py
|
@ -144,6 +144,13 @@ def base32z(hex):
|
|||
b'ybndrfg8ejkmcpqxot1uwisza345h769')).decode().rstrip('=')
|
||||
|
||||
|
||||
@app.template_filter('ellipsize')
|
||||
def ellipsize(string, leading=10, trailing=5):
|
||||
if len(string) <= leading + trailing + 3:
|
||||
return string
|
||||
return string[0:leading] + "..." + ('' if not trailing else string[-trailing:])
|
||||
|
||||
|
||||
@app.after_request
|
||||
def add_global_headers(response):
|
||||
if 'Cache-Control' not in response.headers:
|
||||
|
@ -259,13 +266,7 @@ def main(refresh=None, page=0, per_page=None, first=None, last=None):
|
|||
txids.append(b['miner_tx_hash'])
|
||||
if 'tx_hashes' in b:
|
||||
txids += b['tx_hashes']
|
||||
txs = FutureJSON(lmq, lokid, 'rpc.get_transactions', args={
|
||||
"txs_hashes": txids,
|
||||
"decode_as_json": True,
|
||||
"tx_extra": True,
|
||||
"prune": True,
|
||||
}).get()
|
||||
txs = txs['txs']
|
||||
txs = parse_txs(tx_req(lmq, lokid, txids, cache_key='mempool').get())
|
||||
i = 0
|
||||
for tx in txs:
|
||||
# TXs should come back in the same order so we can just skip ahead one when the block
|
||||
|
@ -278,7 +279,6 @@ def main(refresh=None, page=0, per_page=None, first=None, last=None):
|
|||
if i >= len(blocks):
|
||||
print("Something getting wrong: have leftover txes")
|
||||
break
|
||||
tx['info'] = json.loads(tx['as_json'])
|
||||
blocks[i]['txs'].append(tx)
|
||||
|
||||
|
||||
|
@ -327,10 +327,10 @@ def sns():
|
|||
inactive_sns=inactive,
|
||||
)
|
||||
|
||||
def tx_req(lmq, lokid, txid, **kwargs):
|
||||
return FutureJSON(lmq, lokid, 'rpc.get_transactions', cache_seconds=10, cache_key='single',
|
||||
def tx_req(lmq, lokid, txids, cache_key='single', **kwargs):
|
||||
return FutureJSON(lmq, lokid, 'rpc.get_transactions', cache_seconds=10, cache_key=cache_key,
|
||||
args={
|
||||
"txs_hashes": [txid],
|
||||
"txs_hashes": txids,
|
||||
"decode_as_json": True,
|
||||
"tx_extra": True,
|
||||
"prune": True,
|
||||
|
@ -342,22 +342,25 @@ def sn_req(lmq, lokid, pubkey, **kwargs):
|
|||
args={"service_node_pubkeys": [pubkey]}, **kwargs
|
||||
)
|
||||
|
||||
def block_req(lmq, lokid, hash_or_height, **kwargs):
|
||||
if len(hash_or_height) <= 10 and hash_or_height.isdigit():
|
||||
|
||||
def block_header_req(lmq, lokid, hash_or_height, **kwargs):
|
||||
if isinstance(hash_or_height, int) or (len(hash_or_height) <= 10 and hash_or_height.isdigit()):
|
||||
return FutureJSON(lmq, lokid, 'rpc.get_block_header_by_height', cache_key='single',
|
||||
args={ "height": int(hash_or_height) }, **kwargs)
|
||||
else:
|
||||
return FutureJSON(lmq, lokid, 'rpc.get_block_header_by_hash', cache_key='single',
|
||||
args={ 'hash': hash_or_height }, **kwargs)
|
||||
|
||||
def get_block_with_txs(lmq, lokid, hash_or_height, **kwargs):
|
||||
if len(hash_or_height) <= 10 and hash_or_height.isdigit():
|
||||
return FutureJSON(lmq, lokid, 'rpc.get_block', cache_key='single',
|
||||
args={ "height": int(hash_or_height), 'get_tx_hashes': True }, **kwargs)
|
||||
|
||||
def block_with_txs_req(lmq, lokid, hash_or_height, **kwargs):
|
||||
args = { 'get_tx_hashes': True }
|
||||
if isinstance(hash_or_height, int) or (len(hash_or_height) <= 10 and hash_or_height.isdigit()):
|
||||
args['height'] = int(hash_or_height)
|
||||
else:
|
||||
return FutureJSON(lmq, lokid, 'rpc.get_block', cache_key='single',
|
||||
args={ 'hash': hash_or_height, 'get_tx_hashes': True }, **kwargs)
|
||||
|
||||
args['hash'] = hash_or_height
|
||||
|
||||
return FutureJSON(lmq, lokid, 'rpc.get_block', cache_key='single', args=args, **kwargs)
|
||||
|
||||
|
||||
@app.route('/service_node/<hex64:pubkey>') # For backwards compatibility with old explorer URLs
|
||||
@app.route('/sn/<hex64:pubkey>')
|
||||
|
@ -390,66 +393,106 @@ def show_sn(pubkey):
|
|||
sn=sn,
|
||||
)
|
||||
|
||||
def parse_txs(txs_rpc):
|
||||
"""Takes a tx_req(...).get() response and parses the embedded nested json into something useful
|
||||
|
||||
@app.route("/block/<val>")
|
||||
def show_block(val):
|
||||
""" """
|
||||
This modifies the txs_rpc['txs'] values in-place. Returns txs_rpc['txs'] if it exists, otherwise an empty list.
|
||||
"""
|
||||
if 'txs' not in txs_rpc:
|
||||
return []
|
||||
|
||||
for tx in txs_rpc['txs']:
|
||||
if 'info' not in tx:
|
||||
# We have serialized JSON data inside a field in the JSON, because of lokid's
|
||||
# multiple incompatible JSON generators 🤮:
|
||||
tx['info'] = json.loads(tx["as_json"])
|
||||
del tx['as_json']
|
||||
# The "extra" field inside as_json is retardedly in per-byte integer values,
|
||||
# convert it to a hex string 🤮:
|
||||
tx['info']['extra'] = bytes_to_hex(tx['info']['extra'])
|
||||
return txs_rpc['txs']
|
||||
|
||||
|
||||
@app.route('/block/<int:height>')
|
||||
@app.route('/block/<int:height>/<int:more_details>')
|
||||
@app.route('/block/<hex64:hash>')
|
||||
@app.route('/block/<hex64:hash>/<int:more_details>')
|
||||
def show_block(height=None, hash=None, more_details=False):
|
||||
lmq, lokid = lmq_connection()
|
||||
block = get_block_with_txs(lmq, lokid, val).get()
|
||||
info = FutureJSON(lmq, lokid, 'rpc.get_info', 1).get()
|
||||
info = FutureJSON(lmq, lokid, 'rpc.get_info', 1)
|
||||
hfinfo = FutureJSON(lmq, lokid, 'rpc.hard_fork_info', 10)
|
||||
if height is not None:
|
||||
val = height
|
||||
elif hash is not None:
|
||||
val = hash
|
||||
|
||||
block = None if val is None else block_with_txs_req(lmq, lokid, val).get()
|
||||
if block is None:
|
||||
return flask.render_template("not_found.html",
|
||||
info=info,
|
||||
hfinfo=hfinfo.get(),
|
||||
type='block')
|
||||
info=info.get(),
|
||||
hfinfo=hfinfo.get(),
|
||||
type='block',
|
||||
height=height,
|
||||
id=hash
|
||||
)
|
||||
|
||||
next_block = None
|
||||
block_height = block['block_header']['height']
|
||||
txs = None
|
||||
hashes = []
|
||||
if 'tx_hashes' in block:
|
||||
hashes += block['tx_hashes']
|
||||
hashes.append(block['block_header']['miner_tx_hash'])
|
||||
if 'info' not in block:
|
||||
try:
|
||||
block['info'] = json.loads(block["json"])
|
||||
del block['info']['miner_tx'] # Doesn't include enough for us, we fetch it separately with extra interpretation instead
|
||||
del block["json"]
|
||||
except Exception as e:
|
||||
print("Something getting wrong: cannot parse block json for block {}: {}".format(block_height, e), file=sys.stderr)
|
||||
|
||||
txs = tx_req(lmq, lokid, hashes, cache_key='block')
|
||||
|
||||
if info.get()['height'] > 1 + block_height:
|
||||
next_block = block_header_req(lmq, lokid, '{}'.format(block_height + 1))
|
||||
|
||||
if more_details:
|
||||
formatter = HtmlFormatter(cssclass="syntax-highlight", style="native")
|
||||
more_details = {
|
||||
'details_css': formatter.get_style_defs('.syntax-highlight'),
|
||||
'details_html': highlight(json.dumps(block, indent="\t", sort_keys=True), JsonLexer(), formatter),
|
||||
}
|
||||
else:
|
||||
next_block = None
|
||||
block_height = block['block_header']['height']
|
||||
transactions = []
|
||||
miner_txs = []
|
||||
if block and 'tx_hashes' in block:
|
||||
hashes = block['tx_hashes']
|
||||
if 'info' not in block:
|
||||
try:
|
||||
block['info'] = json.loads(block["json"])
|
||||
del block["json"]
|
||||
except:
|
||||
pass
|
||||
if 'info' in block:
|
||||
hashes += block['info']['miner_tx']
|
||||
txs = FutureJSON(lmq, lokid, 'rpc.get_transactions', args={
|
||||
"txs_hashes": hashes,
|
||||
"tx_extra": True,
|
||||
"decode_as_json": True,
|
||||
"prune": True
|
||||
}).get()
|
||||
if 'txs' in txs:
|
||||
for tx in txs['txs']:
|
||||
if 'info' not in tx:
|
||||
tx['info'] = json.loads(tx["as_json"])
|
||||
del tx["as_json"]
|
||||
if 'extra' in tx['info']:
|
||||
tx['info']['extra'] = bytes_to_hex(tx['info']['extra'])
|
||||
transactions.append(tx)
|
||||
if info['height'] > 1 + block_height:
|
||||
next_block = block_req(lmq, lokid, '{}'.format(block_height + 1)).get()
|
||||
return flask.render_template("block.html",
|
||||
info=info,
|
||||
hfinfo=hfinfo.get(),
|
||||
block_header=block['block_header'],
|
||||
block=block,
|
||||
transactions=transactions,
|
||||
next_block=next_block)
|
||||
more_details = {}
|
||||
|
||||
transactions = [] if txs is None else parse_txs(txs.get()).copy()
|
||||
miner_tx = transactions.pop() if transactions else []
|
||||
|
||||
return flask.render_template("block.html",
|
||||
info=info.get(),
|
||||
hfinfo=hfinfo.get(),
|
||||
block_header=block['block_header'],
|
||||
block=block,
|
||||
miner_tx=miner_tx,
|
||||
transactions=transactions,
|
||||
next_block=next_block.get() if next_block else None,
|
||||
**more_details,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@app.route('/block/latest')
|
||||
def show_block_latest():
|
||||
lmq, lokid = lmq_connection()
|
||||
height = FutureJSON(lmq, lokid, 'rpc.get_info', 1).get()['height'] - 1
|
||||
return flask.redirect(flask.url_for('show_block', height=height), code=302)
|
||||
|
||||
|
||||
@app.route('/tx/<hex64:txid>')
|
||||
@app.route('/tx/<hex64:txid>/<int:more_details>')
|
||||
def show_tx(txid, more_details=False):
|
||||
lmq, lokid = lmq_connection()
|
||||
info = FutureJSON(lmq, lokid, 'rpc.get_info', 1)
|
||||
txs = tx_req(lmq, lokid, txid).get()
|
||||
txs = tx_req(lmq, lokid, [txid]).get()
|
||||
|
||||
if 'txs' not in txs or not txs['txs']:
|
||||
return flask.render_template('not_found.html',
|
||||
|
@ -457,13 +500,7 @@ def show_tx(txid, more_details=False):
|
|||
type='tx',
|
||||
id=txid,
|
||||
)
|
||||
tx = txs['txs'][0]
|
||||
if 'info' not in tx:
|
||||
tx['info'] = json.loads(tx["as_json"])
|
||||
del tx["as_json"]
|
||||
|
||||
# The "extra" field is retardedly in per-byte values, convert it to a hex string:
|
||||
tx['info']['extra'] = bytes_to_hex(tx['info']['extra'])
|
||||
tx = parse_txs(txs)[0]
|
||||
|
||||
kindex_info = {} # { amount => { keyindex => {output-info} } }
|
||||
block_info_req = None
|
||||
|
@ -538,7 +575,8 @@ def search():
|
|||
val = (flask.request.args.get('value') or '').strip()
|
||||
|
||||
if val and len(val) < 10 and val.isdigit(): # Block height
|
||||
return show_block(val)
|
||||
return flask.redirect(flask.url_for('show_block', height=val), code=301)
|
||||
|
||||
if not val or len(val) != 64 or any(c not in string.hexdigits for c in val):
|
||||
return flask.render_template('not_found.html',
|
||||
info=info.get(),
|
||||
|
@ -548,8 +586,8 @@ def search():
|
|||
|
||||
# Initiate all the lookups at once, then redirect to whichever one responds affirmatively
|
||||
snreq = sn_req(lmq, lokid, val)
|
||||
blreq = block_req(lmq, lokid, val, fail_okay=True)
|
||||
txreq = tx_req(lmq, lokid, val)
|
||||
blreq = block_header_req(lmq, lokid, val, fail_okay=True)
|
||||
txreq = tx_req(lmq, lokid, [val])
|
||||
|
||||
sn = snreq.get()
|
||||
if 'service_node_states' in sn and sn['service_node_states']:
|
||||
|
|
|
@ -353,6 +353,10 @@ span.icon {
|
|||
cursor: help;
|
||||
}
|
||||
|
||||
.comment {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.syntax-highlight>pre {
|
||||
font-size: 125%;
|
||||
overflow-x: auto;
|
||||
|
|
|
@ -2,43 +2,89 @@
|
|||
|
||||
{% block content %}
|
||||
<div class="Wrapper">
|
||||
<h4>Block Hash (height): {{block_header.hash}} ({{block_header.height}})</h4>
|
||||
<h4 style="margin:5px"><label>Block Hash:</label> {{block_header.hash}}</h4>
|
||||
<h4 style="margin:5px"><label>Block Height:</label> {{block_header.height}}
|
||||
{%if block_header.height < info.height - 1%}
|
||||
<span class="comment">({{info.height - 1 - block_header.height}} blocks ago)</span>
|
||||
{%elif block_header.height == info.height - 1%}
|
||||
<span class="comment">(current top block)</span>
|
||||
{%endif%}
|
||||
</h4>
|
||||
<h4 style="margin:5px" class="prev_next_block">
|
||||
{%if block_header.height > 0%}
|
||||
<a href="/block/{{block_header.height-1}}{%if details_html%}/1{%endif%}">« Block {{block_header.height-1}} <span class="comment">({{block_header.prev_hash | ellipsize(8,3)}})</span></a>
|
||||
{%endif%}
|
||||
{%if block_header.height < info.height - 1%}
|
||||
{%if block_header.height > 0%} | {%endif%}
|
||||
<a href="/block/{{block_header.height+1}}{%if details_html%}/1{%endif%}">Block {{block_header.height-1}} <span class="comment">({{block_header.prev_hash | ellipsize(10,3)}})</span> »</a>
|
||||
|
|
||||
<a href="/block/latest">Latest block ⏭</a>
|
||||
{%endif%}
|
||||
</h4>
|
||||
|
||||
|
||||
{% if block_header.prev_hash %}
|
||||
<p>Previous Block: <a href="/block/{{block_header.prev_hash}}">{{block_header.prev_hash}}</a></p>
|
||||
{% endif %}
|
||||
|
||||
{% if next_block %}
|
||||
<p>Next Block: <a href="/block/{{next_block.block_header.hash}}">{{next_block.block_header.hash}}</a></p>
|
||||
{% endif %}
|
||||
|
||||
<h2>Metadata</h2>
|
||||
<div class="TitleDivider"></div>
|
||||
<table class="Table">
|
||||
<tr>
|
||||
<td>Timestamp [UCT] (epoch):</td><td>{{block_header.timestamp | from_timestamp | format_datetime}} ({{block_header.timestamp}})</td>
|
||||
<!-- <td>Age:</td><td></td> -->
|
||||
<td>Age [h:m:s]:</td><td>{{ block_header.timestamp | from_timestamp | ago }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Major.Minor Version:</td><td>{{block_header.major_version}}.{{block_header.minor_version}}</td>
|
||||
<td>Block Reward:</td><td>{{block_header.reward | loki }}</td>
|
||||
<td>Block Size [kB]:</td><td>{{block_header.block_size | si}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Nonce:</td><td>{{block_header.nonce}}</td>
|
||||
<td>Total Fees:</td><td>{{sum_fees}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Service Node Winner:</td><td><a href="/service_node/{{block_header.service_node_winner}}">{{block_header.service_node_winner}}</a></td>
|
||||
<td>Cumulative Difficulty:</td><td>{{block_header.cumulative_difficulty}}</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="TitleUnderliner"></div>
|
||||
|
||||
<h2>Miner Reward Transaction</h3>
|
||||
<div class="TitleDivider"></div>
|
||||
<h4 class="info_list nowrap-spans">
|
||||
<span title="Unix timestamp: {{block_header.timestamp}}"><label>Timestamp:</label> {{block_header.timestamp | from_timestamp | format_datetime('short')}} UTC
|
||||
({{block_header.timestamp | from_timestamp | ago}} ago)</span>
|
||||
|
||||
<span><label>Major.minor version:</label> {{block_header.major_version}}.{{block_header.minor_version}}</span>
|
||||
|
||||
<span><label>Block size:</label> {{block_header.block_size | si}}B</span>
|
||||
|
||||
{%if 'nonce' in block_header and block_header['nonce'] != 0%}
|
||||
<span title="Random value added by a miner to achieve sufficient block difficulty"><label>Miner nonce:</label> {{block_header.nonce}}</span>
|
||||
<span title="~ {{(block_header.difficulty / 120) | si }}H/s network hashrate
|
||||
|
||||
Cumulative difficulty {{block_header.cumulative_difficulty}}"><label>Difficulty:</label> {{block_header.difficulty}}</span>
|
||||
{%elif 'pulse' in block_header.info%}
|
||||
{%if block_header.info.pulse.round > 0%}
|
||||
<span title="When > 0 this indicates how many Service Node rounds failed to produce a valid, signed Pulse block in time">
|
||||
<title>Pulse round:</title> {{block_header.info.pulse.round}}
|
||||
</span>
|
||||
{%endif%}
|
||||
<span title="Random value produced by the Pulse round that provide entropy for the blockchain
|
||||
|
||||
Pulse participation bits: {{":011b".format(block_header.info.pulse.validator_bitset)}}">
|
||||
<label>Pulse random value:</label>
|
||||
{{block_header.info.pulse.random_value}}
|
||||
</span>
|
||||
{%endif%}
|
||||
|
||||
{%set sum_burned = transactions | selectattr('extra.burn_amount') | sum(attribute='extra.burn_amount') %}
|
||||
{%set sum_fees = transactions | selectattr('info.rct_signatures') | selectattr('info.rct_signatures.txnFee') | sum(attribute='info.rct_signatures.txnFee') - sum_burned%}
|
||||
|
||||
<span title="{{(block_header.reward - sum_fees) | loki(fixed=True)}} created in this block.{%if sum_fees > 0%}
|
||||
|
||||
Note that this value does not include earned transaction fees ({{sum_fees | loki(fixed=True, decimals=4)}}){%endif%}"><label>Block reward:</label>
|
||||
{{(block_header.reward - sum_fees) | loki(decimals=4)}}</span>
|
||||
|
||||
{%if sum_fees > 0%}
|
||||
<span title="Earned TX fees: {{sum_fees | loki(fixed=True)}}"><label>Block TX fees:</label> {{ sum_fees | loki(fixed=True, decimals=4) }}</span>
|
||||
{%endif%}
|
||||
|
||||
{%if sum_burned > 0%}
|
||||
<span title="{{sum_burned | loki(fixed=True)}} burned in the transactions included in block">
|
||||
<label>Burned fees:</label>
|
||||
{{sum_burned | loki(decimals=4)}} <span class="icon">🔥</span>
|
||||
</span>
|
||||
{%endif%}
|
||||
|
||||
{%if miner_tx.extra.sn_winner%}
|
||||
<span><label>Service Node Winner:</label>
|
||||
{%if miner_tx.extra.sn_winner == "0000000000000000000000000000000000000000000000000000000000000000"%}
|
||||
None
|
||||
{%else%}
|
||||
<a href="/sn/{{miner_tx.extra.sn_winner}}">{{miner_tx.extra.sn_winner}}</a>
|
||||
{%endif%}
|
||||
</span>
|
||||
{%endif%}
|
||||
</h4>
|
||||
|
||||
<h2>Miner Reward Transaction</h2>
|
||||
<div class="TitleUnderliner"></div>
|
||||
<table class="Table">
|
||||
<tr class="TableHeader">
|
||||
<td>Hash</td>
|
||||
|
@ -46,20 +92,18 @@
|
|||
<td>Size [kB]</td>
|
||||
<td>Version</td>
|
||||
</tr>
|
||||
{% for tx in miner_tx %}
|
||||
<tr>
|
||||
<td><a href="/tx/{{tx.hash}}">{{tx.hash}}</a>
|
||||
<td>{{tx.sum_outputs}}</td>
|
||||
<td>{{tx.tx_size}}</td>
|
||||
<td>{{tx.version}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
<tr>
|
||||
<td><a href="/tx/{{miner_tx.tx_hash}}">{{miner_tx.tx_hash}}</a></td>
|
||||
<td>{{miner_tx.info.vout | sum(attribute='amount') | loki}}</td>
|
||||
<td>{{miner_tx.size}}</td>
|
||||
<td>{{miner_tx.info.version}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
<h2>Transactions ({{transactions | length}})</h2>
|
||||
<div class="TitleDivider"></div>
|
||||
{% if not transactions.empty %}
|
||||
<div class="TitleUnderliner"></div>
|
||||
{% if transactions %}
|
||||
<table class="Table">
|
||||
<thead>
|
||||
<tr>
|
||||
|
@ -81,13 +125,24 @@
|
|||
<td>{{fee.display(tx)}}</td>
|
||||
<td></td>
|
||||
<td>{{tx.info.vin | length}}/{{tx.info.vout | length}}</td>
|
||||
<td>{{tx.size | si}}</td>
|
||||
<td>{{tx.size | si}}B</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
|
||||
{%if details_html%}
|
||||
<style type="text/css">
|
||||
{{details_css | safe}}
|
||||
</style>
|
||||
<div class="TitleDivider" id="more_details"></div>
|
||||
{{details_html | safe}}
|
||||
{%else%}
|
||||
<h5>
|
||||
<a href="/block/{{block_header.hash}}/1#more_details">Show raw details</a>
|
||||
</h5>
|
||||
{%endif%}
|
||||
|
||||
{% if enable_as_hex %}
|
||||
<h5 style="margin-top:1px">
|
||||
|
|
Loading…
Reference in New Issue