View on GitHub
File Changes
+
#!/usr/bin/env nix-shell
+
#!nix-shell -p python3Packages.requests python3Packages.tabulate -i python3
+

                      
+
"""
+
Jormungandr Analysis Tools
+
"""
+

                      
+
__version__ = "0.1.0"
+

                      
+
import argparse, requests, os, json, sys
+
from argparse import RawTextHelpFormatter
+
from requests.exceptions import HTTPError
+
from tabulate import tabulate
+

                      
+

                      
+
globalAggregate = None
+
globalEpochBlocks = None
+
globalPools = None
+

                      
+
api_url_base = None
+
api_url = None
+

                      
+

                      
+
def get_api(path):
+
    r = endpoint(f'{api_url}/{path}')
+
    return r.text
+

                      
+

                      
+
def get_tip():
+
    return get_api("tip")
+

                      
+

                      
+
def get_block(block_id):
+
    r = endpoint(f'{api_url}/block/{block_id}')
+
    hex_block = r.content.hex()
+
    return hex_block
+

                      
+

                      
+
def parse_block(block):
+
    return {
+
      "epoch": int(block[16:24], 16),
+
      "slot": int(block[24:32], 16),
+
      "parent": block[104:168],
+
      "pool": block[168:232],
+
    }
+

                      
+

                      
+
def aggregate(silent=False):
+

                      
+
    global globalAggregate
+
    global globalEpochBlocks
+
    tip = get_tip()
+
    block = parse_block(get_block(tip))
+
    epochBlockTotal = {}
+
    currentEpoch = block['epoch']
+
    epochs = {}
+
    pools = {}
+

                      
+
    while block["parent"] != ("0" * 64):
+
        if args.full == False:
+
            if (currentEpoch - args.aggregate + 1) > block['epoch']:
+
                break
+
        epoch = block['epoch']
+
        parent = block['parent']
+
        pool = block['pool']
+
        if epoch not in epochs:
+
            epochs[epoch] = {}
+
            epochBlockTotal[epoch] = 0
+

                      
+
        if pool not in epochs[epoch]:
+
            epochs[epoch][pool] = {}
+
            epochs[epoch][pool]['blocks'] = 1
+
            epochBlockTotal[epoch] = epochBlockTotal[epoch] + 1
+
        else:
+
            epochs[epoch][pool]['blocks'] = epochs[epoch][pool]['blocks'] + 1
+
            epochBlockTotal[epoch] = epochBlockTotal[epoch] + 1
+
        block = parse_block(get_block(block['parent']))
+

                      
+
    for epoch, epochData in epochs.items():
+
        epochs[epoch]['stats'] = {}
+
        epochs[epoch]['stats']['blocksum'] = epochBlockTotal[epoch]
+
        for pool, poolData in epochData.items():
+
            if pool != 'stats':
+
                epochs[epoch][pool]['percent'] = poolData['blocks'] / epochBlockTotal[epoch] * 100
+

                      
+
    if silent == False:
+
        if args.json == True:
+
            print(json.dumps(epochs, sort_keys=True))
+
        else:
+
            print('\nJormungandr Epoch Block Aggregate:\n')
+
            for epoch, epochData in epochs.items():
+
                headers = [f'EPOCH {epoch}, Pool (Node ID)', "Blocks (#)", "Block Percent (%)"]
+
                table = []
+
                for pool, data in epochData.items():
+
                    if pool != 'stats':
+
                        record = [ pool, data['blocks'], data['percent'] ]
+
                        table.append(record)
+
                if args.bigvaluesort == True:
+
                    print(f'{tabulate(sorted(table, key=lambda x: x[1], reverse=True), headers, tablefmt="psql")}')
+
                else:
+
                    print(f'{tabulate(sorted(table, key=lambda x: x[0]), headers, tablefmt="psql")}')
+
                print(f'{"Totalblocks:":<21}{epochData["stats"]["blocksum"]}\n\n')
+
    globalAggregate = epochs
+

                      
+

                      
+
def distribution(silent=False):
+
    global globalPools
+
    epoch = 0
+
    unassigned = 0
+
    dangling = 0
+
    stakeSum = 0
+
    totalPercentStaked = 0
+
    total = 0
+
    pools = {}
+

                      
+
    r = endpoint(f'{api_url}/stake')
+
    raw = r.json()
+

                      
+
    epoch = raw['epoch']
+
    dangling = raw['stake']['dangling']
+
    unassigned = raw['stake']['unassigned']
+

                      
+
    if args.bigvaluesort == True:
+
        sortedRaw = sorted(raw['stake']['pools'], key = lambda x: x[1], reverse=True)
+
    else:
+
        sortedRaw = sorted(raw['stake']['pools'])
+
    for [pool, stake] in sortedRaw:
+
        pools[pool] = {}
+
        pools[pool]['stake'] = stake
+
        pools[pool]['percent'] = 0
+
        stakeSum = stakeSum + stake
+

                      
+
    total = stakeSum + unassigned + dangling
+
    totalPercentStaked = stakeSum / total
+

                      
+
    # Calculate percentage stake delegation of total staked ADA
+
    for pool in pools.keys():
+
        pools[pool]['percent'] = pools[pool]['stake'] / stakeSum * 100
+

                      
+
    pools['stats'] = {}
+
    pools['stats']['epoch'] = epoch
+
    pools['stats']['dangling'] = dangling
+
    pools['stats']['unassigned'] = unassigned
+
    pools['stats']['total'] = total
+
    pools['stats']['stakesum'] = stakeSum
+
    pools['stats']['totalpercentstaked'] = totalPercentStaked
+

                      
+
    if silent == False:
+
        if args.json == True:
+
            print(json.dumps(pools, sort_keys=True))
+
        else:
+
            print('\nJormungandr Stake Pool Distribution:\n')
+
            print(f'{"Epoch:":<21}{epoch}')
+
            print(f'{"Dangling:":<21}{dangling / 1e6:,.6f} ADA')
+
            print(f'{"Unassigned:":<21}{unassigned / 1e6:,.6f} ADA')
+
            print(f'{"Total:":<21}{total / 1e6:,.6f} ADA')
+
            print(f'{"TotalStaked:":<21}{stakeSum / 1e6:,.6f} ADA')
+
            print(f'{"TotalPercentStaked:":<21}{totalPercentStaked * 100:.2f}%\n')
+
            headers = [f'EPOCH {epoch}, Pool (Node ID)', "Stake (ADA)", "Percent (%)"]
+
            table = []
+
            for pool, poolData in pools.items():
+
                if pool != 'stats':
+
                    if args.nozero == False or poolData['stake'] != 0:
+
                        record = [ pool, poolData['stake'] / 1e6, poolData['percent'] ]
+
                        table.append(record)
+
            if args.bigvaluesort == True:
+
                print(f'{tabulate(sorted(table, key=lambda x: x[1], reverse=True), headers, tablefmt="psql", floatfmt=("%s", "0.6f"))}\n\n')
+
            else:
+
                print(f'{tabulate(sorted(table, key=lambda x: x[0]), headers, tablefmt="psql", floatfmt=("%s", "0.6f"))}\n\n')
+
    globalPools = pools
+

                      
+

                      
+
def crossref():
+

                      
+
    if globalAggregate == None:
+
        args.aggregate = 1
+
        aggregate(silent=True)
+

                      
+
    if globalPools == None:
+
        distribution(silent=True)
+

                      
+
    crossref = globalPools
+
    epoch = crossref['stats']['epoch']
+
    for pool, poolData in crossref.items():
+
        if pool != 'stats':
+
            if pool in globalAggregate[epoch]:
+
                crossref[pool]['blocks'] = globalAggregate[epoch][pool]['blocks']
+
                crossref[pool]['percentBlocks'] = globalAggregate[epoch][pool]['percent']
+
            else:
+
                crossref[pool]['blocks'] = None
+
                crossref[pool]['percentBlocks'] = None
+

                      
+
    if args.json == True:
+
        print(json.dumps(crossref, sort_keys=True))
+
    else:
+
        print('\nJormungandr Stake and Block Distribution Cross Reference:\n')
+
        print(f'{"Epoch:":<21}{epoch}')
+
        print(f'{"Dangling:":<21}{crossref["stats"]["dangling"] / 1e6:,.6f} ADA')
+
        print(f'{"Unassigned:":<21}{crossref["stats"]["unassigned"] / 1e6:,.6f} ADA')