def server_version(self, client_name='', protocol_version=None): '''Returns the server version as a string. client_name: a string identifying the client protocol_version: the protocol version spoken by the client ''' if self.sv_seen and self.protocol_tuple >= (1, 4): raise RPCError(BAD_REQUEST, f'server.version already sent') self.sv_seen = True if client_name: client_name = str(client_name) if self.env.drop_client is not None and \ self.env.drop_client.match(client_name): self.close_after_send = True raise RPCError(BAD_REQUEST, f'unsupported client: {client_name}') self.client = client_name[:17] # Find the highest common protocol version. Disconnect if # that protocol version in unsupported. ptuple, client_min = util.protocol_version( protocol_version, self.PROTOCOL_MIN, self.PROTOCOL_MAX) if ptuple is None: if client_min > self.PROTOCOL_MIN: self.logger.info(f'client requested future protocol version ' f'{util.version_string(client_min)} ' f'- is your software out of date?') self.close_after_send = True raise RPCError(BAD_REQUEST, f'unsupported protocol version: {protocol_version}') self.set_protocol_handlers(ptuple) return (electrumx.version, self.protocol_version_string())
def server_version(self, client_name=None, protocol_version=None): '''Returns the server version as a string. client_name: a string identifying the client protocol_version: the protocol version spoken by the client ''' if client_name: if self.env.drop_client is not None and \ self.env.drop_client.match(client_name): self.close_after_send = True raise RPCError(BAD_REQUEST, f'unsupported client: {client_name}') self.client = str(client_name)[:17] try: self.client_version = tuple(int(part) for part in self.client.split('.')) except Exception: pass # Find the highest common protocol version. Disconnect if # that protocol version in unsupported. ptuple = self.controller.protocol_tuple(protocol_version) if ptuple is None: self.logger.info('unsupported protocol version request {}' .format(protocol_version)) self.close_after_send = True raise RPCError(BAD_REQUEST, f'unsupported protocol version: {protocol_version}') self.set_protocol_handlers(ptuple) return (self.controller.VERSION, self.protocol_version)
async def wrapper(*args, **kwargs): try: return await decorated_function(*args, **kwargs) except DaemonError as daemon_error: error_dict = daemon_error.args[0] message, code = error_dict['message'], error_dict['code'] raise RPCError(code=code, message=message)
async def raw_header(self, height): '''Return the binary header at the given height.''' try: return await self.db.raw_header(height) except IndexError: raise RPCError(BAD_REQUEST, f'height {height:,d} ' 'out of range') from None
def new_subscription(self): if self.subs_room <= 0: self.subs_room = self.max_subs - self._sub_count() if self.subs_room <= 0: raise RPCError(BAD_REQUEST, f'server subscription limit ' f'{self.max_subs:,d} reached') self.subs_room -= 1
async def transaction_broadcast(self, raw_tx): '''Broadcast a raw transaction to the network. raw_tx: the raw transaction as a hexadecimal string''' # This returns errors as JSON RPC errors, as is natural try: hex_hash = await self.session_mgr.broadcast_transaction(raw_tx) except DaemonError as e: error, = e.args message = error['message'] self.logger.info(f'error sending transaction: {message}') raise RPCError(BAD_REQUEST, 'the transaction was rejected by ' f'network rules.\n\n{message}\n[{raw_tx}]') else: self.txs_sent += 1 client_ver = util.protocol_tuple(self.client) if client_ver != (0, ): msg = self.coin.upgrade_required(client_ver) if msg: self.logger.info(f'sent tx: {hex_hash}. and warned user to upgrade their ' f'client from {self.client}') return msg self.logger.info(f'sent tx: {hex_hash}') return hex_hash
async def transaction_get_merkle(self, tx_hash, height): '''Return the markle tree to a confirmed transaction given its hash and height. tx_hash: the transaction hash as a hexadecimal string height: the height of the block it is in ''' self.assert_tx_hash(tx_hash) height = non_negative_integer(height) hex_hashes = await self.daemon_request('block_hex_hashes', height, 1) block_hash = hex_hashes[0] block = await self.daemon_request('deserialised_block', block_hash) tx_hashes = block['tx'] try: pos = tx_hashes.index(tx_hash) except ValueError: raise RPCError( BAD_REQUEST, f'tx hash {tx_hash} not in ' f'block {block_hash} at height {height:,d}') hashes = [hex_str_to_hash(hash) for hash in tx_hashes] branch, root = self.bp.merkle.branch_and_root(hashes, pos) branch = [hash_to_hex_str(hash) for hash in branch] return {"block_height": height, "merkle": branch, "pos": pos}
async def tx_merkle(self, tx_hash, height): '''tx_hash is a hex string.''' hex_hashes = await self.daemon_request('block_hex_hashes', height, 1) block = await self.daemon_request('deserialised_block', hex_hashes[0]) tx_hashes = block['tx'] try: pos = tx_hashes.index(tx_hash) except ValueError: raise RPCError( BAD_REQUEST, f'tx hash {tx_hash} not in ' f'block {hex_hashes[0]} at height {height:,d}') idx = pos hashes = [hex_str_to_hash(txh) for txh in tx_hashes] merkle_branch = [] while len(hashes) > 1: if len(hashes) & 1: hashes.append(hashes[-1]) idx = idx - 1 if (idx & 1) else idx + 1 merkle_branch.append(hash_to_str(hashes[idx])) idx //= 2 hashes = [ double_sha256(hashes[n] + hashes[n + 1]) for n in range(0, len(hashes), 2) ] return {"block_height": height, "merkle": merkle_branch, "pos": pos}
def scripthash_to_hashX(scripthash): try: bin_hash = hex_str_to_hash(scripthash) if len(bin_hash) == 32: return bin_hash[:HASHX_LEN] except Exception: pass raise RPCError(BAD_REQUEST, f'{scripthash} is not a valid script hash')
async def rpc_daemon_url(self, daemon_url): '''Replace the daemon URL.''' daemon_url = daemon_url or self.env.daemon_url try: self.daemon.set_url(daemon_url) except Exception as e: raise RPCError(BAD_REQUEST, f'an error occured: {e!r}') return f'now using daemon at {self.daemon.logged_url()}'
def rpc_daemon_url(self, daemon_url=None): '''Replace the daemon URL.''' daemon_url = daemon_url or self.env.daemon_url try: self.daemon.set_urls(self.env.coin.daemon_urls(daemon_url)) except Exception as e: raise RPCError(BAD_REQUEST, f'an error occured: {e}') return 'now using daemon at {}'.format(self.daemon.logged_url())
def assert_tx_hash(self, value): '''Raise an RPCError if the value is not a valid transaction hash.''' try: if len(util.hex_to_bytes(value)) == 32: return except Exception: pass raise RPCError(BAD_REQUEST, f'{value} should be a transaction hash')
def rpc_reorg(self, count=3): '''Force a reorg of the given number of blocks. count: number of blocks to reorg (default 3) ''' count = non_negative_integer(count) if not self.controller.bp.force_chain_reorg(count): raise RPCError(BAD_REQUEST, 'still catching up with daemon') return 'scheduled a reorg of {:,d} blocks'.format(count)
async def test_estimatefee(daemon): method_not_found = RPCError(JSONRPCv1.METHOD_NOT_FOUND, 'nope') if isinstance(daemon, FakeEstimateFeeDaemon): result = daemon.coin.ESTIMATE_FEE else: result = -1 with ClientSessionGood(('estimatesmartfee', [], method_not_found), ('estimatefee', [2], result)): assert await daemon.estimatefee(2) == result
def assert_claim_id(self, value): '''Raise an RPCError if the value is not a valid claim id hash.''' try: if len(util.hex_to_bytes(value)) == 20: return except Exception: pass raise RPCError(1, f'{value} should be a claim id hash')
async def rpc_reorg(self, count): '''Force a reorg of the given number of blocks. count: number of blocks to reorg ''' count = non_negative_integer(count) if not self.bp.force_chain_reorg(count): raise RPCError(BAD_REQUEST, 'still catching up with daemon') return f'scheduled a reorg of {count:,d} blocks'
def non_negative_integer(value): '''Return param value it is or can be converted to a non-negative integer, otherwise raise an RPCError.''' try: value = int(value) if value >= 0: return value except ValueError: pass raise RPCError(BAD_REQUEST, f'{value} should be a non-negative integer')
async def hashX_subscribe(self, hashX, alias): # First check our limit. if len(self.hashX_subs) >= self.max_subs: raise RPCError(BAD_REQUEST, 'your address subscription limit ' f'{self.max_subs:,d} reached') # Now let the controller check its limit self.controller.new_subscription() self.hashX_subs[hashX] = alias return await self.address_status(hashX)
async def masternode_announce_broadcast(self, signmnb): '''Pass through the masternode announce message to be broadcast by the daemon.''' try: return await self.daemon.masternode_broadcast(['relay', signmnb]) except DaemonError as e: error, = e.args message = error['message'] self.logger.info('masternode_broadcast: {}'.format(message)) raise RPCError(BAD_REQUEST, 'the masternode broadcast was ' f'rejected.\n\n{message}\n[{signmnb}]')
async def contract_event_subscribe(self, hash160, contract_addr, topic): if len(self.contract_subs) >= self.max_subs: raise RPCError( 'your contract subscription limit {:,d} reached'.format( self.max_subs)) hashY = self.controller.coin.hash160_contract_to_hashY( hash160, contract_addr) hashY = hashY + topic.encode() self.contract_subs[hashY] = (hash160, contract_addr, topic) return await self.hash160_contract_status(hash160, contract_addr, topic)
async def test_warming_up(daemon, caplog): warming_up = RPCError(-28, 'reading block index') height = 100 daemon.init_retry = 0.01 with caplog.at_level(logging.INFO): with ClientSessionGood(('getblockcount', [], warming_up), ('getblockcount', [], height)): assert await daemon.height() == height assert in_caplog(caplog, "starting up checking blocks") assert in_caplog(caplog, "running normally")
async def transaction_get(self, tx_hash, verbose=False): '''Return the serialized raw transaction given its hash tx_hash: the transaction hash as a hexadecimal string verbose: passed on to the daemon ''' self.assert_tx_hash(tx_hash) if verbose not in (True, False): raise RPCError(BAD_REQUEST, f'"verbose" must be a boolean') return await self.daemon_request('getrawtransaction', tx_hash, verbose)
def _merkle_proof(self, cp_height, height): max_height = self.height() if not height <= cp_height <= max_height: raise RPCError(BAD_REQUEST, f'require header height {height:,d} <= ' f'cp_height {cp_height:,d} <= ' f'chain height {max_height:,d}') branch, root = self.bp.header_mc.branch_and_root(cp_height + 1, height) return { 'branch': [hash_to_hex_str(elt) for elt in branch], 'root': hash_to_hex_str(root), }
async def transaction_id_from_pos(self, height, tx_pos, merkle=False): '''Return the txid and optionally a merkle proof, given a block height and position in the block. ''' tx_pos = non_negative_integer(tx_pos) height = non_negative_integer(height) if merkle not in (True, False): raise RPCError(BAD_REQUEST, f'"merkle" must be a boolean') block_hash, tx_hashes = await self._block_hash_and_tx_hashes(height) try: tx_hash = tx_hashes[tx_pos] except IndexError: raise RPCError(BAD_REQUEST, f'no tx at position {tx_pos:,d} in ' f'block {block_hash} at height {height:,d}') if merkle: branch = self._get_merkle_branch(tx_hashes, tx_pos) return {"tx_hash": tx_hash, "merkle": branch} else: return tx_hash
def _for_each_session(self, session_ids, operation): if not isinstance(session_ids, list): raise RPCError(BAD_REQUEST, 'expected a list of session IDs') result = [] for session_id in session_ids: session = self._lookup_session(session_id) if session: result.append(operation(session)) else: result.append('unknown session: {}'.format(session_id)) return result
def server_version(self, client_name=None, protocol_version=None): '''Returns the server version as a string. client_name: a string identifying the client protocol_version: the protocol version spoken by the client ''' if client_name: if self.env.drop_client is not None and \ self.env.drop_client.match(client_name): self.close_after_send = True raise RPCError(BAD_REQUEST, f'unsupported client: {client_name}') self.client = str(client_name)[:17] try: self.client_version = tuple( int(part) for part in self.client.split('.')) except Exception: pass # Find the highest common protocol version. Disconnect if # that protocol version in unsupported. ptuple = util.protocol_version(protocol_version, version.PROTOCOL_MIN, version.PROTOCOL_MAX) # From protocol version 1.1, protocol_version cannot be omitted if ptuple is None or (ptuple >= (1, 1) and protocol_version is None): self.logger.info('unsupported protocol version request {}'.format( protocol_version)) self.close_after_send = True raise RPCError( BAD_REQUEST, f'unsupported protocol version: {protocol_version}') self.set_protocol_handlers(ptuple) # The return value depends on the protocol version if ptuple < (1, 1): return version.VERSION else: return (version.VERSION, self.protocol_version)
async def test_estimatefee_smart(daemon): bad_args = RPCError(JSONRPCv1.INVALID_ARGS, 'bad args') if isinstance(daemon, FakeEstimateFeeDaemon): return rate = 0.0002 result = {'feerate': rate} with ClientSessionGood(('estimatesmartfee', [], bad_args), ('estimatesmartfee', [2], result)): assert await daemon.estimatefee(2) == rate # Test the rpc_available_cache is used with ClientSessionGood(('estimatesmartfee', [2], result)): assert await daemon.estimatefee(2) == rate
async def test_warming_up_batch(daemon, caplog): warming_up = RPCError(-28, 'reading block index') first = 5 count = 1 daemon.init_retry = 0.01 hashes = ['hex_hash5'] with caplog.at_level(logging.INFO): with ClientSessionGood(('getblockhash', [[first]], [warming_up]), ('getblockhash', [[first]], hashes)): assert await daemon.block_hex_hashes(first, count) == hashes assert in_caplog(caplog, "starting up checking blocks") assert in_caplog(caplog, "running normally")
async def test_get_raw_transactions(daemon): hex_hashes = ['deadbeef0', 'deadbeef1'] args_list = [[hex_hash, 0] for hex_hash in hex_hashes] raw_txs_hex = ['fffefdfc', '0a0b0c0d'] raw_txs = [bytes.fromhex(raw_tx) for raw_tx in raw_txs_hex] # Test 0 - old daemon's reject False with ClientSessionGood(('getrawtransaction', args_list, raw_txs_hex)): assert await daemon.getrawtransactions(hex_hashes) == raw_txs # Test one error tx_not_found = RPCError(-1, 'some error message') results = ['ff0b7d', tx_not_found] raw_txs = [bytes.fromhex(results[0]), None] with ClientSessionGood(('getrawtransaction', args_list, results)): assert await daemon.getrawtransactions(hex_hashes) == raw_txs
async def transaction_broadcast(self, raw_tx): '''Broadcast a raw transaction to the network. raw_tx: the raw transaction as a hexadecimal string''' # This returns errors as JSON RPC errors, as is natural try: tx_hash = await self.daemon.sendrawtransaction([raw_tx]) self.txs_sent += 1 self.logger.info('sent tx: {}'.format(tx_hash)) self.controller.sent_tx(tx_hash) return tx_hash except DaemonError as e: error, = e.args message = error['message'] self.logger.info('sendrawtransaction: {}'.format(message)) raise RPCError(BAD_REQUEST, 'the transaction was rejected by ' f'network rules.\n\n{message}\n[{raw_tx}]')