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:
Jason Rhinelander 2020-09-29 01:36:13 -03:00
parent a7cf7f8769
commit 33b362db96
3 changed files with 218 additions and 121 deletions

View File

@ -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']:

View File

@ -353,6 +353,10 @@ span.icon {
cursor: help;
}
.comment {
font-style: italic;
}
.syntax-highlight>pre {
font-size: 125%;
overflow-x: auto;

View File

@ -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 &gt; 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">