def _decode_transaction(self, data: Union[bytes, str]) -> Tuple[str, List[Tuple[str, str, Any]]]: """ Decode tx data :param data: Tx data as `hex string` or `bytes` :return: Tuple with the `function name` and a List of sorted tuples with the `name` of the argument, `type` and `value` :raises: CannotDecode if data cannot be decoded. You should catch this exception when using this function :raises: UnexpectedProblemDecoding if there's an unexpected problem decoding (it shouldn't happen) """ if not data: raise CannotDecode(data) data = HexBytes(data) selector, params = data[:4], data[4:] if selector not in self.supported_fn_selectors: raise CannotDecode(data.hex()) try: contract_fn = self.supported_fn_selectors[selector] names = get_abi_input_names(contract_fn.abi) types = get_abi_input_types(contract_fn.abi) decoded = self.dummy_w3.codec.decode_abi(types, cast(HexBytes, params)) normalized = map_abi_data(BASE_RETURN_NORMALIZERS, types, decoded) values = map(self._parse_decoded_arguments, normalized) except ValueError as exc: raise UnexpectedProblemDecoding from exc return contract_fn.fn_name, list(zip(names, types, values))
def solidityKeccak(cls, abi_types: List[TypeStr], values: List[Any]) -> bytes: """ Executes keccak256 exactly as Solidity does. Takes list of abi_types as inputs -- `[uint24, int8[], bool]` and list of corresponding values -- `[20, [-1, 5, 0], True]` """ if len(abi_types) != len(values): raise ValueError( "Length mismatch between provided abi types and values. Got " "{0} types and {1} values.".format(len(abi_types), len(values))) if isinstance(cls, type): w3 = None else: w3 = cls normalized_values = map_abi_data([abi_ens_resolver(w3)], abi_types, values) hex_string = add_0x_prefix( HexStr(''.join( remove_0x_prefix(hex_encode_abi_type(abi_type, value)) for abi_type, value in zip(abi_types, normalized_values)))) return cls.keccak(hexstr=hex_string)
def encode_abi(web3, abi, arguments, data=None): argument_types = get_abi_input_types(abi) if not check_if_arguments_can_be_encoded(abi, arguments, {}): raise TypeError( "One or more arguments could not be encoded to the necessary " "ABI type. Expected types are: {0}".format( ', '.join(argument_types), )) normalizers = [ abi_ens_resolver(web3), abi_address_to_hex, abi_bytes_to_bytes, abi_string_to_text, ] normalized_arguments = map_abi_data( normalizers, argument_types, arguments, ) encoded_arguments = eth_abi_encode_abi( argument_types, normalized_arguments, ) if data: return to_hex(HexBytes(data) + encoded_arguments) else: return encode_hex(encoded_arguments)
def encode_abi(w3: "Web3", abi: ABIFunction, arguments: Sequence[Any], data: Optional[HexStr] = None) -> HexStr: argument_types = get_abi_input_types(abi) if not check_if_arguments_can_be_encoded(abi, w3.codec, arguments, {}): raise TypeError( "One or more arguments could not be encoded to the necessary " f"ABI type. Expected types are: {', '.join(argument_types)}") normalizers = [ abi_ens_resolver(w3), abi_address_to_hex, abi_bytes_to_bytes, abi_string_to_text, ] normalized_arguments = map_abi_data( normalizers, argument_types, arguments, ) encoded_arguments = w3.codec.encode_abi( argument_types, normalized_arguments, ) if data: return to_hex(HexBytes(data) + encoded_arguments) else: return encode_hex(encoded_arguments)
def decode_function_input(self, data): data = HexBytes(data) selector, params = data[:4], data[4:] func = self.get_function_by_selector(selector) names = [x['name'] for x in func.abi['inputs']] types = [x['type'] for x in func.abi['inputs']] decoded = decode_abi(types, params) normalized = map_abi_data(BASE_RETURN_NORMALIZERS, types, decoded) return func, dict(zip(names, normalized))
def abi_request_formatters(normalizers, abis): for method, abi_types in abis.items(): if isinstance(abi_types, list): yield method, map_abi_data(normalizers, abi_types) elif isinstance(abi_types, dict): single_dict_formatter = apply_abi_formatters_to_dict(normalizers, abi_types) yield method, apply_formatter_at_index(single_dict_formatter, 0) else: raise TypeError("ABI definitions must be a list or dictionary, got %r" % abi_types)
def apply_abi_formatters_to_dict(normalizers, abi_dict, data): fields = list(set(abi_dict.keys()) & set(data.keys())) formatted_values = map_abi_data( normalizers, [abi_dict[field] for field in fields], [data[field] for field in fields], ) formatted_dict = dict(zip(fields, formatted_values)) return dict(data, **formatted_dict)
def decode_function_input(self, data): data = HexBytes(data) selector, params = data[:4], data[4:] func = self.get_function_by_selector(selector) names = get_abi_input_names(func.abi) types = get_abi_input_types(func.abi) decoded = self.web3.codec.decode_abi(types, params) normalized = map_abi_data(BASE_RETURN_NORMALIZERS, types, decoded) return func, dict(zip(names, normalized))
def parse_call_response(fn_abi, result): output_types = get_abi_output_types(fn_abi) output_data = Web3().codec.decode_abi(get_abi_output_types(fn_abi), binascii.unhexlify(result)) _normalizers = itertools.chain( BASE_RETURN_NORMALIZERS, [], ) normalized_data = map_abi_data(_normalizers, output_types, output_data) if len(normalized_data) == 1: return normalized_data[0] else: return normalized_data
def decode_func_with_fallback(abis_to_try, data): for abi in abis_to_try: try: selector, params = data[:4], data[4:] names = get_abi_input_names(abi) types = get_abi_input_types(abi) decoded = w3.codec.decode_abi(types, params) normalized = map_abi_data(BASE_RETURN_NORMALIZERS, types, decoded) return abi['name'], dict(zip(names, normalized)) except DecodingError: logger.debug('trying fallback fn input decoder') raise DecodingError('could not decode fn input')
def apply_abi_formatters_to_dict(normalizers: Sequence[Callable[[TypeStr, Any], Tuple[TypeStr, Any]]], abi_dict: Dict[str, Any], data: Dict[Any, Any]) -> Dict[Any, Any]: fields = list(abi_dict.keys() & data.keys()) formatted_values = map_abi_data( normalizers, [abi_dict[field] for field in fields], [data[field] for field in fields], ) formatted_dict = dict(zip(fields, formatted_values)) return dict(data, **formatted_dict)
def abi_request_formatters( normalizers: Sequence[Callable[[TypeStr, Any], Tuple[TypeStr, Any]]], abis: Dict[RPCEndpoint, Any], ) -> Iterable[Tuple[RPCEndpoint, Callable[..., Any]]]: for method, abi_types in abis.items(): if isinstance(abi_types, list): yield method, map_abi_data(normalizers, abi_types) elif isinstance(abi_types, dict): single_dict_formatter = apply_abi_formatters_to_dict( normalizers, abi_types) yield method, apply_formatter_at_index(single_dict_formatter, 0) else: raise TypeError( f"ABI definitions must be a list or dictionary, got {abi_types!r}" )
def _decode_data(self, output_type: Sequence[str], data: bytes) -> Optional[Any]: """ :param output_type: :param data: :return: :raises: DecodingError """ if data: try: decoded_values = self.w3.codec.decode_abi(output_type, data) normalized_data = map_abi_data( BASE_RETURN_NORMALIZERS, output_type, decoded_values ) if len(normalized_data) == 1: return normalized_data[0] else: return normalized_data except DecodingError: logger.warning( "Cannot decode %s using output-type %s", data, output_type ) return data
def encode_abi(web3, abi, arguments, data=None): argument_types = get_abi_input_types(abi) if not check_if_arguments_can_be_encoded(abi, arguments, {}): raise TypeError( "One or more arguments could not be encoded to the necessary " "ABI type. Expected types are: {0}".format( ', '.join(argument_types), ) ) try: normalizers = [ abi_ens_resolver(web3), abi_address_to_hex, abi_bytes_to_bytes, abi_string_to_text, ] normalized_arguments = map_abi_data( normalizers, argument_types, arguments, ) encoded_arguments = eth_abi_encode_abi( argument_types, normalized_arguments, ) except EncodingError as e: raise TypeError( "One or more arguments could not be encoded to the necessary " "ABI type: {0}".format(str(e)) ) if data: return to_hex(HexBytes(data) + encoded_arguments) else: return encode_hex(encoded_arguments)
def _decode_data( self, data: Union[bytes, str], address: Optional[ChecksumAddress] = None ) -> Tuple[str, List[Tuple[str, str, Any]]]: """ Decode tx data :param data: Tx data as `hex string` or `bytes` :param address: contract address in case of ABI colliding :return: Tuple with the `function name` and a List of sorted tuples with the `name` of the argument, `type` and `value` :raises: CannotDecode if data cannot be decoded. You should catch this exception when using this function :raises: UnexpectedProblemDecoding if there's an unexpected problem decoding (it shouldn't happen) """ if not data: raise CannotDecode(data) data = HexBytes(data) params = data[4:] fn_abi = self.get_abi_function(data, address) if not fn_abi: raise CannotDecode(data.hex()) try: names = get_abi_input_names(fn_abi) types = get_abi_input_types(fn_abi) decoded = self.dummy_w3.codec.decode_abi(types, cast(HexBytes, params)) normalized = map_abi_data(BASE_RETURN_NORMALIZERS, types, decoded) values = map(self._parse_decoded_arguments, normalized) except (ValueError, DecodingError) as exc: logger.warning("Cannot decode %s", data.hex()) raise UnexpectedProblemDecoding(data) from exc return fn_abi["name"], list(zip(names, types, values))
def test_map_abi_data(types, data, funcs, expected): assert map_abi_data(funcs, types, data) == expected
def get_event_data(abi_codec: ABICodec, event_abi: ABIEvent, log_entry: LogReceipt) -> EventData: """ Given an event ABI and a log entry for that event, return the decoded event data """ if event_abi['anonymous']: log_topics = log_entry['topics'] elif not log_entry['topics']: raise MismatchedABI("Expected non-anonymous event to have 1 or more topics") # type ignored b/c event_abi_to_log_topic(event_abi: Dict[str, Any]) elif event_abi_to_log_topic(event_abi) != log_entry['topics'][0]: # type: ignore raise MismatchedABI("The event signature did not match the provided ABI") else: log_topics = log_entry['topics'][1:] log_topics_abi = get_indexed_event_inputs(event_abi) log_topic_normalized_inputs = normalize_event_input_types(log_topics_abi) log_topic_types = get_event_abi_types_for_decoding(log_topic_normalized_inputs) log_topic_names = get_abi_input_names(ABIEvent({'inputs': log_topics_abi})) if len(log_topics) != len(log_topic_types): raise LogTopicError("Expected {0} log topics. Got {1}".format( len(log_topic_types), len(log_topics), )) log_data = hexstr_if_str(to_bytes, log_entry['data']) log_data_abi = exclude_indexed_event_inputs(event_abi) log_data_normalized_inputs = normalize_event_input_types(log_data_abi) log_data_types = get_event_abi_types_for_decoding(log_data_normalized_inputs) log_data_names = get_abi_input_names(ABIEvent({'inputs': log_data_abi})) # sanity check that there are not name intersections between the topic # names and the data argument names. duplicate_names = set(log_topic_names).intersection(log_data_names) if duplicate_names: raise InvalidEventABI( "The following argument names are duplicated " f"between event inputs: '{', '.join(duplicate_names)}'" ) decoded_log_data = abi_codec.decode_abi(log_data_types, log_data) normalized_log_data = map_abi_data( BASE_RETURN_NORMALIZERS, log_data_types, decoded_log_data ) decoded_topic_data = [ abi_codec.decode_single(topic_type, topic_data) for topic_type, topic_data in zip(log_topic_types, log_topics) ] normalized_topic_data = map_abi_data( BASE_RETURN_NORMALIZERS, log_topic_types, decoded_topic_data ) event_args = dict(itertools.chain( zip(log_topic_names, normalized_topic_data), zip(log_data_names, normalized_log_data), )) event_data = { 'args': event_args, 'event': event_abi['name'], 'logIndex': log_entry['logIndex'], 'transactionIndex': log_entry['transactionIndex'], 'transactionHash': log_entry['transactionHash'], 'address': log_entry['address'], 'blockHash': log_entry['blockHash'], 'blockNumber': log_entry['blockNumber'], } return cast(EventData, AttributeDict.recursive(event_data))
def call_contract_function(web3, address, normalizers, function_identifier, transaction, block_id=None, contract_abi=None, fn_abi=None, *args, **kwargs): """ Helper function for interacting with a contract function using the `eth_call` API. """ call_transaction = prepare_transaction( address, web3, fn_identifier=function_identifier, contract_abi=contract_abi, fn_abi=fn_abi, transaction=transaction, fn_args=args, fn_kwargs=kwargs, ) if block_id is None: return_data = web3.eth.call(call_transaction) else: return_data = web3.eth.call(call_transaction, block_identifier=block_id) if fn_abi is None: fn_abi = find_matching_fn_abi(contract_abi, function_identifier, args, kwargs) output_types = get_abi_output_types(fn_abi) try: output_data = decode_abi(output_types, return_data) except DecodingError as e: # Provide a more helpful error message than the one provided by # eth-abi-utils is_missing_code_error = (return_data in ACCEPTABLE_EMPTY_STRINGS and web3.eth.getCode(address) in ACCEPTABLE_EMPTY_STRINGS) if is_missing_code_error: msg = ( "Could not transact with/call contract function, is contract " "deployed correctly and chain synced?") else: msg = ( "Could not decode contract function call {} return data {} for " "output_types {}".format(function_identifier, return_data, output_types)) raise BadFunctionCallOutput(msg) from e _normalizers = itertools.chain( BASE_RETURN_NORMALIZERS, normalizers, ) normalized_data = map_abi_data(_normalizers, output_types, output_data) if len(normalized_data) == 1: return normalized_data[0] else: return normalized_data
def call_contract_function( web3, address, normalizers, function_identifier, transaction, block_id=None, contract_abi=None, fn_abi=None, *args, **kwargs): """ Helper function for interacting with a contract function using the `eth_call` API. """ call_transaction = prepare_transaction( address, web3, fn_identifier=function_identifier, contract_abi=contract_abi, fn_abi=fn_abi, transaction=transaction, fn_args=args, fn_kwargs=kwargs, ) if block_id is None: return_data = web3.eth.call(call_transaction) else: return_data = web3.eth.call(call_transaction, block_identifier=block_id) if fn_abi is None: fn_abi = find_matching_fn_abi(contract_abi, function_identifier, args, kwargs) output_types = get_abi_output_types(fn_abi) try: output_data = decode_abi(output_types, return_data) except DecodingError as e: # Provide a more helpful error message than the one provided by # eth-abi-utils is_missing_code_error = ( return_data in ACCEPTABLE_EMPTY_STRINGS and web3.eth.getCode(address) in ACCEPTABLE_EMPTY_STRINGS ) if is_missing_code_error: msg = ( "Could not transact with/call contract function, is contract " "deployed correctly and chain synced?" ) else: msg = ( "Could not decode contract function call {} return data {} for " "output_types {}".format( function_identifier, return_data, output_types ) ) raise BadFunctionCallOutput(msg) from e _normalizers = itertools.chain( BASE_RETURN_NORMALIZERS, normalizers, ) normalized_data = map_abi_data(_normalizers, output_types, output_data) if len(normalized_data) == 1: return normalized_data[0] else: return normalized_data