Module jumpscale.packages.stellar_stats.bottle.stats_service

Expand source code
#!/usr/bin/env python
# pylint: disable=no-value-for-parameter

from jumpscale.loader import j
import click
import stellar_sdk
import datetime

from urllib import parse


_ASSET_ISUERS = {
    "test": {
        "TFT": "GA47YZA3PKFUZMPLQ3B5F2E3CJIB57TGGU7SPCQT2WAEYKN766PWIMB3",
        "FreeTFT": "GBLDUINEFYTF7XEE7YNWA3JQS4K2VD37YU7I2YAE7R5AHZDKQXSS2J6R",
        "TFTA": "GB55A4RR4G2MIORJTQA4L6FENZU7K4W7ATGY6YOT2CW47M5SZYGYKSCT",
    },
    "public": {
        "TFT": "GBOVQKJYHXRR3DX6NOX2RRYFRCUMSADGDESTDNBDS6CDVLGVESRTAC47",
        "FreeTFT": "GCBGS5TFE2BPPUVY55ZPEMWWGR6CLQ7T6P46SOFGHXEBJ34MSP6HVEUT",
        "TFTA": "GBUT4GP5GJ6B3XW5PXENHQA7TXJI5GOPW3NF4W3ZIW6OOO4ISY6WNLN2",
    },
}
_HORIZON_NETWORKS = {"test": "https://horizon-testnet.stellar.org", "public": "https://horizon.stellar.org"}
_THREEFOLDFOUNDATION_TFTSTELLAR_SERVICES = {"test": "testnet.threefold.io", "public": "tokenservices.threefold.io"}

_NETWORK_PASSPHRASES = {
    "test": stellar_sdk.Network.TESTNET_NETWORK_PASSPHRASE,
    "public": stellar_sdk.Network.PUBLIC_NETWORK_PASSPHRASE,
}


class StatisticsCollector(object):
    """
    Gathers statistics on TFT, TFTA and FreeTFT.
    Reuse an instance of this class to cache the locktimes of escrow accounts and speed up the detailed statistics gathering
    """

    def __init__(self, network: str):
        self._network = network
        self.locked_accounts = {}

    @property
    def _horizon_server(self):
        server_url = _HORIZON_NETWORKS[self._network]
        return stellar_sdk.Server(horizon_url=server_url)

    @property
    def _network_passphrase(self):
        return _NETWORK_PASSPHRASES[self._network]

    def get_accounts(self, tokencode: str, issuer: str):
        locked_accounts = []
        horizon_server = self._horizon_server
        asset = stellar_sdk.Asset(tokencode, issuer)
        accounts_endpoint = horizon_server.accounts().for_asset(asset).limit(50)
        old_cursor = "old"
        new_cursor = ""
        while new_cursor != old_cursor:
            old_cursor = new_cursor
            accounts_endpoint.cursor(new_cursor)
            response = accounts_endpoint.call()
            next_link = response["_links"]["next"]["href"]
            next_link_query = parse.urlsplit(next_link).query
            new_cursor = parse.parse_qs(next_link_query)["cursor"][0]
            accounts = response["_embedded"]["records"]
            for account in accounts:
                account_id = account["account_id"]
                preauth_signers = [signer["key"] for signer in account["signers"] if signer["type"] == "preauth_tx"]
                tokenbalances = [
                    float(b["balance"])
                    for b in account["balances"]
                    if b["asset_type"] == "credit_alphanum4"
                    and b["asset_code"] == tokencode
                    and b["asset_issuer"] == issuer
                ]
                tokenbalance = tokenbalances[0] if tokenbalances else 0
                if len(preauth_signers) > 0:
                    locked_accounts.append(
                        {"account": account_id, "amount": tokenbalance, "preauth_signers": preauth_signers}
                    )
        return locked_accounts

    def _get_url(self, endpoint):
        url = _THREEFOLDFOUNDATION_TFTSTELLAR_SERVICES[self._network]
        if not j.sals.nettools.wait_connection_test(url, 443, 5):
            raise j.exceptions.Timeout(f"Can not connect to server {url}, connection timeout")
        return f"https://{url}{endpoint}"

    def _get_unlockhash_transaction(self, unlockhash):
        data = {"unlockhash": unlockhash}
        resp = j.tools.http.post(
            self._get_url("/threefoldfoundation/unlock_service/get_unlockhash_transaction"), json={"args": data}
        )
        resp.raise_for_status()
        return resp.json()

    def lookup_lock_time(self, preauth_signer: str):
        unlock_tx = self._get_unlockhash_transaction(unlockhash=preauth_signer)
        if unlock_tx is None:
            return None
        txe = stellar_sdk.TransactionEnvelope.from_xdr(unlock_tx["transaction_xdr"], self._network_passphrase)
        tx = txe.transaction
        if tx.time_bounds is not None:
            return tx.time_bounds.min_time
        return None

    def getstatistics(self, tokencode: str, detailed: bool):
        stats = {"asset": tokencode}
        horizon_server = self._horizon_server
        asset_issuer = _ASSET_ISUERS[self._network][tokencode]
        stats["issuer"] = asset_issuer
        response = horizon_server.assets().for_code(tokencode).for_issuer(asset_issuer).call()
        record = response["_embedded"]["records"][0]
        stats["total"] = float(record["amount"])
        stats["num_accounts"] = record["num_accounts"]
        locked_accounts = self.get_accounts(tokencode, asset_issuer)
        total_locked = 0.0
        for locked_account in locked_accounts:
            total_locked += locked_account["amount"]
        stats["total_locked"] = total_locked
        if not detailed:
            return stats
        amounts_per_locktime = {}
        for locked_account in locked_accounts:
            if locked_account["account"] in self.locked_accounts:
                locked_account["until"] = self.locked_accounts[locked_account["account"]]["until"]
            else:
                locked_account["until"] = self.lookup_lock_time(locked_account["preauth_signers"][0])
                self.locked_accounts[locked_account["account"]] = locked_account
        for locked_account in locked_accounts:
            amount = amounts_per_locktime.get(locked_account["until"], 0.0)
            amount += locked_account["amount"]
            amounts_per_locktime[locked_account["until"]] = amount
        locked_amounts = []
        for until, amount in amounts_per_locktime.items():
            locked_amounts.append({"until": until, "amount": amount})

        def sort_key(a):
            return a["until"]

        locked_amounts.sort(key=sort_key)
        stats["locked"] = locked_amounts
        return stats


@click.command(help="Statistics about TFT, TFTA and FreeTFT")
@click.argument("tokencode", type=click.Choice(["TFT", "TFTA", "FreeTFT"]), default="TFT")
@click.option("--network", type=click.Choice(["test", "public"], case_sensitive=False), default="public")
@click.option("--detailed/--not-detailed", default=False)
def show_stats(tokencode, network, detailed):
    print(f"Statistics for {tokencode} on the {network} network")
    collector = StatisticsCollector(network)
    stats = collector.getstatistics(tokencode, detailed)
    print(f"Total amount of tokens: {stats['total']:,.7f}")
    print(f"Number of accounts: {stats['num_accounts']}")
    print(f"Amount of locked tokens: {stats['total_locked']:,.7f}")
    if detailed:
        for locked_amount in stats["locked"]:
            print(
                f"{locked_amount['amount']:,.7f} locked until {datetime.datetime.fromtimestamp(locked_amount['until'])}"
            )


if __name__ == "__main__":
    show_stats()

Classes

class StatisticsCollector (network: str)

Gathers statistics on TFT, TFTA and FreeTFT. Reuse an instance of this class to cache the locktimes of escrow accounts and speed up the detailed statistics gathering

Expand source code
class StatisticsCollector(object):
    """
    Gathers statistics on TFT, TFTA and FreeTFT.
    Reuse an instance of this class to cache the locktimes of escrow accounts and speed up the detailed statistics gathering
    """

    def __init__(self, network: str):
        self._network = network
        self.locked_accounts = {}

    @property
    def _horizon_server(self):
        server_url = _HORIZON_NETWORKS[self._network]
        return stellar_sdk.Server(horizon_url=server_url)

    @property
    def _network_passphrase(self):
        return _NETWORK_PASSPHRASES[self._network]

    def get_accounts(self, tokencode: str, issuer: str):
        locked_accounts = []
        horizon_server = self._horizon_server
        asset = stellar_sdk.Asset(tokencode, issuer)
        accounts_endpoint = horizon_server.accounts().for_asset(asset).limit(50)
        old_cursor = "old"
        new_cursor = ""
        while new_cursor != old_cursor:
            old_cursor = new_cursor
            accounts_endpoint.cursor(new_cursor)
            response = accounts_endpoint.call()
            next_link = response["_links"]["next"]["href"]
            next_link_query = parse.urlsplit(next_link).query
            new_cursor = parse.parse_qs(next_link_query)["cursor"][0]
            accounts = response["_embedded"]["records"]
            for account in accounts:
                account_id = account["account_id"]
                preauth_signers = [signer["key"] for signer in account["signers"] if signer["type"] == "preauth_tx"]
                tokenbalances = [
                    float(b["balance"])
                    for b in account["balances"]
                    if b["asset_type"] == "credit_alphanum4"
                    and b["asset_code"] == tokencode
                    and b["asset_issuer"] == issuer
                ]
                tokenbalance = tokenbalances[0] if tokenbalances else 0
                if len(preauth_signers) > 0:
                    locked_accounts.append(
                        {"account": account_id, "amount": tokenbalance, "preauth_signers": preauth_signers}
                    )
        return locked_accounts

    def _get_url(self, endpoint):
        url = _THREEFOLDFOUNDATION_TFTSTELLAR_SERVICES[self._network]
        if not j.sals.nettools.wait_connection_test(url, 443, 5):
            raise j.exceptions.Timeout(f"Can not connect to server {url}, connection timeout")
        return f"https://{url}{endpoint}"

    def _get_unlockhash_transaction(self, unlockhash):
        data = {"unlockhash": unlockhash}
        resp = j.tools.http.post(
            self._get_url("/threefoldfoundation/unlock_service/get_unlockhash_transaction"), json={"args": data}
        )
        resp.raise_for_status()
        return resp.json()

    def lookup_lock_time(self, preauth_signer: str):
        unlock_tx = self._get_unlockhash_transaction(unlockhash=preauth_signer)
        if unlock_tx is None:
            return None
        txe = stellar_sdk.TransactionEnvelope.from_xdr(unlock_tx["transaction_xdr"], self._network_passphrase)
        tx = txe.transaction
        if tx.time_bounds is not None:
            return tx.time_bounds.min_time
        return None

    def getstatistics(self, tokencode: str, detailed: bool):
        stats = {"asset": tokencode}
        horizon_server = self._horizon_server
        asset_issuer = _ASSET_ISUERS[self._network][tokencode]
        stats["issuer"] = asset_issuer
        response = horizon_server.assets().for_code(tokencode).for_issuer(asset_issuer).call()
        record = response["_embedded"]["records"][0]
        stats["total"] = float(record["amount"])
        stats["num_accounts"] = record["num_accounts"]
        locked_accounts = self.get_accounts(tokencode, asset_issuer)
        total_locked = 0.0
        for locked_account in locked_accounts:
            total_locked += locked_account["amount"]
        stats["total_locked"] = total_locked
        if not detailed:
            return stats
        amounts_per_locktime = {}
        for locked_account in locked_accounts:
            if locked_account["account"] in self.locked_accounts:
                locked_account["until"] = self.locked_accounts[locked_account["account"]]["until"]
            else:
                locked_account["until"] = self.lookup_lock_time(locked_account["preauth_signers"][0])
                self.locked_accounts[locked_account["account"]] = locked_account
        for locked_account in locked_accounts:
            amount = amounts_per_locktime.get(locked_account["until"], 0.0)
            amount += locked_account["amount"]
            amounts_per_locktime[locked_account["until"]] = amount
        locked_amounts = []
        for until, amount in amounts_per_locktime.items():
            locked_amounts.append({"until": until, "amount": amount})

        def sort_key(a):
            return a["until"]

        locked_amounts.sort(key=sort_key)
        stats["locked"] = locked_amounts
        return stats

Methods

def get_accounts(self, tokencode: str, issuer: str)
Expand source code
def get_accounts(self, tokencode: str, issuer: str):
    locked_accounts = []
    horizon_server = self._horizon_server
    asset = stellar_sdk.Asset(tokencode, issuer)
    accounts_endpoint = horizon_server.accounts().for_asset(asset).limit(50)
    old_cursor = "old"
    new_cursor = ""
    while new_cursor != old_cursor:
        old_cursor = new_cursor
        accounts_endpoint.cursor(new_cursor)
        response = accounts_endpoint.call()
        next_link = response["_links"]["next"]["href"]
        next_link_query = parse.urlsplit(next_link).query
        new_cursor = parse.parse_qs(next_link_query)["cursor"][0]
        accounts = response["_embedded"]["records"]
        for account in accounts:
            account_id = account["account_id"]
            preauth_signers = [signer["key"] for signer in account["signers"] if signer["type"] == "preauth_tx"]
            tokenbalances = [
                float(b["balance"])
                for b in account["balances"]
                if b["asset_type"] == "credit_alphanum4"
                and b["asset_code"] == tokencode
                and b["asset_issuer"] == issuer
            ]
            tokenbalance = tokenbalances[0] if tokenbalances else 0
            if len(preauth_signers) > 0:
                locked_accounts.append(
                    {"account": account_id, "amount": tokenbalance, "preauth_signers": preauth_signers}
                )
    return locked_accounts
def getstatistics(self, tokencode: str, detailed: bool)
Expand source code
def getstatistics(self, tokencode: str, detailed: bool):
    stats = {"asset": tokencode}
    horizon_server = self._horizon_server
    asset_issuer = _ASSET_ISUERS[self._network][tokencode]
    stats["issuer"] = asset_issuer
    response = horizon_server.assets().for_code(tokencode).for_issuer(asset_issuer).call()
    record = response["_embedded"]["records"][0]
    stats["total"] = float(record["amount"])
    stats["num_accounts"] = record["num_accounts"]
    locked_accounts = self.get_accounts(tokencode, asset_issuer)
    total_locked = 0.0
    for locked_account in locked_accounts:
        total_locked += locked_account["amount"]
    stats["total_locked"] = total_locked
    if not detailed:
        return stats
    amounts_per_locktime = {}
    for locked_account in locked_accounts:
        if locked_account["account"] in self.locked_accounts:
            locked_account["until"] = self.locked_accounts[locked_account["account"]]["until"]
        else:
            locked_account["until"] = self.lookup_lock_time(locked_account["preauth_signers"][0])
            self.locked_accounts[locked_account["account"]] = locked_account
    for locked_account in locked_accounts:
        amount = amounts_per_locktime.get(locked_account["until"], 0.0)
        amount += locked_account["amount"]
        amounts_per_locktime[locked_account["until"]] = amount
    locked_amounts = []
    for until, amount in amounts_per_locktime.items():
        locked_amounts.append({"until": until, "amount": amount})

    def sort_key(a):
        return a["until"]

    locked_amounts.sort(key=sort_key)
    stats["locked"] = locked_amounts
    return stats
def lookup_lock_time(self, preauth_signer: str)
Expand source code
def lookup_lock_time(self, preauth_signer: str):
    unlock_tx = self._get_unlockhash_transaction(unlockhash=preauth_signer)
    if unlock_tx is None:
        return None
    txe = stellar_sdk.TransactionEnvelope.from_xdr(unlock_tx["transaction_xdr"], self._network_passphrase)
    tx = txe.transaction
    if tx.time_bounds is not None:
        return tx.time_bounds.min_time
    return None