Source code for graphenecommon.chain

# -*- coding: utf-8 -*-
import logging
from graphenestorage import SqliteConfigurationStore

log = logging.getLogger(__name__)


class AbstractGrapheneChain:
    def define_classes(self):
        raise NotImplementedError

    def __init__(
        self,
        node="",
        rpcuser="",
        rpcpassword="",
        debug=False,
        skip_wallet_init=False,
        **kwargs
    ):

        self.define_classes()
        assert self.rpc_class
        assert self.wallet_class
        assert self.default_key_store_app_name
        assert self.transactionbuilder_class
        assert self.proposalbuilder_class
        assert self.account_class
        assert self.blockchainobject_class

        # More specific set of APIs to register to
        if "apis" not in kwargs:
            kwargs["apis"] = ["database", "network_broadcast"]

        self.rpc = None
        self.debug = debug

        self.offline = bool(kwargs.get("offline", False))
        self.nobroadcast = bool(kwargs.get("nobroadcast", False))
        self.unsigned = bool(kwargs.get("unsigned", False))
        self.expiration = int(kwargs.get("expiration", 30))
        self.bundle = bool(kwargs.get("bundle", False))
        self.blocking = bool(kwargs.get("blocking", False))

        # Legacy Proposal attributes
        self.proposer = kwargs.get("proposer", None)
        self.proposal_expiration = int(kwargs.get("proposal_expiration", 60 * 60 * 24))
        self.proposal_review = int(kwargs.get("proposal_review", 0))

        # Store self.config for access through other Classes
        kwargs["appname"] = self.default_key_store_app_name
        self.config = kwargs.get("config_store", SqliteConfigurationStore(**kwargs))

        # Connect
        if not self.offline:
            self.connect(node=node, rpcuser=rpcuser, rpcpassword=rpcpassword, **kwargs)

        # txbuffers/propbuffer are initialized and cleared
        self.clear()

        if not skip_wallet_init:
            self.wallet = kwargs.get(
                "wallet", self.wallet_class(blockchain_instance=self, **kwargs)
            )

    # -------------------------------------------------------------------------
    # RPC
    # -------------------------------------------------------------------------
    def connect(self, node="", rpcuser="", rpcpassword="", **kwargs):
        """ Connect to blockchain network (internal use only)
        """
        if not node:
            if "node" in self.config:
                node = self.config["node"]
            else:
                raise ValueError("A Blockchain node needs to be provided!")

        if not rpcuser and "rpcuser" in self.config:
            rpcuser = self.config["rpcuser"]

        if not rpcpassword and "rpcpassword" in self.config:
            rpcpassword = self.config["rpcpassword"]

        self.rpc = self.rpc_class(node, rpcuser, rpcpassword, **kwargs)

    def is_connected(self):
        return bool(self.rpc)

    # -------------------------------------------------------------------------
    # General methods
    # -------------------------------------------------------------------------
    @property
    def prefix(self):
        """ Contains the prefix of the blockchain
        """
        return self.rpc.chain_params["prefix"]

    def set_blocking(self, block=True):
        """ This sets a flag that forces the broadcast to block until the
            transactions made it into a block
        """
        self.blocking = block

    def info(self):
        """ Returns the global properties
        """
        return self.rpc.get_dynamic_global_properties()

    # -------------------------------------------------------------------------
    # Wallet
    # -------------------------------------------------------------------------
    def set_default_account(self, account):
        """ Set the default account to be used
        """
        self.account_class(account)
        self.config["default_account"] = account

    def newWallet(self, pwd):
        return self.new_wallet(pwd)

    def new_wallet(self, pwd):
        """ Create a new wallet. This method is basically only calls
            :func:`wallet.Wallet.create`.

            :param str pwd: Password to use for the new wallet
            :raises exceptions.WalletExists: if there is already a
                wallet created
        """
        return self.wallet.create(pwd)

    def unlock(self, *args, **kwargs):
        """ Unlock the internal wallet
        """
        return self.wallet.unlock(*args, **kwargs)

    # -------------------------------------------------------------------------
    # Shared instance interface
    # -------------------------------------------------------------------------
    def set_shared_instance(self):
        """ This method allows to set the current instance as default
        """
        # self._sharedInstance.instance = self
        log.warning(
            DeprecationWarning(
                "set_shared_instance in chaininstance is no longer supported. "
                "This interface no longer exists, please use .instance.set_shared_instance() instead."
            )
        )

    # -------------------------------------------------------------------------
    # General transaction/operation stuff
    # -------------------------------------------------------------------------
    def finalizeOp(self, ops, account, permission, **kwargs):
        """ This method obtains the required private keys if present in
            the wallet, finalizes the transaction, signs it and
            broadacasts it

            :param operation ops: The operation (or list of operaions) to
                broadcast
            :param operation account: The account that authorizes the
                operation
            :param string permission: The required permission for
                signing (active, owner, posting)
            :param object append_to: This allows to provide an instance of
                ProposalsBuilder (see :func:`new_proposal`) or
                TransactionBuilder (see :func:`new_tx()`) to specify
                where to put a specific operation.

            ... note:: ``append_to`` is exposed to every method used in the
                this class

            ... note::

                If ``ops`` is a list of operation, they all need to be
                signable by the same key! Thus, you cannot combine ops
                that require active permission with ops that require
                posting permission. Neither can you use different
                accounts for different operations!

            ... note:: This uses ``txbuffer`` as instance of
                :class:`transactionbuilder.TransactionBuilder`.
                You may want to use your own txbuffer
        """
        if "append_to" in kwargs and kwargs["append_to"]:
            if self.proposer:
                log.warning(
                    "You may not use append_to and self.proposer at "
                    "the same time. Append new_proposal(..) instead"
                )
            # Append to the append_to and return
            append_to = kwargs["append_to"]
            parent = append_to.get_parent()
            assert isinstance(
                append_to, (self.transactionbuilder_class, self.proposalbuilder_class)
            )
            append_to.appendOps(ops)
            # Add the signer to the buffer so we sign the tx properly
            if isinstance(append_to, self.proposalbuilder_class):
                parent.appendSigner(append_to.proposer, permission)
            else:
                parent.appendSigner(account, permission)
            # This returns as we used append_to, it does NOT broadcast, or sign
            return append_to.get_parent()
        elif self.proposer:
            # Legacy proposer mode!
            proposal = self.proposal()
            proposal.set_proposer(self.proposer)
            proposal.set_expiration(self.proposal_expiration)
            proposal.set_review(self.proposal_review)
            proposal.appendOps(ops)
            # Go forward to see what the other options do ...
        else:
            # Append tot he default buffer
            self.txbuffer.appendOps(ops)

        # The API that obtains the fee only allows to specify one particular
        # fee asset for all operations in that transaction even though the
        # blockchain itself could allow to pay multiple operations with
        # different fee assets.
        if "fee_asset" in kwargs and kwargs["fee_asset"]:
            self.txbuffer.set_fee_asset(kwargs["fee_asset"])

        # Add signing information, signer, sign and optionally broadcast
        if self.unsigned:
            # In case we don't want to sign anything
            self.txbuffer.addSigningInformation(account, permission)
            return self.txbuffer
        elif self.bundle:
            # In case we want to add more ops to the tx (bundle)
            self.txbuffer.appendSigner(account, permission)
            return self.txbuffer
        else:
            # default behavior: sign + broadcast
            self.txbuffer.appendSigner(account, permission)
            self.txbuffer.sign()
            return self.txbuffer.broadcast()

    def sign(self, tx=None, wifs=[]):
        """ Sign a provided transaction witht he provided key(s)

            :param dict tx: The transaction to be signed and returned
            :param string wifs: One or many wif keys to use for signing
                a transaction. If not present, the keys will be loaded
                from the wallet as defined in "missing_signatures" key
                of the transactions.
        """
        if tx:
            txbuffer = self.transactionbuilder_class(tx, blockchain_instance=self)
        else:
            txbuffer = self.txbuffer
        txbuffer.appendWif(wifs)
        txbuffer.appendMissingSignatures()
        txbuffer.sign()
        return txbuffer.json()

    def broadcast(self, tx=None):
        """ Broadcast a transaction to the Blockchain

            :param tx tx: Signed transaction to broadcast
        """
        if tx:
            # If tx is provided, we broadcast the tx
            return self.transactionbuilder_class(
                tx, blockchain_instance=self
            ).broadcast()
        else:
            return self.txbuffer.broadcast()

    # -------------------------------------------------------------------------
    # Transaction Buffers
    # -------------------------------------------------------------------------
    @property
    def txbuffer(self):
        """ Returns the currently active tx buffer
        """
        return self.tx()

    @property
    def propbuffer(self):
        """ Return the default proposal buffer
        """
        return self.proposal()

    def tx(self):
        """ Returns the default transaction buffer
        """
        return self._txbuffers[0]

    def proposal(self, proposer=None, proposal_expiration=None, proposal_review=None):
        """ Return the default proposal buffer

            ... note:: If any parameter is set, the default proposal
               parameters will be changed!
        """
        if not self._propbuffer:
            return self.new_proposal(
                self.tx(), proposer, proposal_expiration, proposal_review
            )
        if proposer:
            self._propbuffer[0].set_proposer(proposer)
        if proposal_expiration:
            self._propbuffer[0].set_expiration(proposal_expiration)
        if proposal_review:
            self._propbuffer[0].set_review(proposal_review)
        return self._propbuffer[0]

    def new_proposal(
        self,
        parent=None,
        proposer=None,
        proposal_expiration=None,
        proposal_review=None,
        **kwargs
    ):
        if not parent:
            parent = self.tx()
        if not proposal_expiration:
            proposal_expiration = self.proposal_expiration

        if not proposal_review:
            proposal_review = self.proposal_review

        if not proposer:
            if "default_account" in self.config:
                proposer = self.config["default_account"]

        # Else, we create a new object
        proposal = self.proposalbuilder_class(
            proposer,
            proposal_expiration,
            proposal_review,
            blockchain_instance=self,
            parent=parent,
            **kwargs
        )
        if parent:
            parent.appendOps(proposal)
        self._propbuffer.append(proposal)
        return proposal

    def new_tx(self, *args, **kwargs):
        """ Let's obtain a new txbuffer

            :returns int txid: id of the new txbuffer
        """
        builder = self.transactionbuilder_class(
            *args, blockchain_instance=self, **kwargs
        )
        self._txbuffers.append(builder)
        return builder

    # -------------------------------------------------------------------------
    # Caches
    # -------------------------------------------------------------------------
    def clear(self):
        self._txbuffers = []
        self._propbuffer = []
        # Base/Default proposal/tx buffers
        self.new_tx()
        # self.new_proposal()

    def clear_cache(self):
        """ Clear Caches
        """
        self.blockchainobject_class.clear_cache()