diff --git a/lib/crewai-tools/src/crewai_tools/tools/__init__.py b/lib/crewai-tools/src/crewai_tools/tools/__init__.py index 56e77ffe4f..b40838396a 100644 --- a/lib/crewai-tools/src/crewai_tools/tools/__init__.py +++ b/lib/crewai-tools/src/crewai_tools/tools/__init__.py @@ -172,6 +172,13 @@ SnowflakeSearchTool, SnowflakeSearchToolInput, ) +from crewai_tools.tools.suwappu_defi_tool import ( + SuwappuGetPricesTool, + SuwappuGetQuoteTool, + SuwappuGetPortfolioTool, + SuwappuListChainsTool, + SuwappuListTokensTool, +) from crewai_tools.tools.spider_tool.spider_tool import SpiderTool from crewai_tools.tools.stagehand_tool.stagehand_tool import StagehandTool from crewai_tools.tools.tavily_extractor_tool.tavily_extractor_tool import ( @@ -277,6 +284,11 @@ "SnowflakeConfig", "SnowflakeSearchTool", "SnowflakeSearchToolInput", + "SuwappuGetPricesTool", + "SuwappuGetQuoteTool", + "SuwappuGetPortfolioTool", + "SuwappuListChainsTool", + "SuwappuListTokensTool", "SpiderTool", "StagehandTool", "TXTSearchTool", diff --git a/lib/crewai-tools/src/crewai_tools/tools/suwappu_defi_tool/README.md b/lib/crewai-tools/src/crewai_tools/tools/suwappu_defi_tool/README.md new file mode 100644 index 0000000000..31b39a98f2 --- /dev/null +++ b/lib/crewai-tools/src/crewai_tools/tools/suwappu_defi_tool/README.md @@ -0,0 +1,55 @@ +# SuwappuDefiTool Documentation + +## Description + +A suite of DeFi tools for cross-chain token operations via the [Suwappu](https://suwappu.com) DEX aggregator API. Enables AI agents to check token prices, get swap quotes, view portfolio balances, and discover supported chains and tokens across 15+ blockchain networks including Ethereum, Base, Arbitrum, Solana, and more. + +## Installation + +```shell +pip install 'crewai[tools]' suwappu +``` + +## Tools + +| Tool | Description | +|------|-------------| +| `SuwappuGetPricesTool` | Get current USD price and 24h change for any token | +| `SuwappuGetQuoteTool` | Get a swap quote with price impact, route, gas, and fees | +| `SuwappuGetPortfolioTool` | Check wallet token balances across chains | +| `SuwappuListChainsTool` | List all supported blockchain networks | +| `SuwappuListTokensTool` | List available tokens on a specific chain | + +## Example + +```python +from crewai import Agent, Task, Crew +from crewai_tools import SuwappuGetPricesTool, SuwappuGetQuoteTool, SuwappuGetPortfolioTool + +# Initialize tools +price_tool = SuwappuGetPricesTool() +quote_tool = SuwappuGetQuoteTool() +portfolio_tool = SuwappuGetPortfolioTool() + +# Create an agent with DeFi capabilities +defi_agent = Agent( + role="DeFi Analyst", + goal="Analyze token prices and find optimal swap routes", + tools=[price_tool, quote_tool, portfolio_tool], +) + +# Example task +task = Task( + description="Check the current price of ETH on Base and get a quote to swap 0.5 ETH to USDC", + agent=defi_agent, +) + +crew = Crew(agents=[defi_agent], tasks=[task]) +result = crew.kickoff() +``` + +## Setup + +1. Install dependencies: `pip install 'crewai[tools]' suwappu` +2. Get a Suwappu API key at [suwappu.com](https://suwappu.com) +3. Set the environment variable: `export SUWAPPU_API_KEY=your_key_here` diff --git a/lib/crewai-tools/src/crewai_tools/tools/suwappu_defi_tool/__init__.py b/lib/crewai-tools/src/crewai_tools/tools/suwappu_defi_tool/__init__.py new file mode 100644 index 0000000000..4d9a307cdd --- /dev/null +++ b/lib/crewai-tools/src/crewai_tools/tools/suwappu_defi_tool/__init__.py @@ -0,0 +1,7 @@ +from .suwappu_defi_tool import ( + SuwappuGetPricesTool, + SuwappuGetQuoteTool, + SuwappuGetPortfolioTool, + SuwappuListChainsTool, + SuwappuListTokensTool, +) diff --git a/lib/crewai-tools/src/crewai_tools/tools/suwappu_defi_tool/suwappu_defi_tool.py b/lib/crewai-tools/src/crewai_tools/tools/suwappu_defi_tool/suwappu_defi_tool.py new file mode 100644 index 0000000000..f7bf29a8e0 --- /dev/null +++ b/lib/crewai-tools/src/crewai_tools/tools/suwappu_defi_tool/suwappu_defi_tool.py @@ -0,0 +1,239 @@ +"""CrewAI tools for cross-chain DeFi operations via the Suwappu DEX API. + +Provides token pricing, swap quotes, portfolio tracking, and chain/token +discovery across 15+ blockchain networks including Ethereum, Base, Arbitrum, +Solana, and more. + +Dependencies: + - suwappu + - pydantic +""" + +from __future__ import annotations + +import asyncio +import os +from typing import Any, List, Optional, Type + +from crewai.tools import BaseTool, EnvVar +from pydantic import BaseModel, Field + + +def _get_client(): + """Create a Suwappu API client using the configured API key.""" + try: + from suwappu import create_client + except ImportError: + raise ImportError( + "The 'suwappu' package is required for SuwappuDefiTools. " + "Install it with: pip install suwappu" + ) + api_key = os.environ.get("SUWAPPU_API_KEY", "") + return create_client(api_key=api_key) + + +def _run_async(coro): + """Run an async coroutine in a sync context.""" + try: + asyncio.get_running_loop() + import concurrent.futures + + with concurrent.futures.ThreadPoolExecutor() as pool: + future = pool.submit(asyncio.run, coro) + return future.result() + except RuntimeError: + return asyncio.run(coro) + + +# --- Schemas --- + + +class GetPricesSchema(BaseModel): + """Input for SuwappuGetPricesTool.""" + + token: str = Field(..., description="Token symbol or address to look up (e.g. 'ETH', 'USDC')") + chain: str = Field(default="", description="Optional chain to filter by (e.g. 'base', 'ethereum')") + + +class GetQuoteSchema(BaseModel): + """Input for SuwappuGetQuoteTool.""" + + from_token: str = Field(..., description="Token symbol to sell (e.g. 'ETH')") + to_token: str = Field(..., description="Token symbol to buy (e.g. 'USDC')") + amount: float = Field(..., description="Amount of from_token to swap") + chain: str = Field(..., description="Blockchain network for the swap (e.g. 'base')") + + +class GetPortfolioSchema(BaseModel): + """Input for SuwappuGetPortfolioTool.""" + + chain: str = Field(default="", description="Optional chain to filter portfolio by") + + +class ListTokensSchema(BaseModel): + """Input for SuwappuListTokensTool.""" + + chain: str = Field(..., description="Chain to list tokens for (e.g. 'base', 'ethereum')") + + +# --- Tools --- + + +class SuwappuGetPricesTool(BaseTool): + """Tool for getting real-time token prices and 24h changes across chains. + + Returns current USD price and 24-hour price change for any token + supported by the Suwappu DEX aggregator. + """ + + name: str = "Suwappu Get Token Prices" + description: str = ( + "Get the current USD price and 24-hour change for a cryptocurrency token. " + "Optionally filter by blockchain network." + ) + args_schema: Type[BaseModel] = GetPricesSchema + env_vars: List[EnvVar] = [ + EnvVar(name="SUWAPPU_API_KEY", description="API key for Suwappu DEX", required=True), + ] + + def _run(self, **kwargs: Any) -> str: + token = kwargs.get("token", "") + chain = kwargs.get("chain", "") + + async def _fetch(): + client = _get_client() + try: + result = await client.get_prices(token, chain or None) + return result.model_dump() if hasattr(result, "model_dump") else str(result) + finally: + await client.close() + + return str(_run_async(_fetch())) + + +class SuwappuGetQuoteTool(BaseTool): + """Tool for getting cross-chain swap quotes with price, route, gas, and fees. + + Returns a detailed quote for swapping one token to another, including + the best execution route, estimated gas costs, and protocol fees. + """ + + name: str = "Suwappu Get Swap Quote" + description: str = ( + "Get a swap quote for trading one token for another. " + "Returns price impact, route, estimated gas, and fees. " + "Use this before executing a swap to preview the trade." + ) + args_schema: Type[BaseModel] = GetQuoteSchema + env_vars: List[EnvVar] = [ + EnvVar(name="SUWAPPU_API_KEY", description="API key for Suwappu DEX", required=True), + ] + + def _run(self, **kwargs: Any) -> str: + from_token = kwargs.get("from_token", "") + to_token = kwargs.get("to_token", "") + amount = kwargs.get("amount", 0) + chain = kwargs.get("chain", "") + + async def _fetch(): + client = _get_client() + try: + result = await client.get_quote(from_token, to_token, amount, chain) + return result.model_dump() if hasattr(result, "model_dump") else str(result) + finally: + await client.close() + + return str(_run_async(_fetch())) + + +class SuwappuGetPortfolioTool(BaseTool): + """Tool for checking wallet token balances across all supported chains. + + Returns the current token holdings and balances for the connected + wallet, optionally filtered to a specific chain. + """ + + name: str = "Suwappu Get Portfolio" + description: str = ( + "Check wallet token balances across all supported blockchain networks, " + "or filter by a specific chain." + ) + args_schema: Type[BaseModel] = GetPortfolioSchema + env_vars: List[EnvVar] = [ + EnvVar(name="SUWAPPU_API_KEY", description="API key for Suwappu DEX", required=True), + ] + + def _run(self, **kwargs: Any) -> str: + chain = kwargs.get("chain", "") + + async def _fetch(): + client = _get_client() + try: + result = await client.get_portfolio(chain or None) + return [ + r.model_dump() if hasattr(r, "model_dump") else str(r) + for r in result + ] + finally: + await client.close() + + return str(_run_async(_fetch())) + + +class SuwappuListChainsTool(BaseTool): + """Tool for listing all blockchain networks supported by Suwappu. + + Returns the full list of supported chains (e.g. Ethereum, Base, + Arbitrum, Solana, etc.) with their chain IDs and metadata. + """ + + name: str = "Suwappu List Supported Chains" + description: str = "List all blockchain networks supported by the Suwappu DEX aggregator." + env_vars: List[EnvVar] = [ + EnvVar(name="SUWAPPU_API_KEY", description="API key for Suwappu DEX", required=True), + ] + + def _run(self, **kwargs: Any) -> str: + async def _fetch(): + client = _get_client() + try: + result = await client.list_chains() + return [ + r.model_dump() if hasattr(r, "model_dump") else str(r) + for r in result + ] + finally: + await client.close() + + return str(_run_async(_fetch())) + + +class SuwappuListTokensTool(BaseTool): + """Tool for listing available tokens on a specific blockchain. + + Returns all tradeable tokens on the given chain, including their + symbols, addresses, and decimals. + """ + + name: str = "Suwappu List Tokens" + description: str = "List all available tokens on a specific blockchain network." + args_schema: Type[BaseModel] = ListTokensSchema + env_vars: List[EnvVar] = [ + EnvVar(name="SUWAPPU_API_KEY", description="API key for Suwappu DEX", required=True), + ] + + def _run(self, **kwargs: Any) -> str: + chain = kwargs.get("chain", "") + + async def _fetch(): + client = _get_client() + try: + result = await client.list_tokens(chain) + return [ + r.model_dump() if hasattr(r, "model_dump") else str(r) + for r in result + ] + finally: + await client.close() + + return str(_run_async(_fetch())) diff --git a/lib/crewai-tools/tests/tools/suwappu_defi_tool_test.py b/lib/crewai-tools/tests/tools/suwappu_defi_tool_test.py new file mode 100644 index 0000000000..b2269bf43e --- /dev/null +++ b/lib/crewai-tools/tests/tools/suwappu_defi_tool_test.py @@ -0,0 +1,189 @@ +"""Tests for Suwappu DeFi tools.""" + +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + + +@pytest.fixture(autouse=True) +def set_api_key(monkeypatch): + monkeypatch.setenv("SUWAPPU_API_KEY", "test-key-123") + + +def _make_model_dump(data): + """Create a mock object with model_dump() returning data.""" + obj = MagicMock() + obj.model_dump.return_value = data + return obj + + +# --- SuwappuGetPricesTool --- + + +def test_get_prices_tool_initialization(): + from crewai_tools.tools.suwappu_defi_tool import SuwappuGetPricesTool + + tool = SuwappuGetPricesTool() + assert tool.name == "Suwappu Get Token Prices" + assert "price" in tool.description.lower() + + +@patch("crewai_tools.tools.suwappu_defi_tool.suwappu_defi_tool._get_client") +def test_get_prices_tool_run(mock_get_client): + from crewai_tools.tools.suwappu_defi_tool import SuwappuGetPricesTool + + mock_client = AsyncMock() + mock_client.get_prices.return_value = _make_model_dump( + {"symbol": "ETH", "price_usd": 3200.50, "change_24h": 2.5} + ) + mock_get_client.return_value = mock_client + + tool = SuwappuGetPricesTool() + result = tool.run(token="ETH", chain="base") + + assert "ETH" in result + assert "3200.5" in result + mock_client.get_prices.assert_called_once_with("ETH", "base") + mock_client.close.assert_called_once() + + +@patch("crewai_tools.tools.suwappu_defi_tool.suwappu_defi_tool._get_client") +def test_get_prices_tool_no_chain(mock_get_client): + from crewai_tools.tools.suwappu_defi_tool import SuwappuGetPricesTool + + mock_client = AsyncMock() + mock_client.get_prices.return_value = _make_model_dump( + {"symbol": "BTC", "price_usd": 65000.0} + ) + mock_get_client.return_value = mock_client + + tool = SuwappuGetPricesTool() + result = tool.run(token="BTC") + + mock_client.get_prices.assert_called_once_with("BTC", None) + + +# --- SuwappuGetQuoteTool --- + + +def test_get_quote_tool_initialization(): + from crewai_tools.tools.suwappu_defi_tool import SuwappuGetQuoteTool + + tool = SuwappuGetQuoteTool() + assert tool.name == "Suwappu Get Swap Quote" + assert "quote" in tool.description.lower() + + +@patch("crewai_tools.tools.suwappu_defi_tool.suwappu_defi_tool._get_client") +def test_get_quote_tool_run(mock_get_client): + from crewai_tools.tools.suwappu_defi_tool import SuwappuGetQuoteTool + + mock_client = AsyncMock() + mock_client.get_quote.return_value = _make_model_dump( + { + "quote_id": "q-123", + "from_token": "ETH", + "to_token": "USDC", + "amount_in": 1.0, + "amount_out": 3200.0, + "gas_estimate": "0.002", + } + ) + mock_get_client.return_value = mock_client + + tool = SuwappuGetQuoteTool() + result = tool.run(from_token="ETH", to_token="USDC", amount=1.0, chain="base") + + assert "q-123" in result + assert "3200" in result + mock_client.get_quote.assert_called_once_with("ETH", "USDC", 1.0, "base") + mock_client.close.assert_called_once() + + +# --- SuwappuGetPortfolioTool --- + + +def test_get_portfolio_tool_initialization(): + from crewai_tools.tools.suwappu_defi_tool import SuwappuGetPortfolioTool + + tool = SuwappuGetPortfolioTool() + assert tool.name == "Suwappu Get Portfolio" + + +@patch("crewai_tools.tools.suwappu_defi_tool.suwappu_defi_tool._get_client") +def test_get_portfolio_tool_run(mock_get_client): + from crewai_tools.tools.suwappu_defi_tool import SuwappuGetPortfolioTool + + mock_client = AsyncMock() + mock_client.get_portfolio.return_value = [ + _make_model_dump({"token": "ETH", "balance": 1.5, "value_usd": 4800.0}), + _make_model_dump({"token": "USDC", "balance": 1000.0, "value_usd": 1000.0}), + ] + mock_get_client.return_value = mock_client + + tool = SuwappuGetPortfolioTool() + result = tool.run(chain="base") + + assert "ETH" in result + assert "USDC" in result + mock_client.get_portfolio.assert_called_once_with("base") + mock_client.close.assert_called_once() + + +# --- SuwappuListChainsTool --- + + +def test_list_chains_tool_initialization(): + from crewai_tools.tools.suwappu_defi_tool import SuwappuListChainsTool + + tool = SuwappuListChainsTool() + assert tool.name == "Suwappu List Supported Chains" + + +@patch("crewai_tools.tools.suwappu_defi_tool.suwappu_defi_tool._get_client") +def test_list_chains_tool_run(mock_get_client): + from crewai_tools.tools.suwappu_defi_tool import SuwappuListChainsTool + + mock_client = AsyncMock() + mock_client.list_chains.return_value = [ + _make_model_dump({"name": "Ethereum", "chain_id": 1}), + _make_model_dump({"name": "Base", "chain_id": 8453}), + ] + mock_get_client.return_value = mock_client + + tool = SuwappuListChainsTool() + result = tool.run() + + assert "Ethereum" in result + assert "Base" in result + mock_client.close.assert_called_once() + + +# --- SuwappuListTokensTool --- + + +def test_list_tokens_tool_initialization(): + from crewai_tools.tools.suwappu_defi_tool import SuwappuListTokensTool + + tool = SuwappuListTokensTool() + assert tool.name == "Suwappu List Tokens" + + +@patch("crewai_tools.tools.suwappu_defi_tool.suwappu_defi_tool._get_client") +def test_list_tokens_tool_run(mock_get_client): + from crewai_tools.tools.suwappu_defi_tool import SuwappuListTokensTool + + mock_client = AsyncMock() + mock_client.list_tokens.return_value = [ + _make_model_dump({"symbol": "ETH", "address": "0x...", "decimals": 18}), + _make_model_dump({"symbol": "USDC", "address": "0x...", "decimals": 6}), + ] + mock_get_client.return_value = mock_client + + tool = SuwappuListTokensTool() + result = tool.run(chain="base") + + assert "ETH" in result + assert "USDC" in result + mock_client.list_tokens.assert_called_once_with("base") + mock_client.close.assert_called_once()