Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 42 additions & 35 deletions skills/token/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,25 +36,38 @@ def get_api_key(self, context: SkillContext) -> str:

Returns:
The API key to use for API requests

Raises:
ValueError: If no API key is available
"""
skill_config = context.config
if skill_config.get("api_key_provider") == "agent_owner":
return skill_config.get("api_key")
return self.skill_store.get_system_config("moralis_api_key")
api_key = skill_config.get("api_key")
if not api_key:
raise ValueError(
"No agent-specific API key provided in the configuration."
)
return api_key

api_key = self.skill_store.get_system_config("moralis_api_key")
if not api_key:
raise ValueError("No Moralis API key provided in the configuration.")
return api_key

def context_from_config(self, config: Optional[RunnableConfig] = None) -> Any:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unless necessary, do not override common functions. This way, if there are any updates upstream, they will not be overwritten here.

"""Extract context from the runnable config."""
"""Extract context from the runnable config.

Raises:
ValueError: If config is invalid or missing required fields
"""
if not config:
logger.error("No config provided to context_from_config")
return None
raise ValueError("No config provided to context_from_config")

if "configurable" not in config:
logger.error("'configurable' not in config")
return None
raise ValueError("'configurable' not in config")

if "agent" not in config["configurable"]:
logger.error("'agent' not in config['configurable']")
return None
raise ValueError("'agent' not in config['configurable']")

agent = config["configurable"].get("agent")
category_config = None
Expand Down Expand Up @@ -118,37 +131,31 @@ async def _make_request(

Returns:
Response data as dictionary

Raises:
ValueError: If API key is missing
aiohttp.ClientError: For HTTP client errors
Exception: For API errors or unexpected errors
"""
url = f"{MORALIS_API_BASE_URL}{endpoint}"

if not api_key:
logger.error("API key is missing")
return {"error": "API key is missing"}
raise ValueError("API key is missing")

headers = {"accept": "application/json", "X-API-Key": api_key}
processed_params = self._prepare_params(params) if params else None

try:
async with aiohttp.ClientSession() as session:
async with session.request(
method=method,
url=url,
headers=headers,
params=processed_params,
json=data,
) as response:
if response.status >= 400:
error_text = await response.text()
logger.error(f"API error {response.status}: {error_text}")
return {
"error": f"API error: {response.status}",
"details": error_text,
}

return await response.json()
except aiohttp.ClientError as e:
logger.error(f"HTTP error making request: {str(e)}")
return {"error": f"HTTP error: {str(e)}"}
except Exception as e:
logger.error(f"Unexpected error making request: {str(e)}")
return {"error": f"Unexpected error: {str(e)}"}
async with aiohttp.ClientSession() as session:
async with session.request(
method=method,
url=url,
headers=headers,
params=processed_params,
json=data,
) as response:
if response.status >= 400:
error_text = await response.text()
logger.error(f"API error {response.status}: {error_text}")
raise Exception(f"API error {response.status}: {error_text}")

return await response.json()
27 changes: 6 additions & 21 deletions skills/token/erc20_transfers.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import logging
from typing import Any, Dict, List, Optional, Type
from typing import Any, Dict, List, Optional

from langchain_core.runnables import RunnableConfig
from pydantic import BaseModel, Field
Expand Down Expand Up @@ -66,7 +66,7 @@ class ERC20Transfers(TokenBaseTool):
"Get ERC20 token transactions for a wallet address, ordered by block number. "
"Returns transaction details, token information, and wallet interactions."
)
args_schema: Type[BaseModel] = ERC20TransfersInput
args_schema: type[BaseModel] = ERC20TransfersInput

async def _arun(
self,
Expand Down Expand Up @@ -102,19 +102,10 @@ async def _arun(
Dict containing ERC20 transfer data
"""
context = self.context_from_config(config)
if context is None:
logger.error("Context is None, cannot retrieve API key")
return {
"error": "Cannot retrieve API key. Please check agent configuration."
}

# Get the API key
api_key = self.get_api_key(context)

if not api_key:
logger.error("No Moralis API key available")
return {"error": "No Moralis API key provided in the configuration."}

# Build query parameters
params = {"chain": chain, "limit": limit, "order": order}

Expand All @@ -133,13 +124,7 @@ async def _arun(
params["cursor"] = cursor

# Call Moralis API
try:
endpoint = f"/{address}/erc20/transfers"
return await self._make_request(
method="GET", endpoint=endpoint, api_key=api_key, params=params
)
except Exception as e:
logger.error(f"Error fetching ERC20 transfers: {e}")
return {
"error": f"An error occurred while fetching ERC20 transfers: {str(e)}. Please try again later."
}
endpoint = f"/{address}/erc20/transfers"
return await self._make_request(
method="GET", endpoint=endpoint, api_key=api_key, params=params
)
27 changes: 6 additions & 21 deletions skills/token/token_analytics.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import logging
from typing import Any, Dict, Type
from typing import Any, Dict

from langchain_core.runnables import RunnableConfig
from pydantic import BaseModel, Field
Expand Down Expand Up @@ -32,7 +32,7 @@ class TokenAnalytics(TokenBaseTool):
"Get analytics for a token by token address. "
"Returns trading volumes, number of buyers/sellers, and liquidity information over various time periods."
)
args_schema: Type[BaseModel] = TokenAnalyticsInput
args_schema: type[BaseModel] = TokenAnalyticsInput

async def _arun(
self,
Expand All @@ -52,30 +52,15 @@ async def _arun(
Dict containing token analytics data
"""
context = self.context_from_config(config)
if context is None:
logger.error("Context is None, cannot retrieve API key")
return {
"error": "Cannot retrieve API key. Please check agent configuration."
}

# Get the API key
api_key = self.get_api_key(context)

if not api_key:
logger.error("No Moralis API key available")
return {"error": "No Moralis API key provided in the configuration."}

# Build query parameters
params = {"chain": chain}

# Call Moralis API
try:
endpoint = f"/tokens/{address}/analytics"
return await self._make_request(
method="GET", endpoint=endpoint, api_key=api_key, params=params
)
except Exception as e:
logger.error(f"Error fetching token analytics: {e}")
return {
"error": f"An error occurred while fetching token analytics: {str(e)}. Please try again later."
}
endpoint = f"/tokens/{address}/analytics"
return await self._make_request(
method="GET", endpoint=endpoint, api_key=api_key, params=params
)
33 changes: 6 additions & 27 deletions skills/token/token_price.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import logging
from typing import Any, Dict, Optional, Type
from typing import Any, Dict, Optional

from langchain_core.runnables import RunnableConfig
from pydantic import BaseModel, Field
Expand Down Expand Up @@ -54,7 +54,7 @@ class TokenPrice(TokenBaseTool):
"Get the token price denominated in the blockchain's native token and USD for a given token contract address. "
"Returns price, token information and exchange data."
)
args_schema: Type[BaseModel] = TokenPriceInput
args_schema: type[BaseModel] = TokenPriceInput

async def _arun(
self,
Expand Down Expand Up @@ -86,19 +86,9 @@ async def _arun(
# Extract context from config
context = self.context_from_config(config)

if context is None:
logger.error("Context is None, cannot retrieve API key")
return {
"error": "Cannot retrieve API key. Please check agent configuration."
}

# Get the API key
api_key = self.get_api_key(context)

if not api_key:
logger.error("No Moralis API key available")
return {"error": "No Moralis API key provided in the configuration."}

# Build query parameters
params = {"chain": chain}

Expand All @@ -115,18 +105,7 @@ async def _arun(
params["min_pair_side_liquidity_usd"] = min_pair_side_liquidity_usd

# Call Moralis API
try:
endpoint = f"/erc20/{address}/price"
response = await self._make_request(
method="GET", endpoint=endpoint, api_key=api_key, params=params
)

if "error" in response:
logger.error(f"API returned error: {response.get('error')}")

return response
except Exception as e:
logger.error(f"Error fetching token price: {e}")
return {
"error": f"An error occurred while fetching token price: {str(e)}. Please try again later."
}
endpoint = f"/erc20/{address}/price"
return await self._make_request(
method="GET", endpoint=endpoint, api_key=api_key, params=params
)
41 changes: 6 additions & 35 deletions skills/token/token_search.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import logging
from typing import Any, Dict, List, Optional, Type
from typing import Any, Dict, List, Optional

from langchain_core.runnables import RunnableConfig
from pydantic import BaseModel, Field
Expand Down Expand Up @@ -45,7 +45,7 @@ class TokenSearch(TokenBaseTool):
"Returns token information including price, market cap, and security information. "
"NOTE: This is a premium endpoint that requires a Moralis Business plan."
)
args_schema: Type[BaseModel] = TokenSearchInput
args_schema: type[BaseModel] = TokenSearchInput

async def _arun(
self,
Expand All @@ -70,19 +70,10 @@ async def _arun(
"""
# Extract context from config
context = self.context_from_config(config)
if context is None:
logger.error("Context is None, cannot retrieve API key")
return {
"error": "Cannot retrieve API key. Please check agent configuration."
}

# Get the API key
api_key = self.get_api_key(context)

if not api_key:
logger.error("No Moralis API key available")
return {"error": "No Moralis API key provided in the configuration."}

# Build query parameters
params = {"query": query}

Expand All @@ -95,27 +86,7 @@ async def _arun(
params["isVerifiedContract"] = is_verified_contract

# Call Moralis API
try:
endpoint = "/tokens/search"
result = await self._make_request(
method="GET", endpoint=endpoint, api_key=api_key, params=params
)

# Add premium notice if there's an error that might be related to plan limits
if "error" in result and "403" in str(result.get("error", "")):
logger.error("Received 403 error - likely a plan limitation")
result["notice"] = (
"This API requires a Moralis Business plan or Enterprise plan. "
"Please ensure your API key is associated with the appropriate plan."
)

return result
except Exception as e:
logger.error(f"Error searching for tokens: {e}")
return {
"error": f"An error occurred while searching for tokens: {str(e)}. Please try again later.",
"notice": (
"This API requires a Moralis Business plan or Enterprise plan. "
"Please ensure your API key is associated with the appropriate plan."
),
}
endpoint = "/tokens/search"
return await self._make_request(
method="GET", endpoint=endpoint, api_key=api_key, params=params
)