|
1 |
| -import os |
| 1 | +import requests |
2 | 2 |
|
3 |
| -from web3 import Web3 |
| 3 | +from typing import Any |
4 | 4 | from moralis import evm_api
|
5 | 5 | from langchain.tools import tool
|
6 |
| -from typing import List, Dict, Any |
7 | 6 |
|
| 7 | +from src.util.configuration import Config |
8 | 8 | from src.agentflow.utils.shared_tools import handle_exceptions
|
9 | 9 |
|
| 10 | +_config = Config.get_config() |
| 11 | +_moralis_config = Config.get_service_config(_config, "moralis") |
10 | 12 |
|
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: |
12 | 99 | """
|
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 |
14 | 104 |
|
15 | 105 | 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: |
17 | 110 |
|
18 |
| - Raises: |
19 |
| - Exception: If the API key is not found. |
| 111 | + - nfts, collections, transactions, nft_transfers, token_transfers |
20 | 112 | """
|
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} |
25 | 125 |
|
26 | 126 |
|
27 | 127 | @tool
|
28 | 128 | @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: |
30 | 130 | """
|
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) |
33 | 133 |
|
34 | 134 | 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 |
36 | 138 |
|
37 | 139 | 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: |
39 | 176 | """
|
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 | +
|
42 | 194 |
|
43 |
| - # Set up parameters for the Moralis API call |
| 195 | + """ |
44 | 196 | params = {
|
45 |
| - "address": contract_address, |
46 |
| - "chain": "eth" |
| 197 | + "chain": "eth", |
| 198 | + "transaction_hash": transaction_hash |
47 | 199 | }
|
48 | 200 |
|
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"], |
52 | 203 | params=params,
|
53 | 204 | )
|
54 | 205 |
|
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() |
89 | 249 |
|
| 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}) |
90 | 255 |
|
| 256 | + return {"cursor": api_result["cursor"], |
| 257 | + "results": final_results} |
0 commit comments