<filename>JumpscaleLib/clients/blockchain/tfchain/TfchainClientFactory.py
"""
Client factory for the Tfchain network, js entry point
"""

from Jumpscale import j

import sys
import requests

from JumpscaleLib.clients.blockchain.rivine import utils
from JumpscaleLib.clients.blockchain.rivine import RivineWallet
from JumpscaleLib.clients.blockchain.tfchain.TfchainClient import TfchainClient
from JumpscaleLib.clients.blockchain.tfchain.TfchainNetwork import TfchainNetwork
from JumpscaleLib.clients.blockchain.tfchain.TfchainTime import TfchainTime
from JumpscaleLib.clients.blockchain.tfchain.errors import NoExplorerNetworkAddresses
from JumpscaleLib.clients.blockchain.rivine.errors import RESTAPIError
from JumpscaleLib.clients.blockchain.tfchain.TfchainThreeBotClient import TfchainThreeBotClient
from JumpscaleLib.clients.blockchain.rivine.types.transaction import TransactionFactory,\
    TransactionV128, TransactionV129, TransactionV210, TransactionSummary, FlatMoneyTransaction
from JumpscaleLib.clients.blockchain.rivine.types.unlockconditions import UnlockHashCondition,\
    LockTimeCondition, MultiSignatureCondition, UnlockCondtionFactory
from JumpscaleLib.clients.blockchain.rivine.types.unlockhash import UnlockHash
from JumpscaleLib.clients.blockchain.tfchain.types import signatures as tftsig
from JumpscaleLib.clients.blockchain.rivine.const import HASTINGS_TFT_VALUE
from JumpscaleLib.clients.blockchain.tfchain import const as tfconst

from JumpscaleLib.clients.blockchain.rivine.errors import WalletAlreadyExistsException

# JSConfigBaseFactory = j.tools.configmanager.JSBaseClassConfigs
JSConfigBaseFactory = j.tools.configmanager.base_class_configs


class TfchainClientFactory(JSConfigBaseFactory):
    """
    Factory class to get a tfchain client object
    """

    def __init__(self):
        self.__jslocation__ = "j.clients.tfchain"
        self.__imports__ = "tfchain"
        JSConfigBaseFactory.__init__(self, TfchainClient)

        self._time = TfchainTime()

    @property
    def time(self):
        return self._time

    @property
    def network(self):
        return TfchainNetwork

    @property
    def threebot(self):
        return TfchainThreeBotClient

    def generate_seed(self):
        """
        Generates a new seed and returns it as a mnemonic
        """
        return j.data.encryption.mnemonic.generate(strength=256)

    def get_transaction(self, identifier, network=TfchainNetwork.STANDARD, explorers=None):
        """
        Get a transaction registered on a TFchain network

        @param identifier: unique transaction id in string hex-encoded format
        @param network: optional network, STANDARD by default, TESTNET is another valid choice, for DEVNET use the network_addresses param instead
        @param explorers: explorer network addresses from which to get the transaction (only required for DEVNET, optional for other networks)
        """
        # TODO: have a clean modular solution to get data from an explorer endpoint, instead of repeating it everywhere
        # TODO: we probably want to move to seperate clients for seperate networks, such that the clients are aware of their networks and explorers
        #       allowing you to do 'j.clients.tfchain.testnet.get_transaction', 'j.clients.tfchain.standard.get_transaction'
        #       as well as 'j.clients.tfchain.devnet(['localhost:23110']).get_transaction
        if not explorers:
            explorers = network.official_explorers()
            if not explorers:
                raise NoExplorerNetworkAddresses(
                    "network {} has no official explorer networks and none were specified by callee".format(network.name.lower()))

        msg = 'Failed to retrieve transaction.'
        result = None
        response = None
        for address in explorers:
            url = '{}/explorer/hashes/{}'.format(address.strip('/'), identifier)
            headers = {'user-agent': 'Rivine-Agent'}
            try:
                response = requests.get(url, headers=headers, timeout=10)
            except requests.exceptions.ConnectionError:
                continue
            if response.status_code == 200:
                result = response.json()
                break

        if result is None:
            if response:
                raise RESTAPIError('{} {}'.format(msg, response.text.strip('\n')))
            else:
                raise RESTAPIError('error while fetching transaction from {} for {}: {}'.format(
                    explorers, identifier, msg))

        tx = result.get('transaction', {})
        if not tx:
            raise RESTAPIError(
                'error while fetching transaction from {} for {}: transaction not found'.format(explorers, identifier))
        return TransactionSummary.from_explorer_transaction(tx)

    def list_incoming_transactions_for(self, addresses, network=TfchainNetwork.STANDARD, explorers=None, min_height=0):
        """
        Get all transactions for the given addresses as registered on a TFchain network

        @param addresses: addresses to look transactions for
        @param network: optional network, STANDARD by default, TESTNET is another valid choice, for DEVNET use the network_addresses param instead
        @param explorers: explorer network addresses from which to get the transaction (only required for DEVNET, optional for other networks)
        """
        # TODO: we probably want to move to seperate clients for seperate networks, such that the clients are aware of their networks and explorers
        #       allowing you to do 'j.clients.tfchain.testnet.get_transaction', 'j.clients.tfchain.standard.get_transaction'
        #       as well as 'j.clients.tfchain.devnet(['localhost:23110']).get_transaction
        if not explorers:
            explorers = network.official_explorers()
            if not explorers:
                raise NoExplorerNetworkAddresses(
                    "network {} has no official explorer networks and none were specified by callee".format(network.name.lower()))
        if not addresses:
            raise ValueError("no addresses given to look transactions for")

        # list all transactions
        # create the list where all found transaction summaries will be stored in
        transactions = []

        # list the transactions of all transactions, one by one
        for address in addresses:
            try:
                address_info = utils.check_address(explorers, address, min_height=min_height, log_errors=False)
            except RESTAPIError:
                pass
            else:
                address_transactions = address_info.get('transactions', {})
                if not address_transactions:
                    continue
                for tx in address_transactions:
                    transactions.extend([tx for tx in FlatMoneyTransaction.create_list(tx)
                                         if tx.to_address in addresses])

        # return all listed transactions, reverse-sorted by block height
        transactions.sort(key=lambda tx: tx.block_height if tx.confirmed else sys.maxsize, reverse=True)
        return transactions

    def create_transaction_from_json(self, txn_json):
        """
        Loads a transaction from a json string

        @param txn_json: Json string representing a transaction
        """
        return TransactionFactory.from_json(txn_json)

    def create_wallet(self, wallet_name, network=TfchainNetwork.STANDARD, seed='', explorers=None, password=''):
        """
        Creates a named wallet

        @param network : defines which network to use, use j.clients.tfchain.network.TESTNET for testnet
        @param seed : restores a wallet from a seed
        """
        if not explorers:
            explorers = []
        if self.exists(wallet_name):
            raise WalletAlreadyExistsException(wallet_name)
        data = {
            'network': network.name.lower(),
            'seed_': seed,
            'explorers': explorers,
            'password': password,
        }
        return self.get(wallet_name, data=data).wallet

    def open_wallet(self, wallet_name):
        """
        Opens a named wallet.
        Raises the j.exceptions.NotFound exception if the wallet is not found.
        """
        if not self.exists(wallet_name):
            raise j.exceptions.NotFound('No wallet found with name {}'.format(wallet_name))
        return self.get(wallet_name).wallet

    def create_minterdefinition_transaction(self, condition=None, description=None, network=TfchainNetwork.STANDARD):
        """
        Create a new minter definition transaction

        @param condition: Set the minter definition to this premade condition
        @param description: Add this description as arbitrary data to the transaction
        """
        tx = TransactionV128()
        tx.add_minerfee(network.minimum_minerfee())
        if condition is not None:
            tx.set_condition(condition)
        if description is not None:
            tx.add_data(description.encode('utf-8'))
        return tx

    def create_coincreation_transaction(self, amount=None, condition=None, description=None, network=TfchainNetwork.STANDARD):
        """
        Create a new coin creation transaction. If both an amount and condition are
        given, they will be used to create a first output in the transaction

        @param amount: The amount of coins to create for the condition, if given
        @param condition: A premade condition, used to create a first output
        @param description: A description which is added to the transaction as arbitrary data
        """
        tx = TransactionV129()
        tx.add_minerfee(network.minimum_minerfee())
        if amount is not None and condition is not None:
            tx.add_output(amount, condition)
        if description is not None:
            tx.add_data(description.encode('utf-8'))
        return tx

    def create_singlesig_condition(self, address, locktime=None):
        """
        Create a new single signature condition
        """
        unlockhash = UnlockHash.from_string(address)
        condition = UnlockHashCondition(unlockhash=unlockhash)
        if locktime is not None:
            condition = LockTimeCondition(condition=condition, locktime=locktime)
        return condition

    def create_multisig_condition(self, unlockhashes, min_nr_sig, locktime=None):
        """
        Create a new multisig condition
        """
        condition = MultiSignatureCondition(unlockhashes=unlockhashes, min_nr_sig=min_nr_sig)
        if locktime is not None:
            condition = LockTimeCondition(condition=condition, locktime=locktime)
        return condition

    def register_erc20_address(self, wallet, public_key=None):
        # create the tx and fill the easiest properties already
        tx = TransactionV210()

        if not public_key:
            key = wallet.generate_key()
            public_key = key.public_key
        pk = tftsig.SiaPublicKey(tftsig.SiaPublicKeySpecifier.ED25519, public_key)
        tx.set_erc20_public_key(pubkey=pk)

        # add coin inputs for miner fees (implicitly computed) and required bot fees
        regfees = tfconst.ERC20_REGISTRATION_FEE_MULTIPLIER * HASTINGS_TFT_VALUE
        input_results, used_addresses, minerfee, remainder = wallet._get_inputs(amount=regfees)
        tx.set_transaction_fee(minerfee)
        tx.set_registration_fee(regfees)
        for input_result in input_results:
            tx.add_coin_input(**input_result)
        for txn_input in tx.coin_inputs:
            if used_addresses[txn_input.parent_id] not in wallet._keys:
                raise RivineWallet.NonExistingOutputError('Trying to spend unexisting output')
        # optionally add the remainder as a refund coin output
        if remainder > 0:
            # TODO: are we sure refunding to first address is always desired?
            tx.set_refund_coin_output(value=remainder, recipient=wallet.addresses[0])

        # sign and commit the Tx, return the tx ID afterwards
        wallet.sign_transaction(transaction=tx, commit=True)
        return tx

<filename>JumpscaleLib/clients/blockchain/tfchain/TfchainClientFactory.py
"""
Client factory for the Tfchain network, js entry point
"""

from Jumpscale import j

import sys
import requests

from JumpscaleLib.clients.blockchain.rivine import utils
from JumpscaleLib.clients.blockchain.rivine import RivineWallet
from JumpscaleLib.clients.blockchain.tfchain.TfchainClient import TfchainClient
from JumpscaleLib.clients.blockchain.tfchain.TfchainNetwork import TfchainNetwork
from JumpscaleLib.clients.blockchain.tfchain.TfchainTime import TfchainTime
from JumpscaleLib.clients.blockchain.tfchain.errors import NoExplorerNetworkAddresses
from JumpscaleLib.clients.blockchain.rivine.errors import RESTAPIError
from JumpscaleLib.clients.blockchain.tfchain.TfchainThreeBotClient import TfchainThreeBotClient
from JumpscaleLib.clients.blockchain.rivine.types.transaction import TransactionFactory,\
    TransactionV128, TransactionV129, TransactionV210, TransactionSummary, FlatMoneyTransaction
from JumpscaleLib.clients.blockchain.rivine.types.unlockconditions import UnlockHashCondition,\
    LockTimeCondition, MultiSignatureCondition, UnlockCondtionFactory
from JumpscaleLib.clients.blockchain.rivine.types.unlockhash import UnlockHash
from JumpscaleLib.clients.blockchain.tfchain.types import signatures as tftsig
from JumpscaleLib.clients.blockchain.rivine.const import HASTINGS_TFT_VALUE
from JumpscaleLib.clients.blockchain.tfchain import const as tfconst

from JumpscaleLib.clients.blockchain.rivine.errors import WalletAlreadyExistsException

# JSConfigBaseFactory = j.tools.configmanager.JSBaseClassConfigs
JSConfigBaseFactory = j.tools.configmanager.base_class_configs


class TfchainClientFactory(JSConfigBaseFactory):
    """
    Factory class to get a tfchain client object
    """

    def __init__(self):
        self.__jslocation__ = "j.clients.tfchain"
        self.__imports__ = "tfchain"
        JSConfigBaseFactory.__init__(self, TfchainClient)

        self._time = TfchainTime()

    @property
    def time(self):
        return self._time

    @property
    def network(self):
        return TfchainNetwork

    @property
    def threebot(self):
        return TfchainThreeBotClient

    def generate_seed(self):
        """
        Generates a new seed and returns it as a mnemonic
        """
        return j.data.encryption.mnemonic.generate(strength=256)

    def get_transaction(self, identifier, network=TfchainNetwork.STANDARD, explorers=None):
        """
        Get a transaction registered on a TFchain network

        @param identifier: unique transaction id in string hex-encoded format
        @param network: optional network, STANDARD by default, TESTNET is another valid choice, for DEVNET use the network_addresses param instead
        @param explorers: explorer network addresses from which to get the transaction (only required for DEVNET, optional for other networks)
        """
        # TODO: have a clean modular solution to get data from an explorer endpoint, instead of repeating it everywhere
        # TODO: we probably want to move to seperate clients for seperate networks, such that the clients are aware of their networks and explorers
        #       allowing you to do 'j.clients.tfchain.testnet.get_transaction', 'j.clients.tfchain.standard.get_transaction'
        #       as well as 'j.clients.tfchain.devnet(['localhost:23110']).get_transaction
        if not explorers:
            explorers = network.official_explorers()
            if not explorers:
                raise NoExplorerNetworkAddresses(
                    "network {} has no official explorer networks and none were specified by callee".format(network.name.lower()))

        msg = 'Failed to retrieve transaction.'
        result = None
        response = None
        for address in explorers:
            url = '{}/explorer/hashes/{}'.format(address.strip('/'), identifier)
            headers = {'user-agent': 'Rivine-Agent'}
            try:
                response = requests.get(url, headers=headers, timeout=10)
            except requests.exceptions.ConnectionError:
                continue
            if response.status_code == 200:
                result = response.json()
                break

        if result is None:
            if response:
                raise RESTAPIError('{} {}'.format(msg, response.text.strip('\n')))
            else:
                raise RESTAPIError('error while fetching transaction from {} for {}: {}'.format(
                    explorers, identifier, msg))

        tx = result.get('transaction', {})
        if not tx:
            raise RESTAPIError(
                'error while fetching transaction from {} for {}: transaction not found'.format(explorers, identifier))
        return TransactionSummary.from_explorer_transaction(tx)

    def list_incoming_transactions_for(self, addresses, network=TfchainNetwork.STANDARD, explorers=None, min_height=0):
        """
        Get all transactions for the given addresses as registered on a TFchain network

        @param addresses: addresses to look transactions for
        @param network: optional network, STANDARD by default, TESTNET is another valid choice, for DEVNET use the network_addresses param instead
        @param explorers: explorer network addresses from which to get the transaction (only required for DEVNET, optional for other networks)
        """
        # TODO: we probably want to move to seperate clients for seperate networks, such that the clients are aware of their networks and explorers
        #       allowing you to do 'j.clients.tfchain.testnet.get_transaction', 'j.clients.tfchain.standard.get_transaction'
        #       as well as 'j.clients.tfchain.devnet(['localhost:23110']).get_transaction
        if not explorers:
            explorers = network.official_explorers()
            if not explorers:
                raise NoExplorerNetworkAddresses(
                    "network {} has no official explorer networks and none were specified by callee".format(network.name.lower()))
        if not addresses:
            raise ValueError("no addresses given to look transactions for")

        # list all transactions
        # create the list where all found transaction summaries will be stored in
        transactions = []

        # list the transactions of all transactions, one by one
        for address in addresses:
            try:
                address_info = utils.check_address(explorers, address, min_height=min_height, log_errors=False)
            except RESTAPIError:
                pass
            else:
                address_transactions = address_info.get('transactions', {})
                if not address_transactions:
                    continue
                for tx in address_transactions:
                    transactions.extend([tx for tx in FlatMoneyTransaction.create_list(tx)
                                         if tx.to_address in addresses])

        # return all listed transactions, reverse-sorted by block height
        transactions.sort(key=lambda tx: tx.block_height if tx.confirmed else sys.maxsize, reverse=True)
        return transactions

    def create_transaction_from_json(self, txn_json):
        """
        Loads a transaction from a json string

        @param txn_json: Json string representing a transaction
        """
        return TransactionFactory.from_json(txn_json)

    def create_wallet(self, wallet_name, network=TfchainNetwork.STANDARD, seed='', explorers=None, password=''):
        """
        Creates a named wallet

        @param network : defines which network to use, use j.clients.tfchain.network.TESTNET for testnet
        @param seed : restores a wallet from a seed
        """
        if not explorers:
            explorers = []
        if self.exists(wallet_name):
            raise WalletAlreadyExistsException(wallet_name)
        data = {
            'network': network.name.lower(),
            'seed_': seed,
            'explorers': explorers,
            'password': password,
        }
        return self.get(wallet_name, data=data).wallet

    def open_wallet(self, wallet_name):
        """
        Opens a named wallet.
        Raises the j.exceptions.NotFound exception if the wallet is not found.
        """
        if not self.exists(wallet_name):
            raise j.exceptions.NotFound('No wallet found with name {}'.format(wallet_name))
        return self.get(wallet_name).wallet

    def create_minterdefinition_transaction(self, condition=None, description=None, network=TfchainNetwork.STANDARD):
        """
        Create a new minter definition transaction

        @param condition: Set the minter definition to this premade condition
        @param description: Add this description as arbitrary data to the transaction
        """
        tx = TransactionV128()
        tx.add_minerfee(network.minimum_minerfee())
        if condition is not None:
            tx.set_condition(condition)
        if description is not None:
            tx.add_data(description.encode('utf-8'))
        return tx

    def create_coincreation_transaction(self, amount=None, condition=None, description=None, network=TfchainNetwork.STANDARD):
        """
        Create a new coin creation transaction. If both an amount and condition are
        given, they will be used to create a first output in the transaction

        @param amount: The amount of coins to create for the condition, if given
        @param condition: A premade condition, used to create a first output
        @param description: A description which is added to the transaction as arbitrary data
        """
        tx = TransactionV129()
        tx.add_minerfee(network.minimum_minerfee())
        if amount is not None and condition is not None:
            tx.add_output(amount, condition)
        if description is not None:
            tx.add_data(description.encode('utf-8'))
        return tx

    def create_singlesig_condition(self, address, locktime=None):
        """
        Create a new single signature condition
        """
        unlockhash = UnlockHash.from_string(address)
        condition = UnlockHashCondition(unlockhash=unlockhash)
        if locktime is not None:
            condition = LockTimeCondition(condition=condition, locktime=locktime)
        return condition

    def create_multisig_condition(self, unlockhashes, min_nr_sig, locktime=None):
        """
        Create a new multisig condition
        """
        condition = MultiSignatureCondition(unlockhashes=unlockhashes, min_nr_sig=min_nr_sig)
        if locktime is not None:
            condition = LockTimeCondition(condition=condition, locktime=locktime)
        return condition

    def register_erc20_address(self, wallet, public_key=None):
        # create the tx and fill the easiest properties already
        tx = TransactionV210()

        if not public_key:
            key = wallet.generate_key()
            public_key = key.public_key
        pk = tftsig.SiaPublicKey(tftsig.SiaPublicKeySpecifier.ED25519, public_key)
        tx.set_erc20_public_key(pubkey=pk)

        # add coin inputs for miner fees (implicitly computed) and required bot fees
        regfees = tfconst.ERC20_REGISTRATION_FEE_MULTIPLIER * HASTINGS_TFT_VALUE
        input_results, used_addresses, minerfee, remainder = wallet._get_inputs(amount=regfees)
        tx.set_transaction_fee(minerfee)
        tx.set_registration_fee(regfees)
        for input_result in input_results:
            tx.add_coin_input(**input_result)
        for txn_input in tx.coin_inputs:
            if used_addresses[txn_input.parent_id] not in wallet._keys:
                raise RivineWallet.NonExistingOutputError('Trying to spend unexisting output')
        # optionally add the remainder as a refund coin output
        if remainder > 0:
            # TODO: are we sure refunding to first address is always desired?
            tx.set_refund_coin_output(value=remainder, recipient=wallet.addresses[0])

        # sign and commit the Tx, return the tx ID afterwards
        wallet.sign_transaction(transaction=tx, commit=True)
        return tx

Client factory for the Tfchain network, js entry point # JSConfigBaseFactory = j.tools.configmanager.JSBaseClassConfigs Factory class to get a tfchain client object Generates a new seed and returns it as a mnemonic Get a transaction registered on a TFchain network          @param identifier: unique transaction id in string hex-encoded format         @param network: optional network, STANDARD by default, TESTNET is another valid choice, for DEVNET use the network_addresses param instead         @param explorers: explorer network addresses from which to get the transaction (only required for DEVNET, optional for other networks) # TODO: have a clean modular solution to get data from an explorer endpoint, instead of repeating it everywhere # TODO: we probably want to move to seperate clients for seperate networks, such that the clients are aware of their networks and explorers #       allowing you to do 'j.clients.tfchain.testnet.get_transaction', 'j.clients.tfchain.standard.get_transaction' #       as well as 'j.clients.tfchain.devnet(['localhost:23110']).get_transaction Get all transactions for the given addresses as registered on a TFchain network          @param addresses: addresses to look transactions for         @param network: optional network, STANDARD by default, TESTNET is another valid choice, for DEVNET use the network_addresses param instead         @param explorers: explorer network addresses from which to get the transaction (only required for DEVNET, optional for other networks) # TODO: we probably want to move to seperate clients for seperate networks, such that the clients are aware of their networks and explorers #       allowing you to do 'j.clients.tfchain.testnet.get_transaction', 'j.clients.tfchain.standard.get_transaction' #       as well as 'j.clients.tfchain.devnet(['localhost:23110']).get_transaction # list all transactions # create the list where all found transaction summaries will be stored in # list the transactions of all transactions, one by one # return all listed transactions, reverse-sorted by block height Loads a transaction from a json string          @param txn_json: Json string representing a transaction Creates a named wallet          @param network : defines which network to use, use j.clients.tfchain.network.TESTNET for testnet         @param seed : restores a wallet from a seed Opens a named wallet.         Raises the j.exceptions.NotFound exception if the wallet is not found. Create a new minter definition transaction          @param condition: Set the minter definition to this premade condition         @param description: Add this description as arbitrary data to the transaction Create a new coin creation transaction. If both an amount and condition are         given, they will be used to create a first output in the transaction          @param amount: The amount of coins to create for the condition, if given         @param condition: A premade condition, used to create a first output         @param description: A description which is added to the transaction as arbitrary data Create a new single signature condition Create a new multisig condition # create the tx and fill the easiest properties already # add coin inputs for miner fees (implicitly computed) and required bot fees # optionally add the remainder as a refund coin output # TODO: are we sure refunding to first address is always desired? # sign and commit the Tx, return the tx ID afterwards