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'
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
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
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)
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
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
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
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))
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
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
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