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:
Jason Rhinelander 2020-08-17 22:17:06 -03:00
commit 4b27018965
11 changed files with 855 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
mainnet.sock
testnet.sock
devnet.sock
pylokimq.cpython-*.so
__pycache__

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "pylokimq"]
path = pylokimq
url = https://github.com/majestrate/pylokimq.git

242
app.py Normal file
View 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

@ -0,0 +1 @@
Subproject commit 32de27c2fe8ff9ccb86031afaf900734ea16508a

233
static/style.css Normal file
View 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
View 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>

View 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>

View 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 -->

View 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 %}

View 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
View 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 %}