Skip to content

Commit 4978250

Browse files
authored
Merge pull request #38 from pattern-tech/feat/adding-moralis
feat[api]: adding moralis agent
2 parents 17afc42 + d1b0f6c commit 4978250

File tree

3 files changed

+291
-61
lines changed

3 files changed

+291
-61
lines changed

api/src/agentflow/agents/hub.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from src.agentflow.agents import ether_scan_agent, goldrush_agent
1+
from src.agentflow.agents import ether_scan_agent, goldrush_agent, moralis_agent
22
from typing import Any, List
33

44

@@ -17,7 +17,8 @@ class AgentHub:
1717
def __init__(self):
1818
self.agents = {
1919
"etherscan": ether_scan_agent.etherscan_agent,
20-
"goldrush": goldrush_agent.goldrush_agent
20+
"goldrush": goldrush_agent.goldrush_agent,
21+
"moralis": moralis_agent.moralis_agent
2122
}
2223

2324
def get_agents(self, agent_names: List[str]) -> List[Any]:
+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
from langchain.tools import tool
2+
from langchain.agents import AgentExecutor
3+
4+
from src.util.configuration import Config
5+
from src.agentflow.utils.enum import AgentType
6+
from src.agentflow.utils.tools_index import get_all_tools
7+
from src.agentflow.utils.shared_tools import handle_exceptions
8+
from src.agentflow.utils.shared_tools import init_llm, init_agent, init_prompt
9+
10+
11+
@tool
12+
@handle_exceptions
13+
def moralis_agent(query: str):
14+
"""
15+
An agent for handling Ethereum blockchain-related queries and tasks.
16+
This agent can perform the following tasks:
17+
18+
- Get active chains for a wallet address across all chains
19+
- Get token balances for a specific wallet address and their token prices in USD. (paginated)
20+
- Get the stats for a wallet address.
21+
- Retrieve the full transaction history of a specified wallet address, including sends, receives, token and NFT transfers and contract interactions.
22+
- Get the contents of a transaction by the given transaction hash.
23+
- Get ERC20 approvals for one or many wallet addresses and/or contract addresses, ordered by block number in descending order.
24+
25+
Args:
26+
query (str): query about Ethereum blockchain tasks.
27+
28+
Returns:
29+
str: Response containing the requested Ethereum blockchain information
30+
"""
31+
config = Config.get_config()
32+
33+
llm = init_llm(service=config["llm"]["provider"],
34+
model_name=config["llm"]["model"],
35+
api_key=config["llm"]["api_key"],
36+
stream=False)
37+
38+
tools = get_all_tools(tools_path="moralis_tools")
39+
40+
prompt = init_prompt(llm, AgentType.BLOCKCHAIN_AGENT)
41+
42+
agent = init_agent(llm, tools, prompt)
43+
44+
agent_executor = AgentExecutor(
45+
agent=agent,
46+
tools=tools,
47+
return_intermediate_steps=True,
48+
verbose=True)
49+
50+
response = agent_executor.invoke({"input": query})
51+
52+
try:
53+
agent_steps = []
54+
for step in response["intermediate_steps"]:
55+
agent_steps.append({
56+
"function_name": step[0].tool,
57+
"function_args": step[0].tool_input,
58+
"function_output": step[-1]
59+
})
60+
return {"agent_steps": agent_steps}
61+
except:
62+
return "no tools called inside agent"
+226-59
Original file line numberDiff line numberDiff line change
@@ -1,90 +1,257 @@
1-
import os
1+
import requests
22

3-
from web3 import Web3
3+
from typing import Any
44
from moralis import evm_api
55
from langchain.tools import tool
6-
from typing import List, Dict, Any
76

7+
from src.util.configuration import Config
88
from src.agentflow.utils.shared_tools import handle_exceptions
99

10+
_config = Config.get_config()
11+
_moralis_config = Config.get_service_config(_config, "moralis")
1012

11-
def _get_api_key() -> str:
13+
14+
@tool
15+
@handle_exceptions
16+
def get_wallet_active_chains(wallet_address: str, output_include: list[str]) -> list[dict[str, Any]]:
17+
"""
18+
Get active chains for a wallet address across all chains
19+
20+
Args:
21+
wallet_address (str): Ethereum wallet address
22+
output_include (list[str]):
23+
A list of field names to include in in the output.
24+
25+
26+
Returns:
27+
List[dict[str, Any]]:
28+
A list of dictionaries where each dictionary only contains the keys
29+
listed in `output_include` (if they exist in the source data).
30+
Possible fields include:
31+
32+
- chain, chain_id, first_transaction, last_transaction
33+
"""
34+
params = {
35+
"address": wallet_address
36+
}
37+
38+
result = evm_api.wallets.get_wallet_active_chains(
39+
api_key=_moralis_config["api_key"],
40+
params=params,
41+
)
42+
43+
results = result["active_chains"]
44+
final_results = []
45+
for result in results:
46+
final_results.append({item: result[item]
47+
for item in result.keys() if item in output_include})
48+
return final_results
49+
50+
51+
@tool
52+
@handle_exceptions
53+
def get_wallet_token_balances(wallet_address: str, output_include: list[str], cursor: str = None) -> dict:
54+
"""
55+
Get token balances for a specific wallet address and their token prices in USD. (paginated)
56+
57+
Args:
58+
wallet_address (str): Ethereum wallet address
59+
output_include (list[str]): A list of field names to include in the output.
60+
cursor (str): The cursor returned in the previous response (used for getting the next page). end of page cursor is None
61+
62+
63+
Returns:
64+
List[dict[str, Any]]:
65+
A list of dictionaries where each dictionary only contains the keys
66+
listed in `output_include` (if they exist in the source data).
67+
Possible fields include:
68+
69+
- token_address, symbol, name, logo, thumbnail, decimals, balance, balance_formatted,
70+
usd_price, usd_price_24hr_percent_change, usd_price_24hr_usd_change, usd_value,
71+
usd_value_24hr_usd_change, native_token, portfolio_percentage
72+
"""
73+
params = {
74+
"chain": "eth",
75+
"address": wallet_address
76+
}
77+
78+
if cursor:
79+
params["cursor"] = cursor
80+
81+
api_result = evm_api.wallets.get_wallet_token_balances_price(
82+
api_key=_moralis_config["api_key"],
83+
params=params,
84+
)
85+
86+
results = api_result["result"]
87+
final_results = []
88+
for result in results:
89+
final_results.append({item: result[item]
90+
for item in result.keys() if item in output_include})
91+
92+
return {"cursor": api_result["cursor"],
93+
"results": final_results}
94+
95+
96+
@tool
97+
@handle_exceptions
98+
def get_wallet_stats(wallet_address: str, output_include: list[str]) -> dict:
1299
"""
13-
Retrieve the Moralis API key from the environment.
100+
Get the stats for a wallet address.
101+
102+
Args:
103+
wallet_address (str): Ethereum wallet address
14104
15105
Returns:
16-
str: The Moralis API key.
106+
List[dict[str, Any]]:
107+
A list of dictionaries where each dictionary only contains the keys
108+
listed in `output_include` (if they exist in the source data).
109+
Possible fields include:
17110
18-
Raises:
19-
Exception: If the API key is not found.
111+
- nfts, collections, transactions, nft_transfers, token_transfers
20112
"""
21-
api_key = os.getenv("MORALIS_API_KEY")
22-
if not api_key:
23-
raise Exception("No API key found for Moralis.")
24-
return api_key
113+
params = {
114+
"chain": "eth",
115+
"address": wallet_address
116+
}
117+
118+
result = evm_api.wallets.get_wallet_stats(
119+
api_key=_moralis_config["api_key"],
120+
params=params,
121+
)
122+
123+
return {item: result[item]
124+
for item in result.keys() if item in output_include}
25125

26126

27127
@tool
28128
@handle_exceptions
29-
def get_contract_transactions(contract_address: str) -> List[Dict[str, Any]]:
129+
def get_wallet_history(wallet_address: str, output_include: list[str], cursor: str = None) -> dict:
30130
"""
31-
Fetch the latest transactions for a specified contract address using the Moralis API
32-
and decode their function inputs.
131+
Retrieve the full transaction history of a specified wallet address, including sends, receives, token and NFT transfers
132+
and contract interactions. (paginated & in descending order)
33133
34134
Args:
35-
contract_address (str): The contract address to fetch transactions for.
135+
wallet_address (str): Ethereum wallet address
136+
output_include (list[str]): A list of field names to include in the output.
137+
cursor (str): The cursor returned in the previous response (used for getting the next page). end of page cursor is None
36138
37139
Returns:
38-
List[Dict[str, Any]]: A list of decoded transaction details, limited to 20 entries.
140+
dict[str, Any]:
141+
A dictionary where each key-value pair only contains the keys
142+
listed in `output_include` (if they exist in the source data).
143+
Possible fields include:
144+
145+
- hash, from_address_entity, from_address_entity_logo, from_address,
146+
from_address_label, to_address_entity, to_address_entity_logo, to_address, to_address_label,
147+
value, receipt_contract_address, block_timestamp, block_number, block_hash, internal_transactions,
148+
nft_transfers, erc20_transfer, native_transfers
149+
"""
150+
params = {
151+
"chain": "eth",
152+
"order": "DESC",
153+
"address": wallet_address
154+
}
155+
156+
if cursor:
157+
params["cursor"] = cursor
158+
159+
api_result = evm_api.wallets.get_wallet_history(
160+
api_key=_moralis_config["api_key"],
161+
params=params,
162+
)
163+
164+
final_results = []
165+
for result in api_result["result"]:
166+
final_results.append({item: result[item]
167+
for item in result.keys() if item in output_include})
168+
169+
return {"cursor": api_result["cursor"],
170+
"results": final_results}
171+
172+
173+
@tool
174+
@handle_exceptions
175+
def get_transaction_detail(transaction_hash: str, output_include: list[str]) -> dict:
39176
"""
40-
# Retrieve API key
41-
api_key = _get_api_key()
177+
Get the contents of a transaction by the given transaction hash.
178+
179+
Args:
180+
transaction_hash (str): transaction hash to be decoded
181+
output_include (list[str]): A list of field names to include in the output.
182+
183+
Returns:
184+
dict[str, Any]:
185+
A dictionary where each key-value pair only contains the keys
186+
listed in `output_include` (if they exist in the source data).
187+
Possible fields include:
188+
189+
- hash, from_address_entity, from_address_entity_logo, from_address,
190+
from_address_label, to_address_entity, to_address_entity_logo, to_address, to_address_label,
191+
value, receipt_gas_used, receipt_contract_address, receipt_root, receipt_status, block_timestamp,
192+
block_number, block_hash, decoded_call, decoded_event
193+
42194
43-
# Set up parameters for the Moralis API call
195+
"""
44196
params = {
45-
"address": contract_address,
46-
"chain": "eth"
197+
"chain": "eth",
198+
"transaction_hash": transaction_hash
47199
}
48200

49-
# Call the Moralis API to get wallet transactions
50-
results = evm_api.transaction.get_wallet_transactions(
51-
api_key=api_key,
201+
result = evm_api.transaction.get_transaction_verbose(
202+
api_key=_moralis_config["api_key"],
52203
params=params,
53204
)
54205

55-
# Set up a Web3 provider
56-
eth_rpc = os.getenv("ETH_RPC")
57-
if not eth_rpc:
58-
raise Exception("ETH_RPC environment variable is not set.")
59-
w3 = Web3(Web3.HTTPProvider(eth_rpc))
60-
61-
# Retrieve the contract ABI.
62-
# NOTE: Adjust the import path for get_contract_abi as needed.
63-
from src.agent.tools.some_module import get_contract_abi
64-
contract_abi = get_contract_abi(contract_address)
65-
66-
contract = w3.eth.contract(address=contract_address, abi=contract_abi)
67-
68-
decoded_transactions: List[Dict[str, Any]] = []
69-
for tx in results.get('result', []):
70-
try:
71-
# Attempt to decode the function input.
72-
# If the input is empty or invalid, an exception may be raised.
73-
decoded_input = contract.decode_function_input(
74-
tx.get('input', '0x'))
75-
function_name = decoded_input[0].fn_name if decoded_input else None
76-
except Exception:
77-
function_name = None
78-
79-
decoded_transactions.append({
80-
'transaction_hash': tx.get('hash'),
81-
'block_number': tx.get('block_number'),
82-
'from': tx.get('from_address'),
83-
'to': tx.get('to_address'),
84-
'value': tx.get('value'),
85-
'function_name': function_name,
86-
})
87-
88-
return decoded_transactions[:20]
206+
return {item: result[item]
207+
for item in result.keys() if item in output_include}
208+
209+
210+
@tool
211+
@handle_exceptions
212+
def get_token_approvals(wallet_address: str, output_include: list[str], cursor: str = None) -> dict:
213+
"""
214+
Get ERC20 approvals for one or many wallet addresses and/or contract addresses, ordered by block number in descending order.
215+
216+
Args:
217+
wallet_address (str): Ethereum wallet address
218+
output_include (list[str]): A list of field names to include in the output.
219+
cursor (str): The cursor returned in the previous response (used for getting the next page). end of page cursor is None
220+
221+
Returns:
222+
List[dict[str, Any]]:
223+
A list of dictionaries where each dictionary only contains the keys
224+
listed in `output_include` (if they exist in the source data).
225+
Possible fields include:
226+
227+
- block_number, block_timestamp, transaction_hash, value, value_formatted, token, spender
228+
"""
229+
base_url = _moralis_config["url"]
230+
api_url = f"{base_url}/wallets/{wallet_address}/approvals"
231+
232+
params = {'chain': 'eth'}
233+
234+
if cursor:
235+
params["cursor"] = cursor
236+
237+
headers = {
238+
'accept': 'application/json',
239+
'X-API-Key': _moralis_config["api_key"]
240+
}
241+
242+
response = requests.get(api_url, headers=headers, params=params)
243+
244+
if response.status_code != 200:
245+
raise Exception(
246+
f"Failed to get token approvals. Status code: {response.status_code}")
247+
248+
api_result = response.json()
89249

250+
results = api_result["result"]
251+
final_results = []
252+
for result in results:
253+
final_results.append({item: result[item]
254+
for item in result.keys() if item in output_include})
90255

256+
return {"cursor": api_result["cursor"],
257+
"results": final_results}

0 commit comments

Comments
 (0)