diff --git a/cdp-agentkit-core/python/cdp_agentkit_core/actions/__init__.py b/cdp-agentkit-core/python/cdp_agentkit_core/actions/__init__.py index 883c3c790..05c6ae3fb 100644 --- a/cdp-agentkit-core/python/cdp_agentkit_core/actions/__init__.py +++ b/cdp-agentkit-core/python/cdp_agentkit_core/actions/__init__.py @@ -9,6 +9,9 @@ from cdp_agentkit_core.actions.pyth.fetch_price_feed_id import PythFetchPriceFeedIDAction from cdp_agentkit_core.actions.register_basename import RegisterBasenameAction from cdp_agentkit_core.actions.request_faucet_funds import RequestFaucetFundsAction +from cdp_agentkit_core.actions.superfluid.create_flow import SuperfluidCreateFlowAction +from cdp_agentkit_core.actions.superfluid.delete_flow import SuperfluidDeleteFlowAction +from cdp_agentkit_core.actions.superfluid.update_flow import SuperfluidUpdateFlowAction from cdp_agentkit_core.actions.trade import TradeAction from cdp_agentkit_core.actions.transfer import TransferAction from cdp_agentkit_core.actions.transfer_nft import TransferNftAction @@ -50,4 +53,7 @@ def get_all_cdp_actions() -> list[type[CdpAction]]: "WrapEthAction", "PythFetchPriceFeedIDAction", "PythFetchPriceAction", + "SuperfluidCreateFlowAction", + "SuperfluidUpdateFlowAction", + "SuperfluidDeleteFlowAction", ] diff --git a/cdp-agentkit-core/python/cdp_agentkit_core/actions/superfluid/__init__.py b/cdp-agentkit-core/python/cdp_agentkit_core/actions/superfluid/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/cdp-agentkit-core/python/cdp_agentkit_core/actions/superfluid/constants.py b/cdp-agentkit-core/python/cdp_agentkit_core/actions/superfluid/constants.py new file mode 100644 index 000000000..f361b417a --- /dev/null +++ b/cdp-agentkit-core/python/cdp_agentkit_core/actions/superfluid/constants.py @@ -0,0 +1,58 @@ +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/cdp-agentkit-core/python/cdp_agentkit_core/actions/superfluid/create_flow.py b/cdp-agentkit-core/python/cdp_agentkit_core/actions/superfluid/create_flow.py new file mode 100644 index 000000000..afb855e36 --- /dev/null +++ b/cdp-agentkit-core/python/cdp_agentkit_core/actions/superfluid/create_flow.py @@ -0,0 +1,89 @@ +from collections.abc import Callable + +from cdp import Wallet +from pydantic import BaseModel, Field + +from cdp_agentkit_core.actions import CdpAction +from cdp_agentkit_core.actions.superfluid.constants import ( + CREATE_ABI, +) + +SUPERFLUID_CREATE_FLOW_PROMPT = """ +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 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 +- This is supported on the following networks: + - Base Sepolia (ie, 'base-sepolia') + - Base Mainnet (ie, 'base', 'base-mainnet') + - Ethereum Mainnet (ie, 'ethereum', 'ethereum-mainnet') + - Polygon Mainnet (ie, 'polygon', 'polygon-mainnet') + - Arbitrum Mainnet (ie, 'arbitrum', 'arbitrum-mainnet') + - Optimism, Celo, Scroll, Avalanche, Gnosis, BNB Smart Chain, Degen Chain, Avalance Fuji, Optimism Sepolia and Scroll Sepolia +""" + +class SuperfluidCreateFlowInput(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" + ) + +def superfluid_create_flow(wallet: Wallet, recipient: str, token_address: str, flow_rate: str) -> str: + """Create a money flow using Superfluid. + + Args: + wallet (Wallet): The wallet initiating the flow. + recipient (str): Recipient's wallet address. + token_address (str): Address of the token that will be streamed. + flow_rate (str): Rate of token flow in wei per second. + + Returns: + str: Confirmation of flow creation. + + """ + try: + invocation = wallet.invoke_contract( + contract_address="0xcfA132E353cB4E398080B9700609bb008eceB125", + abi=CREATE_ABI, + method="createFlow", + args={ + "token": token_address, + "sender": wallet.default_address.address_id, + "receiver": recipient, + "flowrate": flow_rate, + "userData": "0x" + }) + + invocation.wait() + + return f"Flow created successfully. Result: {invocation}" + + except Exception as e: + return f"Error creating flow: {e!s}" + +class SuperfluidCreateFlowAction(CdpAction): + """Create flow action.""" + + name: str = "superfluid_create_flow" + description: str = SUPERFLUID_CREATE_FLOW_PROMPT + args_schema: type[BaseModel] | None = SuperfluidCreateFlowInput + func: Callable[..., str] = superfluid_create_flow diff --git a/cdp-agentkit-core/python/cdp_agentkit_core/actions/superfluid/delete_flow.py b/cdp-agentkit-core/python/cdp_agentkit_core/actions/superfluid/delete_flow.py new file mode 100644 index 000000000..5f940fbd7 --- /dev/null +++ b/cdp-agentkit-core/python/cdp_agentkit_core/actions/superfluid/delete_flow.py @@ -0,0 +1,77 @@ +from collections.abc import Callable + +from cdp import Wallet +from pydantic import BaseModel, Field + +from cdp_agentkit_core.actions import CdpAction +from cdp_agentkit_core.actions.superfluid.constants import ( + DELETE_ABI, +) + +SUPERFLUID_DELETE_FLOW_PROMPT = """ +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 + +Important Notes: +- This is supported on the following networks: + - Base Sepolia (ie, 'base-sepolia') + - Base Mainnet (ie, 'base', 'base-mainnet') + - Ethereum Mainnet (ie, 'ethereum', 'ethereum-mainnet') + - Polygon Mainnet (ie, 'polygon', 'polygon-mainnet') + - Arbitrum Mainnet (ie, 'arbitrum', 'arbitrum-mainnet') + - Optimism, Celo, Scroll, Avalanche, Gnosis, BNB Smart Chain, Degen Chain, Avalance Fuji, Optimism Sepolia and Scroll Sepolia +""" + +class SuperfluidDeleteFlowInput(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" + ) + +def superfluid_delete_flow(wallet: Wallet, recipient: str, token_address: str) -> str: + """Delete an existing money flow using Superfluid. + + Args: + wallet (Wallet): The wallet closing the flow. + recipient (str): Recipient's wallet address. + token_address (str): Address of the token being streamed. + + Returns: + str: Confirmation of flow closure. + + """ + try: + invocation = wallet.invoke_contract( + contract_address="0xcfA132E353cB4E398080B9700609bb008eceB125", + abi=DELETE_ABI, + method="deleteFlow", + args={ + "token": token_address, + "sender": wallet.default_address.address_id, + "receiver": recipient, + "userData": "0x" + }) + + invocation.wait() + + return f"Flow deleted successfully. Result: {invocation}" + except Exception as e: + return f"Error deleting flow: {e!s}" + +class SuperfluidDeleteFlowAction(CdpAction): + """Delete flow action.""" + + name: str = "superfluid_delete_flow" + description: str = SUPERFLUID_DELETE_FLOW_PROMPT + args_schema: type[BaseModel] | None = SuperfluidDeleteFlowInput + func: Callable[..., str] = superfluid_delete_flow diff --git a/cdp-agentkit-core/python/cdp_agentkit_core/actions/superfluid/update_flow.py b/cdp-agentkit-core/python/cdp_agentkit_core/actions/superfluid/update_flow.py new file mode 100644 index 000000000..99ad622a8 --- /dev/null +++ b/cdp-agentkit-core/python/cdp_agentkit_core/actions/superfluid/update_flow.py @@ -0,0 +1,89 @@ +from collections.abc import Callable + +from cdp import Wallet +from pydantic import BaseModel, Field + +from cdp_agentkit_core.actions import CdpAction +from cdp_agentkit_core.actions.superfluid.constants import ( + UPDATE_ABI, +) + +SUPERFLUID_UPDATE_FLOW_PROMPT = """ +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 +- This is supported on the following networks: + - Base Sepolia (ie, 'base-sepolia') + - Base Mainnet (ie, 'base', 'base-mainnet') + - Ethereum Mainnet (ie, 'ethereum', 'ethereum-mainnet') + - Polygon Mainnet (ie, 'polygon', 'polygon-mainnet') + - Arbitrum Mainnet (ie, 'arbitrum', 'arbitrum-mainnet') + - Optimism, Celo, Scroll, Avalanche, Gnosis, BNB Smart Chain, Degen Chain, Avalance Fuji, Optimism Sepolia and Scroll Sepolia +""" + +class SuperfluidUpdateFlowInput(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" + ) + +def superfluid_update_flow(wallet: Wallet, recipient: str, token_address: str, new_flow_rate: str) -> str: + """Update an existing money flow using Superfluid. + + Args: + wallet (Wallet): The wallet initiating the update. + recipient (str): Recipient's wallet address. + token_address (str): Address of the token that is being streamed. + new_flow_rate (str): New rate of token flow in wei per second. + + Returns: + str: Confirmation of flow update. + + """ + try: + invocation = wallet.invoke_contract( + contract_address="0xcfA132E353cB4E398080B9700609bb008eceB125", + abi=UPDATE_ABI, + method="updateFlow", + args={ + "token": token_address, + "sender": wallet.default_address.address_id, + "receiver": recipient, + "flowrate": new_flow_rate, + "userData": "0x" + }) + + invocation.wait() + + return f"Flow updated successfully. Result: {invocation}" + + except Exception as e: + return f"Error updating flow: {e!s}" + +class SuperfluidUpdateFlowAction(CdpAction): + """Update flow action.""" + + name: str = "superfluid_update_flow" + description: str = SUPERFLUID_UPDATE_FLOW_PROMPT + args_schema: type[BaseModel] | None = SuperfluidUpdateFlowInput + func: Callable[..., str] = superfluid_update_flow diff --git a/cdp-agentkit-core/python/tests/actions/superfluid/test_create_flow.py b/cdp-agentkit-core/python/tests/actions/superfluid/test_create_flow.py new file mode 100644 index 000000000..076f051b9 --- /dev/null +++ b/cdp-agentkit-core/python/tests/actions/superfluid/test_create_flow.py @@ -0,0 +1,91 @@ +from unittest.mock import patch + +import pytest + +from cdp_agentkit_core.actions.superfluid.constants import CREATE_ABI +from cdp_agentkit_core.actions.superfluid.create_flow import ( + SuperfluidCreateFlowInput, + superfluid_create_flow, +) + +MOCK_RECIPIENT = "0xvalidRecipientAddress" +MOCK_TOKEN_ADDRESS = "0xvalidTokenAddress" +MOCK_FLOW_RATE = "1000000000000000" + + +def test_create_flow_input_model_valid(): + """Test that CreateFlowInput accepts valid parameters.""" + input_model = SuperfluidCreateFlowInput( + recipient=MOCK_RECIPIENT, + token_address=MOCK_TOKEN_ADDRESS, + flow_rate=MOCK_FLOW_RATE, + ) + + assert input_model.recipient == MOCK_RECIPIENT + assert input_model.token_address == MOCK_TOKEN_ADDRESS + assert input_model.flow_rate == MOCK_FLOW_RATE + + +def test_create_flow_input_model_missing_params(): + """Test that CreateFlowInput raises error when params are missing.""" + with pytest.raises(ValueError): + SuperfluidCreateFlowInput() + + +def test_create_flow_success(wallet_factory, contract_invocation_factory): + """Test successful flow creation with valid parameters.""" + mock_wallet = wallet_factory() + mock_contract_invocation = contract_invocation_factory() + + with ( + patch.object( + mock_wallet, "invoke_contract", return_value=mock_contract_invocation + ) as mock_invoke_contract, + patch.object( + mock_contract_invocation, "wait", return_value=mock_contract_invocation + ) as mock_contract_invocation_wait, + ): + action_response = superfluid_create_flow( + mock_wallet, MOCK_RECIPIENT, MOCK_TOKEN_ADDRESS, MOCK_FLOW_RATE + ) + + expected_response = f"Flow created successfully. Result: {mock_contract_invocation}" + assert action_response == expected_response + mock_invoke_contract.assert_called_once_with( + contract_address="0xcfA132E353cB4E398080B9700609bb008eceB125", + abi=CREATE_ABI, + method="createFlow", + args={ + "token": MOCK_TOKEN_ADDRESS, + "sender": mock_wallet.default_address.address_id, + "receiver": MOCK_RECIPIENT, + "flowrate": MOCK_FLOW_RATE, + "userData": "0x", + }, + ) + mock_contract_invocation_wait.assert_called_once_with() + + +def test_create_flow_api_error(wallet_factory): + """Test flow creation when API error occurs.""" + mock_wallet = wallet_factory() + + with patch.object(mock_wallet, "invoke_contract", side_effect=Exception("API error")) as mock_invoke_contract: + action_response = superfluid_create_flow( + mock_wallet, MOCK_RECIPIENT, MOCK_TOKEN_ADDRESS, MOCK_FLOW_RATE + ) + + expected_response = "Error creating flow: API error" + assert action_response == expected_response + mock_invoke_contract.assert_called_once_with( + contract_address="0xcfA132E353cB4E398080B9700609bb008eceB125", + abi=CREATE_ABI, + method="createFlow", + args={ + "token": MOCK_TOKEN_ADDRESS, + "sender": mock_wallet.default_address.address_id, + "receiver": MOCK_RECIPIENT, + "flowrate": MOCK_FLOW_RATE, + "userData": "0x", + }, + ) diff --git a/cdp-agentkit-core/python/tests/actions/superfluid/test_delete_flow.py b/cdp-agentkit-core/python/tests/actions/superfluid/test_delete_flow.py new file mode 100644 index 000000000..9b7965d1c --- /dev/null +++ b/cdp-agentkit-core/python/tests/actions/superfluid/test_delete_flow.py @@ -0,0 +1,86 @@ +from unittest.mock import patch + +import pytest + +from cdp_agentkit_core.actions.superfluid.constants import DELETE_ABI +from cdp_agentkit_core.actions.superfluid.delete_flow import ( + SuperfluidDeleteFlowInput, + superfluid_delete_flow, +) + +MOCK_RECIPIENT = "0xvalidRecipientAddress" +MOCK_TOKEN_ADDRESS = "0xvalidTokenAddress" + + +def test_delete_flow_input_model_valid(): + """Test that DeleteFlowInput accepts valid parameters.""" + input_model = SuperfluidDeleteFlowInput( + recipient=MOCK_RECIPIENT, + token_address=MOCK_TOKEN_ADDRESS, + ) + + assert input_model.recipient == MOCK_RECIPIENT + assert input_model.token_address == MOCK_TOKEN_ADDRESS + + +def test_delete_flow_input_model_missing_params(): + """Test that DeleteFlowInput raises error when params are missing.""" + with pytest.raises(ValueError): + SuperfluidDeleteFlowInput() + + +def test_delete_flow_success(wallet_factory, contract_invocation_factory): + """Test successful flow deletion with valid parameters.""" + mock_wallet = wallet_factory() + mock_contract_invocation = contract_invocation_factory() + + with ( + patch.object( + mock_wallet, "invoke_contract", return_value=mock_contract_invocation + ) as mock_invoke_contract, + patch.object( + mock_contract_invocation, "wait", return_value=mock_contract_invocation + ) as mock_contract_invocation_wait, + ): + action_response = superfluid_delete_flow( + mock_wallet, MOCK_RECIPIENT, MOCK_TOKEN_ADDRESS + ) + + expected_response = f"Flow deleted successfully. Result: {mock_contract_invocation}" + assert action_response == expected_response + mock_invoke_contract.assert_called_once_with( + contract_address="0xcfA132E353cB4E398080B9700609bb008eceB125", + abi=DELETE_ABI, + method="deleteFlow", + args={ + "token": MOCK_TOKEN_ADDRESS, + "sender": mock_wallet.default_address.address_id, + "receiver": MOCK_RECIPIENT, + "userData": "0x", + }, + ) + mock_contract_invocation_wait.assert_called_once_with() + + +def test_delete_flow_api_error(wallet_factory): + """Test flow deletion when API error occurs.""" + mock_wallet = wallet_factory() + + with patch.object(mock_wallet, "invoke_contract", side_effect=Exception("API error")) as mock_invoke_contract: + action_response = superfluid_delete_flow( + mock_wallet, MOCK_RECIPIENT, MOCK_TOKEN_ADDRESS + ) + + expected_response = "Error deleting flow: API error" + assert action_response == expected_response + mock_invoke_contract.assert_called_once_with( + contract_address="0xcfA132E353cB4E398080B9700609bb008eceB125", + abi=DELETE_ABI, + method="deleteFlow", + args={ + "token": MOCK_TOKEN_ADDRESS, + "sender": mock_wallet.default_address.address_id, + "receiver": MOCK_RECIPIENT, + "userData": "0x", + }, + ) diff --git a/cdp-agentkit-core/python/tests/actions/superfluid/test_update_flow.py b/cdp-agentkit-core/python/tests/actions/superfluid/test_update_flow.py new file mode 100644 index 000000000..28abd0a9a --- /dev/null +++ b/cdp-agentkit-core/python/tests/actions/superfluid/test_update_flow.py @@ -0,0 +1,91 @@ +from unittest.mock import patch + +import pytest + +from cdp_agentkit_core.actions.superfluid.constants import UPDATE_ABI +from cdp_agentkit_core.actions.superfluid.update_flow import ( + SuperfluidUpdateFlowInput, + superfluid_update_flow, +) + +MOCK_RECIPIENT = "0xvalidRecipientAddress" +MOCK_TOKEN_ADDRESS = "0xvalidTokenAddress" +MOCK_NEW_FLOW_RATE = "2000000000000000" + + +def test_update_flow_input_model_valid(): + """Test that UpdateFlowInput accepts valid parameters.""" + input_model = SuperfluidUpdateFlowInput( + recipient=MOCK_RECIPIENT, + token_address=MOCK_TOKEN_ADDRESS, + new_flow_rate=MOCK_NEW_FLOW_RATE, + ) + + assert input_model.recipient == MOCK_RECIPIENT + assert input_model.token_address == MOCK_TOKEN_ADDRESS + assert input_model.new_flow_rate == MOCK_NEW_FLOW_RATE + + +def test_update_flow_input_model_missing_params(): + """Test that UpdateFlowInput raises error when params are missing.""" + with pytest.raises(ValueError): + SuperfluidUpdateFlowInput() + + +def test_update_flow_success(wallet_factory, contract_invocation_factory): + """Test successful flow update with valid parameters.""" + mock_wallet = wallet_factory() + mock_contract_invocation = contract_invocation_factory() + + with ( + patch.object( + mock_wallet, "invoke_contract", return_value=mock_contract_invocation + ) as mock_invoke_contract, + patch.object( + mock_contract_invocation, "wait", return_value=mock_contract_invocation + ) as mock_contract_invocation_wait, + ): + action_response = superfluid_update_flow( + mock_wallet, MOCK_RECIPIENT, MOCK_TOKEN_ADDRESS, MOCK_NEW_FLOW_RATE + ) + + expected_response = f"Flow updated successfully. Result: {mock_contract_invocation}" + assert action_response == expected_response + mock_invoke_contract.assert_called_once_with( + contract_address="0xcfA132E353cB4E398080B9700609bb008eceB125", + abi=UPDATE_ABI, + method="updateFlow", + args={ + "token": MOCK_TOKEN_ADDRESS, + "sender": mock_wallet.default_address.address_id, + "receiver": MOCK_RECIPIENT, + "flowrate": MOCK_NEW_FLOW_RATE, + "userData": "0x", + }, + ) + mock_contract_invocation_wait.assert_called_once_with() + + +def test_update_flow_api_error(wallet_factory): + """Test flow update when API error occurs.""" + mock_wallet = wallet_factory() + + with patch.object(mock_wallet, "invoke_contract", side_effect=Exception("API error")) as mock_invoke_contract: + action_response = superfluid_update_flow( + mock_wallet, MOCK_RECIPIENT, MOCK_TOKEN_ADDRESS, MOCK_NEW_FLOW_RATE + ) + + expected_response = "Error updating flow: API error" + assert action_response == expected_response + mock_invoke_contract.assert_called_once_with( + contract_address="0xcfA132E353cB4E398080B9700609bb008eceB125", + abi=UPDATE_ABI, + method="updateFlow", + args={ + "token": MOCK_TOKEN_ADDRESS, + "sender": mock_wallet.default_address.address_id, + "receiver": MOCK_RECIPIENT, + "flowrate": MOCK_NEW_FLOW_RATE, + "userData": "0x", + }, + )