summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJason Rhinelander <jason@imaginary.ca>2020-09-29 01:36:13 -0300
committerJason Rhinelander <jason@imaginary.ca>2020-09-29 01:36:13 -0300
commit33b362db96722fd659618314158cd1ea23292a39 (patch)
tree62daec696c3babaef6ec206dc191798fb4c27c24
parenta7cf7f876965d9e3fb9ea77e93cff8a849563f98 (diff)
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
-rw-r--r--observer.py192
-rw-r--r--static/style.css4
-rw-r--r--templates/block.html141
3 files changed, 217 insertions, 120 deletions
diff --git a/observer.py b/observer.py
index 11c9b61..d47ed5a 100644
--- a/observer.py
+++ b/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']:
diff --git a/static/style.css b/static/style.css
index 31b6761..5340fdf 100644
--- a/static/style.css
+++ b/static/style.css
@@ -353,6 +353,10 @@ span.icon {
cursor: help;
}
+.comment {
+ font-style: italic;
+}
+
.syntax-highlight>pre {
font-size: 125%;
overflow-x: auto;
diff --git a/templates/block.html b/templates/block.html
index 94a360a..fbc6dcc 100644
--- a/templates/block.html
+++ b/templates/block.html
@@ -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 %}
+ <h2>Metadata</h2>
+ <div class="TitleUnderliner"></div>
- {% if next_block %}
- <p>Next Block: <a href="/block/{{next_block.block_header.hash}}">{{next_block.block_header.hash}}</a></p>
- {% endif %}
+ <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>
- <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>
+ <span><label>Major.minor version:</label> {{block_header.major_version}}.{{block_header.minor_version}}</span>
- <h2>Miner Reward Transaction</h3>
-<div class="TitleDivider"></div>
+ <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">