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 )
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): # 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
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)
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 = 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 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.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 _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'], }, )
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 convert_value_to_standard_unit(value, symbol='i'): # type: (Text, Text) -> float """ Converts between any two standard units of iota. :param value: Value (affixed) to convert. For example: '1.618 Mi'. :param symbol: Unit symbol of iota 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 __init__(self, trytes): # type: (TrytesCompatible) -> None super(Nonce, 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, }, )
async def send_request(self, payload: Dict, **kwargs: Any) -> dict: """ Mimic asynchronous behavior of `HttpAdapter.send_request`. """ # 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 await async_return(response)
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 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 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 absorb(self, trits: Sequence[int], offset: Optional[int] = 0, length: Optional[int] = None) -> 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, balance=None, key_index=None, security_level=None): # type: (TrytesCompatible, Optional[int], 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:`iota.commands.extended.get_inputs` - :py:meth:`ProposedBundle.add_inputs` """ self.key_index = key_index """ Index of the key used to generate this address. Must be set manually via ``AddressGenerator``. References: - :py:class:`iota.crypto.addresses.AddressGenerator` """ self.security_level = security_level """
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 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 __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 __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 __init__(self, trytes): # type: (TrytesCompatible) -> None super(Nonce, 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, 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
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']: while decoded['status'] in (STATUS_QUEUED, STATUS_RUNNING): self._wait_to_poll() 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
def __init__(self, trytes, balance=None, key_index=None, security_level=None): # type: (TrytesCompatible, Optional[int], 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:`iota.commands.extended.get_inputs` - :py:meth:`ProposedBundle.add_inputs` """ self.key_index = key_index """ Index of the key used to generate this address. Must be set manually via ``AddressGenerator``. References: - :py:class:`iota.crypto.addresses.AddressGenerator` """ self.security_level = security_level """
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
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): # 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, (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, }, )
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]
async def fetch_and_validate(tx_hash): bundle = (await TraverseBundleCommand(self.adapter)( transaction=tx_hash ))['bundles'][0] # Currently 1 bundle only 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 bundle
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 __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 __add__(self, other): # type: (TrytesCompatible) -> TryteString if isinstance(other, TryteString): return TryteString(self._trytes + other._trytes) elif isinstance(other, text_type): return TryteString(self._trytes + other.encode('ascii')) elif isinstance(other, (binary_type, bytearray)): return TryteString(self._trytes + other) else: raise with_context( exc=TypeError( 'Invalid type for TryteString concatenation ' '(expected Union[TryteString, {binary_type}, bytearray], ' 'actual {type}).'.format( binary_type=binary_type.__name__, type=type(other).__name__, ), ), context={ 'other': other, }, )
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: TrytesCompatible, key_index: Optional[int] = None, security_level: Optional[int] = None) -> 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 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 __eq__(self, other: TrytesCompatible) -> bool: if isinstance(other, TryteString): return self._trytes == other._trytes elif isinstance(other, str): return self._trytes == other.encode('ascii') elif isinstance(other, (bytes, bytearray)): return self._trytes == other else: raise with_context( exc=TypeError( 'Invalid type for TryteString comparison ' '(expected Union[TryteString, {bytes}, bytearray], ' 'actual {type}).'.format( bytes=bytes.__name__, type=type(other).__name__, ), ), context={ 'other': other, }, )
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], }
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], }
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, }, )
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
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:`iota.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, ))
def create_generator(self, start=0, step=1, iterations=1): # type: (int, int) -> Generator[PrivateKey] """ Creates a generator that can be used to progressively generate new keys. :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 key. This value can be negative; the generator will exit if it reaches an index < 0. Warning: The generator may take awhile to advance between iterations if ``step`` is a large number! :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. """ 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, }, ) current = start fragment_length = FRAGMENT_LENGTH * TRITS_PER_TRYTE hashes_per_fragment = FRAGMENT_LENGTH // Hash.LEN while current >= 0: sponge = self._create_sponge(current) key = [0] * (fragment_length * iterations) buffer = [0] * HASH_LENGTH # type: MutableSequence[int] for fragment_seq in range(iterations): # Squeeze trits from the buffer and append them to the key, one # hash at a time. for hash_seq in range(hashes_per_fragment): sponge.squeeze(buffer) key_start =\ (fragment_seq * fragment_length) + (hash_seq * HASH_LENGTH) key_stop = key_start + HASH_LENGTH key[key_start:key_stop] = buffer private_key = PrivateKey.from_trits(key) private_key.key_index = current yield private_key current += step
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_generator` 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, }, ) generator = self.create_generator(start, step, iterations) keys = [] for _ in range(count): try: next_key = next(generator) except StopIteration: break else: keys.append(next_key) return keys
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.iota:14265`` - Correct: ``https://sandbox.iota: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
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 _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 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 _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(), }
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 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 _execute(self, request): change_address = request['changeAddress'] # type: Optional[Address] multisig_input = request['multisigInput'] # type: MultisigAddress transfers = request['transfers'] # type: List[ProposedTransaction] bundle = ProposedMultisigBundle(transfers) want_to_spend = bundle.balance if want_to_spend > 0: gb_response =\ GetBalancesCommand(self.adapter)( addresses = [multisig_input], ) multisig_input.balance = gb_response['balances'][0] if multisig_input.balance < want_to_spend: raise with_context( exc = ValueError( 'Insufficient balance; found {found}, need {need} ' '(``exc.context`` has more info).'.format( found = multisig_input.balance, need = want_to_spend, ), ), # The structure of this context object is intended to match # the one from ``PrepareTransferCommand``. context = { 'available_to_spend': multisig_input.balance, 'confirmed_inputs': [multisig_input], 'request': request, 'want_to_spend': want_to_spend, }, ) bundle.add_inputs([multisig_input]) if bundle.balance < 0: if change_address: bundle.send_unspent_inputs_to(change_address) else: # # Unlike :py:meth:`iota.api.Iota.prepare_transfer` where all # of the inputs are owned by the same seed, creating a # multisig transfer usually involves multiple people. # # It would be unfair to the participants of the transaction # if we were to automatically generate a change address using # the seed of whoever happened to invoke the # :py:meth:`MultisigIota.prepare_multisig_transfer` method! # raise with_context( exc = ValueError( 'Bundle has unspent inputs, but no change address specified.', ), context = { 'available_to_spend': multisig_input.balance, 'balance': bundle.balance, 'confirmed_inputs': [multisig_input], 'request': request, 'want_to_spend': want_to_spend, }, ) else: raise with_context( exc = ValueError( 'Use ``prepare_transfer`` ' 'to create a bundle without spending IOTAs.', ), context = { 'request': request, }, ) bundle.finalize() # Return the bundle with inputs unsigned. return { 'trytes': bundle.as_tryte_strings(), }
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 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 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 sign_input_transactions(self, bundle, start_index): # type: (Bundle, int) -> None """ Signs the inputs starting at the specified index. :param Bundle bundle: The bundle that contains the input transactions to sign. :param int start_index: The index of the first input transaction. If necessary, the resulting signature will be split across subsequent transactions automatically. :raises ValuError: - if ``bundle`` is not finalized. - if attempting to sign non-input transactions. - if attempting to sign transactions with non-empty ``signature_message_fragment`` field. :raises IndexError: if wrong ``start_index`` is provided. """ 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.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 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)