def fetch_transacton_timestamps(result): if result.creation_transaction.unconfirmed: return result # return as is ps = [self._block_get_by_hash(result.creation_transaction.blockid)] if result.spend_transaction != None and not result.spend_transaction.unconfirmed: ps.append( self._block_get_by_hash(result.spend_transaction.blockid)) p = jsasync.wait(*ps) def aggregate(results): if len(results) == 1: # assign just the creation transacton timestamp _, block = results[0] _assign_block_properties_to_transacton( result.creation_transaction, block) return result # assign both creation- and spend transaction timestamp _, block_a = results[0] _, block_b = results[1] if block_a.id.__ne__(result.creation_transaction.blockid): block_c = block_a block_a = block_b block_b = block_c _assign_block_properties_to_transacton( result.creation_transaction, block_a) _assign_block_properties_to_transacton( result.spend_transaction, block_b) return result return jsasync.chain(p, aggregate)
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() if not jsobj.is_js_obj(transaction): raise TypeError("transaction is of an invalid type {}".format( type(transaction))) endpoint = "/transactionpool/transactions" def cb(result): _, transaction = result try: return Hash(value=transaction['transactionid']).__str__() except (KeyError, ValueError, TypeError) as exc: # return a KeyError as an invalid Explorer Response raise tferrors.ExplorerInvalidResponse(str(exc), endpoint, transaction) from exc return jsasync.chain( self.explorer_post(endpoint=endpoint, data=transaction), cb)
def fetch_transacton_block(result): transactions = {} for transaction in result.transactions: if not transaction.unconfirmed: bid = transaction.blockid.__str__() if bid not in transactions: transactions[bid] = [] transactions[bid].append(transaction) if len(transactions) == 0: return result # return as is, nothing to do def generator(): for blockid in jsobj.get_keys(transactions): yield self._block_get_by_hash(blockid) def result_cb(block_result): _, block = block_result for transaction in transactions[block.get_or('blockid', '')]: _assign_block_properties_to_transacton(transaction, block) def aggregate(): return result return jsasync.chain( jsasync.promise_pool_new(generator, cb=result_cb), aggregate)
def unconfirmed_transactions_get(self): """ Get all unconfirmed transactions from an available Explorer node. """ endpoint = "/transactionpool/transactions" def cb(result): _, resp = result try: # parse the unconfirmed transactions unconfirmed_transactions = [] resp_transactions = resp['transactions'] if resp_transactions != None and jsobj.is_js_arr( resp_transactions): for etxn in resp_transactions: # parse the (raw) transaction transaction = transactions.from_json(obj=etxn) # compute the transactionID manually transaction.id = transaction.transaction_id_new() # force it to be unconfirmed transaction.unconfirmed = True # append the transaction to the list of transactions unconfirmed_transactions.append(transaction) return unconfirmed_transactions except (KeyError, ValueError, TypeError) as exc: # return a KeyError as an invalid Explorer Response raise tferrors.ExplorerInvalidResponse(str(exc), endpoint, transaction) from exc return jsasync.chain(self.explorer_get(endpoint=endpoint), cb)
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 != None: if not isinstance(height, (int, str)): raise TypeError("invalid block height given") if isinstance(height, str): height = jsstr.to_int(height) endpoint += "/{}".format(height) # define the cb to get the mint condition def cb(result): _, resp = result 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 tferrors.ExplorerInvalidResponse(str(exc), endpoint, resp) from exc # get + parse the mint condition as a promise return jsasync.chain(self._client.explorer_get(endpoint=endpoint), cb)
def blockchain_info_get(self): """ Get the current blockchain info, using the last known block, as reported by an explorer. """ def get_block(result): used_addr, raw_block = result address = used_addr def get_block_with_tag(result): return ('b', (address, result)) blockid = Hash.from_json(obj=raw_block['blockid']) return jsasync.chain(self.block_get(blockid), get_block_with_tag) def get_constants_with_tag(result): return ('c', result) def get_info(results): if len(results) != 2: raise RuntimeError( "expected 2 values as result, but received: {}".format( results)) d = dict(results) _, raw_constants = d['c'] info = raw_constants['chaininfo'] constants = BlockchainConstants( info['Name'], info['ChainVersion'], info['NetworkName'], ) address, last_block = d['b'] return ExplorerBlockchainInfo( constants=constants, last_block=last_block, explorer_address=address, ) return jsasync.chain( jsasync.wait( jsasync.chain(self.explorer_get(endpoint="/explorer"), get_block), jsasync.chain( self.explorer_get(endpoint="/explorer/constants"), get_constants_with_tag), ), get_info)
def f(reason): if isinstance(reason, tferrors.ExplorerUserError): raise reason # no need to retry user errors jslog.debug( "retrying on another server, previous POST call failed: {}" .format(reason)) # do the request and check the response return jsasync.chain( jshttp.http_post(address, endpoint, s, headers), resolve)
def get_block(result): used_addr, raw_block = result address = used_addr def get_block_with_tag(result): return ('b', (address, result)) blockid = Hash.from_json(obj=raw_block['blockid']) return jsasync.chain(self.block_get(blockid), get_block_with_tag)
def fetch_transacton_timestamps(result): _, transaction = result p = self._block_get_by_hash(transaction.blockid) def aggregate(result): _, block = result _assign_block_properties_to_transacton(transaction, block) return transaction return jsasync.chain(p, aggregate)
def _block_get_by_height(self, value): endpoint = "/explorer/blocks/{}".format(value) def get_block_prop(result): _, result = result block = result.get_or('block', None) if block == None: raise tferrors.ExplorerInvalidResponse( "block property is undefined", endpoint, result) return (endpoint, block) return jsasync.chain(self.explorer_get(endpoint=endpoint), get_block_prop)
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 """ p = None # get the explorer block if isinstance(value, int): p = self._block_get_by_height(value) else: p = self._block_get_by_hash(value) return jsasync.chain(p, self._block_get_parse_cb)
def data_get(self, endpoint): """ get data from an explorer at the endpoint from any explorer that is available on one of the given urls. The list of urls is traversed in random order until an explorer returns with a 200 OK status. @param endpoint: the endpoint to get the data from """ # if we need to update our consensus about the explorers, do this first cp = self._update_consensus_if_needed() if cp == None: return self._data_get_body(endpoint) return jsasync.chain(cp, lambda _: self._data_get_body(endpoint))
def _update_consensus_if_needed(self): now = int(datetime.now().timestamp()) if now - self._last_consensus_update_time < 60 * 5: return None # nothing to do self._last_consensus_update_time = now # probe all explorer nodes for consensus def generator(): for addr in self._addresses: yield jsasync.catch_promise( jsasync.chain(jshttp.http_get(addr, "/explorer"), self._height_result_cb), self._height_error_cb_new(addr)) def result_cb(results): d = jsobj.new_dict() a = {} for addr, height in results: if height not in a: a[height] = [] a[height].append(addr) d[height] = d.get_or(height, 0) + 1 c_height = -1 c_height_votes = -1 for height, votes in jsobj.get_items(d): if votes > c_height_votes or (votes == c_height_votes and height > c_height): c_height = height c_height_votes = votes if c_height == -1: jslog.error( "update_consensus of explorer (HTTP): no explorer addresses are available" ) # assign all addresses and hope for the best all_addresses = [] for _, addresses in jsobj.get_items(a): all_addresses = jsarr.concat(all_addresses, addresses) self._consensus_addresses = addresses else: # select the explorer addresses with the desired height self._consensus_addresses = a[c_height] return jsasync.chain(jsasync.promise_pool_new(generator), result_cb)
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 def cb(result): _, result = result try: if result['hashtype'] != 'transactionid': raise tferrors.ExplorerInvalidResponse( "expected hash type 'transactionid' not '{}'".format( result['hashtype']), endpoint, result) txnresult = result['transaction'] if txnresult['id'] != txid: raise tferrors.ExplorerInvalidResponse( "expected transaction ID '{}' not '{}'".format( txid, txnresult['id']), endpoint, result) return self._transaction_from_explorer_transaction( txnresult, endpoint=endpoint, resp=result) except KeyError as exc: # return a KeyError as an invalid Explorer Response raise tferrors.ExplorerInvalidResponse(str(exc), endpoint, result) from exc # fetch timestamps seperately # TODO: make a pull request in Rivine to return [parent] block optionally with (or instead of) transaction. def fetch_transacton_timestamps(result): _, transaction = result p = self._block_get_by_hash(transaction.blockid) def aggregate(result): _, block = result _assign_block_properties_to_transacton(transaction, block) return transaction return jsasync.chain(p, aggregate) return jsasync.chain(self.explorer_get(endpoint=endpoint), cb, fetch_transacton_timestamps)
def _block_get_by_hash(self, value): blockid = self._normalize_id(value) endpoint = "/explorer/hashes/" + blockid def get_block_prop(result): _, result = result try: if result['hashtype'] != 'blockid': raise tferrors.ExplorerInvalidResponse( "expected hash type 'blockid' not '{}'".format( result['hashtype']), endpoint, result) block = result['block'] if block['blockid'] != blockid: raise tferrors.ExplorerInvalidResponse( "expected block ID '{}' not '{}'".format( blockid.__str__(), block['blockid']), endpoint, result) return (endpoint, block) except KeyError as exc: raise tferrors.ExplorerInvalidResponse(str(exc), endpoint, result) from exc return jsasync.chain(self.explorer_get(endpoint=endpoint), get_block_prop)
def _data_get_body(self, endpoint): indices = list(range(len(self._consensus_addresses))) random.shuffle(indices) def resolve(result): if result.code == 200: return (result.address, jsobj.as_dict(result.data)) if result.code == 204 or (result.code == 400 and ('unrecognized hash' in result.data or 'not found' in result.data)): raise tferrors.ExplorerNoContent( "GET: no content available (code: 204)", endpoint) if result.code == 400: # are there other error codes? raise tferrors.ExplorerBadRequest( "error (code: {}): {}".format(result.code, result.data), endpoint) raise tferrors.ExplorerServerError( "error (code: {}): {}".format(result.code, result.data), endpoint) address = self._consensus_addresses[indices[0]] if not isinstance(address, str): raise TypeError( "explorer address expected to be a string, not {}".format( type(address))) # do the request and check the response p = jsasync.chain(jshttp.http_get(address, endpoint), resolve) # factory for our fallback callbacks # called to try on another server in case # a non-user error occured on the previous one def create_fallback_catch_cb(address): def f(reason): if isinstance(reason, tferrors.ExplorerUserError): raise reason # no need to retry user errors jslog.debug( "retrying on another server, previous GET call failed: {}". format(reason)) # do the request and check the response return jsasync.chain(jshttp.http_get(address, endpoint), resolve) return f # for any remaining index, do the same logic, but as a chained catch for idx in indices[1:]: address = self._consensus_addresses[idx] if not isinstance(address, str): raise TypeError( "explorer address expected to be a string, not {}".format( type(address))) cb = create_fallback_catch_cb(address) p = jsasync.catch_promise(p, cb) # define final catch cb, as a last fallback def final_catch(reason): # pass on user errors if isinstance(reason, tferrors.ExplorerUserError): raise reason # no need to retry user errors jslog.debug( "servers exhausted, previous GET call failed as well: {}". format(reason)) raise tferrors.ExplorerNotAvailable("no explorer was available", endpoint, self._consensus_addresses) # return the final promise chain return jsasync.catch_promise(p, final_catch)
def generator(): for addr in self._addresses: yield jsasync.catch_promise( jsasync.chain(jshttp.http_get(addr, "/explorer"), self._height_result_cb), self._height_error_cb_new(addr))
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 def cb(result): _, result = result try: hash_type = result['hashtype'] if hash_type != expected_hash_type: raise tferrors.ExplorerInvalidResponse( "expected hash type '{}', not '{}'".format( expected_hash_type, hash_type), endpoint, result) tresp = result['transactions'] lresp = len(tresp) if lresp not in (1, 2): raise tferrors.ExplorerInvalidResponse( "expected one or two transactions to be returned, not {}" .format(lresp), endpoint, result) # 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=result) if spend_txn != None: spend_txn = self._transaction_from_explorer_transaction( spend_txn, endpoint=endpoint, resp=result) # 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 == None: raise tferrors.ExplorerInvalidResponse( "expected output {} to be part of creation Tx, but it wasn't" .format(id), endpoint, result) # return the output and related transaction(s) return ExplorerOutputResult(output, creation_txn, spend_txn) except KeyError as exc: # return a KeyError as an invalid Explorer Response raise tferrors.ExplorerInvalidResponse(str(exc), endpoint, result) from exc # fetch timestamps seperately # TODO: make a pull request in Rivine to return timestamps together with regular results, # as it is rediculous to have to do this def fetch_transacton_timestamps(result): if result.creation_transaction.unconfirmed: return result # return as is ps = [self._block_get_by_hash(result.creation_transaction.blockid)] if result.spend_transaction != None and not result.spend_transaction.unconfirmed: ps.append( self._block_get_by_hash(result.spend_transaction.blockid)) p = jsasync.wait(*ps) def aggregate(results): if len(results) == 1: # assign just the creation transacton timestamp _, block = results[0] _assign_block_properties_to_transacton( result.creation_transaction, block) return result # assign both creation- and spend transaction timestamp _, block_a = results[0] _, block_b = results[1] if block_a.id.__ne__(result.creation_transaction.blockid): block_c = block_a block_a = block_b block_b = block_c _assign_block_properties_to_transacton( result.creation_transaction, block_a) _assign_block_properties_to_transacton( result.spend_transaction, block_b) return result return jsasync.chain(p, aggregate) # return as chained promise return jsasync.chain(self.explorer_get(endpoint=endpoint), cb, fetch_transacton_timestamps)
def _data_post_body(self, endpoint, data): indices = list(range(len(self._consensus_addresses))) random.shuffle(indices) headers = { 'Content-Type': 'Application/json;charset=UTF-8', } s = data if not isinstance(s, str): s = jsjson.json_dumps(s) def resolve(result): if result.code == 200: return (result.address, jsobj.as_dict(result.data)) if result.code == 400: # are there other error codes? jslog.warning( "invalid data object posted to {}:".format(endpoint), s) raise tferrors.ExplorerBadRequest( "error (code: {}): {}".format(result.code, result.data), endpoint) raise tferrors.ExplorerServerPostError( "POST: unexpected error (code: {}): {}".format( result.code, result.data), endpoint, data=data) address = self._consensus_addresses[indices[0]] if not isinstance(address, str): raise TypeError( "explorer address expected to be a string, not {}".format( type(address))) # do the request and check the response p = jsasync.chain(jshttp.http_post(address, endpoint, s, headers), resolve) # factory for our fallback callbacks # called to try on another server in case # a non-user error occured on the previous one def create_fallback_catch_cb(address): def f(reason): if isinstance(reason, tferrors.ExplorerUserError): raise reason # no need to retry user errors jslog.debug( "retrying on another server, previous POST call failed: {}" .format(reason)) # do the request and check the response return jsasync.chain( jshttp.http_post(address, endpoint, s, headers), resolve) return f # for any remaining index, do the same logic, but as a chained catch for idx in indices[1:]: address = self._consensus_addresses[idx] if not isinstance(address, str): raise TypeError( "explorer address expected to be a string, not {}".format( type(address))) cb = create_fallback_catch_cb(address) p = jsasync.catch_promise(p, cb) # define final catch cb, as a last fallback def final_catch(reason): # pass on user errors if isinstance(reason, tferrors.ExplorerUserError): raise reason # no need to retry user errors jslog.debug( "servers exhausted, previous POST call failed as well: {}". format(reason)) raise tferrors.ExplorerNotAvailable("no explorer was available", endpoint, self._consensus_addresses) # return the final promise chain return jsasync.catch_promise(p, final_catch)
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 = ConditionTypes.from_recipient(target).unlockhash.__str__() endpoint = "/explorer/hashes/" + unlockhash def catch_no_content(reason): if isinstance(reason, tferrors.ExplorerNoContent): return ExplorerUnlockhashResult( unlockhash=UnlockHash.from_json(unlockhash), transactions=[], multisig_addresses=None, erc20_info=None, ) # pass on any other reason raise reason def cb(result): _, resp = result try: if resp['hashtype'] != 'unlockhash': raise tferrors.ExplorerInvalidResponse( "expected hash type 'unlockhash' not '{}'".format( resp['hashtype']), endpoint, resp) # parse the transactions transactions = [] resp_transactions = resp['transactions'] if resp_transactions != None and jsobj.is_js_arr( resp_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_or('multisigaddresses', None) or [] ] for addr in multisig_addresses: if addr.uhtype.__ne__(UnlockHashType.MULTI_SIG): raise tferrors.ExplorerInvalidResponse( "invalid unlock hash type {} for MultiSignature Address (expected: 3)" .format(addr.uhtype.value), 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 def txn_arr_sort(a, b): height_a = pow(2, 64) if a.height < 0 else a.height height_b = pow(2, 64) if b.height < 0 else b.height if height_a < height_b: return -1 if height_a > height_b: return 1 tx_order_a = pow( 2, 64) if a.transaction_order < 0 else a.transaction_order tx_order_b = pow( 2, 64) if b.transaction_order < 0 else b.transaction_order if tx_order_a < tx_order_b: return -1 if tx_order_a > tx_order_b: return 1 return 0 transactions = jsarr.sort(transactions, txn_arr_sort, 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, ) except KeyError as exc: # return a KeyError as an invalid Explorer Response raise tferrors.ExplorerInvalidResponse(str(exc), endpoint, resp) from exc # fetch timestamps seperately # TODO: make a pull request in Rivine to return timestamps together with regular results, # as it is rediculous to have to do this def fetch_transacton_block(result): transactions = {} for transaction in result.transactions: if not transaction.unconfirmed: bid = transaction.blockid.__str__() if bid not in transactions: transactions[bid] = [] transactions[bid].append(transaction) if len(transactions) == 0: return result # return as is, nothing to do def generator(): for blockid in jsobj.get_keys(transactions): yield self._block_get_by_hash(blockid) def result_cb(block_result): _, block = block_result for transaction in transactions[block.get_or('blockid', '')]: _assign_block_properties_to_transacton(transaction, block) def aggregate(): return result return jsasync.chain( jsasync.promise_pool_new(generator, cb=result_cb), aggregate) return jsasync.catch_promise( jsasync.chain(self.explorer_get(endpoint=endpoint), cb, fetch_transacton_block), catch_no_content)