Add tx details page

Plus various improvements around how small metadata is displayed.
This commit is contained in:
Jason Rhinelander 2020-08-30 13:32:17 -03:00
parent 2c394386e3
commit bdf9056c2a
12 changed files with 927 additions and 84 deletions

View File

@ -15,7 +15,7 @@ Quick and dirty setup instructions for now:
make -j6
cd ../..
ln -s pylokimq/build/pylokimq/pylokimq.cpython-*.so .
sudo apt install python3-flask python3-babel
sudo apt install python3-flask python3-babel python3-pygments
(Note that we require a very recent python3-jinja package (2.11+), which may not be installed by the
above.)

View File

@ -9,10 +9,11 @@ blocks_per_page=20
max_blocks_per_page=100
# Some display and/or feature options:
pusher=True
key_image_checker=True
output_key_checker=True
pusher=False
key_image_checker=False
output_key_checker=False
autorefresh_option=True
enable_mixins_details=True
# URLs to networks other than the one we are on:
mainnet_url='https://blocks.lokinet.dev'

View File

@ -1,11 +1,15 @@
#!/usr/bin/env python3
import flask
from datetime import datetime
from datetime import datetime, timedelta
import babel.dates
import json
import sys
import statistics
from werkzeug.routing import BaseConverter
from pygments import highlight
from pygments.lexers import JsonLexer
from pygments.formatters import HtmlFormatter
import config
from lmq import FutureJSON, lmq_connection
@ -19,6 +23,14 @@ if __name__ == '__main__':
app.config['TEMPLATES_AUTO_RELOAD'] = True
app.jinja_env.auto_reload = True
class Hex64Converter(BaseConverter):
def __init__(self, url_map):
super().__init__(url_map)
self.regex = "[0-9a-fA-F]{64}"
app.url_map.converters['hex64'] = Hex64Converter
@app.template_filter('format_datetime')
def format_datetime(value, format='long'):
return babel.dates.format_datetime(value, format, tzinfo=babel.dates.get_timezone('UTC'))
@ -36,20 +48,32 @@ def datetime_ago(value):
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)
disp += '{:d}:{:02d}:{:02d}'.format(delta.seconds // 3600, delta.seconds // 60 % 60, delta.seconds % 60)
return disp
@app.template_filter('reltime')
def relative_time(seconds):
def relative_time(seconds, two_part=False, in_ago=True, neg_is_now=False):
if isinstance(seconds, timedelta):
seconds = seconds.seconds + 86400*seconds.days
ago = False
if seconds == 0:
if seconds == 0 or (neg_is_now and seconds < 0):
return 'now'
elif seconds < 0:
seconds = -seconds
ago = True
if seconds < 90:
if two_part:
if seconds < 3600:
delta = '{:.0f} minutes {:.0f} seconds'.format(seconds//60, seconds%60//1)
elif seconds < 24 * 3600:
delta = '{:.0f} hours {:.1f} minutes'.format(seconds//3600, seconds%3600/60)
elif seconds < 10 * 86400:
delta = '{:.0f} days {:.1f} hours'.format(seconds//86400, seconds%86400/3600)
else:
delta = '{:.1f} days'.format(seconds / 86400)
elif seconds < 90:
delta = '{:.0f} seconds'.format(seconds)
elif seconds < 90 * 60:
delta = '{:.1f} minutes'.format(seconds / 60)
@ -60,7 +84,7 @@ def relative_time(seconds):
else:
delta = '{:.0f} days'.format(seconds / 86400)
return delta + ' ago' if ago else 'in ' + delta
return delta if not in_ago else delta + ' ago' if ago else 'in ' + delta
@app.template_filter('roundish')
@ -84,19 +108,29 @@ def format_si(value):
return filter_round(value) + '{}'.format(si_suffix[i])
@app.template_filter('loki')
def format_loki(atomic, tag=True, fixed=False, decimals=9):
def format_loki(atomic, tag=True, fixed=False, decimals=9, zero=None):
"""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
fixed - if specified, replace 0 with this string
"""
disp = "{{:.{}f}}".format(decimals).format(atomic * 1e-9)
if not fixed and decimals > 0:
disp = disp.rstrip('0').rstrip('.')
if atomic == 0 and zero:
disp = zero
else:
disp = "{{:.{}f}}".format(decimals).format(atomic * 1e-9)
if not fixed and decimals > 0:
disp = disp.rstrip('0').rstrip('.')
if tag:
disp += ' LOKI'
return disp
# For some inexplicable reason some hex fields are provided as array of byte integer values rather
# than hex. This converts such a monstrosity to hex.
@app.template_filter('bytes_to_hex')
def bytes_to_hex(b):
return "".join("{:02x}".format(x) for x in b)
@app.after_request
def add_global_headers(response):
if 'Cache-Control' not in response.headers:
@ -110,13 +144,13 @@ def css():
def get_sns_future(lmq, lokid):
return FutureJSON(lmq, lokid, 'rpc.get_service_nodes', 5,
args=[json.dumps({
args={
'all': False,
'fields': { x: True for x in ('service_node_pubkey', 'requested_unlock_height', 'last_reward_block_height',
'last_reward_transaction_index', 'active', 'funded', 'earned_downtime_blocks',
'service_node_version', 'contributors', 'total_contributed', 'total_reserved',
'staking_requirement', 'portions_for_operator', 'operator_address', 'pubkey_ed25519',
'last_uptime_proof', 'service_node_version') } }).encode()])
'last_uptime_proof', 'service_node_version') } })
def get_sns(sns_future, info_future):
info = info_future.get()
@ -140,7 +174,7 @@ def get_sns(sns_future, info_future):
def template_globals():
return {
'config': conf,
'server': { 'timestamp': datetime.utcnow() }
'server': { 'datetime': datetime.utcnow() }
}
@ -155,14 +189,14 @@ def main(refresh=None, page=0, per_page=None, first=None, last=None):
stake = FutureJSON(lmq, lokid, 'rpc.get_staking_requirement', 10)
base_fee = FutureJSON(lmq, lokid, 'rpc.get_fee_estimate', 10)
hfinfo = FutureJSON(lmq, lokid, 'rpc.hard_fork_info', 10)
mempool = FutureJSON(lmq, lokid, 'rpc.get_transaction_pool', 5, args=[json.dumps({"tx_extra":True}).encode()])
mempool = FutureJSON(lmq, lokid, 'rpc.get_transaction_pool', 5, args={"tx_extra":True})
sns = get_sns_future(lmq, lokid)
# 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(lmq, lokid, 'admin.get_coinbase_tx_sum', 10, timeout=1, fail_okay=True,
args=[json.dumps({"height":0, "count":2**31-1}).encode()])
args={"height":0, "count":2**31-1})
custom_per_page = ''
if per_page is None or per_page <= 0 or per_page > config.max_blocks_per_page:
@ -190,11 +224,11 @@ def main(refresh=None, page=0, per_page=None, first=None, last=None):
end_height = max(0, height - per_page*page - 1)
start_height = max(0, end_height - per_page + 1)
blocks = FutureJSON(lmq, lokid, 'rpc.get_block_headers_range', args=[json.dumps({
blocks = FutureJSON(lmq, lokid, 'rpc.get_block_headers_range', args={
'start_height': start_height,
'end_height': end_height,
'get_tx_hashes': True,
}).encode()]).get()['headers']
}).get()['headers']
# If 'txs' is already there then it is probably left over from our cached previous call through
# here.
@ -205,12 +239,12 @@ 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=[json.dumps({
txs = FutureJSON(lmq, lokid, 'rpc.get_transactions', args={
"txs_hashes": txids,
"decode_as_json": True,
"tx_extra": True,
"prune": True,
}).encode()]).get()
}).get()
txs = txs['txs']
i = 0
for tx in txs:
@ -276,3 +310,84 @@ def sns():
inactive_sns=inactive,
**template_globals(),
)
@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 = FutureJSON(lmq, lokid, 'rpc.get_transactions', cache_seconds=10, args={
"txs_hashes": [txid],
"decode_as_json": True,
"tx_extra": True,
"prune": True,
}).get()
if 'txs' not in txs or not txs['txs']:
return flask.render_template('not_found.html',
info=info.get(),
type='tx',
id=txid,
**template_globals(),
)
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'])
koffset_info = {} # { amount => { keyoffset => {output-info} } }
block_info_req = None
if 'vin' in tx['info']:
if len(tx['info']['vin']) == 1 and 'gen' in tx['info']['vin'][0]:
tx['coinbase'] = True
elif tx['info']['vin'] and config.enable_mixins_details:
# Load output details for all outputs contained in the inputs
outs_req = [{"amount":inp['key']['amount'], "index":koff} for inp in tx['info']['vin'] for koff in inp['key']['key_offsets']]
outputs = FutureJSON(lmq, lokid, 'rpc.get_outs', args={
'get_txid': True,
'outputs': outs_req,
}).get()
if outputs and 'outs' in outputs and len(outputs['outs']) == len(outs_req):
outputs = outputs['outs']
# Also load block details for all of those outputs:
block_info_req = FutureJSON(lmq, lokid, 'rpc.get_block_header_by_height', args={
'heights': [o["height"] for o in outputs]
})
i = 0
for inp in tx['info']['vin']:
amount = inp['key']['amount']
if amount not in koffset_info:
koffset_info[amount] = {}
ki = koffset_info[amount]
for ko in inp['key']['key_offsets']:
ki[ko] = outputs[i]
i += 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(tx, indent="\t", sort_keys=True), JsonLexer(), formatter),
}
else:
more_details = {}
block_info = {} # { height => {block-info} }
if block_info_req:
bi = block_info_req.get()
if 'block_headers' in bi:
for bh in bi['block_headers']:
block_info[bh['height']] = bh
return flask.render_template('tx.html',
info=info.get(),
tx=tx,
koffset_info=koffset_info,
block_info=block_info,
**more_details,
**template_globals(),
)

View File

@ -18,6 +18,18 @@ h1, h2, h3, h4, h5, h6 {
margin-top: 5px;
margin-bottom: 3px;
}
.info_list>span:not(:first-child):before {
content: " | ";
}
.info_list>span {
font-weight: bold;
}
.info_list>span>label {
font-weight: normal;
}
p>label, span>label, h3>label, h4>label, td>label, .info-item>label {
color: #30a532;
}
.nowrap-spans {
text-indent: -2em;
padding-left: 2em;
@ -25,6 +37,18 @@ h1, h2, h3, h4, h5, h6 {
.nowrap-spans>span {
white-space: nowrap;
}
.info-item:before {
content: " | ";
white-space: nowrap;
}
.info-item {
word-wrap: break-word;
padding-left: 6em;
text-indent: -4em;
}
.info-item>label {
white-space: nowrap;
}
.Subtitle {
font-size: 1.0em;
@ -92,12 +116,18 @@ table thead tr,
background-color: #008522;
}
.TitleDivider {
.TitleUnderliner {
height: 2px;
width: 100%;
background-color: #008522;
margin-bottom: 1em;
}
.TitleDivider {
height: 2px;
width: 100%;
background-color: #008522;
margin: 1em 0;
}
.LinkNoUnderline {
text-decoration: none !important;
@ -290,3 +320,18 @@ h3 .sn-count {
span.icon {
cursor: help;
}
.syntax-highlight>pre {
font-size: 125%;
overflow-x: auto;
tab-size: 4;
-o-tab-size: 4;
-moz-tab-size: 4;
border: 1px solid #008522;
padding: 10px;
}
.tx-inputs, .tx-outputs {
max-width: 1000px;
margin: auto;
}

View File

@ -31,7 +31,7 @@
<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 class="TitleUnderliner" style="margin-bottom: 1em"></div>
</div>
{% endblock %}
</div>

View File

@ -7,7 +7,7 @@
<h4 class="Subtitle">{{mempool.transactions|length}} transactions,
{{mempool.transactions|sum(attribute='blob_size') | si}}B</h4>
<div class="TitleDivider"></div>
<div class="TitleUnderliner"></div>
<table style="width:100%">
<thead>

View File

@ -22,7 +22,7 @@
<td>
{%if sn.requested_unlock_height%}
<span title="Service Node unlock in progress (unlocks at block {{sn.requested_unlock_height}})">🔓</span>
{{((sn.requested_unlock_height - info.height) * 120 + server.timestamp.timestamp()) | from_timestamp | format_datetime('short')}}
{{((sn.requested_unlock_height - info.height) * 120 + server.datetime.timestamp()) | from_timestamp | format_datetime('short')}}
({{((sn.requested_unlock_height - info.height) * 120) | reltime}})
{%else%}
Staking Infinitely

View File

@ -28,7 +28,7 @@
<td>
{%if sn.requested_unlock_height%}
<span title="Service Node unlock in progress (unlocks at block {{sn.requested_unlock_height}})">🔓</span>
{{((sn.requested_unlock_height - info.height) * 120 + server.timestamp.timestamp()) | from_timestamp | format_datetime('short')}}
{{((sn.requested_unlock_height - info.height) * 120 + server.datetime.timestamp()) | from_timestamp | format_datetime('short')}}
({{((sn.requested_unlock_height - info.height) * 120) | reltime}})
{%else%}
Staking Infinitely

View File

@ -1,24 +1,24 @@
{% macro display(tx) -%}
{%- macro display(tx, text=false) -%}
{% 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>
<span class="icon" title="Service Node decommission">👎{%if text%} decommission{%endif%}</span>
{% elif tx.extra.sn_state_change.type == 'recom' -%}
<span class="icon" title="Service Node recommission">👍</span>
<span class="icon" title="Service Node recommission">👍{%if text%} recommission{%endif%}</span>
{% elif tx.extra.sn_state_change.type == 'dereg' -%}
<span class="icon" title="Service Node deregistration">🚫</span>
<span class="icon" title="Service Node deregistration">🚫{%if text%} deregistration{%endif%}</span>
{% elif tx.extra.sn_state_change.type == 'ip' -%}
<span class="icon" title="Service Node IP change penalty">📋</span>
<span class="icon" title="Service Node IP change penalty">📋{%if text%} ip change{%endif%}</span>
{% else -%}
<span class="icon" title="Unknown state change transaction"></span><!-- Either a bug or a malformed transaction -->
<span class="icon" title="Unknown state change transaction">{%if text%} unknown state change{%endif%}</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>
<span class="icon" title="Service Node stake unlock — {{tx.extra.sn_pubkey}}">🔓{%if text%} unlock{%endif%}</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>
<span class="icon" title="Loki Name Service Buying">🎫{%if text%} LNS purchase{%endif%}</span>
{% elif 'update' in tx.extra.lns -%}
<span class="icon" title="Loki Name Service Updating">💾</span>
<span class="icon" title="Loki Name Service Updating">💾{%if text%} LNS update{%endif%}</span>
{% endif -%}
{% elif 'sn_registration' in tx.extra -%}
<span class="icon" title="Service Node registration
@ -28,9 +28,14 @@
{{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 %}
{%-endfor%}">🏁{%if text%} registration{%endif%}</span>
{%- elif 'sn_contributor' in tx.extra -%}
<span class="icon" title="Service Node contribution {{tx.extra.sn_pubkey}} / {{tx.extra.sn_contributor}}">
{%-if text%} contribution{%endif%}</span>
{%- elif text -%}
{%if tx.coinbase%}block reward{%else%}transfer{%endif%}
{%- endif -%}
{%- elif standard -%}
{%if tx.coinbase%}block reward{%else%}transfer{%endif%}
{%- endif -%}
{% endmacro -%}

View File

@ -3,20 +3,20 @@
{% 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>
<div class="info_list nowrap-spans">
<span><label>Server Time:</label> {{ server.datetime | format_datetime }}</span>
<span><a href="/txpool">Transaction Pool</a></span>
{% if config.pusher %}
<span>| <a href="/rawtx">Transaction pusher </a></span>
<span><a href="/rawtx">Transaction pusher </a></span>
{% endif %}
{% if config.key_image_checker %}
<span>| <a href="/rawkeyimgs">Key images checker</a></span>
<span><a href="/rawkeyimgs">Key images checker</a></span>
{% endif %}
{% if config.output_key_checker %}
<span>| <a href="/rawoutputkeys">Output keys checker</a></span>
<span><a href="/rawoutputkeys">Output keys checker</a></span>
{% endif %}
{% if config.autorefresh_option %}
<span>|
<span>
{% if refresh %}
<a href="/">Autorefresh is ON ({{refresh}} s)</a>
{% else %}
@ -25,53 +25,66 @@
</span>
{% endif %}
{% if config.testnet_url and not info.testnet %}
<span>| <a href="{{config.testnet_url}}">Go to testnet explorer</a></span>
<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>
<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>
<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>
<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>
<span>This is <span style="color:#af5bd2; font-weight: bold">DEVNET</span> blockchain</span>
{% endif %}
</div>
<div class="TitleDivider"></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:
<h3 class="general_info info_list nowrap-spans">
<span><label>Height:</label> {{info.height}}</span>
<span><label>Hard fork:</label> v{{hf.version}}</span>
{% if hf.version >= 16 %}
<span title="{{ info.pulse_target_timestamp | from_timestamp | format_datetime }}
{%-if info.pulse_target_timestamp != info.pulse_ideal_timestamp %}
{{ (info.pulse_target_timestamp - info.pulse_ideal_timestamp) | reltime(two_part=true, in_ago=false) }}
{%-if info.pulse_target_timestamp > info.pulse_ideal_timestamp %} behind {%else%} ahead of {%endif%} schedule
{%-endif-%}
">
<label>Next Pulse:</label> {{(info.pulse_target_timestamp|from_timestamp - server.datetime) | reltime(neg_is_now=true) }}
</span>
{% else %}
<span><label>Network difficulty:</label> {{info.difficulty}}</span>
<span><label>Hash rate:</label> ~{{(info.difficulty / info.target) | si }}H/s</span>
{% endif %}
<span><label>Staking requirement:</label> {{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)">
<label>Base fee:</label>
{{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:
<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)">
<label>Blink fee:</label>
{{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:
<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.">
<label>Block size limit:</label>
{{(info.block_size_limit / 2) | si}}B/{{info.block_size_limit | si}}B
</span>
<span>| Blockchain size: {{info.database_size | si}}B</span>
<span><label>Blockchain size:</label> {{info.database_size | si}}B</span>
</h3>
{% endif %}
<h4 class="nowrap-spans">
<span><span style="font-weight: bold">Circulating Supply*</span>:
<h4 class="info_list nowrap-spans">
<span><label>Circulating Supply*:</label>:
{% if not emission or 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>
<span><label>(Coinbase:</label> {{emission.emission_amount | loki}}</span>
<span><label>Fees:</label> {{emission.fee_amount | loki}}</span>
<span><label>Burned:</label> {{emission.burn_amount | loki}}<label>).</label></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.
@ -79,18 +92,18 @@
</p>
</h4>
<div class="TitleDivider"></div>
<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 class="tx-type-legend info_list nowrap-spans" style="margin-top: 0">
<span><label>TX Type Legend:</label> 🏁 Service Node Registration</span>
<span>⚑ Contribution</span>
<span>👍 Recommission</span>
<span>👎 Decommission</span>
<span>🚫 Deregistration</span>
<span>📋 IP Change Penalty</span>
<span>🔓 Stake Unlock</span>
<span>🎫 Loki Name System Purchase</span>
<span>💾 LNS Update</span>
</h4>
</div>
@ -115,7 +128,7 @@
{{block_sizes[-1] | si}}B)
</h4>
{%endif%}
<div class="TitleDivider"></div>
<div class="TitleUnderliner"></div>
{% include 'include/block_page_controls.html' %}

15
templates/not_found.html Normal file
View File

@ -0,0 +1,15 @@
{% extends "_basic.html" %}
{% block content %}
<div class="Wrapper">
<h1>Not Found!</h1>
{%if type == 'tx'%}
<h2>The transaction with id <code>{{id}}</code> was not found on the blockchain.</h2>
{%else%}
<h3>Whoops! Couldn't find what you were looking for.</h3>
{%endif%}
</div>
{% endblock %}

649
templates/tx.html Normal file
View File

@ -0,0 +1,649 @@
{% extends "_basic.html" %}
{% block content %}
<div class="Wrapper">
<h4 style="margin:5px"><label>TX Hash:</label> {{tx.tx_hash}}</H4>
{# FIXME
{%if config.enable_mixins_details%}
<H4 style="margin:5px"><label>TX Prefix Hash:</label> {{tx.tx_prefix_hash}}</H4>
{%endif%}
#}
<h4 style="margin:5px"><label>TX Public Key:</label> <span id="tx_pub_key">{{tx.extra.pubkey}}</span></H4>
<span id="add_tx_pub_keys" style="display: none;">
{%-for pk in tx.extra.additional_pubkeys%}
{%-if not loop.first%}; {%endif-%}
{{pk}}
{%-endfor-%}
</span>
{%if tx.extra.payment_id%}
<h4 style="margin:5px"><label>Payment ID ({%if tx.extra_payment_id|length == 64%}un{%endif%}encrypted):</label> <span id="payment_id">{{tx.extra.payment_id}}</span></h4>
{%endif%}
{# FIXME - what is this?
{%if have_prev_hash%}
<h4>Previous TX: <a href="/tx/{{prev_hash}}">{{prev_hash}}</a></h4>
{%endif%}
{%if have_next_hash%}
<h4>Next TX: <a href="/tx/{{next_hash}}">{{next_hash}}</a></h4>
{%endif%}
#}
<h2>Metadata</h2>
<div class="TitleUnderliner"></div>
<h4 class="info_list nowrap-spans">
<span><label>In block:</label> <a href="/block/{{tx.block_height}}">{{tx.block_height}}</a></span>
{% import 'include/tx_type_symbol.html' as sym %}
<span><label>TX Version/Type:</label> {{tx.info.version}}/{{sym.display(tx, text=true)}}</span>
{%if not have_raw_tx%}
<span title="Unix timestamp: {{tx.block_timestamp}}"><label>Timestamp:</label> {{tx.block_timestamp | from_timestamp | format_datetime('short')}} UTC
({{tx.block_timestamp | from_timestamp | ago}} ago)</span>
{%endif%}
{# FIXME - if in mempool then link to mempool page instead #}
<span><label>Fee (Per kB):</label>
{%if tx.coinbase%}
N/A
{%else%}
{% import 'include/tx_fee.html' as fee %}
{{fee.display(tx)}}
({{(tx.info.rct_signatures.txnFee * 1000 / tx.size) | loki(tag=false, decimals=6)}})
{%endif%}
</span>
<span title="{{tx.size}} bytes"><label>TX Size:</label> {{tx.size|si}}B</span>
<span><label>No. Confirmations:</label> {{info.height - tx.block_height}}</span>
<span><label>RingCT/RingCT Type:</label> {%if tx.info.version >= 2 and not tx.coinbase%}Yes/{{tx.info.rct_signatures.type}}{%else%}No{%endif%}</span>
{%if tx.coinbase and tx.extra.sn_winner%}
<span><label>Service Node Winner:</label>
{%if tx.extra.sn_winner == "0000000000000000000000000000000000000000000000000000000000000000"%}
None
{%else%}
<a href="/sn/{{tx.extra.sn_winner}}">{{tx.extra.sn_winner}}</a>
{%endif%}
</span>
{%endif%}
</h4>
<div class="info-item"><label>Extra: </label>{{tx.info.extra}}</div>
{% if tx.info.version >= 4 -%}
{% if tx.info.type == 1 and 'sn_state_change' in tx.extra %}
<h2>
{%- if tx.extra.sn_state_change.type == 'decom' -%}
👎 Service Node Decommission Metadata
{% elif tx.extra.sn_state_change.type == 'recom' -%}
👍 Service Node Recommission Metadata
{% elif tx.extra.sn_state_change.type == 'dereg' -%}
🚫 Service Node Deregistration Metadata
{% elif tx.extra.sn_state_change.type == 'ip' -%}
📋 Service Node IP Change Metadata
{% else -%}
❓ Unknown State Change Metadata
{% endif -%}
</h2>
{#FIXME -- all the values below need to be fixed#}
<div class="TitleDivider"></div>
{%if state_change_have_pubkey_info%}
<p class="state-change-pubkey">Service Node Public Key: <a href="/sn/{{state_change_service_node_pubkey}}">{{state_change_service_node_pubkey}}</a></p>
{%endif%}
<p>Service Node Index: {{state_change_service_node_index}}</p>
<p>Block Height: <a href="/block/{{state_change_block_height}}">{{state_change_block_height}}</a></p>
<table class="Table">
<tr class="TableHeader">
<th class="voter-index">Voters Quorum Index</th>
{%if state_change_have_pubkey_info%}
<th class="voter-pubkey">Voter Public Key</th>
{%endif%}
<th class="voter-signature">Signature</th>
</tr>
{%if state_change_vote_array%}
<tr>
<td class="voter-index">{{state_change_voters_quorum_index}}</td>
{%if state_change_have_pubkey_info%}
<td class="voter-pubkey"><a href="/sn/{{state_change_voter_pubkey}}">{{state_change_voter_pubkey}}</a></td>
{%endif%}
<td class="voter-signature" title="{{state_change_signature}}">{{state_change_signature}}</td>
</tr>
{%endif%}
</table>
{% elif tx.info.type == 2 %}
<h2>🔓 Service Node Unlock</h2>
<p class="unlock-pubkey"><label>Service Node Public Key:</label> <a href="/sn/{{tx.extra.sn_pubkey}}">{{tx.extra.sn_pubkey}}</a></p>
<p><label>Unlock key image:</label> {{unlock_key_image}}</p> {# FIXME #}
<p><label>Unlock signature:</label> {{unlock_signature}}</p> {# FIXME #}
{% elif tx.info.type == 4 and 'lns' in tx.extra %}
{% if 'buy' in tx.extra.lns %}
<h2>🎫 Loki Name Service Registration</h2>
{% elif 'update' in tx.extra.lns %}
<h2>💾 Loki Name Service Update</h2>
{% endif %}
{#FIXME - show some metadata?#}
{% elif 'sn_registration' in tx.extra %}
<h2>🏁 Service Node Register Metadata</h2>
<div class="TitleDivider"></div>
<p><label>Service Node Public Key:</label> {{tx.extra.sn_pubkey}}</p>
<p><label>Operator fee:</label>
{%if tx.extra.sn_registration.fee == 1000000%}N/A (solo registration)
{%else%}{{(tx.extra.sn_registration.fee / 10000) | chop0}}%
{%endif%}</p>
<p><label>Expiration:</label> {{tx.extra.sn_registration.expiry | from_timestamp | format_datetime}} ({{tx.extra.sn_registration.expiry}}),
or {{(tx.extra.sn_registration.expiry|from_timestamp - server.timestamp) | reltime}}</p>
<h2>Service Node Registration Address(es)</h2>
<div class="TitleDivider"></div>
<table class="Table">
<thead>
<tr>
<td>Address</td>
<td>Portions</td>
</tr>
</thead>
<tbody>
{%for c in tx.extra.sn_registration.contributors%}
<tr>
<td>{{c.wallet}}</td>
<td>{{(c.portion / 10000) | chop0}}%</td>
</tr>
{%endfor%}
</tbody>
</table>
{% elif 'sn_contributor' in tx.extra %}
<h2>⚑ Service Node Contribution</h2>
<div class="TitleDivider"></div>
<p><label>Service Node Public Key:</label> {{tx.extra.sn_pubkey}}</p>
<p><label>Contributor Address:</label> {{tx.extra.sn_contributor}}</p>
<p><label>Contribution Amount:</label> {#FIXME {contribution_amount}#}</p>
{% endif %}
{% endif %}
<h2>Outputs</h2>
<h4 class="Subtitle">{{tx.info.vout|length}} output(s) for total of
{{tx.info.vout | sum(attribute='amount') | loki(zero='<span title="Regular LOKI tx amounts are private">???</span>') | safe}}</h4>
<div class="TitleDivider"></div>
<div class="tx-outputs">
<table class="Table">
<thead>
<tr>
<td>Stealth Address</td>
<td>Amount</td>
<td title="Global blockchain output counter">Output Index</td>
</tr>
</thead>
<tbody>
{%for out in tx.info.vout%}
<tr>
<td><label>{{loop.index0}}:</label> {{out.target.key}}</td>
<td>{{out.amount | loki(zero='?')}}</td>
<td>{{tx.output_indices[loop.index0]}}{# FIXME: of {{num_outputs}}#}</td>
</tr>
{%endfor%}
</tbody>
</table>
</div>
{#FIXME
{%if not have_raw_tx%}
<div>
<div class="tabs">
<div class="tab">
<input type="radio" id="tab-1" name="tab-group-1" checked>
<label for="tab-1">Decode Outputs</label>
<div class="content">
<p style="margin: 0px">Check which outputs belong to given Loki address/subaddress and viewkey</p>
<p style="margin: 0px">
For RingCT transactions, outputs' amounts are also decoded
<br/>
{%if enable_js%}
Note: Address/Subaddress and viewkey are NOT sent to the server, as the calculations are done on the client side
{%else%}
Note: address/Subaddress and viewkey are sent to the server, as the calculations are done on the server side
{%endif%}
</p>
<form action="/myoutputs" method="post" style="width:100%; margin-top:2px" class="style-1">
<input type="hidden" name="tx_hash" value="{{tx_hash}}"><br/>
<input type="text" name="lok_address" size="110" placeholder="Loki Address/Subaddress"><br/>
<input type="text" name="viewkey" size="110" placeholder="Private Viewkey" style="margin-top:5px"><br/>
<input type="hidden" name="raw_tx_data" value="{{raw_tx_data}}">
<!--above raw_tx_data field only used when checking raw tx data through tx pusher-->
{%if enable_js%}
<!-- if have js, DONOT submit the form to server.
change submit button, to just a button -->
<button type="button" class="PageButton" style="min-width: 10em; font-weight: bold; margin-top:5px" id="decode_btn" >Decode outputs</button>
{%else%}
<input type="submit" class="PageButton" value="Decode Outputs" style="min-width: 10em; margin-top:5px" >
{%endif%}
</form>
</div>
</div>
<div class="tab">
<input type="radio" id="tab-2" name="tab-group-1">
<label for="tab-2">Prove Sending</label>
<div class="content">
<p style="margin: 0px">Prove to someone that you have sent them Loki in this transaction</p>
<p style="margin: 0px">
TX private key can be obtained using <i>get_tx_key</i>
command in <i>loki-wallet-cli</i> command line tool
<br/>
{%if enable_js%}
Note: Address/Subaddress and TX private key are NOT sent to the server, as the calculations are done on the client side
{%else%}
Note: Address/Subaddress and TX private key are sent to the server, as the calculations are done on the server side
{%endif%}
</p>
<form action="/prove" method="post" style="width:100%;margin-top:2px" class="style-1">
<input type="hidden" name="txhash" value="{{tx_hash}}"><br/>
<input type="text" name="txprvkey" size="120" placeholder="TX Private Key"><br/>
<input type="hidden" name="raw_tx_data" value="{{raw_tx_data}}">
<!--above raw_tx_data field only used when checking raw tx data through tx pusher-->
<input type="text" name="lokaddress" size="120" placeholder="Recipient's Loki Address/Subaddress" style="margin-top:5px"><br/>
{%if enable_js%}
<!-- if have js, DONOT submit the form to server.
change submit button, to just a button -->
<button type="button" class="PageButton" style="min-width: 10em; margin-top:5px" id="prove_btn">Prove sending</button>
{%else%}
<input type="submit" class="PageButton" value="Prove Sending" style="min-width: 10em; margin-top:5px">
{%endif%}
</form>
</div>
</div>
</div>
</div>
{%endif%}
#}
{#FIXME
{%if enable_js%}
<!-- to disply results from deconding and proving txs using js -->
<div id="decode-prove-results" class="center" style="width: 80%; margin-top:10px;border-style: dotted">
</div>
<script>
// here we handle button presses from the above forms
// to decode and prove txs.
$(document).ready(function() {
// we need output pubplic keys, their indexes and amounts.
// all this is already avaliable on the html, but we can use
// musch framework to produce js array for this
var tx_json = {%if tx_json_raw%}{%endif%};
var tx_public_key = $("#tx_pub_key").text();
// when we process multi-ouput tx, it can have extra public keys
// due to sub-addresses
var add_tx_pub_keys = $("#add_tx_pub_keys").text().split(';').slice(0, -1);
//console.log("add_tx_pub_keys: ", add_tx_pub_keys);
var payment_id = $("#payment_id").text();
$("#decode_btn").click(function() {
var address = $("input[name=lok_address]").val().trim();
var viewkey = $("input[name=viewkey]").val().trim();
if (!address || !viewkey) {
$("#decode-prove-results").html("<h4>Address or viewkey key not provided!</h4>");
return;
}
// not used when decoding, but used when proving.
// so we just use array here
multiple_tx_secret_keys = [];
try {
var address_decoded = decode_address(address);
decodeOutputs(tx_json, tx_public_key, viewkey,
address_decoded.spend, payment_id,
add_tx_pub_keys, multiple_tx_secret_keys, false);
} catch(err){
console.log(err);
$("#decode-prove-results").html('<h4>Error: ' + err + '</h4>' );
}
});
$("#prove_btn").click(function() {
var address = $("input[name=lokaddress]").val().trim();
var tx_prv_key = $("input[name=txprvkey]").val().trim();
if (!address || !tx_prv_key) {
$("#decode-prove-results").html("<h4>Address or tx private key not provided!</h4>");
return;
}
try {
// when using subaddress, there can be more than one tx_prv_key
var multiple_tx_prv_keys = parse_str_secret_key(tx_prv_key);
var address_decoded = decode_address(address);
decodeOutputs(tx_json, address_decoded.view, tx_prv_key,
address_decoded.spend, payment_id,
add_tx_pub_keys, multiple_tx_prv_keys, true);
} catch(err){
console.log(err);
$("#decode-prove-results").html('<h4>Error: ' + err + '</h4>' );
}
});
});
// based on C++ code by stoffu
function parse_str_secret_key(key_str) {
var multiple_tx_secret_keys = [];
var num_keys = Math.floor(key_str.length / 64);
if (num_keys * 64 != key_str.length)
throw "num_keys * 64 != key_str.length";
for (var i = 0; i < num_keys; i++)
{
multiple_tx_secret_keys.push(key_str.slice(64*i, 64*i + 64));
}
return multiple_tx_secret_keys;
}
function decodeOutputs(tx_json, pub_key, sec_key,
address_pub_key, payment_id,
add_tx_pub_keys, multiple_tx_prv_keys, tx_prove) {
//console.log(tx_json);
var is_rct = (tx_json.version === 2);
var rct_type = (is_rct ? tx_json.rct_signatures.type : -1);
var key_derivation = "";
if (tx_prove)
key_derivation = generate_key_derivation(pub_key, multiple_tx_prv_keys[0]);
else
key_derivation = generate_key_derivation(pub_key, sec_key);
var add_key_derivation = [];
if (add_tx_pub_keys) {
for (var i = 0; i < add_tx_pub_keys.length; i++)
{
if (!tx_prove)
add_key_derivation.push(generate_key_derivation(add_tx_pub_keys[i], sec_key));
else
add_key_derivation.push(generate_key_derivation(pub_key, multiple_tx_prv_keys[i+1]));
}
}
//console.log("add_key_derivation: ", add_key_derivation);
// go over each tx output, and check if it is ours or not
var decoding_results_str = '<h3>Output decoding results</h3>';
decoding_results_str += '<table class="center">';
decoding_results_str += '<tr>' +
'<td></td>' +
'<td>output public key</td>' +
'<td>amount</td>' +
'<td>output match?</td>' +
'</tr>';
var output_idx = 0;
var sum_outptus = 0;
tx_json.vout.forEach(function(output) {
var output_pub_key = output.target.key;
var amount = output.amount;
var pubkey_generated = derive_public_key(key_derivation, output_idx, address_pub_key);
var mine_output = (output_pub_key == pubkey_generated);
var with_additional = false;
var mine_output_str = "false";
if (!mine_output && add_tx_pub_keys.length == tx_json.vout.length) {
pubkey_generated = derive_public_key(add_key_derivation[output_idx],
output_idx, address_pub_key);
mine_output = (output_pub_key == pubkey_generated);
with_additional = true;
}
if (mine_output) {
mine_output_str = '<span style="color: #008009;font-weight: bold">true</span>';
if (is_rct && rct_type > 0 /* not coinbase*/) {
try {
//var ecdh = decodeRct(tx_json.rct_signatures, output_idx, key_derivation);
var ecdh = decodeRct(tx_json.rct_signatures, output_idx,
(with_additional ? add_key_derivation[output_idx] : key_derivation));
amount = parseInt(ecdh.amount);
} catch (err) {
decoding_results_str += "<span class='validNo'>RingCT amount for output " + i + " with pubkey: " + output_pub_key + "</span>" + "<br>"; //rct commitment != computed
throw "invalid rct amount";
}
}
sum_outptus += amount;
}
decoding_results_str += "<tr>"
+"<td>" + output_idx + "</td>"
+"<td>" + output_pub_key + "</td>"
+"<td>" + (amount / 1e9) + "</td>"
+"<td>" + mine_output_str + "</td>"
+"</tr>";
//console.log(output[1], pubkey_generated);
output_idx++;
});
decoding_results_str += "</table>";
decoding_results_str += "<h3>Sum LOK from matched outputs (i.e., incoming LOK): " + (sum_outptus / 1e9) + "</h3>"
// decrypt payment_id8 which results in using
// integrated address
if (payment_id.length == 16) {
if (pub_key) {
var decrypted_payment_id8
= decrypt_payment_id(payment_id, pub_key, sec_key);
console.log("decrypted_payment_id8: " + decrypted_payment_id8);
decoding_results_str += "<h5>Decrypted payment id: "
+ decrypted_payment_id8
+ " (value incorrect if you are not the recipient of the tx)</h5>"
}
}
$("#decode-prove-results").html(decoding_results_str);
}
</script>
{%endif%}
#}
{%if not tx.coinbase and tx.info.vin|length > 0 %}
<div style="height: 1em"></div>
<h2>Inputs</h2>
<h4 class="Subtitle">{{tx.info.vin|length}} input(s) for total of
{{tx.info.vin | sum(attribute='key.amount') | loki(zero='<span title="Regular LOKI tx amounts are private">???</span>') | safe}}</h4>
<div class="TitleDivider"></div>
{#FIXME#}
{%if enable_mixins_details%}
<h3>Inputs' ring size time scale (from {{min_mix_time}} till {{max_mix_time}};
resolution: {{timescales_scale}} days{%if have_raw_tx%}; R - real ring member {%endif%})
</h3>
<div class="center">
<ul class="center">
{%if timescales%}
<li style="list-style-type: none; text-align: center; font-size: 8px">|{{timescale}}|</li>
{%endif%}
</ul>
</div>
{%endif%}
<div class="tx-inputs">
<table class="Table">
{%for inp in tx.info.vin if 'key' in inp%}
<tr>
<td style="text-align: left;" colspan="2">
<label>Key Image {{loop.index0}}:</label> {{inp.key.k_image}}
{#FIXME:#}
{%if have_raw_tx%}
Already spent:
{%if already_spent%}
<span style="color: red; font-weight: bold;">True</span>
{%else%}
False
{%endif%}
{%endif%}
</td>
<td style="text-align: right">Amount: {{inp.key.amount | loki(zero='?')}}</td>
</tr>
{%if config.enable_mixins_details%}
<tr class="TableHeader">
<td>Ring Members</td>
{%if have_raw_tx%}
<td>Is It Real?</td>
{%endif%}
<td>Block</td>
<td>Timestamp (UTC)</td>
</tr>
{%for koffset in inp.key.key_offsets%}
{%set oinfo = koffset_info[inp.key.amount][koffset]%}
{%set binfo = block_info[oinfo.height]%}
<tr>
<td><label>- {{loop.index0}}:</label> <a href="/tx/{{oinfo.txid}}">{{oinfo.key}}</a></td>
{%if have_raw_tx%}
{%if mix_is_it_real%}
<td><span style="color: #008009;font-weight: bold">{{mix_is_it_real}}</span></td>
{%else%}
<td>{{mix_is_it_real}}</td>
{%endif%}
{%endif%}
<td><a href="/block/{{binfo.height}}">{{binfo.height}}</a></td>
<td>{{binfo.timestamp | from_timestamp | format_datetime('short')}}
({{(binfo.timestamp|from_timestamp - server.datetime) | reltime}})
</td>
</tr>
{%endfor%}
{%endif%}
{%endfor%}
</table>
</div>
{%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="/tx/{{tx.tx_hash}}/1">Show raw details</a>
</h5>
{%endif%}
{%if not have_raw_tx%}
<div style="height: 1em;"></div>
{%if with_ring_signatures%}
<div class="TitleDivider"></div>
<div id="decoded-inputs">
<div style="border: 1px solid #008552; margin-top: 1em; padding: 1em; background-color: rgba(0, 0, 0, 0.2)">
<code class="center" style="white-space: pre-wrap; font-size: 1.5em;">
{{tx_json}}
</code>
</div>
</div>
<br/><br/>
<p style="margin-top:1px"><a href="/tx/{{tx_hash}}">Less Details</a></p>
{%elif show_more_details_link%}
<h5 style="margin-top:1px">
<a href="/tx/{{tx_hash}}/1">More Details</a>
{%if enable_as_hex%}
| <a href="/txhex/{{tx_hash}}">TX As Hex</a>
| <a href="/ringmembershex/{{tx_hash}}">TX Ring Members As Hex</a>
{%endif%}
</h5>
{%endif%}
{%endif%}
{%if show_cache_times%}
<div>
{%if construction_time%}
<div style="height: 1em;"></div>
<div class="TitleDivider"></div>
<p style="margin-top: 1px;color:#949490">
TX details construction time: {{construction_time}} s
{%if from_cache%}
<br/>TX read from the TX cache
{%endif%}
</p>
{%endif%}
</div>
{%endif%}
<div>
{%if has_error%}
<h4 style="color:red">Attempt failed</h4>
{%if error_tx_not_found%}
<h4>Tx {{tx_hash}} not found. </h4>
<div class="center" style="text-align: center;width:80%">
<p> If this is newly made tx, it can take some time (up to minute)
for it to get propagated to all nodes' txpools.
<br/><br/>
Please refresh in 10-20 seconds to check if its here then.
</p>
</div>
{%endif%}
{%elif txs%}
{{tx_details}}
{%endif%}
</div>
</div>
{% endblock %}