Loki -> Oxen

This commit is contained in:
Jason Rhinelander 2021-01-07 18:04:00 -04:00
parent 868fd7dc0b
commit 007baea846
18 changed files with 149 additions and 149 deletions

View File

@ -1,6 +1,6 @@
# Loki Observer OMG block explorer
# Oxen Observer OMG block explorer
Block explorer using Loki 8+ LMQ RPC interface that does everything through RPC requests. Sexy,
Block explorer using Oxen 8+ LMQ RPC interface that does everything through RPC requests. Sexy,
awesome, safe.
## Prerequisite packages
@ -23,7 +23,7 @@ Quick and dirty setup instructions for now:
(Note that we require a very recent python3-jinja package (2.11+), which may not be installed by the
above.)
You'll also need to run lokid with `--lmq-local-control ipc:///path/to/loki-observer/mainnet.sock`.
You'll also need to run oxend with `--lmq-local-control ipc:///path/to/loki-observer/mainnet.sock`.
## Running in debug mode
@ -60,13 +60,13 @@ Create a "vassal" config for loki-observer, `/etc/uwsgi-emperor/vassals/loki-obs
logger = file:logfile=/path/to/loki-observer/mainnet.log
Set ownership of this user to whatever use you want it to run as, and set the group to `_loki` (so
that it can open the lokid unix socket):
that it can open the oxend unix socket):
chown MYUSERNAME:_loki /etc/uwsgi-emperor/vassals/loki-observer.ini
In the loki-observer/mainnet.py, set:
config.lokid_rpc = 'ipc:///var/lib/loki/lokid.sock'
config.oxend_rpc = 'ipc:///var/lib/loki/oxend.sock'
and finally, proxy requests from the webserver to the wsgi socket. For Apache I do this with:
@ -88,4 +88,4 @@ make uwsgi restart (for example because you are changing things) then it is suff
apache2/uwsgi-emperor layers).
If you want to set up a testnet or devnet observer the procedure is essentially the same, but
using testnet.py or devnet.py pointing to a lokid.sock from a testnet or devnet lokid.
using testnet.py or devnet.py pointing to a oxend.sock from a testnet or devnet oxend.

View File

@ -7,10 +7,10 @@
# into `mainnet.py`/`testnet.py`/etc.
# LMQ RPC endpoint of lokid; can be a unix socket 'ipc:///path/to/lokid.sock' (preferred) or a tcp
# LMQ RPC endpoint of oxend; can be a unix socket 'ipc:///path/to/oxend.sock' (preferred) or a tcp
# socket 'tcp://127.0.0.1:5678'. Typically you want this running with admin permission.
# Leave this as None here, and set it for each observer in the mainnet.py/testnet.py/etc. script.
lokid_rpc = None
oxend_rpc = None
# Default blocks per page for the index.
blocks_per_page=20
@ -25,9 +25,9 @@ autorefresh_option=True
enable_mixins_details=True
# URLs to networks other than the one we are on:
mainnet_url='https://blocks.lokinet.dev'
testnet_url='https://testnet.lokinet.dev'
devnet_url='https://devnet.lokinet.dev'
mainnet_url='https://oxen.observer'
testnet_url='https://testnet.oxen.observer'
devnet_url='https://devnet.oxen.observer'
# Same as above, but these apply if we are on a .loki URL:
lokinet_mainnet_url='http://blocks.loki'

View File

@ -1,3 +1,3 @@
from observer import app, config
config.lokid_rpc = 'ipc://lokid/devnet.sock'
config.oxend_rpc = 'ipc://oxend/devnet.sock'

16
lmq.py
View File

@ -4,16 +4,16 @@ import json
import sys
from datetime import datetime, timedelta
lmq, lokid = None, None
lmq, oxend = None, None
def lmq_connection():
global lmq, lokid
global lmq, oxend
if lmq is None:
lmq = pylokimq.LokiMQ(pylokimq.LogLevel.warn)
lmq.max_message_size = 200*1024*1024
lmq.start()
if lokid is None:
lokid = lmq.connect_remote(config.lokid_rpc)
return (lmq, lokid)
if oxend is None:
oxend = lmq.connect_remote(config.oxend_rpc)
return (lmq, oxend)
cached = {}
cached_args = {}
@ -31,7 +31,7 @@ class FutureJSON():
result in unbounded memory growth.
lmq - the lmq object
lokid - the lokid lmq connection id object
oxend - the oxend lmq connection id object
endpoint - the lmq endpoint, e.g. 'rpc.get_info'
cache_seconds - how long to cache the response; can be None to not cache it at all
cache_key - fixed string to enable different caches of the same endpoint
@ -40,7 +40,7 @@ class FutureJSON():
timeout - maximum time to spend waiting for a reply
"""
def __init__(self, lmq, lokid, endpoint, cache_seconds=3, *, cache_key='', args=None, fail_okay=False, timeout=10):
def __init__(self, lmq, oxend, endpoint, cache_seconds=3, *, cache_key='', args=None, fail_okay=False, timeout=10):
self.endpoint = endpoint
self.cache_key = self.endpoint + cache_key
self.fail_okay = fail_okay
@ -53,7 +53,7 @@ class FutureJSON():
else:
self.json = None
self.args = args
self.future = lmq.request_future(lokid, self.endpoint, [] if self.args is None else [self.args], timeout=timeout)
self.future = lmq.request_future(oxend, self.endpoint, [] if self.args is None else [self.args], timeout=timeout)
self.cache_seconds = cache_seconds
def get(self):

View File

@ -1,3 +1,3 @@
from observer import app, config
config.lokid_rpc = 'ipc://lokid/mainnet.sock'
config.oxend_rpc = 'ipc://oxend/mainnet.sock'

View File

@ -115,10 +115,10 @@ def format_si(value):
i += 1
return filter_round(value) + '{}'.format(si_suffix[i])
@app.template_filter('loki')
def format_loki(atomic, tag=True, fixed=False, decimals=9, zero=None):
@app.template_filter('oxen')
def format_oxen(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"
tag - if False then don't append " OXEN"
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
@ -130,7 +130,7 @@ def format_loki(atomic, tag=True, fixed=False, decimals=9, zero=None):
if not fixed and decimals > 0:
disp = disp.rstrip('0').rstrip('.')
if tag:
disp += ' LOKI'
disp += ' OXEN'
return disp
# For some inexplicable reason some hex fields are provided as array of byte integer values rather
@ -169,8 +169,8 @@ def css():
return flask.send_from_directory('static', 'style.css')
def get_sns_future(lmq, lokid):
return FutureJSON(lmq, lokid, 'rpc.get_service_nodes', 5,
def get_sns_future(lmq, oxend):
return FutureJSON(lmq, oxend, 'rpc.get_service_nodes', 5,
args={
'all': False,
'fields': { x: True for x in ('service_node_pubkey', 'requested_unlock_height', 'last_reward_block_height',
@ -200,8 +200,8 @@ def get_sns(sns_future, info_future):
return awaiting_sns, active_sns, inactive_sns
def get_quorums_future(lmq, lokid, height):
return FutureJSON(lmq, lokid, 'rpc.get_quorum_state', 30,
def get_quorums_future(lmq, oxend, height):
return FutureJSON(lmq, oxend, 'rpc.get_quorum_state', 30,
args={ 'start_height': height-55, 'end_height': height })
@ -218,8 +218,8 @@ def get_quorums(quorums_future):
print("Something getting wrong in quorums: found unknown quorum_type={}".format(q['quorum_type']), file=sys.stderr)
return quo
def get_mempool_future(lmq, lokid):
return FutureJSON(lmq, lokid, 'rpc.get_transaction_pool', 5, args={"tx_extra":True, "stake_info":True})
def get_mempool_future(lmq, oxend):
return FutureJSON(lmq, oxend, 'rpc.get_transaction_pool', 5, args={"tx_extra":True, "stake_info":True})
def parse_mempool(mempool_future):
# mempool RPC return values are about as nasty as can be. For each mempool tx, we get back
@ -257,19 +257,19 @@ def template_globals():
@app.route('/autorefresh/<int:refresh>')
@app.route('/')
def main(refresh=None, page=0, per_page=None, first=None, last=None):
lmq, lokid = lmq_connection()
inforeq = FutureJSON(lmq, lokid, 'rpc.get_info', 1)
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 = get_mempool_future(lmq, lokid)
sns = get_sns_future(lmq, lokid)
checkpoints = FutureJSON(lmq, lokid, 'rpc.get_checkpoints', args={"count": 3})
lmq, oxend = lmq_connection()
inforeq = FutureJSON(lmq, oxend, 'rpc.get_info', 1)
stake = FutureJSON(lmq, oxend, 'rpc.get_staking_requirement', 10)
base_fee = FutureJSON(lmq, oxend, 'rpc.get_fee_estimate', 10)
hfinfo = FutureJSON(lmq, oxend, 'rpc.hard_fork_info', 10)
mempool = get_mempool_future(lmq, oxend)
sns = get_sns_future(lmq, oxend)
checkpoints = FutureJSON(lmq, oxend, 'rpc.get_checkpoints', args={"count": 3})
# This call is slow the first time it gets called in lokid but will be fast after that, so call
# This call is slow the first time it gets called in oxend 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,
coinbase = FutureJSON(lmq, oxend, 'admin.get_coinbase_tx_sum', 10, timeout=1, fail_okay=True,
args={"height":0, "count":2**31-1})
custom_per_page = ''
@ -298,7 +298,7 @@ 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', cache_key='main', args={
blocks = FutureJSON(lmq, oxend, 'rpc.get_block_headers_range', cache_key='main', args={
'start_height': start_height,
'end_height': end_height,
'get_tx_hashes': True,
@ -313,7 +313,7 @@ def main(refresh=None, page=0, per_page=None, first=None, last=None):
txids.append(b['miner_tx_hash'])
if 'tx_hashes' in b:
txids += b['tx_hashes']
txs = parse_txs(tx_req(lmq, lokid, txids, cache_key='mempool').get())
txs = parse_txs(tx_req(lmq, oxend, txids, cache_key='mempool').get())
i = 0
for tx in txs:
# TXs should come back in the same order so we can just skip ahead one when the block
@ -353,9 +353,9 @@ def main(refresh=None, page=0, per_page=None, first=None, last=None):
@app.route('/txpool')
def mempool():
lmq, lokid = lmq_connection()
info = FutureJSON(lmq, lokid, 'rpc.get_info', 1)
mempool = get_mempool_future(lmq, lokid)
lmq, oxend = lmq_connection()
info = FutureJSON(lmq, oxend, 'rpc.get_info', 1)
mempool = get_mempool_future(lmq, oxend)
return flask.render_template('mempool.html',
info=info.get(),
@ -364,9 +364,9 @@ def mempool():
@app.route('/service_nodes')
def sns():
lmq, lokid = lmq_connection()
info = FutureJSON(lmq, lokid, 'rpc.get_info', 1)
awaiting, active, inactive = get_sns(get_sns_future(lmq, lokid), info)
lmq, oxend = lmq_connection()
info = FutureJSON(lmq, oxend, 'rpc.get_info', 1)
awaiting, active, inactive = get_sns(get_sns_future(lmq, oxend), info)
return flask.render_template('service_nodes.html',
info=info.get(),
@ -375,8 +375,8 @@ def sns():
inactive_sns=inactive,
)
def tx_req(lmq, lokid, txids, cache_key='single', **kwargs):
return FutureJSON(lmq, lokid, 'rpc.get_transactions', cache_seconds=10, cache_key=cache_key,
def tx_req(lmq, oxend, txids, cache_key='single', **kwargs):
return FutureJSON(lmq, oxend, 'rpc.get_transactions', cache_seconds=10, cache_key=cache_key,
args={
"txs_hashes": txids,
"decode_as_json": True,
@ -386,38 +386,38 @@ def tx_req(lmq, lokid, txids, cache_key='single', **kwargs):
},
**kwargs)
def sn_req(lmq, lokid, pubkey, **kwargs):
return FutureJSON(lmq, lokid, 'rpc.get_service_nodes', 5, cache_key='single',
def sn_req(lmq, oxend, pubkey, **kwargs):
return FutureJSON(lmq, oxend, 'rpc.get_service_nodes', 5, cache_key='single',
args={"service_node_pubkeys": [pubkey]}, **kwargs
)
def block_header_req(lmq, lokid, hash_or_height, **kwargs):
def block_header_req(lmq, oxend, hash_or_height, **kwargs):
if isinstance(hash_or_height, int) or (len(hash_or_height) <= 10 and hash_or_height.isdigit()):
return FutureJSON(lmq, lokid, 'rpc.get_block_header_by_height', cache_key='single',
return FutureJSON(lmq, oxend, 'rpc.get_block_header_by_height', cache_key='single',
args={ "height": int(hash_or_height) }, **kwargs)
else:
return FutureJSON(lmq, lokid, 'rpc.get_block_header_by_hash', cache_key='single',
return FutureJSON(lmq, oxend, 'rpc.get_block_header_by_hash', cache_key='single',
args={ 'hash': hash_or_height }, **kwargs)
def block_with_txs_req(lmq, lokid, hash_or_height, **kwargs):
def block_with_txs_req(lmq, oxend, hash_or_height, **kwargs):
args = { 'get_tx_hashes': True }
if isinstance(hash_or_height, int) or (len(hash_or_height) <= 10 and hash_or_height.isdigit()):
args['height'] = int(hash_or_height)
else:
args['hash'] = hash_or_height
return FutureJSON(lmq, lokid, 'rpc.get_block', cache_key='single', args=args, **kwargs)
return FutureJSON(lmq, oxend, 'rpc.get_block', cache_key='single', args=args, **kwargs)
@app.route('/service_node/<hex64:pubkey>') # For backwards compatibility with old explorer URLs
@app.route('/sn/<hex64:pubkey>')
def show_sn(pubkey):
lmq, lokid = lmq_connection()
info = FutureJSON(lmq, lokid, 'rpc.get_info', 1)
hfinfo = FutureJSON(lmq, lokid, 'rpc.hard_fork_info', 10)
sn = sn_req(lmq, lokid, pubkey).get()
lmq, oxend = lmq_connection()
info = FutureJSON(lmq, oxend, 'rpc.get_info', 1)
hfinfo = FutureJSON(lmq, oxend, 'rpc.hard_fork_info', 10)
sn = sn_req(lmq, oxend, pubkey).get()
if 'service_node_states' not in sn or not sn['service_node_states']:
return flask.render_template('not_found.html',
@ -452,7 +452,7 @@ def parse_txs(txs_rpc):
for tx in txs_rpc['txs']:
if 'info' not in tx:
# We have serialized JSON data inside a field in the JSON, because of lokid's
# We have serialized JSON data inside a field in the JSON, because of oxend's
# multiple incompatible JSON generators 🤮:
tx['info'] = json.loads(tx["as_json"])
del tx['as_json']
@ -462,7 +462,7 @@ def parse_txs(txs_rpc):
return txs_rpc['txs']
def get_block_txs_future(lmq, lokid, block):
def get_block_txs_future(lmq, oxend, block):
hashes = []
if 'tx_hashes' in block:
hashes += block['tx_hashes']
@ -475,7 +475,7 @@ def get_block_txs_future(lmq, lokid, block):
except Exception as e:
print("Something getting wrong: cannot parse block json for block {}: {}".format(block_height, e), file=sys.stderr)
return tx_req(lmq, lokid, hashes, cache_key='block')
return tx_req(lmq, oxend, hashes, cache_key='block')
@app.route('/block/<int:height>')
@ -483,15 +483,15 @@ def get_block_txs_future(lmq, lokid, block):
@app.route('/block/<hex64:hash>')
@app.route('/block/<hex64:hash>/<int:more_details>')
def show_block(height=None, hash=None, more_details=False):
lmq, lokid = lmq_connection()
info = FutureJSON(lmq, lokid, 'rpc.get_info', 1)
hfinfo = FutureJSON(lmq, lokid, 'rpc.hard_fork_info', 10)
lmq, oxend = lmq_connection()
info = FutureJSON(lmq, oxend, 'rpc.get_info', 1)
hfinfo = FutureJSON(lmq, oxend, 'rpc.hard_fork_info', 10)
if height is not None:
val = height
elif hash is not None:
val = hash
block = None if val is None else block_with_txs_req(lmq, lokid, val).get()
block = None if val is None else block_with_txs_req(lmq, oxend, val).get()
if block is None:
return flask.render_template("not_found.html",
info=info.get(),
@ -503,10 +503,10 @@ def show_block(height=None, hash=None, more_details=False):
next_block = None
block_height = block['block_header']['height']
txs = get_block_txs_future(lmq, lokid, block)
txs = get_block_txs_future(lmq, oxend, block)
if info.get()['height'] > 1 + block_height:
next_block = block_header_req(lmq, lokid, '{}'.format(block_height + 1))
next_block = block_header_req(lmq, oxend, '{}'.format(block_height + 1))
if more_details:
formatter = HtmlFormatter(cssclass="syntax-highlight", style="native")
@ -534,17 +534,17 @@ def show_block(height=None, hash=None, more_details=False):
@app.route('/block/latest')
def show_block_latest():
lmq, lokid = lmq_connection()
height = FutureJSON(lmq, lokid, 'rpc.get_info', 1).get()['height'] - 1
lmq, oxend = lmq_connection()
height = FutureJSON(lmq, oxend, 'rpc.get_info', 1).get()['height'] - 1
return flask.redirect(flask.url_for('show_block', height=height), code=302)
@app.route('/tx/<hex64:txid>')
@app.route('/tx/<hex64:txid>/<int:more_details>')
def show_tx(txid, more_details=False):
lmq, lokid = lmq_connection()
info = FutureJSON(lmq, lokid, 'rpc.get_info', 1)
txs = tx_req(lmq, lokid, [txid]).get()
lmq, oxend = lmq_connection()
info = FutureJSON(lmq, oxend, 'rpc.get_info', 1)
txs = tx_req(lmq, oxend, [txid]).get()
if 'txs' not in txs or not txs['txs']:
return flask.render_template('not_found.html',
@ -557,7 +557,7 @@ def show_tx(txid, more_details=False):
# If this is a state change, see if we have the quorum stored to provide context
testing_quorum = None
if tx['info']['version'] >= 4 and 'sn_state_change' in tx['extra']:
testing_quorum = FutureJSON(lmq, lokid, 'rpc.get_quorum_state', 60, cache_key='tx_state_change',
testing_quorum = FutureJSON(lmq, oxend, 'rpc.get_quorum_state', 60, cache_key='tx_state_change',
args={ 'quorum_type': 0, 'start_height': tx['extra']['sn_state_change']['height'] })
kindex_info = {} # { amount => { keyindex => {output-info} } }
@ -581,14 +581,14 @@ def show_tx(txid, more_details=False):
del inp['key']['key_offsets']
outs_req = [{"amount":inp['key']['amount'], "index":ki} for inp in tx['info']['vin'] for ki in inp['key']['key_indices']]
outputs = FutureJSON(lmq, lokid, 'rpc.get_outs', args={
outputs = FutureJSON(lmq, oxend, '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={
block_info_req = FutureJSON(lmq, oxend, 'rpc.get_block_header_by_height', args={
'heights': [o["height"] for o in outputs]
})
i = 0
@ -637,9 +637,9 @@ def show_tx(txid, more_details=False):
@app.route('/quorums')
def show_quorums():
lmq, lokid = lmq_connection()
info = FutureJSON(lmq, lokid, 'rpc.get_info', 1)
quos = get_quorums_future(lmq, lokid, info.get()['height'])
lmq, oxend = lmq_connection()
info = FutureJSON(lmq, oxend, 'rpc.get_info', 1)
quos = get_quorums_future(lmq, oxend, info.get()['height'])
return flask.render_template('quorums.html',
info=info.get(),
@ -649,8 +649,8 @@ def show_quorums():
@app.route('/search')
def search():
lmq, lokid = lmq_connection()
info = FutureJSON(lmq, lokid, 'rpc.get_info', 1)
lmq, oxend = lmq_connection()
info = FutureJSON(lmq, oxend, 'rpc.get_info', 1)
val = (flask.request.args.get('value') or '').strip()
if val and len(val) < 10 and val.isdigit(): # Block height
@ -664,9 +664,9 @@ def search():
)
# Initiate all the lookups at once, then redirect to whichever one responds affirmatively
snreq = sn_req(lmq, lokid, val)
blreq = block_header_req(lmq, lokid, val, fail_okay=True)
txreq = tx_req(lmq, lokid, [val])
snreq = sn_req(lmq, oxend, val)
blreq = block_header_req(lmq, oxend, val, fail_okay=True)
txreq = tx_req(lmq, oxend, [val])
sn = snreq.get()
if 'service_node_states' in sn and sn['service_node_states']:
@ -686,9 +686,9 @@ def search():
@app.route('/api/networkinfo')
def api_networkinfo():
lmq, lokid = lmq_connection()
info = FutureJSON(lmq, lokid, 'rpc.get_info', 1)
hfinfo = FutureJSON(lmq, lokid, 'rpc.hard_fork_info', 10)
lmq, oxend = lmq_connection()
info = FutureJSON(lmq, oxend, 'rpc.get_info', 1)
hfinfo = FutureJSON(lmq, oxend, 'rpc.hard_fork_info', 10)
info = info.get()
data = {**info}
@ -700,9 +700,9 @@ def api_networkinfo():
@app.route('/api/emission')
def api_emission():
lmq, lokid = lmq_connection()
info = FutureJSON(lmq, lokid, 'rpc.get_info', 1)
coinbase = FutureJSON(lmq, lokid, 'admin.get_coinbase_tx_sum', 10, timeout=1, fail_okay=True,
lmq, oxend = lmq_connection()
info = FutureJSON(lmq, oxend, 'rpc.get_info', 1)
coinbase = FutureJSON(lmq, oxend, 'admin.get_coinbase_tx_sum', 10, timeout=1, fail_okay=True,
args={"height":0, "count":2**31-1}).get()
if not coinbase:
return flask.jsonify(None)
@ -722,8 +722,8 @@ def api_emission():
@app.route('/api/circulating_supply')
def api_circulating_supply():
lmq, lokid = lmq_connection()
coinbase = FutureJSON(lmq, lokid, 'admin.get_coinbase_tx_sum', 10, timeout=1, fail_okay=True,
lmq, oxend = lmq_connection()
coinbase = FutureJSON(lmq, oxend, 'admin.get_coinbase_tx_sum', 10, timeout=1, fail_okay=True,
args={"height":0, "count":2**31-1}).get()
return flask.jsonify((coinbase["emission_amount"] - coinbase["burn_amount"]) // 1000000000 if coinbase else None)
@ -731,8 +731,8 @@ def api_circulating_supply():
# FIXME: need better error handling here
@app.route('/api/transaction/<hex64:txid>')
def api_tx(txid):
lmq, lokid = lmq_connection()
tx = tx_req(lmq, lokid, [txid]).get()
lmq, oxend = lmq_connection()
tx = tx_req(lmq, oxend, [txid]).get()
txs = parse_txs(tx)
return flask.jsonify({
"status": tx['status'],
@ -742,9 +742,9 @@ def api_tx(txid):
@app.route('/api/block/<int:height>')
@app.route('/api/block/<hex64:blkid>')
def api_block(blkid=None, height=None):
lmq, lokid = lmq_connection()
block = block_with_txs_req(lmq, lokid, blkid if blkid is not None else height).get()
txs = get_block_txs_future(lmq, lokid, block)
lmq, oxend = lmq_connection()
block = block_with_txs_req(lmq, oxend, blkid if blkid is not None else height).get()
txs = get_block_txs_future(lmq, oxend, block)
if 'block_header' in block:
data = block['block_header'].copy()
@ -761,7 +761,7 @@ ticker_cache, ticker_cache_expires = {}, None
@app.route('/api/price/<fiat>')
def api_price(fiat=None):
global ticker_cache, ticker_cache_expires, ticker_vs, ticker_vs_expires
# TODO: will need to change to 'oxen' when the ticker changes:
# TODO: will need to change to 'oxen' when/if the ticker changes:
ticker = 'loki-network'
if not ticker_cache or not ticker_cache_expires or ticker_cache_expires < time.time():

View File

@ -3,7 +3,7 @@
<html lang="en">
<head>
{% block head %}
<title>{% block title %}{% endblock %}Loki{{' TESTNET' if info and info.testnet else ' DEVNET' if info and info.devnet else ''}}
<title>{% block title %}{% endblock %}Oxen{{' TESTNET' if info and info.testnet else ' DEVNET' if info and info.devnet else ''}}
Blockchain Explorer</title>
<link rel="stylesheet" type="text/css" href="/style.css">
{% if refresh %}
@ -16,7 +16,7 @@
<div id="header" class="Wrapper">
{% block header %}
<div id="header-content">
<h1 class="Header"><a href="/">Loki
<h1 class="Header"><a href="/">Oxen
{%if info and info.testnet%}
<span style="color:#ff6b62">Testnet</span>
{%elif info and info.devnet%}
@ -41,7 +41,7 @@
<p style="margin-top:10px">
<a href="https://github.com/loki-project/loki-observer">Source Code</a>
| Explorer Revision: {{server.revision}}
| Loki Version: {{info.version}}
| Oxen Version: {{info.version}}
</p>
{% endblock %}
</div>

View File

@ -52,19 +52,19 @@ Validator bits: {{"{:011b}".format(block.info.pulse.validator_bitset)}}"><label>
{%set sum_burned = transactions | selectattr('extra.burn_amount') | sum(attribute='extra.burn_amount') %}
{%set sum_fees = transactions | selectattr('info.rct_signatures') | selectattr('info.rct_signatures.txnFee') | sum(attribute='info.rct_signatures.txnFee') - sum_burned%}
<span title="{{(block_header.reward - sum_fees) | loki(fixed=True)}} created in this block.{%if sum_fees > 0%}
<span title="{{(block_header.reward - sum_fees) | oxen(fixed=True)}} created in this block.{%if sum_fees > 0%}
Note that this value does not include earned transaction fees ({{sum_fees | loki(fixed=True, decimals=4)}}){%endif%}"><label>Block reward:</label>
{{(block_header.reward - sum_fees) | loki(decimals=4)}}</span>
Note that this value does not include earned transaction fees ({{sum_fees | oxen(fixed=True, decimals=4)}}){%endif%}"><label>Block reward:</label>
{{(block_header.reward - sum_fees) | oxen(decimals=4)}}</span>
{%if sum_fees > 0%}
<span title="Earned TX fees: {{sum_fees | loki(fixed=True)}}"><label>Block TX fees:</label> {{ sum_fees | loki(fixed=True, decimals=4) }}</span>
<span title="Earned TX fees: {{sum_fees | oxen(fixed=True)}}"><label>Block TX fees:</label> {{ sum_fees | oxen(fixed=True, decimals=4) }}</span>
{%endif%}
{%if sum_burned > 0%}
<span title="{{sum_burned | loki(fixed=True)}} burned in the transactions included in block">
<span title="{{sum_burned | oxen(fixed=True)}} burned in the transactions included in block">
<label>Burned fees:</label>
{{sum_burned | loki(decimals=4)}} <span class="icon">🔥</span>
{{sum_burned | oxen(decimals=4)}} <span class="icon">🔥</span>
</span>
{%endif%}
@ -90,7 +90,7 @@ Note that this value does not include earned transaction fees ({{sum_fees | loki
</tr>
<tr>
<td><a href="/tx/{{miner_tx.tx_hash}}">{{miner_tx.tx_hash}}</a></td>
<td>{{miner_tx.info.vout | sum(attribute='amount') | loki}}</td>
<td>{{miner_tx.info.vout | sum(attribute='amount') | oxen}}</td>
<td>{{miner_tx.size}}</td>
<td>{{miner_tx.info.version}}</td>
</tr>

View File

@ -30,7 +30,7 @@
<td><a href="/tx/{{tx.id_hash}}">{{tx.id_hash}}</a></td>
<td>
{%if 'rct_signatures' in tx.info%}
{{fee.display(tx)}} / {{(tx.info.rct_signatures.txnFee * 1000 / tx.blob_size) | loki(tag=false, decimals=4)}}
{{fee.display(tx)}} / {{(tx.info.rct_signatures.txnFee * 1000 / tx.blob_size) | oxen(tag=false, decimals=4)}}
{%else%}
N/A
{%endif%}

View File

@ -16,7 +16,7 @@
<tr>
{%include 'include/sn_kcf.html'%}
<td>{{sn.staking_requirement | loki(tag=false, fixed=true)}}</td>
<td>{{sn.staking_requirement | oxen(tag=false, fixed=true)}}</td>
<td>{{sn.last_reward_block_height}}</td>
<td>{{sn.last_uptime_proof | from_timestamp | ago if sn.last_uptime_proof > 0 else "Not Received"}}</td>
<td>

View File

@ -16,15 +16,15 @@
{%for sn in (awaiting_sns | sort(attribute='portions_for_operator,contribution_open,contribution_required,service_node_pubkey'))[:limit_awaiting]%}
<tr>
{%include 'include/sn_kcf.html'%}
<td>{{sn.total_contributed | loki(tag=false, fixed=true)}}</td>
<td>{{sn.total_contributed | oxen(tag=false, fixed=true)}}</td>
{%if sn.total_reserved >= sn.staking_requirement%}
<td title="All remaining contribution room is reserved for specific contributors">
⛔ {{sn.contribution_open | loki(tag=false, fixed=true)}}
⛔ {{sn.contribution_open | oxen(tag=false, fixed=true)}}
</td>
{%else%}
<td>{{sn.contribution_open | loki(tag=false, fixed=true)}}</td>
<td>{{sn.contribution_open | oxen(tag=false, fixed=true)}}</td>
{%endif%}
<td>{{ (0 if sn.num_contributions >= 4 else (sn.contribution_open / (4 - sn.num_contributions)) | round(method='ceil')) | loki(tag=false, fixed=true) }}</td>
<td>{{ (0 if sn.num_contributions >= 4 else (sn.contribution_open / (4 - sn.num_contributions)) | round(method='ceil')) | oxen(tag=false, fixed=true) }}</td>
<td>
{%if sn.requested_unlock_height%}
<span title="Service Node unlock in progress (unlocks at block {{sn.requested_unlock_height}})">🔓</span>

View File

@ -4,6 +4,6 @@
<td><a href="/sn/{{sn.service_node_pubkey}}">{{sn.service_node_pubkey}}</a></td>
<td title="
{%-for c in sn.contributors%}{%for lc in c.locked_contributions%}{{c.address | truncate(15)}} ({{lc.amount | loki(decimals=0)}} = {{(lc.amount / sn.staking_requirement * 100) | round(1) | chop0}}%)
{%-for c in sn.contributors%}{%for lc in c.locked_contributions%}{{c.address | truncate(15)}} ({{lc.amount | oxen(decimals=0)}} = {{(lc.amount / sn.staking_requirement * 100) | round(1) | chop0}}%)
{%endfor%}{%endfor%}"><span class="icon">{{sn.contributors | length}}/4</span></td>
<td>{%if sn.portions_for_operator != portions_base%}{{ (sn.portions_for_operator / portions_base * 100) | round(3) | chop0 }}{%endif%}</td>

View File

@ -3,10 +3,10 @@
(tx.info.rct_signatures.txnFee if 'rct_signatures' in tx.info and 'txnFee' in tx.info.rct_signatures else 0)
-%}
{% if fee > 0 or show_zero -%}
{{ fee | loki(tag=False, fixed=True, decimals=4) }}
{{ fee | oxen(tag=False, fixed=True, decimals=4) }}
{%- if 'burn_amount' in tx.extra %}
<span class="icon" title="= {{(fee - tx.extra.burn_amount) | loki}} TX fee
+ {{tx.extra.burn_amount | loki}} burned">🔥</span>
<span class="icon" title="= {{(fee - tx.extra.burn_amount) | oxen}} TX fee
+ {{tx.extra.burn_amount | oxen}} burned">🔥</span>
{%-endif-%}
{%endif-%}
{% endmacro %}

View File

@ -74,14 +74,14 @@
<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)">
<span><label>Staking requirement:</label> {{stake.staking_requirement | oxen}}</span>
<span title="{{(2500 * fees.fee_per_byte + 2*fees.fee_per_output) | oxen}} 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
{{fees.fee_per_output | oxen}}/output + {{(fees.fee_per_byte * 1000) | oxen}}/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)">
<span title="{{(2500 * fees.blink_fee_per_byte + 2*fees.blink_fee_per_output) | oxen}} 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
{{fees.blink_fee_per_output | oxen}}/output + {{(fees.blink_fee_per_byte * 1000) | oxen}}/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.">
<label>Block size limit:</label>
@ -96,10 +96,10 @@
{% if not emission or emission.status == 'BUSY' %}
(still calculating...)</span>
{% elif emission.status == 'OK' %}
{{(emission.emission_amount - 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>
{{(emission.emission_amount - emission.burn_amount) | oxen}}</span>
<span><label>(Coinbase:</label> {{emission.emission_amount | oxen}}</span>
<span><label>Fees:</label> {{emission.fee_amount | oxen}}</span>
<span><label>Burned:</label> {{emission.burn_amount | oxen}}<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.
@ -117,7 +117,7 @@
<span>🚫 Deregistration</span>
<span>📋 IP Change Penalty</span>
<span>🔓 Stake Unlock</span>
<span>🎫 Loki Name System Purchase</span>
<span>🎫 Oxen Name System Purchase</span>
<span>💾 LNS Update</span>
</h4>
</div>
@ -172,7 +172,7 @@
<td>{{symbol.display(b.txs[0])}}</td>
<td><a href="/tx/{{b.miner_tx_hash}}">{{b.miner_tx_hash}}</a></td>
<td>{{fee.display(b.txs[0])}}</td>
<td>{{b.reward | loki(tag=False, fixed=True, decimals=2)}}</td>
<td>{{b.reward | oxen(tag=False, fixed=True, decimals=2)}}</td>
<td>0/{{b.txs[0].info.vout | length}}</td>
<td>{{b.txs[0].size | si}}</td>
</tr>

View File

@ -13,7 +13,7 @@
<span>🚫 Deregistration</span>
<span>📋 IP Change Penalty</span>
<span>🔓 Stake Unlock</span>
<span>🎫 Loki Name System Purchase</span>
<span>🎫 Oxen Name System Purchase</span>
<span>💾 LNS Update</span>
</h4>
</div>

View File

@ -41,7 +41,7 @@
<a href="/block/{{sn.state_height}}">{{sn.state_height}}</a>
</span>
<span><label>Staking requirement:</label> {{sn.staking_requirement | loki}}</span>
<span><label>Staking requirement:</label> {{sn.staking_requirement | oxen}}</span>
{%if not solo_node%}
<span><label>Operator fee:</label> {{(sn.portions_for_operator / portions_base * 100) | round(3) | chop0}}%</span>
@ -50,12 +50,12 @@
<span><label>Total contributed:</label>
{%if sn.total_contributed >= sn.staking_requirement%}100%
{%else%}
{{sn.total_contributed | loki}} ({{(sn.total_contributed / sn.staking_requirement * 100) | round(2) | chop0}}%)
{{sn.total_contributed | oxen}} ({{(sn.total_contributed / sn.staking_requirement * 100) | round(2) | chop0}}%)
{%endif%}
</span>
{%if sn.total_reserved != sn.total_contributed%}
<span><label>Total reserved:</label> {{sn.total_reserved | loki}}</span>
<span><label>Total reserved:</label> {{sn.total_reserved | oxen}}</span>
{%endif%}
<span>
@ -132,10 +132,10 @@
{%endif%}
</p>
{%else%}
<p class="sn-awaiting">Awaiting registration. This service node has <span class="loki required">{{(sn.staking_requirement - sn.total_contributed) | loki}}</span>
<p class="sn-awaiting">Awaiting registration. This service node has <span class="oxen required">{{(sn.staking_requirement - sn.total_contributed) | oxen}}</span>
remaining to be contributed.
{%if sn.num_open_spots > 0%}
The minimum required stake contribution is <span class="loki required">{{((sn.staking_requirement - sn.total_reserved) / sn.num_open_spots) | loki}}</span>.
The minimum required stake contribution is <span class="oxen required">{{((sn.staking_requirement - sn.total_reserved) / sn.num_open_spots) | oxen}}</span>.
{%endif%}
</p>
{%endif%}
@ -166,12 +166,12 @@
{%for c in sn.contributors%}
<tr>
<td>{{c.address}}</td>
<td>{{c.amount | loki}}
<td>{{c.amount | oxen}}
{%-if c.locked_contributions and c.locked_contributions|length > 1%}
({{c.locked_contributions|length}} contributions)
{%endif-%}
</td>
<td>{{c.reserved | loki}}</td>
<td>{{c.reserved | oxen}}</td>
</tr>
{%endfor%}
</tbody>

View File

@ -58,7 +58,7 @@
{%else%}
{% import 'include/tx_fee.html' as fee %}
{{fee.display(tx)}}
({{(tx.info.rct_signatures.txnFee * 1000 / tx.size) | loki(tag=false, decimals=6)}})
({{(tx.info.rct_signatures.txnFee * 1000 / tx.size) | oxen(tag=false, decimals=6)}})
{%endif%}
</span>
<span title="{{tx.size}} bytes"><label>TX Size:</label> {{tx.size|si}}B</span>
@ -138,9 +138,9 @@ This tx does not includes a vote from this testing service node (only 7 votes ar
<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>
<h2>🎫 Oxen Name Service Registration</h2>
{% elif 'update' in tx.extra.lns %}
<h2>💾 Loki Name Service Update</h2>
<h2>💾 Oxen Name Service Update</h2>
{% endif %}
{#FIXME - show some metadata?#}
{% elif 'sn_registration' in tx.extra %}
@ -187,7 +187,7 @@ This tx does not includes a vote from this testing service node (only 7 votes ar
{%if tx.info.vout%}
<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>
{{tx.info.vout | sum(attribute='amount') | oxen(zero='<span title="Regular LOKI tx amounts are private">???</span>') | safe}}</h4>
<div class="TitleDivider"></div>
<div class="tx-outputs">
<table class="Table">
@ -203,7 +203,7 @@ This tx does not includes a vote from this testing service node (only 7 votes ar
{%for out in tx.info.vout%}
<tr>
<td><label>{{loop.index0}}:</label> {{out.target.key}}</td>
<td>{{out.amount | loki(zero='?')}}</td>
<td>{{out.amount | oxen(zero='?')}}</td>
<td>{%if 'output_indices' in tx%}{{tx.output_indices[loop.index0]}}{# FIXME: of {{num_outputs}}#}{%endif%}</td>
</tr>
{%endfor%}
@ -256,7 +256,7 @@ This tx does not includes a vote from this testing service node (only 7 votes ar
<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
command in <i>oxen-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
@ -522,7 +522,7 @@ This tx does not includes a vote from this testing service node (only 7 votes ar
<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>
{{tx.info.vin | sum(attribute='key.amount') | oxen(zero='<span title="Regular LOKI tx amounts are private">???</span>') | safe}}</h4>
<div class="TitleDivider"></div>
{#FIXME#}
@ -556,7 +556,7 @@ This tx does not includes a vote from this testing service node (only 7 votes ar
{%endif%}
{%endif%}
</td>
<td style="text-align: right">Amount: {{inp.key.amount | loki(zero='?')}}</td>
<td style="text-align: right">Amount: {{inp.key.amount | oxen(zero='?')}}</td>
</tr>
{%if config.enable_mixins_details%}
<tr class="TableHeader">

View File

@ -1,3 +1,3 @@
from observer import app, config
config.lokid_rpc = 'ipc://lokid/testnet.sock'
config.oxend_rpc = 'ipc://oxend/testnet.sock'