Source code for dposlib.ark.v2

# -*- coding: utf-8 -*-
# © Toons

import io
import os
import sys
import pytz
import pprint
import hashlib

from datetime import datetime
from importlib import import_module

from dposlib import rest, PY3, HOME
from dposlib.ark import crypto
from dposlib.ark.v2 import api
from dposlib.blockchain import cfg, slots, Transaction
from dposlib.util.asynch import setInterval
from dposlib.util.bin import hexlify, unhexlify

cfg.headers["API-Version"] = "2"

DAEMON_PEERS = None
TRANSACTIONS = {
    0: "transfer",
    1: "secondSignature",
    2: "delegateRegistration",
    3: "vote",
    4: "multiSignature",
    5: "ipfs",
    6: "multiPayment",
    7: "delegateResignation",
    8: "htlcLock",
    9: "htlcClaim",
    10: "htlcRefund",
}
TYPING = {
    "amount": int,
    "asset": dict,
    "blockId": str,
    "confirmations": int,
    "expiration": int,
    "fee": int,
    "id": str,
    "MultiSignatureAddress": str,
    "network": int,
    "nonce": int,
    "recipientId": str,
    "senderPublicKey": str,
    "senderId": str,
    "signature": str,
    "signSignature": str,
    "signatures": list,
    "timestamp": int,
    "timelockType": int,
    "timelock": int,
    "type": int,
    "typeGroup": int,
    "vendorField": str,
    "vendorFieldHex": str,
    "version": int,
}


def _select_peers():
    api_port = cfg.ports["core-api"]
    peers = []
    candidates = rest.GET.api.peers(
        version=cfg.version,
        orderBy="height:desc"
    ).get("data", [])
    for candidate in candidates:
        peers.append("http://%s:%s" % (candidate["ip"], api_port))
        if len(peers) >= cfg.broadcast:
            break
    if len(peers):
        cfg.peers = peers


@setInterval(30)
def _rotate_peers():
    _select_peers()


def _write_module(path, configuration={}, fees={}):
    with io.open(
        path, "w" if PY3 else "wb", **({"encoding": "utf-8"} if PY3 else {})
    ) as module:
        module.write(
            "# -*- coding: utf-8 -*-\n"
            "# automatically generated by dposlib.ark.v2 module\n\n"
        )
        module.write("configuration = ")
        module.write(pprint.pformat(configuration))
        module.write("\nfees = ")
        module.write(pprint.pformat(fees))
        module.write("\n")


def init(seed=None):
    """
    Blockchain initialisation. It stores root values in :mod:`cfg` modules.
    """
    global DAEMON_PEERS

    # configure cold package path and fils according to installation
    if ".zip" in __file__ or ".egg" in __file__:
        # --> module loaded from zip or egg file
        path_module = os.path.join(HOME, cfg.network + ".py")
        package_path = cfg.network
    else:
        # --> module loaded from python package
        path_module = os.path.join(
            os.path.join(__path__[0], "cold"), cfg.network + ".py"
        )
        package_path = __package__ + ".cold." + cfg.network
    path_module = os.path.normpath(path_module)

    # if network connection available
    if cfg.hotmode:
        CONFIG = rest.GET(* "api/node/configuration".split("/"), peer=seed)
        # nethash must be added before next api endpoint call
        cfg.headers["nethash"] = CONFIG["data"]["nethash"]
        FEES = rest.GET(* "api/node/fees".split("/"), peer=seed)
        # write configuration in python module, overriding former one
        _write_module(path_module, CONFIG, FEES)
    else:
        # remove cold package
        if hasattr(sys.modules[__package__], "cold"):
            del sys.modules[__package__].cold
        # load cold package
        try:
            sys.modules[__package__].cold = import_module(
                package_path
            )
            CONFIG = sys.modules[__package__].cold.configuration
            FEES = sys.modules[__package__].cold.fees
        except Exception:
            CONFIG = FEES = {}

    # no network connetcion neither local configuration files
    if "data" not in CONFIG:
        raise Exception("no data available")

    data = CONFIG.get("data", {})
    constants = data["constants"]

    # -- root configuration ---------------------------------------------------
    cfg.version = data.get("core", {}).get("version", "2")
    cfg.explorer = data["explorer"]
    cfg.marker = "%x" % data["version"]
    cfg.pubkeyHash = data["version"]
    cfg.token = data["token"]
    cfg.symbol = data["symbol"]
    cfg.ports = dict(
        [k.split("/")[-1], v] for k, v in data["ports"].items()
    )
    cfg.activeDelegates = constants["activeDelegates"]
    cfg.maxTransactions = constants["block"]["maxTransactions"]
    cfg.blocktime = constants["blocktime"]
    cfg.begintime = pytz.utc.localize(
        datetime.strptime(constants["epoch"], "%Y-%m-%dT%H:%M:%S.000Z")
    )
    cfg.blockreward = float(constants["reward"])/100000000
    # since ark v2.4 wif and slip44 are provided by network
    if "wif" in data:
        cfg.wif = "%x" % data["wif"]
    if "slip44" in data:
        cfg.slip44 = str(data["slip44"])

    # -- static fee management ------------------------------------------------
    cfg.fees = constants["fees"]

    # -- dynamic fee management -----------------------------------------------
    # on v2.0 dynamicFees are in "fees" field
    cfg.doffsets = cfg.fees.get("dynamicFees", {}).get("addonBytes", {})
    # on v2.1 dynamicFees are in "transactionPool" field
    cfg.doffsets.update(
        data.get("transactionPool", {})
        .get("dynamicFees", {})
        .get("addonBytes", {})
    )
    # before ark v2.4 dynamicFees statistics are in "feeStatistics" field
    cfg.feestats = dict(
        [i["type"], i["fees"]] for i in data.get("feeStatistics", {})
    )
    # since ark v2.4 fee statistics moved to ~/api/node/fees endpoint
    if cfg.feestats == {}:
        fees = FEES["data"]
        if isinstance(fees, list):
            cfg.feestats = dict([
                int(i["type"]), {
                    "avgFee": int(i["avg"]),
                    "minFee": int(i["min"]),
                    "maxFee": int(i["max"]),
                }
            ] for i in fees)
        # since ark v2.6 fee statistic structure is a dictionary
        elif isinstance(fees, dict):
            NUM = dict([v, k] for k, v in TRANSACTIONS.items())
            cfg.feestats = dict([
                NUM[k], {
                    "avgFee": int(v["avg"]),
                    "minFee": int(v["min"]),
                    "maxFee": int(v["max"]),
                }
            ] for k, v in fees.get("1", {}).items())
    # activate dynamic fees
    Transaction.useDynamicFee()
    # -- network connection management ----------------------------------------
    # change peers every 30 seconds
    if getattr(cfg, "hotmode", False):
        DAEMON_PEERS = _rotate_peers()

    return True


def stop():
    """
    Stop daemon initialized by ``init`` call.
    """
    global DAEMON_PEERS
    if DAEMON_PEERS is not None:
        DAEMON_PEERS.set()


def computeDynamicFees(tx, FMULT=None):
    """
    Compute transaction fees according to
    `AIP 16 <https://github.com/ArkEcosystem/AIPs/blob/master/AIPS/aip-16.md>`_

    Arguments:
        tx (:class:`dict` or :class:`Transaction`): transaction object
    Returns:
        :class:`int`: fees
    """
    typ_ = tx.get("type", 0)
    version = tx.get("version", 0x01)

    vendorField = tx.get("vendorField", "")
    vendorField = \
        vendorField if isinstance(vendorField, bytes) else \
        vendorField.encode("utf-8")
    lenVF = len(vendorField)
    payload = crypto.serializePayload(tx)
    T = cfg.doffsets.get(TRANSACTIONS[typ_], 0)
    return int(
        (T + 55 + (4 if version >= 0x02 else 0) + lenVF + len(payload)) *
        Transaction.FMULT if FMULT is None else FMULT
    )


def broadcastTransactions(*transactions, **params):
    chunk_size = params.pop("chunk_size", cfg.maxTransactions)
    report = []
    for chunk in [
        transactions[i:i+chunk_size] for i in
        range(0, len(transactions), chunk_size)
    ]:
        report.append(rest.POST.api.transactions(transactions=chunk))
    return \
        None if len(report) == 0 else \
        report[0] if len(report) == 1 else \
        report


[docs]def transfer(amount, address, vendorField=None, expiration=0): """ Build a transfer transaction. Emoji can be included in transaction vendorField using unicode formating. >>> u"message with sparkles \u2728" Arguments: amount (:class:`float`): transaction amount in ark address (:class:`str`): valid recipient address vendorField (:class:`str`): vendor field message expiration (:class:`float`): time of persistance in hour Returns: :class:`dposlib.blockchain.Transaction`: transaction object """ if cfg.txversion > 1 and expiration > 0: block_remaining = expiration*60*60//rest.cfg.blocktime expiration = int( rest.GET.api.blockchain() .get("data", {}).get("block", {}).get("height", -block_remaining) + block_remaining ) return Transaction( type=0, amount=amount*100000000, recipientId=address, vendorField=vendorField, version=cfg.txversion, expiration=None if cfg.txversion < 2 else expiration )
[docs]def registerSecondSecret(secondSecret): """ Build a second secret registration transaction. Arguments: secondSecret (:class:`str`): passphrase Returns: :class:`dposlib.blockchain.Transaction`: transaction object """ return registerSecondPublicKey( crypto.getKeys(secondSecret)["publicKey"], version=cfg.txversion )
[docs]def registerSecondPublicKey(secondPublicKey): """ Build a second secret registration transaction. .. note:: You must own the secret issuing secondPublicKey Arguments: secondPublicKey (:class:`str`): public key as hex string Returns: :class:`dposlib.blockchain.Transaction`: transaction object """ return Transaction( type=1, version=cfg.txversion, asset={ "signature": { "publicKey": secondPublicKey } } )
[docs]def registerAsDelegate(username): """ Build a delegate registration transaction. Arguments: username (:class:`str`): delegate username Returns: :class:`dposlib.blockchain.Transaction`: transaction object """ return Transaction( type=2, version=cfg.txversion, asset={ "delegate": { "username": username } } )
[docs]def upVote(*usernames): """ Build an upvote transaction. Arguments: usernames (:class:`iterable`): delegate usernames as :class:`str` iterable Returns: :class:`dposlib.blockchain.Transaction`: transaction object """ try: votes = [ "+"+rest.GET.api.delegates(username, returnKey="data")["publicKey"] for username in usernames ] except KeyError: raise Exception("one of delegate %s does not exist" % ",".join(usernames)) return Transaction( type=3, version=cfg.txversion, asset={ "votes": votes }, )
[docs]def downVote(*usernames): """ Build a downvote transaction. Arguments: usernames (:class:`iterable`): delegate usernames as :class:`str` iterable Returns: :class:`dposlib.blockchain.Transaction`: transaction object """ try: votes = [ "-"+rest.GET.api.delegates(username, returnKey="data")["publicKey"] for username in usernames ] except KeyError: raise Exception("one of delegate %s does not exist" % ",".join(usernames)) return Transaction( type=3, version=cfg.txversion, asset={ "votes": votes }, )
# https://github.com/ArkEcosystem/AIPs/blob/master/AIPS/aip-18.md
[docs]def registerMultiSignature(minSig, *publicKeys): """ Build a multisignature registration transaction. Args: minSig (:class:`int`): minimum signature required publicKeys (:class:`list of str`): public key list Returns: :class:`dposlib.blockchain.Transaction`: transaction object """ return Transaction( version=cfg.txversion, type=4, MultiSignatureAddress=crypto.getAddress( crypto.getMultiSignaturePublicKey( minSig, *publicKeys ) ), asset={ "multiSignature": { "min": minSig, "publicKeys": publicKeys } }, )
[docs]def registerIpfs(ipfs): """ Build an IPFS registration transaction. Arguments: ipfs (:class:`str`): ipfs DAG Returns: :class:`dposlib.blockchain.Transaction`: transaction object """ return Transaction( version=cfg.txversion, type=5, asset={ "ipfs": ipfs } )
[docs]def multiPayment(*pairs, **kwargs): """ Build multi-payment transaction. Emoji can be included in transaction vendorField using unicode formating. >>> u"message with sparkles \u2728" Arguments: pairs (:class:`iterable`): recipient-amount pair iterable vendorField (:class:`str`): vendor field message Returns: :class:`dposlib.blockchain.Transaction`: transaction object """ return Transaction( version=cfg.txversion, type=6, vendorField=kwargs.get("vendorField", None), asset={ "payments": [ {"amount": int(a*100000000), "recipientId": r} for a, r in pairs ] } )
[docs]def delegateResignation(): """ Build a delegate resignation transaction. Returns: :class:`dposlib.blockchain.Transaction`: transaction object """ return Transaction( version=cfg.txversion, type=7 )
[docs]def htlcSecret(secret): """ Compute an HTLC secret hex string from passphrase. Arguments: secret (:class:`str`): passphrase Returns: :class:`dposlib.blockchain.Transaction`: transaction object """ return hexlify(hashlib.sha256( secret if isinstance(secret, bytes) else secret.encode("utf-8") ).digest()[:16])
[docs]def htlcLock(amount, address, secret, expiration=24, vendorField=None): """ Build an HTLC lock transaction. Emoji can be included in transaction vendorField using unicode formating. >>> u"message with sparkles \u2728" Arguments: amount (:class:`float`): transaction amount in ark address (:class:`str`): valid recipient address secret (:class:`str`): lock passphrase expiration (:class:`float`): transaction validity in hour vendorField (:class:`str`): vendor field message Returns: :class:`dposlib.blockchain.Transaction`: transaction object """ return Transaction( version=cfg.txversion, type=8, amount=amount*100000000, recipientId=address, vendorField=vendorField, asset={ "lock": { "secretHash": hexlify( hashlib.sha256(htlcSecret(secret).encode("utf-8")).digest() ), "expiration": { "type": 1, "value": int(slots.getTime() + expiration*60*60) } } } )
[docs]def htlcClaim(txid, secret): """ Build an HTLC claim transaction. Arguments: txid (:class:`str`): htlc lock transaction id secret (:class:`str`): passphrase used by htlc lock transaction Returns: :class:`dposlib.blockchain.Transaction`: transaction object """ return Transaction( version=cfg.txversion, type=9, asset={ "claim": { "lockTransactionId": txid, "unlockSecret": htlcSecret(secret) } } )
[docs]def htlcRefund(txid): """ Build an HTLC refund transaction. Arguments: txid (:class:`str`): htlc lock transaction id Returns: :class:`dposlib.blockchain.Transaction`: transaction object """ return Transaction( version=cfg.txversion, type=10, asset={ "refund": { "lockTransactionId": txid, } } )
__all__ = [ "crypto", "hexlify", "unhexlify", "Transaction", "broadcastTransactions", "transfer", "registerSecondSecret", "registerSecondPublicKey", "registerAsDelegate", "upVote", "downVote", "registerMultiSignature", "registerIpfs", "multiPayment", "delegateResignation", "htlcLock", "htlcClaim", "htlcRefund" ]