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)
def squeeze(self, trits, offset=0, length=HASH_LENGTH): # type: (MutableSequence[int], Optional[int], Optional[int]) -> None """ Squeeze trits from the sponge. :param trits: Sequence that the squeezed trits will be copied to. Note: this object will be modified! :param offset: Starting offset in ``trits``. :param length: Number of trits to squeeze, default to ``HASH_LENGTH`` """ # Squeeze is kind of like the opposite of absorb; it copies # trits from internal state to the ``trits`` parameter, one hash # at a time, and transforming internal state in between hashes. # # However, only the first hash of the state is "public", so we # can simplify the implementation somewhat. # Ensure length can be mod by HASH_LENGTH if length % HASH_LENGTH != 0: raise with_context( exc=ValueError('Invalid length passed to ``squeeze`.'), context={ 'trits': trits, 'offset': offset, 'length': length, }) # Ensure that ``trits`` can hold at least one hash worth of # trits. trits.extend([0] * max(0, length - len(trits))) # Check trits with offset can handle hash length if len(trits) - offset < HASH_LENGTH: raise with_context( exc=ValueError('Invalid offset passed to ``squeeze``.'), context={ 'trits': trits, 'offset': offset, 'length': length }, ) while length >= HASH_LENGTH: # Copy exactly one hash. trits[offset:offset + HASH_LENGTH] = self._state[0:HASH_LENGTH] # One hash worth of trits copied; now transform. self._transform() offset += HASH_LENGTH length -= HASH_LENGTH
async 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 = (await 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)
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)
async def __call__(self, **kwargs): # type: (**Any) -> 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 = await self._execute(self.request) replacement = self._prepare_response(self.response) if replacement is not None: self.response = replacement self.called = True return self.response
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
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, }, ) if txn.address.security_level is None: raise with_context( exc=ValueError( 'Unable to sign input {input}; ' '``security_level`` is None ' '(``exc.context`` has more info).'.format( input=txn.address, ), ), context={ 'transaction': txn, }, ) self.sign_input_at(i, key_generator.get_key_for(txn.address)) i += txn.address.security_level else: # No signature needed (nor even possible, in some # cases); skip this transaction. i += 1
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
def __init__( self, trytes, # type: TrytesCompatible balance=None, # type: Optional[int] key_index=None, # type: Optional[int] security_level=None, # type: Optional[int] ): # type: (...) -> 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. Defaults to ``None``; usually set via the ``getInputs`` command. References: - :py:class:`iota_async.commands.extended.get_inputs` - :py:meth:`ProposedBundle.add_inputs` """ self.key_index = key_index """ Index of the key used to generate this address. Defaults to ``None``; usually set via ``AddressGenerator``. References: - :py:class:`iota_async.crypto.addresses.AddressGenerator` """ self.security_level = security_level """
def add_inputs(self, inputs): # type: (Iterable[Address]) -> 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: Addresses to use as the inputs for this bundle. .. important:: Must have ``balance`` and ``key_index`` attributes! Use :py:meth:`iota_async.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, }, ) self._create_input_transactions(addy)
def convert_value_to_standard_unit(value, symbol='i'): # type: (Text, Text) -> float """ Converts between any two standard units of iota_async. :param value: Value (affixed) to convert. For example: '1.618 Mi'. :param symbol: Unit symbol of iota_async to convert to. For example: 'Gi'. :return: Float as units of given symbol to convert to. """ try: # Get input value value_tuple = value.split() amount = float(value_tuple[0]) except (ValueError, IndexError, AttributeError): raise with_context( ValueError('Value to convert is not valid.'), context={ 'value': value, }, ) try: # Set unit symbols and find factor/multiplier. unit_symbol_from = value_tuple[1] unit_factor_from = float(STANDARD_UNITS[unit_symbol_from]) unit_factor_to = float(STANDARD_UNITS[symbol]) except (KeyError, IndexError): # Invalid symbol or no factor raise with_context( ValueError('Invalid IOTA unit.'), context={ 'value': value, 'symbol': symbol, }, ) return amount * (unit_factor_from / unit_factor_to)
def absorb(self, trits, offset=0, length=None): # type: (MutableSequence[int], int, Optional[int]) -> None """ Absorb trits into the sponge from a buffer. :param trits: Buffer that contains the trits to absorb. :param offset: Starting offset in ``trits``. :param length: Number of trits to absorb. Defaults to ``len(trits)``. """ # Pad input if necessary, so that it can be divided evenly into # hashes. # Note that this operation creates a COPY of ``trits``; the # incoming buffer is not modified! pad = ((len(trits) % TRIT_HASH_LENGTH) or TRIT_HASH_LENGTH) trits += [0] * (TRIT_HASH_LENGTH - pad) if length is None: length = len(trits) if length < 1: raise with_context( exc=ValueError('Invalid length passed to ``absorb``.'), context={ 'trits': trits, 'offset': offset, 'length': length, }, ) while offset < length: stop = min(offset + TRIT_HASH_LENGTH, length) # If we're copying over a full chunk, zero last trit. if stop - offset == TRIT_HASH_LENGTH: trits[stop - 1] = 0 signed_nums = conv.convertToBytes(trits[offset:stop]) # Convert signed bytes into their equivalent unsigned # representation, in order to use Python's built-in bytes # type. unsigned_bytes = bytearray( conv.convert_sign(b) for b in signed_nums) self.k.update(unsigned_bytes) offset += TRIT_HASH_LENGTH
def __init__(self, trytes): # type: (TrytesCompatible) -> None super(Tag, 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, }, )
def __init__(self, seed, start, step, security_level): # 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, 'security_level': security_level, }, ) if security_level < 1: raise with_context( exc=ValueError('``security_level`` must be >= 1.'), context={ 'start': start, 'step': step, 'security_level': security_level, }, ) # In order to work correctly, the seed must be padded so that it # is a multiple of 81 trytes. seed += b'9' * (Hash.LEN - ((len(seed) % Hash.LEN) or Hash.LEN)) self.security_level = security_level self.seed_as_trits = seed.as_trits() self.start = start self.step = step self.current = self.start self.fragment_length = FRAGMENT_LENGTH * TRITS_PER_TRYTE self.hashes_per_fragment = FRAGMENT_LENGTH // Hash.LEN
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
def absorb(self, trits, offset=0, length=None): # type: (Sequence[int], Optional[int], Optional[int]) -> None """ Absorb trits into the sponge. :param trits: Sequence of trits to absorb. :param offset: Starting offset in ``trits``. :param length: Number of trits to absorb. Defaults to ``len(trits)``. """ pad = ((len(trits) % HASH_LENGTH) or HASH_LENGTH) trits += [0] * (HASH_LENGTH - pad) if length is None: length = len(trits) if length < 1: raise with_context( exc=ValueError('Invalid length passed to ``absorb``.'), context={ 'trits': trits, 'offset': offset, 'length': length, }, ) # Copy trits from ``trits`` into internal state, one hash at a # time, transforming internal state in between hashes. while offset < length: start = offset stop = min(start + HASH_LENGTH, length) # Copy the next hash worth of trits to internal state. # # Note that we always copy the trits to the start of the # state. ``self._state`` is 3 hashes long, but only the # first hash is "public"; the other 2 are only accessible to # :py:meth:`_transform`. self._state[0:stop - start] = trits[start:stop] # Transform. self._transform() # Move on to the next hash. offset += HASH_LENGTH
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, }, )
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()
def __init__(self, trytes, key_index=None, security_level=None): # type: (TrytesCompatible, Optional[int], 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': self._trytes, }, ) self.key_index = key_index self.security_level = security_level
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]
def __init__(self, trytes, key_index=None): # type: (TrytesCompatible, Optional[int]) -> None super(Digest, self).__init__(trytes) # A digest is a series of hashes; its length should reflect # that. if len(self) % Hash.LEN: raise with_context( exc=ValueError( 'Length of {cls} values ' 'must be a multiple of {len} trytes.'.format( cls=type(self).__name__, len=Hash.LEN, ), ), context={ 'trytes': trytes, }, ) self.key_index = key_index
def __eq__(self, other): # type: (TrytesCompatible) -> bool if isinstance(other, TryteString): return self._trytes == other._trytes elif isinstance(other, text_type): return self._trytes == other.encode('ascii') 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, }, )
async def _execute(self, request): transaction_hash = request['transaction'] # type: TransactionHash bundle = Bundle(await 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], }
def squeeze(self, trits, offset=0, length=None): # type: (MutableSequence[int], int, Optional[int]) -> None """ Squeeze trits from the sponge into a buffer. :param trits: Buffer that will hold the squeezed trits. IMPORTANT: If ``trits`` is too small, it will be extended! :param offset: Starting offset in ``trits``. :param length: Number of trits to squeeze from the sponge. If not specified, defaults to :py:data:`TRIT_HASH_LENGTH` (i.e., by default, we will try to squeeze exactly 1 hash). """ # Pad input if necessary, so that it can be divided evenly into # hashes. pad = ((len(trits) % TRIT_HASH_LENGTH) or TRIT_HASH_LENGTH) trits += [0] * (TRIT_HASH_LENGTH - pad) if length is None: # By default, we will try to squeeze one hash. # Note that this is different than ``absorb``. length = len(trits) or TRIT_HASH_LENGTH if length < 1: raise with_context( exc=ValueError('Invalid length passed to ``squeeze``.'), context={ 'trits': trits, 'offset': offset, 'length': length, }, ) while offset < length: unsigned_hash = self.k.digest() if PY2: unsigned_hash = map(ord, unsigned_hash) # type: ignore signed_hash = [conv.convert_sign(b) for b in unsigned_hash] trits_from_hash = conv.convertToTrits(signed_hash) trits_from_hash[TRIT_HASH_LENGTH - 1] = 0 stop = min(TRIT_HASH_LENGTH, length - offset) trits[offset:offset + stop] = trits_from_hash[0:stop] flipped_bytes = bytearray( conv.convert_sign(~b) for b in unsigned_hash) # Reset internal state before feeding back in. self.reset() self.k.update(flipped_bytes) offset += TRIT_HASH_LENGTH
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
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, }, )
def sign_input_transactions(self, bundle, start_index): # type: (Bundle, int) -> None """ Signs the inputs starting at the specified index. :param bundle: The bundle that contains the input transactions to sign. :param start_index: The index of the first input transaction. If necessary, the resulting signature will be split across subsequent transactions automatically. """ if not bundle.hash: raise with_context( exc=ValueError('Cannot sign inputs without a bundle hash!'), context={ 'bundle': bundle, 'key_index': self.key_index, 'start_index': start_index, }, ) from iota_async.crypto.signing import SignatureFragmentGenerator signature_fragment_generator = (SignatureFragmentGenerator( self, bundle.hash)) # We can only fit one signature fragment into each transaction, # so we have to split the entire signature. for j in range(self.security_level): # Do lots of validation before we attempt to sign the # transaction, and attach lots of context info to any # exception. # # This method is likely to be invoked at a very low level in # the application, so if anything goes wrong, we want to # make sure it's as easy to troubleshoot as possible! try: txn = bundle[start_index + j] except IndexError as e: raise with_context( exc=e, context={ 'bundle': bundle, 'key_index': self.key_index, 'current_index': start_index + j, }, ) # Only inputs can be signed. if txn.value > 0: raise with_context( exc=ValueError( 'Attempting to sign non-input transaction #{i} ' '(value={value}).'.format( i=txn.current_index, value=txn.value, ), ), context={ 'bundle': bundle, 'key_index': self.key_index, 'start_index': start_index, }, ) if txn.signature_message_fragment: raise with_context( exc=ValueError( 'Attempting to sign input transaction #{i}, ' 'but it has a non-empty fragment ' '(is it already signed?).'.format( i=txn.current_index, ), ), context={ 'bundle': bundle, 'key_index': self.key_index, 'start_index': start_index, }, ) txn.signature_message_fragment = next(signature_fragment_generator)
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, also known as security level. 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 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)
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 isinstance(trytes, text_type): trytes = encode(trytes, 'ascii') if not isinstance(trytes, bytearray): trytes = bytearray(trytes) for i, ordinal in enumerate(trytes): if ordinal not in AsciiTrytesCodec.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