URI: 
       tImplement blockchain.transaction.id_from_pos - obelisk - Electrum server using libbitcoin as its backend
  HTML git clone https://git.parazyd.org/obelisk
   DIR Log
   DIR Files
   DIR Refs
   DIR README
   DIR LICENSE
       ---
   DIR commit 9b6accd058e54b69c16596c303fa67682134f34c
   DIR parent f1a7d430089c56ba5f6446a2eae245ce50c86967
  HTML Author: parazyd <parazyd@dyne.org>
       Date:   Wed,  7 Apr 2021 19:14:06 +0200
       
       Implement blockchain.transaction.id_from_pos
       
       Diffstat:
         M electrumobelisk/protocol.py         |      40 ++++++++++++++++++++++++++++---
         M electrumobelisk/util.py             |      28 ++++++++++++++++++++++++++++
         M electrumobelisk/zeromq.py           |      22 ++++++++++++++++++++++
       
       3 files changed, 87 insertions(+), 3 deletions(-)
       ---
   DIR diff --git a/electrumobelisk/protocol.py b/electrumobelisk/protocol.py
       t@@ -17,7 +17,13 @@
        import asyncio
        import json
        
       -from electrumobelisk.util import is_non_negative_integer, safe_hexlify
       +from electrumobelisk.merkle import merkle_branch
       +from electrumobelisk.util import (
       +    is_boolean,
       +    is_hash256_str,
       +    is_non_negative_integer,
       +    safe_hexlify,
       +)
        from electrumobelisk.zeromq import Client
        
        VERSION = 0.0
       t@@ -235,8 +241,36 @@ class ElectrumProtocol(asyncio.Protocol):  # pylint: disable=R0904,R0902
            async def blockchain_transaction_get_merkle(self, query):
                return
        
       -    async def blockchain_transaction_from_pos(self, query):
       -        return
       +    async def blockchain_transaction_from_pos(self, query):  # pylint: disable=R0911
       +        if "params" not in query or len(query["params"]) < 2:
       +            return {"error": "malformed request"}
       +        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 {"error": "height is not a non-negative integer"}
       +        if not is_non_negative_integer(tx_pos):
       +            return {"error": "tx_pos is not a non-negative integer"}
       +        if not is_boolean(merkle):
       +            return {"error": "merkle is not a boolean value"}
       +
       +        _ec, hashes = await self.bx.fetch_block_transaction_hashes(height)
       +        if _ec and _ec != 0:
       +            self.log.debug("Got error: %s", repr(_ec))
       +            return {"error": "request corrupted"}
       +
       +        if len(hashes) - 1 < tx_pos:
       +            return {"error": "index not in block"}
       +
       +        # Decouple from tuples
       +        hashes = [i[0] for i in hashes] 
       +        txid = safe_hexlify(hashes[tx_pos][::-1])
       +
       +        if not merkle:
       +            return {"result": txid}
       +        branch = merkle_branch(hashes, tx_pos)
       +        return {"result": {"tx_hash": txid, "merkle": branch}}
        
            async def mempool_get_fee_histogram(self, query):  # pylint: disable=W0613
                # Help wanted
   DIR diff --git a/electrumobelisk/util.py b/electrumobelisk/util.py
       t@@ -30,6 +30,34 @@ def is_non_negative_integer(val):
            return False
        
        
       +def is_boolean(val):
       +    """Check if val is of type bool"""
       +    return isinstance(val, bool)
       +
       +
       +def is_hex_str(text):
       +    """Check if text is a hex string"""
       +    if not isinstance(text, str):
       +        return False
       +    try:
       +        b = bytes.fromhex(text)
       +    except:
       +        return False
       +    # Forbid whitespaces in text:
       +    if len(text) != 2 * len(b):
       +        return False
       +    return True
       +
       +
       +def is_hash256_str(text):
       +    """Check if text is a sha256 hash"""
       +    if not isinstance(text, str):
       +        return False
       +    if len(text) != 64:
       +        return False
       +    return is_hex_str(text)
       +
       +
        def safe_hexlify(val):
            """hexlify and return a string"""
            return str(hexlify(val), "utf-8")
   DIR diff --git a/electrumobelisk/zeromq.py b/electrumobelisk/zeromq.py
       t@@ -45,6 +45,19 @@ def pack_block_index(index):
            )
        
        
       +def unpack_table(row_fmt, data):
       +    # Get the number of rows
       +    row_size = struct.calcsize(row_fmt)
       +    nrows = len(data) // row_size
       +    # Unpack
       +    rows = []
       +    for idx in range(nrows):
       +        offset = idx * row_size
       +        row = struct.unpack_from(row_fmt, data, offset)
       +        rows.append(row)
       +    return rows
       +
       +
        class ClientSettings:
            """Class implementing ZMQ client settings"""
            def __init__(self, timeout=10, context=None, loop=None):
       t@@ -254,3 +267,12 @@ class Client:
                command = b"blockchain.fetch_block_header"
                data = pack_block_index(index)
                return await self._simple_request(command, data)
       +
       +    async def fetch_block_transaction_hashes(self, index):
       +        """Fetch transaction hashes in a block at height index"""
       +        command = b"blockchain.fetch_block_transaction_hashes"
       +        data = pack_block_index(index)
       +        error_code, data = await self._simple_request(command, data)
       +        if error_code:
       +            return error_code, None
       +        return error_code, unpack_table("32s", data)