# coding=utf-8
from __future__ import absolute_import, division, print_function, \
    unicode_literals

from typing import Dict, Iterable, Optional, Text

from six import with_metaclass

from iota import AdapterSpec, Address, BundleHash, ProposedTransaction, Tag, \
    TransactionHash, TransactionTrytes, TryteString, TrytesCompatible
from iota.adapter import BaseAdapter, resolve_adapter
from iota.commands import BaseCommand, CustomCommand, core, \
    discover_commands, extended
from iota.commands.extended.helpers import Helpers
from iota.crypto.addresses import AddressGenerator
from iota.crypto.types import Seed

__all__ = [
    'InvalidCommand',
    'Iota',
    'StrictIota',
]

class InvalidCommand(ValueError):
    """
    Indicates that an invalid command name was specified.
    """
    pass

class ApiMeta(type):
    """
    Manages command registries for IOTA API base classes.
    """

    def __init__(cls, name, bases=None, attrs=None):
        super(ApiMeta, cls).__init__(name, bases, attrs)

        if not hasattr(cls, 'commands'):
            cls.commands = {}

        # Copy command registry from base class to derived class, but
        # in the event of a conflict, preserve the derived class'
        # commands.
        commands = {}
        for base in bases:
            if isinstance(base, ApiMeta):
                commands.update(base.commands)

        if commands:
            commands.update(cls.commands)
            cls.commands = commands

class StrictIota(with_metaclass(ApiMeta)):
    """
    API to send HTTP requests for communicating with an IOTA node.

    This implementation only exposes the "core" API methods.  For a more
    feature-complete implementation, use :py:class:`Iota` instead.

    References:

    - https://iota.readme.io/docs/getting-started
    """
    commands = discover_commands('iota.commands.core')

    def __init__(self, adapter, testnet=False):
        # type: (AdapterSpec, bool) -> None
        """
        :param adapter:
            URI string or BaseAdapter instance.

        :param testnet:
            Whether to use testnet settings for this instance.
        """
        super(StrictIota, self).__init__()

        if not isinstance(adapter, BaseAdapter):
            adapter = resolve_adapter(adapter)

        self.adapter = adapter  # type: BaseAdapter
        self.testnet = testnet

    def __getattr__(self, command):
        # type: (Text) -> BaseCommand
        """
        Creates a pre-configured command instance.

        This method will only return commands supported by the API
        class.

        If you want to execute an arbitrary API command, use
        :py:meth:`create_command`.

        :param command:
            The name of the command to create.

        References:

        - https://iota.readme.io/docs/making-requests
        """
        # Fix an error when invoking :py:func:`help`.
        # https://github.com/iotaledger/iota.lib.py/issues/41
        if command == '__name__':
            # noinspection PyTypeChecker
            return None

        # Fix an error when invoking dunder methods.
        # https://github.com/iotaledger/iota.lib.py/issues/206
        if command.startswith("__"):
            # noinspection PyUnresolvedReferences
            return super(StrictIota, self).__getattr__(command)

        try:
            command_class = self.commands[command]
        except KeyError:
            raise InvalidCommand(
                '{cls} does not support {command!r} command.'.format(
                    cls=type(self).__name__,
                    command=command,
                ),
            )

        return command_class(self.adapter)

    def create_command(self, command):
        # type: (Text) -> CustomCommand
        """
        Creates a pre-configured CustomCommand instance.

        This method is useful for invoking undocumented or experimental
        methods, or if you just want to troll your node for awhile.

        :param command:
            The name of the command to create.
        """
        return CustomCommand(self.adapter, command)

    @property
    def default_min_weight_magnitude(self):
        # type: () -> int
        """
        Returns the default ``min_weight_magnitude`` value to use for
        API requests.
        """
        return 9 if self.testnet else 14

    def add_neighbors(self, uris):
        # type: (Iterable[Text]) -> dict
        """
        Add one or more neighbors to the node.  Lasts until the node is
        restarted.

        :param uris:
            Use format ``<protocol>://<ip address>:<port>``.
            Example: ``add_neighbors(['udp://example.com:14265'])``

            .. note::
                These URIs are for node-to-node communication (e.g.,
                weird things will happen if you specify a node's HTTP
                API URI here).

        References:

        - https://iota.readme.io/docs/addneighors
        """
        return core.AddNeighborsCommand(self.adapter)(uris=uris)

    def attach_to_tangle(
            self,
            trunk_transaction,  # type: TransactionHash
            branch_transaction,  # type: TransactionHash
            trytes,  # type: Iterable[TryteString]
            min_weight_magnitude=None,  # type: Optional[int]
    ):
        # type: (...) -> dict
        """
        Attaches the specified transactions (trytes) to the Tangle by
        doing Proof of Work. You need to supply branchTransaction as
        well as trunkTransaction (basically the tips which you're going
        to validate and reference with this transaction) - both of which
        you'll get through the getTransactionsToApprove API call.

        The returned value is a different set of tryte values which you
        can input into :py:meth:`broadcast_transactions` and
        :py:meth:`store_transactions`.

        References:

        - https://iota.readme.io/docs/attachtotangle
        """
        if min_weight_magnitude is None:
            min_weight_magnitude = self.default_min_weight_magnitude

        return core.AttachToTangleCommand(self.adapter)(
            trunkTransaction=trunk_transaction,
            branchTransaction=branch_transaction,
            minWeightMagnitude=min_weight_magnitude,
            trytes=trytes,
        )

    def broadcast_transactions(self, trytes):
        # type: (Iterable[TryteString]) -> dict
        """
        Broadcast a list of transactions to all neighbors.

        The input trytes for this call are provided by
        :py:meth:`attach_to_tangle`.

        References:

        - https://iota.readme.io/docs/broadcasttransactions
        """
        return core.BroadcastTransactionsCommand(self.adapter)(trytes=trytes)

    def check_consistency(self, tails):
        # type: (Iterable[TransactionHash]) -> dict
        """
        Used to ensure tail resolves to a consistent ledger which is
        necessary to validate before attempting promotionChecks
        transaction hashes for promotability.

        This is called with a pending transaction (or more of them) and
        it will tell you if it is still possible for this transaction
        (or all the transactions simultaneously if you give more than
        one) to be confirmed, or not (because it conflicts with another
        already confirmed transaction).

        :param tails:
            Transaction hashes. Must be tail transactions.

        :return:
            Dict with the following structure::

                {
                    'state': bool,

                    'info': str,
                        This field will only exist set if ``state`` is
                        ``False``.
                }
        """
        return core.CheckConsistencyCommand(self.adapter)(
            tails=tails,
        )

    def find_transactions(
            self,
            bundles=None,  # type: Optional[Iterable[BundleHash]]
            addresses=None,  # type: Optional[Iterable[Address]]
            tags=None,  # type: Optional[Iterable[Tag]]
            approvees=None,  # type: Optional[Iterable[TransactionHash]]
    ):
        # type: (...) -> dict
        """
        Find the transactions which match the specified input and
        return.

        All input values are lists, for which a list of return values
        (transaction hashes), in the same order, is returned for all
        individual elements.

        Using multiple of these input fields returns the intersection of
        the values.

        :param bundles:
          List of bundle IDs.

        :param addresses:
            List of addresses.

        :param tags:
            List of tags.

        :param approvees:
            List of approvee transaction IDs.

        References:

        - https://iota.readme.io/docs/findtransactions
        """
        return core.FindTransactionsCommand(self.adapter)(
            bundles=bundles,
            addresses=addresses,
            tags=tags,
            approvees=approvees,
        )

    def get_balances(self, addresses, threshold=100):
        # type: (Iterable[Address], int) -> dict
        """
        Similar to :py:meth:`get_inclusion_states`. Returns the
        confirmed balance which a list of addresses have at the latest
        confirmed milestone.

        In addition to the balances, it also returns the milestone as
        well as the index with which the confirmed balance was
        determined.  The balances are returned as a list in the same
        order as the addresses were provided as input.

        :param addresses:
            List of addresses to get the confirmed balance for.

        :param threshold:
            Confirmation threshold.

        References:

        - https://iota.readme.io/docs/getbalances
        """
        return core.GetBalancesCommand(self.adapter)(
            addresses=addresses,
            threshold=threshold,
        )

    def get_inclusion_states(self, transactions, tips):
        # type: (Iterable[TransactionHash], Iterable[TransactionHash]) -> dict
        """
        Get the inclusion states of a set of transactions. This is for
        determining if a transaction was accepted and confirmed by the
        network or not. You can search for multiple tips (and thus,
        milestones) to get past inclusion states of transactions.

        :param transactions:
            List of transactions you want to get the inclusion state
            for.

        :param tips:
            List of tips (including milestones) you want to search for
            the inclusion state.

        References:

        - https://iota.readme.io/docs/getinclusionstates
        """
        return core.GetInclusionStatesCommand(self.adapter)(
            transactions=transactions,
            tips=tips,
        )

    def get_neighbors(self):
        # type: () -> dict
        """
        Returns the set of neighbors the node is connected with, as well
        as their activity count.

        The activity counter is reset after restarting IRI.

        References:

        - https://iota.readme.io/docs/getneighborsactivity
        """
        return core.GetNeighborsCommand(self.adapter)()

    def get_node_info(self):
        # type: () -> dict
        """
        Returns information about the node.

        References:

        - https://iota.readme.io/docs/getnodeinfo
        """
        return core.GetNodeInfoCommand(self.adapter)()

    def get_tips(self):
        # type: () -> dict
        """
        Returns the list of tips (transactions which have no other
        transactions referencing them).

        References:

        - https://iota.readme.io/docs/gettips
        - https://iota.readme.io/docs/glossary#iota-terms
        """
        return core.GetTipsCommand(self.adapter)()

    def get_transactions_to_approve(self, depth):
        # type: (int) -> dict
        """
        Tip selection which returns ``trunkTransaction`` and
        ``branchTransaction``.

        :param depth:
          Determines how many bundles to go back to when finding the
          transactions to approve.

          The higher the depth value, the more "babysitting" the node
          will perform for the network (as it will confirm more
          transactions that way).

        References:

        - https://iota.readme.io/docs/gettransactionstoapprove
        """
        return core.GetTransactionsToApproveCommand(self.adapter)(depth=depth)

    def get_trytes(self, hashes):
        # type: (Iterable[TransactionHash]) -> dict
        """
        Returns the raw transaction data (trytes) of one or more
        transactions.

        References:

        - https://iota.readme.io/docs/gettrytes
        """
        return core.GetTrytesCommand(self.adapter)(hashes=hashes)

    def interrupt_attaching_to_tangle(self):
        # type: () -> dict
        """
        Interrupts and completely aborts the :py:meth:`attach_to_tangle`
        process.

        References:

        - https://iota.readme.io/docs/interruptattachingtotangle
        """
        return core.InterruptAttachingToTangleCommand(self.adapter)()

    def remove_neighbors(self, uris):
        # type: (Iterable[Text]) -> dict
        """
        Removes one or more neighbors from the node.  Lasts until the
        node is restarted.

        :param uris:
            Use format ``<protocol>://<ip address>:<port>``.
            Example: `remove_neighbors(['udp://example.com:14265'])`

        References:

        - https://iota.readme.io/docs/removeneighors
        """
        return core.RemoveNeighborsCommand(self.adapter)(uris=uris)

    def store_transactions(self, trytes):
        # type: (Iterable[TryteString]) -> dict
        """
        Store transactions into local storage.

        The input trytes for this call are provided by
        :py:meth:`attach_to_tangle`.

        References:

        - https://iota.readme.io/docs/storetransactions
        """
        return core.StoreTransactionsCommand(self.adapter)(trytes=trytes)

    def were_addresses_spent_from(self, addresses):
        # type: (Iterable[Address]) -> dict
        """
        Check if a list of addresses was ever spent from, in the current
        epoch, or in previous epochs.

        :param addresses:
            List of addresses to check.

        References:

        - https://iota.readme.io/docs/wereaddressesspentfrom
        """
        return core.WereAddressesSpentFromCommand(self.adapter)(
            addresses=addresses,
        )

class Iota(StrictIota):
    """
    Implements the core API, plus additional wrapper methods for common
    operations.

    References:

    - https://iota.readme.io/docs/getting-started
    - https://github.com/iotaledger/wiki/blob/master/api-proposal.md
    """
    commands = discover_commands('iota.commands.extended')

    def __init__(self, adapter, seed=None, testnet=False):
        # type: (AdapterSpec, Optional[TrytesCompatible], bool) -> None
        """
        :param seed:
            Seed used to generate new addresses.
            If not provided, a random one will be generated.

            .. note::
                This value is never transferred to the node/network.
        """
        super(Iota, self).__init__(adapter, testnet)

        self.seed = Seed(seed) if seed else Seed.random()
        self.helpers = Helpers(self)

    def broadcast_and_store(self, trytes):
        # type: (Iterable[TransactionTrytes]) -> dict
        """
        Broadcasts and stores a set of transaction trytes.

        :return:
            Dict with the following structure::

                {
                    'trytes': List[TransactionTrytes],
                        List of TransactionTrytes that were broadcast.
                        Same as the input ``trytes``.
                }

        References:

        - https://github.com/iotaledger/wiki/blob/master/api-proposal.md#broadcastandstore
        """
        return extended.BroadcastAndStoreCommand(self.adapter)(trytes=trytes)

    def get_account_data(self, start=0, stop=None, inclusion_states=False, security_level=None):
        # type: (int, Optional[int], bool, Optional[int]) -> dict
        """
        More comprehensive version of :py:meth:`get_transfers` that
        returns addresses and account balance in addition to bundles.

        This function is useful in getting all the relevant information
        of your account.

        :param start:
            Starting key index.

        :param stop:
            Stop before this index.

            Note that this parameter behaves like the ``stop`` attribute
            in a :py:class:`slice` object; the stop index is *not*
            included in the result.

            If ``None`` (default), then this method will check every
            address until it finds one without any transfers.

        :param inclusion_states:
            Whether to also fetch the inclusion states of the transfers.

            This requires an additional API call to the node, so it is
            disabled by default.

        :param security_level:
            Number of iterations to use when generating new addresses
            (see :py:meth:`get_new_addresses`).

            This value must be between 1 and 3, inclusive.

            If not set, defaults to
            :py:attr:`AddressGenerator.DEFAULT_SECURITY_LEVEL`.

        :return:
            Dict with the following structure::

                {
                    'addresses': List[Address],
                        List of generated addresses.

                        Note that this list may include unused
                        addresses.

                    'balance': int,
                        Total account balance.  Might be 0.

                    'bundles': List[Bundle],
                        List of bundles with transactions to/from this
                        account.
                }
        """
        return extended.GetAccountDataCommand(self.adapter)(
            seed=self.seed,
            start=start,
            stop=stop,
            inclusionStates=inclusion_states,
            security_level=security_level
        )

    def get_bundles(self, transaction):
        # type: (TransactionHash) -> dict
        """
        Returns the bundle(s) associated with the specified transaction
        hash.

        :param transaction:
            Transaction hash.  Must be a tail transaction.

        :return:
            Dict with the following structure::

             {
               'bundles': List[Bundle],
                    List of matching bundles.  Note that this value is
                    always a list, even if only one bundle was found.
             }

        :raise:
          - :py:class:`iota.adapter.BadApiResponse` if any of the
            bundles fails validation.

        References:

        - https://github.com/iotaledger/wiki/blob/master/api-proposal.md#getbundle
        """
        return extended.GetBundlesCommand(self.adapter)(transaction=transaction)

    def get_inputs(
            self,
            start=0,
            stop=None,
            threshold=None,
            security_level=None,
    ):
        # type: (int, Optional[int], Optional[int], Optional[int]) -> dict
        """
        Gets all possible inputs of a seed and returns them, along with
        the total balance.

        This is either done deterministically (by generating all
        addresses until :py:meth:`find_transactions` returns an empty
        result), or by providing a key range to search.

        :param start:
            Starting key index.
            Defaults to 0.

        :param stop:
            Stop before this index.

            Note that this parameter behaves like the ``stop`` attribute
            in a :py:class:`slice` object; the stop index is *not*
            included in the result.

            If ``None`` (default), then this method will not stop until
            it finds an unused address.

        :param threshold:
            If set, determines the minimum threshold for a successful
            result:

            - As soon as this threshold is reached, iteration will stop.
            - If the command runs out of addresses before the threshold
              is reached, an exception is raised.

            .. note::
                This method does not attempt to "optimize" the result
                (e.g., smallest number of inputs, get as close to
                ``threshold`` as possible, etc.); it simply accumulates
                inputs in order until the threshold is met.

            If ``threshold`` is 0, the first address in the key range
            with a non-zero balance will be returned (if it exists).

            If ``threshold`` is ``None`` (default), this method will
            return **all** inputs in the specified key range.

        :param security_level:
            Number of iterations to use when generating new addresses
            (see :py:meth:`get_new_addresses`).

            This value must be between 1 and 3, inclusive.

            If not set, defaults to
            :py:attr:`AddressGenerator.DEFAULT_SECURITY_LEVEL`.

        :return:
            Dict with the following structure::

                {
                    'inputs': List[Address],
                        Addresses with nonzero balances that can be used
                        as inputs.

                    'totalBalance': int,
                        Aggregate balance from all matching addresses.
                }

            Note that each Address in the result has its ``balance``
            attribute set.

            Example:

            .. code-block:: python

                response = iota.get_inputs(...)

                input0 = response['inputs'][0] # type: Address
                input0.balance # 42

        :raise:
            - :py:class:`iota.adapter.BadApiResponse` if ``threshold``
              is not met.  Not applicable if ``threshold`` is ``None``.

        References:

        - https://github.com/iotaledger/wiki/blob/master/api-proposal.md#getinputs
        """
        return extended.GetInputsCommand(self.adapter)(
            seed=self.seed,
            start=start,
            stop=stop,
            threshold=threshold,
            securityLevel=security_level
        )

    def get_latest_inclusion(self, hashes):
        # type: (Iterable[TransactionHash]) -> Dict[TransactionHash, bool]
        """
        Fetches the inclusion state for the specified transaction
        hashes, as of the latest milestone that the node has processed.

        Effectively, this is ``getNodeInfo`` + ``getInclusionStates``.

        :param hashes:
            Iterable of transaction hashes.

        :return:
            Dict with the following structure::

                {
                    "states": Dict[TransactionHash, bool]
                        Dict with one boolean per transaction hash in
                        ``hashes``.
                }
        """
        return extended.GetLatestInclusionCommand(self.adapter)(hashes=hashes)

    def get_new_addresses(
            self,
            index=0,
            count=1,
            security_level=AddressGenerator.DEFAULT_SECURITY_LEVEL,
            checksum=False,
    ):
        # type: (int, Optional[int], int, bool) -> dict
        """
        Generates one or more new addresses from the seed.

        :param index:
            The key index of the first new address to generate (must be
            >= 1).

        :param count:
            Number of addresses to generate (must be >= 1).

            .. tip::
                This is more efficient than calling ``get_new_address``
                inside a loop.

            If ``None``, this method will progressively generate
            addresses and scan the Tangle until it finds one that has no
            transactions referencing it.

        :param security_level:
            Number of iterations to use when generating new addresses.

            Larger values take longer, but the resulting signatures are
            more secure.

            This value must be between 1 and 3, inclusive.

        :param checksum:
            Specify whether to return the address with the checksum.
            Defaults to ``False``.

        :return:
            Dict with the following structure::

            {
                'addresses': List[Address],
                    Always a list, even if only one address was
                    generated.
            }

        References:

        - https://github.com/iotaledger/wiki/blob/master/api-proposal.md#getnewaddress
        """
        return extended.GetNewAddressesCommand(self.adapter)(
            count=count,
            index=index,
            securityLevel=security_level,
            checksum=checksum,
            seed=self.seed,
        )

    def get_transfers(self, start=0, stop=None, inclusion_states=False):
        # type: (int, Optional[int], bool) -> dict
        """
        Returns all transfers associated with the seed.

        :param start:
            Starting key index.

        :param stop:
            Stop before this index.

            Note that this parameter behaves like the ``stop`` attribute
            in a :py:class:`slice` object; the stop index is *not*
            included in the result.

            If ``None`` (default), then this method will check every
            address until it finds one without any transfers.

        :param inclusion_states:
            Whether to also fetch the inclusion states of the transfers.

            This requires an additional API call to the node, so it is
            disabled by default.

        :return:
            Dict with the following structure::

                {
                    'bundles': List[Bundle],
                        Matching bundles, sorted by tail transaction
                        timestamp.

                        This value is always a list, even if only one
                        bundle was found.
                }

        References:

        - https://github.com/iotaledger/wiki/blob/master/api-proposal.md#gettransfers
        """
        return extended.GetTransfersCommand(self.adapter)(
            seed=self.seed,
            start=start,
            stop=stop,
            inclusionStates=inclusion_states,
        )

    def prepare_transfer(
            self,
            transfers,  # type: Iterable[ProposedTransaction]
            inputs=None,  # type: Optional[Iterable[Address]]
            change_address=None,  # type: Optional[Address]
            security_level=None,  # type: Optional[int]
    ):
        # type: (...) -> dict
        """
        Prepares transactions to be broadcast to the Tangle, by
        generating the correct bundle, as well as choosing and signing
        the inputs (for value transfers).

        :param transfers:
            Transaction objects to prepare.

        :param inputs:
            List of addresses used to fund the transfer.
            Ignored for zero-value transfers.

            If not provided, addresses will be selected automatically by
            scanning the Tangle for unspent inputs.  Depending on how
            many transfers you've already sent with your seed, this
            process could take awhile.

        :param change_address:
            If inputs are provided, any unspent amount will be sent to
            this address.

            If not specified, a change address will be generated
            automatically.

        :param security_level:
            Number of iterations to use when generating new addresses
            (see :py:meth:`get_new_addresses`).

            This value must be between 1 and 3, inclusive.

            If not set, defaults to
            :py:attr:`AddressGenerator.DEFAULT_SECURITY_LEVEL`.

        :return:
            Dict with the following structure::

                {
                    'trytes': List[TransactionTrytes],
                        Raw trytes for the transactions in the bundle,
                        ready to be provided to :py:meth:`send_trytes`.
                }

        References:

        - https://github.com/iotaledger/wiki/blob/master/api-proposal.md#preparetransfers
        """
        return extended.PrepareTransferCommand(self.adapter)(
            seed=self.seed,
            transfers=transfers,
            inputs=inputs,
            changeAddress=change_address,
            securityLevel=security_level,
        )

    def promote_transaction(
            self,
            transaction,
            depth=3,
            min_weight_magnitude=None,
    ):
        # type: (TransactionHash, int, Optional[int]) -> dict
        """
        Promotes a transaction by adding spam on top of it.

        :return:
            Dict with the following structure::

                {
                    'bundle': Bundle,
                        The newly-published bundle.
                }
        """
        if min_weight_magnitude is None:
            min_weight_magnitude = self.default_min_weight_magnitude

        return extended.PromoteTransactionCommand(self.adapter)(
            transaction=transaction,
            depth=depth,
            minWeightMagnitude=min_weight_magnitude,
        )

    def replay_bundle(
            self,
            transaction,
            depth=3,
            min_weight_magnitude=None,
    ):
        # type: (TransactionHash, int, Optional[int]) -> dict
        """
        Takes a tail transaction hash as input, gets the bundle
        associated with the transaction and then replays the bundle by
        attaching it to the Tangle.

        :param transaction:
            Transaction hash.  Must be a tail.

        :param depth:
            Depth at which to attach the bundle.
            Defaults to 3.

        :param min_weight_magnitude:
            Min weight magnitude, used by the node to calibrate Proof of
            Work.

            If not provided, a default value will be used.

        :return:
            Dict with the following structure::

                {
                    'trytes': List[TransactionTrytes],
                        Raw trytes that were published to the Tangle.
                }

        References:

        - https://github.com/iotaledger/wiki/blob/master/api-proposal.md#replaytransfer
        """
        if min_weight_magnitude is None:
            min_weight_magnitude = self.default_min_weight_magnitude

        return extended.ReplayBundleCommand(self.adapter)(
            transaction=transaction,
            depth=depth,
            minWeightMagnitude=min_weight_magnitude,
        )

    def send_transfer(
            self,
            transfers,  # type: Iterable[ProposedTransaction]
            depth=3,  # type: int
            inputs=None,  # type: Optional[Iterable[Address]]
            change_address=None,  # type: Optional[Address]
            min_weight_magnitude=None,  # type: Optional[int]
            security_level=None,  # type: Optional[int]
    ):
        # type: (...) -> dict
        """
        Prepares a set of transfers and creates the bundle, then
        attaches the bundle to the Tangle, and broadcasts and stores the
        transactions.

        :param transfers:
            Transfers to include in the bundle.

        :param depth:
            Depth at which to attach the bundle.
            Defaults to 3.

        :param inputs:
            List of inputs used to fund the transfer.
            Not needed for zero-value transfers.

        :param change_address:
            If inputs are provided, any unspent amount will be sent to
            this address.

            If not specified, a change address will be generated
            automatically.

        :param min_weight_magnitude:
            Min weight magnitude, used by the node to calibrate Proof of
            Work.

            If not provided, a default value will be used.

        :param security_level:
            Number of iterations to use when generating new addresses
            (see :py:meth:`get_new_addresses`).

            This value must be between 1 and 3, inclusive.

            If not set, defaults to
            :py:attr:`AddressGenerator.DEFAULT_SECURITY_LEVEL`.

        :return:
            Dict with the following structure::

                {
                    'bundle': Bundle,
                        The newly-published bundle.
                }

        References:

        - https://github.com/iotaledger/wiki/blob/master/api-proposal.md#sendtransfer
        """
        if min_weight_magnitude is None:
            min_weight_magnitude = self.default_min_weight_magnitude

        return extended.SendTransferCommand(self.adapter)(
            seed=self.seed,
            depth=depth,
            transfers=transfers,
            inputs=inputs,
            changeAddress=change_address,
            minWeightMagnitude=min_weight_magnitude,
            securityLevel=security_level,
        )

    def send_trytes(self, trytes, depth=3, min_weight_magnitude=None):
        # type: (Iterable[TransactionTrytes], int, Optional[int]) -> dict
        """
        Attaches transaction trytes to the Tangle, then broadcasts and
        stores them.

        :param trytes:
            Transaction encoded as a tryte sequence.

        :param depth:
            Depth at which to attach the bundle.
            Defaults to 3.

        :param min_weight_magnitude:
            Min weight magnitude, used by the node to calibrate Proof of
            Work.

            If not provided, a default value will be used.

        :return:
            Dict with the following structure::

                {
                    'trytes': List[TransactionTrytes],
                        Raw trytes that were published to the Tangle.
                }

        References:

        - https://github.com/iotaledger/wiki/blob/master/api-proposal.md#sendtrytes
        """
        if min_weight_magnitude is None:
            min_weight_magnitude = self.default_min_weight_magnitude

        return extended.SendTrytesCommand(self.adapter)(
            trytes=trytes,
            depth=depth,
            minWeightMagnitude=min_weight_magnitude,
        )

    def is_reattachable(self, addresses):
        # type: (Iterable[Address]) -> dict
        """
        This API function helps you to determine whether you should
        replay a transaction or make a new one (either with the same
        input, or a different one).

        This method takes one or more input addresses (i.e. from spent
        transactions) as input and then checks whether any transactions
        with a value transferred are confirmed.

        If yes, it means that this input address has already been
        successfully used in a different transaction, and as such you
        should no longer replay the transaction.

        :param addresses:
            List of addresses.

        :return:
            Dict with the following structure::

                {
                  'reattachable': List[bool],
                    Always a list, even if only one address was queried.
                }
        """
        return extended.IsReattachableCommand(self.adapter)(
            addresses=addresses
        )


# coding=utf-8
from __future__ import absolute_import, division, print_function, \
    unicode_literals

import json
from abc import ABCMeta, abstractmethod as abstract_method
from collections import deque
from inspect import isabstract as is_abstract
from logging import DEBUG, Logger
from socket import getdefaulttimeout as get_default_timeout
from typing import Container, Dict, List, Optional, Text, Tuple, Union

from requests import Response, auth, codes, request
from six import PY2, binary_type, iteritems, moves as compat, text_type, \
    with_metaclass

from iota.exceptions import with_context
from iota.json import JsonEncoder

__all__ = [
    'API_VERSION',
    'AdapterSpec',
    'BadApiResponse',
    'BaseAdapter',
    'HttpAdapter',
    'InvalidUri',
    'MockAdapter',
    'resolve_adapter',
]

if PY2:
    # Fix an error when importing this package using the ``imp`` library
    # (note: ``imp`` is deprecated since Python 3.4 in favor of
    # ``importlib``).
    # https://docs.python.org/3/library/imp.html
    # https://travis-ci.org/iotaledger/iota.lib.py/jobs/191974244
    __all__ = map(binary_type, __all__)

API_VERSION = '1'
"""
API protocol version.
https://github.com/iotaledger/iota.lib.py/issues/84
"""

# Custom types for type hints and docstrings.
AdapterSpec = Union[Text, 'BaseAdapter']

# Load SplitResult for IDE type hinting and autocompletion.
if PY2:
    # noinspection PyCompatibility,PyUnresolvedReferences
    from urlparse import SplitResult
else:
    # noinspection PyCompatibility,PyUnresolvedReferences
    from urllib.parse import SplitResult

class BadApiResponse(ValueError):
    """
    Indicates that a non-success response was received from the node.
    """
    pass

class InvalidUri(ValueError):
    """
    Indicates that an invalid URI was provided to `resolve_adapter`.
    """
    pass

adapter_registry = {}  # type: Dict[Text, AdapterMeta]
"""
Keeps track of available adapters and their supported protocols.
"""

def resolve_adapter(uri):
    # type: (AdapterSpec) -> BaseAdapter
    """
    Given a URI, returns a properly-configured adapter instance.
    """
    if isinstance(uri, BaseAdapter):
        return uri

    parsed = compat.urllib_parse.urlsplit(uri)  # type: SplitResult

    if not parsed.scheme:
        raise with_context(
            exc=InvalidUri(
                'URI must begin with "<protocol>://" (e.g., "udp://").',
            ),

            context={
                'parsed': parsed,
                'uri': uri,
            },
        )

    try:
        adapter_type = adapter_registry[parsed.scheme]
    except KeyError:
        raise with_context(
            exc=InvalidUri('Unrecognized protocol {protocol!r}.'.format(
                protocol=parsed.scheme,
            )),

            context={
                'parsed': parsed,
                'uri': uri,
            },
        )

    return adapter_type.configure(parsed)

class AdapterMeta(ABCMeta):
    """
    Automatically registers new adapter classes in ``adapter_registry``.
    """

    # noinspection PyShadowingBuiltins
    def __init__(cls, what, bases=None, dict=None):
        super(AdapterMeta, cls).__init__(what, bases, dict)

        if not is_abstract(cls):
            for protocol in getattr(cls, 'supported_protocols', ()):
                # Note that we will not overwrite existing registered
                # adapters.
                adapter_registry.setdefault(protocol, cls)

    def configure(cls, parsed):
        # type: (Union[Text, SplitResult]) -> HttpAdapter
        """
        Creates a new instance using the specified URI.

        :param parsed:
          Result of :py:func:`urllib.parse.urlsplit`.
        """
        return cls(parsed)

class BaseAdapter(with_metaclass(AdapterMeta)):
    """
    Interface for IOTA API adapters.

    Adapters make it easy to customize the way an StrictIota instance
    communicates with a node.
    """
    supported_protocols = ()  # type: Tuple[Text]
    """
    Protocols that ``resolve_adapter`` can use to identify this adapter
    type.
    """

    def __init__(self):
        super(BaseAdapter, self).__init__()

        self._logger = None  # type: Logger

    @abstract_method
    def get_uri(self):
        # type: () -> Text
        """
        Returns the URI that this adapter will use.
        """
        raise NotImplementedError(
            'Not implemented in {cls}.'.format(cls=type(self).__name__),
        )

    @abstract_method
    def send_request(self, payload, **kwargs):
        # type: (dict, dict) -> dict
        """
        Sends an API request to the node.

        :param payload:
            JSON payload.

        :param kwargs:
            Additional keyword arguments for the adapter.

        :return:
            Decoded response from the node.

        :raise:
            - :py:class:`BadApiResponse` if a non-success response was
              received.
        """
        raise NotImplementedError(
            'Not implemented in {cls}.'.format(cls=type(self).__name__),
        )

    def set_logger(self, logger):
        # type: (Logger) -> BaseAdapter
        """
        Attaches a logger instance to the adapter.
        The adapter will send information about API requests/responses
        to the logger.
        """
        self._logger = logger
        return self

    def _log(self, level, message, context=None):
        # type: (int, Text, Optional[dict]) -> None
        """
        Sends a message to the instance's logger, if configured.
        """
        if self._logger:
            self._logger.log(level, message, extra={'context': context or {}})

class HttpAdapter(BaseAdapter):
    """
    Sends standard HTTP requests.
    """
    supported_protocols = ('http', 'https',)

    DEFAULT_HEADERS = {
        'Content-type': 'application/json',

        # https://github.com/iotaledger/iota.lib.py/issues/84
        'X-IOTA-API-Version': API_VERSION,
    }
    """
    Default headers sent with every request.
    These can be overridden on a per-request basis, by specifying values
    in the ``headers`` kwarg.
    """

    def __init__(self, uri, timeout=None, authentication=None):
        # type: (Union[Text, SplitResult], Optional[int]) -> None
        super(HttpAdapter, self).__init__()

        self.timeout = timeout
        self.authentication = authentication

        if isinstance(uri, text_type):
            uri = compat.urllib_parse.urlsplit(uri)  # type: SplitResult

        if uri.scheme not in self.supported_protocols:
            raise with_context(
                exc=InvalidUri('Unsupported protocol {protocol!r}.'.format(
                    protocol=uri.scheme,
                )),

                context={
                    'uri': uri,
                },
            )

        if not uri.hostname:
            raise with_context(
                exc=InvalidUri(
                    'Empty hostname in URI {uri!r}.'.format(
                        uri=uri.geturl(),
                    ),
                ),

                context={
                    'uri': uri,
                },
            )

        try:
            # noinspection PyStatementEffect
            uri.port
        except ValueError:
            raise with_context(
                exc=InvalidUri(
                    'Non-numeric port in URI {uri!r}.'.format(
                        uri=uri.geturl(),
                    ),
                ),

                context={
                    'uri': uri,
                },
            )

        self.uri = uri

    @property
    def node_url(self):
        # type: () -> Text
        """
        Returns the node URL.
        """
        return self.uri.geturl()

    def get_uri(self):
        # type: () -> Text
        return self.uri.geturl()

    def send_request(self, payload, **kwargs):
        # type: (dict, dict) -> dict
        kwargs.setdefault('headers', {})
        for key, value in iteritems(self.DEFAULT_HEADERS):
            kwargs['headers'].setdefault(key, value)

        response = self._send_http_request(
            # Use a custom JSON encoder that knows how to convert Tryte
            # values.
            payload=JsonEncoder().encode(payload),

            url=self.node_url,
            **kwargs
        )

        return self._interpret_response(response, payload, {codes['ok']})

    def _send_http_request(self, url, payload, method='post', **kwargs):
        # type: (Text, Optional[Text], Text, dict) -> Response
        """
        Sends the actual HTTP request.

        Split into its own method so that it can be mocked during unit
        tests.
        """
        kwargs.setdefault(
            'timeout',
            self.timeout if self.timeout else get_default_timeout(),
        )

        if self.authentication:
            kwargs.setdefault('auth', auth.HTTPBasicAuth(*self.authentication))

        self._log(
            level=DEBUG,

            message='Sending {method} to {url}: {payload!r}'.format(
                method=method,
                payload=payload,
                url=url,
            ),

            context={
                'request_method': method,
                'request_kwargs': kwargs,
                'request_payload': payload,
                'request_url': url,
            },
        )

        response = request(method=method, url=url, data=payload, **kwargs)

        self._log(
            level=DEBUG,

            message='Receiving {method} from {url}: {response!r}'.format(
                method=method,
                response=response.content,
                url=url,
            ),

            context={
                'request_method': method,
                'request_kwargs': kwargs,
                'request_payload': payload,
                'request_url': url,

                'response_headers': response.headers,
                'response_content': response.content,
            },
        )

        return response

    def _interpret_response(self, response, payload, expected_status):
        # type: (Response, dict, Container[int]) -> dict
        """
        Interprets the HTTP response from the node.

        :param response:
            The response object received from
            :py:meth:`_send_http_request`.

        :param payload:
            The request payload that was sent (used for debugging).

        :param expected_status:
            The response should match one of these status codes to be
            considered valid.
        """
        raw_content = response.text
        if not raw_content:
            raise with_context(
                exc=BadApiResponse(
                    'Empty {status} response from node.'.format(
                        status=response.status_code,
                    ),
                ),

                context={
                    'request': payload,
                },
            )

        try:
            decoded = json.loads(raw_content)  # type: dict
        # :bc: py2k doesn't have JSONDecodeError
        except ValueError:
            raise with_context(
                exc=BadApiResponse(
                    'Non-JSON {status} response from node: '
                    '{raw_content}'.format(
                        status=response.status_code,
                        raw_content=raw_content,
                    )
                ),

                context={
                    'request': payload,
                    'raw_response': raw_content,
                },
            )

        if not isinstance(decoded, dict):
            raise with_context(
                exc=BadApiResponse(
                    'Malformed {status} response from node: {decoded!r}'.format(
                        status=response.status_code,
                        decoded=decoded,
                    ),
                ),

                context={
                    'request': payload,
                    'response': decoded,
                },
            )

        if response.status_code in expected_status:
            return decoded

        error = None
        try:
            if response.status_code == codes['bad_request']:
                error = decoded['error']
            elif response.status_code == codes['internal_server_error']:
                error = decoded['exception']
        except KeyError:
            pass

        raise with_context(
            exc=BadApiResponse(
                '{status} response from node: {error}'.format(
                    error=error or decoded,
                    status=response.status_code,
                ),
            ),

            context={
                'request': payload,
                'response': decoded,
            },
        )

class MockAdapter(BaseAdapter):
    """
    An mock adapter used for simulating API responses.

    To use this adapter, you must first "seed" the responses that the
    adapter should return for each request.  The adapter will then return
    the appropriate seeded response each time it "sends" a request.
    """
    supported_protocols = ('mock',)

    # noinspection PyUnusedLocal
    @classmethod
    def configure(cls, uri):
        return cls()

    def __init__(self):
        super(MockAdapter, self).__init__()

        self.responses = {}  # type: Dict[Text, deque]
        self.requests = []  # type: List[dict]

    def get_uri(self):
        return 'mock://'

    def seed_response(self, command, response):
        # type: (Text, dict) -> MockAdapter
        """
        Sets the response that the adapter will return for the specified
        command.

        You can seed multiple responses per command; the adapter will
        put them into a FIFO queue.  When a request comes in, the
        adapter will pop the corresponding response off of the queue.

        Example:

        .. code-block:: python

            adapter.seed_response('sayHello', {'message': 'Hi!'})
            adapter.seed_response('sayHello', {'message': 'Hello!'})

            adapter.send_request({'command': 'sayHello'})
            # {'message': 'Hi!'}

            adapter.send_request({'command': 'sayHello'})
            # {'message': 'Hello!'}
        """
        if command not in self.responses:
            self.responses[command] = deque()

        self.responses[command].append(response)
        return self

    def send_request(self, payload, **kwargs):
        # type: (dict, dict) -> dict
        # Store a snapshot so that we can inspect the request later.
        self.requests.append(dict(payload))

        command = payload['command']

        try:
            response = self.responses[command].popleft()
        except KeyError:
            raise with_context(
                exc=BadApiResponse(
                    'No seeded response for {command!r} '
                    '(expected one of: {seeds!r}).'.format(
                        command=command,
                        seeds=list(sorted(self.responses.keys())),
                    ),
                ),

                context={
                    'request': payload,
                },
            )
        except IndexError:
            raise with_context(
                exc=BadApiResponse(
                    '{command} called too many times; '
                    'no seeded responses left.'.format(
                        command=command,
                    ),
                ),

                context={
                    'request': payload,
                },
            )

        error = response.get('exception') or response.get('error')
        if error:
            raise with_context(BadApiResponse(error),
                               context={'request': payload})

        return response


# coding=utf-8
from __future__ import absolute_import, division, print_function, \
    unicode_literals

from typing import List, Optional

from iota.crypto import HASH_LENGTH
from iota.crypto.kerl import Kerl
from iota.crypto.types import Digest
from iota.multisig.types import MultisigAddress

__all__ = [
    'MultisigAddressBuilder',
]

class MultisigAddressBuilder(object):
    """
    Creates multisig addresses.

    Note that this class generates a single address from multiple
    inputs (digests), unlike
    :py:class:`iota.crypto.addresses.AddressGenerator` which generates
    multiple addresses from a single input (seed).
    """

    def __init__(self):
        super(MultisigAddressBuilder, self).__init__()

        self._digests = []  # type: List[Digest]
        """
        Keeps track of digests that were added, so that we can attach
        them to the final :py:class:`MultisigAddress` object.
        """

        self._address = None  # type: Optional[MultisigAddress]
        """
        Caches the generated address.

        Generating the address modifies the internal state of the curl
        sponge, so each :py:class:`MultisigAddressBuilder` instance can
        only generate a single address.
        """

        self._sponge = Kerl()

    def add_digest(self, digest):
        # type: (Digest) -> None
        """
        Absorbs a digest into the sponge.

        .. important::
            Keep track of the order that digests are added!

            To spend inputs from a multisig address, you must provide
            the private keys in the same order!

        References:

        - https://github.com/iotaledger/wiki/blob/master/multisigs.md#spending-inputs
        """
        if self._address:
            raise ValueError('Cannot add digests once an address is extracted.')

        self._sponge.absorb(digest.as_trits())
        self._digests.append(digest)

    def get_address(self):
        # type: () -> MultisigAddress
        """
        Returns the new multisig address.

        Note that you can continue to add digests after extracting an
        address; the next address will use *all* of the digests that
        have been added so far.
        """
        if not self._digests:
            raise ValueError(
                'Must call ``add_digest`` at least once '
                'before calling ``get_address``.',
            )

        if not self._address:
            address_trits = [0] * HASH_LENGTH
            self._sponge.squeeze(address_trits)

            self._address = MultisigAddress.from_trits(
                address_trits,
                digests=self._digests[:],
            )

        return self._address


# coding=utf-8
from __future__ import absolute_import, division, print_function, \
    unicode_literals

from typing import Generator, Iterable, List

from iota import Address, TRITS_PER_TRYTE, TrytesCompatible
from iota.crypto.kerl import Kerl
from iota.crypto.signing import KeyGenerator, KeyIterator
from iota.crypto.types import Digest, PrivateKey, Seed
from iota.exceptions import with_context

__all__ = [
    'AddressGenerator',
]

class AddressGenerator(Iterable[Address]):
    """
    Generates new addresses using a standard algorithm.

    Note: This class does not check if addresses have already been used;
    if you want to exclude used addresses, invoke
    :py:meth:`iota.api.IotaApi.get_new_addresses` instead.

    Note also that :py:meth:`iota.api.IotaApi.get_new_addresses` uses
    ``AddressGenerator`` internally, so you get the best of both worlds
    when you use the API (:
    """
    DEFAULT_SECURITY_LEVEL = 2
    """
    Default number of iterations to use when creating digests, used to
    create addresses.

    Note: this also impacts a few other things like length of
    transaction signatures.

    References:

    - :py:meth:`iota.transaction.ProposedBundle.sign_inputs`
    - :py:class:`iota.transaction.BundleValidator`
    """

    def __init__(
            self,
            seed,
            security_level=DEFAULT_SECURITY_LEVEL,
            checksum=False,
    ):
        # type: (TrytesCompatible, int, bool) -> None
        super(AddressGenerator, self).__init__()

        self.security_level = security_level
        self.checksum = checksum
        self.seed = Seed(seed)

    def __iter__(self):
        # type: () -> Generator[Address, None, None]
        """
        Returns a generator for creating new addresses, starting at
        index 0 and potentially continuing on forever.
        """
        return self.create_iterator()

    def get_addresses(self, start, count=1, step=1):
        # type: (int, int, int) -> List[Address]
        """
        Generates and returns one or more addresses at the specified
        index(es).

        This is a one-time operation; if you want to create lots of
        addresses across multiple contexts, consider invoking
        :py:meth:`create_iterator` and sharing the resulting generator
        object instead.

        Warning: This method may take awhile to run if the starting
        index and/or the number of requested addresses is a large
        number!

        :param start:
            Starting index.
            Must be >= 0.

        :param count:
            Number of addresses to generate.
            Must be > 0.

        :param step:
            Number of indexes to advance after each address.
            This may be any non-zero (positive or negative) integer.

        :return:
            Always returns a list, even if only one address is generated.

            The returned list will contain ``count`` addresses, except
            when ``step * count < start`` (only applies when ``step`` is
            negative).
        """
        if count < 1:
            raise with_context(
                exc=ValueError('``count`` must be positive.'),

                context={
                    'start': start,
                    'count': count,
                    'step': step,
                },
            )

        if not step:
            raise with_context(
                exc=ValueError('``step`` must not be zero.'),

                context={
                    'start': start,
                    'count': count,
                    'step': step,
                },
            )

        generator = self.create_iterator(start, step)

        addresses = []
        for _ in range(count):
            try:
                next_addy = next(generator)
            except StopIteration:
                break
            else:
                addresses.append(next_addy)

        return addresses

    def create_iterator(self, start=0, step=1):
        # type: (int, int) -> Generator[Address, None, None]
        """
        Creates an iterator that can be used to progressively generate new
        addresses.

        :param start:
            Starting index.

            Warning: This method may take awhile to reset if ``start``
            is a large number!

        :param step:
            Number of indexes to advance after each address.

            Warning: The generator may take awhile to advance between
            iterations if ``step`` is a large number!
        """
        key_iterator = (
            KeyGenerator(self.seed).create_iterator(
                start,
                step,
                self.security_level,
            )
        )

        while True:
            yield self._generate_address(key_iterator)

    @staticmethod
    def address_from_digest(digest):
        # type: (Digest) -> Address
        """
        Generates an address from a private key digest.
        """
        address_trits = [0] * (Address.LEN * TRITS_PER_TRYTE)  # type: List[int]

        sponge = Kerl()
        sponge.absorb(digest.as_trits())
        sponge.squeeze(address_trits)

        return Address.from_trits(
            trits=address_trits,

            key_index=digest.key_index,
            security_level=digest.security_level,
        )

    def _generate_address(self, key_iterator):
        # type: (KeyIterator) -> Address
        """
        Generates a new address.

        Used in the event of a cache miss.
        """
        if self.checksum:
            return (
                self.address_from_digest(
                    digest=self._get_digest(key_iterator),
                ).with_valid_checksum()
            )
        else:
            return self.address_from_digest(self._get_digest(key_iterator))

    @staticmethod
    def _get_digest(key_iterator):
        # type: (KeyIterator) -> Digest
        """
        Extracts parameters for :py:meth:`address_from_digest`.

        Split into a separate method so that it can be mocked during
        unit tests.
        """
        private_key = next(key_iterator)  # type: PrivateKey
        return private_key.get_digest()


# coding=utf-8
from __future__ import absolute_import, division, print_function, \
    unicode_literals

from typing import Generator, Iterable, List, Optional, Tuple

from iota import Address, Bundle, Transaction, \
    TransactionHash
from iota.adapter import BaseAdapter
from iota.commands.core.find_transactions import FindTransactionsCommand
from iota.commands.core.get_trytes import GetTrytesCommand
from iota.commands.extended.get_bundles import GetBundlesCommand
from iota.commands.extended.get_latest_inclusion import \
    GetLatestInclusionCommand
from iota.crypto.addresses import AddressGenerator
from iota.crypto.types import Seed

def find_transaction_objects(adapter, **kwargs):
    # type: (BaseAdapter, **Iterable) -> List[Transaction]
    """
    Finds transactions matching the specified criteria, fetches the
    corresponding trytes and converts them into Transaction objects.
    """
    ft_response = FindTransactionsCommand(adapter)(**kwargs)

    hashes = ft_response['hashes']

    if hashes:
        gt_response = GetTrytesCommand(adapter)(hashes=hashes)

        return list(map(
            Transaction.from_tryte_string,
            gt_response.get('trytes') or [],
        ))  # type: List[Transaction]

    return []

def iter_used_addresses(
        adapter,  # type: BaseAdapter
        seed,  # type: Seed
        start,  # type: int
        security_level=None,  # type: Optional[int]
):
    # type: (...) -> Generator[Tuple[Address, List[TransactionHash]], None, None]
    """
    Scans the Tangle for used addresses.

    This is basically the opposite of invoking ``getNewAddresses`` with
    ``stop=None``.
    """
    if security_level is None:
        security_level = AddressGenerator.DEFAULT_SECURITY_LEVEL

    ft_command = FindTransactionsCommand(adapter)

    for addy in AddressGenerator(seed, security_level).create_iterator(start):
        ft_response = ft_command(addresses=[addy])

        if ft_response['hashes']:
            yield addy, ft_response['hashes']
        else:
            break

        # Reset the command so that we can call it again.
        ft_command.reset()

def get_bundles_from_transaction_hashes(
        adapter,
        transaction_hashes,
        inclusion_states,
):
    # type: (BaseAdapter, Iterable[TransactionHash], bool) -> List[Bundle]
    """
    Given a set of transaction hashes, returns the corresponding bundles,
    sorted by tail transaction timestamp.
    """
    transaction_hashes = list(transaction_hashes)
    if not transaction_hashes:
        return []

    my_bundles = []  # type: List[Bundle]

    # Sort transactions into tail and non-tail.
    tail_transaction_hashes = set()
    non_tail_bundle_hashes = set()

    gt_response = GetTrytesCommand(adapter)(hashes=transaction_hashes)
    all_transactions = list(map(
        Transaction.from_tryte_string,
        gt_response['trytes'],
    ))  # type: List[Transaction]

    for txn in all_transactions:
        if txn.is_tail:
            tail_transaction_hashes.add(txn.hash)
        else:
            # Capture the bundle ID instead of the transaction hash so
            # that we can query the node to find the tail transaction
            # for that bundle.
            non_tail_bundle_hashes.add(txn.bundle_hash)

    if non_tail_bundle_hashes:
        for txn in find_transaction_objects(
                adapter=adapter,
                bundles=list(non_tail_bundle_hashes),
        ):
            if txn.is_tail:
                if txn.hash not in tail_transaction_hashes:
                    all_transactions.append(txn)
                    tail_transaction_hashes.add(txn.hash)

    # Filter out all non-tail transactions.
    tail_transactions = [
        txn
        for txn in all_transactions
        if txn.hash in tail_transaction_hashes
    ]

    # Attach inclusion states, if requested.
    if inclusion_states:
        gli_response = GetLatestInclusionCommand(adapter)(
            hashes=list(tail_transaction_hashes),
        )

        for txn in tail_transactions:
            txn.is_confirmed = gli_response['states'].get(txn.hash)

    # Find the bundles for each transaction.
    for txn in tail_transactions:
        gb_response = GetBundlesCommand(adapter)(transaction=txn.hash)
        txn_bundles = gb_response['bundles']  # type: List[Bundle]

        if inclusion_states:
            for bundle in txn_bundles:
                bundle.is_confirmed = txn.is_confirmed

        my_bundles.extend(txn_bundles)

    return list(sorted(
        my_bundles,
        key=lambda bundle_: bundle_.tail_transaction.timestamp,
    ))


# coding=utf-8
from __future__ import absolute_import, division, print_function, \
  unicode_literals

from typing import Iterable

from iota import ProposedBundle
from iota.exceptions import with_context
from iota.multisig.types import MultisigAddress

__all__ = [
  'ProposedMultisigBundle',
]

class ProposedMultisigBundle(ProposedBundle):
  """
  A collection of proposed transactions, with multisig inputs.

  Note: at this time, only a single multisig input is supported per
  bundle.
  """
  def add_inputs(self, inputs):
    # type: (Iterable[MultisigAddress]) -> None
    """
    Adds inputs to spend in the bundle.

    Note that each input may require multiple transactions, in order to
    hold the entire signature.

    :param inputs:
      MultisigAddresses to use as the inputs for this bundle.

      Note: at this time, only a single multisig input is supported.
    """
    if self.hash:
      raise RuntimeError('Bundle is already finalized.')

    count = 0
    for addy in inputs:
      if count > 0:
        raise ValueError(
          '{cls} only supports 1 input.'.format(cls=type(self).__name__),
        )

      if not isinstance(addy, MultisigAddress):
        raise with_context(
          exc =
            TypeError(
              'Incorrect input type for {cls} '
              '(expected {expected}, actual {actual}).'.format(
                actual    = type(addy).__name__,
                cls       = type(self).__name__,
                expected  = MultisigAddress.__name__,
              ),
            ),

          context = {
            'actual_input': addy,
          },
        )

      security_level = addy.security_level
      if security_level < 1:
        raise with_context(
          exc =
            ValueError(
              'Unable to determine security level for {type} '
              '(is ``digests`` populated correctly?).'.format(
                type = type(addy).__name__,
              ),
            ),

          context = {
            'actual_input':   addy,
            'security_level': security_level,
          },
        )

      if not addy.balance:
        raise with_context(
          exc =
            ValueError(
              'Cannot add input with empty/unknown balance to {type} '
              '(use ``Iota.get_balances`` to get balance first).'.format(
                type = type(self).__name__,
              ),
            ),

          context = {
            'actual_input': addy,
          },
        )

      self._create_input_transactions(addy)

      count += 1


# coding=utf-8
from __future__ import absolute_import, division, print_function, \
    unicode_literals

from codecs import Codec, CodecInfo, register as lookup_function
from warnings import warn

from six import PY3, binary_type

from iota.exceptions import with_context

__all__ = [
    'AsciiTrytesCodec',
    'TrytesDecodeError',
]

class TrytesDecodeError(ValueError):
    """
    Indicates that a tryte string could not be decoded to bytes.
    """
    pass

class AsciiTrytesCodec(Codec):
    """
    Legacy codec for converting byte strings into trytes, and vice
    versa.

    This method encodes each pair of trytes as an ASCII code point (and
    vice versa when decoding).

    The end result requires more space than if the trytes were converted
    mathematically, but because the result is ASCII, it's easier to work
    with.

    Think of this kind of like Base 64 for balanced ternary (:
    """
    name = 'trytes_ascii'

    compat_name = 'trytes'
    """
    Old name for this codec.
    Note:  Will be removed in PyOTA v2.1!
    """

    # :bc: Without the bytearray cast, Python 2 will populate the dict
    # with characters instead of integers.
    # noinspection SpellCheckingInspection
    alphabet = dict(enumerate(bytearray(b'9ABCDEFGHIJKLMNOPQRSTUVWXYZ')))
    """
    Used to encode bytes into trytes.
    """

    index = dict(zip(alphabet.values(), alphabet.keys()))
    """
    Used to decode trytes into bytes.
    """

    @classmethod
    def get_codec_info(cls):
        """
        Returns information used by the codecs library to configure the
        codec for use.
        """
        codec = cls()

        codec_info = {
            'encode': codec.encode,
            'decode': codec.decode,
        }

        # In Python 2, all codecs are made equal.
        # In Python 3, some codecs are more equal than others.
        if PY3:
            codec_info['_is_text_encoding'] = False

        return CodecInfo(**codec_info)

    # noinspection PyShadowingBuiltins
    def encode(self, input, errors='strict'):
        """
        Encodes a byte string into trytes.
        """
        if isinstance(input, memoryview):
            input = input.tobytes()

        if not isinstance(input, (binary_type, bytearray)):
            raise with_context(
                exc=TypeError(
                    "Can't encode {type}; byte string expected.".format(
                        type=type(input).__name__,
                    )),

                context={
                    'input': input,
                },
            )

        # :bc: In Python 2, iterating over a byte string yields
        # characters instead of integers.
        if not isinstance(input, bytearray):
            input = bytearray(input)

        trytes = bytearray()

        for c in input:
            second, first = divmod(c, len(self.alphabet))

            trytes.append(self.alphabet[first])
            trytes.append(self.alphabet[second])

        return binary_type(trytes), len(input)

    # noinspection PyShadowingBuiltins
    def decode(self, input, errors='strict'):
        """
        Decodes a tryte string into bytes.
        """
        if isinstance(input, memoryview):
            input = input.tobytes()

        if not isinstance(input, (binary_type, bytearray)):
            raise with_context(
                exc=TypeError(
                    "Can't decode {type}; byte string expected.".format(
                        type=type(input).__name__,
                    )),

                context={
                    'input': input,
                },
            )

        # :bc: In Python 2, iterating over a byte string yields
        # characters instead of integers.
        if not isinstance(input, bytearray):
            input = bytearray(input)

        bytes_ = bytearray()

        for i in range(0, len(input), 2):
            try:
                first, second = input[i:i + 2]
            except ValueError:
                if errors == 'strict':
                    raise with_context(
                        exc=TrytesDecodeError(
                            "'{name}' codec can't decode value; "
                            "tryte sequence has odd length.".format(
                                name=self.name,
                            ),
                        ),

                        context={
                            'input': input,
                        },
                    )
                elif errors == 'replace':
                    bytes_ += b'?'

                continue

            try:
                bytes_.append(
                    self.index[first]
                    + (self.index[second] * len(self.index))
                )
            except ValueError:
                # This combination of trytes yields a value > 255 when
                # decoded.
                # Naturally, we can't represent this using ASCII.
                if errors == 'strict':
                    raise with_context(
                        exc=TrytesDecodeError(
                            "'{name}' codec can't decode trytes {pair} "
                            "at position {i}-{j}: "
                            "ordinal not in range(255)".format(
                                name=self.name,
                                pair=chr(first) + chr(second),
                                i=i,
                                j=i + 1,
                            ),
                        ),

                        context={
                            'input': input,
                        }
                    )
                elif errors == 'replace':
                    bytes_ += b'?'

        return binary_type(bytes_), len(input)

@lookup_function
def check_trytes_codec(encoding):
    """
    Determines which codec to use for the specified encoding.

    References:

    - https://docs.python.org/3/library/codecs.html#codecs.register
    """
    if encoding == AsciiTrytesCodec.name:
        return AsciiTrytesCodec.get_codec_info()

    elif encoding == AsciiTrytesCodec.compat_name:
        warn(
            '"{old_codec}" codec will be removed in PyOTA v2.1. '
            'Use "{new_codec}" instead.'.format(
                new_codec=AsciiTrytesCodec.name,
                old_codec=AsciiTrytesCodec.compat_name,
            ),

            DeprecationWarning,
        )
        return AsciiTrytesCodec.get_codec_info()

    return None


# coding=utf-8
from __future__ import absolute_import, division, print_function, \
    unicode_literals

from typing import List, Optional

import filters as f

from iota import Address
from iota.commands import FilterCommand, RequestFilter
from iota.commands.core.find_transactions import FindTransactionsCommand
from iota.crypto.addresses import AddressGenerator
from iota.crypto.types import Seed
from iota.filters import SecurityLevel, Trytes

__all__ = [
    'GetNewAddressesCommand',
]

class GetNewAddressesCommand(FilterCommand):
    """
    Executes ``getNewAddresses`` extended API command.

    See :py:meth:`iota.api.Iota.get_new_addresses` for more info.
    """
    command = 'getNewAddresses'

    def get_request_filter(self):
        return GetNewAddressesRequestFilter()

    def get_response_filter(self):
        pass

    def _execute(self, request):
        checksum = request['checksum']  # type: bool
        count = request['count']  # type: Optional[int]
        index = request['index']  # type: int
        security_level = request['securityLevel']  # type: int
        seed = request['seed']  # type: Seed

        return {
            'addresses':
                self._find_addresses(
                    seed,
                    index,
                    count,
                    security_level,
                    checksum,
                ),
        }

    def _find_addresses(self, seed, index, count, security_level, checksum):
        # type: (Seed, int, Optional[int], int, bool) -> List[Address]
        """
        Find addresses matching the command parameters.
        """
        generator = AddressGenerator(seed, security_level, checksum)

        if count is None:
            # Connect to Tangle and find the first address without any
            # transactions.
            for addy in generator.create_iterator(start=index):
                # We use addy.address here because FindTransactions does
                # not work on an address with a checksum
                response = FindTransactionsCommand(self.adapter)(
                    addresses=[addy.address],
                )

                if not response.get('hashes'):
                    return [addy]

        return generator.get_addresses(start=index, count=count)

class GetNewAddressesRequestFilter(RequestFilter):
    def __init__(self):
        super(GetNewAddressesRequestFilter, self).__init__(
            {
                # Everything except ``seed`` is optional.
                'checksum': f.Type(bool) | f.Optional(default=False),
                'count': f.Type(int) | f.Min(1),
                'index': f.Type(int) | f.Min(0) | f.Optional(default=0),
                'securityLevel': SecurityLevel,

                'seed': f.Required | Trytes(Seed),
            },

            allow_missing_keys={
                'checksum',
                'count',
                'index',
                'securityLevel',
            },
        )


# coding=utf-8
from __future__ import absolute_import, division, print_function, \
    unicode_literals

from abc import ABCMeta, abstractmethod as abstract_method
from typing import Dict, Text

from six import with_metaclass

from iota.adapter import AdapterSpec, BaseAdapter, resolve_adapter

__all__ = [
    'RoutingWrapper',
]

class BaseWrapper(with_metaclass(ABCMeta, BaseAdapter)):
    """
    Base functionality for "adapter wrappers", used to extend the
    functionality of IOTA adapters.
    """

    def __init__(self, adapter):
        # type: (AdapterSpec) -> None
        super(BaseWrapper, self).__init__()

        if not isinstance(adapter, BaseAdapter):
            adapter = resolve_adapter(adapter)

        self.adapter = adapter  # type: BaseAdapter

    def get_uri(self):
        # type: () -> Text
        return self.adapter.get_uri()

    @abstract_method
    def send_request(self, payload, **kwargs):
        # type: (dict, dict) -> dict
        raise NotImplementedError(
            'Not implemented in {cls}.'.format(cls=type(self).__name__),
        )

class RoutingWrapper(BaseWrapper):
    """
    Routes commands to different nodes.

    This allows you to, for example, send POW requests to a local node,
    while routing all other requests to a remote one.

    Example:

    .. code-block:: python

        # Route POW to localhost, everything else to 12.34.56.78.
        iota = Iota(
          RoutingWrapper('http://12.34.56.78:14265')
            .add_route('attachToTangle', 'http://localhost:14265')
            .add_route('interruptAttachingToTangle', 'http://localhost:14265')
        )
    """

    def __init__(self, default_adapter):
        # type: (AdapterSpec) -> None
        """
        :param default_adapter:
            Adapter to use for any routes not listed in ``routes``.
        """
        super(RoutingWrapper, self).__init__(default_adapter)

        # Try to limit the number of distinct adapter instances we create
        # when resolving URIs.
        self.adapter_aliases = {}  # type: Dict[AdapterSpec, BaseAdapter]

        self.routes = {}  # type: Dict[Text, BaseAdapter]

    def add_route(self, command, adapter):
        # type: (Text, AdapterSpec) -> RoutingWrapper
        """
        Adds a route to the wrapper.

        :param command:
            The name of the command to route (e.g., "attachToTangle").

        :param adapter:
            The adapter object or URI to route requests to.
        """
        if not isinstance(adapter, BaseAdapter):
            try:
                adapter = self.adapter_aliases[adapter]
            except KeyError:
                self.adapter_aliases[adapter] = adapter = resolve_adapter(
                    adapter
                )

        self.routes[command] = adapter

        return self

    def get_adapter(self, command):
        # type: (Text) -> BaseAdapter
        """
        Return the adapter for the specified command.
        """
        return self.routes.get(command, self.adapter)

    def send_request(self, payload, **kwargs):
        # type: (dict, dict) -> dict
        command = payload.get('command')

        return self.get_adapter(command).send_request(payload, **kwargs)


# coding=utf-8
from __future__ import absolute_import, division, print_function, \
    unicode_literals

from operator import attrgetter
from typing import Iterable, Iterator, List, MutableSequence, \
    Optional, Sequence, Text

from iota.codecs import TrytesDecodeError
from iota.crypto import Curl, HASH_LENGTH
from iota.json import JsonSerializable
from iota.transaction.types import BundleHash, Fragment, Nonce, \
    TransactionHash, TransactionTrytes
from iota.trits import int_from_trits, trits_from_int
from iota.types import Address, Tag, TryteString, TrytesCompatible

__all__ = [
    'Bundle',
    'Transaction',
]

class Transaction(JsonSerializable):
    """
    A transaction that has been attached to the Tangle.
    """

    @classmethod
    def from_tryte_string(cls, trytes, hash_=None):
        # type: (TrytesCompatible, Optional[TransactionHash]) -> Transaction
        """
        Creates a Transaction object from a sequence of trytes.

        :param trytes:
            Raw trytes.  Should be exactly 2673 trytes long.

        :param hash_:
            The transaction hash, if available.

            If not provided, it will be computed from the transaction
            trytes.
        """
        tryte_string = TransactionTrytes(trytes)

        if not hash_:
            hash_trits = [0] * HASH_LENGTH  # type: MutableSequence[int]

            sponge = Curl()
            sponge.absorb(tryte_string.as_trits())
            sponge.squeeze(hash_trits)

            hash_ = TransactionHash.from_trits(hash_trits)

        return cls(
            hash_=hash_,
            signature_message_fragment=Fragment(tryte_string[0:2187]),
            address=Address(tryte_string[2187:2268]),
            value=int_from_trits(tryte_string[2268:2295].as_trits()),
            legacy_tag=Tag(tryte_string[2295:2322]),
            timestamp=int_from_trits(tryte_string[2322:2331].as_trits()),
            current_index=int_from_trits(tryte_string[2331:2340].as_trits()),
            last_index=int_from_trits(tryte_string[2340:2349].as_trits()),
            bundle_hash=BundleHash(tryte_string[2349:2430]),
            trunk_transaction_hash=TransactionHash(tryte_string[2430:2511]),
            branch_transaction_hash=TransactionHash(tryte_string[2511:2592]),
            tag=Tag(tryte_string[2592:2619]),

            attachment_timestamp=int_from_trits(
                tryte_string[2619:2628].as_trits()),

            attachment_timestamp_lower_bound=int_from_trits(
                tryte_string[2628:2637].as_trits()),

            attachment_timestamp_upper_bound=int_from_trits(
                tryte_string[2637:2646].as_trits()),

            nonce=Nonce(tryte_string[2646:2673]),
        )

    def __init__(
            self,
            hash_,  # type: Optional[TransactionHash]
            signature_message_fragment,  # type: Optional[Fragment]
            address,  # type: Address
            value,  # type: int
            timestamp,  # type: int
            current_index,  # type: Optional[int]
            last_index,  # type: Optional[int]
            bundle_hash,  # type: Optional[BundleHash]
            trunk_transaction_hash,  # type: Optional[TransactionHash]
            branch_transaction_hash,  # type: Optional[TransactionHash]
            tag,  # type: Optional[Tag]
            attachment_timestamp,  # type: Optional[int]
            attachment_timestamp_lower_bound,  # type: Optional[int]
            attachment_timestamp_upper_bound,  # type: Optional[int]
            nonce,  # type: Optional[Nonce]
            legacy_tag=None  # type: Optional[Tag]
    ):
        self.hash = hash_
        """
        Transaction ID, generated by taking a hash of the transaction
        trits.
        """

        self.bundle_hash = bundle_hash
        """
        Bundle hash, generated by taking a hash of metadata from all the
        transactions in the bundle.
        """

        self.address = address
        """
        The address associated with this transaction.

        If ``value`` is != 0, the associated address' balance is
        adjusted as a result of this transaction.
        """

        self.value = value
        """
        Amount to adjust the balance of ``address``.
        Can be negative (i.e., for spending inputs).
        """

        self._legacy_tag = legacy_tag
        """
        Optional classification legacy_tag applied to this transaction.
        """

        self.nonce = nonce
        """
        Unique value used to increase security of the transaction hash.
        """

        self.timestamp = timestamp
        """
        Timestamp used to increase the security of the transaction hash.

        .. important::
            This value is easy to forge!
            Do not rely on it when resolving conflicts!
        """

        self.current_index = current_index
        """
        The position of the transaction inside the bundle.

        For value transfers, the "spend" transaction is generally in the
        0th position, followed by inputs, and the "change" transaction
        is last.
        """

        self.last_index = last_index
        """
        The position of the final transaction inside the bundle.
        """

        self.trunk_transaction_hash = trunk_transaction_hash
        """
        In order to add a transaction to the Tangle, the client must
        perform PoW to "approve" two existing transactions, called the
        "trunk" and "branch" transactions.

        The trunk transaction is generally used to link transactions
        within a bundle.
        """

        self.branch_transaction_hash = branch_transaction_hash
        """
        In order to add a transaction to the Tangle, the client must
        perform PoW to "approve" two existing transactions, called the
        "trunk" and "branch" transactions.

        The branch transaction may be selected strategically to maximize
        the bundle's chances of getting confirmed; otherwise it usually
        has no significance.
        """

        self.tag = tag
        """
        Optional classification tag applied to this transaction.
        """

        self.attachment_timestamp = attachment_timestamp

        self.attachment_timestamp_lower_bound = attachment_timestamp_lower_bound

        self.attachment_timestamp_upper_bound = attachment_timestamp_upper_bound

        self.signature_message_fragment = signature_message_fragment
        """
        "Signature/Message Fragment" (note the slash):

        - For inputs, this contains a fragment of the cryptographic
          signature, used to verify the transaction (depending on the
          security level of the corresponding address, the entire
          signature is usually too large to fit into a single
          transaction, so it is split across multiple transactions
          instead).

        - For other transactions, this contains a fragment of the
          message attached to the transaction (if any).  This can be
          pretty much any value.  Like signatures, the message may be
          split across multiple transactions if it is too large to fit
          inside a single transaction.
        """

        self.is_confirmed = None  # type: Optional[bool]
        """
        Whether this transaction has been confirmed by neighbor nodes.
        Must be set manually via the ``getInclusionStates`` API command.

        References:

        - :py:meth:`iota.api.StrictIota.get_inclusion_states`
        - :py:meth:`iota.api.Iota.get_transfers`
        """

    @property
    def is_tail(self):
        # type: () -> bool
        """
        Returns whether this transaction is a tail (first one in the
        bundle).

        Because of the way the Tangle is organized, the tail transaction
        is generally the last one in the bundle that gets attached, even
        though it occupies the first logical position inside the bundle.
        """
        return self.current_index == 0

    @property
    def value_as_trytes(self):
        # type: () -> TryteString
        """
        Returns a TryteString representation of the transaction's
        :py:attr:`value`.
        """
        # Note that we are padding to 81 *trits*.
        return TryteString.from_trits(trits_from_int(self.value, pad=81))

    @property
    def timestamp_as_trytes(self):
        # type: () -> TryteString
        """
        Returns a TryteString representation of the transaction's
        :py:attr:`timestamp`.
        """
        # Note that we are padding to 27 *trits*.
        return TryteString.from_trits(trits_from_int(self.timestamp, pad=27))

    @property
    def current_index_as_trytes(self):
        # type: () -> TryteString
        """
        Returns a TryteString representation of the transaction's
        :py:attr:`current_index`.
        """
        # Note that we are padding to 27 *trits*.
        return TryteString.from_trits(
            trits_from_int(self.current_index, pad=27),
        )

    @property
    def last_index_as_trytes(self):
        # type: () -> TryteString
        """
        Returns a TryteString representation of the transaction's
        :py:attr:`last_index`.
        """
        # Note that we are padding to 27 *trits*.
        return TryteString.from_trits(trits_from_int(self.last_index, pad=27))

    @property
    def attachment_timestamp_as_trytes(self):
        # type: () -> TryteString
        """
        Returns a TryteString representation of the transaction's
        :py:attr:`attachment_timestamp`.
        """
        # Note that we are padding to 27 *trits*.
        return TryteString.from_trits(
            trits_from_int(self.attachment_timestamp, pad=27),
        )

    @property
    def attachment_timestamp_lower_bound_as_trytes(self):
        # type: () -> TryteString
        """
        Returns a TryteString representation of the transaction's
        :py:attr:`attachment_timestamp_lower_bound`.
        """
        # Note that we are padding to 27 *trits*.
        return TryteString.from_trits(
            trits_from_int(self.attachment_timestamp_lower_bound, pad=27),
        )

    @property
    def attachment_timestamp_upper_bound_as_trytes(self):
        # type: () -> TryteString
        """
        Returns a TryteString representation of the transaction's
        :py:attr:`attachment_timestamp_upper_bound`.
        """
        # Note that we are padding to 27 *trits*.
        return TryteString.from_trits(
            trits_from_int(self.attachment_timestamp_upper_bound, pad=27),
        )

    def as_json_compatible(self):
        # type: () -> dict
        """
        Returns a JSON-compatible representation of the object.

        References:

        - :py:class:`iota.json.JsonEncoder`.
        """
        return {
            'hash_': self.hash,
            'signature_message_fragment': self.signature_message_fragment,
            'address': self.address,
            'value': self.value,
            'legacy_tag': self.legacy_tag,
            'timestamp': self.timestamp,
            'current_index': self.current_index,
            'last_index': self.last_index,
            'bundle_hash': self.bundle_hash,
            'trunk_transaction_hash': self.trunk_transaction_hash,
            'branch_transaction_hash': self.branch_transaction_hash,
            'tag': self.tag,
            'attachment_timestamp': self.attachment_timestamp,

            'attachment_timestamp_lower_bound':
                self.attachment_timestamp_lower_bound,

            'attachment_timestamp_upper_bound':
                self.attachment_timestamp_upper_bound,

            'nonce': self.nonce,
        }

    def as_tryte_string(self):
        # type: () -> TransactionTrytes
        """
        Returns a TryteString representation of the transaction.
        """
        return TransactionTrytes(
            self.signature_message_fragment
            + self.address.address
            + self.value_as_trytes
            + self.legacy_tag
            + self.timestamp_as_trytes
            + self.current_index_as_trytes
            + self.last_index_as_trytes
            + self.bundle_hash
            + self.trunk_transaction_hash
            + self.branch_transaction_hash
            + self.tag
            + self.attachment_timestamp_as_trytes
            + self.attachment_timestamp_lower_bound_as_trytes
            + self.attachment_timestamp_upper_bound_as_trytes
            + self.nonce
        )

    def get_signature_validation_trytes(self):
        # type: () -> TryteString
        """
        Returns the values needed to validate the transaction's
        ``signature_message_fragment`` value.
        """
        return (
                self.address.address
                + self.value_as_trytes
                + self.legacy_tag
                + self.timestamp_as_trytes
                + self.current_index_as_trytes
                + self.last_index_as_trytes
        )

    @property
    def legacy_tag(self):
        # type: () -> Tag
        """
        Return the legacy tag of the transaction.
        If no legacy tag was set, returns the tag instead.
        """
        return self._legacy_tag or self.tag

class Bundle(JsonSerializable, Sequence[Transaction]):
    """
    A collection of transactions, treated as an atomic unit when
    attached to the Tangle.

    Note: unlike a block in a blockchain, bundles are not first-class
    citizens in IOTA; only transactions get stored in the Tangle.

    Instead, Bundles must be inferred by following linked transactions
    with the same bundle hash.

    References:

    - :py:class:`iota.commands.extended.get_bundles.GetBundlesCommand`
    """

    @classmethod
    def from_tryte_strings(cls, trytes):
        # type: (Iterable[TryteString]) -> Bundle
        """
        Creates a Bundle object from a list of tryte values.
        """
        return cls(map(Transaction.from_tryte_string, trytes))

    def __init__(self, transactions=None):
        # type: (Optional[Iterable[Transaction]]) -> None
        super(Bundle, self).__init__()

        self.transactions = []  # type: List[Transaction]
        if transactions:
            self.transactions.extend(
                sorted(transactions, key=attrgetter('current_index')),
            )

        self._is_confirmed = None  # type: Optional[bool]
        """
        Whether this bundle has been confirmed by neighbor nodes.
        Must be set manually.

        References:

        - :py:class:`iota.commands.extended.get_transfers.GetTransfersCommand`
        """

    def __contains__(self, transaction):
        # type: (Transaction) -> bool
        return transaction in self.transactions

    def __getitem__(self, index):
        # type: (int) -> Transaction
        return self.transactions[index]

    def __iter__(self):
        # type: () -> Iterator[Transaction]
        return iter(self.transactions)

    def __len__(self):
        # type: () -> int
        return len(self.transactions)

    @property
    def is_confirmed(self):
        # type: () -> Optional[bool]
        """
        Returns whether this bundle has been confirmed by neighbor
        nodes.

        This attribute must be set manually.

        References:

        - :py:class:`iota.commands.extended.get_transfers.GetTransfersCommand`
        """
        return self._is_confirmed

    @is_confirmed.setter
    def is_confirmed(self, new_is_confirmed):
        # type: (bool) -> None
        """
        Sets the ``is_confirmed`` for the bundle.
        """
        self._is_confirmed = new_is_confirmed

        for txn in self:
            txn.is_confirmed = new_is_confirmed

    @property
    def hash(self):
        # type: () -> Optional[BundleHash]
        """
        Returns the hash of the bundle.

        This value is determined by inspecting the bundle's tail
        transaction, so in a few edge cases, it may be incorrect.

        If the bundle has no transactions, this method returns ``None``.
        """
        try:
            return self.tail_transaction.bundle_hash
        except IndexError:
            return None

    @property
    def tail_transaction(self):
        # type: () -> Transaction
        """
        Returns the tail transaction of the bundle.
        """
        return self[0]

    def get_messages(self, errors='drop'):
        # type: (Text) -> List[Text]
        """
        Attempts to decipher encoded messages from the transactions in
        the bundle.

        :param errors:
            How to handle trytes that can't be converted, or bytes that
            can't be decoded using UTF-8:

            'drop'
                Drop the trytes from the result.

            'strict'
                Raise an exception.

            'replace'
                Replace with a placeholder character.

            'ignore'
                Omit the invalid tryte/byte sequence.
        """
        decode_errors = 'strict' if errors == 'drop' else errors

        messages = []

        for group in self.group_transactions():
            # Ignore inputs.
            if group[0].value < 0:
                continue

            message_trytes = TryteString(b'')
            for txn in group:
                message_trytes += txn.signature_message_fragment

            if message_trytes:
                try:
                    messages.append(message_trytes.decode(decode_errors))
                except (TrytesDecodeError, UnicodeDecodeError):
                    if errors != 'drop':
                        raise

        return messages

    def as_tryte_strings(self, head_to_tail=False):
        # type: (bool) -> List[TransactionTrytes]
        """
        Returns TryteString representations of the transactions in this
        bundle.

        :param head_to_tail:
            Determines the order of the transactions:

            - ``True``: head txn first, tail txn last.
            - ``False`` (default): tail txn first, head txn last.

            Note that the order is reversed by default, as this is the
            way bundles are typically broadcast to the Tangle.
        """
        transactions = self if head_to_tail else reversed(self)
        return [t.as_tryte_string() for t in transactions]

    def as_json_compatible(self):
        # type: () -> List[dict]
        """
        Returns a JSON-compatible representation of the object.

        References:

        - :py:class:`iota.json.JsonEncoder`.
        """
        return [txn.as_json_compatible() for txn in self]

    def group_transactions(self):
        # type: () -> List[List[Transaction]]
        """
        Groups transactions in the bundle by address.
        """
        groups = []

        if self:
            last_txn = self.tail_transaction
            current_group = [last_txn]
            for current_txn in self.transactions[1:]:
                # Transactions are grouped by address, so as long as the
                # address stays consistent from one transaction to
                # another, we are still in the same group.
                if current_txn.address == last_txn.address:
                    current_group.append(current_txn)
                else:
                    groups.append(current_group)
                    current_group = [current_txn]

                last_txn = current_txn

            if current_group:
                groups.append(current_group)

        return groups