@@ -69,7 +69,7 @@ def fetch_contract_source_code(contract_address: str, api_key: str) -> str:
6969 "apikey" : api_key
7070 }
7171 response = requests .get (url , params = params )
72- return response .json ()["result" ][0 ][ "SourceCode" ]
72+ return response .json ()["result" ][0 ]
7373
7474
7575@handle_exceptions
@@ -295,3 +295,293 @@ def convert_timestamp_to_block_number(timestamp: int) -> int:
295295 """
296296 api_key = _ether_scan_config ["api_key" ]
297297 return timestamp_to_block_number (timestamp , api_key )
298+
299+
300+ @tool
301+ @handle_exceptions
302+ def get_latest_eth_block_hash () -> str :
303+ """
304+ Retrieve the hash of the latest Ethereum block.
305+
306+ Returns:
307+ str: The hash of the latest block on the Ethereum mainnet.
308+ """
309+ web3 = Web3 (Web3 .HTTPProvider (_ETH_RPC ))
310+ latest_block = web3 .eth .get_block ('latest' )
311+ return web3 .to_hex (latest_block .hash )
312+
313+
314+ @tool
315+ @handle_exceptions
316+ def get_block_transactions (block_number : int , output_include : List [str ]) -> List [Dict [str , Any ]]:
317+ """
318+ Retrieve all transactions in a specific Ethereum block.
319+
320+ Args:
321+ block_number (int): The block number to retrieve transactions from.
322+
323+ Returns:
324+ List[dict[str, Any]]:
325+ A list of dictionaries where each dictionary only contains the keys
326+ listed in `output_include` (if they exist in the source data).
327+ Possible fields include:
328+
329+ - blockHash, blockNumber, from, gas, gasPrice, maxPriorityFeePerGas, maxFeePerGas,
330+ hash, input, nonce, to, transactionIndex, value, type, accessList, chainId, v, yParity, r, s
331+ """
332+ web3 = Web3 (Web3 .HTTPProvider (_ETH_RPC ))
333+
334+ # Validate block number
335+ latest_block = web3 .eth .block_number
336+ if block_number < 0 or block_number >= latest_block :
337+ raise ValueError (f"Block number must be between 0 and { latest_block } " )
338+
339+ # Get the block with all transactions
340+ block = web3 .eth .get_block (block_number , full_transactions = True )
341+
342+ # Process transactions to make them JSON serializable
343+ transactions = []
344+ for tx in block .transactions :
345+ # Convert transaction to dictionary
346+ if isinstance (tx , dict ):
347+ tx_dict = tx
348+ else :
349+ tx_dict = dict (tx )
350+
351+ # Convert non-serializable objects to strings
352+ for key , value in tx_dict .items ():
353+ if isinstance (value , bytes ):
354+ tx_dict [key ] = web3 .to_hex (value )
355+ elif isinstance (value , int ) and key == 'value' :
356+ # Convert Wei to Ether for readability
357+ tx_dict [key ] = web3 .from_wei (value , 'ether' )
358+
359+ transactions .append (tx_dict )
360+
361+ final_results = []
362+ for result in transactions :
363+ final_results .append ({item : result [item ]
364+ for item in result .keys () if item in output_include })
365+ return final_results
366+
367+
368+ @tool
369+ @handle_exceptions
370+ def decode_transaction_input (transaction_input : str , contract_address : str ) -> Dict [str , Any ]:
371+ """
372+ Decode the input data of an Ethereum transaction using the ABI of the contract.
373+
374+ Args:
375+ transaction_input (str): The input data of the transaction (hex string starting with '0x')
376+ contract_address (str): The address of the contract that was called in the transaction
377+
378+ Returns:
379+ Dict[str, Any]: A dictionary containing the decoded transaction input with the following fields:
380+ - function_name: The name of the function that was called
381+ - function_signature: The signature of the function (e.g., 'transfer(address,uint256)')
382+ - parameters: A list of dictionaries, each containing:
383+ - name: The parameter name
384+ - type: The parameter type
385+ - value: The parameter value (decoded)
386+
387+ Raises:
388+ Exception: If the input data cannot be decoded or the contract ABI cannot be retrieved
389+ """
390+ # Validate input
391+ if not transaction_input .startswith ('0x' ):
392+ raise ValueError ("Transaction input must start with '0x'" )
393+
394+ if len (transaction_input ) < 10 : # '0x' + 8 chars (4 bytes)
395+ return {
396+ "error" : "Transaction input too short to contain a function selector" ,
397+ "raw_input" : transaction_input
398+ }
399+
400+ # Get the function selector (first 4 bytes/8 hex chars after '0x')
401+ function_selector = transaction_input [:10 ] # includes '0x'
402+
403+ try :
404+ # Get the contract ABI
405+ abi = get_contract_abi (contract_address )
406+
407+ # Initialize Web3
408+ web3 = Web3 (Web3 .HTTPProvider (_ETH_RPC ))
409+ contract = web3 .eth .contract (address = contract_address , abi = abi )
410+
411+ # Try direct decoding first using web3.py's built-in functionality
412+ try :
413+ function_obj , decoded_params = contract .decode_function_input (
414+ transaction_input )
415+
416+ # If we get here, decoding succeeded
417+ function_name = function_obj .fn_name
418+ function_inputs = [param for param in function_obj .abi ['inputs' ]]
419+
420+ # Create the function signature string
421+ input_types = [input .get ('type' ) for input in function_inputs ]
422+ function_signature_str = f"{ function_name } ({ ',' .join (input_types )} )"
423+
424+ # Format the parameters for output
425+ parameters = []
426+ for param in function_inputs :
427+ param_name = param .get ('name' )
428+ param_type = param .get ('type' )
429+ param_value = decoded_params .get (param_name )
430+
431+ # Convert bytes and addresses to readable format
432+ if isinstance (param_value , bytes ):
433+ param_value = web3 .to_hex (param_value )
434+ elif param_type .startswith ('address' ) and isinstance (param_value , str ):
435+ param_value = param_value .lower () # Normalize address
436+ elif param_type .startswith ('uint' ) or param_type .startswith ('int' ):
437+ # Keep as int but convert large numbers to string to avoid JSON serialization issues
438+ if isinstance (param_value , int ) and (param_value > 9007199254740991 or param_value < - 9007199254740991 ):
439+ param_value = str (param_value )
440+
441+ parameters .append ({
442+ "name" : param_name ,
443+ "type" : param_type ,
444+ "value" : param_value
445+ })
446+
447+ return {
448+ "function_name" : function_name ,
449+ "function_signature" : function_signature_str ,
450+ "parameters" : parameters
451+ }
452+
453+ except Exception as decode_error :
454+ # If direct decoding fails, fall back to manual method
455+ # Find the matching function in the ABI
456+ function = None
457+ all_selectors = []
458+
459+ for item in abi :
460+ if item .get ('type' ) == 'function' :
461+ # Calculate the function selector
462+ fn_name = item .get ('name' , '' )
463+ input_types = [input .get ('type' )
464+ for input in item .get ('inputs' , [])]
465+ fn_signature = f"{ fn_name } ({ ',' .join (input_types )} )"
466+
467+ # Calculate selector correctly - only first 4 bytes (8 hex chars) after '0x'
468+ calculated_selector = '0x' + \
469+ web3 .keccak (text = fn_signature ).hex ()[2 :10 ]
470+ all_selectors .append ((calculated_selector , fn_signature ))
471+
472+ if calculated_selector == function_selector :
473+ function = item
474+ break
475+
476+ if not function :
477+ # Try to get implementation contract if this might be a proxy
478+ try :
479+ # Check for common proxy patterns
480+ # EIP-1967 proxy implementation slot
481+ implementation_slot = '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc'
482+ implementation_address = web3 .eth .get_storage_at (
483+ Web3 .to_checksum_address (contract_address ),
484+ implementation_slot
485+ )
486+
487+ # Convert to address format (last 20 bytes)
488+ if implementation_address and int (implementation_address .hex (), 16 ) != 0 :
489+ implementation_address = '0x' + \
490+ implementation_address .hex ()[- 40 :]
491+
492+ # Try with implementation ABI
493+ implementation_abi = get_contract_abi (
494+ implementation_address )
495+ implementation_contract = web3 .eth .contract (
496+ address = contract_address ,
497+ abi = implementation_abi
498+ )
499+
500+ # Try decoding with implementation contract
501+ function_obj , decoded_params = implementation_contract .decode_function_input (
502+ transaction_input )
503+
504+ # If we get here, decoding succeeded with implementation contract
505+ return {
506+ "function_name" : function_obj .fn_name ,
507+ "function_signature" : f"{ function_obj .fn_name } ({ ',' .join ([i ['type' ] for i in function_obj .abi ['inputs' ]])} )" ,
508+ "parameters" : [
509+ {
510+ "name" : param .get ('name' ),
511+ "type" : param .get ('type' ),
512+ "value" : decoded_params .get (param .get ('name' ))
513+ }
514+ for param in function_obj .abi ['inputs' ]
515+ ],
516+ "note" : "Decoded using implementation contract ABI"
517+ }
518+ except Exception as proxy_error :
519+ # Proxy detection failed, continue with original error
520+ pass
521+
522+ # If we get here, both direct decoding and proxy detection failed
523+ return {
524+ "error" : "Function signature not found in contract ABI" ,
525+ "function_selector" : function_selector ,
526+ # Show first 10 known selectors for debugging
527+ "known_selectors" : all_selectors [:10 ],
528+ "raw_input" : transaction_input ,
529+ "decode_error" : str (decode_error )
530+ }
531+
532+ # If we found the function through manual matching, try to decode
533+ try :
534+ # Decode the function inputs
535+ function_name = function .get ('name' )
536+ function_inputs = function .get ('inputs' , [])
537+
538+ # Create the function signature string
539+ input_types = [input .get ('type' ) for input in function_inputs ]
540+ function_signature_str = f"{ function_name } ({ ',' .join (input_types )} )"
541+
542+ # Try to decode parameters using the contract object
543+ decoded_params = contract .decode_function_input (
544+ transaction_input )
545+
546+ # Format the parameters for output
547+ parameters = []
548+ for param in function_inputs :
549+ param_name = param .get ('name' )
550+ param_type = param .get ('type' )
551+ param_value = decoded_params [1 ].get (param_name )
552+
553+ # Convert bytes and addresses to readable format
554+ if isinstance (param_value , bytes ):
555+ param_value = web3 .to_hex (param_value )
556+ elif param_type .startswith ('address' ) and isinstance (param_value , str ):
557+ param_value = param_value .lower () # Normalize address
558+ elif param_type .startswith ('uint' ) or param_type .startswith ('int' ):
559+ # Keep as int but convert large numbers to string to avoid JSON serialization issues
560+ if isinstance (param_value , int ) and (param_value > 9007199254740991 or param_value < - 9007199254740991 ):
561+ param_value = str (param_value )
562+
563+ parameters .append ({
564+ "name" : param_name ,
565+ "type" : param_type ,
566+ "value" : param_value
567+ })
568+
569+ return {
570+ "function_name" : function_name ,
571+ "function_signature" : function_signature_str ,
572+ "parameters" : parameters
573+ }
574+ except Exception as manual_decode_error :
575+ return {
576+ "error" : f"Found function signature but failed to decode parameters: { str (manual_decode_error )} " ,
577+ "function_name" : function .get ('name' ),
578+ "function_signature" : function_signature_str ,
579+ "raw_input" : transaction_input
580+ }
581+
582+ except Exception as e :
583+ return {
584+ "error" : f"Failed to decode transaction input: { str (e )} " ,
585+ "raw_input" : transaction_input ,
586+ "function_selector" : function_selector
587+ }
0 commit comments