Exemple #1
0
    def get_wallet_address(blockchain_name: str):
        """
        Returns the wallet address for the node that is running this server.
        """
        try:
            blockchain_name = blockchain_name.strip()
            if not blockchain_name:
                raise ValueError("Blockchain name can't be empty")

            args = [
                NetworkController.MULTICHAIN_ARG,
                blockchain_name,
                NetworkController.GET_WALLET_ADDRESSES_ARG,
            ]
            output = run(args, check=True, capture_output=True)

            wallet_addresses = json.loads(output.stdout)
            wallet_address = wallet_addresses[0]

            if not wallet_address:
                raise ValueError("The wallet address is empty")

            return wallet_address
        except CalledProcessError as err:
            raise MultiChainError(err.stderr)
        except Exception as err:
            raise err
Exemple #2
0
    def create_stream(blockchain_name: str, stream_name: str, is_open: bool):
        """
        Creates a new stream on the blockchain called name. 
        Pass the value "stream" in the type parameter. If open is true 
        then anyone with global send permissions can publish to the stream, 
        otherwise publishers must be explicitly granted per-stream write permissions. 
        Returns the txid of the transaction creating the stream.
        """
        try:
            blockchain_name = blockchain_name.strip()
            if not blockchain_name:
                raise ValueError("Blockchain name can't be empty")

            stream_name = stream_name.strip()
            if not stream_name:
                raise ValueError("Stream name can't be empty")

            args = [
                DataStreamController.MULTICHAIN_ARG,
                blockchain_name,
                DataStreamController.CREATE_ARG,
                DataStreamController.STREAM_ARG,
                stream_name,
                json.dumps(is_open),
            ]
            output = run(args, check=True, capture_output=True)

            return output.stdout.strip()
        except CalledProcessError as err:
            raise MultiChainError(err.stderr)
        except ValueError as err:
            raise err
        except Exception as err:
            raise err
Exemple #3
0
    def unsubscribe(blockchain_name: str, streams: list):
        """
        Instructs the node to stop tracking one or more stream(s). 
        Streams are specified using an array of one ore more items.
        """
        try:
            blockchain_name = blockchain_name.strip()
            if not blockchain_name:
                raise ValueError("Blockchain name can't be empty")

            streams = [stream.strip() for stream in streams if stream.strip()]
            if not streams:
                raise ValueError("Stream names can't be empty")

            args = [
                DataStreamController.MULTICHAIN_ARG,
                blockchain_name,
                DataStreamController.UNSUBSCRIBE_FROM_STREAM_ARG,
                json.dumps(streams),
            ]
            output = run(args, check=True, capture_output=True)

            # returns True if output is empty (meaning it was a success)
            #
            return not output.stdout.strip()
        except CalledProcessError as err:
            raise MultiChainError(err.stderr)
        except ValueError as err:
            raise err
        except Exception as err:
            raise err
Exemple #4
0
    def get_items_by_keys(
        blockchain_name: str,
        stream: str,
        keys: list,
        verbose: bool = DEFAULT_VERBOSE_VALUE,
    ):
        """
        Retrieves items in stream which match all of the specified keys in query. 
        The query is an object with a keys field. The keys field should 
        specify an array of keys. Note that, unlike other stream retrieval APIs, 
        liststreamqueryitems cannot rely completely on prior indexing, 
        so the maxqueryscanitems runtime parameter limits how many 
        items will be scanned after using the best index. If more than 
        this is needed, an error will be returned.
        """
        try:
            blockchain_name = blockchain_name.strip()
            original_number_of_keys = len(keys)
            stream = stream.strip()
            keys = [key.strip() for key in keys if key.strip()]
            new_number_of_keys = len(keys)

            if not stream:
                raise ValueError("Stream name can't be empty")

            # If any of the provided keys is invalid then an exception is thrown. This is done to prevent MultiChain from
            # retrieving records that belong to existing key(s) that match the valid keys.
            # Example: stream contains KEY1. Provided keys: ['KEY1', '        ']. The second key is invalid, so after cleaning
            # Provided keys: ['KEY1']. This key already exists so a different record will be retrieved than what is expected.
            #
            if new_number_of_keys != original_number_of_keys:
                raise ValueError(
                    "Only " + str(new_number_of_keys) + "/" +
                    str(original_number_of_keys) +
                    " keys are valid. Please check the keys provided")

            if not keys:
                raise ValueError("keys can't be empty")

            if not blockchain_name:
                raise ValueError("Blockchain name can't be empty")

            args = [
                DataController.MULTICHAIN_ARG,
                blockchain_name,
                DataController.GET_STREAM_KEYS_ITEMS_ARG,
                stream,
                json.dumps({"keys": keys}),
                json.dumps(verbose),
            ]
            items = run(args, check=True, capture_output=True)

            return json.loads(items.stdout)
        except CalledProcessError as err:
            raise MultiChainError(err.stderr)
        except ValueError as err:
            raise err
        except Exception as err:
            raise err
Exemple #5
0
    def publish_item(blockchain_name: str, stream: str, keys: list, data: str):
        """
        Publishes an item in stream, passed as a stream name, an array of keys 
        and data in JSON format.
        """
        try:
            blockchain_name = blockchain_name.strip()
            original_number_of_keys = len(keys)
            stream = stream.strip()
            keys = [key.strip() for key in keys if key.strip()]
            new_number_of_keys = len(keys)

            # If any of the provided keys is invalid then an exception is thrown. This is done to prevent MultiChain from
            # overwritting records that belong to existing key(s) that match the valid keys.
            # Example: stream contains KEY1. Provided keys: ['KEY1', '        ']. The second key is invalid, so after cleaning
            # Provided keys: ['KEY1']. This key already exists so the data will be overwritten.
            #
            if new_number_of_keys != original_number_of_keys:
                raise ValueError(
                    "Only " + str(new_number_of_keys) + "/" +
                    str(original_number_of_keys) +
                    " keys are valid. Please check the keys provided")

            if not stream:
                raise ValueError("Stream name can't be empty")

            if not blockchain_name:
                raise ValueError("Blockchain name can't be empty")

            if not keys:
                raise ValueError("key(s) can't be empty")

            # This is used to ensure that the json_data provided is a valid JSON object
            #
            if not DataController.__is_json(data):
                data = '"' + data + '"'

            json_data = json.loads('{"json":' + data + "}")
            formatted_data = json.dumps(json_data)

            args = [
                DataController.MULTICHAIN_ARG,
                blockchain_name,
                DataController.PUBLISH_ITEM_ARG,
                stream,
                json.dumps(keys),
                formatted_data,
            ]
            output = run(args, check=True, capture_output=True)

            return output.stdout.strip()
        except CalledProcessError as err:
            raise MultiChainError(err.stderr)
        except ValueError as err:
            raise err
        except Exception as err:
            raise err
Exemple #6
0
    def get_stream_publishers(
        blockchain_name: str,
        stream: str,
        publishers: list = DEFAULT_PUBLISHERS_LIST_CONTENT,
        verbose: bool = DEFAULT_VERBOSE_VALUE,
        count: int = DEFAULT_ITEM_COUNT_VALUE,
        start: int = DEFAULT_ITEM_START_VALUE,
        local_ordering: bool = DEFAULT_LOCAL_ORDERING_VALUE,
    ):
        """
        Provides information about publishers who have written to stream, 
        passed as a stream name. Pass an array for multiple publishers, or 
        use the default value for all publishers. Set verbose to true to include 
        information about  the first and last item by each publisher shown. 
        See liststreamitems for details of the count, start and local-ordering 
        parameters, relevant only if all publishers is requested.
        """
        try:
            blockchain_name = blockchain_name.strip()
            stream = stream.strip()

            if not stream:
                raise ValueError("Stream name can't be empty")

            if not blockchain_name:
                raise ValueError("Blockchain name can't be empty")

            address_selector = "*"
            if publishers is not None:
                publishers = [
                    address.strip() for address in publishers
                    if address.strip()
                ]
                if not publishers:
                    raise ValueError("Addresses can't be empty")
                address_selector = json.dumps(publishers)

            args = [
                DataController.MULTICHAIN_ARG,
                blockchain_name,
                DataController.GET_STREAM_PUBLISHERS_ARG,
                stream,
                address_selector,
                json.dumps(verbose),
                json.dumps(count),
                json.dumps(start),
                json.dumps(local_ordering),
            ]
            publishers = run(args, check=True, capture_output=True)

            return json.loads(publishers.stdout)
        except CalledProcessError as err:
            raise MultiChainError(err.stderr)
        except ValueError as err:
            raise err
        except Exception as err:
            raise err
    def get_permissions(
        blockchain_name: str,
        permissions: list = DEFAULT_PERMISSIONS_LIST_CONTENT,
        addresses: list = DEFAULT_ADDRESSES_LIST_CONTENT,
        verbose: bool = DEFAULT_VERBOSE_VALUE,
    ):
        """
        Returns a list of all permissions which have been explicitly granted to addresses. 
        To list information about specific global permissions, set permissions to a list of connect, 
        send, receive, issue, mine, activate, admin. Omit to list all global permissions. 
        Provide a list in addresses to list the permissions for particular addresses or omit for all addresses. 
        If verbose is true, the admins output field lists the administrator/s who assigned the corresponding permission, 
        and the pending field lists permission changes which are waiting to reach consensus.
        """
        try:
            blockchain_name = blockchain_name.strip()
            if not blockchain_name:
                raise ValueError("Blockchain name can't be empty")

            permission_selector = "*"
            if permissions is not None:
                permissions = [
                    permission.strip()
                    for permission in permissions
                    if permission.strip()
                ]
                if not permissions:
                    raise ValueError("The list of permissions is empty")

                else:
                    permission_selector = ",".join(permissions)

            address_selector = "*"
            if addresses is not None:
                if not addresses:
                    raise ValueError("The list of addresses is empty")
                address_selector = ",".join(addresses)

            args = [
                PermissionController.MULTICHAIN_ARG,
                blockchain_name,
                PermissionController.GET_PERMISSION_ARG,
                permission_selector,
                address_selector,
                json.dumps(verbose),
            ]
            output = run(args, check=True, capture_output=True)

            return json.loads(output.stdout)
        except CalledProcessError as err:
            raise MultiChainError(err.stderr)
        except ValueError as err:
            raise err
        except Exception as err:
            raise err
Exemple #8
0
    def get_items_by_publishers(
        blockchain_name: str,
        stream: str,
        publishers: list,
        verbose: bool = DEFAULT_VERBOSE_VALUE,
    ):
        """
        Retrieves items in stream which match all of the specified publishers in query. 
        The query is an object with a publishers field. The publishers field should 
        specify an array of publishers. Note that, unlike other stream retrieval APIs, 
        liststreamqueryitems cannot rely completely on prior indexing, 
        so the maxqueryscanitems runtime parameter limits how many 
        items will be scanned after using the best index. If more than 
        this is needed, an error will be returned.
        """
        try:
            blockchain_name = blockchain_name.strip()
            stream = stream.strip()
            publishers = [
                publisher.strip() for publisher in publishers
                if publisher.strip()
            ]

            if not stream:
                raise ValueError("Stream name can't be empty")

            if not publishers:
                raise ValueError("Publishers can't be empty")

            if not blockchain_name:
                raise ValueError("Blockchain name can't be empty")

            publisher_label = "publishers"

            if len(publishers) == 1:
                publisher_label = "publisher"
                publishers = publishers[0]

            args = [
                DataController.MULTICHAIN_ARG,
                blockchain_name,
                DataController.GET_STREAM_KEYS_ITEMS_ARG,
                stream,
                json.dumps({publisher_label: publishers}),
                json.dumps(verbose),
            ]
            items = run(args, check=True, capture_output=True)

            return json.loads(items.stdout)
        except CalledProcessError as err:
            raise MultiChainError(err.stderr)
        except ValueError as err:
            raise err
        except Exception as err:
            raise err
Exemple #9
0
    def get_stream_keys(
        blockchain_name: str,
        stream: str,
        keys: list = DEFAULT_KEYS_LIST_CONTENT,
        verbose: bool = DEFAULT_VERBOSE_VALUE,
        count: int = DEFAULT_ITEM_COUNT_VALUE,
        start: int = DEFAULT_ITEM_START_VALUE,
        local_ordering: bool = DEFAULT_LOCAL_ORDERING_VALUE,
    ):
        """
        Provides information about stream keys that belong to a stream, 
        passed as a stream name. Pass an array of keys for specifc infromation regarding
        the provided keys, or use the default value for all keys. Set verbose to true to include 
        information about the first and last item by each key shown. 
        See liststreamitems for details of the count, start and local-ordering parameters.
        """
        try:
            blockchain_name = blockchain_name.strip()
            stream = stream.strip()

            if not stream:
                raise ValueError("Stream name can't be empty")

            if not blockchain_name:
                raise ValueError("Blockchain name can't be empty")

            keys_selector = "*"
            if keys is not None:
                keys = [key.strip() for key in keys if key.strip()]
                if not keys:
                    raise ValueError("Addresses can't be empty")
                keys_selector = json.dumps(keys)

            args = [
                DataController.MULTICHAIN_ARG,
                blockchain_name,
                DataController.GET_STREAM_KEYS_ARG,
                stream,
                keys_selector,
                json.dumps(verbose),
                json.dumps(count),
                json.dumps(start),
                json.dumps(local_ordering),
            ]
            stream_keys = run(args, check=True, capture_output=True)

            return json.loads(stream_keys.stdout)
        except CalledProcessError as err:
            raise MultiChainError(err.stderr)
        except ValueError as err:
            raise err
        except Exception as err:
            raise err
    def grant_global_permission(
        blockchain_name: str, addresses: list, permissions: list
    ):
        """
        Grants permissions to a list of addresses. 
        Set permissions to a list of connect, send, receive, create, issue, mine, 
        activate, admin. 
        Returns the txid of the transaction granting the permissions.
        """
        try:
            blockchain_name = blockchain_name.strip()
            if not blockchain_name:
                raise ValueError("Blockchain name can't be empty")

            addresses = [address.strip() for address in addresses if address.strip()]
            if not addresses:
                raise ValueError("The list of addresses is empty")

            permissions = [
                permission.strip() for permission in permissions if permission.strip()
            ]
            if not permissions:
                raise ValueError("The list of permissions is empty")

            if not set(permissions).issubset(
                PermissionController.GLOBAL_PERMISSIONS_LIST
            ):
                raise ValueError(
                    "The permission(s) proivded: "
                    + str(permissions)
                    + " does not exist."
                )

            args = [
                PermissionController.MULTICHAIN_ARG,
                blockchain_name,
                PermissionController.GRANT_ARG,
                ",".join(addresses),
                ",".join(permissions),
            ]
            output = run(args, check=True, capture_output=True)

            return output.stdout
        except CalledProcessError as err:
            raise MultiChainError(err.stderr)
        except ValueError as err:
            raise err
        except Exception as err:
            raise err
Exemple #11
0
 def connect_to_admin_node(admin_node_address: str):
     """
     Initializes the connection between the current node and the admin
     :param admin_node_address: Node address of the admin node
     If successful, it returns a wallet address, which must used on the admin node to verify
     """
     cmd = NodeController.MULTICHAIN_D_ARG + [admin_node_address]
     try:
         output = run(cmd, capture_output=True, check=True)
         return (re.findall(r"(?<=grant )(.*)(?= connect\\n)",
                            str(output.stdout.strip())))[0]
     except CalledProcessError as err:
         raise MultiChainError(err.stderr)
     except Exception as err:
         raise err
Exemple #12
0
    def get_stream_items(
        blockchain_name: str,
        stream: str,
        verbose: bool = DEFAULT_VERBOSE_VALUE,
        count: int = DEFAULT_ITEM_COUNT_VALUE,
        start: int = DEFAULT_ITEM_START_VALUE,
        local_ordering: bool = DEFAULT_VERBOSE_VALUE,
    ):
        """
        Retrieves items in stream, passed as a stream name. 
        Set verbose to true for additional information about each item’s transaction. 
        Use count and start to retrieve part of the list only, with negative start 
        values (like the default) indicating the most recent items. 
        Set local-ordering to true to order items by when first seen by this node, 
        rather than their order in the chain. If an item’s data is larger than 
        the maxshowndata runtime parameter, it will be returned as an object 
        whose fields can be used with gettxoutdata.
        """
        try:
            blockchain_name = blockchain_name.strip()
            stream = stream.strip()

            if not stream:
                raise ValueError("Stream name can't be empty")

            if not blockchain_name:
                raise ValueError("Blockchain name can't be empty")

            args = [
                DataController.MULTICHAIN_ARG,
                blockchain_name,
                DataController.GET_STERAM_ITEMS_ARG,
                stream,
                json.dumps(verbose),
                json.dumps(count),
                json.dumps(start),
                json.dumps(local_ordering),
            ]
            items = run(args, check=True, capture_output=True)

            return json.loads(items.stdout)
        except CalledProcessError as err:
            raise MultiChainError(err.stderr)
        except ValueError as err:
            raise err
        except Exception as err:
            raise err
Exemple #13
0
    def get_items_by_key(
        blockchain_name: str,
        stream: str,
        key: str,
        verbose: bool = DEFAULT_VERBOSE_VALUE,
        count: int = DEFAULT_ITEM_COUNT_VALUE,
        start: int = DEFAULT_ITEM_START_VALUE,
        local_ordering: bool = DEFAULT_LOCAL_ORDERING_VALUE,
    ):
        """
        Retrieves items that belong to the specified key from stream, passed as a stream name to 
        which the node must be subscribed. Set verbose to true for additional 
        information about the item’s transaction. If an item’s data is larger 
        than the maxshowndata runtime parameter, it will be returned as an 
        object whose fields can be used with gettxoutdata.
        """
        try:
            blockchain_name = blockchain_name.strip()
            stream = stream.strip()
            key = key.strip()

            if not stream:
                raise ValueError("Stream name can't be empty")

            if not key:
                raise ValueError("key can't be empty")

            if not blockchain_name:
                raise ValueError("Blockchain name can't be empty")

            args = [
                DataController.MULTICHAIN_ARG,
                blockchain_name,
                DataController.GET_STREAM_KEY_ITEMS_ARG,
                stream,
                key,
                json.dumps(verbose),
                json.dumps(count),
                json.dumps(start),
                json.dumps(local_ordering),
            ]
            items = run(args, check=True, capture_output=True)
            return json.loads(items.stdout)
        except CalledProcessError as err:
            raise MultiChainError(err.stderr)
        except Exception as err:
            raise err
    def revoke_stream_permission(
        blockchain_name: str, address: str, stream_name: str, permission: str
    ):
        """
        Revokes permissions from an address for a stream. 
        set permission to one of write, activate, admin.
        Returns the txid of transaction revoking the permissions. 
        """
        try:
            blockchain_name = blockchain_name.strip()
            if not blockchain_name:
                raise ValueError("Blockchain name can't be empty")

            stream_name = stream_name.strip()
            permission = permission.strip()

            if not address:
                raise ValueError("The address is empty")

            if not stream_name:
                raise ValueError("The stream name is empty")

            if not permission:
                raise ValueError("The permission is empty")

            if permission.lower() not in PermissionController.STREAM_PERMISSIONS_LIST:
                raise ValueError(
                    "The permission provided: " + permission + " does not exist."
                )

            args = [
                PermissionController.MULTICHAIN_ARG,
                blockchain_name,
                PermissionController.REVOKE_ARG,
                address,
                stream_name + "." + permission.lower(),
            ]
            output = run(args, check=True, capture_output=True)

            return output.stdout
        except CalledProcessError as err:
            raise MultiChainError(err.stderr)
        except ValueError as err:
            raise err
        except Exception as err:
            raise err
Exemple #15
0
 def add_node(blockchain_name: str, new_node_wallet_address: str):
     """
     Adds a node to the blockchain network with the provided wallet address of the node
     that was generated in the connection process.
     :param new_node_wallet_address:
     :return:
     """
     try:
         cmd = (NodeController.MULTICHAIN_CLI_ARG + [blockchain_name] +
                NodeController.GRANT_ARG + [new_node_wallet_address] +
                NodeController.CONNECT_ARG)
         output = run(cmd, capture_output=True, check=True)
         return output.stdout.strip().decode("utf-8")
     except CalledProcessError as err:
         raise MultiChainError(err.stderr)
     except Exception as err:
         raise err
Exemple #16
0
    def get_peer_info(blockchain_name: str):
        """
        Returns information about the other nodes to which this node is connected. The main information that is returned is:
        "lastsend":                (numeric) The date and time of the last send
        "lastrecv":                (numeric) The data and time of the last receive
        "bytessent":               (numeric) The total bytes sent
        "bytesrecv":               (numeric) The total bytes received
        "conntime":                (numeric) The connection date and time
        "timeoffset":              (numeric) The time offset in seconds
        "pingtime":                (numeric) ping time (if available)
        """
        try:
            blockchain_name = blockchain_name.strip()
            if not blockchain_name:
                raise ValueError("Blockchain name can't be empty")

            args = [
                NetworkController.MULTICHAIN_ARG,
                blockchain_name,
                NetworkController.GET_PEER_INFO_ARG,
            ]
            output = run(args, check=True, capture_output=True)

            json_peer_info = json.loads(output.stdout)

            # Iterate over each peer and convert the time in seconds since epoch (Jan 1 1970 GMT)
            # to a human readable date and time
            for peer in json_peer_info:
                peer["lastsend"] = time.strftime(
                    NetworkController.TARGET_DATE_TIME_FORMAT,
                    time.localtime(int(peer["lastsend"])),
                )
                peer["lastrecv"] = time.strftime(
                    NetworkController.TARGET_DATE_TIME_FORMAT,
                    time.localtime(int(peer["lastrecv"])),
                )
                peer["conntime"] = time.strftime(
                    NetworkController.TARGET_DATE_TIME_FORMAT,
                    time.localtime(int(peer["conntime"])),
                )

            return json_peer_info
        except CalledProcessError as err:
            raise MultiChainError(err.stderr)
        except Exception as err:
            raise err
Exemple #17
0
    def get_streams(
        blockchain_name: str,
        streams: list = DEFAULT_STREAMS_LIST_CONTENT,
        verbose: bool = DEFAULT_VERBOSE_VALUE,
        count: int = DEFAULT_STREAM_COUNT_VALUE,
        start: int = DEFAULT_STREAM_START_VALUE,
    ):
        """
        Returns information about streams created on the blockchain. Pass an array
        of stream name(s) to retrieve information about the stream(s), 
        or use the default value for all streams. Use count and start to retrieve part of the 
        list only, with negative start values (like the default) indicating the most recently 
        created streams. Extra fields are shown for streams to which this node has subscribed.
        """
        try:
            blockchain_name = blockchain_name.strip()
            if not blockchain_name:
                raise ValueError("Blockchain name can't be empty")

            stream_selector = "*"
            if streams is not None:
                streams = [
                    stream.strip() for stream in streams if stream.strip()
                ]
                if not streams:
                    raise ValueError("Stream names can't be empty")
                stream_selector = json.dumps(streams)

            args = [
                DataStreamController.MULTICHAIN_ARG,
                blockchain_name,
                DataStreamController.GET_STREAMS_ARG,
                stream_selector,
                json.dumps(verbose),
                json.dumps(count),
                json.dumps(start),
            ]
            streams = run(args, check=True, capture_output=True)
            return json.loads(streams.stdout)
        except CalledProcessError as err:
            raise MultiChainError(err.stderr)
        except ValueError as err:
            raise err
        except Exception as err:
            raise err
Exemple #18
0
    def create_chain(blockchain_name: str, params_path="", install_path=""):

        """
        Creates a new blockcahin with the provided name.
        Returns messange acknowledging that blockchain has been succesfully created :
        MultiChain 2.0 alpha 5 Utilities (latest protocol 20004)

        Blockchain parameter set was successfully generated.
        You can edit it in /home/darshgrewal/.multichain/blockchain_name/params.dat before running multichaind for the first time.

        To generate blockchain please run "multichaind blockchain_name -daemon".
        """
        try:
            cmd = ConfigurationController.CREATE_ARG + [blockchain_name]+[ConfigurationController.DATA_DIR_ARG+ConfigurationController.validate_params_path(params_path)]
            output = subprocess.run(cmd, check=True, capture_output=True, cwd=ConfigurationController.validate_install_path(install_path))
            config = ConfigObj(ConfigurationController.validate_params_path(params_path)+blockchain_name + ConfigurationController.PARAMS_FILE)
            config[ConfigurationController.MINE_EMPTY_ROUNDS] = ConfigurationController.MINE_EMPTY_ROUNDS_VALUE
            config.write()
            return output.stdout.strip()
        except CalledProcessError as err:
            raise MultiChainError(err.stderr)
        except Exception as err:
            raise err
Exemple #19
0
    def subscribe(blockchain_name: str,
                  streams: list,
                  rescan: bool = DEFAULT_RESCAN_VALUE):
        """
        Instructs the node to start tracking one or more stream(s). 
        These are specified using an array of one or more items. 
        If rescan is true, the node will reindex all items from when the streams 
        were created, as well as those in other subscribed entities. 
        Returns True if successful.
        """
        try:
            blockchain_name = blockchain_name.strip()
            if not blockchain_name:
                raise ValueError("Blockchain name can't be empty")

            streams = [stream.strip() for stream in streams if stream.strip()]
            if not streams:
                raise ValueError("Stream names can't be empty")

            args = [
                DataStreamController.MULTICHAIN_ARG,
                blockchain_name,
                DataStreamController.SUBSCRIBE_TO_STREAM_ARG,
                json.dumps(streams),
                json.dumps(rescan),
            ]
            output = run(args, check=True, capture_output=True)

            # returns True if output is empty (meaning it was a success)
            #
            return not output.stdout.strip()
        except CalledProcessError as err:
            raise MultiChainError(err.stderr)
        except ValueError as err:
            raise err
        except Exception as err:
            raise err