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, 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
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 __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
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 _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 _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 __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 __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, }, )
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, 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 __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 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 __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]
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 __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 """
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, ))
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
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(), }
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 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
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
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 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)