Exemple #1
0
    def __init__(self, explorer_client=None, network_type=None):
        self._network_type = network_type
        if self._network_type is not None:
            if isinstance(self._network_type, str):
                self._network_type = NetworkType.from_str(self._network_type)
            elif not isinstance(self._network_type, NetworkType):
                raise TypeError(
                    "network_type is expected to be None or of type str or NetworkType, not of type {}"
                    .format(type(self._network_type)))
        # create explorer_client if not given
        self._explorer_client = explorer_client
        if not self._explorer_client:
            if self._network_type is None:
                self._network_type = NetworkType.STANDARD
            explorer_addresses = self._network_type.default_explorer_addresses(
            )
            self._explorer_client = TFChainExplorerClient(
                addresses=explorer_addresses)
        elif not isinstance(explorer_client, TFChainExplorerClient):
            raise TypeError(
                "explorer client has to be of type TFChainExplorerClient not {}"
                .format(type(explorer_client)))
        elif self._network_type is None:
            # fetch network type from the explorer client
            resp = self._explorer_client.get("/explorer/constants")
            resp = json_loads(resp)
            self._network_type = NetworkType.from_str(
                resp.get("chaininfo", {}).get("NetworkName", "standard"))

        # create subclients
        self._threebot = TFChainThreeBotClient(self)
        self._minter = TFChainMinterClient(self)
        self._erc20 = TFChainERC20Client(self)
def test_explorer_client():
    client = TFChainExplorerClient(
        addresses=['https://explorer2.threefoldtoken.com'])
    resp = client.get(endpoint='/explorer/constants')
    data = json_loads(resp)
    assert data['chaininfo']['Name'] == 'tfchain'
    assert data['chaininfo']['CoinUnit'] == 'TFT'
Exemple #3
0
    def transaction_get(self, txid):
        """
        Get a transaction from an available explorer Node.

        @param txid: the identifier (bytes, bytearray, hash or string) that points to the desired transaction
        """
        txid = self._normalize_id(txid)
        endpoint = "/explorer/hashes/" + txid
        resp = self.explorer_get(endpoint=endpoint)
        resp = json_loads(resp)
        try:
            if resp['hashtype'] != 'transactionid':
                raise tfchain.errors.ExplorerInvalidResponse(
                    "expected hash type 'transactionid' not '{}'".format(
                        resp['hashtype']), endpoint, resp)
            resp = resp['transaction']
            if resp['id'] != txid:
                raise tfchain.errors.ExplorerInvalidResponse(
                    "expected transaction ID '{}' not '{}'".format(
                        txid, resp['id']), endpoint, resp)
            return self._transaction_from_explorer_transaction(
                resp, endpoint=endpoint, resp=resp)
        except KeyError as exc:
            # return a KeyError as an invalid Explorer Response
            raise tfchain.errors.ExplorerInvalidResponse(
                str(exc), endpoint, resp) from exc
Exemple #4
0
    def condition_get(self, height=None):
        """
        Get the latest (coin) mint condition or the (coin) mint condition at the specified block height.

        @param height: if defined the block height at which to look up the (coin) mint condition (if none latest block will be used)
        """
        # define the endpoint
        endpoint = "/explorer/mintcondition"
        if height is not None:
            if not isinstance(height, (int, str)):
                raise TypeError("invalid block height given")
            height = int(height)
            endpoint += "/%d" % (height)

        # get the mint condition
        resp = self._client.explorer_get(endpoint=endpoint)
        resp = json_loads(resp)

        try:
            # return the decoded mint condition
            return ConditionTypes.from_json(obj=resp['mintcondition'])
        except KeyError as exc:
            # return a KeyError as an invalid Explorer Response
            raise tfchain.errors.ExplorerInvalidResponse(
                str(exc), endpoint, resp) from exc
Exemple #5
0
 def blockchain_info_get(self):
     """
     Get the current blockchain info, using the last known block, as reported by an explorer.
     """
     resp = self.explorer_get(endpoint="/explorer")
     resp = json_loads(resp)
     blockid = Hash.from_json(obj=resp['blockid'])
     last_block = self.block_get(blockid)
     return ExplorerBlockchainInfo(last_block=last_block)
Exemple #6
0
    def record_get(self, identifier):
        """
        Get a 3Bot record registered on a TFchain network
        @param identifier: unique 3Bot id, public key or (bot) name to search a 3Bot record for
        """
        endpoint = "/explorer/3bot"
        if isinstance(identifier, int):
            identifier = str(identifier)
        elif isinstance(identifier, BotName):
            endpoint = "/explorer/whois/3bot"
            identifier = str(identifier)
        elif isinstance(identifier, PublicKey):
            identifier = str(identifier)
        elif isinstance(identifier, str):
            if BotName.REGEXP.match(identifier) is not None:
                endpoint = "/explorer/whois/3bot"
            else:
                try:
                    PublicKey.from_json(identifier)
                except ValueError as exc:
                    raise ValueError(
                        "a 3Bot identifier in string format has to be either a valid BotName or PublicKey, '{}' is neither"
                        .format(identifier)) from exc
        else:
            raise TypeError("identifier of type {} is invalid".format(
                type(identifier)))
        # identifier is a str at this point
        # and endpoint is configured

        # fetch the data
        endpoint += "/{}".format(identifier)
        try:
            resp = self._client.explorer_get(endpoint=endpoint)
        except tfchain.errors.ExplorerNoContent as exc:
            raise tfchain.errors.ThreeBotNotFound(identifier) from exc
        resp = json_loads(resp)
        try:
            # return the fetched record as a named tuple, for easy semi-typed access
            record = resp['record']
            return ThreeBotRecord(
                identifier=int(record['id']),
                names=[
                    BotName.from_json(name)
                    for name in record.get('names', []) or []
                ],
                addresses=[
                    NetworkAddress.from_json(address)
                    for address in record.get('addresses', []) or []
                ],
                public_key=PublicKey.from_json(record['publickey']),
                expiration=int(record['expiration']),
            )
        except KeyError as exc:
            # return a KeyError as an invalid Explorer Response
            raise tfchain.errors.ExplorerInvalidResponse(
                str(exc), endpoint, resp) from exc
Exemple #7
0
    def transaction_put(self, transaction):
        """
        Submit a transaction to an available explorer Node.

        @param transaction: the transaction to push to the client transaction pool
        """
        if isinstance(transaction, TransactionBaseClass):
            transaction = transaction.json()
        endpoint = "/transactionpool/transactions"
        resp = self.explorer_post(endpoint=endpoint, data=transaction)
        resp = json_loads(resp)
        try:
            return str(Hash(value=resp['transactionid']))
        except KeyError as exc:
            # return a KeyError as an invalid Explorer Response
            raise tfchain.errors.ExplorerInvalidResponse(
                str(exc), endpoint, resp) from exc
Exemple #8
0
    def address_get(self, unlockhash):
        """
        Get the ERC20 (withdraw) address for the given unlock hash,
        ExplorerNoContent error is raised when no address could be found for the given unlock hash.

        Only type 01 addresses can be looked up for this method (personal wallet addresses),
        as there can be no MultiSignature (wallet) address registered as an ERC20 withdraw address.

        @param unlockhash: the str or wallet address to be looked up
        """
        if isinstance(unlockhash, str):
            unlockhash = UnlockHash.from_json(unlockhash)
        elif not isinstance(unlockhash, UnlockHash):
            raise TypeError(
                "{} is not a valid type and cannot be used as unlock hash".
                format(type(unlockhash)))
        if unlockhash.type != UnlockHashType.PUBLIC_KEY:
            raise TypeError(
                "only person wallet addresses cannot be registered as withdrawel addresses: {} is an invalid unlock hash type"
                .format(unlockhash.type))

        endpoint = "/explorer/hashes/" + str(unlockhash)
        resp = self._client.explorer_get(endpoint=endpoint)
        resp = json_loads(resp)
        try:
            if resp['hashtype'] != 'unlockhash':
                raise tfchain.errors.ExplorerInvalidResponse(
                    "expected hash type 'unlockhash' not '{}'".format(
                        resp['hashtype']), endpoint, resp)
            # parse the ERC20 Info
            if not 'erc20info' in resp:
                raise tfchain.errors.ExplorerNoContent(
                    "{} could be found but is not registered as an ERC20 withdraw address"
                    .format(str(unlockhash)),
                    endpoint=endpoint)
            info = resp['erc20info']
            return ERC20AddressInfo(
                address_tft=UnlockHash.from_json(info['tftaddress']),
                address_erc20=ERC20Address.from_json(info['erc20address']),
                confirmations=int(info['confirmations']),
            )
        except KeyError as exc:
            # return a KeyError as an invalid Explorer Response
            raise tfchain.errors.ExplorerInvalidResponse(
                str(exc), endpoint, resp) from exc
Exemple #9
0
def from_json(obj, id=None):
    """
    Create a TFChain transaction from a JSON string or dictionary.

    @param obj: JSON-encoded str, bytes, bytearray or JSON-decoded dict that contains a raw JSON Tx.
    """
    if isinstance(obj, (str, bytes, bytearray)):
        obj = json_loads(obj)
    if not isinstance(obj, dict):
        raise TypeError(
            "only a dictionary or JSON-encoded dictionary is supported as input: type {} is not supported",
            type(obj))
    tt = obj.get('version', -1)

    txn = None
    if tt == TransactionVersion.STANDARD:
        txn = TransactionV1.from_json(obj)
    elif tt == TransactionVersion.THREEBOT_REGISTRATION:
        txn = TransactionV144.from_json(obj)
    elif tt == TransactionVersion.THREEBOT_RECORD_UPDATE:
        txn = TransactionV145.from_json(obj)
    elif tt == TransactionVersion.THREEBOT_NAME_TRANSFER:
        txn = TransactionV146.from_json(obj)
    elif tt == TransactionVersion.ERC20_CONVERT:
        txn = TransactionV208.from_json(obj)
    elif tt == TransactionVersion.ERC20_COIN_CREATION:
        txn = TransactionV209.from_json(obj)
    elif tt == TransactionVersion.ERC20_ADDRESS_REGISTRATION:
        txn = TransactionV210.from_json(obj)
    elif tt == TransactionVersion.MINTER_DEFINITION:
        txn = TransactionV128.from_json(obj)
    elif tt == TransactionVersion.MINTER_COIN_CREATION:
        txn = TransactionV129.from_json(obj)
    elif tt == TransactionVersion.LEGACY:
        txn = TransactionV1.legacy_from_json(obj)

    if isinstance(txn, TransactionBaseClass):
        txn.id = id
        return txn

    raise UnknownTransansactionVersion(
        "transaction version {} is unknown".format(tt))
Exemple #10
0
    def _output_get(self, id, expected_hash_type):
        """
        Get an output from an available explorer Node.

        Returns (output, creation_txn, spend_txn).

        @param id: the identifier (bytes, bytearray, hash or string) that points to the desired output
        @param expected_hash_type: one of ('coinoutputid', 'blockstakeoutputid')
        """
        if expected_hash_type not in ('coinoutputid', 'blockstakeoutputid'):
            raise ValueError(
                "expected hash type should be one of ('coinoutputid', 'blockstakeoutputid'), not {}"
                .format(expected_hash_type))
        id = self._normalize_id(id)
        endpoint = "/explorer/hashes/" + id
        resp = self.explorer_get(endpoint=endpoint)
        resp = json_loads(resp)
        try:
            hash_type = resp['hashtype']
            if hash_type != expected_hash_type:
                raise tfchain.errors.ExplorerInvalidResponse(
                    "expected hash type '{}', not '{}'".format(
                        expected_hash_type, hash_type), endpoint, resp)
            tresp = resp['transactions']
            lresp = len(tresp)
            if lresp not in (1, 2):
                raise tfchain.errors.ExplorerInvalidResponse(
                    "expected one or two transactions to be returned, not {}".
                    format(lresp), endpoint, resp)
            # parse the transaction(s)
            creation_txn = tresp[0]
            spend_txn = None
            if lresp == 2:
                if tresp[1]['height'] > creation_txn['height']:
                    spend_txn = tresp[1]
                else:
                    spend_txn = creation_txn
                    creation_txn = tresp[1]
            creation_txn = self._transaction_from_explorer_transaction(
                creation_txn, endpoint=endpoint, resp=resp)
            if spend_txn is not None:
                spend_txn = self._transaction_from_explorer_transaction(
                    spend_txn, endpoint=endpoint, resp=resp)
            # collect the output
            output = None
            for out in (creation_txn.coin_outputs
                        if hash_type == 'coinoutputid' else
                        creation_txn.blockstake_outputs):
                if str(out.id) == id:
                    output = out
                    break
            if output is None:
                raise tfchain.errors.ExplorerInvalidResponse(
                    "expected output {} to be part of creation Tx, but it wasn't"
                    .format(id), endpoint, resp)
            # return the output and related transaction(s)
            return (output, creation_txn, spend_txn)
        except KeyError as exc:
            # return a KeyError as an invalid Explorer Response
            raise tfchain.errors.ExplorerInvalidResponse(
                str(exc), endpoint, resp) from exc
Exemple #11
0
    def unlockhash_get(self, target):
        """
        Get all transactions linked to the given unlockhash (target),
        as well as other information such as the multisig addresses linked to the given unlockhash (target).

        target can be any of:
            - None: unlockhash of the Free-For-All wallet will be used
            - str (or unlockhash/bytes/bytearray): target is assumed to be the unlockhash of a personal wallet
            - list: target is assumed to be the addresses of a MultiSig wallet where all owners (specified as a list of addresses) have to sign
            - tuple (addresses, sigcount): target is a sigcount-of-addresscount MultiSig wallet

        @param target: the target wallet to look up transactions for in the explorer, see above for more info
        """
        unlockhash = str(ConditionTypes.from_recipient(target).unlockhash)
        endpoint = "/explorer/hashes/" + unlockhash
        resp = self.explorer_get(endpoint=endpoint)
        resp = json_loads(resp)
        try:
            if resp['hashtype'] != 'unlockhash':
                raise tfchain.errors.ExplorerInvalidResponse(
                    "expected hash type 'unlockhash' not '{}'".format(
                        resp['hashtype']), endpoint, resp)
            # parse the transactions
            transactions = []
            for etxn in resp['transactions']:
                # parse the explorer transaction
                transaction = self._transaction_from_explorer_transaction(
                    etxn, endpoint=endpoint, resp=resp)
                # append the transaction to the list of transactions
                transactions.append(transaction)
            # collect all multisig addresses
            multisig_addresses = [
                UnlockHash.from_json(obj=uh)
                for uh in resp.get('multisigaddresses', None) or []
            ]
            for addr in multisig_addresses:
                if addr.type != UnlockHashType.MULTI_SIG:
                    raise tfchain.errors.ExplorerInvalidResponse(
                        "invalid unlock hash type {} for MultiSignature Address (expected: 3)"
                        .format(addr.type), endpoint, resp)
            erc20_info = None
            if 'erc20info' in resp:
                info = resp['erc20info']
                erc20_info = ERC20AddressInfo(
                    address_tft=UnlockHash.from_json(info['tftaddress']),
                    address_erc20=ERC20Address.from_json(info['erc20address']),
                    confirmations=int(info['confirmations']),
                )

            # sort the transactions by height
            transactions.sort(key=(lambda txn: sys.maxsize
                                   if txn.height < 0 else txn.height),
                              reverse=True)

            # return explorer data for the unlockhash
            return ExplorerUnlockhashResult(
                unlockhash=UnlockHash.from_json(unlockhash),
                transactions=transactions,
                multisig_addresses=multisig_addresses,
                erc20_info=erc20_info,
                client=self)
        except KeyError as exc:
            # return a KeyError as an invalid Explorer Response
            raise tfchain.errors.ExplorerInvalidResponse(
                str(exc), endpoint, resp) from exc
Exemple #12
0
 def block_get(self, value):
     """
     Get a block from an available explorer Node.
     
     @param value: the identifier or height that points to the desired block
     """
     endpoint = "/explorer/?"
     resp = {}
     try:
         # get the explorer block
         if isinstance(value, int):
             endpoint = "/explorer/blocks/{}".format(int(value))
             resp = self.explorer_get(endpoint=endpoint)
             resp = json_loads(resp)
             resp = resp['block']
         else:
             blockid = self._normalize_id(value)
             endpoint = "/explorer/hashes/" + blockid
             resp = self.explorer_get(endpoint=endpoint)
             resp = json_loads(resp)
             if resp['hashtype'] != 'blockid':
                 raise tfchain.errors.ExplorerInvalidResponse(
                     "expected hash type 'blockid' not '{}'".format(
                         resp['hashtype']), endpoint, resp)
             resp = resp['block']
             if resp['blockid'] != blockid:
                 raise tfchain.errors.ExplorerInvalidResponse(
                     "expected block ID '{}' not '{}'".format(
                         blockid, resp['blockid']), endpoint, resp)
         # parse the transactions
         transactions = []
         for etxn in resp['transactions']:
             # parse the explorer transaction
             transaction = self._transaction_from_explorer_transaction(
                 etxn, endpoint=endpoint, resp=resp)
             # append the transaction to the list of transactions
             transactions.append(transaction)
         rawblock = resp['rawblock']
         # parse the parent id
         parentid = Hash.from_json(obj=rawblock['parentid'])
         # parse the miner payouts
         miner_payouts = []
         minerpayoutids = resp.get('minerpayoutids', None) or []
         eminerpayouts = rawblock.get('minerpayouts', None) or []
         if len(eminerpayouts) != len(minerpayoutids):
             raise tfchain.errors.ExplorerInvalidResponse(
                 "amount of miner payouts and payout ids are not matching: {} != {}"
                 .format(len(eminerpayouts),
                         len(minerpayoutids)), endpoint, resp)
         for idx, mp in enumerate(eminerpayouts):
             id = Hash.from_json(minerpayoutids[idx])
             value = Currency.from_json(mp['value'])
             unlockhash = UnlockHash.from_json(mp['unlockhash'])
             miner_payouts.append(
                 ExplorerMinerPayout(id=id,
                                     value=value,
                                     unlockhash=unlockhash))
         # get the timestamp and height
         height = int(resp['height'])
         timestamp = int(rawblock['timestamp'])
         # get the block's identifier
         blockid = Hash.from_json(resp['blockid'])
         # return the block, as reported by the explorer
         return ExplorerBlock(id=blockid,
                              parentid=parentid,
                              height=height,
                              timestamp=timestamp,
                              transactions=transactions,
                              miner_payouts=miner_payouts)
     except KeyError as exc:
         # return a KeyError as an invalid Explorer Response
         raise tfchain.errors.ExplorerInvalidResponse(
             str(exc), endpoint, resp) from exc