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 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 _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 test_finds_function_with_matching_args(web3, arguments, expected_types): Contract = web3.eth.contract(abi=MULTIPLE_FUNCTIONS) abi = Contract._find_matching_fn_abi('a', arguments) assert abi['name'] == 'a' assert len(abi['inputs']) == len(expected_types) assert set(get_abi_input_types(abi)) == set(expected_types)
def test_finds_function_with_matching_args_deprecation_warning(w3): Contract = w3.eth.contract(abi=MULTIPLE_FUNCTIONS) with pytest.warns(DeprecationWarning): abi = Contract._find_matching_fn_abi('a', ['']) assert abi['name'] == 'a' assert len(abi['inputs']) == len(['bytes32']) assert get_abi_input_types(abi) == ['bytes32']
def decode_log(log_data, events): topic_inputs = get_indexed_event_inputs(log_data['abi']) topic_types = get_abi_input_types({'inputs': topic_inputs}) topic_names = get_abi_input_names({'inputs': topic_inputs}) topic_data = [HexBytes(topic) for topic in log_data['topics'][1:]] decoded_topics = [ decode_single(topic_type, topic_data) for topic_type, topic_data in zip(topic_types, topic_data) ] data_inputs = exclude_indexed_event_inputs(log_data['abi']) data_types = get_abi_input_types({'inputs': data_inputs}) data_names = get_abi_input_names({'inputs': data_inputs}) decoded_data = decode_abi(data_types, HexBytes(log_data['data'])) return dict( itertools.chain( zip(topic_names, decoded_topics), zip(data_names, decoded_data), ))
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 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 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 mock_requests_get(url, *args, **kwargs): if 'etherscan.io/api?module=account&action=balance&address' in url: addr = url[67:109] value = eth_map[addr].get('ETH', '0') response = f'{{"status":"1","message":"OK","result":{value}}}' elif 'etherscan.io/api?module=account&action=balancemulti' in url: queried_accounts = [] length = 72 # process url and get the accounts while True: if len(url) < length: break potential_address = url[length:length + 42] if 'apikey=' in potential_address: break queried_accounts.append(potential_address) length += 43 accounts = [] for addr in queried_accounts: value = eth_map[addr].get('ETH', '0') accounts.append({'account': addr, 'balance': eth_map[addr]['ETH']}) response = f'{{"status":"1","message":"OK","result":{json.dumps(accounts)}}}' elif 'api.etherscan.io/api?module=account&action=tokenbalance' in url: token_address = url[80:122] msg = 'token address missing from test mapping' assert token_address in CONTRACT_ADDRESS_TO_TOKEN, msg response = '{"status":"1","message":"OK","result":"0"}' token = CONTRACT_ADDRESS_TO_TOKEN[token_address] account = url[131:173] value = eth_map[account].get(token.identifier, 0) response = f'{{"status":"1","message":"OK","result":"{value}"}}' elif 'api.etherscan.io/api?module=account&action=txlistinternal&' in url: if 'transactions' in original_queries: return original_requests_get(url, *args, **kwargs) # By default when mocking, don't query for transactions response = '{"status":"1","message":"OK","result":[]}' elif 'api.etherscan.io/api?module=account&action=txlist&' in url: if 'transactions' in original_queries: return original_requests_get(url, *args, **kwargs) # By default when mocking, don't query for transactions response = '{"status":"1","message":"OK","result":[]}' elif 'api.etherscan.io/api?module=logs&action=getLogs&' in url: if 'logs' in original_queries: return original_requests_get(url, *args, **kwargs) # By default when mocking, don't query logs response = '{"status":"1","message":"OK","result":[]}' elif 'api.etherscan.io/api?module=block&action=getblocknobytime&' in url: if 'blocknobytime' in original_queries: return original_requests_get(url, *args, **kwargs) # By default when mocking don't query blocknobytime response = '{"status":"1","message":"OK","result":"1"}' elif f'api.etherscan.io/api?module=proxy&action=eth_call&to={ZERION_ADAPTER_ADDRESS}' in url: # noqa: E501 if 'zerion' in original_queries: return original_requests_get(url, *args, **kwargs) web3 = Web3() contract = web3.eth.contract(address=ZERION_ADAPTER_ADDRESS, abi=ZERION_ABI) if 'data=0xc84aae17' in url: # getBalances data = url.split('data=')[1] if '&apikey' in data: data = data.split('&apikey')[0] fn_abi = contract._find_matching_fn_abi( fn_identifier='getBalances', args=['address'], ) input_types = get_abi_input_types(fn_abi) output_types = get_abi_output_types(fn_abi) decoded_input = web3.codec.decode_abi(input_types, bytes.fromhex(data[10:])) # TODO: This here always returns empty response. If/when we want to # mock it for etherscan, this is where we do it args = [] result = '0x' + web3.codec.encode_abi(output_types, [args]).hex() response = f'{{"jsonrpc":"2.0","id":1,"result":"{result}"}}' elif 'data=0x85c6a7930' in url: # getProtocolBalances data = url.split('data=')[1] if '&apikey' in data: data = data.split('&apikey')[0] fn_abi = contract._find_matching_fn_abi( fn_identifier='getProtocolBalances', args=['address', ['some', 'protocol', 'names']], ) input_types = get_abi_input_types(fn_abi) output_types = get_abi_output_types(fn_abi) decoded_input = web3.codec.decode_abi(input_types, bytes.fromhex(data[10:])) # TODO: This here always returns empty response. If/when we want to # mock it for etherscan, this is where we do it args = [] result = '0x' + web3.codec.encode_abi(output_types, [args]).hex() response = f'{{"jsonrpc":"2.0","id":1,"result":"{result}"}}' elif 'data=0x3b692f52' in url: # getProtocolNames data = url.split('data=')[1] if '&apikey' in data: data = data.split('&apikey')[0] fn_abi = contract._find_matching_fn_abi( fn_identifier='getProtocolNames', ) input_types = get_abi_input_types(fn_abi) output_types = get_abi_output_types(fn_abi) decoded_input = web3.codec.decode_abi(input_types, bytes.fromhex(data[10:])) # TODO: This here always returns empty response. If/when we want to # mock it for etherscan, this is where we do it args = [] result = '0x' + web3.codec.encode_abi(output_types, [args]).hex() response = f'{{"jsonrpc":"2.0","id":1,"result":"{result}"}}' else: raise AssertionError(f'Unexpected etherscan call during tests: {url}') elif 'api.etherscan.io/api?module=proxy&action=eth_call&to=0xB6456b57f03352bE48Bf101B46c1752a0813491a' in url: # noqa: E501 # ADEX Staking contract if 'adex_staking' in original_queries: return original_requests_get(url, *args, **kwargs) if 'data=0x447b15f4' in url: # a mocked share value response = '{"jsonrpc":"2.0","id":1,"result":"0x0000000000000000000000000000000000000000000000000fc4a48782d85b51"}' # noqa: E501 else: raise AssertionError(f'Unknown call to Adex Staking pool during tests: {url}') elif f'api.etherscan.io/api?module=proxy&action=eth_call&to={ETH_MULTICALL.address}' in url: # noqa: E501 web3 = Web3() contract = web3.eth.contract(address=ETH_MULTICALL.address, abi=ETH_MULTICALL.abi) if 'b6456b57f03352be48bf101b46c1752a0813491a' in url: multicall_purpose = 'adex_staking' elif '5f3b5dfeb7b28cdbd7faba78963ee202a494e2a2' in url: multicall_purpose = 'vecrv' else: raise AssertionError('Unknown multicall in mocked tests') if 'data=0x252dba42' in url: # aggregate if multicall_purpose == 'adex_staking': if 'adex_staking' in original_queries: return original_requests_get(url, *args, **kwargs) if 'mocked_adex_staking_balance' in extra_flags: # mock adex staking balance for a single account response = '{"jsonrpc": "2.0", "id": 1, "result": "0x0000000000000000000000000000000000000000000000000000000000bb45aa000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000152982285d2e4d5aeaa9"}' # noqa: E501 return MockResponse(200, response) data = url.split('data=')[1] if '&apikey' in data: data = data.split('&apikey')[0] fn_abi = contract.functions.abi[1] assert fn_abi['name'] == 'aggregate', 'Abi position of multicall aggregate changed' input_types = get_abi_input_types(fn_abi) output_types = get_abi_output_types(fn_abi) decoded_input = web3.codec.decode_abi(input_types, bytes.fromhex(data[10:])) # For now the only mocked multicall is 32 bytes for multicall balance # of both veCRV and adex staking pool. # When there is more we have to figure out a way to differentiate # between them in mocking. Just return empty response here # all pylint ignores below due to https://github.com/PyCQA/pylint/issues/4114 args = [1, [b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' for x in decoded_input[0]]] # pylint: disable=unsubscriptable-object # noqa: E501 result = '0x' + web3.codec.encode_abi(output_types, args).hex() response = f'{{"jsonrpc":"2.0","id":1,"result":"{result}"}}' else: raise AssertionError('Unexpected etherscan multicall during tests: {url}') elif f'api.etherscan.io/api?module=proxy&action=eth_call&to={ETH_SCAN.address}' in url: if 'ethscan' in original_queries: return original_requests_get(url, *args, **kwargs) web3 = Web3() contract = web3.eth.contract(address=ETH_SCAN.address, abi=ETH_SCAN.abi) if 'data=0xdbdbb51b' in url: # Eth balance query data = url.split('data=')[1] if '&apikey' in data: data = data.split('&apikey')[0] fn_abi = contract._find_matching_fn_abi( fn_identifier='etherBalances', args=[list(eth_map.keys())], ) input_types = get_abi_input_types(fn_abi) output_types = get_abi_output_types(fn_abi) decoded_input = web3.codec.decode_abi(input_types, bytes.fromhex(data[10:])) args = [] for account_address in decoded_input[0]: # pylint: disable=unsubscriptable-object # noqa: E501 account_address = deserialize_ethereum_address(account_address) args.append(int(eth_map[account_address]['ETH'])) result = '0x' + web3.codec.encode_abi(output_types, [args]).hex() response = f'{{"jsonrpc":"2.0","id":1,"result":"{result}"}}' elif 'data=0x06187b4f' in url: # Multi token multiaddress balance query data = url.split('data=')[1] if '&apikey' in data: data = data.split('&apikey')[0] # not really the given args, but we just want the fn abi args = [list(eth_map.keys()), list(eth_map.keys())] fn_abi = contract._find_matching_fn_abi( fn_identifier='tokensBalances', args=args, ) input_types = get_abi_input_types(fn_abi) output_types = get_abi_output_types(fn_abi) decoded_input = web3.codec.decode_abi(input_types, bytes.fromhex(data[10:])) args = [] for account_address in decoded_input[0]: # pylint: disable=unsubscriptable-object # noqa: E501 account_address = deserialize_ethereum_address(account_address) x = [] for token_address in decoded_input[1]: # pylint: disable=unsubscriptable-object # noqa: E501 token_address = deserialize_ethereum_address(token_address) value_to_add = 0 for given_asset, value in eth_map[account_address].items(): given_asset = _get_token(given_asset) if given_asset is None: # not a token continue if token_address != given_asset.ethereum_address: continue value_to_add = int(value) break x.append(value_to_add) args.append(x) result = '0x' + web3.codec.encode_abi(output_types, [args]).hex() response = f'{{"jsonrpc":"2.0","id":1,"result":"{result}"}}' elif 'data=0xe5da1b68' in url: # Multi token balance query data = url.split('data=')[1] if '&apikey' in data: data = data.split('&apikey')[0] # not really the given args, but we just want the fn abi args = ['str', list(eth_map.keys())] fn_abi = contract._find_matching_fn_abi( fn_identifier='tokensBalance', args=args, ) input_types = get_abi_input_types(fn_abi) output_types = get_abi_output_types(fn_abi) decoded_input = web3.codec.decode_abi(input_types, bytes.fromhex(data[10:])) args = [] account_address = deserialize_ethereum_address(decoded_input[0]) # pylint: disable=unsubscriptable-object # noqa: E501 x = [] for token_address in decoded_input[1]: # pylint: disable=unsubscriptable-object # noqa: E501 token_address = deserialize_ethereum_address(token_address) value_to_add = 0 for given_asset, value in eth_map[account_address].items(): given_asset = _get_token(given_asset) if given_asset is None: # not a token continue if token_address != given_asset.ethereum_address: continue value_to_add = int(value) break args.append(value_to_add) result = '0x' + web3.codec.encode_abi(output_types, [args]).hex() response = f'{{"jsonrpc":"2.0","id":1,"result":"{result}"}}' else: raise AssertionError(f'Unexpected etherscan call during tests: {url}') else: return original_requests_get(url, *args, **kwargs) return MockResponse(200, response)
def mock_requests_get(url, *args, **kwargs): if 'etherscan.io/api?module=account&action=balance&address' in url: addr = url[67:109] value = eth_map[addr].get('ETH', '0') response = f'{{"status":"1","message":"OK","result":{value}}}' elif 'etherscan.io/api?module=account&action=balancemulti' in url: queried_accounts = [] length = 72 # process url and get the accounts while True: if len(url) < length: break potential_address = url[length:length + 42] if 'apikey=' in potential_address: break queried_accounts.append(potential_address) length += 43 accounts = [] for addr in queried_accounts: value = eth_map[addr].get('ETH', '0') accounts.append({ 'account': addr, 'balance': eth_map[addr]['ETH'] }) response = f'{{"status":"1","message":"OK","result":{json.dumps(accounts)}}}' elif 'api.etherscan.io/api?module=account&action=tokenbalance' in url: token_address = url[80:122] msg = 'token address missing from test mapping' assert token_address in CONTRACT_ADDRESS_TO_TOKEN, msg response = '{"status":"1","message":"OK","result":"0"}' token = CONTRACT_ADDRESS_TO_TOKEN[token_address] account = url[131:173] value = eth_map[account].get(token.identifier, 0) response = f'{{"status":"1","message":"OK","result":"{value}"}}' elif f'api.etherscan.io/api?module=proxy&action=eth_call&to={ETH_SCAN_ADDRESS}' in url: web3 = Web3() contract = web3.eth.contract(address=ETH_SCAN_ADDRESS, abi=ETH_SCAN_ABI) if 'data=0xdbdbb51b' in url: # Eth balance query data = url.split('data=')[1] if '&apikey' in data: data = data.split('&apikey')[0] fn_abi = contract._find_matching_fn_abi( fn_identifier='etherBalances', args=[list(eth_map.keys())], ) input_types = get_abi_input_types(fn_abi) output_types = get_abi_output_types(fn_abi) decoded_input = web3.codec.decode_abi(input_types, bytes.fromhex(data[10:])) args = [] for account_address in decoded_input[0]: account_address = to_checksum_address(account_address) args.append(int(eth_map[account_address]['ETH'])) result = '0x' + web3.codec.encode_abi(output_types, [args]).hex() response = f'{{"jsonrpc":"2.0","id":1,"result":"{result}"}}' elif 'data=0x06187b4f' in url: # Multi token balance query data = url.split('data=')[1] if '&apikey' in data: data = data.split('&apikey')[0] # not really the given args, but we just want the fn abi args = [list(eth_map.keys()), list(eth_map.keys())] fn_abi = contract._find_matching_fn_abi( fn_identifier='tokensBalances', args=args, ) input_types = get_abi_input_types(fn_abi) output_types = get_abi_output_types(fn_abi) decoded_input = web3.codec.decode_abi(input_types, bytes.fromhex(data[10:])) args = [] for account_address in decoded_input[0]: account_address = to_checksum_address(account_address) x = [] for token_address in decoded_input[1]: token_address = to_checksum_address(token_address) value_to_add = 0 for given_asset, value in eth_map[ account_address].items(): if not isinstance(given_asset, EthereumToken): continue if token_address != given_asset.ethereum_address: continue value_to_add = int(value) break x.append(value_to_add) args.append(x) result = '0x' + web3.codec.encode_abi(output_types, [args]).hex() response = f'{{"jsonrpc":"2.0","id":1,"result":"{result}"}}' else: raise AssertionError( f'Unexpected etherscan call during tests: {url}') else: return original_requests_get(url, *args, **kwargs) return MockResponse(200, response)
def parse_args_to_matching_types_for_function(args, function_abi): types = get_abi_input_types(function_abi) return [parse_arg_to_matching_type(arg, type) for arg, type in zip(args, types)]
def mock_requests_get(url, *args, **kwargs): if 'etherscan.io/api?module=account&action=balance&address' in url: addr = url[67:109] value = eth_map[addr].get('ETH', '0') response = f'{{"status":"1","message":"OK","result":{value}}}' elif 'etherscan.io/api?module=account&action=balancemulti' in url: queried_accounts = [] length = 72 # process url and get the accounts while True: if len(url) < length: break potential_address = url[length:length + 42] if 'apikey=' in potential_address: break queried_accounts.append(potential_address) length += 43 accounts = [] for addr in queried_accounts: value = eth_map[addr].get('ETH', '0') accounts.append({ 'account': addr, 'balance': eth_map[addr]['ETH'] }) response = f'{{"status":"1","message":"OK","result":{json.dumps(accounts)}}}' elif 'api.etherscan.io/api?module=account&action=tokenbalance' in url: token_address = url[80:122] msg = 'token address missing from test mapping' assert token_address in CONTRACT_ADDRESS_TO_TOKEN, msg response = '{"status":"1","message":"OK","result":"0"}' token = CONTRACT_ADDRESS_TO_TOKEN[token_address] account = url[131:173] value = eth_map[account].get(token.identifier, 0) response = f'{{"status":"1","message":"OK","result":"{value}"}}' elif 'api.etherscan.io/api?module=account&action=txlistinternal&' in url: # By default when mocking, don't query for transactions response = '{"status":"1","message":"OK","result":[]}' elif 'api.etherscan.io/api?module=account&action=txlist&' in url: # By default when mocking, don't query for transactions response = '{"status":"1","message":"OK","result":[]}' elif 'api.etherscan.io/api?module=logs&action=getLogs&' in url: if 'logs' in original_queries: return original_requests_get(url, *args, **kwargs) # By default when mocking, don't query logs response = '{"status":"1","message":"OK","result":[]}' elif 'api.etherscan.io/api?module=block&action=getblocknobytime&' in url: if 'blocknobytime' in original_queries: return original_requests_get(url, *args, **kwargs) # By default when mocking don't query blocknobytime response = '{"status":"1","message":"OK","result":"1"}' elif f'api.etherscan.io/api?module=proxy&action=eth_call&to={ZERION_ADAPTER_ADDRESS}' in url: # noqa: E501 if 'zerion' in original_queries: return original_requests_get(url, *args, **kwargs) web3 = Web3() contract = web3.eth.contract(address=ZERION_ADAPTER_ADDRESS, abi=ZERION_ABI) if 'data=0xc84aae17' in url: # getBalances data = url.split('data=')[1] if '&apikey' in data: data = data.split('&apikey')[0] fn_abi = contract._find_matching_fn_abi( fn_identifier='getBalances', args=['address'], ) input_types = get_abi_input_types(fn_abi) output_types = get_abi_output_types(fn_abi) decoded_input = web3.codec.decode_abi(input_types, bytes.fromhex(data[10:])) # TODO: This here always returns empty response. If/when we want to # mock it for etherscan, this is where we do it args = [] result = '0x' + web3.codec.encode_abi(output_types, [args]).hex() response = f'{{"jsonrpc":"2.0","id":1,"result":"{result}"}}' else: raise AssertionError( f'Unexpected etherscan call during tests: {url}') elif f'api.etherscan.io/api?module=proxy&action=eth_call&to={ETH_SCAN.address}' in url: if 'ethscan' in original_queries: return original_requests_get(url, *args, **kwargs) web3 = Web3() contract = web3.eth.contract(address=ETH_SCAN.address, abi=ETH_SCAN.abi) if 'data=0xdbdbb51b' in url: # Eth balance query data = url.split('data=')[1] if '&apikey' in data: data = data.split('&apikey')[0] fn_abi = contract._find_matching_fn_abi( fn_identifier='etherBalances', args=[list(eth_map.keys())], ) input_types = get_abi_input_types(fn_abi) output_types = get_abi_output_types(fn_abi) decoded_input = web3.codec.decode_abi(input_types, bytes.fromhex(data[10:])) args = [] for account_address in decoded_input[0]: account_address = to_checksum_address(account_address) args.append(int(eth_map[account_address]['ETH'])) result = '0x' + web3.codec.encode_abi(output_types, [args]).hex() response = f'{{"jsonrpc":"2.0","id":1,"result":"{result}"}}' elif 'data=0x06187b4f' in url: # Multi token multiaddress balance query data = url.split('data=')[1] if '&apikey' in data: data = data.split('&apikey')[0] # not really the given args, but we just want the fn abi args = [list(eth_map.keys()), list(eth_map.keys())] fn_abi = contract._find_matching_fn_abi( fn_identifier='tokensBalances', args=args, ) input_types = get_abi_input_types(fn_abi) output_types = get_abi_output_types(fn_abi) decoded_input = web3.codec.decode_abi(input_types, bytes.fromhex(data[10:])) args = [] for account_address in decoded_input[0]: account_address = to_checksum_address(account_address) x = [] for token_address in decoded_input[1]: token_address = to_checksum_address(token_address) value_to_add = 0 for given_asset, value in eth_map[ account_address].items(): given_asset = _get_token(given_asset) if given_asset is None: # not a token continue if token_address != given_asset.ethereum_address: continue value_to_add = int(value) break x.append(value_to_add) args.append(x) result = '0x' + web3.codec.encode_abi(output_types, [args]).hex() response = f'{{"jsonrpc":"2.0","id":1,"result":"{result}"}}' elif 'data=0xe5da1b68' in url: # Multi token balance query data = url.split('data=')[1] if '&apikey' in data: data = data.split('&apikey')[0] # not really the given args, but we just want the fn abi args = ['str', list(eth_map.keys())] fn_abi = contract._find_matching_fn_abi( fn_identifier='tokensBalance', args=args, ) input_types = get_abi_input_types(fn_abi) output_types = get_abi_output_types(fn_abi) decoded_input = web3.codec.decode_abi(input_types, bytes.fromhex(data[10:])) args = [] account_address = to_checksum_address(decoded_input[0]) x = [] for token_address in decoded_input[1]: token_address = to_checksum_address(token_address) value_to_add = 0 for given_asset, value in eth_map[account_address].items(): given_asset = _get_token(given_asset) if given_asset is None: # not a token continue if token_address != given_asset.ethereum_address: continue value_to_add = int(value) break args.append(value_to_add) result = '0x' + web3.codec.encode_abi(output_types, [args]).hex() response = f'{{"jsonrpc":"2.0","id":1,"result":"{result}"}}' else: raise AssertionError( f'Unexpected etherscan call during tests: {url}') else: return original_requests_get(url, *args, **kwargs) return MockResponse(200, response)