Skip to content

Commit b8a4b7e

Browse files
authored
Merge pull request #62 from pattern-tech/feat/tools-manipulation
feat: method added for decoding trx input
2 parents d59cc71 + c27f981 commit b8a4b7e

4 files changed

Lines changed: 305 additions & 5 deletions

File tree

src/agentflow/agents/ether_scan_agent.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,9 @@ def etherscan_agent(query: str):
2424
- Retrieve the ABI of a smart contract
2525
- Retrieve the ABI of a specific event from a smart contract
2626
- Fetch events for a given smart contract event within a block range
27-
- Retrieve the latest Ethereum block number
27+
- Retrieve the latest Ethereum block number and hash
2828
- Convert a Unix timestamp to the nearest Ethereum block number
29+
- Decode the input data of an Ethereum transaction
2930
3031
Args:
3132
query (str): query about Ethereum blockchain tasks.
@@ -63,6 +64,9 @@ def etherscan_agent(query: str):
6364
"function_args": step[0].tool_input,
6465
"function_output": step[-1]
6566
})
66-
return {"agent_steps": agent_steps}
67+
if agent_steps:
68+
return {"agent_steps": agent_steps}
69+
else:
70+
return {"agent_answer": response["output"]}
6771
except:
6872
return "no tools called inside agent"

src/agentflow/agents/goldrush_agent.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ def goldrush_agent(query: str):
5656
"function_args": step[0].tool_input,
5757
"function_output": step[-1]
5858
})
59-
return {"agent_steps": agent_steps}
59+
if agent_steps:
60+
return {"agent_steps": agent_steps}
61+
else:
62+
return {"agent_answer": response["output"]}
6063
except:
6164
return "no tools called inside agent"

src/agentflow/agents/moralis_agent.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ def moralis_agent(query: str):
5757
"function_args": step[0].tool_input,
5858
"function_output": step[-1]
5959
})
60-
return {"agent_steps": agent_steps}
60+
if agent_steps:
61+
return {"agent_steps": agent_steps}
62+
else:
63+
return {"agent_answer": response["output"]}
6164
except:
6265
return "no tools called inside agent"

src/agentflow/providers/ether_scan_tools.py

Lines changed: 291 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)