Add tx details page
Plus various improvements around how small metadata is displayed.
This commit is contained in:
parent
2c394386e3
commit
bdf9056c2a
|
@ -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.)
|
||||
|
|
|
@ -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'
|
||||
|
|
153
observer.py
153
observer.py
|
@ -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(),
|
||||
)
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 -%}
|
||||
|
|
|
@ -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' %}
|
||||
|
||||
|
|
|
@ -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 %}
|
|
@ -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 %}
|
Loading…
Reference in New Issue