diff --git a/python/coinbase-agentkit/coinbase_agentkit/__init__.py b/python/coinbase-agentkit/coinbase_agentkit/__init__.py index e07a80789..bdad35261 100644 --- a/python/coinbase-agentkit/coinbase_agentkit/__init__.py +++ b/python/coinbase-agentkit/coinbase_agentkit/__init__.py @@ -10,6 +10,7 @@ erc20_action_provider, morpho_action_provider, pyth_action_provider, + superfluid_action_provider, twitter_action_provider, wallet_action_provider, weth_action_provider, @@ -47,4 +48,5 @@ "twitter_action_provider", "wallet_action_provider", "weth_action_provider", + "superfluid_action_provider", ] diff --git a/python/coinbase-agentkit/coinbase_agentkit/action_providers/__init__.py b/python/coinbase-agentkit/coinbase_agentkit/action_providers/__init__.py index 4fc645a8b..07dc72b5d 100644 --- a/python/coinbase-agentkit/coinbase_agentkit/action_providers/__init__.py +++ b/python/coinbase-agentkit/coinbase_agentkit/action_providers/__init__.py @@ -9,6 +9,10 @@ from .erc20.erc20_action_provider import ERC20ActionProvider, erc20_action_provider from .morpho.morpho_action_provider import MorphoActionProvider, morpho_action_provider from .pyth.pyth_action_provider import PythActionProvider, pyth_action_provider +from .superfluid.superfluid_action_provider import ( + SuperfluidActionProvider, + superfluid_action_provider, +) from .twitter.twitter_action_provider import TwitterActionProvider, twitter_action_provider from .wallet.wallet_action_provider import WalletActionProvider, wallet_action_provider from .weth.weth_action_provider import WethActionProvider, weth_action_provider @@ -35,4 +39,6 @@ "weth_action_provider", "BasenameActionProvider", "basename_action_provider", + "SuperfluidActionProvider", + "superfluid_action_provider", ] diff --git a/python/coinbase-agentkit/coinbase_agentkit/action_providers/morpho/morpho_action_provider.py b/python/coinbase-agentkit/coinbase_agentkit/action_providers/morpho/morpho_action_provider.py index 233ee1cac..4fdff8744 100644 --- a/python/coinbase-agentkit/coinbase_agentkit/action_providers/morpho/morpho_action_provider.py +++ b/python/coinbase-agentkit/coinbase_agentkit/action_providers/morpho/morpho_action_provider.py @@ -67,10 +67,10 @@ def deposit(self, wallet: EvmWalletProvider, args: dict[str, Any]) -> str: "data": encoded_data, } - hash = wallet.send_transaction(params) - wallet.wait_for_transaction_receipt(hash) + tx_hash = wallet.send_transaction(params) + wallet.wait_for_transaction_receipt(tx_hash) - return f"Deposited {args['assets']} to Morpho Vault {args['vault_address']} with transaction hash: {hash}" + return f"Deposited {args['assets']} to Morpho Vault {args['vault_address']} with transaction hash: {tx_hash}" except Exception as e: return f"Error depositing to Morpho Vault: {e!s}" @@ -105,10 +105,10 @@ def withdraw(self, wallet: EvmWalletProvider, args: dict[str, Any]) -> str: "data": encoded_data, } - hash = wallet.send_transaction(params) - wallet.wait_for_transaction_receipt(hash) + tx_hash = wallet.send_transaction(params) + wallet.wait_for_transaction_receipt(tx_hash) - return f"Withdrawn {args['assets']} from Morpho Vault {args['vault_address']} with transaction hash: {hash}" + return f"Withdrawn {args['assets']} from Morpho Vault {args['vault_address']} with transaction hash: {tx_hash}" except Exception as e: return f"Error withdrawing from Morpho Vault: {e!s}" diff --git a/python/coinbase-agentkit/coinbase_agentkit/action_providers/superfluid/__init__.py b/python/coinbase-agentkit/coinbase_agentkit/action_providers/superfluid/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/python/coinbase-agentkit/coinbase_agentkit/action_providers/superfluid/constants.py b/python/coinbase-agentkit/coinbase_agentkit/action_providers/superfluid/constants.py new file mode 100644 index 000000000..08d197e84 --- /dev/null +++ b/python/coinbase-agentkit/coinbase_agentkit/action_providers/superfluid/constants.py @@ -0,0 +1,62 @@ +"""Constants for Superfluid action provider.""" + +SUPERFLUID_HOST_ADDRESS = "0xcfA132E353cB4E398080B9700609bb008eceB125" + +CREATE_ABI = [ + { + "inputs": [ + { + "internalType": "contract ISuperToken", + "name": "token", + "type": "address", + }, + {"internalType": "address", "name": "sender", "type": "address"}, + {"internalType": "address", "name": "receiver", "type": "address"}, + {"internalType": "int96", "name": "flowrate", "type": "int96"}, + {"internalType": "bytes", "name": "userData", "type": "bytes"}, + ], + "name": "createFlow", + "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], + "stateMutability": "nonpayable", + "type": "function", + } +] + +UPDATE_ABI = [ + { + "inputs": [ + { + "internalType": "contract ISuperToken", + "name": "token", + "type": "address", + }, + {"internalType": "address", "name": "sender", "type": "address"}, + {"internalType": "address", "name": "receiver", "type": "address"}, + {"internalType": "int96", "name": "flowrate", "type": "int96"}, + {"internalType": "bytes", "name": "userData", "type": "bytes"}, + ], + "name": "updateFlow", + "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], + "stateMutability": "nonpayable", + "type": "function", + } +] + +DELETE_ABI = [ + { + "inputs": [ + { + "internalType": "contract ISuperToken", + "name": "token", + "type": "address", + }, + {"internalType": "address", "name": "sender", "type": "address"}, + {"internalType": "address", "name": "receiver", "type": "address"}, + {"internalType": "bytes", "name": "userData", "type": "bytes"}, + ], + "name": "deleteFlow", + "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], + "stateMutability": "nonpayable", + "type": "function", + } +] diff --git a/python/coinbase-agentkit/coinbase_agentkit/action_providers/superfluid/schemas.py b/python/coinbase-agentkit/coinbase_agentkit/action_providers/superfluid/schemas.py new file mode 100644 index 000000000..a5c0ed435 --- /dev/null +++ b/python/coinbase-agentkit/coinbase_agentkit/action_providers/superfluid/schemas.py @@ -0,0 +1,26 @@ +"""Schema definitions for Superfluid action provider.""" + +from pydantic import BaseModel, Field + + +class CreateFlowInput(BaseModel): + """Input argument schema for creating a flow.""" + + recipient: str = Field(..., description="The wallet address of the recipient") + token_address: str = Field(..., description="The address of the token that will be streamed") + flow_rate: str = Field(..., description="The flow rate of tokens in wei per second") + + +class DeleteFlowInput(BaseModel): + """Input argument schema for deleting a flow.""" + + recipient: str = Field(..., description="The wallet address of the recipient") + token_address: str = Field(..., description="The address of the token being flowed") + + +class UpdateFlowInput(BaseModel): + """Input argument schema for updating a flow.""" + + recipient: str = Field(..., description="The wallet address of the recipient") + token_address: str = Field(..., description="The address of the token that is being streamed") + new_flow_rate: str = Field(..., description="The new flow rate of tokens in wei per second") diff --git a/python/coinbase-agentkit/coinbase_agentkit/action_providers/superfluid/superfluid_action_provider.py b/python/coinbase-agentkit/coinbase_agentkit/action_providers/superfluid/superfluid_action_provider.py new file mode 100644 index 000000000..17b2ee4f1 --- /dev/null +++ b/python/coinbase-agentkit/coinbase_agentkit/action_providers/superfluid/superfluid_action_provider.py @@ -0,0 +1,158 @@ +from typing import Any + +from web3 import Web3 + +from ...network import Network +from ...wallet_providers import EvmWalletProvider +from ..action_decorator import create_action +from ..action_provider import ActionProvider +from .constants import CREATE_ABI, DELETE_ABI, SUPERFLUID_HOST_ADDRESS, UPDATE_ABI +from .schemas import CreateFlowInput, DeleteFlowInput, UpdateFlowInput + + +class SuperfluidActionProvider(ActionProvider[EvmWalletProvider]): + """Provides actions for interacting with Superfluid protocol.""" + + def __init__(self): + super().__init__("superfluid", []) + + @create_action( + name="create_flow", + description=""" +This tool will create a money flow to a specified token recipient using Superfluid. Do not use this tool for any other purpose, or trading other assets. +Inputs: +- Wallet address to send the tokens to +- Super token contract address +- The flowrate of flow in wei per second +Important notes: +- The token must be a Superfluid Super token. If errors occur, confirm that the token is a valid Superfluid Super token. +- The flowrate cannot have any decimal points, since the unit of measurement is wei per second. +- Make sure to use the exact amount provided, and if there's any doubt, check by getting more information before continuing with the action. +- 1 wei = 0.000000000000000001 ETH""", + schema=CreateFlowInput, + ) + def create_flow(self, wallet_provider: EvmWalletProvider, args: dict[str, Any]) -> str: + """Create a money flow using Superfluid.""" + try: + superfluid_host_contract = Web3().eth.contract( + address=SUPERFLUID_HOST_ADDRESS, abi=CREATE_ABI + ) + + encoded_data = superfluid_host_contract.encode_abi( + "createFlow", + args=[ + args["token_address"], + wallet_provider.get_address(), + args["recipient"], + int(args["flow_rate"]), + "0x", + ], + ) + + params = {"to": SUPERFLUID_HOST_ADDRESS, "data": encoded_data} + + tx_hash = wallet_provider.send_transaction(params) + + wallet_provider.wait_for_transaction_receipt(tx_hash) + + return f"Flow created successfully. Transaction hash: {tx_hash}" + + except Exception as e: + return f"Error creating flow: {e!s}" + + @create_action( + name="update_flow", + description=""" +This tool will update an existing money flow to a specified token recipient using Superfluid. Do not use this tool for any other purpose, or trading other assets. +Inputs: +- Wallet address that the tokens are being streamed to +- Super token contract address +- The new flowrate of flow in wei per second +Important notes: +- The flowrate cannot have any decimal points, since the unit of measurement is wei per second. +- Make sure to use the exact amount provided, and if there's any doubt, check by getting more information before continuing with the action. +- 1 wei = 0.000000000000000001 ETH""", + schema=UpdateFlowInput, + ) + def update_flow(self, wallet_provider: EvmWalletProvider, args: dict[str, Any]) -> str: + """Update an existing money flow using Superfluid.""" + try: + superfluid_host_contract = Web3().eth.contract( + address=SUPERFLUID_HOST_ADDRESS, abi=UPDATE_ABI + ) + + encoded_data = superfluid_host_contract.encode_abi( + "updateFlow", + args=[ + args["token_address"], + wallet_provider.get_address(), + args["recipient"], + int(args["new_flow_rate"]), + "0x", + ], + ) + + params = {"to": SUPERFLUID_HOST_ADDRESS, "data": encoded_data} + + tx_hash = wallet_provider.send_transaction(params) + + wallet_provider.wait_for_transaction_receipt(tx_hash) + + return f"Flow updated successfully. Transaction hash: {tx_hash}" + + except Exception as e: + return f"Error updating flow: {e!s}" + + @create_action( + name="delete_flow", + description=""" +This tool will delete an existing money flow to a token recipient using Superfluid. Do not use this tool for any other purpose, or trading other assets. +Inputs: +- Wallet address that the tokens are being streamed to or being streamed from +- Super token contract address""", + schema=DeleteFlowInput, + ) + def delete_flow(self, wallet_provider: EvmWalletProvider, args: dict[str, Any]) -> str: + """Delete an existing money flow using Superfluid.""" + try: + superfluid_host_contract = Web3().eth.contract( + address=SUPERFLUID_HOST_ADDRESS, abi=DELETE_ABI + ) + + encoded_data = superfluid_host_contract.encode_abi( + "deleteFlow", + args=[ + args["token_address"], + wallet_provider.get_address(), + args["recipient"], + "0x", + ], + ) + + params = {"to": SUPERFLUID_HOST_ADDRESS, "data": encoded_data} + + tx_hash = wallet_provider.send_transaction(params) + + wallet_provider.wait_for_transaction_receipt(tx_hash) + + return f"Flow deleted successfully. Transaction hash: {tx_hash}" + + except Exception as e: + return f"Error deleting flow: {e!s}" + + def supports_network(self, network: Network) -> bool: + """Check if network is supported by Superfluid actions. + + Args: + network: The network to check support for. + + Returns: + bool: True if the network is supported, False otherwise. + + """ + return network.protocol_family == "evm" + + +def superfluid_action_provider() -> SuperfluidActionProvider: + """Create a new SuperfluidActionProvider instance.""" + return SuperfluidActionProvider() diff --git a/python/coinbase-agentkit/coinbase_agentkit/wallet_providers/cdp_wallet_provider.py b/python/coinbase-agentkit/coinbase_agentkit/wallet_providers/cdp_wallet_provider.py index 200651966..699afd52b 100644 --- a/python/coinbase-agentkit/coinbase_agentkit/wallet_providers/cdp_wallet_provider.py +++ b/python/coinbase-agentkit/coinbase_agentkit/wallet_providers/cdp_wallet_provider.py @@ -242,7 +242,7 @@ def _prepare_transaction(self, transaction: TxParams) -> TxParams: transaction["to"] = b"" transaction["from"] = self._address - transaction["value"] = int(transaction["value"]) + transaction["value"] = int(transaction.get("value", 0)) transaction["type"] = 2 transaction["chainId"] = self._network.chain_id diff --git a/python/coinbase-agentkit/coinbase_agentkit/wallet_providers/evm_wallet_provider.py b/python/coinbase-agentkit/coinbase_agentkit/wallet_providers/evm_wallet_provider.py index 4c35e694c..1089cb34c 100644 --- a/python/coinbase-agentkit/coinbase_agentkit/wallet_providers/evm_wallet_provider.py +++ b/python/coinbase-agentkit/coinbase_agentkit/wallet_providers/evm_wallet_provider.py @@ -16,9 +16,7 @@ def sign_message(self, message: str | bytes) -> HexStr: pass @abstractmethod - def sign_typed_data( - self, typed_data: dict[str, Any] - ) -> HexStr: + def sign_typed_data(self, typed_data: dict[str, Any]) -> HexStr: """Sign typed data according to EIP-712 standard.""" pass diff --git a/python/coinbase-agentkit/tests/action_providers/superfluid/test_superfluid_action_provider.py b/python/coinbase-agentkit/tests/action_providers/superfluid/test_superfluid_action_provider.py new file mode 100644 index 000000000..2c7159b73 --- /dev/null +++ b/python/coinbase-agentkit/tests/action_providers/superfluid/test_superfluid_action_provider.py @@ -0,0 +1,376 @@ +"""Tests for Superfluid action provider.""" + +from unittest.mock import MagicMock, patch + +import pytest +from pydantic import ValidationError + +from coinbase_agentkit.action_providers.superfluid.constants import ( + CREATE_ABI, + DELETE_ABI, + SUPERFLUID_HOST_ADDRESS, + UPDATE_ABI, +) +from coinbase_agentkit.action_providers.superfluid.schemas import ( + CreateFlowInput, + DeleteFlowInput, + UpdateFlowInput, +) +from coinbase_agentkit.action_providers.superfluid.superfluid_action_provider import ( + SuperfluidActionProvider, +) +from coinbase_agentkit.network import Network + +MOCK_TX_HASH = "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" +MOCK_RECEIPT = {"status": 1, "transactionHash": MOCK_TX_HASH} +MOCK_ADDRESS = "0xmockWalletAddress" + + +def test_create_flowinput_model_valid(): + """Test that CreateFlowInput schema accepts valid parameters.""" + valid_input = { + "recipient": "0xRecipientAddress", + "token_address": "0xTokenAddress", + "flow_rate": "1000", + } + input_model = CreateFlowInput(**valid_input) + + assert isinstance(input_model, CreateFlowInput) + assert input_model.recipient == valid_input["recipient"] + assert input_model.token_address == valid_input["token_address"] + assert input_model.flow_rate == valid_input["flow_rate"] + + +def test_create_flow_input_model_missing_params(): + """Test that CreateFlowInput schema raises error when params are missing.""" + with pytest.raises(ValidationError): + CreateFlowInput() + + +def test_update_flow_input_model_valid(): + """Test that UpdateFlowInput schema accepts valid parameters.""" + valid_input = { + "recipient": "0xRecipientAddress", + "token_address": "0xTokenAddress", + "new_flow_rate": "2000", + } + input_model = UpdateFlowInput(**valid_input) + + assert isinstance(input_model, UpdateFlowInput) + assert input_model.recipient == valid_input["recipient"] + assert input_model.token_address == valid_input["token_address"] + assert input_model.new_flow_rate == valid_input["new_flow_rate"] + + +def test_update_flow_input_model_missing_params(): + """Test that UpdateFlowInput schema raises error when params are missing.""" + with pytest.raises(ValidationError): + UpdateFlowInput() + + +def test_delete_flow_input_model_valid(): + """Test that DeleteFlowInput schema accepts valid parameters.""" + valid_input = {"recipient": "0xRecipientAddress", "token_address": "0xTokenAddress"} + input_model = DeleteFlowInput(**valid_input) + + assert isinstance(input_model, DeleteFlowInput) + assert input_model.recipient == valid_input["recipient"] + assert input_model.token_address == valid_input["token_address"] + + +def test_delete_flow_input_model_missing_params(): + """Test that DeleteFlowInput schema raises error when params are missing.""" + with pytest.raises(ValidationError): + DeleteFlowInput() + + +def test_create_flow_success(): + """Test successful flow creation.""" + with ( + patch( + "coinbase_agentkit.action_providers.superfluid.superfluid_action_provider.Web3" + ) as mock_web3, + ): + mock_contract = mock_web3.return_value.eth.contract.return_value + mock_contract.encode_abi.return_value = "0xencoded" + mock_wallet = MagicMock() + mock_wallet.send_transaction.return_value = MOCK_TX_HASH + mock_wallet.wait_for_transaction_receipt.return_value = MOCK_RECEIPT + mock_wallet.get_address.return_value = MOCK_ADDRESS + + provider = SuperfluidActionProvider() + args = { + "recipient": "0xRecipientAddress", + "token_address": "0xTokenAddress", + "flow_rate": "1000", + } + response = provider.create_flow(mock_wallet, args) + + expected_response = f"Flow created successfully. Transaction hash: {MOCK_TX_HASH}" + assert response == expected_response + + mock_web3.return_value.eth.contract.assert_called_once_with( + address=SUPERFLUID_HOST_ADDRESS, + abi=CREATE_ABI, + ) + + mock_contract.encode_abi.assert_called_once_with( + "createFlow", args=["0xTokenAddress", MOCK_ADDRESS, "0xRecipientAddress", 1000, "0x"] + ) + + mock_wallet.send_transaction.assert_called_once() + tx = mock_wallet.send_transaction.call_args[0][0] + assert tx["to"] == SUPERFLUID_HOST_ADDRESS + assert tx["data"] == "0xencoded" + + mock_wallet.wait_for_transaction_receipt.assert_called_once_with(MOCK_TX_HASH) + + +def test_update_flow_success(): + """Test successful flow update.""" + with ( + patch( + "coinbase_agentkit.action_providers.superfluid.superfluid_action_provider.Web3" + ) as mock_web3, + ): + mock_contract = mock_web3.return_value.eth.contract.return_value + mock_contract.encode_abi.return_value = "0xencoded" + mock_wallet = MagicMock() + mock_wallet.send_transaction.return_value = MOCK_TX_HASH + mock_wallet.wait_for_transaction_receipt.return_value = MOCK_RECEIPT + mock_wallet.get_address.return_value = MOCK_ADDRESS + + provider = SuperfluidActionProvider() + args = { + "recipient": "0xRecipientAddress", + "token_address": "0xTokenAddress", + "new_flow_rate": "2000", + } + response = provider.update_flow(mock_wallet, args) + + expected_response = f"Flow updated successfully. Transaction hash: {MOCK_TX_HASH}" + assert response == expected_response + + mock_web3.return_value.eth.contract.assert_called_once_with( + address=SUPERFLUID_HOST_ADDRESS, + abi=UPDATE_ABI, + ) + + mock_contract.encode_abi.assert_called_once_with( + "updateFlow", + args=[ + "0xTokenAddress", + MOCK_ADDRESS, + "0xRecipientAddress", + 2000, + "0x", + ], + ) + + mock_wallet.send_transaction.assert_called_once() + tx = mock_wallet.send_transaction.call_args[0][0] + assert tx["to"] == SUPERFLUID_HOST_ADDRESS + assert tx["data"] == "0xencoded" + + mock_wallet.wait_for_transaction_receipt.assert_called_once_with(MOCK_TX_HASH) + + +def test_delete_flow_success(): + """Test successful flow deletion.""" + with ( + patch( + "coinbase_agentkit.action_providers.superfluid.superfluid_action_provider.Web3" + ) as mock_web3, + ): + mock_contract = mock_web3.return_value.eth.contract.return_value + mock_contract.encode_abi.return_value = "0xencoded" + mock_wallet = MagicMock() + mock_wallet.send_transaction.return_value = MOCK_TX_HASH + mock_wallet.wait_for_transaction_receipt.return_value = MOCK_RECEIPT + mock_wallet.get_address.return_value = MOCK_ADDRESS + + provider = SuperfluidActionProvider() + args = {"recipient": "0xRecipientAddress", "token_address": "0xTokenAddress"} + response = provider.delete_flow(mock_wallet, args) + + expected_response = f"Flow deleted successfully. Transaction hash: {MOCK_TX_HASH}" + assert response == expected_response + + mock_web3.return_value.eth.contract.assert_called_once_with( + address=SUPERFLUID_HOST_ADDRESS, + abi=DELETE_ABI, + ) + + mock_contract.encode_abi.assert_called_once_with( + "deleteFlow", + args=[ + "0xTokenAddress", + MOCK_ADDRESS, + "0xRecipientAddress", + "0x", + ], + ) + + mock_wallet.send_transaction.assert_called_once() + tx = mock_wallet.send_transaction.call_args[0][0] + assert tx["to"] == SUPERFLUID_HOST_ADDRESS + assert tx["data"] == "0xencoded" + + mock_wallet.wait_for_transaction_receipt.assert_called_once_with(MOCK_TX_HASH) + + +def test_create_flow_error(): + """Test flow creation when transaction fails.""" + with ( + patch( + "coinbase_agentkit.action_providers.superfluid.superfluid_action_provider.Web3" + ) as mock_web3, + ): + mock_contract = mock_web3.return_value.eth.contract.return_value + mock_contract.encode_abi.return_value = "0xencoded" + mock_wallet = MagicMock() + mock_wallet.get_address.return_value = MOCK_ADDRESS + mock_wallet.send_transaction.side_effect = Exception("Transaction failed") + + provider = SuperfluidActionProvider() + args = { + "recipient": "0xRecipientAddress", + "token_address": "0xTokenAddress", + "flow_rate": "1000", + } + + response = provider.create_flow(mock_wallet, args) + + expected_response = "Error creating flow: Transaction failed" + assert response == expected_response + + mock_web3.return_value.eth.contract.assert_called_once_with( + address=SUPERFLUID_HOST_ADDRESS, + abi=CREATE_ABI, + ) + + mock_contract.encode_abi.assert_called_once_with( + "createFlow", args=["0xTokenAddress", MOCK_ADDRESS, "0xRecipientAddress", 1000, "0x"] + ) + + mock_wallet.send_transaction.assert_called_once() + tx = mock_wallet.send_transaction.call_args[0][0] + assert tx["to"] == SUPERFLUID_HOST_ADDRESS + assert tx["data"] == "0xencoded" + + +def test_update_flow_error(): + """Test flow update when transaction fails.""" + with ( + patch( + "coinbase_agentkit.action_providers.superfluid.superfluid_action_provider.Web3" + ) as mock_web3, + ): + mock_contract = mock_web3.return_value.eth.contract.return_value + mock_contract.encode_abi.return_value = "0xencoded" + mock_wallet = MagicMock() + mock_wallet.get_address.return_value = MOCK_ADDRESS + mock_wallet.send_transaction.side_effect = Exception("Transaction failed") + + provider = SuperfluidActionProvider() + args = { + "recipient": "0xRecipientAddress", + "token_address": "0xTokenAddress", + "new_flow_rate": "2000", + } + response = provider.update_flow(mock_wallet, args) + + expected_response = "Error updating flow: Transaction failed" + assert response == expected_response + + mock_web3.return_value.eth.contract.assert_called_once_with( + address=SUPERFLUID_HOST_ADDRESS, + abi=UPDATE_ABI, + ) + + mock_contract.encode_abi.assert_called_once_with( + "updateFlow", + args=[ + "0xTokenAddress", + MOCK_ADDRESS, + "0xRecipientAddress", + 2000, + "0x", + ], + ) + + mock_wallet.send_transaction.assert_called_once() + tx = mock_wallet.send_transaction.call_args[0][0] + assert tx["to"] == SUPERFLUID_HOST_ADDRESS + assert tx["data"] == "0xencoded" + + +def test_delete_flow_error(): + """Test flow deletion when transaction fails.""" + with ( + patch( + "coinbase_agentkit.action_providers.superfluid.superfluid_action_provider.Web3" + ) as mock_web3, + ): + mock_contract = mock_web3.return_value.eth.contract.return_value + mock_contract.encode_abi.return_value = "0xencoded" + mock_wallet = MagicMock() + mock_wallet.get_address.return_value = MOCK_ADDRESS + mock_wallet.send_transaction.side_effect = Exception("Transaction failed") + + provider = SuperfluidActionProvider() + args = {"recipient": "0xRecipientAddress", "token_address": "0xTokenAddress"} + response = provider.delete_flow(mock_wallet, args) + + expected_response = "Error deleting flow: Transaction failed" + assert response == expected_response + + mock_web3.return_value.eth.contract.assert_called_once_with( + address=SUPERFLUID_HOST_ADDRESS, + abi=DELETE_ABI, + ) + + mock_contract.encode_abi.assert_called_once_with( + "deleteFlow", + args=[ + "0xTokenAddress", + MOCK_ADDRESS, + "0xRecipientAddress", + "0x", + ], + ) + + mock_wallet.send_transaction.assert_called_once() + tx = mock_wallet.send_transaction.call_args[0][0] + assert tx["to"] == SUPERFLUID_HOST_ADDRESS + assert tx["data"] == "0xencoded" + + +def test_supports_network(): + """Test network support validation.""" + provider = SuperfluidActionProvider() + + test_cases = [ + ("base-mainnet", 8453, "evm", True), + ("base-sepolia", 84532, "evm", True), + ("ethereum-mainnet", 1, "evm", True), + ("arbitrum-one", 42161, "evm", True), + ("optimism", 10, "evm", True), + ("base-goerli", 84531, "evm", True), + ("mainnet", None, "bitcoin", False), + ("mainnet", None, "solana", False), + ] + + for network_id, chain_id, protocol_family, expected_result in test_cases: + network = Network(protocol_family=protocol_family, chain_id=chain_id, network_id=network_id) + result = provider.supports_network(network) + assert ( + result is expected_result + ), f"Network {network_id} (chain_id: {chain_id}) should{' ' if expected_result else ' not '}be supported" + + +def test_action_provider_initialization(): + """Test action provider initialization.""" + provider = SuperfluidActionProvider() + assert provider.name == "superfluid" + assert provider.action_providers == [] diff --git a/python/examples/langchain-cdp-chatbot/chatbot.py b/python/examples/langchain-cdp-chatbot/chatbot.py index 7ad94b2bd..f869e8d60 100644 --- a/python/examples/langchain-cdp-chatbot/chatbot.py +++ b/python/examples/langchain-cdp-chatbot/chatbot.py @@ -24,6 +24,7 @@ wallet_action_provider, cdp_api_action_provider, weth_action_provider, + superfluid_action_provider, ) from coinbase_agentkit_langchain import get_langchain_tools @@ -80,6 +81,7 @@ def initialize_agent(): wallet_action_provider(), cdp_api_action_provider(), weth_action_provider(), + superfluid_action_provider(), ] ))