async def transaction_get(self, writer, query): # pylint: disable=W0613 """Method: blockchain.transaction.get Return a raw transaction. """ if "params" not in query or len(query["params"]) < 1: return JsonRPCError.invalidparams() tx_hash = query["params"][0] verbose = query["params"][1] if len(query["params"]) > 1 else False if not is_hex_str(tx_hash): return JsonRPCError.invalidparams() # _ec, rawtx = await self.bx.fetch_blockchain_transaction(tx_hash) _ec, rawtx = await self.bx.fetch_mempool_transaction(tx_hash) if _ec and _ec != ZMQError.success and _ec != ZMQError.not_found: self.log.error("fetch_mempool_transaction: %s", _ec.name) return JsonRPCError.internalerror() # Behaviour is undefined in spec if not rawtx: return JsonRPCError.internalerror() # return {"result": None} if verbose: # TODO: Help needed return JsonRPCError.invalidrequest() return {"result": bh2u(rawtx)}
async def blockchain_block_headers(self, writer, query): # pylint: disable=W0613,R0911 """Method: blockchain.block.headers Return a concatenated chunk of block headers from the main chain. """ if "params" not in query or len(query["params"]) < 2: return JsonRPCError.invalidparams() # Electrum doesn't allow max_chunk_size to be less than 2016 # gopher://bitreich.org/9/memecache/convenience-store.mkv max_chunk_size = 2016 start_height = query["params"][0] count = query["params"][1] cp_height = query["params"][2] if len(query["params"]) == 3 else 0 if not is_non_negative_integer(start_height): return JsonRPCError.invalidparams() if not is_non_negative_integer(count): return JsonRPCError.invalidparams() if cp_height != 0 and not start_height + (count - 1) <= cp_height: return JsonRPCError.invalidparams() count = min(count, max_chunk_size) headers = bytearray() for i in range(count): _ec, data = await self.bx.fetch_block_header(start_height + i) if _ec and _ec != 0: self.log.debug("Got error: %s", repr(_ec)) return JsonRPCError.internalerror() headers.extend(data) resp = { "hex": safe_hexlify(headers), "count": len(headers) // 80, "max": max_chunk_size, } # The assumption is to fetch more headers if necessary. # TODO: Review if cp_height > 0 and cp_height - start_height > count: for i in range(cp_height - start_height): _ec, data = await self.bx.fetch_block_header(start_height + count + i) if _ec and _ec != 0: self.log.debug("Got error: %s", repr(_ec)) return JsonRPCError.internalerror() headers.extend(data) if cp_height > 0: # TODO: Review # TODO: Is index is 0 or last elem? hdr_lst = [headers[i:i + 80] for i in range(0, len(headers), 80)] branch, root = merkle_branch_and_root(hdr_lst, 0) resp["branch"] = [safe_hexlify(i) for i in branch] resp["root"] = safe_hexlify(root) return {"result": resp}
async def block_header(self, writer, query): # pylint: disable=W0613,R0911 """Method: blockchain.block.header Return the block header at the given height. """ if "params" not in query or len(query["params"]) < 1: return JsonRPCError.invalidparams() index = query["params"][0] cp_height = query["params"][1] if len(query["params"]) == 2 else 0 if not is_non_negative_integer(index): return JsonRPCError.invalidparams() if not is_non_negative_integer(cp_height): return JsonRPCError.invalidparams() if cp_height != 0 and not index <= cp_height: return JsonRPCError.invalidparams() if cp_height == 0: _ec, header = await self.bx.fetch_block_header(index) if _ec and _ec != ZMQError.success: self.log.error("bx.fetch_block_header: %s", _ec.name) return JsonRPCError.internalerror() return {"result": safe_hexlify(header)} res = await self._merkle_proof_for_headers(cp_height, index) return {"result": res}
async def scripthash_subscribe(self, writer, query): # pylint: disable=W0613 """Method: blockchain.scripthash.subscribe Subscribe to a script hash. """ if "params" not in query or len(query["params"]) != 1: return JsonRPCError.invalidparams() scripthash = query["params"][0] if not is_hash256_str(scripthash): return JsonRPCError.invalidparams() _ec, history = await self.bx.fetch_history4(scripthash) if _ec and _ec != ZMQError.success: self.log.error("bx.fetch_history4: %s", _ec.name) return JsonRPCError.internalerror() # TODO: Check how history4 acts for mempool/unconfirmed status = ElectrumProtocol.__scripthash_status_from_history(history) self.peers[self._get_peer(writer)]["sh"][scripthash] = { "status": status } task = asyncio.create_task(self.scripthash_notifier( writer, scripthash)) self.peers[self._get_peer(writer)]["sh"][scripthash]["task"] = task if len(history) < 1: return {"result": None} return {"result": ElectrumProtocol.__scripthash_status_encode(status)}
async def scripthash_listunspent(self, writer, query): # pylint: disable=W0613 """Method: blockchain.scripthash.listunspent Return an ordered list of UTXOs sent to a script hash. """ if "params" not in query or len(query["params"]) != 1: return JsonRPCError.invalidparams() scripthash = query["params"][0] if not is_hash256_str(scripthash): return JsonRPCError.invalidparams() _ec, utxo = await self.bx.fetch_utxo(scripthash) if _ec and _ec != ZMQError.success: self.log.error("bx.fetch_utxo: %s", _ec.name) return JsonRPCError.internalerror() ret = [] for i in utxo: rec = i["received"] ret.append({ "tx_pos": rec["index"], "value": i["value"], "tx_hash": hash_to_hex_str(rec["hash"]), "height": rec["height"] if rec["height"] != 4294967295 else 0, }) return {"result": ret}
async def blockchain_scripthash_listunspent(self, writer, query): # pylint: disable=W0613 """Method: blockchain.scripthash.listunspent Return an ordered list of UTXOs sent to a script hash. """ if "params" not in query or len(query["params"]) != 1: return JsonRPCError.invalidparams() scripthash = query["params"][0] if not is_hash256_str(scripthash): return JsonRPCError.invalidparams() _ec, utxo = await self.bx.fetch_utxo(scripthash) if _ec and _ec != 0: self.log.debug("Got error: %s", repr(_ec)) return JsonRPCError.internalerror() # TODO: Check mempool ret = [] for i in utxo: rec = i["received"] ret.append({ "tx_pos": rec["index"], "value": i["value"], "tx_hash": hash_to_hex_str(rec["hash"]), "height": rec["height"], }) if len(ret) >= 2: ret.reverse() return {"result": ret}
async def blockchain_transaction_get_merkle(self, writer, query): # pylint: disable=W0613 """Method: blockchain.transaction.get_merkle Return the merkle branch to a confirmed transaction given its hash and height. """ if "params" not in query or len(query["params"]) != 2: return JsonRPCError.invalidparams() tx_hash = query["params"][0] height = query["params"][1] if not is_hash256_str(tx_hash): return JsonRPCError.invalidparams() if not is_non_negative_integer(height): return JsonRPCError.invalidparams() _ec, hashes = await self.bx.fetch_block_transaction_hashes(height) if _ec and _ec != 0: self.log.debug("Got error: %s", repr(_ec)) return JsonRPCError.internalerror() # Decouple from tuples hashes = [i[0] for i in hashes] tx_pos = hashes.index(unhexlify(tx_hash)[::-1]) branch = merkle_branch(hashes, tx_pos) res = { "block_height": int(height), "pos": int(tx_pos), "merkle": branch, } return {"result": res}
async def blockchain_scripthash_get_history(self, writer, query): # pylint: disable=W0613 """Method: blockchain.scripthash.get_history Return the confirmed and unconfirmed history of a script hash. """ if "params" not in query or len(query["params"]) != 1: return JsonRPCError.invalidparams() if not is_hash256_str(query["params"][0]): return JsonRPCError.invalidparams() _ec, data = await self.bx.fetch_history4(query["params"][0]) if _ec and _ec != 0: self.log.debug("Got error: %s", repr(_ec)) return JsonRPCError.internalerror() self.log.debug("hist: %s", data) ret = [] # TODO: mempool for i in data: if "received" in i: ret.append({ "height": i["received"]["height"], "tx_hash": hash_to_hex_str(i["received"]["hash"]), }) if "spent" in i: ret.append({ "height": i["spent"]["height"], "tx_hash": hash_to_hex_str(i["spent"]["hash"]), }) return {"result": ret}
async def blockchain_headers_subscribe(self, writer, query): # pylint: disable=W0613 """Method: blockchain.headers.subscribe Subscribe to receive block headers when a new block is found. """ # Tip height and header are returned upon request _ec, height = await self.bx.fetch_last_height() if _ec and _ec != 0: self.log.debug("Got error: %s", repr(_ec)) return JsonRPCError.internalerror() _ec, tip_header = await self.bx.fetch_block_header(height) if _ec and _ec != 0: self.log.debug("Got error: %s", repr(_ec)) return JsonRPCError.internalerror() self.tasks.append(asyncio.create_task(self.header_notifier(writer))) ret = {"height": height, "hex": safe_hexlify(tip_header)} return {"result": ret}
async def headers_subscribe(self, writer, query): # pylint: disable=W0613 """Method: blockchain.headers.subscribe Subscribe to receive block headers when a new block is found. """ # Tip height and header are returned upon request _ec, height = await self.bx.fetch_last_height() if _ec and _ec != ZMQError.success: self.log.error("bx.fetch_last_height: %s", _ec.name) return JsonRPCError.internalerror() _ec, tip_header = await self.bx.fetch_block_header(height) if _ec and _ec != ZMQError.success: self.log.error("bx.fetch_block_header: %s", _ec.name) return JsonRPCError.internalerror() self.peers[self._get_peer(writer)]["tasks"].append( asyncio.create_task(self.header_notifier(writer))) ret = {"height": height, "hex": safe_hexlify(tip_header)} return {"result": ret}
async def blockchain_block_header(self, writer, query): # pylint: disable=W0613,R0911 """Method: blockchain.block.header Return the block header at the given height. """ if "params" not in query or len(query["params"]) < 1: return JsonRPCError.invalidparams() index = query["params"][0] cp_height = query["params"][1] if len(query["params"]) == 2 else 0 if not is_non_negative_integer(index): return JsonRPCError.invalidparams() if not is_non_negative_integer(cp_height): return JsonRPCError.invalidparams() if cp_height != 0 and not index <= cp_height: return JsonRPCError.invalidparams() if cp_height == 0: _ec, header = await self.bx.fetch_block_header(index) if _ec and _ec != 0: self.log.debug("Got error: %s", repr(_ec)) return JsonRPCError.internalerror() return {"result": safe_hexlify(header)} cp_headers = [] # headers up to and including cp_height for i in range(index, cp_height + 1): _ec, data = await self.bx.fetch_block_header(i) if _ec and _ec != 0: self.log.debug("Got error: %s", repr(_ec)) return JsonRPCError.internalerror() cp_headers.append(data) # TODO: Review # TODO: Is index is 0 or last elem? branch, root = merkle_branch_and_root(cp_headers, 0) return { "result": { "branch": [safe_hexlify(i) for i in branch], "header": safe_hexlify(cp_headers[0]), "root": safe_hexlify(root), } }
async def blockchain_transaction_id_from_pos(self, writer, query): # pylint: disable=R0911,W0613 """Method: blockchain.transaction.id_from_pos Return a transaction hash and optionally a merkle proof, given a block height and a position in the block. """ if "params" not in query or len(query["params"]) < 2: return JsonRPCError.invalidparams() height = query["params"][0] tx_pos = query["params"][1] merkle = query["params"][2] if len(query["params"]) > 2 else False if not is_non_negative_integer(height): return JsonRPCError.invalidparams() if not is_non_negative_integer(tx_pos): return JsonRPCError.invalidparams() if not is_boolean(merkle): return JsonRPCError.invalidparams() _ec, hashes = await self.bx.fetch_block_transaction_hashes(height) if _ec and _ec != 0: self.log.debug("Got error: %s", repr(_ec)) return JsonRPCError.internalerror() if len(hashes) - 1 < tx_pos: return JsonRPCError.internalerror() # Decouple from tuples hashes = [i[0] for i in hashes] txid = hash_to_hex_str(hashes[tx_pos]) if not merkle: return {"result": txid} branch = merkle_branch(hashes, tx_pos) return {"result": {"tx_hash": txid, "merkle": branch}}
async def scripthash_get_balance(self, writer, query): # pylint: disable=W0613 """Method: blockchain.scripthash.get_balance Return the confirmed and unconfirmed balances of a script hash. """ if "params" not in query or len(query["params"]) != 1: return JsonRPCError.invalidparams() if not is_hash256_str(query["params"][0]): return JsonRPCError.invalidparams() _ec, data = await self.bx.fetch_balance(query["params"][0]) if _ec and _ec != ZMQError.success: self.log.error("bx.fetch_balance: %s", _ec.name) return JsonRPCError.internalerror() ret = {"confirmed": data[0], "unconfirmed": data[1]} return {"result": ret}
async def blockchain_scripthash_get_balance(self, writer, query): # pylint: disable=W0613 """Method: blockchain.scripthash.get_balance Return the confirmed and unconfirmed balances of a script hash. """ if "params" not in query or len(query["params"]) != 1: return JsonRPCError.invalidparams() if not is_hash256_str(query["params"][0]): return JsonRPCError.invalidparams() _ec, data = await self.bx.fetch_balance(query["params"][0]) if _ec and _ec != 0: self.log.debug("Got error: %s", repr(_ec)) return JsonRPCError.internalerror() # TODO: confirmed/unconfirmed, see what's happening in libbitcoin ret = {"confirmed": data, "unconfirmed": 0} return {"result": ret}
async def block_headers(self, writer, query): # pylint: disable=W0613,R0911 """Method: blockchain.block.headers Return a concatenated chunk of block headers from the main chain. """ if "params" not in query or len(query["params"]) < 2: return JsonRPCError.invalidparams() # Electrum doesn't allow max_chunk_size to be less than 2016 # gopher://bitreich.org/9/memecache/convenience-store.mkv max_chunk_size = 2016 start_height = query["params"][0] count = query["params"][1] cp_height = query["params"][2] if len(query["params"]) == 3 else 0 if not is_non_negative_integer(start_height): return JsonRPCError.invalidparams() if not is_non_negative_integer(count): return JsonRPCError.invalidparams() # BUG: spec says <= cp_height if cp_height != 0 and not start_height + (count - 1) < cp_height: return JsonRPCError.invalidparams() count = min(count, max_chunk_size) headers = bytearray() for i in range(count): _ec, data = await self.bx.fetch_block_header(start_height + i) if _ec and _ec != ZMQError.success: self.log.error("bx.fetch_block_header: %s", _ec.name) return JsonRPCError.internalerror() headers.extend(data) resp = { "hex": safe_hexlify(headers), "count": len(headers) // 80, "max": max_chunk_size, } if cp_height > 0: data = await self._merkle_proof_for_headers( cp_height, start_height + (len(headers) // 80) - 1) resp["branch"] = data["branch"] resp["root"] = data["root"] return {"result": resp}
async def blockchain_transaction_broadcast(self, writer, query): # pylint: disable=W0613 """Method: blockchain.transaction.broadcast Broadcast a transaction to the network. """ # Note: Not yet implemented in bs v4 if "params" not in query or len(query["params"]) != 1: return JsonRPCError.invalidparams() hextx = query["params"][0] if not is_hex_str(hextx): return JsonRPCError.invalidparams() _ec, _ = await self.bx.broadcast_transaction(unhexlify(hextx)[::-1]) if _ec and _ec != 0: self.log.debug("Got error: %s", repr(_ec)) return JsonRPCError.internalerror() rawtx = unhexlify(hextx) txid = double_sha256(rawtx) return {"result": hash_to_hex_str(txid)}
async def _merkle_proof_for_headers(self, height, idx): """Extremely inefficient merkle proof for headers""" # The following works, but is extremely inefficient. # The best solution would be to figure something out in # libbitcoin-server cp_headers = [] for i in range(0, height + 1): _ec, data = await self.bx.fetch_block_header(i) if _ec and _ec != ZMQError.success: self.log.error("bx.fetch_block_header: %s", _ec.name) return JsonRPCError.internalerror() cp_headers.append(data) branch, root = merkle_branch_and_root( [double_sha256(i) for i in cp_headers], idx) return { "branch": [hash_to_hex_str(i) for i in branch], "header": safe_hexlify(cp_headers[idx]), "root": hash_to_hex_str(root), }
async def blockchain_scripthash_subscribe(self, writer, query): # pylint: disable=W0613 """Method: blockchain.scripthash.subscribe Subscribe to a script hash. """ if "params" not in query or len(query["params"]) != 1: return JsonRPCError.invalidparams() scripthash = query["params"][0] if not is_hash256_str(scripthash): return JsonRPCError.invalidparams() _ec, history = await self.bx.fetch_history4(scripthash) if _ec and _ec != 0: return JsonRPCError.internalerror() task = asyncio.create_task(self.scripthash_notifier(writer, scripthash)) self.sh_subscriptions[scripthash] = {"task": task} if len(history) < 1: return {"result": None} # TODO: Check how history4 acts for mempool/unconfirmed status = [] for i in history: if "received" in i: status.append(( hash_to_hex_str(i["received"]["hash"]), i["received"]["height"], )) if "spent" in i: status.append(( hash_to_hex_str(i["spent"]["hash"]), i["spent"]["height"], )) self.sh_subscriptions[scripthash]["status"] = status return {"result": ElectrumProtocol.__scripthash_status(status)}