Edit on GitHub

communex.client

   1import json
   2import queue
   3from concurrent.futures import Future, ThreadPoolExecutor
   4from contextlib import contextmanager
   5from copy import deepcopy
   6from dataclasses import dataclass
   7from typing import Any, Mapping, TypeVar, cast
   8
   9from substrateinterface import ExtrinsicReceipt  # type: ignore
  10from substrateinterface import Keypair  # type: ignore
  11from substrateinterface import SubstrateInterface  # type: ignore
  12from substrateinterface.storage import StorageKey  # type: ignore
  13
  14from communex.errors import ChainTransactionError, NetworkQueryError
  15from communex.types import NetworkParams, Ss58Address, SubnetParams
  16
  17# TODO: InsufficientBalanceError, MismatchedLengthError etc
  18
  19MAX_REQUEST_SIZE = 9_000_000
  20
  21
  22@dataclass
  23class Chunk:
  24    batch_requests: list[tuple[Any, Any]]
  25    prefix_list: list[list[str]]
  26    fun_params: list[tuple[Any, Any, Any, Any, str]]
  27
  28
  29T1 = TypeVar("T1")
  30T2 = TypeVar("T2")
  31
  32
  33class CommuneClient:
  34    """
  35    A client for interacting with Commune network nodes, querying storage,
  36    submitting transactions, etc.
  37
  38    Attributes:
  39        wait_for_finalization: Whether to wait for transaction finalization.
  40
  41    Example:
  42    ```py
  43    client = CommuneClient()
  44    client.query(name='function_name', params=['param1', 'param2'])
  45    ```
  46
  47    Raises:
  48        AssertionError: If the maximum connections value is less than or equal
  49          to zero.
  50    """
  51
  52    wait_for_finalization: bool
  53    _num_connections: int
  54    _connection_queue: queue.Queue[SubstrateInterface]
  55    url: str
  56
  57    def __init__(
  58        self,
  59        url: str,
  60        num_connections: int = 1,
  61        wait_for_finalization: bool = False,
  62    ):
  63        """
  64        Args:
  65            url: The URL of the network node to connect to.
  66            num_connections: The number of websocket connections to be opened.
  67        """
  68        assert num_connections > 0
  69        self._num_connections = num_connections
  70        self.wait_for_finalization = wait_for_finalization
  71        self._connection_queue = queue.Queue(num_connections)
  72        self.url = url
  73
  74        for _ in range(num_connections):
  75            self._connection_queue.put(SubstrateInterface(url))
  76
  77    @property
  78    def connections(self) -> int:
  79        """
  80        Gets the maximum allowed number of simultaneous connections to the
  81        network node.
  82        """
  83        return self._num_connections
  84
  85    @contextmanager
  86    def get_conn(self, timeout: float | None = None, init: bool = False):
  87        """
  88        Context manager to get a connection from the pool.
  89
  90        Tries to get a connection from the pool queue. If the queue is empty,
  91        it blocks for `timeout` seconds until a connection is available. If
  92        `timeout` is None, it blocks indefinitely.
  93
  94        Args:
  95            timeout: The maximum time in seconds to wait for a connection.
  96
  97        Yields:
  98            The connection object from the pool.
  99
 100        Raises:
 101            QueueEmptyError: If no connection is available within the timeout
 102              period.
 103        """
 104        conn = self._connection_queue.get(timeout=timeout)
 105        if init:
 106            conn.init_runtime()  # type: ignore
 107        try:
 108            yield conn
 109        finally:
 110            self._connection_queue.put(conn)
 111
 112    def _get_storage_keys(
 113        self,
 114        storage: str,
 115        queries: list[tuple[str, list[Any]]],
 116        block_hash: str | None,
 117    ):
 118
 119        send: list[tuple[str, list[Any]]] = []
 120        prefix_list: list[Any] = []
 121
 122        key_idx = 0
 123        with self.get_conn(init=True) as substrate:
 124            for function, params in queries:
 125                storage_key = StorageKey.create_from_storage_function(  # type: ignore
 126                    storage, function, params, runtime_config=substrate.runtime_config, metadata=substrate.metadata  # type: ignore
 127                )
 128
 129                prefix = storage_key.to_hex()
 130                prefix_list.append(prefix)
 131                send.append(("state_getKeys", [prefix, block_hash]))
 132                key_idx += 1
 133        return send, prefix_list
 134
 135    def _get_lists(
 136        self,
 137        storage_module: str,
 138        queries: list[tuple[str, list[Any]]],
 139        substrate: SubstrateInterface,
 140    ) -> list[tuple[Any, Any, Any, Any, str]]:
 141        """
 142        Generates a list of tuples containing parameters for each storage function based on the given functions and substrate interface.
 143
 144        Args:
 145            functions (dict[str, list[query_call]]): A dictionary where keys are storage module names and values are lists of tuples.
 146                Each tuple consists of a storage function name and its parameters.
 147            substrate: An instance of the SubstrateInterface class used to interact with the substrate.
 148
 149        Returns:
 150            A list of tuples in the format `(value_type, param_types, key_hashers, params, storage_function)` for each storage function in the given functions.
 151
 152        Example:
 153            >>> _get_lists(
 154                    functions={'storage_module': [('storage_function', ['param1', 'param2'])]},
 155                    substrate=substrate_instance
 156                )
 157            [('value_type', 'param_types', 'key_hashers', ['param1', 'param2'], 'storage_function'), ...]
 158        """
 159
 160        function_parameters: list[tuple[Any, Any, Any, Any, str]] = []
 161
 162        metadata_pallet = substrate.metadata.get_metadata_pallet(  # type: ignore
 163            storage_module
 164        )
 165        for storage_function, params in queries:
 166            storage_item = metadata_pallet.get_storage_function(  # type: ignore
 167                storage_function
 168            )
 169
 170            value_type = storage_item.get_value_type_string()  # type: ignore
 171            param_types = storage_item.get_params_type_string()  # type: ignore
 172            key_hashers = storage_item.get_param_hashers()  # type: ignore
 173            function_parameters.append(
 174                (
 175                    value_type,
 176                    param_types,
 177                    key_hashers,
 178                    params,
 179                    storage_function,
 180                )  # type: ignore
 181            )
 182        return function_parameters
 183
 184    def _send_batch(
 185        self,
 186        batch_payload: list[Any],
 187        request_ids: list[int],
 188        extract_result: bool = True,
 189    ):
 190        """
 191        Sends a batch of requests to the substrate and collects the results.
 192
 193        Args:
 194            substrate: An instance of the substrate interface.
 195            batch_payload: The payload of the batch request.
 196            request_ids: A list of request IDs for tracking responses.
 197            results: A list to store the results of the requests.
 198            extract_result: Whether to extract the result from the response.
 199
 200        Raises:
 201            NetworkQueryError: If there is an `error` in the response message.
 202
 203        Note:
 204            No explicit return value as results are appended to the provided 'results' list.
 205        """
 206        results: list[str | dict[Any, Any]] = []
 207        with self.get_conn(init=True) as substrate:
 208            try:
 209
 210                substrate.websocket.send(  #  type: ignore
 211                    json.dumps(batch_payload)
 212                )  # type: ignore
 213            except NetworkQueryError:
 214                pass
 215            while len(results) < len(request_ids):
 216                received_messages = json.loads(
 217                    substrate.websocket.recv()  # type: ignore
 218                )  # type: ignore
 219                if isinstance(received_messages, dict):
 220                    received_messages: list[dict[Any, Any]] = [received_messages]
 221
 222                for message in received_messages:
 223                    if message.get("id") in request_ids:
 224                        if extract_result:
 225                            try:
 226                                results.append(message["result"])
 227                            except Exception:
 228                                raise (
 229                                    RuntimeError(
 230                                        f"Error extracting result from message: {message}"
 231                                    )
 232                                )
 233                        else:
 234                            results.append(message)
 235                    if "error" in message:
 236                        raise NetworkQueryError(message["error"])
 237
 238            return results
 239
 240    def _make_request_smaller(
 241        self,
 242        batch_request: list[tuple[T1, T2]],
 243        prefix_list: list[list[str]],
 244        fun_params: list[tuple[Any, Any, Any, Any, str]],
 245    ) -> tuple[list[list[tuple[T1, T2]]], list[Chunk]]:
 246        """
 247        Splits a batch of requests into smaller batches, each not exceeding the specified maximum size.
 248
 249        Args:
 250            batch_request: A list of requests to be sent in a batch.
 251            max_size: Maximum size of each batch in bytes.
 252
 253        Returns:
 254            A list of smaller request batches.
 255
 256        Example:
 257            >>> _make_request_smaller(batch_request=[('method1', 'params1'), ('method2', 'params2')], max_size=1000)
 258            [[('method1', 'params1')], [('method2', 'params2')]]
 259        """
 260        assert len(prefix_list) == len(fun_params) == len(batch_request)
 261
 262        def estimate_size(request: tuple[T1, T2]):
 263            """Convert the batch request to a string and measure its length"""
 264            return len(json.dumps(request))
 265
 266        # Initialize variables
 267        result: list[list[tuple[T1, T2]]] = []
 268        current_batch = []
 269        current_prefix_batch = []
 270        current_params_batch = []
 271        current_size = 0
 272
 273        chunk_list: list[Chunk] = []
 274
 275        # Iterate through each request in the batch
 276        for request, prefix, params in zip(batch_request, prefix_list, fun_params):
 277            request_size = estimate_size(request)
 278
 279            # Check if adding this request exceeds the max size
 280            if current_size + request_size > MAX_REQUEST_SIZE:
 281                # If so, start a new batch
 282
 283                # Essentiatly checks that it's not the first iteration
 284                if current_batch:
 285                    chunk = Chunk(
 286                        current_batch, current_prefix_batch, current_params_batch
 287                    )
 288                    chunk_list.append(chunk)
 289                    result.append(current_batch)
 290
 291                current_batch = [request]
 292                current_prefix_batch = [prefix]
 293                current_params_batch = [params]
 294                current_size = request_size
 295            else:
 296                # Otherwise, add to the current batch
 297                current_batch.append(request)
 298                current_size += request_size
 299                current_prefix_batch.append(prefix)
 300                current_params_batch.append(params)
 301
 302        # Add the last batch if it's not empty
 303        if current_batch:
 304            result.append(current_batch)
 305            chunk = Chunk(current_batch, current_prefix_batch, current_params_batch)
 306            chunk_list.append(chunk)
 307
 308        return result, chunk_list
 309
 310    def _are_changes_equal(self, change_a: Any, change_b: Any):
 311        for (a, b), (c, d) in zip(change_a, change_b):
 312            if a != c or b != d:
 313                return False
 314
 315    def _rpc_request_batch(
 316        self, batch_requests: list[tuple[str, list[Any]]], extract_result: bool = True
 317    ) -> list[str]:
 318        """
 319        Sends batch requests to the substrate node using multiple threads and collects the results.
 320
 321        Args:
 322            substrate: An instance of the substrate interface.
 323            batch_requests : A list of requests to be sent in batches.
 324            max_size: Maximum size of each batch in bytes.
 325            extract_result: Whether to extract the result from the response message.
 326
 327        Returns:
 328            A list of results from the batch requests.
 329
 330        Example:
 331            >>> _rpc_request_batch(substrate_instance, [('method1', ['param1']), ('method2', ['param2'])])
 332            ['result1', 'result2', ...]
 333        """
 334
 335        chunk_results: list[Any] = []
 336        # smaller_requests = self._make_request_smaller(batch_requests)
 337        request_id = 0
 338        with ThreadPoolExecutor() as executor:
 339            futures: list[Future[list[str | dict[Any, Any]]]] = []
 340            for chunk in [batch_requests]:
 341                request_ids: list[int] = []
 342                batch_payload: list[Any] = []
 343                for method, params in chunk:
 344                    request_id += 1
 345                    request_ids.append(request_id)
 346                    batch_payload.append(
 347                        {
 348                            "jsonrpc": "2.0",
 349                            "method": method,
 350                            "params": params,
 351                            "id": request_id,
 352                        }
 353                    )
 354
 355                futures.append(
 356                    executor.submit(
 357                        self._send_batch,
 358                        batch_payload=batch_payload,
 359                        request_ids=request_ids,
 360                        extract_result=extract_result,
 361                    )
 362                )
 363            for future in futures:
 364                resul = future.result()
 365                chunk_results.append(resul)
 366        return chunk_results
 367
 368    def _rpc_request_batch_chunked(
 369        self, chunk_requests: list[Chunk], extract_result: bool = True
 370    ):
 371        """
 372        Sends batch requests to the substrate node using multiple threads and collects the results.
 373
 374        Args:
 375            substrate: An instance of the substrate interface.
 376            batch_requests : A list of requests to be sent in batches.
 377            max_size: Maximum size of each batch in bytes.
 378            extract_result: Whether to extract the result from the response message.
 379
 380        Returns:
 381            A list of results from the batch requests.
 382
 383        Example:
 384            >>> _rpc_request_batch(substrate_instance, [('method1', ['param1']), ('method2', ['param2'])])
 385            ['result1', 'result2', ...]
 386        """
 387
 388        def split_chunks(chunk: Chunk, chunk_info: list[Chunk], chunk_info_idx: int):
 389            manhattam_chunks: list[tuple[Any, Any]] = []
 390            mutaded_chunk_info = deepcopy(chunk_info)
 391            max_n_keys = 35000
 392            for query in chunk.batch_requests:
 393                result_keys = query[1][0]
 394                keys_amount = len(result_keys)
 395                if keys_amount > max_n_keys:
 396                    mutaded_chunk_info.pop(chunk_info_idx)
 397                    for i in range(0, keys_amount, max_n_keys):
 398                        new_chunk = deepcopy(chunk)
 399                        splitted_keys = result_keys[i : i + max_n_keys]
 400                        splitted_query = deepcopy(query)
 401                        splitted_query[1][0] = splitted_keys
 402                        new_chunk.batch_requests = [splitted_query]
 403                        manhattam_chunks.append(splitted_query)
 404                        mutaded_chunk_info.insert(chunk_info_idx, new_chunk)
 405                else:
 406                    manhattam_chunks.append(query)
 407            return manhattam_chunks, mutaded_chunk_info
 408
 409        assert len(chunk_requests) > 0
 410        mutated_chunk_info: list[Chunk] = []
 411        chunk_results: list[Any] = []
 412        # smaller_requests = self._make_request_smaller(batch_requests)
 413        request_id = 0
 414
 415        with ThreadPoolExecutor() as executor:
 416            futures: list[Future[list[str | dict[Any, Any]]]] = []
 417            for idx, macro_chunk in enumerate(chunk_requests):
 418                _, mutated_chunk_info = split_chunks(macro_chunk, chunk_requests, idx)
 419            for chunk in mutated_chunk_info:
 420                request_ids: list[int] = []
 421                batch_payload: list[Any] = []
 422                for method, params in chunk.batch_requests:
 423                    # for method, params in micro_chunk:
 424                    request_id += 1
 425                    request_ids.append(request_id)
 426                    batch_payload.append(
 427                        {
 428                            "jsonrpc": "2.0",
 429                            "method": method,
 430                            "params": params,
 431                            "id": request_id,
 432                        }
 433                    )
 434                futures.append(
 435                    executor.submit(
 436                        self._send_batch,
 437                        batch_payload=batch_payload,
 438                        request_ids=request_ids,
 439                        extract_result=extract_result,
 440                    )
 441                )
 442            for future in futures:
 443                resul = future.result()
 444                chunk_results.append(resul)
 445        return chunk_results, mutated_chunk_info
 446
 447    def _decode_response(
 448        self,
 449        response: list[str],
 450        function_parameters: list[tuple[Any, Any, Any, Any, str]],
 451        prefix_list: list[Any],
 452        block_hash: str,
 453    ) -> dict[str, dict[Any, Any]]:
 454        """
 455        Decodes a response from the substrate interface and organizes the data into a dictionary.
 456
 457        Args:
 458            response: A list of encoded responses from a substrate query.
 459            function_parameters: A list of tuples containing the parameters for each storage function.
 460            last_keys: A list of the last keys used in the substrate query.
 461            prefix_list: A list of prefixes used in the substrate query.
 462            substrate: An instance of the SubstrateInterface class.
 463            block_hash: The hash of the block to be queried.
 464
 465        Returns:
 466            A dictionary where each key is a storage function name and the value is another dictionary.
 467            This inner dictionary's key is the decoded key from the response and the value is the corresponding decoded value.
 468
 469        Raises:
 470            ValueError: If an unsupported hash type is encountered in the `concat_hash_len` function.
 471
 472        Example:
 473            >>> _decode_response(
 474                    response=[...],
 475                    function_parameters=[...],
 476                    last_keys=[...],
 477                    prefix_list=[...],
 478                    substrate=substrate_instance,
 479                    block_hash="0x123..."
 480                )
 481            {'storage_function_name': {decoded_key: decoded_value, ...}, ...}
 482        """
 483
 484        def concat_hash_len(key_hasher: str) -> int:
 485            """
 486            Determines the length of the hash based on the given key hasher type.
 487
 488            Args:
 489                key_hasher: The type of key hasher.
 490
 491            Returns:
 492                The length of the hash corresponding to the given key hasher type.
 493
 494            Raises:
 495                ValueError: If the key hasher type is not supported.
 496
 497            Example:
 498                >>> concat_hash_len("Blake2_128Concat")
 499                16
 500            """
 501
 502            if key_hasher == "Blake2_128Concat":
 503                return 16
 504            elif key_hasher == "Twox64Concat":
 505                return 8
 506            elif key_hasher == "Identity":
 507                return 0
 508            else:
 509                raise ValueError("Unsupported hash type")
 510
 511        assert len(response) == len(function_parameters) == len(prefix_list)
 512        result_dict: dict[str, dict[Any, Any]] = {}
 513        for res, fun_params_tuple, prefix in zip(
 514            response, function_parameters, prefix_list
 515        ):
 516            if not res:
 517                continue
 518            res = res[0]
 519            changes = res["changes"]  # type: ignore
 520            value_type, param_types, key_hashers, params, storage_function = (
 521                fun_params_tuple
 522            )
 523            with self.get_conn(init=True) as substrate:
 524                for item in changes:
 525                    # Determine type string
 526                    key_type_string: list[Any] = []
 527                    for n in range(len(params), len(param_types)):
 528                        key_type_string.append(
 529                            f"[u8; {concat_hash_len(key_hashers[n])}]"
 530                        )
 531                        key_type_string.append(param_types[n])
 532
 533                    item_key_obj = substrate.decode_scale(  # type: ignore
 534                        type_string=f"({', '.join(key_type_string)})",
 535                        scale_bytes="0x" + item[0][len(prefix) :],
 536                        return_scale_obj=True,
 537                        block_hash=block_hash,
 538                    )
 539                    # strip key_hashers to use as item key
 540                    if len(param_types) - len(params) == 1:
 541                        item_key = item_key_obj.value_object[1]  # type: ignore
 542                    else:
 543                        item_key = tuple(  # type: ignore
 544                            item_key_obj.value_object[key + 1]  # type: ignore
 545                            for key in range(  # type: ignore
 546                                len(params), len(param_types) + 1, 2
 547                            )
 548                        )
 549
 550                    item_value = substrate.decode_scale(  # type: ignore
 551                        type_string=value_type,
 552                        scale_bytes=item[1],
 553                        return_scale_obj=True,
 554                        block_hash=block_hash,
 555                    )
 556                    result_dict.setdefault(storage_function, {})
 557
 558                    result_dict[storage_function][item_key.value] = item_value.value  # type: ignore
 559
 560        return result_dict
 561
 562    def query_batch(
 563        self, functions: dict[str, list[tuple[str, list[Any]]]]
 564    ) -> dict[str, str]:
 565        """
 566        Executes batch queries on a substrate and returns results in a dictionary format.
 567
 568        Args:
 569            substrate: An instance of SubstrateInterface to interact with the substrate.
 570            functions (dict[str, list[query_call]]): A dictionary mapping module names to lists of query calls (function name and parameters).
 571
 572        Returns:
 573            A dictionary where keys are storage function names and values are the query results.
 574
 575        Raises:
 576            Exception: If no result is found from the batch queries.
 577
 578        Example:
 579            >>> query_batch(substrate_instance, {'module_name': [('function_name', ['param1', 'param2'])]})
 580            {'function_name': 'query_result', ...}
 581        """
 582
 583        result: dict[str, str] = {}
 584        if not functions:
 585            raise Exception("No result")
 586        with self.get_conn(init=True) as substrate:
 587            for module, queries in functions.items():
 588                storage_keys: list[Any] = []
 589                for fn, params in queries:
 590                    storage_function = substrate.create_storage_key(  # type: ignore
 591                        pallet=module, storage_function=fn, params=params
 592                    )
 593                    storage_keys.append(storage_function)
 594
 595                block_hash = substrate.get_block_hash()
 596                responses: list[Any] = substrate.query_multi(  # type: ignore
 597                    storage_keys=storage_keys, block_hash=block_hash
 598                )
 599
 600                for item in responses:
 601                    fun = item[0]
 602                    query = item[1]
 603                    storage_fun = fun.storage_function
 604                    result[storage_fun] = query.value
 605
 606        return result
 607
 608    def query_batch_map(
 609        self,
 610        functions: dict[str, list[tuple[str, list[Any]]]],
 611        block_hash: str | None = None,
 612    ) -> dict[str, dict[Any, Any]]:
 613        """
 614        Queries multiple storage functions using a map batch approach and returns the combined result.
 615
 616        Args:
 617            substrate: An instance of SubstrateInterface for substrate interaction.
 618            functions (dict[str, list[query_call]]): A dictionary mapping module names to lists of query calls.
 619
 620        Returns:
 621            The combined result of the map batch query.
 622
 623        Example:
 624            >>> query_batch_map(substrate_instance, {'module_name': [('function_name', ['param1', 'param2'])]})
 625            # Returns the combined result of the map batch query
 626        """
 627        multi_result: dict[str, dict[Any, Any]] = {}
 628
 629        def recursive_update(
 630            d: dict[str, dict[T1, T2] | dict[str, Any]],
 631            u: Mapping[str, dict[Any, Any] | str],
 632        ) -> dict[str, dict[T1, T2]]:
 633            for k, v in u.items():
 634                if isinstance(v, dict):
 635                    d[k] = recursive_update(d.get(k, {}), v)  # type: ignore
 636                else:
 637                    d[k] = v  # type: ignore
 638            return d  # type: ignore
 639
 640        def get_page():
 641            send, prefix_list = self._get_storage_keys(storage, queries, block_hash)
 642            with self.get_conn(init=True) as substrate:
 643                function_parameters = self._get_lists(storage, queries, substrate)
 644            responses = self._rpc_request_batch(send)
 645            # assumption because send is just the storage_function keys
 646            # so it should always be really small regardless of the amount of queries
 647            assert len(responses) == 1
 648            res = responses[0]
 649            built_payload: list[tuple[str, list[Any]]] = []
 650            for result_keys in res:
 651                built_payload.append(
 652                    ("state_queryStorageAt", [result_keys, block_hash])
 653                )
 654            _, chunks_info = self._make_request_smaller(
 655                built_payload, prefix_list, function_parameters
 656            )
 657            chunks_response, chunks_info = self._rpc_request_batch_chunked(chunks_info)
 658            return chunks_response, chunks_info
 659
 660        if not block_hash:
 661            with self.get_conn(init=True) as substrate:
 662                block_hash = substrate.get_block_hash()
 663        for storage, queries in functions.items():
 664            chunks, chunks_info = get_page()
 665            # if this doesn't happen something is wrong on the code
 666            # and we won't be able to decode the data properly
 667            assert len(chunks) == len(chunks_info)
 668            for chunk_info, response in zip(chunks_info, chunks):
 669                storage_result = self._decode_response(
 670                    response, chunk_info.fun_params, chunk_info.prefix_list, block_hash
 671                )
 672                multi_result = recursive_update(multi_result, storage_result)
 673
 674        return multi_result
 675
 676    def query(
 677        self,
 678        name: str,
 679        params: list[Any] = [],
 680        module: str = "SubspaceModule",
 681    ) -> Any:
 682        """
 683        Queries a storage function on the network.
 684
 685        Sends a query to the network and retrieves data from a
 686        specified storage function.
 687
 688        Args:
 689            name: The name of the storage function to query.
 690            params: The parameters to pass to the storage function.
 691            module: The module where the storage function is located.
 692
 693        Returns:
 694            The result of the query from the network.
 695
 696        Raises:
 697            NetworkQueryError: If the query fails or is invalid.
 698        """
 699
 700        result = self.query_batch({module: [(name, params)]})
 701
 702        return result[name]
 703
 704    def query_map(
 705        self,
 706        name: str,
 707        params: list[Any] = [],
 708        module: str = "SubspaceModule",
 709        extract_value: bool = True,
 710    ) -> dict[Any, Any]:
 711        """
 712        Queries a storage map from a network node.
 713
 714        Args:
 715            name: The name of the storage map to query.
 716            params: A list of parameters for the query.
 717            module: The module in which the storage map is located.
 718
 719        Returns:
 720            A dictionary representing the key-value pairs
 721              retrieved from the storage map.
 722
 723        Raises:
 724            QueryError: If the query to the network fails or is invalid.
 725        """
 726
 727        result = self.query_batch_map({module: [(name, params)]})
 728
 729        if extract_value:
 730            return {k.value: v.value for k, v in result}  # type: ignore
 731
 732        return result
 733
 734    def compose_call(
 735        self,
 736        fn: str,
 737        params: dict[str, Any],
 738        key: Keypair | None,
 739        module: str = "SubspaceModule",
 740        wait_for_inclusion: bool = True,
 741        wait_for_finalization: bool | None = None,
 742        sudo: bool = False,
 743        unsigned: bool = False,
 744    ) -> ExtrinsicReceipt:
 745        """
 746        Composes and submits a call to the network node.
 747
 748        Composes and signs a call with the provided keypair, and submits it to
 749        the network. The call can be a standard extrinsic or a sudo extrinsic if
 750        elevated permissions are required. The method can optionally wait for
 751        the call's inclusion in a block and/or its finalization.
 752
 753        Args:
 754            fn: The function name to call on the network.
 755            params: A dictionary of parameters for the call.
 756            key: The keypair for signing the extrinsic.
 757            module: The module containing the function.
 758            wait_for_inclusion: Wait for the call's inclusion in a block.
 759            wait_for_finalization: Wait for the transaction's finalization.
 760            sudo: Execute the call as a sudo (superuser) operation.
 761
 762        Returns:
 763            The receipt of the submitted extrinsic, if
 764              `wait_for_inclusion` is True. Otherwise, returns a string
 765              identifier of the extrinsic.
 766
 767        Raises:
 768            ChainTransactionError: If the transaction fails.
 769        """
 770
 771        if key is None and not unsigned:
 772            raise ValueError("Key must be provided for signed extrinsics.")
 773
 774        with self.get_conn() as substrate:
 775            if wait_for_finalization is None:
 776                wait_for_finalization = self.wait_for_finalization
 777
 778            call = substrate.compose_call(  # type: ignore
 779                call_module=module, call_function=fn, call_params=params
 780            )
 781            if sudo:
 782                call = substrate.compose_call(  # type: ignore
 783                    call_module="Sudo",
 784                    call_function="sudo",
 785                    call_params={
 786                        "call": call.value,  # type: ignore
 787                    },
 788                )
 789
 790            if not unsigned:
 791                extrinsic = substrate.create_signed_extrinsic(  # type: ignore
 792                    call=call, keypair=key  # type: ignore
 793                )  # type: ignore
 794            else:
 795                extrinsic = substrate.create_unsigned_extrinsic(call=call)  # type: ignore
 796
 797            response = substrate.submit_extrinsic(
 798                extrinsic=extrinsic,
 799                wait_for_inclusion=wait_for_inclusion,
 800                wait_for_finalization=wait_for_finalization,
 801            )
 802        if wait_for_inclusion:
 803            if not response.is_success:
 804                raise ChainTransactionError(
 805                    response.error_message, response  # type: ignore
 806                )
 807
 808        return response
 809
 810    def compose_call_multisig(
 811        self,
 812        fn: str,
 813        params: dict[str, Any],
 814        key: Keypair,
 815        signatories: list[Ss58Address],
 816        threshold: int,
 817        module: str = "SubspaceModule",
 818        wait_for_inclusion: bool = True,
 819        wait_for_finalization: bool | None = None,
 820        sudo: bool = False,
 821        era: dict[str, int] | None = None,
 822    ) -> ExtrinsicReceipt:
 823        """
 824        Composes and submits a multisignature call to the network node.
 825
 826        This method allows the composition and submission of a call that
 827        requires multiple signatures for execution, known as a multisignature
 828        call. It supports specifying signatories, a threshold of signatures for
 829        the call's execution, and an optional era for the call's mortality. The
 830        call can be a standard extrinsic, a sudo extrinsic for elevated
 831        permissions, or a multisig extrinsic if multiple signatures are
 832        required. Optionally, the method can wait for the call's inclusion in a
 833        block and/or its finalization. Make sure to pass all keys,
 834        that are part of the multisignature.
 835
 836        Args:
 837            fn: The function name to call on the network. params: A dictionary
 838            of parameters for the call. key: The keypair for signing the
 839            extrinsic. signatories: List of SS58 addresses of the signatories.
 840            Include ALL KEYS that are part of the multisig. threshold: The
 841            minimum number of signatories required to execute the extrinsic.
 842            module: The module containing the function to call.
 843            wait_for_inclusion: Whether to wait for the call's inclusion in a
 844            block. wait_for_finalization: Whether to wait for the transaction's
 845            finalization. sudo: Execute the call as a sudo (superuser)
 846            operation. era: Specifies the call's mortality in terms of blocks in
 847            the format
 848                {'period': amount_blocks}. If omitted, the extrinsic is
 849                immortal.
 850
 851        Returns:
 852            The receipt of the submitted extrinsic if `wait_for_inclusion` is
 853            True. Otherwise, returns a string identifier of the extrinsic.
 854
 855        Raises:
 856            ChainTransactionError: If the transaction fails.
 857        """
 858
 859        # getting the call ready
 860        with self.get_conn() as substrate:
 861            if wait_for_finalization is None:
 862                wait_for_finalization = self.wait_for_finalization
 863
 864            # prepares the `GenericCall` object
 865            call = substrate.compose_call(  # type: ignore
 866                call_module=module, call_function=fn, call_params=params
 867            )
 868            if sudo:
 869                call = substrate.compose_call(  # type: ignore
 870                    call_module="Sudo",
 871                    call_function="sudo",
 872                    call_params={
 873                        "call": call.value,  # type: ignore
 874                    },
 875                )
 876
 877            # modify the rpc methods at runtime, to allow for correct payment
 878            # fee calculation parity has a bug in this version,
 879            # where the method has to be removed
 880            rpc_methods = substrate.config.get("rpc_methods")  # type: ignore
 881
 882            if "state_call" in rpc_methods:  # type: ignore
 883                rpc_methods.remove("state_call")  # type: ignore
 884
 885            # create the multisig account
 886            multisig_acc = substrate.generate_multisig_account(  # type: ignore
 887                signatories, threshold
 888            )
 889
 890            # send the multisig extrinsic
 891            extrinsic = substrate.create_multisig_extrinsic(  # type: ignore
 892                call=call,  # type: ignore
 893                keypair=key,
 894                multisig_account=multisig_acc,  # type: ignore
 895                era=era,  # type: ignore
 896            )  # type: ignore
 897
 898            response = substrate.submit_extrinsic(
 899                extrinsic=extrinsic,
 900                wait_for_inclusion=wait_for_inclusion,
 901                wait_for_finalization=wait_for_finalization,
 902            )
 903
 904        if wait_for_inclusion:
 905            if not response.is_success:
 906                raise ChainTransactionError(
 907                    response.error_message, response  # type: ignore
 908                )
 909
 910        return response
 911
 912    def transfer(
 913        self,
 914        key: Keypair,
 915        amount: int,
 916        dest: Ss58Address,
 917    ) -> ExtrinsicReceipt:
 918        """
 919        Transfers a specified amount of tokens from the signer's account to the
 920        specified account.
 921
 922        Args:
 923            key: The keypair associated with the sender's account.
 924            amount: The amount to transfer, in nanotokens.
 925            dest: The SS58 address of the recipient.
 926
 927        Returns:
 928            A receipt of the transaction.
 929
 930        Raises:
 931            InsufficientBalanceError: If the sender's account does not have
 932              enough balance.
 933            ChainTransactionError: If the transaction fails.
 934        """
 935
 936        params = {"dest": dest, "value": amount}
 937
 938        return self.compose_call(
 939            module="Balances", fn="transfer_keep_alive", params=params, key=key
 940        )
 941
 942    def transfer_multiple(
 943        self,
 944        key: Keypair,
 945        destinations: list[Ss58Address],
 946        amounts: list[int],
 947        netuid: str | int = 0,
 948    ) -> ExtrinsicReceipt:
 949        """
 950        Transfers specified amounts of tokens from the signer's account to
 951        multiple target accounts.
 952
 953        The `destinations` and `amounts` lists must be of the same length.
 954
 955        Args:
 956            key: The keypair associated with the sender's account.
 957            destinations: A list of SS58 addresses of the recipients.
 958            amounts: Amount to transfer to each recipient, in nanotokens.
 959            netuid: The network identifier.
 960
 961        Returns:
 962            A receipt of the transaction.
 963
 964        Raises:
 965            InsufficientBalanceError: If the sender's account does not have
 966              enough balance for all transfers.
 967            ChainTransactionError: If the transaction fails.
 968        """
 969
 970        assert len(destinations) == len(amounts)
 971
 972        # extract existential deposit from amounts
 973        existential_deposit = self.get_existential_deposit()
 974        amounts = [a - existential_deposit for a in amounts]
 975
 976        params = {
 977            "netuid": netuid,
 978            "destinations": destinations,
 979            "amounts": amounts,
 980        }
 981
 982        return self.compose_call(
 983            module="SubspaceModule", fn="transfer_multiple", params=params, key=key
 984        )
 985
 986    def stake(
 987        self,
 988        key: Keypair,
 989        amount: int,
 990        dest: Ss58Address,
 991        netuid: int = 0,
 992    ) -> ExtrinsicReceipt:
 993        """
 994        Stakes the specified amount of tokens to a module key address.
 995
 996        Args:
 997            key: The keypair associated with the staker's account.
 998            amount: The amount of tokens to stake, in nanotokens.
 999            dest: The SS58 address of the module key to stake to.
1000            netuid: The network identifier.
1001
1002        Returns:
1003            A receipt of the staking transaction.
1004
1005        Raises:
1006            InsufficientBalanceError: If the staker's account does not have
1007              enough balance.
1008            ChainTransactionError: If the transaction fails.
1009        """
1010
1011        params = {"amount": amount, "netuid": netuid, "module_key": dest}
1012
1013        return self.compose_call(fn="add_stake", params=params, key=key)
1014
1015    def unstake(
1016        self,
1017        key: Keypair,
1018        amount: int,
1019        dest: Ss58Address,
1020        netuid: int = 0,
1021    ) -> ExtrinsicReceipt:
1022        """
1023        Unstakes the specified amount of tokens from a module key address.
1024
1025        Args:
1026            key: The keypair associated with the unstaker's account.
1027            amount: The amount of tokens to unstake, in nanotokens.
1028            dest: The SS58 address of the module key to unstake from.
1029            netuid: The network identifier.
1030
1031        Returns:
1032            A receipt of the unstaking transaction.
1033
1034        Raises:
1035            InsufficientStakeError: If the staked key does not have enough
1036              staked tokens by the signer key.
1037            ChainTransactionError: If the transaction fails.
1038        """
1039
1040        params = {"amount": amount, "netuid": netuid, "module_key": dest}
1041        return self.compose_call(fn="remove_stake", params=params, key=key)
1042
1043    def update_module(
1044        self,
1045        key: Keypair,
1046        name: str,
1047        address: str,
1048        metadata: str | None = None,
1049        delegation_fee: int = 20,
1050        netuid: int = 0,
1051    ) -> ExtrinsicReceipt:
1052        """
1053        Updates the parameters of a registered module.
1054
1055        The delegation fee must be an integer between 0 and 100.
1056
1057        Args:
1058            key: The keypair associated with the module's account.
1059            name: The new name for the module. If None, the name is not updated.
1060            address: The new address for the module.
1061                If None, the address is not updated.
1062            delegation_fee: The new delegation fee for the module,
1063                between 0 and 100.
1064            netuid: The network identifier.
1065
1066        Returns:
1067            A receipt of the module update transaction.
1068
1069        Raises:
1070            InvalidParameterError: If the provided parameters are invalid.
1071            ChainTransactionError: If the transaction fails.
1072        """
1073
1074        assert isinstance(delegation_fee, int)
1075
1076        params = {
1077            "netuid": netuid,
1078            "name": name,
1079            "address": address,
1080            "delegation_fee": delegation_fee,
1081            "metadata": metadata,
1082        }
1083
1084        response = self.compose_call("update_module", params=params, key=key)
1085
1086        return response
1087
1088    def register_module(
1089        self,
1090        key: Keypair,
1091        name: str,
1092        address: str | None = None,
1093        subnet: str = "commune",
1094        min_stake: int | None = None,
1095        metadata: str | None = None,
1096    ) -> ExtrinsicReceipt:
1097        """
1098        Registers a new module in the network.
1099
1100        Args:
1101            key: The keypair used for registering the module.
1102            name: The name of the module. If None, a default or previously
1103                set name is used. # How does this work?
1104            address: The address of the module. If None, a default or
1105                previously set address is used. # How does this work?
1106            subnet: The network subnet to register the module in.
1107            min_stake: The minimum stake required for the module, in nanotokens.
1108                If None, a default value is used.
1109
1110        Returns:
1111            A receipt of the registration transaction.
1112
1113        Raises:
1114            InvalidParameterError: If the provided parameters are invalid.
1115            ChainTransactionError: If the transaction fails.
1116        """
1117
1118        stake = self.get_min_stake() if min_stake is None else min_stake
1119
1120        key_addr = key.ss58_address
1121
1122        params = {
1123            "network": subnet,
1124            "address": address,
1125            "name": name,
1126            "stake": stake,
1127            "module_key": key_addr,
1128            "metadata": metadata,
1129        }
1130
1131        response = self.compose_call("register", params=params, key=key)
1132        return response
1133
1134    def vote(
1135        self,
1136        key: Keypair,
1137        uids: list[int],
1138        weights: list[int],
1139        netuid: int = 0,
1140    ) -> ExtrinsicReceipt:
1141        """
1142        Casts votes on a list of module UIDs with corresponding weights.
1143
1144        The length of the UIDs list and the weights list should be the same.
1145        Each weight corresponds to the UID at the same index.
1146
1147        Args:
1148            key: The keypair used for signing the vote transaction.
1149            uids: A list of module UIDs to vote on.
1150            weights: A list of weights corresponding to each UID.
1151            netuid: The network identifier.
1152
1153        Returns:
1154            A receipt of the voting transaction.
1155
1156        Raises:
1157            InvalidParameterError: If the lengths of UIDs and weights lists
1158                do not match.
1159            ChainTransactionError: If the transaction fails.
1160        """
1161
1162        assert len(uids) == len(weights)
1163
1164        params = {
1165            "uids": uids,
1166            "weights": weights,
1167            "netuid": netuid,
1168        }
1169
1170        response = self.compose_call("set_weights", params=params, key=key)
1171
1172        return response
1173
1174    def update_subnet(
1175        self,
1176        key: Keypair,
1177        params: SubnetParams,
1178        netuid: int = 0,
1179    ) -> ExtrinsicReceipt:
1180        """
1181        Update a subnet's configuration.
1182
1183        It requires the founder key for authorization.
1184
1185        Args:
1186            key: The founder keypair of the subnet.
1187            params: The new parameters for the subnet.
1188            netuid: The network identifier.
1189
1190        Returns:
1191            A receipt of the subnet update transaction.
1192
1193        Raises:
1194            AuthorizationError: If the key is not authorized.
1195            ChainTransactionError: If the transaction fails.
1196        """
1197
1198        general_params = dict(params)
1199        general_params["netuid"] = netuid
1200
1201        response = self.compose_call(
1202            fn="update_subnet",
1203            params=general_params,
1204            key=key,
1205        )
1206
1207        return response
1208
1209    def transfer_stake(
1210        self,
1211        key: Keypair,
1212        amount: int,
1213        from_module_key: Ss58Address,
1214        dest_module_address: Ss58Address,
1215        netuid: int = 0,
1216    ) -> ExtrinsicReceipt:
1217        """
1218        Realocate staked tokens from one staked module to another module.
1219
1220        Args:
1221            key: The keypair associated with the account that is delegating the tokens.
1222            amount: The amount of staked tokens to transfer, in nanotokens.
1223            from_module_key: The SS58 address of the module you want to transfer from (currently delegated by the key).
1224            dest_module_address: The SS58 address of the destination (newly delegated key).
1225            netuid: The network identifier.
1226
1227        Returns:
1228            A receipt of the stake transfer transaction.
1229
1230        Raises:
1231            InsufficientStakeError: If the source module key does not have
1232            enough staked tokens. ChainTransactionError: If the transaction
1233            fails.
1234        """
1235
1236        amount = amount - self.get_existential_deposit()
1237
1238        params = {
1239            "amount": amount,
1240            "netuid": netuid,
1241            "module_key": from_module_key,
1242            "new_module_key": dest_module_address,
1243        }
1244
1245        response = self.compose_call("transfer_stake", key=key, params=params)
1246
1247        return response
1248
1249    def multiunstake(
1250        self,
1251        key: Keypair,
1252        keys: list[Ss58Address],
1253        amounts: list[int],
1254        netuid: int = 0,
1255    ) -> ExtrinsicReceipt:
1256        """
1257        Unstakes tokens from multiple module keys.
1258
1259        And the lists `keys` and `amounts` must be of the same length. Each
1260        amount corresponds to the module key at the same index.
1261
1262        Args:
1263            key: The keypair associated with the unstaker's account.
1264            keys: A list of SS58 addresses of the module keys to unstake from.
1265            amounts: A list of amounts to unstake from each module key,
1266              in nanotokens.
1267            netuid: The network identifier.
1268
1269        Returns:
1270            A receipt of the multi-unstaking transaction.
1271
1272        Raises:
1273            MismatchedLengthError: If the lengths of keys and amounts lists do
1274            not match. InsufficientStakeError: If any of the module keys do not
1275            have enough staked tokens. ChainTransactionError: If the transaction
1276            fails.
1277        """
1278
1279        assert len(keys) == len(amounts)
1280
1281        params = {"netuid": netuid, "module_keys": keys, "amounts": amounts}
1282
1283        response = self.compose_call("remove_stake_multiple", params=params, key=key)
1284
1285        return response
1286
1287    def multistake(
1288        self,
1289        key: Keypair,
1290        keys: list[Ss58Address],
1291        amounts: list[int],
1292        netuid: int = 0,
1293    ) -> ExtrinsicReceipt:
1294        """
1295        Stakes tokens to multiple module keys.
1296
1297        The lengths of the `keys` and `amounts` lists must be the same. Each
1298        amount corresponds to the module key at the same index.
1299
1300        Args:
1301            key: The keypair associated with the staker's account.
1302            keys: A list of SS58 addresses of the module keys to stake to.
1303            amounts: A list of amounts to stake to each module key,
1304                in nanotokens.
1305            netuid: The network identifier.
1306
1307        Returns:
1308            A receipt of the multi-staking transaction.
1309
1310        Raises:
1311            MismatchedLengthError: If the lengths of keys and amounts lists
1312                do not match.
1313            ChainTransactionError: If the transaction fails.
1314        """
1315
1316        assert len(keys) == len(amounts)
1317
1318        params = {
1319            "module_keys": keys,
1320            "amounts": amounts,
1321            "netuid": netuid,
1322        }
1323
1324        response = self.compose_call("add_stake_multiple", params=params, key=key)
1325
1326        return response
1327
1328    def add_profit_shares(
1329        self,
1330        key: Keypair,
1331        keys: list[Ss58Address],
1332        shares: list[int],
1333    ) -> ExtrinsicReceipt:
1334        """
1335        Allocates profit shares to multiple keys.
1336
1337        The lists `keys` and `shares` must be of the same length,
1338        with each share amount corresponding to the key at the same index.
1339
1340        Args:
1341            key: The keypair associated with the account
1342                distributing the shares.
1343            keys: A list of SS58 addresses to allocate shares to.
1344            shares: A list of share amounts to allocate to each key,
1345                in nanotokens.
1346
1347        Returns:
1348            A receipt of the profit sharing transaction.
1349
1350        Raises:
1351            MismatchedLengthError: If the lengths of keys and shares
1352                lists do not match.
1353            ChainTransactionError: If the transaction fails.
1354        """
1355
1356        assert len(keys) == len(shares)
1357
1358        params = {"keys": keys, "shares": shares}
1359
1360        response = self.compose_call("add_profit_shares", params=params, key=key)
1361
1362        return response
1363
1364    def add_subnet_proposal(
1365        self, key: Keypair, params: SubnetParams, ipfs: str, netuid: int = 0
1366    ) -> ExtrinsicReceipt:
1367        """
1368        Submits a proposal for creating or modifying a subnet within the
1369        network.
1370
1371        The proposal includes various parameters like the name, founder, share
1372        allocations, and other subnet-specific settings.
1373
1374        Args:
1375            key: The keypair used for signing the proposal transaction.
1376            params: The parameters for the subnet proposal.
1377            netuid: The network identifier.
1378
1379        Returns:
1380            A receipt of the subnet proposal transaction.
1381
1382        Raises:
1383            InvalidParameterError: If the provided subnet
1384                parameters are invalid.
1385            ChainTransactionError: If the transaction fails.
1386        """
1387
1388        general_params = dict(params)
1389        general_params["subnet_id"] = netuid
1390        general_params["data"] = ipfs
1391        # breakpoint()
1392        # general_params["burn_config"] = json.dumps(general_params["burn_config"])
1393        response = self.compose_call(
1394            fn="add_subnet_params_proposal",
1395            params=general_params,
1396            key=key,
1397            module="GovernanceModule",
1398        )
1399
1400        return response
1401
1402    def add_custom_proposal(
1403        self,
1404        key: Keypair,
1405        cid: str,
1406    ) -> ExtrinsicReceipt:
1407
1408        params = {"data": cid}
1409
1410        response = self.compose_call(
1411            fn="add_global_custom_proposal",
1412            params=params,
1413            key=key,
1414            module="GovernanceModule",
1415        )
1416        return response
1417
1418    def add_custom_subnet_proposal(
1419        self,
1420        key: Keypair,
1421        cid: str,
1422        netuid: int = 0,
1423    ) -> ExtrinsicReceipt:
1424        """
1425        Submits a proposal for creating or modifying a custom subnet within the
1426        network.
1427
1428        The proposal includes various parameters like the name, founder, share
1429        allocations, and other subnet-specific settings.
1430
1431        Args:
1432            key: The keypair used for signing the proposal transaction.
1433            params: The parameters for the subnet proposal.
1434            netuid: The network identifier.
1435
1436        Returns:
1437            A receipt of the subnet proposal transaction.
1438        """
1439
1440        params = {
1441            "data": cid,
1442            "subnet_id": netuid,
1443        }
1444
1445        response = self.compose_call(
1446            fn="add_subnet_custom_proposal",
1447            params=params,
1448            key=key,
1449            module="GovernanceModule",
1450        )
1451
1452        return response
1453
1454    def add_global_proposal(
1455        self,
1456        key: Keypair,
1457        params: NetworkParams,
1458        cid: str | None,
1459    ) -> ExtrinsicReceipt:
1460        """
1461        Submits a proposal for altering the global network parameters.
1462
1463        Allows for the submission of a proposal to
1464        change various global parameters
1465        of the network, such as emission rates, rate limits, and voting
1466        thresholds. It is used to
1467        suggest changes that affect the entire network's operation.
1468
1469        Args:
1470            key: The keypair used for signing the proposal transaction.
1471            params: A dictionary containing global network parameters
1472                    like maximum allowed subnets, modules,
1473                    transaction rate limits, and others.
1474
1475        Returns:
1476            A receipt of the global proposal transaction.
1477
1478        Raises:
1479            InvalidParameterError: If the provided network
1480                parameters are invalid.
1481            ChainTransactionError: If the transaction fails.
1482        """
1483        general_params = cast(dict[str, Any], params)
1484        cid = cid or ""
1485        general_params["data"] = cid
1486
1487        response = self.compose_call(
1488            fn="add_global_params_proposal",
1489            params=general_params,
1490            key=key,
1491            module="GovernanceModule",
1492        )
1493
1494        return response
1495
1496    def vote_on_proposal(
1497        self,
1498        key: Keypair,
1499        proposal_id: int,
1500        agree: bool,
1501    ) -> ExtrinsicReceipt:
1502        """
1503        Casts a vote on a specified proposal within the network.
1504
1505        Args:
1506            key: The keypair used for signing the vote transaction.
1507            proposal_id: The unique identifier of the proposal to vote on.
1508
1509        Returns:
1510            A receipt of the voting transaction in nanotokens.
1511
1512        Raises:
1513            InvalidProposalIDError: If the provided proposal ID does not
1514                exist or is invalid.
1515            ChainTransactionError: If the transaction fails.
1516        """
1517
1518        params = {"proposal_id": proposal_id, "agree": agree}
1519
1520        response = self.compose_call(
1521            "vote_proposal",
1522            key=key,
1523            params=params,
1524            module="GovernanceModule",
1525        )
1526
1527        return response
1528
1529    def unvote_on_proposal(
1530        self,
1531        key: Keypair,
1532        proposal_id: int,
1533    ) -> ExtrinsicReceipt:
1534        """
1535        Retracts a previously cast vote on a specified proposal.
1536
1537        Args:
1538            key: The keypair used for signing the unvote transaction.
1539            proposal_id: The unique identifier of the proposal to withdraw the
1540                vote from.
1541
1542        Returns:
1543            A receipt of the unvoting transaction in nanotokens.
1544
1545        Raises:
1546            InvalidProposalIDError: If the provided proposal ID does not
1547                exist or is invalid.
1548            ChainTransactionError: If the transaction fails to be processed, or
1549                if there was no prior vote to retract.
1550        """
1551
1552        params = {"proposal_id": proposal_id}
1553
1554        response = self.compose_call(
1555            "remove_vote_proposal",
1556            key=key,
1557            params=params,
1558            module="GovernanceModule",
1559        )
1560
1561        return response
1562
1563    def enable_vote_power_delegation(self, key: Keypair) -> ExtrinsicReceipt:
1564        """
1565        Enables vote power delegation for the signer's account.
1566
1567        Args:
1568            key: The keypair used for signing the delegation transaction.
1569
1570        Returns:
1571            A receipt of the vote power delegation transaction.
1572
1573        Raises:
1574            ChainTransactionError: If the transaction fails.
1575        """
1576
1577        response = self.compose_call(
1578            "enable_vote_power_delegation",
1579            params={},
1580            key=key,
1581            module="GovernanceModule",
1582        )
1583
1584        return response
1585
1586    def disable_vote_power_delegation(self, key: Keypair) -> ExtrinsicReceipt:
1587        """
1588        Disables vote power delegation for the signer's account.
1589
1590        Args:
1591            key: The keypair used for signing the delegation transaction.
1592
1593        Returns:
1594            A receipt of the vote power delegation transaction.
1595
1596        Raises:
1597            ChainTransactionError: If the transaction fails.
1598        """
1599
1600        response = self.compose_call(
1601            "disable_vote_power_delegation",
1602            params={},
1603            key=key,
1604            module="GovernanceModule",
1605        )
1606
1607        return response
1608
1609    def add_dao_application(
1610        self, key: Keypair, application_key: Ss58Address, data: str
1611    ) -> ExtrinsicReceipt:
1612        """
1613        Submits a new application to the general subnet DAO.
1614
1615        Args:
1616            key: The keypair used for signing the application transaction.
1617            application_key: The SS58 address of the application key.
1618            data: The data associated with the application.
1619
1620        Returns:
1621            A receipt of the application transaction.
1622
1623        Raises:
1624            ChainTransactionError: If the transaction fails.
1625        """
1626
1627        params = {"application_key": application_key, "data": data}
1628
1629        response = self.compose_call("add_dao_application", key=key, params=params)
1630
1631        return response
1632
1633    def query_map_curator_applications(self) -> dict[str, dict[str, str]]:
1634        query_result = self.query_map(
1635            "CuratorApplications", params=[], extract_value=False
1636        )
1637        applications = query_result.get("CuratorApplications", {})
1638        return applications
1639
1640    def query_map_proposals(
1641        self, extract_value: bool = False
1642    ) -> dict[int, dict[str, Any]]:
1643        """
1644        Retrieves a mappping of proposals from the network.
1645
1646        Queries the network and returns a mapping of proposal IDs to
1647        their respective parameters.
1648
1649        Returns:
1650            A dictionary mapping proposal IDs
1651            to dictionaries of their parameters.
1652
1653        Raises:
1654            QueryError: If the query to the network fails or is invalid.
1655        """
1656
1657        return self.query_map(
1658            "Proposals", extract_value=extract_value, module="GovernanceModule"
1659        )["Proposals"]
1660
1661    def query_map_weights(
1662        self, netuid: int = 0, extract_value: bool = False
1663    ) -> dict[int, list[int]]:
1664        """
1665        Retrieves a mapping of weights for keys on the network.
1666
1667        Queries the network and returns a mapping of key UIDs to
1668        their respective weights.
1669
1670        Args:
1671            netuid: The network UID from which to get the weights.
1672
1673        Returns:
1674            A dictionary mapping key UIDs to lists of their weights.
1675
1676        Raises:
1677            QueryError: If the query to the network fails or is invalid.
1678        """
1679
1680        return self.query_map("Weights", [netuid], extract_value=extract_value)[
1681            "Weights"
1682        ]
1683
1684    def query_map_key(
1685        self,
1686        netuid: int = 0,
1687        extract_value: bool = False,
1688    ) -> dict[int, Ss58Address]:
1689        """
1690        Retrieves a map of keys from the network.
1691
1692        Fetches a mapping of key UIDs to their associated
1693        addresses on the network.
1694        The query can be targeted at a specific network UID if required.
1695
1696        Args:
1697            netuid: The network UID from which to get the keys.
1698
1699        Returns:
1700            A dictionary mapping key UIDs to their addresses.
1701
1702        Raises:
1703            QueryError: If the query to the network fails or is invalid.
1704        """
1705        return self.query_map("Keys", [netuid], extract_value=extract_value)["Keys"]
1706
1707    def query_map_address(
1708        self, netuid: int = 0, extract_value: bool = False
1709    ) -> dict[int, str]:
1710        """
1711        Retrieves a map of key addresses from the network.
1712
1713        Queries the network for a mapping of key UIDs to their addresses.
1714
1715        Args:
1716            netuid: The network UID from which to get the addresses.
1717
1718        Returns:
1719            A dictionary mapping key UIDs to their addresses.
1720
1721        Raises:
1722            QueryError: If the query to the network fails or is invalid.
1723        """
1724
1725        return self.query_map("Address", [netuid], extract_value=extract_value)[
1726            "Address"
1727        ]
1728
1729    def query_map_emission(self, extract_value: bool = False) -> dict[int, list[int]]:
1730        """
1731        Retrieves a map of emissions for keys on the network.
1732
1733        Queries the network to get a mapping of
1734        key UIDs to their emission values.
1735
1736        Returns:
1737            A dictionary mapping key UIDs to lists of their emission values.
1738
1739        Raises:
1740            QueryError: If the query to the network fails or is invalid.
1741        """
1742
1743        return self.query_map("Emission", extract_value=extract_value)["Emission"]
1744
1745    def query_map_incentive(self, extract_value: bool = False) -> dict[int, list[int]]:
1746        """
1747        Retrieves a mapping of incentives for keys on the network.
1748
1749        Queries the network and returns a mapping of key UIDs to
1750        their respective incentive values.
1751
1752        Returns:
1753            A dictionary mapping key UIDs to lists of their incentive values.
1754
1755        Raises:
1756            QueryError: If the query to the network fails or is invalid.
1757        """
1758
1759        return self.query_map("Incentive", extract_value=extract_value)["Incentive"]
1760
1761    def query_map_dividend(self, extract_value: bool = False) -> dict[int, list[int]]:
1762        """
1763        Retrieves a mapping of dividends for keys on the network.
1764
1765        Queries the network for a mapping of key UIDs to
1766        their dividend values.
1767
1768        Returns:
1769            A dictionary mapping key UIDs to lists of their dividend values.
1770
1771        Raises:
1772            QueryError: If the query to the network fails or is invalid.
1773        """
1774
1775        return self.query_map("Dividends", extract_value=extract_value)["Dividends"]
1776
1777    def query_map_regblock(
1778        self, netuid: int = 0, extract_value: bool = False
1779    ) -> dict[int, int]:
1780        """
1781        Retrieves a mapping of registration blocks for keys on the network.
1782
1783        Queries the network for a mapping of key UIDs to
1784        the blocks where they were registered.
1785
1786        Args:
1787            netuid: The network UID from which to get the registration blocks.
1788
1789        Returns:
1790            A dictionary mapping key UIDs to their registration blocks.
1791
1792        Raises:
1793            QueryError: If the query to the network fails or is invalid.
1794        """
1795
1796        return self.query_map(
1797            "RegistrationBlock", [netuid], extract_value=extract_value
1798        )["RegistrationBlock"]
1799
1800    def query_map_lastupdate(self, extract_value: bool = False) -> dict[int, list[int]]:
1801        """
1802        Retrieves a mapping of the last update times for keys on the network.
1803
1804        Queries the network for a mapping of key UIDs to their last update times.
1805
1806        Returns:
1807            A dictionary mapping key UIDs to lists of their last update times.
1808
1809        Raises:
1810            QueryError: If the query to the network fails or is invalid.
1811        """
1812
1813        return self.query_map("LastUpdate", extract_value=extract_value)["LastUpdate"]
1814
1815    def query_map_total_stake(self, extract_value: bool = False) -> dict[int, int]:
1816        """
1817        Retrieves a mapping of total stakes for keys on the network.
1818
1819        Queries the network for a mapping of key UIDs to their total stake amounts.
1820
1821        Returns:
1822            A dictionary mapping key UIDs to their total stake amounts.
1823
1824        Raises:
1825            QueryError: If the query to the network fails or is invalid.
1826        """
1827
1828        return self.query_map("TotalStake", extract_value=extract_value)["TotalStake"]
1829
1830    def query_map_stakefrom(
1831        self, netuid: int = 0, extract_value: bool = False
1832    ) -> dict[str, list[tuple[str, int]]]:
1833        """
1834        Retrieves a mapping of stakes from various sources for keys on the network.
1835
1836        Queries the network to obtain a mapping of key addresses to the sources
1837        and amounts of stakes they have received.
1838
1839        Args:
1840            netuid: The network UID from which to get the stakes.
1841
1842        Returns:
1843            A dictionary mapping key addresses to lists of tuples
1844            (module_key_address, amount).
1845
1846        Raises:
1847            QueryError: If the query to the network fails or is invalid.
1848        """
1849
1850        return self.query_map("StakeFrom", [netuid], extract_value=extract_value)[
1851            "StakeFrom"
1852        ]
1853
1854    def query_map_staketo(
1855        self, netuid: int = 0, extract_value: bool = False
1856    ) -> dict[str, list[tuple[str, int]]]:
1857        """
1858        Retrieves a mapping of stakes to destinations for keys on the network.
1859
1860        Queries the network for a mapping of key addresses to the destinations
1861        and amounts of stakes they have made.
1862
1863        Args:
1864            netuid: The network UID from which to get the stakes.
1865
1866        Returns:
1867            A dictionary mapping key addresses to lists of tuples
1868            (module_key_address, amount).
1869
1870        Raises:
1871            QueryError: If the query to the network fails or is invalid.
1872        """
1873
1874        return self.query_map("StakeTo", [netuid], extract_value=extract_value)[
1875            "StakeTo"
1876        ]
1877
1878    def query_map_stake(
1879        self, netuid: int = 0, extract_value: bool = False
1880    ) -> dict[str, int]:
1881        """
1882        Retrieves a mapping of stakes for keys on the network.
1883
1884        Queries the network and returns a mapping of key addresses to their
1885        respective delegated staked balances amounts.
1886        The query can be targeted at a specific network UID if required.
1887
1888        Args:
1889            netuid: The network UID from which to get the stakes.
1890
1891        Returns:
1892            A dictionary mapping key addresses to their stake amounts.
1893
1894        Raises:
1895            QueryError: If the query to the network fails or is invalid.
1896        """
1897
1898        return self.query_map("Stake", [netuid], extract_value=extract_value)["Stake"]
1899
1900    def query_map_delegationfee(
1901        self, netuid: int = 0, extract_value: bool = False
1902    ) -> dict[str, int]:
1903        """
1904        Retrieves a mapping of delegation fees for keys on the network.
1905
1906        Queries the network to obtain a mapping of key addresses to their
1907        respective delegation fees.
1908
1909        Args:
1910            netuid: The network UID to filter the delegation fees.
1911
1912        Returns:
1913            A dictionary mapping key addresses to their delegation fees.
1914
1915        Raises:
1916            QueryError: If the query to the network fails or is invalid.
1917        """
1918
1919        return self.query_map("DelegationFee", [netuid], extract_value=extract_value)[
1920            "DelegationFee"
1921        ]
1922
1923    def query_map_tempo(self, extract_value: bool = False) -> dict[int, int]:
1924        """
1925        Retrieves a mapping of tempo settings for the network.
1926
1927        Queries the network to obtain the tempo (rate of reward distributions)
1928        settings for various network subnets.
1929
1930        Returns:
1931            A dictionary mapping network UIDs to their tempo settings.
1932
1933        Raises:
1934            QueryError: If the query to the network fails or is invalid.
1935        """
1936
1937        return self.query_map("Tempo", extract_value=extract_value)["Tempo"]
1938
1939    def query_map_immunity_period(self, extract_value: bool) -> dict[int, int]:
1940        """
1941        Retrieves a mapping of immunity periods for the network.
1942
1943        Queries the network for the immunity period settings,
1944        which represent the time duration during which modules
1945        can not get deregistered.
1946
1947        Returns:
1948            A dictionary mapping network UIDs to their immunity period settings.
1949
1950        Raises:
1951            QueryError: If the query to the network fails or is invalid.
1952        """
1953
1954        return self.query_map("ImmunityPeriod", extract_value=extract_value)[
1955            "ImmunityPeriod"
1956        ]
1957
1958    def query_map_min_allowed_weights(
1959        self, extract_value: bool = False
1960    ) -> dict[int, int]:
1961        """
1962        Retrieves a mapping of minimum allowed weights for the network.
1963
1964        Queries the network to obtain the minimum allowed weights,
1965        which are the lowest permissible weight values that can be set by
1966        validators.
1967
1968        Returns:
1969            A dictionary mapping network UIDs to
1970            their minimum allowed weight values.
1971
1972        Raises:
1973            QueryError: If the query to the network fails or is invalid.
1974        """
1975
1976        return self.query_map("MinAllowedWeights", extract_value=extract_value)[
1977            "MinAllowedWeights"
1978        ]
1979
1980    def query_map_max_allowed_weights(
1981        self, extract_value: bool = False
1982    ) -> dict[int, int]:
1983        """
1984        Retrieves a mapping of maximum allowed weights for the network.
1985
1986        Queries the network for the maximum allowed weights,
1987        which are the highest permissible
1988        weight values that can be set by validators.
1989
1990        Returns:
1991            A dictionary mapping network UIDs to
1992            their maximum allowed weight values.
1993
1994        Raises:
1995            QueryError: If the query to the network fails or is invalid.
1996        """
1997
1998        return self.query_map("MaxAllowedWeights", extract_value=extract_value)[
1999            "MaxAllowedWeights"
2000        ]
2001
2002    def query_map_max_allowed_uids(self, extract_value: bool = False) -> dict[int, int]:
2003        """
2004        Queries the network for the maximum number of allowed user IDs (UIDs)
2005        for each network subnet.
2006
2007        Fetches a mapping of network subnets to their respective
2008        limits on the number of user IDs that can be created or used.
2009
2010        Returns:
2011            A dictionary mapping network UIDs (unique identifiers) to their
2012            maximum allowed number of UIDs.
2013            Each entry represents a network subnet
2014            with its corresponding UID limit.
2015
2016        Raises:
2017            QueryError: If the query to the network fails or is invalid.
2018        """
2019
2020        return self.query_map("MaxAllowedUids", extract_value=extract_value)[
2021            "MaxAllowedUids"
2022        ]
2023
2024    def query_map_min_stake(self, extract_value: bool = False) -> dict[int, int]:
2025        """
2026        Retrieves a mapping of minimum allowed stake on the network.
2027
2028        Queries the network to obtain the minimum number of stake,
2029        which is represented in nanotokens.
2030
2031        Returns:
2032            A dictionary mapping network UIDs to
2033            their minimum allowed stake values.
2034
2035        Raises:
2036            QueryError: If the query to the network fails or is invalid.
2037        """
2038
2039        return self.query_map("MinStake", extract_value=extract_value)["MinStake"]
2040
2041    def query_map_max_stake(self, extract_value: bool = False) -> dict[int, int]:
2042        """
2043        Retrieves a mapping of the maximum stake values for the network.
2044
2045        Queries the network for the maximum stake values across various s
2046        ubnets of the network.
2047
2048        Returns:
2049            A dictionary mapping network UIDs to their maximum stake values.
2050
2051        Raises:
2052            QueryError: If the query to the network fails or is invalid.
2053        """
2054
2055        return self.query_map("MaxStake", extract_value=extract_value)["MaxStake"]
2056
2057    def query_map_founder(self, extract_value: bool = False) -> dict[int, str]:
2058        """
2059        Retrieves a mapping of founders for the network.
2060
2061        Queries the network to obtain the founders associated with
2062        various subnets.
2063
2064        Returns:
2065            A dictionary mapping network UIDs to their respective founders.
2066
2067        Raises:
2068            QueryError: If the query to the network fails or is invalid.
2069        """
2070
2071        return self.query_map("Founder", extract_value=extract_value)["Founder"]
2072
2073    def query_map_founder_share(self, extract_value: bool = False) -> dict[int, int]:
2074        """
2075        Retrieves a mapping of founder shares for the network.
2076
2077        Queries the network for the share percentages
2078        allocated to founders across different subnets.
2079
2080        Returns:
2081            A dictionary mapping network UIDs to their founder share percentages.
2082
2083        Raises:
2084            QueryError: If the query to the network fails or is invalid.
2085        """
2086
2087        return self.query_map("FounderShare", extract_value=extract_value)[
2088            "FounderShare"
2089        ]
2090
2091    def query_map_incentive_ratio(self, extract_value: bool = False) -> dict[int, int]:
2092        """
2093        Retrieves a mapping of incentive ratios for the network.
2094
2095        Queries the network for the incentive ratios,
2096        which are the proportions of rewards or incentives
2097        allocated in different subnets of the network.
2098
2099        Returns:
2100            A dictionary mapping network UIDs to their incentive ratios.
2101
2102        Raises:
2103            QueryError: If the query to the network fails or is invalid.
2104        """
2105
2106        return self.query_map("IncentiveRatio", extract_value=extract_value)[
2107            "IncentiveRatio"
2108        ]
2109
2110    def query_map_trust_ratio(self, extract_value: bool = False) -> dict[int, int]:
2111        """
2112        Retrieves a mapping of trust ratios for the network.
2113
2114        Queries the network for trust ratios,
2115        indicative of the level of trust or credibility assigned
2116        to different subnets of the network.
2117
2118        Returns:
2119            A dictionary mapping network UIDs to their trust ratios.
2120
2121        Raises:
2122            QueryError: If the query to the network fails or is invalid.
2123        """
2124
2125        return self.query_map("TrustRatio", extract_value=extract_value)["TrustRatio"]
2126
2127    def query_map_vote_mode_subnet(self, extract_value: bool = False) -> dict[int, str]:
2128        """
2129        Retrieves a mapping of vote modes for subnets within the network.
2130
2131        Queries the network for the voting modes used in different
2132        subnets, which define the methodology or approach of voting within those
2133        subnets.
2134
2135        Returns:
2136            A dictionary mapping network UIDs to their vote
2137            modes for subnets.
2138
2139        Raises:
2140            QueryError: If the query to the network fails or is invalid.
2141        """
2142
2143        return self.query_map("VoteModeSubnet", extract_value=extract_value)[
2144            "VoteModeSubnet"
2145        ]
2146
2147    def query_map_legit_whitelist(
2148        self, extract_value: bool = False
2149    ) -> dict[Ss58Address, int]:
2150        """
2151        Retrieves a mapping of whitelisted addresses for the network.
2152
2153        Queries the network for a mapping of whitelisted addresses
2154        and their respective legitimacy status.
2155
2156        Returns:
2157            A dictionary mapping addresses to their legitimacy status.
2158
2159        Raises:
2160            QueryError: If the query to the network fails or is invalid.
2161        """
2162
2163        return self.query_map("LegitWhitelist", extract_value=extract_value)[
2164            "LegitWhitelist"
2165        ]
2166
2167    def query_map_subnet_names(self, extract_value: bool = False) -> dict[int, str]:
2168        """
2169        Retrieves a mapping of subnet names within the network.
2170
2171        Queries the network for the names of various subnets,
2172        providing an overview of the different
2173        subnets within the network.
2174
2175        Returns:
2176            A dictionary mapping network UIDs to their subnet names.
2177
2178        Raises:
2179            QueryError: If the query to the network fails or is invalid.
2180        """
2181
2182        return self.query_map("SubnetNames", extract_value=extract_value)["SubnetNames"]
2183
2184    def query_map_balances(
2185        self, extract_value: bool = False
2186    ) -> dict[str, dict["str", int | dict[str, int]]]:
2187        """
2188        Retrieves a mapping of account balances within the network.
2189
2190        Queries the network for the balances associated with different accounts.
2191        It provides detailed information including various types of
2192        balances for each account.
2193
2194        Returns:
2195            A dictionary mapping account addresses to their balance details.
2196
2197        Raises:
2198            QueryError: If the query to the network fails or is invalid.
2199        """
2200
2201        return self.query_map("Account", module="System", extract_value=extract_value)[
2202            "Account"
2203        ]
2204
2205    def query_map_registration_blocks(
2206        self, netuid: int = 0, extract_value: bool = False
2207    ) -> dict[int, int]:
2208        """
2209        Retrieves a mapping of registration blocks for UIDs on the network.
2210
2211        Queries the network to find the block numbers at which various
2212        UIDs were registered.
2213
2214        Args:
2215            netuid: The network UID from which to get the registrations.
2216
2217        Returns:
2218            A dictionary mapping UIDs to their registration block numbers.
2219
2220        Raises:
2221            QueryError: If the query to the network fails or is invalid.
2222        """
2223
2224        return self.query_map(
2225            "RegistrationBlock", [netuid], extract_value=extract_value
2226        )["RegistrationBlock"]
2227
2228    def query_map_name(
2229        self, netuid: int = 0, extract_value: bool = False
2230    ) -> dict[int, str]:
2231        """
2232        Retrieves a mapping of names for keys on the network.
2233
2234        Queries the network for the names associated with different keys.
2235        It provides a mapping of key UIDs to their registered names.
2236
2237        Args:
2238            netuid: The network UID from which to get the names.
2239
2240        Returns:
2241            A dictionary mapping key UIDs to their names.
2242
2243        Raises:
2244            QueryError: If the query to the network fails or is invalid.
2245        """
2246
2247        return self.query_map("Name", [netuid], extract_value=extract_value)["Name"]
2248
2249    #  == QUERY FUNCTIONS == #
2250
2251    def get_immunity_period(self, netuid: int = 0) -> int:
2252        """
2253        Queries the network for the immunity period setting.
2254
2255        The immunity period is a time duration during which a module
2256        can not be deregistered from the network.
2257        Fetches the immunity period for a specified network subnet.
2258
2259        Args:
2260            netuid: The network UID for which to query the immunity period.
2261
2262        Returns:
2263            The immunity period setting for the specified network subnet.
2264
2265        Raises:
2266            QueryError: If the query to the network fails or is invalid.
2267        """
2268
2269        return self.query(
2270            "ImmunityPeriod",
2271            params=[netuid],
2272        )
2273
2274    def get_max_set_weights_per_epoch(self):
2275        return self.query("MaximumSetWeightCallsPerEpoch")
2276
2277    def get_min_allowed_weights(self, netuid: int = 0) -> int:
2278        """
2279        Queries the network for the minimum allowed weights setting.
2280
2281        Retrieves the minimum weight values that are possible to set
2282        by a validator within a specific network subnet.
2283
2284        Args:
2285            netuid: The network UID for which to query the minimum allowed
2286              weights.
2287
2288        Returns:
2289            The minimum allowed weight values for the specified network
2290              subnet.
2291
2292        Raises:
2293            QueryError: If the query to the network fails or is invalid.
2294        """
2295
2296        return self.query(
2297            "MinAllowedWeights",
2298            params=[netuid],
2299        )
2300
2301    def get_dao_treasury_address(self) -> Ss58Address:
2302        return self.query("DaoTreasuryAddress", module="GovernanceModule")
2303
2304    def get_max_allowed_weights(self, netuid: int = 0) -> int:
2305        """
2306        Queries the network for the maximum allowed weights setting.
2307
2308        Retrieves the maximum weight values that are possible to set
2309        by a validator within a specific network subnet.
2310
2311        Args:
2312            netuid: The network UID for which to query the maximum allowed
2313              weights.
2314
2315        Returns:
2316            The maximum allowed weight values for the specified network
2317              subnet.
2318
2319        Raises:
2320            QueryError: If the query to the network fails or is invalid.
2321        """
2322
2323        return self.query("MaxAllowedWeights", params=[netuid])
2324
2325    def get_max_allowed_uids(self, netuid: int = 0) -> int:
2326        """
2327        Queries the network for the maximum allowed UIDs setting.
2328
2329        Fetches the upper limit on the number of user IDs that can
2330        be allocated or used within a specific network subnet.
2331
2332        Args:
2333            netuid: The network UID for which to query the maximum allowed UIDs.
2334
2335        Returns:
2336            The maximum number of allowed UIDs for the specified network subnet.
2337
2338        Raises:
2339            QueryError: If the query to the network fails or is invalid.
2340        """
2341
2342        return self.query("MaxAllowedUids", params=[netuid])
2343
2344    def get_name(self, netuid: int = 0) -> str:
2345        """
2346        Queries the network for the name of a specific subnet.
2347
2348        Args:
2349            netuid: The network UID for which to query the name.
2350
2351        Returns:
2352            The name of the specified network subnet.
2353
2354        Raises:
2355            QueryError: If the query to the network fails or is invalid.
2356        """
2357
2358        return self.query("Name", params=[netuid])
2359
2360    def get_subnet_name(self, netuid: int = 0) -> str:
2361        """
2362        Queries the network for the name of a specific subnet.
2363
2364        Args:
2365            netuid: The network UID for which to query the name.
2366
2367        Returns:
2368            The name of the specified network subnet.
2369
2370        Raises:
2371            QueryError: If the query to the network fails or is invalid.
2372        """
2373
2374        return self.query("SubnetNames", params=[netuid])
2375
2376    def get_global_dao_treasury(self):
2377        return self.query("GlobalDaoTreasury", module="GovernanceModule")
2378
2379    def get_n(self, netuid: int = 0) -> int:
2380        """
2381        Queries the network for the 'N' hyperparameter, which represents how
2382        many modules are on the network.
2383
2384        Args:
2385            netuid: The network UID for which to query the 'N' hyperparameter.
2386
2387        Returns:
2388            The value of the 'N' hyperparameter for the specified network
2389              subnet.
2390
2391        Raises:
2392            QueryError: If the query to the network fails or is invalid.
2393        """
2394
2395        return self.query("N", params=[netuid])
2396
2397    def get_tempo(self, netuid: int = 0) -> int:
2398        """
2399        Queries the network for the tempo setting, measured in blocks, for the
2400        specified subnet.
2401
2402        Args:
2403            netuid: The network UID for which to query the tempo.
2404
2405        Returns:
2406            The tempo setting for the specified subnet.
2407
2408        Raises:
2409            QueryError: If the query to the network fails or is invalid.
2410        """
2411
2412        return self.query("Tempo", params=[netuid])
2413
2414    def get_total_stake(self, netuid: int = 0):
2415        """
2416        Queries the network for the total stake amount.
2417
2418        Retrieves the total amount of stake within a specific network subnet.
2419
2420        Args:
2421            netuid: The network UID for which to query the total stake.
2422
2423        Returns:
2424            The total stake amount for the specified network subnet.
2425
2426        Raises:
2427            QueryError: If the query to the network fails or is invalid.
2428        """
2429
2430        return self.query(
2431            "TotalStake",
2432            params=[netuid],
2433        )
2434
2435    def get_registrations_per_block(self):
2436        """
2437        Queries the network for the number of registrations per block.
2438
2439        Fetches the number of registrations that are processed per
2440        block within the network.
2441
2442        Returns:
2443            The number of registrations processed per block.
2444
2445        Raises:
2446            QueryError: If the query to the network fails or is invalid.
2447        """
2448
2449        return self.query(
2450            "RegistrationsPerBlock",
2451        )
2452
2453    def max_registrations_per_block(self, netuid: int = 0):
2454        """
2455        Queries the network for the maximum number of registrations per block.
2456
2457        Retrieves the upper limit of registrations that can be processed in
2458        each block within a specific network subnet.
2459
2460        Args:
2461            netuid: The network UID for which to query.
2462
2463        Returns:
2464            The maximum number of registrations per block for
2465            the specified network subnet.
2466
2467        Raises:
2468            QueryError: If the query to the network fails or is invalid.
2469        """
2470
2471        return self.query(
2472            "MaxRegistrationsPerBlock",
2473            params=[netuid],
2474        )
2475
2476    def get_proposal(self, proposal_id: int = 0):
2477        """
2478        Queries the network for a specific proposal.
2479
2480        Args:
2481            proposal_id: The ID of the proposal to query.
2482
2483        Returns:
2484            The details of the specified proposal.
2485
2486        Raises:
2487            QueryError: If the query to the network fails, is invalid,
2488                or if the proposal ID does not exist.
2489        """
2490
2491        return self.query(
2492            "Proposals",
2493            params=[proposal_id],
2494        )
2495
2496    def get_trust(self, netuid: int = 0):
2497        """
2498        Queries the network for the trust setting of a specific network subnet.
2499
2500        Retrieves the trust level or score, which may represent the
2501        level of trustworthiness or reliability within a
2502        particular network subnet.
2503
2504        Args:
2505            netuid: The network UID for which to query the trust setting.
2506
2507        Returns:
2508            The trust level or score for the specified network subnet.
2509
2510        Raises:
2511            QueryError: If the query to the network fails or is invalid.
2512        """
2513
2514        return self.query(
2515            "Trust",
2516            params=[netuid],
2517        )
2518
2519    def get_uids(self, key: Ss58Address, netuid: int = 0) -> bool | None:
2520        """
2521        Queries the network for module UIDs associated with a specific key.
2522
2523        Args:
2524            key: The key address for which to query UIDs.
2525            netuid: The network UID within which to search for the key.
2526
2527        Returns:
2528            A list of UIDs associated with the specified key.
2529
2530        Raises:
2531            QueryError: If the query to the network fails or is invalid.
2532        """
2533
2534        return self.query(
2535            "Uids",
2536            params=[netuid, key],
2537        )
2538
2539    def get_unit_emission(self) -> int:
2540        """
2541        Queries the network for the unit emission setting.
2542
2543        Retrieves the unit emission value, which represents the
2544        emission rate or quantity for the $COMAI token.
2545
2546        Returns:
2547            The unit emission value in nanos for the network.
2548
2549        Raises:
2550            QueryError: If the query to the network fails or is invalid.
2551        """
2552
2553        return self.query("UnitEmission")
2554
2555    def get_tx_rate_limit(self) -> int:
2556        """
2557        Queries the network for the transaction rate limit.
2558
2559        Retrieves the rate limit for transactions within the network,
2560        which defines the maximum number of transactions that can be
2561        processed within a certain timeframe.
2562
2563        Returns:
2564            The transaction rate limit for the network.
2565
2566        Raises:
2567            QueryError: If the query to the network fails or is invalid.
2568        """
2569
2570        return self.query(
2571            "TxRateLimit",
2572        )
2573
2574    def get_burn_rate(self) -> int:
2575        """
2576        Queries the network for the burn rate setting.
2577
2578        Retrieves the burn rate, which represents the rate at
2579        which the $COMAI token is permanently
2580        removed or 'burned' from circulation.
2581
2582        Returns:
2583            The burn rate for the network.
2584
2585        Raises:
2586            QueryError: If the query to the network fails or is invalid.
2587        """
2588
2589        return self.query(
2590            "BurnRate",
2591            params=[],
2592        )
2593
2594    def get_burn(self, netuid: int = 0) -> int:
2595        """
2596        Queries the network for the burn setting.
2597
2598        Retrieves the burn value, which represents the amount of the
2599        $COMAI token that is 'burned' or permanently removed from
2600        circulation.
2601
2602        Args:
2603            netuid: The network UID for which to query the burn value.
2604
2605        Returns:
2606            The burn value for the specified network subnet.
2607
2608        Raises:
2609            QueryError: If the query to the network fails or is invalid.
2610        """
2611
2612        return self.query("Burn", params=[netuid])
2613
2614    def get_min_burn(self) -> int:
2615        """
2616        Queries the network for the minimum burn setting.
2617
2618        Retrieves the minimum burn value, indicating the lowest
2619        amount of the $COMAI tokens that can be 'burned' or
2620        permanently removed from circulation.
2621
2622        Returns:
2623            The minimum burn value for the network.
2624
2625        Raises:
2626            QueryError: If the query to the network fails or is invalid.
2627        """
2628
2629        return self.query(
2630            "BurnConfig",
2631            params=[],
2632        )["min_burn"]
2633
2634    def get_min_weight_stake(self) -> int:
2635        """
2636        Queries the network for the minimum weight stake setting.
2637
2638        Retrieves the minimum weight stake, which represents the lowest
2639        stake weight that is allowed for certain operations or
2640        transactions within the network.
2641
2642        Returns:
2643            The minimum weight stake for the network.
2644
2645        Raises:
2646            QueryError: If the query to the network fails or is invalid.
2647        """
2648
2649        return self.query("MinWeightStake", params=[])
2650
2651    def get_vote_mode_global(self) -> str:
2652        """
2653        Queries the network for the global vote mode setting.
2654
2655        Retrieves the global vote mode, which defines the overall voting
2656        methodology or approach used across the network in default.
2657
2658        Returns:
2659            The global vote mode setting for the network.
2660
2661        Raises:
2662            QueryError: If the query to the network fails or is invalid.
2663        """
2664
2665        return self.query(
2666            "VoteModeGlobal",
2667        )
2668
2669    def get_max_proposals(self) -> int:
2670        """
2671        Queries the network for the maximum number of proposals allowed.
2672
2673        Retrieves the upper limit on the number of proposals that can be
2674        active or considered at any given time within the network.
2675
2676        Returns:
2677            The maximum number of proposals allowed on the network.
2678
2679        Raises:
2680            QueryError: If the query to the network fails or is invalid.
2681        """
2682
2683        return self.query(
2684            "MaxProposals",
2685        )
2686
2687    def get_max_registrations_per_block(self) -> int:
2688        """
2689        Queries the network for the maximum number of registrations per block.
2690
2691        Retrieves the maximum number of registrations that can
2692        be processed in each block within the network.
2693
2694        Returns:
2695            The maximum number of registrations per block on the network.
2696
2697        Raises:
2698            QueryError: If the query to the network fails or is invalid.
2699        """
2700
2701        return self.query(
2702            "MaxRegistrationsPerBlock",
2703            params=[],
2704        )
2705
2706    def get_max_name_length(self) -> int:
2707        """
2708        Queries the network for the maximum length allowed for names.
2709
2710        Retrieves the maximum character length permitted for names
2711        within the network. Such as the module names
2712
2713        Returns:
2714            The maximum length allowed for names on the network.
2715
2716        Raises:
2717            QueryError: If the query to the network fails or is invalid.
2718        """
2719
2720        return self.query(
2721            "MaxNameLength",
2722            params=[],
2723        )
2724
2725    def get_global_vote_threshold(self) -> int:
2726        """
2727        Queries the network for the global vote threshold.
2728
2729        Retrieves the global vote threshold, which is the critical value or
2730        percentage required for decisions in the network's governance process.
2731
2732        Returns:
2733            The global vote threshold for the network.
2734
2735        Raises:
2736            QueryError: If the query to the network fails or is invalid.
2737        """
2738
2739        return self.query(
2740            "GlobalVoteThreshold",
2741        )
2742
2743    def get_max_allowed_subnets(self) -> int:
2744        """
2745        Queries the network for the maximum number of allowed subnets.
2746
2747        Retrieves the upper limit on the number of subnets that can
2748        be created or operated within the network.
2749
2750        Returns:
2751            The maximum number of allowed subnets on the network.
2752
2753        Raises:
2754            QueryError: If the query to the network fails or is invalid.
2755        """
2756
2757        return self.query(
2758            "MaxAllowedSubnets",
2759            params=[],
2760        )
2761
2762    def get_max_allowed_modules(self) -> int:
2763        """
2764        Queries the network for the maximum number of allowed modules.
2765
2766        Retrieves the upper limit on the number of modules that
2767        can be registered within the network.
2768
2769        Returns:
2770            The maximum number of allowed modules on the network.
2771
2772        Raises:
2773            QueryError: If the query to the network fails or is invalid.
2774        """
2775
2776        return self.query(
2777            "MaxAllowedModules",
2778            params=[],
2779        )
2780
2781    def get_min_stake(self, netuid: int = 0) -> int:
2782        """
2783        Queries the network for the minimum stake required to register a key.
2784
2785        Retrieves the minimum amount of stake necessary for
2786        registering a key within a specific network subnet.
2787
2788        Args:
2789            netuid: The network UID for which to query the minimum stake.
2790
2791        Returns:
2792            The minimum stake required for key registration in nanos.
2793
2794        Raises:
2795            QueryError: If the query to the network fails or is invalid.
2796        """
2797
2798        return self.query("MinStake", params=[netuid])
2799
2800    def get_stake(
2801        self,
2802        key: Ss58Address,
2803        netuid: int = 0,
2804    ) -> int:
2805        """
2806        Queries the network for the stake delegated with a specific key.
2807
2808        Retrieves the amount of total staked tokens
2809        delegated a specific key address
2810
2811        Args:
2812            key: The address of the key to query the stake for.
2813            netuid: The network UID from which to get the query.
2814
2815        Returns:
2816            The amount of stake held by the specified key in nanos.
2817
2818        Raises:
2819            QueryError: If the query to the network fails or is invalid.
2820        """
2821
2822        return self.query(
2823            "Stake",
2824            params=[netuid, key],
2825        )
2826
2827    def get_stakefrom(
2828        self,
2829        key_addr: Ss58Address,
2830        netuid: int = 0,
2831    ) -> dict[str, int]:
2832        """
2833        Retrieves a list of keys from which a specific key address is staked.
2834
2835        Queries the network for all the stakes received by a
2836        particular key from different sources.
2837
2838        Args:
2839            key_addr: The address of the key to query stakes from.
2840
2841            netuid: The network UID from which to get the query.
2842
2843        Returns:
2844            A dictionary mapping key addresses to the amount of stake
2845            received from each.
2846
2847        Raises:
2848            QueryError: If the query to the network fails or is invalid.
2849        """
2850        result = self.query("StakeFrom", [netuid, key_addr])
2851
2852        return {k: v for k, v in result}
2853
2854    def get_staketo(
2855        self,
2856        key_addr: Ss58Address,
2857        netuid: int = 0,
2858    ) -> dict[str, int]:
2859        """
2860        Retrieves a list of keys to which a specific key address stakes to.
2861
2862        Queries the network for all the stakes made by a particular key to
2863        different destinations.
2864
2865        Args:
2866            key_addr: The address of the key to query stakes to.
2867
2868            netuid: The network UID from which to get the query.
2869
2870        Returns:
2871            A dictionary mapping key addresses to the
2872            amount of stake given to each.
2873
2874        Raises:
2875            QueryError: If the query to the network fails or is invalid.
2876        """
2877
2878        result = self.query("StakeTo", [netuid, key_addr])
2879
2880        return {k: v for k, v in result}
2881
2882    def get_balance(
2883        self,
2884        addr: Ss58Address,
2885    ) -> int:
2886        """
2887        Retrieves the balance of a specific key.
2888
2889        Args:
2890            addr: The address of the key to query the balance for.
2891
2892        Returns:
2893            The balance of the specified key.
2894
2895        Raises:
2896            QueryError: If the query to the network fails or is invalid.
2897        """
2898
2899        result = self.query("Account", module="System", params=[addr])
2900
2901        return result["data"]["free"]
2902
2903    def get_block(self, block_hash: str | None = None) -> dict[Any, Any] | None:
2904        """
2905        Retrieves information about a specific block in the network.
2906
2907        Queries the network for details about a block, such as its number,
2908        hash, and other relevant information.
2909
2910        Returns:
2911            The requested information about the block,
2912            or None if the block does not exist
2913            or the information is not available.
2914
2915        Raises:
2916            QueryError: If the query to the network fails or is invalid.
2917        """
2918
2919        with self.get_conn() as substrate:
2920            block: dict[Any, Any] | None = substrate.get_block(  # type: ignore
2921                block_hash  # type: ignore
2922            )
2923
2924        return block
2925
2926    def get_existential_deposit(self, block_hash: str | None = None) -> int:
2927        """
2928        Retrieves the existential deposit value for the network.
2929
2930        The existential deposit is the minimum balance that must be maintained
2931        in an account to prevent it from being purged. Denotated in nano units.
2932
2933        Returns:
2934            The existential deposit value in nano units.
2935        Note:
2936            The value returned is a fixed value defined in the
2937            client and may not reflect changes in the network's configuration.
2938        """
2939
2940        with self.get_conn() as substrate:
2941            result: int = substrate.get_constant(  #  type: ignore
2942                "Balances", "ExistentialDeposit", block_hash
2943            ).value  #  type: ignore
2944
2945        return result
2946
2947    def get_voting_power_delegators(self) -> list[Ss58Address]:
2948        result = self.query("NotDelegatingVotingPower", [], module="GovernanceModule")
2949        return result
2950
2951    def add_transfer_dao_treasury_proposal(
2952        self,
2953        key: Keypair,
2954        data: str,
2955        amount_nano: int,
2956        dest: Ss58Address,
2957    ):
2958        params = {"dest": dest, "value": amount_nano, "data": data}
2959
2960        return self.compose_call(
2961            module="GovernanceModule",
2962            fn="add_transfer_dao_treasury_proposal",
2963            params=params,
2964            key=key,
2965        )
MAX_REQUEST_SIZE = 9000000
@dataclass
class Chunk:
23@dataclass
24class Chunk:
25    batch_requests: list[tuple[Any, Any]]
26    prefix_list: list[list[str]]
27    fun_params: list[tuple[Any, Any, Any, Any, str]]
Chunk( batch_requests: list[tuple[typing.Any, typing.Any]], prefix_list: list[list[str]], fun_params: list[tuple[typing.Any, typing.Any, typing.Any, typing.Any, str]])
batch_requests: list[tuple[typing.Any, typing.Any]]
prefix_list: list[list[str]]
fun_params: list[tuple[typing.Any, typing.Any, typing.Any, typing.Any, str]]
class CommuneClient:
  34class CommuneClient:
  35    """
  36    A client for interacting with Commune network nodes, querying storage,
  37    submitting transactions, etc.
  38
  39    Attributes:
  40        wait_for_finalization: Whether to wait for transaction finalization.
  41
  42    Example:
  43    ```py
  44    client = CommuneClient()
  45    client.query(name='function_name', params=['param1', 'param2'])
  46    ```
  47
  48    Raises:
  49        AssertionError: If the maximum connections value is less than or equal
  50          to zero.
  51    """
  52
  53    wait_for_finalization: bool
  54    _num_connections: int
  55    _connection_queue: queue.Queue[SubstrateInterface]
  56    url: str
  57
  58    def __init__(
  59        self,
  60        url: str,
  61        num_connections: int = 1,
  62        wait_for_finalization: bool = False,
  63    ):
  64        """
  65        Args:
  66            url: The URL of the network node to connect to.
  67            num_connections: The number of websocket connections to be opened.
  68        """
  69        assert num_connections > 0
  70        self._num_connections = num_connections
  71        self.wait_for_finalization = wait_for_finalization
  72        self._connection_queue = queue.Queue(num_connections)
  73        self.url = url
  74
  75        for _ in range(num_connections):
  76            self._connection_queue.put(SubstrateInterface(url))
  77
  78    @property
  79    def connections(self) -> int:
  80        """
  81        Gets the maximum allowed number of simultaneous connections to the
  82        network node.
  83        """
  84        return self._num_connections
  85
  86    @contextmanager
  87    def get_conn(self, timeout: float | None = None, init: bool = False):
  88        """
  89        Context manager to get a connection from the pool.
  90
  91        Tries to get a connection from the pool queue. If the queue is empty,
  92        it blocks for `timeout` seconds until a connection is available. If
  93        `timeout` is None, it blocks indefinitely.
  94
  95        Args:
  96            timeout: The maximum time in seconds to wait for a connection.
  97
  98        Yields:
  99            The connection object from the pool.
 100
 101        Raises:
 102            QueueEmptyError: If no connection is available within the timeout
 103              period.
 104        """
 105        conn = self._connection_queue.get(timeout=timeout)
 106        if init:
 107            conn.init_runtime()  # type: ignore
 108        try:
 109            yield conn
 110        finally:
 111            self._connection_queue.put(conn)
 112
 113    def _get_storage_keys(
 114        self,
 115        storage: str,
 116        queries: list[tuple[str, list[Any]]],
 117        block_hash: str | None,
 118    ):
 119
 120        send: list[tuple[str, list[Any]]] = []
 121        prefix_list: list[Any] = []
 122
 123        key_idx = 0
 124        with self.get_conn(init=True) as substrate:
 125            for function, params in queries:
 126                storage_key = StorageKey.create_from_storage_function(  # type: ignore
 127                    storage, function, params, runtime_config=substrate.runtime_config, metadata=substrate.metadata  # type: ignore
 128                )
 129
 130                prefix = storage_key.to_hex()
 131                prefix_list.append(prefix)
 132                send.append(("state_getKeys", [prefix, block_hash]))
 133                key_idx += 1
 134        return send, prefix_list
 135
 136    def _get_lists(
 137        self,
 138        storage_module: str,
 139        queries: list[tuple[str, list[Any]]],
 140        substrate: SubstrateInterface,
 141    ) -> list[tuple[Any, Any, Any, Any, str]]:
 142        """
 143        Generates a list of tuples containing parameters for each storage function based on the given functions and substrate interface.
 144
 145        Args:
 146            functions (dict[str, list[query_call]]): A dictionary where keys are storage module names and values are lists of tuples.
 147                Each tuple consists of a storage function name and its parameters.
 148            substrate: An instance of the SubstrateInterface class used to interact with the substrate.
 149
 150        Returns:
 151            A list of tuples in the format `(value_type, param_types, key_hashers, params, storage_function)` for each storage function in the given functions.
 152
 153        Example:
 154            >>> _get_lists(
 155                    functions={'storage_module': [('storage_function', ['param1', 'param2'])]},
 156                    substrate=substrate_instance
 157                )
 158            [('value_type', 'param_types', 'key_hashers', ['param1', 'param2'], 'storage_function'), ...]
 159        """
 160
 161        function_parameters: list[tuple[Any, Any, Any, Any, str]] = []
 162
 163        metadata_pallet = substrate.metadata.get_metadata_pallet(  # type: ignore
 164            storage_module
 165        )
 166        for storage_function, params in queries:
 167            storage_item = metadata_pallet.get_storage_function(  # type: ignore
 168                storage_function
 169            )
 170
 171            value_type = storage_item.get_value_type_string()  # type: ignore
 172            param_types = storage_item.get_params_type_string()  # type: ignore
 173            key_hashers = storage_item.get_param_hashers()  # type: ignore
 174            function_parameters.append(
 175                (
 176                    value_type,
 177                    param_types,
 178                    key_hashers,
 179                    params,
 180                    storage_function,
 181                )  # type: ignore
 182            )
 183        return function_parameters
 184
 185    def _send_batch(
 186        self,
 187        batch_payload: list[Any],
 188        request_ids: list[int],
 189        extract_result: bool = True,
 190    ):
 191        """
 192        Sends a batch of requests to the substrate and collects the results.
 193
 194        Args:
 195            substrate: An instance of the substrate interface.
 196            batch_payload: The payload of the batch request.
 197            request_ids: A list of request IDs for tracking responses.
 198            results: A list to store the results of the requests.
 199            extract_result: Whether to extract the result from the response.
 200
 201        Raises:
 202            NetworkQueryError: If there is an `error` in the response message.
 203
 204        Note:
 205            No explicit return value as results are appended to the provided 'results' list.
 206        """
 207        results: list[str | dict[Any, Any]] = []
 208        with self.get_conn(init=True) as substrate:
 209            try:
 210
 211                substrate.websocket.send(  #  type: ignore
 212                    json.dumps(batch_payload)
 213                )  # type: ignore
 214            except NetworkQueryError:
 215                pass
 216            while len(results) < len(request_ids):
 217                received_messages = json.loads(
 218                    substrate.websocket.recv()  # type: ignore
 219                )  # type: ignore
 220                if isinstance(received_messages, dict):
 221                    received_messages: list[dict[Any, Any]] = [received_messages]
 222
 223                for message in received_messages:
 224                    if message.get("id") in request_ids:
 225                        if extract_result:
 226                            try:
 227                                results.append(message["result"])
 228                            except Exception:
 229                                raise (
 230                                    RuntimeError(
 231                                        f"Error extracting result from message: {message}"
 232                                    )
 233                                )
 234                        else:
 235                            results.append(message)
 236                    if "error" in message:
 237                        raise NetworkQueryError(message["error"])
 238
 239            return results
 240
 241    def _make_request_smaller(
 242        self,
 243        batch_request: list[tuple[T1, T2]],
 244        prefix_list: list[list[str]],
 245        fun_params: list[tuple[Any, Any, Any, Any, str]],
 246    ) -> tuple[list[list[tuple[T1, T2]]], list[Chunk]]:
 247        """
 248        Splits a batch of requests into smaller batches, each not exceeding the specified maximum size.
 249
 250        Args:
 251            batch_request: A list of requests to be sent in a batch.
 252            max_size: Maximum size of each batch in bytes.
 253
 254        Returns:
 255            A list of smaller request batches.
 256
 257        Example:
 258            >>> _make_request_smaller(batch_request=[('method1', 'params1'), ('method2', 'params2')], max_size=1000)
 259            [[('method1', 'params1')], [('method2', 'params2')]]
 260        """
 261        assert len(prefix_list) == len(fun_params) == len(batch_request)
 262
 263        def estimate_size(request: tuple[T1, T2]):
 264            """Convert the batch request to a string and measure its length"""
 265            return len(json.dumps(request))
 266
 267        # Initialize variables
 268        result: list[list[tuple[T1, T2]]] = []
 269        current_batch = []
 270        current_prefix_batch = []
 271        current_params_batch = []
 272        current_size = 0
 273
 274        chunk_list: list[Chunk] = []
 275
 276        # Iterate through each request in the batch
 277        for request, prefix, params in zip(batch_request, prefix_list, fun_params):
 278            request_size = estimate_size(request)
 279
 280            # Check if adding this request exceeds the max size
 281            if current_size + request_size > MAX_REQUEST_SIZE:
 282                # If so, start a new batch
 283
 284                # Essentiatly checks that it's not the first iteration
 285                if current_batch:
 286                    chunk = Chunk(
 287                        current_batch, current_prefix_batch, current_params_batch
 288                    )
 289                    chunk_list.append(chunk)
 290                    result.append(current_batch)
 291
 292                current_batch = [request]
 293                current_prefix_batch = [prefix]
 294                current_params_batch = [params]
 295                current_size = request_size
 296            else:
 297                # Otherwise, add to the current batch
 298                current_batch.append(request)
 299                current_size += request_size
 300                current_prefix_batch.append(prefix)
 301                current_params_batch.append(params)
 302
 303        # Add the last batch if it's not empty
 304        if current_batch:
 305            result.append(current_batch)
 306            chunk = Chunk(current_batch, current_prefix_batch, current_params_batch)
 307            chunk_list.append(chunk)
 308
 309        return result, chunk_list
 310
 311    def _are_changes_equal(self, change_a: Any, change_b: Any):
 312        for (a, b), (c, d) in zip(change_a, change_b):
 313            if a != c or b != d:
 314                return False
 315
 316    def _rpc_request_batch(
 317        self, batch_requests: list[tuple[str, list[Any]]], extract_result: bool = True
 318    ) -> list[str]:
 319        """
 320        Sends batch requests to the substrate node using multiple threads and collects the results.
 321
 322        Args:
 323            substrate: An instance of the substrate interface.
 324            batch_requests : A list of requests to be sent in batches.
 325            max_size: Maximum size of each batch in bytes.
 326            extract_result: Whether to extract the result from the response message.
 327
 328        Returns:
 329            A list of results from the batch requests.
 330
 331        Example:
 332            >>> _rpc_request_batch(substrate_instance, [('method1', ['param1']), ('method2', ['param2'])])
 333            ['result1', 'result2', ...]
 334        """
 335
 336        chunk_results: list[Any] = []
 337        # smaller_requests = self._make_request_smaller(batch_requests)
 338        request_id = 0
 339        with ThreadPoolExecutor() as executor:
 340            futures: list[Future[list[str | dict[Any, Any]]]] = []
 341            for chunk in [batch_requests]:
 342                request_ids: list[int] = []
 343                batch_payload: list[Any] = []
 344                for method, params in chunk:
 345                    request_id += 1
 346                    request_ids.append(request_id)
 347                    batch_payload.append(
 348                        {
 349                            "jsonrpc": "2.0",
 350                            "method": method,
 351                            "params": params,
 352                            "id": request_id,
 353                        }
 354                    )
 355
 356                futures.append(
 357                    executor.submit(
 358                        self._send_batch,
 359                        batch_payload=batch_payload,
 360                        request_ids=request_ids,
 361                        extract_result=extract_result,
 362                    )
 363                )
 364            for future in futures:
 365                resul = future.result()
 366                chunk_results.append(resul)
 367        return chunk_results
 368
 369    def _rpc_request_batch_chunked(
 370        self, chunk_requests: list[Chunk], extract_result: bool = True
 371    ):
 372        """
 373        Sends batch requests to the substrate node using multiple threads and collects the results.
 374
 375        Args:
 376            substrate: An instance of the substrate interface.
 377            batch_requests : A list of requests to be sent in batches.
 378            max_size: Maximum size of each batch in bytes.
 379            extract_result: Whether to extract the result from the response message.
 380
 381        Returns:
 382            A list of results from the batch requests.
 383
 384        Example:
 385            >>> _rpc_request_batch(substrate_instance, [('method1', ['param1']), ('method2', ['param2'])])
 386            ['result1', 'result2', ...]
 387        """
 388
 389        def split_chunks(chunk: Chunk, chunk_info: list[Chunk], chunk_info_idx: int):
 390            manhattam_chunks: list[tuple[Any, Any]] = []
 391            mutaded_chunk_info = deepcopy(chunk_info)
 392            max_n_keys = 35000
 393            for query in chunk.batch_requests:
 394                result_keys = query[1][0]
 395                keys_amount = len(result_keys)
 396                if keys_amount > max_n_keys:
 397                    mutaded_chunk_info.pop(chunk_info_idx)
 398                    for i in range(0, keys_amount, max_n_keys):
 399                        new_chunk = deepcopy(chunk)
 400                        splitted_keys = result_keys[i : i + max_n_keys]
 401                        splitted_query = deepcopy(query)
 402                        splitted_query[1][0] = splitted_keys
 403                        new_chunk.batch_requests = [splitted_query]
 404                        manhattam_chunks.append(splitted_query)
 405                        mutaded_chunk_info.insert(chunk_info_idx, new_chunk)
 406                else:
 407                    manhattam_chunks.append(query)
 408            return manhattam_chunks, mutaded_chunk_info
 409
 410        assert len(chunk_requests) > 0
 411        mutated_chunk_info: list[Chunk] = []
 412        chunk_results: list[Any] = []
 413        # smaller_requests = self._make_request_smaller(batch_requests)
 414        request_id = 0
 415
 416        with ThreadPoolExecutor() as executor:
 417            futures: list[Future[list[str | dict[Any, Any]]]] = []
 418            for idx, macro_chunk in enumerate(chunk_requests):
 419                _, mutated_chunk_info = split_chunks(macro_chunk, chunk_requests, idx)
 420            for chunk in mutated_chunk_info:
 421                request_ids: list[int] = []
 422                batch_payload: list[Any] = []
 423                for method, params in chunk.batch_requests:
 424                    # for method, params in micro_chunk:
 425                    request_id += 1
 426                    request_ids.append(request_id)
 427                    batch_payload.append(
 428                        {
 429                            "jsonrpc": "2.0",
 430                            "method": method,
 431                            "params": params,
 432                            "id": request_id,
 433                        }
 434                    )
 435                futures.append(
 436                    executor.submit(
 437                        self._send_batch,
 438                        batch_payload=batch_payload,
 439                        request_ids=request_ids,
 440                        extract_result=extract_result,
 441                    )
 442                )
 443            for future in futures:
 444                resul = future.result()
 445                chunk_results.append(resul)
 446        return chunk_results, mutated_chunk_info
 447
 448    def _decode_response(
 449        self,
 450        response: list[str],
 451        function_parameters: list[tuple[Any, Any, Any, Any, str]],
 452        prefix_list: list[Any],
 453        block_hash: str,
 454    ) -> dict[str, dict[Any, Any]]:
 455        """
 456        Decodes a response from the substrate interface and organizes the data into a dictionary.
 457
 458        Args:
 459            response: A list of encoded responses from a substrate query.
 460            function_parameters: A list of tuples containing the parameters for each storage function.
 461            last_keys: A list of the last keys used in the substrate query.
 462            prefix_list: A list of prefixes used in the substrate query.
 463            substrate: An instance of the SubstrateInterface class.
 464            block_hash: The hash of the block to be queried.
 465
 466        Returns:
 467            A dictionary where each key is a storage function name and the value is another dictionary.
 468            This inner dictionary's key is the decoded key from the response and the value is the corresponding decoded value.
 469
 470        Raises:
 471            ValueError: If an unsupported hash type is encountered in the `concat_hash_len` function.
 472
 473        Example:
 474            >>> _decode_response(
 475                    response=[...],
 476                    function_parameters=[...],
 477                    last_keys=[...],
 478                    prefix_list=[...],
 479                    substrate=substrate_instance,
 480                    block_hash="0x123..."
 481                )
 482            {'storage_function_name': {decoded_key: decoded_value, ...}, ...}
 483        """
 484
 485        def concat_hash_len(key_hasher: str) -> int:
 486            """
 487            Determines the length of the hash based on the given key hasher type.
 488
 489            Args:
 490                key_hasher: The type of key hasher.
 491
 492            Returns:
 493                The length of the hash corresponding to the given key hasher type.
 494
 495            Raises:
 496                ValueError: If the key hasher type is not supported.
 497
 498            Example:
 499                >>> concat_hash_len("Blake2_128Concat")
 500                16
 501            """
 502
 503            if key_hasher == "Blake2_128Concat":
 504                return 16
 505            elif key_hasher == "Twox64Concat":
 506                return 8
 507            elif key_hasher == "Identity":
 508                return 0
 509            else:
 510                raise ValueError("Unsupported hash type")
 511
 512        assert len(response) == len(function_parameters) == len(prefix_list)
 513        result_dict: dict[str, dict[Any, Any]] = {}
 514        for res, fun_params_tuple, prefix in zip(
 515            response, function_parameters, prefix_list
 516        ):
 517            if not res:
 518                continue
 519            res = res[0]
 520            changes = res["changes"]  # type: ignore
 521            value_type, param_types, key_hashers, params, storage_function = (
 522                fun_params_tuple
 523            )
 524            with self.get_conn(init=True) as substrate:
 525                for item in changes:
 526                    # Determine type string
 527                    key_type_string: list[Any] = []
 528                    for n in range(len(params), len(param_types)):
 529                        key_type_string.append(
 530                            f"[u8; {concat_hash_len(key_hashers[n])}]"
 531                        )
 532                        key_type_string.append(param_types[n])
 533
 534                    item_key_obj = substrate.decode_scale(  # type: ignore
 535                        type_string=f"({', '.join(key_type_string)})",
 536                        scale_bytes="0x" + item[0][len(prefix) :],
 537                        return_scale_obj=True,
 538                        block_hash=block_hash,
 539                    )
 540                    # strip key_hashers to use as item key
 541                    if len(param_types) - len(params) == 1:
 542                        item_key = item_key_obj.value_object[1]  # type: ignore
 543                    else:
 544                        item_key = tuple(  # type: ignore
 545                            item_key_obj.value_object[key + 1]  # type: ignore
 546                            for key in range(  # type: ignore
 547                                len(params), len(param_types) + 1, 2
 548                            )
 549                        )
 550
 551                    item_value = substrate.decode_scale(  # type: ignore
 552                        type_string=value_type,
 553                        scale_bytes=item[1],
 554                        return_scale_obj=True,
 555                        block_hash=block_hash,
 556                    )
 557                    result_dict.setdefault(storage_function, {})
 558
 559                    result_dict[storage_function][item_key.value] = item_value.value  # type: ignore
 560
 561        return result_dict
 562
 563    def query_batch(
 564        self, functions: dict[str, list[tuple[str, list[Any]]]]
 565    ) -> dict[str, str]:
 566        """
 567        Executes batch queries on a substrate and returns results in a dictionary format.
 568
 569        Args:
 570            substrate: An instance of SubstrateInterface to interact with the substrate.
 571            functions (dict[str, list[query_call]]): A dictionary mapping module names to lists of query calls (function name and parameters).
 572
 573        Returns:
 574            A dictionary where keys are storage function names and values are the query results.
 575
 576        Raises:
 577            Exception: If no result is found from the batch queries.
 578
 579        Example:
 580            >>> query_batch(substrate_instance, {'module_name': [('function_name', ['param1', 'param2'])]})
 581            {'function_name': 'query_result', ...}
 582        """
 583
 584        result: dict[str, str] = {}
 585        if not functions:
 586            raise Exception("No result")
 587        with self.get_conn(init=True) as substrate:
 588            for module, queries in functions.items():
 589                storage_keys: list[Any] = []
 590                for fn, params in queries:
 591                    storage_function = substrate.create_storage_key(  # type: ignore
 592                        pallet=module, storage_function=fn, params=params
 593                    )
 594                    storage_keys.append(storage_function)
 595
 596                block_hash = substrate.get_block_hash()
 597                responses: list[Any] = substrate.query_multi(  # type: ignore
 598                    storage_keys=storage_keys, block_hash=block_hash
 599                )
 600
 601                for item in responses:
 602                    fun = item[0]
 603                    query = item[1]
 604                    storage_fun = fun.storage_function
 605                    result[storage_fun] = query.value
 606
 607        return result
 608
 609    def query_batch_map(
 610        self,
 611        functions: dict[str, list[tuple[str, list[Any]]]],
 612        block_hash: str | None = None,
 613    ) -> dict[str, dict[Any, Any]]:
 614        """
 615        Queries multiple storage functions using a map batch approach and returns the combined result.
 616
 617        Args:
 618            substrate: An instance of SubstrateInterface for substrate interaction.
 619            functions (dict[str, list[query_call]]): A dictionary mapping module names to lists of query calls.
 620
 621        Returns:
 622            The combined result of the map batch query.
 623
 624        Example:
 625            >>> query_batch_map(substrate_instance, {'module_name': [('function_name', ['param1', 'param2'])]})
 626            # Returns the combined result of the map batch query
 627        """
 628        multi_result: dict[str, dict[Any, Any]] = {}
 629
 630        def recursive_update(
 631            d: dict[str, dict[T1, T2] | dict[str, Any]],
 632            u: Mapping[str, dict[Any, Any] | str],
 633        ) -> dict[str, dict[T1, T2]]:
 634            for k, v in u.items():
 635                if isinstance(v, dict):
 636                    d[k] = recursive_update(d.get(k, {}), v)  # type: ignore
 637                else:
 638                    d[k] = v  # type: ignore
 639            return d  # type: ignore
 640
 641        def get_page():
 642            send, prefix_list = self._get_storage_keys(storage, queries, block_hash)
 643            with self.get_conn(init=True) as substrate:
 644                function_parameters = self._get_lists(storage, queries, substrate)
 645            responses = self._rpc_request_batch(send)
 646            # assumption because send is just the storage_function keys
 647            # so it should always be really small regardless of the amount of queries
 648            assert len(responses) == 1
 649            res = responses[0]
 650            built_payload: list[tuple[str, list[Any]]] = []
 651            for result_keys in res:
 652                built_payload.append(
 653                    ("state_queryStorageAt", [result_keys, block_hash])
 654                )
 655            _, chunks_info = self._make_request_smaller(
 656                built_payload, prefix_list, function_parameters
 657            )
 658            chunks_response, chunks_info = self._rpc_request_batch_chunked(chunks_info)
 659            return chunks_response, chunks_info
 660
 661        if not block_hash:
 662            with self.get_conn(init=True) as substrate:
 663                block_hash = substrate.get_block_hash()
 664        for storage, queries in functions.items():
 665            chunks, chunks_info = get_page()
 666            # if this doesn't happen something is wrong on the code
 667            # and we won't be able to decode the data properly
 668            assert len(chunks) == len(chunks_info)
 669            for chunk_info, response in zip(chunks_info, chunks):
 670                storage_result = self._decode_response(
 671                    response, chunk_info.fun_params, chunk_info.prefix_list, block_hash
 672                )
 673                multi_result = recursive_update(multi_result, storage_result)
 674
 675        return multi_result
 676
 677    def query(
 678        self,
 679        name: str,
 680        params: list[Any] = [],
 681        module: str = "SubspaceModule",
 682    ) -> Any:
 683        """
 684        Queries a storage function on the network.
 685
 686        Sends a query to the network and retrieves data from a
 687        specified storage function.
 688
 689        Args:
 690            name: The name of the storage function to query.
 691            params: The parameters to pass to the storage function.
 692            module: The module where the storage function is located.
 693
 694        Returns:
 695            The result of the query from the network.
 696
 697        Raises:
 698            NetworkQueryError: If the query fails or is invalid.
 699        """
 700
 701        result = self.query_batch({module: [(name, params)]})
 702
 703        return result[name]
 704
 705    def query_map(
 706        self,
 707        name: str,
 708        params: list[Any] = [],
 709        module: str = "SubspaceModule",
 710        extract_value: bool = True,
 711    ) -> dict[Any, Any]:
 712        """
 713        Queries a storage map from a network node.
 714
 715        Args:
 716            name: The name of the storage map to query.
 717            params: A list of parameters for the query.
 718            module: The module in which the storage map is located.
 719
 720        Returns:
 721            A dictionary representing the key-value pairs
 722              retrieved from the storage map.
 723
 724        Raises:
 725            QueryError: If the query to the network fails or is invalid.
 726        """
 727
 728        result = self.query_batch_map({module: [(name, params)]})
 729
 730        if extract_value:
 731            return {k.value: v.value for k, v in result}  # type: ignore
 732
 733        return result
 734
 735    def compose_call(
 736        self,
 737        fn: str,
 738        params: dict[str, Any],
 739        key: Keypair | None,
 740        module: str = "SubspaceModule",
 741        wait_for_inclusion: bool = True,
 742        wait_for_finalization: bool | None = None,
 743        sudo: bool = False,
 744        unsigned: bool = False,
 745    ) -> ExtrinsicReceipt:
 746        """
 747        Composes and submits a call to the network node.
 748
 749        Composes and signs a call with the provided keypair, and submits it to
 750        the network. The call can be a standard extrinsic or a sudo extrinsic if
 751        elevated permissions are required. The method can optionally wait for
 752        the call's inclusion in a block and/or its finalization.
 753
 754        Args:
 755            fn: The function name to call on the network.
 756            params: A dictionary of parameters for the call.
 757            key: The keypair for signing the extrinsic.
 758            module: The module containing the function.
 759            wait_for_inclusion: Wait for the call's inclusion in a block.
 760            wait_for_finalization: Wait for the transaction's finalization.
 761            sudo: Execute the call as a sudo (superuser) operation.
 762
 763        Returns:
 764            The receipt of the submitted extrinsic, if
 765              `wait_for_inclusion` is True. Otherwise, returns a string
 766              identifier of the extrinsic.
 767
 768        Raises:
 769            ChainTransactionError: If the transaction fails.
 770        """
 771
 772        if key is None and not unsigned:
 773            raise ValueError("Key must be provided for signed extrinsics.")
 774
 775        with self.get_conn() as substrate:
 776            if wait_for_finalization is None:
 777                wait_for_finalization = self.wait_for_finalization
 778
 779            call = substrate.compose_call(  # type: ignore
 780                call_module=module, call_function=fn, call_params=params
 781            )
 782            if sudo:
 783                call = substrate.compose_call(  # type: ignore
 784                    call_module="Sudo",
 785                    call_function="sudo",
 786                    call_params={
 787                        "call": call.value,  # type: ignore
 788                    },
 789                )
 790
 791            if not unsigned:
 792                extrinsic = substrate.create_signed_extrinsic(  # type: ignore
 793                    call=call, keypair=key  # type: ignore
 794                )  # type: ignore
 795            else:
 796                extrinsic = substrate.create_unsigned_extrinsic(call=call)  # type: ignore
 797
 798            response = substrate.submit_extrinsic(
 799                extrinsic=extrinsic,
 800                wait_for_inclusion=wait_for_inclusion,
 801                wait_for_finalization=wait_for_finalization,
 802            )
 803        if wait_for_inclusion:
 804            if not response.is_success:
 805                raise ChainTransactionError(
 806                    response.error_message, response  # type: ignore
 807                )
 808
 809        return response
 810
 811    def compose_call_multisig(
 812        self,
 813        fn: str,
 814        params: dict[str, Any],
 815        key: Keypair,
 816        signatories: list[Ss58Address],
 817        threshold: int,
 818        module: str = "SubspaceModule",
 819        wait_for_inclusion: bool = True,
 820        wait_for_finalization: bool | None = None,
 821        sudo: bool = False,
 822        era: dict[str, int] | None = None,
 823    ) -> ExtrinsicReceipt:
 824        """
 825        Composes and submits a multisignature call to the network node.
 826
 827        This method allows the composition and submission of a call that
 828        requires multiple signatures for execution, known as a multisignature
 829        call. It supports specifying signatories, a threshold of signatures for
 830        the call's execution, and an optional era for the call's mortality. The
 831        call can be a standard extrinsic, a sudo extrinsic for elevated
 832        permissions, or a multisig extrinsic if multiple signatures are
 833        required. Optionally, the method can wait for the call's inclusion in a
 834        block and/or its finalization. Make sure to pass all keys,
 835        that are part of the multisignature.
 836
 837        Args:
 838            fn: The function name to call on the network. params: A dictionary
 839            of parameters for the call. key: The keypair for signing the
 840            extrinsic. signatories: List of SS58 addresses of the signatories.
 841            Include ALL KEYS that are part of the multisig. threshold: The
 842            minimum number of signatories required to execute the extrinsic.
 843            module: The module containing the function to call.
 844            wait_for_inclusion: Whether to wait for the call's inclusion in a
 845            block. wait_for_finalization: Whether to wait for the transaction's
 846            finalization. sudo: Execute the call as a sudo (superuser)
 847            operation. era: Specifies the call's mortality in terms of blocks in
 848            the format
 849                {'period': amount_blocks}. If omitted, the extrinsic is
 850                immortal.
 851
 852        Returns:
 853            The receipt of the submitted extrinsic if `wait_for_inclusion` is
 854            True. Otherwise, returns a string identifier of the extrinsic.
 855
 856        Raises:
 857            ChainTransactionError: If the transaction fails.
 858        """
 859
 860        # getting the call ready
 861        with self.get_conn() as substrate:
 862            if wait_for_finalization is None:
 863                wait_for_finalization = self.wait_for_finalization
 864
 865            # prepares the `GenericCall` object
 866            call = substrate.compose_call(  # type: ignore
 867                call_module=module, call_function=fn, call_params=params
 868            )
 869            if sudo:
 870                call = substrate.compose_call(  # type: ignore
 871                    call_module="Sudo",
 872                    call_function="sudo",
 873                    call_params={
 874                        "call": call.value,  # type: ignore
 875                    },
 876                )
 877
 878            # modify the rpc methods at runtime, to allow for correct payment
 879            # fee calculation parity has a bug in this version,
 880            # where the method has to be removed
 881            rpc_methods = substrate.config.get("rpc_methods")  # type: ignore
 882
 883            if "state_call" in rpc_methods:  # type: ignore
 884                rpc_methods.remove("state_call")  # type: ignore
 885
 886            # create the multisig account
 887            multisig_acc = substrate.generate_multisig_account(  # type: ignore
 888                signatories, threshold
 889            )
 890
 891            # send the multisig extrinsic
 892            extrinsic = substrate.create_multisig_extrinsic(  # type: ignore
 893                call=call,  # type: ignore
 894                keypair=key,
 895                multisig_account=multisig_acc,  # type: ignore
 896                era=era,  # type: ignore
 897            )  # type: ignore
 898
 899            response = substrate.submit_extrinsic(
 900                extrinsic=extrinsic,
 901                wait_for_inclusion=wait_for_inclusion,
 902                wait_for_finalization=wait_for_finalization,
 903            )
 904
 905        if wait_for_inclusion:
 906            if not response.is_success:
 907                raise ChainTransactionError(
 908                    response.error_message, response  # type: ignore
 909                )
 910
 911        return response
 912
 913    def transfer(
 914        self,
 915        key: Keypair,
 916        amount: int,
 917        dest: Ss58Address,
 918    ) -> ExtrinsicReceipt:
 919        """
 920        Transfers a specified amount of tokens from the signer's account to the
 921        specified account.
 922
 923        Args:
 924            key: The keypair associated with the sender's account.
 925            amount: The amount to transfer, in nanotokens.
 926            dest: The SS58 address of the recipient.
 927
 928        Returns:
 929            A receipt of the transaction.
 930
 931        Raises:
 932            InsufficientBalanceError: If the sender's account does not have
 933              enough balance.
 934            ChainTransactionError: If the transaction fails.
 935        """
 936
 937        params = {"dest": dest, "value": amount}
 938
 939        return self.compose_call(
 940            module="Balances", fn="transfer_keep_alive", params=params, key=key
 941        )
 942
 943    def transfer_multiple(
 944        self,
 945        key: Keypair,
 946        destinations: list[Ss58Address],
 947        amounts: list[int],
 948        netuid: str | int = 0,
 949    ) -> ExtrinsicReceipt:
 950        """
 951        Transfers specified amounts of tokens from the signer's account to
 952        multiple target accounts.
 953
 954        The `destinations` and `amounts` lists must be of the same length.
 955
 956        Args:
 957            key: The keypair associated with the sender's account.
 958            destinations: A list of SS58 addresses of the recipients.
 959            amounts: Amount to transfer to each recipient, in nanotokens.
 960            netuid: The network identifier.
 961
 962        Returns:
 963            A receipt of the transaction.
 964
 965        Raises:
 966            InsufficientBalanceError: If the sender's account does not have
 967              enough balance for all transfers.
 968            ChainTransactionError: If the transaction fails.
 969        """
 970
 971        assert len(destinations) == len(amounts)
 972
 973        # extract existential deposit from amounts
 974        existential_deposit = self.get_existential_deposit()
 975        amounts = [a - existential_deposit for a in amounts]
 976
 977        params = {
 978            "netuid": netuid,
 979            "destinations": destinations,
 980            "amounts": amounts,
 981        }
 982
 983        return self.compose_call(
 984            module="SubspaceModule", fn="transfer_multiple", params=params, key=key
 985        )
 986
 987    def stake(
 988        self,
 989        key: Keypair,
 990        amount: int,
 991        dest: Ss58Address,
 992        netuid: int = 0,
 993    ) -> ExtrinsicReceipt:
 994        """
 995        Stakes the specified amount of tokens to a module key address.
 996
 997        Args:
 998            key: The keypair associated with the staker's account.
 999            amount: The amount of tokens to stake, in nanotokens.
1000            dest: The SS58 address of the module key to stake to.
1001            netuid: The network identifier.
1002
1003        Returns:
1004            A receipt of the staking transaction.
1005
1006        Raises:
1007            InsufficientBalanceError: If the staker's account does not have
1008              enough balance.
1009            ChainTransactionError: If the transaction fails.
1010        """
1011
1012        params = {"amount": amount, "netuid": netuid, "module_key": dest}
1013
1014        return self.compose_call(fn="add_stake", params=params, key=key)
1015
1016    def unstake(
1017        self,
1018        key: Keypair,
1019        amount: int,
1020        dest: Ss58Address,
1021        netuid: int = 0,
1022    ) -> ExtrinsicReceipt:
1023        """
1024        Unstakes the specified amount of tokens from a module key address.
1025
1026        Args:
1027            key: The keypair associated with the unstaker's account.
1028            amount: The amount of tokens to unstake, in nanotokens.
1029            dest: The SS58 address of the module key to unstake from.
1030            netuid: The network identifier.
1031
1032        Returns:
1033            A receipt of the unstaking transaction.
1034
1035        Raises:
1036            InsufficientStakeError: If the staked key does not have enough
1037              staked tokens by the signer key.
1038            ChainTransactionError: If the transaction fails.
1039        """
1040
1041        params = {"amount": amount, "netuid": netuid, "module_key": dest}
1042        return self.compose_call(fn="remove_stake", params=params, key=key)
1043
1044    def update_module(
1045        self,
1046        key: Keypair,
1047        name: str,
1048        address: str,
1049        metadata: str | None = None,
1050        delegation_fee: int = 20,
1051        netuid: int = 0,
1052    ) -> ExtrinsicReceipt:
1053        """
1054        Updates the parameters of a registered module.
1055
1056        The delegation fee must be an integer between 0 and 100.
1057
1058        Args:
1059            key: The keypair associated with the module's account.
1060            name: The new name for the module. If None, the name is not updated.
1061            address: The new address for the module.
1062                If None, the address is not updated.
1063            delegation_fee: The new delegation fee for the module,
1064                between 0 and 100.
1065            netuid: The network identifier.
1066
1067        Returns:
1068            A receipt of the module update transaction.
1069
1070        Raises:
1071            InvalidParameterError: If the provided parameters are invalid.
1072            ChainTransactionError: If the transaction fails.
1073        """
1074
1075        assert isinstance(delegation_fee, int)
1076
1077        params = {
1078            "netuid": netuid,
1079            "name": name,
1080            "address": address,
1081            "delegation_fee": delegation_fee,
1082            "metadata": metadata,
1083        }
1084
1085        response = self.compose_call("update_module", params=params, key=key)
1086
1087        return response
1088
1089    def register_module(
1090        self,
1091        key: Keypair,
1092        name: str,
1093        address: str | None = None,
1094        subnet: str = "commune",
1095        min_stake: int | None = None,
1096        metadata: str | None = None,
1097    ) -> ExtrinsicReceipt:
1098        """
1099        Registers a new module in the network.
1100
1101        Args:
1102            key: The keypair used for registering the module.
1103            name: The name of the module. If None, a default or previously
1104                set name is used. # How does this work?
1105            address: The address of the module. If None, a default or
1106                previously set address is used. # How does this work?
1107            subnet: The network subnet to register the module in.
1108            min_stake: The minimum stake required for the module, in nanotokens.
1109                If None, a default value is used.
1110
1111        Returns:
1112            A receipt of the registration transaction.
1113
1114        Raises:
1115            InvalidParameterError: If the provided parameters are invalid.
1116            ChainTransactionError: If the transaction fails.
1117        """
1118
1119        stake = self.get_min_stake() if min_stake is None else min_stake
1120
1121        key_addr = key.ss58_address
1122
1123        params = {
1124            "network": subnet,
1125            "address": address,
1126            "name": name,
1127            "stake": stake,
1128            "module_key": key_addr,
1129            "metadata": metadata,
1130        }
1131
1132        response = self.compose_call("register", params=params, key=key)
1133        return response
1134
1135    def vote(
1136        self,
1137        key: Keypair,
1138        uids: list[int],
1139        weights: list[int],
1140        netuid: int = 0,
1141    ) -> ExtrinsicReceipt:
1142        """
1143        Casts votes on a list of module UIDs with corresponding weights.
1144
1145        The length of the UIDs list and the weights list should be the same.
1146        Each weight corresponds to the UID at the same index.
1147
1148        Args:
1149            key: The keypair used for signing the vote transaction.
1150            uids: A list of module UIDs to vote on.
1151            weights: A list of weights corresponding to each UID.
1152            netuid: The network identifier.
1153
1154        Returns:
1155            A receipt of the voting transaction.
1156
1157        Raises:
1158            InvalidParameterError: If the lengths of UIDs and weights lists
1159                do not match.
1160            ChainTransactionError: If the transaction fails.
1161        """
1162
1163        assert len(uids) == len(weights)
1164
1165        params = {
1166            "uids": uids,
1167            "weights": weights,
1168            "netuid": netuid,
1169        }
1170
1171        response = self.compose_call("set_weights", params=params, key=key)
1172
1173        return response
1174
1175    def update_subnet(
1176        self,
1177        key: Keypair,
1178        params: SubnetParams,
1179        netuid: int = 0,
1180    ) -> ExtrinsicReceipt:
1181        """
1182        Update a subnet's configuration.
1183
1184        It requires the founder key for authorization.
1185
1186        Args:
1187            key: The founder keypair of the subnet.
1188            params: The new parameters for the subnet.
1189            netuid: The network identifier.
1190
1191        Returns:
1192            A receipt of the subnet update transaction.
1193
1194        Raises:
1195            AuthorizationError: If the key is not authorized.
1196            ChainTransactionError: If the transaction fails.
1197        """
1198
1199        general_params = dict(params)
1200        general_params["netuid"] = netuid
1201
1202        response = self.compose_call(
1203            fn="update_subnet",
1204            params=general_params,
1205            key=key,
1206        )
1207
1208        return response
1209
1210    def transfer_stake(
1211        self,
1212        key: Keypair,
1213        amount: int,
1214        from_module_key: Ss58Address,
1215        dest_module_address: Ss58Address,
1216        netuid: int = 0,
1217    ) -> ExtrinsicReceipt:
1218        """
1219        Realocate staked tokens from one staked module to another module.
1220
1221        Args:
1222            key: The keypair associated with the account that is delegating the tokens.
1223            amount: The amount of staked tokens to transfer, in nanotokens.
1224            from_module_key: The SS58 address of the module you want to transfer from (currently delegated by the key).
1225            dest_module_address: The SS58 address of the destination (newly delegated key).
1226            netuid: The network identifier.
1227
1228        Returns:
1229            A receipt of the stake transfer transaction.
1230
1231        Raises:
1232            InsufficientStakeError: If the source module key does not have
1233            enough staked tokens. ChainTransactionError: If the transaction
1234            fails.
1235        """
1236
1237        amount = amount - self.get_existential_deposit()
1238
1239        params = {
1240            "amount": amount,
1241            "netuid": netuid,
1242            "module_key": from_module_key,
1243            "new_module_key": dest_module_address,
1244        }
1245
1246        response = self.compose_call("transfer_stake", key=key, params=params)
1247
1248        return response
1249
1250    def multiunstake(
1251        self,
1252        key: Keypair,
1253        keys: list[Ss58Address],
1254        amounts: list[int],
1255        netuid: int = 0,
1256    ) -> ExtrinsicReceipt:
1257        """
1258        Unstakes tokens from multiple module keys.
1259
1260        And the lists `keys` and `amounts` must be of the same length. Each
1261        amount corresponds to the module key at the same index.
1262
1263        Args:
1264            key: The keypair associated with the unstaker's account.
1265            keys: A list of SS58 addresses of the module keys to unstake from.
1266            amounts: A list of amounts to unstake from each module key,
1267              in nanotokens.
1268            netuid: The network identifier.
1269
1270        Returns:
1271            A receipt of the multi-unstaking transaction.
1272
1273        Raises:
1274            MismatchedLengthError: If the lengths of keys and amounts lists do
1275            not match. InsufficientStakeError: If any of the module keys do not
1276            have enough staked tokens. ChainTransactionError: If the transaction
1277            fails.
1278        """
1279
1280        assert len(keys) == len(amounts)
1281
1282        params = {"netuid": netuid, "module_keys": keys, "amounts": amounts}
1283
1284        response = self.compose_call("remove_stake_multiple", params=params, key=key)
1285
1286        return response
1287
1288    def multistake(
1289        self,
1290        key: Keypair,
1291        keys: list[Ss58Address],
1292        amounts: list[int],
1293        netuid: int = 0,
1294    ) -> ExtrinsicReceipt:
1295        """
1296        Stakes tokens to multiple module keys.
1297
1298        The lengths of the `keys` and `amounts` lists must be the same. Each
1299        amount corresponds to the module key at the same index.
1300
1301        Args:
1302            key: The keypair associated with the staker's account.
1303            keys: A list of SS58 addresses of the module keys to stake to.
1304            amounts: A list of amounts to stake to each module key,
1305                in nanotokens.
1306            netuid: The network identifier.
1307
1308        Returns:
1309            A receipt of the multi-staking transaction.
1310
1311        Raises:
1312            MismatchedLengthError: If the lengths of keys and amounts lists
1313                do not match.
1314            ChainTransactionError: If the transaction fails.
1315        """
1316
1317        assert len(keys) == len(amounts)
1318
1319        params = {
1320            "module_keys": keys,
1321            "amounts": amounts,
1322            "netuid": netuid,
1323        }
1324
1325        response = self.compose_call("add_stake_multiple", params=params, key=key)
1326
1327        return response
1328
1329    def add_profit_shares(
1330        self,
1331        key: Keypair,
1332        keys: list[Ss58Address],
1333        shares: list[int],
1334    ) -> ExtrinsicReceipt:
1335        """
1336        Allocates profit shares to multiple keys.
1337
1338        The lists `keys` and `shares` must be of the same length,
1339        with each share amount corresponding to the key at the same index.
1340
1341        Args:
1342            key: The keypair associated with the account
1343                distributing the shares.
1344            keys: A list of SS58 addresses to allocate shares to.
1345            shares: A list of share amounts to allocate to each key,
1346                in nanotokens.
1347
1348        Returns:
1349            A receipt of the profit sharing transaction.
1350
1351        Raises:
1352            MismatchedLengthError: If the lengths of keys and shares
1353                lists do not match.
1354            ChainTransactionError: If the transaction fails.
1355        """
1356
1357        assert len(keys) == len(shares)
1358
1359        params = {"keys": keys, "shares": shares}
1360
1361        response = self.compose_call("add_profit_shares", params=params, key=key)
1362
1363        return response
1364
1365    def add_subnet_proposal(
1366        self, key: Keypair, params: SubnetParams, ipfs: str, netuid: int = 0
1367    ) -> ExtrinsicReceipt:
1368        """
1369        Submits a proposal for creating or modifying a subnet within the
1370        network.
1371
1372        The proposal includes various parameters like the name, founder, share
1373        allocations, and other subnet-specific settings.
1374
1375        Args:
1376            key: The keypair used for signing the proposal transaction.
1377            params: The parameters for the subnet proposal.
1378            netuid: The network identifier.
1379
1380        Returns:
1381            A receipt of the subnet proposal transaction.
1382
1383        Raises:
1384            InvalidParameterError: If the provided subnet
1385                parameters are invalid.
1386            ChainTransactionError: If the transaction fails.
1387        """
1388
1389        general_params = dict(params)
1390        general_params["subnet_id"] = netuid
1391        general_params["data"] = ipfs
1392        # breakpoint()
1393        # general_params["burn_config"] = json.dumps(general_params["burn_config"])
1394        response = self.compose_call(
1395            fn="add_subnet_params_proposal",
1396            params=general_params,
1397            key=key,
1398            module="GovernanceModule",
1399        )
1400
1401        return response
1402
1403    def add_custom_proposal(
1404        self,
1405        key: Keypair,
1406        cid: str,
1407    ) -> ExtrinsicReceipt:
1408
1409        params = {"data": cid}
1410
1411        response = self.compose_call(
1412            fn="add_global_custom_proposal",
1413            params=params,
1414            key=key,
1415            module="GovernanceModule",
1416        )
1417        return response
1418
1419    def add_custom_subnet_proposal(
1420        self,
1421        key: Keypair,
1422        cid: str,
1423        netuid: int = 0,
1424    ) -> ExtrinsicReceipt:
1425        """
1426        Submits a proposal for creating or modifying a custom subnet within the
1427        network.
1428
1429        The proposal includes various parameters like the name, founder, share
1430        allocations, and other subnet-specific settings.
1431
1432        Args:
1433            key: The keypair used for signing the proposal transaction.
1434            params: The parameters for the subnet proposal.
1435            netuid: The network identifier.
1436
1437        Returns:
1438            A receipt of the subnet proposal transaction.
1439        """
1440
1441        params = {
1442            "data": cid,
1443            "subnet_id": netuid,
1444        }
1445
1446        response = self.compose_call(
1447            fn="add_subnet_custom_proposal",
1448            params=params,
1449            key=key,
1450            module="GovernanceModule",
1451        )
1452
1453        return response
1454
1455    def add_global_proposal(
1456        self,
1457        key: Keypair,
1458        params: NetworkParams,
1459        cid: str | None,
1460    ) -> ExtrinsicReceipt:
1461        """
1462        Submits a proposal for altering the global network parameters.
1463
1464        Allows for the submission of a proposal to
1465        change various global parameters
1466        of the network, such as emission rates, rate limits, and voting
1467        thresholds. It is used to
1468        suggest changes that affect the entire network's operation.
1469
1470        Args:
1471            key: The keypair used for signing the proposal transaction.
1472            params: A dictionary containing global network parameters
1473                    like maximum allowed subnets, modules,
1474                    transaction rate limits, and others.
1475
1476        Returns:
1477            A receipt of the global proposal transaction.
1478
1479        Raises:
1480            InvalidParameterError: If the provided network
1481                parameters are invalid.
1482            ChainTransactionError: If the transaction fails.
1483        """
1484        general_params = cast(dict[str, Any], params)
1485        cid = cid or ""
1486        general_params["data"] = cid
1487
1488        response = self.compose_call(
1489            fn="add_global_params_proposal",
1490            params=general_params,
1491            key=key,
1492            module="GovernanceModule",
1493        )
1494
1495        return response
1496
1497    def vote_on_proposal(
1498        self,
1499        key: Keypair,
1500        proposal_id: int,
1501        agree: bool,
1502    ) -> ExtrinsicReceipt:
1503        """
1504        Casts a vote on a specified proposal within the network.
1505
1506        Args:
1507            key: The keypair used for signing the vote transaction.
1508            proposal_id: The unique identifier of the proposal to vote on.
1509
1510        Returns:
1511            A receipt of the voting transaction in nanotokens.
1512
1513        Raises:
1514            InvalidProposalIDError: If the provided proposal ID does not
1515                exist or is invalid.
1516            ChainTransactionError: If the transaction fails.
1517        """
1518
1519        params = {"proposal_id": proposal_id, "agree": agree}
1520
1521        response = self.compose_call(
1522            "vote_proposal",
1523            key=key,
1524            params=params,
1525            module="GovernanceModule",
1526        )
1527
1528        return response
1529
1530    def unvote_on_proposal(
1531        self,
1532        key: Keypair,
1533        proposal_id: int,
1534    ) -> ExtrinsicReceipt:
1535        """
1536        Retracts a previously cast vote on a specified proposal.
1537
1538        Args:
1539            key: The keypair used for signing the unvote transaction.
1540            proposal_id: The unique identifier of the proposal to withdraw the
1541                vote from.
1542
1543        Returns:
1544            A receipt of the unvoting transaction in nanotokens.
1545
1546        Raises:
1547            InvalidProposalIDError: If the provided proposal ID does not
1548                exist or is invalid.
1549            ChainTransactionError: If the transaction fails to be processed, or
1550                if there was no prior vote to retract.
1551        """
1552
1553        params = {"proposal_id": proposal_id}
1554
1555        response = self.compose_call(
1556            "remove_vote_proposal",
1557            key=key,
1558            params=params,
1559            module="GovernanceModule",
1560        )
1561
1562        return response
1563
1564    def enable_vote_power_delegation(self, key: Keypair) -> ExtrinsicReceipt:
1565        """
1566        Enables vote power delegation for the signer's account.
1567
1568        Args:
1569            key: The keypair used for signing the delegation transaction.
1570
1571        Returns:
1572            A receipt of the vote power delegation transaction.
1573
1574        Raises:
1575            ChainTransactionError: If the transaction fails.
1576        """
1577
1578        response = self.compose_call(
1579            "enable_vote_power_delegation",
1580            params={},
1581            key=key,
1582            module="GovernanceModule",
1583        )
1584
1585        return response
1586
1587    def disable_vote_power_delegation(self, key: Keypair) -> ExtrinsicReceipt:
1588        """
1589        Disables vote power delegation for the signer's account.
1590
1591        Args:
1592            key: The keypair used for signing the delegation transaction.
1593
1594        Returns:
1595            A receipt of the vote power delegation transaction.
1596
1597        Raises:
1598            ChainTransactionError: If the transaction fails.
1599        """
1600
1601        response = self.compose_call(
1602            "disable_vote_power_delegation",
1603            params={},
1604            key=key,
1605            module="GovernanceModule",
1606        )
1607
1608        return response
1609
1610    def add_dao_application(
1611        self, key: Keypair, application_key: Ss58Address, data: str
1612    ) -> ExtrinsicReceipt:
1613        """
1614        Submits a new application to the general subnet DAO.
1615
1616        Args:
1617            key: The keypair used for signing the application transaction.
1618            application_key: The SS58 address of the application key.
1619            data: The data associated with the application.
1620
1621        Returns:
1622            A receipt of the application transaction.
1623
1624        Raises:
1625            ChainTransactionError: If the transaction fails.
1626        """
1627
1628        params = {"application_key": application_key, "data": data}
1629
1630        response = self.compose_call("add_dao_application", key=key, params=params)
1631
1632        return response
1633
1634    def query_map_curator_applications(self) -> dict[str, dict[str, str]]:
1635        query_result = self.query_map(
1636            "CuratorApplications", params=[], extract_value=False
1637        )
1638        applications = query_result.get("CuratorApplications", {})
1639        return applications
1640
1641    def query_map_proposals(
1642        self, extract_value: bool = False
1643    ) -> dict[int, dict[str, Any]]:
1644        """
1645        Retrieves a mappping of proposals from the network.
1646
1647        Queries the network and returns a mapping of proposal IDs to
1648        their respective parameters.
1649
1650        Returns:
1651            A dictionary mapping proposal IDs
1652            to dictionaries of their parameters.
1653
1654        Raises:
1655            QueryError: If the query to the network fails or is invalid.
1656        """
1657
1658        return self.query_map(
1659            "Proposals", extract_value=extract_value, module="GovernanceModule"
1660        )["Proposals"]
1661
1662    def query_map_weights(
1663        self, netuid: int = 0, extract_value: bool = False
1664    ) -> dict[int, list[int]]:
1665        """
1666        Retrieves a mapping of weights for keys on the network.
1667
1668        Queries the network and returns a mapping of key UIDs to
1669        their respective weights.
1670
1671        Args:
1672            netuid: The network UID from which to get the weights.
1673
1674        Returns:
1675            A dictionary mapping key UIDs to lists of their weights.
1676
1677        Raises:
1678            QueryError: If the query to the network fails or is invalid.
1679        """
1680
1681        return self.query_map("Weights", [netuid], extract_value=extract_value)[
1682            "Weights"
1683        ]
1684
1685    def query_map_key(
1686        self,
1687        netuid: int = 0,
1688        extract_value: bool = False,
1689    ) -> dict[int, Ss58Address]:
1690        """
1691        Retrieves a map of keys from the network.
1692
1693        Fetches a mapping of key UIDs to their associated
1694        addresses on the network.
1695        The query can be targeted at a specific network UID if required.
1696
1697        Args:
1698            netuid: The network UID from which to get the keys.
1699
1700        Returns:
1701            A dictionary mapping key UIDs to their addresses.
1702
1703        Raises:
1704            QueryError: If the query to the network fails or is invalid.
1705        """
1706        return self.query_map("Keys", [netuid], extract_value=extract_value)["Keys"]
1707
1708    def query_map_address(
1709        self, netuid: int = 0, extract_value: bool = False
1710    ) -> dict[int, str]:
1711        """
1712        Retrieves a map of key addresses from the network.
1713
1714        Queries the network for a mapping of key UIDs to their addresses.
1715
1716        Args:
1717            netuid: The network UID from which to get the addresses.
1718
1719        Returns:
1720            A dictionary mapping key UIDs to their addresses.
1721
1722        Raises:
1723            QueryError: If the query to the network fails or is invalid.
1724        """
1725
1726        return self.query_map("Address", [netuid], extract_value=extract_value)[
1727            "Address"
1728        ]
1729
1730    def query_map_emission(self, extract_value: bool = False) -> dict[int, list[int]]:
1731        """
1732        Retrieves a map of emissions for keys on the network.
1733
1734        Queries the network to get a mapping of
1735        key UIDs to their emission values.
1736
1737        Returns:
1738            A dictionary mapping key UIDs to lists of their emission values.
1739
1740        Raises:
1741            QueryError: If the query to the network fails or is invalid.
1742        """
1743
1744        return self.query_map("Emission", extract_value=extract_value)["Emission"]
1745
1746    def query_map_incentive(self, extract_value: bool = False) -> dict[int, list[int]]:
1747        """
1748        Retrieves a mapping of incentives for keys on the network.
1749
1750        Queries the network and returns a mapping of key UIDs to
1751        their respective incentive values.
1752
1753        Returns:
1754            A dictionary mapping key UIDs to lists of their incentive values.
1755
1756        Raises:
1757            QueryError: If the query to the network fails or is invalid.
1758        """
1759
1760        return self.query_map("Incentive", extract_value=extract_value)["Incentive"]
1761
1762    def query_map_dividend(self, extract_value: bool = False) -> dict[int, list[int]]:
1763        """
1764        Retrieves a mapping of dividends for keys on the network.
1765
1766        Queries the network for a mapping of key UIDs to
1767        their dividend values.
1768
1769        Returns:
1770            A dictionary mapping key UIDs to lists of their dividend values.
1771
1772        Raises:
1773            QueryError: If the query to the network fails or is invalid.
1774        """
1775
1776        return self.query_map("Dividends", extract_value=extract_value)["Dividends"]
1777
1778    def query_map_regblock(
1779        self, netuid: int = 0, extract_value: bool = False
1780    ) -> dict[int, int]:
1781        """
1782        Retrieves a mapping of registration blocks for keys on the network.
1783
1784        Queries the network for a mapping of key UIDs to
1785        the blocks where they were registered.
1786
1787        Args:
1788            netuid: The network UID from which to get the registration blocks.
1789
1790        Returns:
1791            A dictionary mapping key UIDs to their registration blocks.
1792
1793        Raises:
1794            QueryError: If the query to the network fails or is invalid.
1795        """
1796
1797        return self.query_map(
1798            "RegistrationBlock", [netuid], extract_value=extract_value
1799        )["RegistrationBlock"]
1800
1801    def query_map_lastupdate(self, extract_value: bool = False) -> dict[int, list[int]]:
1802        """
1803        Retrieves a mapping of the last update times for keys on the network.
1804
1805        Queries the network for a mapping of key UIDs to their last update times.
1806
1807        Returns:
1808            A dictionary mapping key UIDs to lists of their last update times.
1809
1810        Raises:
1811            QueryError: If the query to the network fails or is invalid.
1812        """
1813
1814        return self.query_map("LastUpdate", extract_value=extract_value)["LastUpdate"]
1815
1816    def query_map_total_stake(self, extract_value: bool = False) -> dict[int, int]:
1817        """
1818        Retrieves a mapping of total stakes for keys on the network.
1819
1820        Queries the network for a mapping of key UIDs to their total stake amounts.
1821
1822        Returns:
1823            A dictionary mapping key UIDs to their total stake amounts.
1824
1825        Raises:
1826            QueryError: If the query to the network fails or is invalid.
1827        """
1828
1829        return self.query_map("TotalStake", extract_value=extract_value)["TotalStake"]
1830
1831    def query_map_stakefrom(
1832        self, netuid: int = 0, extract_value: bool = False
1833    ) -> dict[str, list[tuple[str, int]]]:
1834        """
1835        Retrieves a mapping of stakes from various sources for keys on the network.
1836
1837        Queries the network to obtain a mapping of key addresses to the sources
1838        and amounts of stakes they have received.
1839
1840        Args:
1841            netuid: The network UID from which to get the stakes.
1842
1843        Returns:
1844            A dictionary mapping key addresses to lists of tuples
1845            (module_key_address, amount).
1846
1847        Raises:
1848            QueryError: If the query to the network fails or is invalid.
1849        """
1850
1851        return self.query_map("StakeFrom", [netuid], extract_value=extract_value)[
1852            "StakeFrom"
1853        ]
1854
1855    def query_map_staketo(
1856        self, netuid: int = 0, extract_value: bool = False
1857    ) -> dict[str, list[tuple[str, int]]]:
1858        """
1859        Retrieves a mapping of stakes to destinations for keys on the network.
1860
1861        Queries the network for a mapping of key addresses to the destinations
1862        and amounts of stakes they have made.
1863
1864        Args:
1865            netuid: The network UID from which to get the stakes.
1866
1867        Returns:
1868            A dictionary mapping key addresses to lists of tuples
1869            (module_key_address, amount).
1870
1871        Raises:
1872            QueryError: If the query to the network fails or is invalid.
1873        """
1874
1875        return self.query_map("StakeTo", [netuid], extract_value=extract_value)[
1876            "StakeTo"
1877        ]
1878
1879    def query_map_stake(
1880        self, netuid: int = 0, extract_value: bool = False
1881    ) -> dict[str, int]:
1882        """
1883        Retrieves a mapping of stakes for keys on the network.
1884
1885        Queries the network and returns a mapping of key addresses to their
1886        respective delegated staked balances amounts.
1887        The query can be targeted at a specific network UID if required.
1888
1889        Args:
1890            netuid: The network UID from which to get the stakes.
1891
1892        Returns:
1893            A dictionary mapping key addresses to their stake amounts.
1894
1895        Raises:
1896            QueryError: If the query to the network fails or is invalid.
1897        """
1898
1899        return self.query_map("Stake", [netuid], extract_value=extract_value)["Stake"]
1900
1901    def query_map_delegationfee(
1902        self, netuid: int = 0, extract_value: bool = False
1903    ) -> dict[str, int]:
1904        """
1905        Retrieves a mapping of delegation fees for keys on the network.
1906
1907        Queries the network to obtain a mapping of key addresses to their
1908        respective delegation fees.
1909
1910        Args:
1911            netuid: The network UID to filter the delegation fees.
1912
1913        Returns:
1914            A dictionary mapping key addresses to their delegation fees.
1915
1916        Raises:
1917            QueryError: If the query to the network fails or is invalid.
1918        """
1919
1920        return self.query_map("DelegationFee", [netuid], extract_value=extract_value)[
1921            "DelegationFee"
1922        ]
1923
1924    def query_map_tempo(self, extract_value: bool = False) -> dict[int, int]:
1925        """
1926        Retrieves a mapping of tempo settings for the network.
1927
1928        Queries the network to obtain the tempo (rate of reward distributions)
1929        settings for various network subnets.
1930
1931        Returns:
1932            A dictionary mapping network UIDs to their tempo settings.
1933
1934        Raises:
1935            QueryError: If the query to the network fails or is invalid.
1936        """
1937
1938        return self.query_map("Tempo", extract_value=extract_value)["Tempo"]
1939
1940    def query_map_immunity_period(self, extract_value: bool) -> dict[int, int]:
1941        """
1942        Retrieves a mapping of immunity periods for the network.
1943
1944        Queries the network for the immunity period settings,
1945        which represent the time duration during which modules
1946        can not get deregistered.
1947
1948        Returns:
1949            A dictionary mapping network UIDs to their immunity period settings.
1950
1951        Raises:
1952            QueryError: If the query to the network fails or is invalid.
1953        """
1954
1955        return self.query_map("ImmunityPeriod", extract_value=extract_value)[
1956            "ImmunityPeriod"
1957        ]
1958
1959    def query_map_min_allowed_weights(
1960        self, extract_value: bool = False
1961    ) -> dict[int, int]:
1962        """
1963        Retrieves a mapping of minimum allowed weights for the network.
1964
1965        Queries the network to obtain the minimum allowed weights,
1966        which are the lowest permissible weight values that can be set by
1967        validators.
1968
1969        Returns:
1970            A dictionary mapping network UIDs to
1971            their minimum allowed weight values.
1972
1973        Raises:
1974            QueryError: If the query to the network fails or is invalid.
1975        """
1976
1977        return self.query_map("MinAllowedWeights", extract_value=extract_value)[
1978            "MinAllowedWeights"
1979        ]
1980
1981    def query_map_max_allowed_weights(
1982        self, extract_value: bool = False
1983    ) -> dict[int, int]:
1984        """
1985        Retrieves a mapping of maximum allowed weights for the network.
1986
1987        Queries the network for the maximum allowed weights,
1988        which are the highest permissible
1989        weight values that can be set by validators.
1990
1991        Returns:
1992            A dictionary mapping network UIDs to
1993            their maximum allowed weight values.
1994
1995        Raises:
1996            QueryError: If the query to the network fails or is invalid.
1997        """
1998
1999        return self.query_map("MaxAllowedWeights", extract_value=extract_value)[
2000            "MaxAllowedWeights"
2001        ]
2002
2003    def query_map_max_allowed_uids(self, extract_value: bool = False) -> dict[int, int]:
2004        """
2005        Queries the network for the maximum number of allowed user IDs (UIDs)
2006        for each network subnet.
2007
2008        Fetches a mapping of network subnets to their respective
2009        limits on the number of user IDs that can be created or used.
2010
2011        Returns:
2012            A dictionary mapping network UIDs (unique identifiers) to their
2013            maximum allowed number of UIDs.
2014            Each entry represents a network subnet
2015            with its corresponding UID limit.
2016
2017        Raises:
2018            QueryError: If the query to the network fails or is invalid.
2019        """
2020
2021        return self.query_map("MaxAllowedUids", extract_value=extract_value)[
2022            "MaxAllowedUids"
2023        ]
2024
2025    def query_map_min_stake(self, extract_value: bool = False) -> dict[int, int]:
2026        """
2027        Retrieves a mapping of minimum allowed stake on the network.
2028
2029        Queries the network to obtain the minimum number of stake,
2030        which is represented in nanotokens.
2031
2032        Returns:
2033            A dictionary mapping network UIDs to
2034            their minimum allowed stake values.
2035
2036        Raises:
2037            QueryError: If the query to the network fails or is invalid.
2038        """
2039
2040        return self.query_map("MinStake", extract_value=extract_value)["MinStake"]
2041
2042    def query_map_max_stake(self, extract_value: bool = False) -> dict[int, int]:
2043        """
2044        Retrieves a mapping of the maximum stake values for the network.
2045
2046        Queries the network for the maximum stake values across various s
2047        ubnets of the network.
2048
2049        Returns:
2050            A dictionary mapping network UIDs to their maximum stake values.
2051
2052        Raises:
2053            QueryError: If the query to the network fails or is invalid.
2054        """
2055
2056        return self.query_map("MaxStake", extract_value=extract_value)["MaxStake"]
2057
2058    def query_map_founder(self, extract_value: bool = False) -> dict[int, str]:
2059        """
2060        Retrieves a mapping of founders for the network.
2061
2062        Queries the network to obtain the founders associated with
2063        various subnets.
2064
2065        Returns:
2066            A dictionary mapping network UIDs to their respective founders.
2067
2068        Raises:
2069            QueryError: If the query to the network fails or is invalid.
2070        """
2071
2072        return self.query_map("Founder", extract_value=extract_value)["Founder"]
2073
2074    def query_map_founder_share(self, extract_value: bool = False) -> dict[int, int]:
2075        """
2076        Retrieves a mapping of founder shares for the network.
2077
2078        Queries the network for the share percentages
2079        allocated to founders across different subnets.
2080
2081        Returns:
2082            A dictionary mapping network UIDs to their founder share percentages.
2083
2084        Raises:
2085            QueryError: If the query to the network fails or is invalid.
2086        """
2087
2088        return self.query_map("FounderShare", extract_value=extract_value)[
2089            "FounderShare"
2090        ]
2091
2092    def query_map_incentive_ratio(self, extract_value: bool = False) -> dict[int, int]:
2093        """
2094        Retrieves a mapping of incentive ratios for the network.
2095
2096        Queries the network for the incentive ratios,
2097        which are the proportions of rewards or incentives
2098        allocated in different subnets of the network.
2099
2100        Returns:
2101            A dictionary mapping network UIDs to their incentive ratios.
2102
2103        Raises:
2104            QueryError: If the query to the network fails or is invalid.
2105        """
2106
2107        return self.query_map("IncentiveRatio", extract_value=extract_value)[
2108            "IncentiveRatio"
2109        ]
2110
2111    def query_map_trust_ratio(self, extract_value: bool = False) -> dict[int, int]:
2112        """
2113        Retrieves a mapping of trust ratios for the network.
2114
2115        Queries the network for trust ratios,
2116        indicative of the level of trust or credibility assigned
2117        to different subnets of the network.
2118
2119        Returns:
2120            A dictionary mapping network UIDs to their trust ratios.
2121
2122        Raises:
2123            QueryError: If the query to the network fails or is invalid.
2124        """
2125
2126        return self.query_map("TrustRatio", extract_value=extract_value)["TrustRatio"]
2127
2128    def query_map_vote_mode_subnet(self, extract_value: bool = False) -> dict[int, str]:
2129        """
2130        Retrieves a mapping of vote modes for subnets within the network.
2131
2132        Queries the network for the voting modes used in different
2133        subnets, which define the methodology or approach of voting within those
2134        subnets.
2135
2136        Returns:
2137            A dictionary mapping network UIDs to their vote
2138            modes for subnets.
2139
2140        Raises:
2141            QueryError: If the query to the network fails or is invalid.
2142        """
2143
2144        return self.query_map("VoteModeSubnet", extract_value=extract_value)[
2145            "VoteModeSubnet"
2146        ]
2147
2148    def query_map_legit_whitelist(
2149        self, extract_value: bool = False
2150    ) -> dict[Ss58Address, int]:
2151        """
2152        Retrieves a mapping of whitelisted addresses for the network.
2153
2154        Queries the network for a mapping of whitelisted addresses
2155        and their respective legitimacy status.
2156
2157        Returns:
2158            A dictionary mapping addresses to their legitimacy status.
2159
2160        Raises:
2161            QueryError: If the query to the network fails or is invalid.
2162        """
2163
2164        return self.query_map("LegitWhitelist", extract_value=extract_value)[
2165            "LegitWhitelist"
2166        ]
2167
2168    def query_map_subnet_names(self, extract_value: bool = False) -> dict[int, str]:
2169        """
2170        Retrieves a mapping of subnet names within the network.
2171
2172        Queries the network for the names of various subnets,
2173        providing an overview of the different
2174        subnets within the network.
2175
2176        Returns:
2177            A dictionary mapping network UIDs to their subnet names.
2178
2179        Raises:
2180            QueryError: If the query to the network fails or is invalid.
2181        """
2182
2183        return self.query_map("SubnetNames", extract_value=extract_value)["SubnetNames"]
2184
2185    def query_map_balances(
2186        self, extract_value: bool = False
2187    ) -> dict[str, dict["str", int | dict[str, int]]]:
2188        """
2189        Retrieves a mapping of account balances within the network.
2190
2191        Queries the network for the balances associated with different accounts.
2192        It provides detailed information including various types of
2193        balances for each account.
2194
2195        Returns:
2196            A dictionary mapping account addresses to their balance details.
2197
2198        Raises:
2199            QueryError: If the query to the network fails or is invalid.
2200        """
2201
2202        return self.query_map("Account", module="System", extract_value=extract_value)[
2203            "Account"
2204        ]
2205
2206    def query_map_registration_blocks(
2207        self, netuid: int = 0, extract_value: bool = False
2208    ) -> dict[int, int]:
2209        """
2210        Retrieves a mapping of registration blocks for UIDs on the network.
2211
2212        Queries the network to find the block numbers at which various
2213        UIDs were registered.
2214
2215        Args:
2216            netuid: The network UID from which to get the registrations.
2217
2218        Returns:
2219            A dictionary mapping UIDs to their registration block numbers.
2220
2221        Raises:
2222            QueryError: If the query to the network fails or is invalid.
2223        """
2224
2225        return self.query_map(
2226            "RegistrationBlock", [netuid], extract_value=extract_value
2227        )["RegistrationBlock"]
2228
2229    def query_map_name(
2230        self, netuid: int = 0, extract_value: bool = False
2231    ) -> dict[int, str]:
2232        """
2233        Retrieves a mapping of names for keys on the network.
2234
2235        Queries the network for the names associated with different keys.
2236        It provides a mapping of key UIDs to their registered names.
2237
2238        Args:
2239            netuid: The network UID from which to get the names.
2240
2241        Returns:
2242            A dictionary mapping key UIDs to their names.
2243
2244        Raises:
2245            QueryError: If the query to the network fails or is invalid.
2246        """
2247
2248        return self.query_map("Name", [netuid], extract_value=extract_value)["Name"]
2249
2250    #  == QUERY FUNCTIONS == #
2251
2252    def get_immunity_period(self, netuid: int = 0) -> int:
2253        """
2254        Queries the network for the immunity period setting.
2255
2256        The immunity period is a time duration during which a module
2257        can not be deregistered from the network.
2258        Fetches the immunity period for a specified network subnet.
2259
2260        Args:
2261            netuid: The network UID for which to query the immunity period.
2262
2263        Returns:
2264            The immunity period setting for the specified network subnet.
2265
2266        Raises:
2267            QueryError: If the query to the network fails or is invalid.
2268        """
2269
2270        return self.query(
2271            "ImmunityPeriod",
2272            params=[netuid],
2273        )
2274
2275    def get_max_set_weights_per_epoch(self):
2276        return self.query("MaximumSetWeightCallsPerEpoch")
2277
2278    def get_min_allowed_weights(self, netuid: int = 0) -> int:
2279        """
2280        Queries the network for the minimum allowed weights setting.
2281
2282        Retrieves the minimum weight values that are possible to set
2283        by a validator within a specific network subnet.
2284
2285        Args:
2286            netuid: The network UID for which to query the minimum allowed
2287              weights.
2288
2289        Returns:
2290            The minimum allowed weight values for the specified network
2291              subnet.
2292
2293        Raises:
2294            QueryError: If the query to the network fails or is invalid.
2295        """
2296
2297        return self.query(
2298            "MinAllowedWeights",
2299            params=[netuid],
2300        )
2301
2302    def get_dao_treasury_address(self) -> Ss58Address:
2303        return self.query("DaoTreasuryAddress", module="GovernanceModule")
2304
2305    def get_max_allowed_weights(self, netuid: int = 0) -> int:
2306        """
2307        Queries the network for the maximum allowed weights setting.
2308
2309        Retrieves the maximum weight values that are possible to set
2310        by a validator within a specific network subnet.
2311
2312        Args:
2313            netuid: The network UID for which to query the maximum allowed
2314              weights.
2315
2316        Returns:
2317            The maximum allowed weight values for the specified network
2318              subnet.
2319
2320        Raises:
2321            QueryError: If the query to the network fails or is invalid.
2322        """
2323
2324        return self.query("MaxAllowedWeights", params=[netuid])
2325
2326    def get_max_allowed_uids(self, netuid: int = 0) -> int:
2327        """
2328        Queries the network for the maximum allowed UIDs setting.
2329
2330        Fetches the upper limit on the number of user IDs that can
2331        be allocated or used within a specific network subnet.
2332
2333        Args:
2334            netuid: The network UID for which to query the maximum allowed UIDs.
2335
2336        Returns:
2337            The maximum number of allowed UIDs for the specified network subnet.
2338
2339        Raises: