def api_emission(): 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) info = info.get() return flask.jsonify({ "data": { "blk_no": info['height'] - 1, "burn": coinbase["burn_amount"], "circulating_supply": coinbase["emission_amount"] - coinbase["burn_amount"], "coinbase": coinbase["emission_amount"] - coinbase["burn_amount"], "emission": coinbase["emission_amount"], "fee": coinbase["fee_amount"] }, "status": "success" })
def show_sn(pubkey): 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', info=info.get(), type='sn', id=pubkey, ) sn = sn['service_node_states'][0] # These are a bit non-trivial to properly calculate: # Number of staked contributions sn['num_contributions'] = sum( len(x["locked_contributions"]) for x in sn["contributors"] if "locked_contributions" in x) # Number of unfilled, reserved contribution spots: sn['num_reserved_spots'] = sum(x["amount"] < x["reserved"] for x in sn["contributors"]) # Available open contribution spots: sn['num_open_spots'] = 0 if sn['total_reserved'] >= sn[ 'staking_requirement'] else max( 0, 4 - sn['num_contributions'] - sn['num_reserved_spots']) return flask.render_template( 'sn.html', info=info.get(), hf=hfinfo.get(), sn=sn, )
def api_service_node_stats(): lmq, oxend = lmq_connection() info = FutureJSON(lmq, oxend, 'rpc.get_info', 1) stakinginfo = FutureJSON(lmq, oxend, 'rpc.get_staking_requirement', 30) sns = get_sns_future(lmq, oxend) sns = sns.get() if 'service_node_states' not in sns: return flask.jsonify({"status": "Error retrieving SN stats"}), 500 sns = sns['service_node_states'] stats = {'active': 0, 'funded': 0, 'awaiting_contribution': 0, 'decommissioned': 0, 'staked': 0} for sn in sns: if sn['funded']: stats['funded'] += 1 if sn['active']: stats['active'] += 1 else: stats['decommissioned'] += 1 else: stats['awaiting_contribution'] += 1 stats['staked'] += sn['total_contributed'] stats['staked'] /= 1_000_000_000 stats['sn_reward'] = 16.5 stats['sn_reward_interval'] = stats['active'] stakinginfo = stakinginfo.get() stats['sn_staking_requirement_full'] = stakinginfo['staking_requirement'] / 1_000_000_000 stats['sn_staking_requirement_min'] = stats['sn_staking_requirement_full'] / 4 info = info.get() stats['height'] = info['height'] return flask.jsonify({"data": stats, "status": "OK"})
def api_tx(txid): lmq, oxend = lmq_connection() tx = tx_req(lmq, oxend, [txid]).get() txs = parse_txs(tx) return flask.jsonify({ "status": tx['status'], "data": (txs[0] if txs else None), })
def show_quorums(): 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(), quorums=get_quorums(quos))
def mempool(): 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(), mempool=parse_mempool(mempool), )
def api_networkinfo(): 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} hfinfo = hfinfo.get() data['current_hf_version'] = hfinfo['version'] data['next_hf_height'] = hfinfo['earliest_height'] if 'earliest_height' in hfinfo else None return flask.jsonify({"data": data, "status": "OK"})
def sns(): 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(), active_sns=active, awaiting_sns=awaiting, inactive_sns=inactive, )
def show_block(height=None, hash=None, more_details=False): 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, oxend, val).get() if block is None: return flask.render_template("not_found.html", info=info.get(), hfinfo=hfinfo.get(), type='block', height=height, id=hash) next_block = None block_height = block['block_header']['height'] txs = get_block_txs_future(lmq, oxend, block) if info.get()['height'] > 1 + block_height: next_block = block_header_req(lmq, oxend, '{}'.format(block_height + 1)) if more_details: formatter = HtmlFormatter(cssclass="syntax-highlight", style="native") more_details = { 'details_css': formatter.get_style_defs('.syntax-highlight'), 'details_html': highlight(json.dumps(block, indent="\t", sort_keys=True), JsonLexer(), formatter), } else: more_details = {} transactions = [] if txs is None else parse_txs(txs.get()).copy() miner_tx = transactions.pop() if transactions else [] return flask.render_template( "block.html", info=info.get(), hfinfo=hfinfo.get(), block_header=block['block_header'], block=block, miner_tx=miner_tx, transactions=transactions, next_block=next_block.get() if next_block else None, **more_details, )
def api_block(blkid=None, height=None): 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() data["txs"] = parse_txs(txs.get()).copy() return flask.jsonify({ "status": block['status'], "data": data, })
def api_circulating_supply(): 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"]) // 1_000_000_000 if coinbase else None)
def search(): 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 return flask.redirect(flask.url_for('show_block', height=val), code=301) if val and len(val) == 58 and val.endswith(".snode") and val[51] in 'yoYO' and all(c in base32z_dict for c in val[0:52].lower()): v, bits = 0, 0 for x in val[0:52].lower(): v = (v << 5) | base32z_map[x] # Arbitrary precision integers hurray! # The above loads 260 bytes (5 bits per char * 52 chars), but we only want 256: v >>= 4 val = "{:64x}".format(v) elif not val or len(val) != 64 or any(c not in string.hexdigits for c in val): return flask.render_template('not_found.html', info=info.get(), type='bad_search', id=val, ) # Initiate all the lookups at once, then redirect to whichever one responds affirmatively 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 sn and 'service_node_states' in sn and sn['service_node_states']: return flask.redirect(flask.url_for('show_sn', pubkey=val), code=301) bl = blreq.get() if bl and 'block_header' in bl and bl['block_header']: return flask.redirect(flask.url_for('show_block', hash=val), code=301) tx = txreq.get() if tx and 'txs' in tx and tx['txs']: return flask.redirect(flask.url_for('show_tx', txid=val), code=301) return flask.render_template('not_found.html', info=info.get(), type='search', id=val, )
def search(): 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 return flask.redirect(flask.url_for('show_block', height=val), code=301) if not val or len(val) != 64 or any(c not in string.hexdigits for c in val): return flask.render_template( 'not_found.html', info=info.get(), type='bad_search', id=val, ) # Initiate all the lookups at once, then redirect to whichever one responds affirmatively 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']: return flask.redirect(flask.url_for('show_sn', pubkey=val), code=301) bl = blreq.get() if bl and 'block_header' in bl and bl['block_header']: return flask.redirect(flask.url_for('show_block', hash=val), code=301) tx = txreq.get() if tx and 'txs' in tx and tx['txs']: return flask.redirect(flask.url_for('show_tx', txid=val), code=301) return flask.render_template( 'not_found.html', info=info.get(), type='search', id=val, )
def show_tx(txid, more_details=False): 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', info=info.get(), type='tx', id=txid, ) tx = parse_txs(txs)[0] # 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, 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} } } 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 = [] for inp in tx['info']['vin']: # Key positions are stored as offsets from the previous index rather than indices, # so de-delta them back into indices: if 'key_offsets' in inp['key'] and 'key_indices' not in inp[ 'key']: kis = [] inp['key']['key_indices'] = kis kbase = 0 for koff in inp['key']['key_offsets']: kbase += koff kis.append(kbase) 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, 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, oxend, '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 kindex_info: kindex_info[amount] = {} ki = kindex_info[amount] for ko in inp['key']['key_indices']: ki[ko] = outputs[i] i += 1 if more_details: formatter = HtmlFormatter(cssclass="syntax-highlight", style="paraiso-dark") 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 if testing_quorum: testing_quorum = testing_quorum.get() if testing_quorum: if 'quorums' in testing_quorum and testing_quorum['quorums']: testing_quorum = testing_quorum['quorums'][0]['quorum'] else: testing_quorum = None return flask.render_template( 'tx.html', info=info.get(), tx=tx, kindex_info=kindex_info, block_info=block_info, testing_quorum=testing_quorum, **more_details, )
def show_block_latest(): 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)
def main(refresh=None, page=0, per_page=None, first=None, last=None): 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 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, oxend, 'admin.get_coinbase_tx_sum', 10, timeout=1, fail_okay=True, 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: per_page = config.blocks_per_page else: custom_per_page = '/{}'.format(per_page) # We have some chained request dependencies here and below, so get() them as needed; all other # non-dependent requests should already have a future initiated above so that they can # potentially run in parallel. info = inforeq.get() height = info['height'] # Permalinked block range: if first is not None and last is not None and 0 <= first <= last and last <= first + 99: start_height, end_height = first, last if end_height - start_height + 1 != per_page: per_page = end_height - start_height + 1 custom_per_page = '/{}'.format(per_page) # We generally can't get a perfect page number because our range (e.g. 5-14) won't line up # with pages (e.g. 10-19, 0-19), so just get as close as we can. Next/Prev page won't be # quite right, but they'll be within half a page. page = round((height - 1 - end_height) / per_page) else: end_height = max(0, height - per_page * page - 1) start_height = max(0, end_height - per_page + 1) blocks = FutureJSON(lmq, oxend, 'rpc.get_block_headers_range', cache_key='main', args={ 'start_height': start_height, 'end_height': end_height, 'get_tx_hashes': True, }).get()['headers'] # If 'txs' is already there then it is probably left over from our cached previous call through # here. if blocks and 'txs' not in blocks[0]: txids = [] for b in blocks: b['txs'] = [] txids.append(b['miner_tx_hash']) if 'tx_hashes' in b: txids += b['tx_hashes'] txs = 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 # height changes rather than needing to search for the block if blocks[i]['height'] != tx['block_height']: i += 1 while i < len( blocks) and blocks[i]['height'] != tx['block_height']: print("Something getting wrong: missing txes?", file=sys.stderr) i += 1 if i >= len(blocks): print("Something getting wrong: have leftover txes") break blocks[i]['txs'].append(tx) # Clean up the SN data a bit to make things easier for the templates awaiting_sns, active_sns, inactive_sns = get_sns(sns, inforeq) return flask.render_template( 'index.html', info=info, stake=stake.get(), fees=base_fee.get(), emission=coinbase.get(), hf=hfinfo.get(), active_sns=active_sns, inactive_sns=inactive_sns, awaiting_sns=awaiting_sns, blocks=blocks, block_size_median=statistics.median(b['block_size'] for b in blocks), page=page, per_page=per_page, custom_per_page=custom_per_page, mempool=parse_mempool(mempool), checkpoints=checkpoints.get(), refresh=refresh, )