Exemple #1
0
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)
Exemple #2
0
    def __init__(self, seed, start, step, iterations):
        # type: (Seed, int, int, int) -> None
        super(KeyIterator, self).__init__()

        if start < 0:
            raise with_context(
                exc=ValueError('``start`` cannot be negative.'),
                context={
                    'start': start,
                    'step': step,
                    'iterations': iterations,
                },
            )

        if iterations < 1:
            raise with_context(
                exc=ValueError('``iterations`` must be >= 1.'),
                context={
                    'start': start,
                    'step': step,
                    'iterations': iterations,
                },
            )

        self.seed = seed
        self.start = start
        self.step = step
        self.iterations = iterations

        self.current = self.start

        self.fragment_length = FRAGMENT_LENGTH * TRITS_PER_TRYTE
        self.hashes_per_fragment = FRAGMENT_LENGTH // Hash.LEN
Exemple #3
0
    def _traverse_bundle(self, txn_hash, target_bundle_hash=None):
        # type: (TransactionHash, Optional[BundleHash]) -> List[Transaction]
        """
    Recursively traverse the Tangle, collecting transactions until we
    hit a new bundle.

    This method is (usually) faster than ``findTransactions``, and it
    ensures we don't collect transactions from replayed bundles.
    """
        trytes = GetTrytesCommand(self.adapter)(
            hashes=[txn_hash])['trytes']  # type: List[TryteString]

        if not trytes:
            raise with_context(
                exc=BadApiResponse(
                    'Bundle transactions not visible (``exc.context`` has more info).',
                ),
                context={
                    'transaction_hash': txn_hash,
                    'target_bundle_hash': target_bundle_hash,
                },
            )

        transaction = Transaction.from_tryte_string(trytes[0])

        if (not target_bundle_hash) and transaction.current_index:
            raise with_context(
                exc=BadApiResponse(
                    '``_traverse_bundle`` started with a non-tail transaction '
                    '(``exc.context`` has more info).', ),
                context={
                    'transaction_object': transaction,
                    'target_bundle_hash': target_bundle_hash,
                },
            )

        if target_bundle_hash:
            if target_bundle_hash != transaction.bundle_hash:
                # We've hit a different bundle; we can stop now.
                return []
        else:
            target_bundle_hash = transaction.bundle_hash

        if transaction.current_index == transaction.last_index == 0:
            # Bundle only has one transaction.
            return [transaction]

        # Recursively follow the trunk transaction, to fetch the next
        # transaction in the bundle.
        return [transaction] + self._traverse_bundle(
            txn_hash=transaction.trunk_transaction_hash,
            target_bundle_hash=target_bundle_hash)
Exemple #4
0
    def __call__(self, **kwargs):
        # type: (dict) -> dict
        """
    Sends the command to the node.
    """
        if self.called:
            raise with_context(
                exc=RuntimeError('Command has already been called.'),
                context={
                    'last_request': self.request,
                    'last_response': self.response,
                },
            )

        self.request = kwargs

        replacement = self._prepare_request(self.request)
        if replacement is not None:
            self.request = replacement

        self.response = self._execute(self.request)

        replacement = self._prepare_response(self.response)
        if replacement is not None:
            self.response = replacement

        self.called = True

        return self.response
Exemple #5
0
    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)
Exemple #6
0
    def _apply_filter(value, filter_, failure_message):
        # type: (dict, Optional[f.BaseFilter], Text) -> dict
        """
    Applies a filter to a value.  If the value does not pass the
    filter, an exception will be raised with lots of contextual info
    attached to it.
    """
        if filter_:
            runner = f.FilterRunner(filter_, value)

            if runner.is_valid():
                return runner.cleaned_data
            else:
                raise with_context(
                    exc=ValueError(
                        '{message} ({error_codes}) '
                        '(`exc.context["filter_errors"]` '
                        'contains more information).'.format(
                            message=failure_message,
                            error_codes=runner.error_codes,
                        ), ),
                    context={
                        'filter_errors': runner.get_errors(with_context=True),
                    },
                )

        return value
Exemple #7
0
    def _execute(self, request):
        stop = request['stop']  # type: Optional[int]
        seed = request['seed']  # type: Seed
        start = request['start']  # type: int
        threshold = request['threshold']  # type: Optional[int]

        # Determine the addresses we will be scanning.
        if stop is None:
            addresses =\
              [addy for addy, _ in iter_used_addresses(self.adapter, seed, start)]
        else:
            addresses = AddressGenerator(seed).get_addresses(start, stop)

        if addresses:
            # Load balances for the addresses that we generated.
            gb_response = GetBalancesCommand(self.adapter)(addresses=addresses)
        else:
            gb_response = {'balances': []}

        result = {
            'inputs': [],
            'totalBalance': 0,
        }

        threshold_met = threshold is None

        for i, balance in enumerate(gb_response['balances']):
            addresses[i].balance = balance

            if balance:
                result['inputs'].append(addresses[i])
                result['totalBalance'] += balance

                if (threshold
                        is not None) and (result['totalBalance'] >= threshold):
                    threshold_met = True
                    break

        if threshold_met:
            return result
        else:
            # This is an exception case, but note that we attach the result
            # to the exception context so that it can be used for
            # troubleshooting.
            raise with_context(
                exc=BadApiResponse(
                    'Accumulated balance {balance} is less than threshold {threshold} '
                    '(``exc.context`` contains more information).'.format(
                        threshold=threshold,
                        balance=result['totalBalance'],
                    ), ),
                context={
                    'inputs': result['inputs'],
                    'request': request,
                    'total_balance': result['totalBalance'],
                },
            )
Exemple #8
0
    def __init__(self, uri):
        # type: (Union[Text, SplitResult]) -> None
        super(HttpAdapter, self).__init__()

        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
Exemple #9
0
    def __init__(self, trytes):
        # type: (TrytesCompatible) -> None
        super(TransactionTrytes, self).__init__(trytes, pad=self.LEN)

        if len(self._trytes) > self.LEN:
            raise with_context(
                exc=ValueError(
                    '{cls} values must be {len} trytes long.'.format(
                        cls=type(self).__name__, len=self.LEN)),
                context={
                    'trytes': trytes,
                },
            )
Exemple #10
0
    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
Exemple #11
0
    def __init__(self, trytes, key_index=None):
        # type: (TrytesCompatible, Optional[int]) -> None
        super(PrivateKey, self).__init__(trytes)

        if len(self._trytes) % FRAGMENT_LENGTH:
            raise with_context(
                exc=ValueError(
                    'Length of {cls} values must be a multiple of {len} trytes.'
                    .format(cls=type(self).__name__, len=FRAGMENT_LENGTH), ),
                context={
                    'trytes': trytes,
                },
            )

        self.key_index = key_index
Exemple #12
0
  def __init__(self, trytes):
    # type: (TrytesCompatible) -> None
    super(AddressChecksum, self).__init__(trytes, pad=None)

    if len(self._trytes) != self.LEN:
      raise with_context(
        exc = ValueError(
          '{cls} values must be exactly {len} trytes long.'.format(
            cls = type(self).__name__,
            len = self.LEN,
          ),
        ),

        context = {
          'trytes': trytes,
        },
      )
Exemple #13
0
    def as_tryte_string(self):
        # type: () -> TryteString
        """
    Returns a TryteString representation of the transaction.
    """
        if not self.bundle_hash:
            raise with_context(
                exc=RuntimeError(
                    'Cannot get TryteString representation of {cls} instance '
                    'without a bundle hash; call ``bundle.finalize()`` first '
                    '(``exc.context`` has more info).'.format(
                        cls=type(self).__name__, ), ),
                context={
                    'transaction': self,
                },
            )

        return super(ProposedTransaction, self).as_tryte_string()
Exemple #14
0
    def sign_inputs(self, key_generator):
        # type: (KeyGenerator) -> None
        """
    Sign inputs in a finalized bundle.
    """
        if not self.hash:
            raise RuntimeError('Cannot sign inputs until bundle is finalized.')

        # Use a counter for the loop so that we can skip ahead as we go.
        i = 0
        while i < len(self):
            txn = self[i]

            if txn.value < 0:
                # In order to sign the input, we need to know the index of
                # the private key used to generate it.
                if txn.address.key_index is None:
                    raise with_context(
                        exc=ValueError(
                            'Unable to sign input {input}; ``key_index`` is None '
                            '(``exc.context`` has more info).'.format(
                                input=txn.address, ), ),
                        context={
                            'transaction': txn,
                        },
                    )

                signature_fragment_generator =\
                  self._create_signature_fragment_generator(key_generator, txn)

                # We can only fit one signature fragment into each transaction,
                # so we have to split the entire signature among the extra
                # transactions we created for this input in
                # :py:meth:`add_inputs`.
                for j in range(AddressGenerator.DIGEST_ITERATIONS):
                    self[i+j].signature_message_fragment =\
                      next(signature_fragment_generator)

                i += AddressGenerator.DIGEST_ITERATIONS
            else:
                # No signature needed (nor even possible, in some cases); skip
                # this transaction.
                i += 1
Exemple #15
0
  def __eq__(self, other):
    # type: (TrytesCompatible) -> bool
    if isinstance(other, TryteString):
      return self._trytes == other._trytes
    elif isinstance(other, (binary_type, bytearray)):
      return self._trytes == other
    else:
      raise with_context(
        exc = TypeError(
          'Invalid type for TryteString comparison '
          '(expected Union[TryteString, {binary_type}, bytearray], '
          'actual {type}).'.format(
            binary_type = binary_type.__name__,
            type        = type(other).__name__,
          ),
        ),

        context = {
          'other': other,
        },
      )
Exemple #16
0
  def __setitem__(self, item, trytes):
    # type: (Union[int, slice], TrytesCompatible) -> None
    new_trytes = TryteString(trytes)

    if isinstance(item, slice):
      self._trytes[item] = new_trytes._trytes
    elif len(new_trytes) > 1:
      raise with_context(
        exc = ValueError(
          'Cannot assign multiple trytes to the same index '
          '(``exc.context`` has more info).'
        ),

        context = {
          'self':       self,
          'index':      item,
          'new_trytes': new_trytes,
        },
      )
    else:
      self._trytes[item] = new_trytes._trytes[0]
Exemple #17
0
    def _execute(self, request):
        transaction_hash = request['transaction']  # type: TransactionHash

        bundle = Bundle(self._traverse_bundle(transaction_hash))
        validator = BundleValidator(bundle)

        if not validator.is_valid():
            raise with_context(
                exc=BadApiResponse(
                    'Bundle failed validation (``exc.context`` has more info).',
                ),
                context={
                    'bundle': bundle,
                    'errors': validator.errors,
                },
            )

        return {
            # Always return a list, so that we have the necessary structure
            # to return multiple bundles in a future iteration.
            'bundles': [bundle],
        }
Exemple #18
0
  def __init__(self, trytes, balance=None, key_index=None):
    # type: (TrytesCompatible, Optional[int], Optional[int]) -> None
    super(Address, self).__init__(trytes, pad=self.LEN)

    self.checksum = None
    if len(self._trytes) == (self.LEN + AddressChecksum.LEN):
      self.checksum = AddressChecksum(self[self.LEN:]) # type: Optional[AddressChecksum]

    elif len(self._trytes) > self.LEN:
      raise with_context(
        exc = ValueError(
          'Address values must be {len_no_checksum} trytes (no checksum), '
          'or {len_with_checksum} trytes (with checksum).'.format(
            len_no_checksum   = self.LEN,
            len_with_checksum = self.LEN + AddressChecksum.LEN,
          ),
        ),

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

    # Make the address sans checksum accessible.
    self.address = self[:self.LEN] # type: TryteString

    self.balance = balance
    """
    Balance owned by this address.
    Must be set manually via the ``getInputs`` command.

    References:
      - :py:class:`cornode.commands.extended.get_inputs`
      - :py:meth:`ProposedBundle.add_inputs`
    """

    self.key_index = key_index
    """
Exemple #19
0
    def add_inputs(self, inputs):
        # type: (Iterable[Address]) -> None
        """
    Adds inputs to spend in the bundle.

    Note that each input requires two transactions, in order to
    hold the entire signature.

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

      IMPORTANT: Must have ``balance`` and ``key_index`` attributes!
      Use :py:meth:`cornode.api.get_inputs` to prepare inputs.
    """
        if self.hash:
            raise RuntimeError('Bundle is already finalized.')

        for addy in inputs:
            if addy.balance is None:
                raise with_context(
                    exc=ValueError(
                        'Address {address} has null ``balance`` '
                        '(``exc.context`` has more info).'.format(
                            address=addy, ), ),
                    context={
                        'address': addy,
                    },
                )

            if addy.key_index is None:
                raise with_context(
                    exc=ValueError(
                        'Address {address} has null ``key_index`` '
                        '(``exc.context`` has more info).'.format(
                            address=addy, ), ),
                    context={
                        'address': addy,
                    },
                )

            # Add the input as a transaction.
            self._transactions.append(
                ProposedTransaction(
                    address=addy,
                    tag=self.tag,

                    # Spend the entire address balance; if necessary, we will add a
                    # change transaction to the bundle.
                    value=-addy.balance,
                ))

            # Signatures require additional transactions to store, due to
            # transaction length limit.
            # Subtract 1 to account for the transaction we just added.
            for _ in range(AddressGenerator.DIGEST_ITERATIONS - 1):
                self._transactions.append(
                    ProposedTransaction(
                        address=addy,
                        tag=self.tag,

                        # Note zero value; this is a meta transaction.
                        value=0,
                    ))
Exemple #20
0
  def __init__(self, trytes, pad=None):
    # type: (TrytesCompatible, Optional[int]) -> None
    """
    :param trytes:
      Byte string or bytearray.

    :param pad:
      Ensure at least this many trytes.

      If there are too few, null trytes will be appended to the
      TryteString.

      Note:  If the TryteString is too long, it will _not_ be
      truncated!
    """
    super(TryteString, self).__init__()

    if isinstance(trytes, (int, float)):
      raise with_context(
        exc = TypeError(
          'Converting {type} is not supported; '
          '{cls} is not a numeric type.'.format(
            type  = type(trytes).__name__,
            cls   = type(self).__name__,
          ),
        ),

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

    if isinstance(trytes, TryteString):
      incoming_type = type(trytes)

      if incoming_type is TryteString or issubclass(incoming_type, type(self)):
        # Create a copy of the incoming TryteString's trytes, to ensure
        # we don't modify it when we apply padding.
        trytes = bytearray(trytes._trytes)

      else:
        raise with_context(
          exc = TypeError(
            '{cls} cannot be initialized from a(n) {type}.'.format(
              type  = type(trytes).__name__,
              cls   = type(self).__name__,
            ),
          ),

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

    else:
      if not isinstance(trytes, bytearray):
        trytes = bytearray(trytes)

      for i, ordinal in enumerate(trytes):
        if ordinal not in TrytesCodec.index:
          raise with_context(
            exc = ValueError(
              'Invalid character {char!r} at position {i} '
              '(expected A-Z or 9).'.format(
                char  = chr(ordinal),
                i     = i,
              ),
            ),

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

    if pad:
      trytes += b'9' * max(0, pad - len(trytes))

    self._trytes = trytes # type: bytearray
Exemple #21
0
    def get_keys(self, start, count=1, step=1, iterations=1):
        # type: (int, int, int, int) -> List[PrivateKey]
        """
    Generates and returns one or more keys at the specified index(es).

    This is a one-time operation; if you want to create lots of keys
    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 keys is a large number!

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

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

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

    :param iterations:
      Number of _transform iterations to apply to each key.
      Must be >= 1.

      Increasing this value makes key generation slower, but more
      resistant to brute-forcing.

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

      The returned list will contain ``count`` keys, 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,
                    'iterations': iterations,
                },
            )

        if not step:
            raise with_context(
                exc=ValueError('``step`` must not be zero.'),
                context={
                    'start': start,
                    'count': count,
                    'step': step,
                    'iterations': iterations,
                },
            )

        iterator = self.create_iterator(start, step, iterations)

        keys = []
        for _ in range(count):
            try:
                next_key = next(iterator)
            except StopIteration:
                break
            else:
                keys.append(next_key)

        return keys
    def _execute(self, request):
        # Required parameters.
        seed = request['seed']  # type: Seed
        bundle = ProposedBundle(request['transfers'])

        # Optional parameters.
        change_address = request.get(
            'changeAddress')  # type: Optional[Address]
        proposed_inputs = request.get(
            'inputs')  # type: Optional[List[Address]]

        want_to_spend = bundle.balance
        if want_to_spend > 0:
            # We are spending inputs, so we need to gather and sign them.
            if proposed_inputs is None:
                # No inputs provided.  Scan addresses for unspent inputs.
                gi_response = GetInputsCommand(self.adapter)(
                    seed=seed,
                    threshold=want_to_spend,
                )

                confirmed_inputs = gi_response['inputs']
            else:
                # Inputs provided.  Check to make sure we have sufficient
                # balance.
                available_to_spend = 0
                confirmed_inputs = []  # type: List[Address]

                gb_response = GetBalancesCommand(self.adapter)(
                    addresses=[i.address for i in proposed_inputs], )

                for i, balance in enumerate(gb_response.get('balances') or []):
                    input_ = proposed_inputs[i]

                    if balance > 0:
                        available_to_spend += balance

                        # Update the address balance from the API response, just in
                        # case somebody tried to cheat.
                        input_.balance = balance
                        confirmed_inputs.append(input_)

                if available_to_spend < want_to_spend:
                    raise with_context(
                        exc=BadApiResponse(
                            'Insufficient balance; found {found}, need {need} '
                            '(``exc.context`` has more info).'.format(
                                found=available_to_spend,
                                need=want_to_spend,
                            ), ),
                        context={
                            'available_to_spend': available_to_spend,
                            'confirmed_inputs': confirmed_inputs,
                            'request': request,
                            'want_to_spend': want_to_spend,
                        },
                    )

            bundle.add_inputs(confirmed_inputs)

            if bundle.balance < 0:
                if not change_address:
                    change_address =\
                      GetNewAddressesCommand(self.adapter)(seed=seed)['addresses'][0]

                bundle.send_unspent_inputs_to(change_address)

            bundle.finalize()

            if confirmed_inputs:
                bundle.sign_inputs(KeyGenerator(seed))
        else:
            bundle.finalize()

        return {
            'trytes': bundle.as_tryte_strings(),
        }
Exemple #23
0
    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,
            },
        )
Exemple #24
0
    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_key = next(generator)
            except StopIteration:
                break
            else:
                addresses.append(next_key)

        return addresses
Exemple #25
0
  def __init__(
      self,
      uri,
      auth_token,
      poll_interval = DEFAULT_POLL_INTERVAL,
      max_polls     = DEFAULT_MAX_POLLS,
  ):
    # type: (Union[Text, SplitResult], Optional[Text], int, int) -> None
    """
    :param uri:
      URI of the node to connect to.
      ``https://` URIs are recommended!

      Note: Make sure the URI specifies the correct path!

      Example:

      - Incorrect: ``https://sandbox.cornode:14265``
      - Correct:   ``https://sandbox.cornode:14265/api/v1/``

    :param auth_token:
      Authorization token used to authenticate requests.

      Contact the node's maintainer to obtain a token.

      If ``None``, the adapter will not include authorization metadata
      with requests.

    :param poll_interval:
      Number of seconds to wait between requests to check job status.
      Must be a positive integer.

      Smaller values will cause the adapter to return a result sooner
      (once the node completes the job), but it increases traffic to
      the node (which may trip a rate limiter and/or incur additional
      costs).

    :param max_polls:
      Max number of times to poll for job status before giving up.
      Must be a positive integer.

      This is effectively a timeout setting for asynchronous jobs;
      multiply by ``poll_interval`` to get the timeout duration.
    """
    super(SandboxAdapter, self).__init__(uri)

    if not (isinstance(auth_token, text_type) or (auth_token is None)):
      raise with_context(
        exc =
          TypeError(
            '``auth_token`` must be a unicode string or ``None`` '
            '(``exc.context`` has more info).'
          ),

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

    if auth_token == '':
      raise with_context(
        exc =
          ValueError(
            'Set ``auth_token=None`` if requests do not require authorization '
            '(``exc.context`` has more info).',
          ),

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

    if not isinstance(poll_interval, int):
      raise with_context(
        exc =
          TypeError(
            '``poll_interval`` must be an int '
            '(``exc.context`` has more info).',
          ),

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

    if poll_interval < 1:
      raise with_context(
        exc =
          ValueError(
            '``poll_interval`` must be > 0 '
            '(``exc.context`` has more info).',
          ),

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

    if not isinstance(max_polls, int):
      raise with_context(
        exc =
          TypeError(
            '``max_polls`` must be an int '
            '(``exc.context`` has more info).',
          ),

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

    if max_polls < 1:
      raise with_context(
        exc =
          ValueError(
            '``max_polls`` must be > 0 '
            '(``exc.context`` has more info).',
          ),

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

    self.auth_token     = auth_token # type: Optional[Text]
    self.poll_interval  = poll_interval # type: int
    self.max_polls      = max_polls # type: int
Exemple #26
0
  def _interpret_response(self, response, payload, expected_status):
    # type: (Response, dict, Container[int], bool) -> dict
    decoded =\
      super(SandboxAdapter, self)._interpret_response(
        response        = response,
        payload         = payload,
        expected_status = {codes['ok'], codes['accepted']},
      )

    # Check to see if the request was queued for asynchronous
    # execution.
    if response.status_code == codes['accepted']:
      poll_count = 0
      while decoded['status'] in (STATUS_QUEUED, STATUS_RUNNING):
        if poll_count >= self.max_polls:
          raise with_context(
            exc =
              BadApiResponse(
                '``{command}`` job timed out after {duration} seconds '
                '(``exc.context`` has more info).'.format(
                  command   = decoded['command'],
                  duration  = self.poll_interval * self.max_polls,
                ),
              ),

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

        self._wait_to_poll()
        poll_count += 1

        poll_response = self._send_http_request(
          headers = {'Authorization': self.authorization_header},
          method  = 'get',
          payload = None,
          url     = self.get_jobs_url(decoded['id']),
        )

        decoded =\
          super(SandboxAdapter, self)._interpret_response(
            response        = poll_response,
            payload         = payload,
            expected_status = {codes['ok']},
          )

      if decoded['status'] == STATUS_FINISHED:
        return decoded['{command}Response'.format(command=decoded['command'])]

      raise with_context(
        exc = BadApiResponse(
              decoded.get('error', {}).get('message')
          or  'Command {status}: {decoded}'.format(
                decoded = decoded,
                status  = decoded['status'].lower(),
              ),
        ),

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

    return decoded
Exemple #27
0
    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)