diff --git a/cdp-agentkit-core/CHANGELOG.md b/cdp-agentkit-core/CHANGELOG.md index 7466d3587..b39d18747 100644 --- a/cdp-agentkit-core/CHANGELOG.md +++ b/cdp-agentkit-core/CHANGELOG.md @@ -4,6 +4,8 @@ ### Added +- Added `wow_create_token` action. + ## [0.0.1] - 2024-11-04 ### Added diff --git a/cdp-agentkit-core/cdp_agentkit_core/actions/__init__.py b/cdp-agentkit-core/cdp_agentkit_core/actions/__init__.py index f49029b8d..e2ae304a2 100644 --- a/cdp-agentkit-core/cdp_agentkit_core/actions/__init__.py +++ b/cdp-agentkit-core/cdp_agentkit_core/actions/__init__.py @@ -8,6 +8,7 @@ from cdp_agentkit_core.actions.request_faucet_funds import RequestFaucetFundsAction from cdp_agentkit_core.actions.trade import TradeAction from cdp_agentkit_core.actions.transfer import TransferAction +from cdp_agentkit_core.actions.wow.create_token import WowCreateTokenAction # WARNING: All new CdpAction subclasses must be imported above, otherwise they will not be discovered @@ -33,5 +34,6 @@ def get_all_cdp_actions() -> list[type[CdpAction]]: "RequestFaucetFundsAction", "TradeAction", "TransferAction", + "WowCreateTokenAction", "CDP_ACTIONS", ] diff --git a/cdp-agentkit-core/cdp_agentkit_core/actions/wow/__init__.py b/cdp-agentkit-core/cdp_agentkit_core/actions/wow/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/cdp-agentkit-core/cdp_agentkit_core/actions/wow/constants.py b/cdp-agentkit-core/cdp_agentkit_core/actions/wow/constants.py new file mode 100644 index 000000000..1f76de4f8 --- /dev/null +++ b/cdp-agentkit-core/cdp_agentkit_core/actions/wow/constants.py @@ -0,0 +1,193 @@ +WOW_FACTORY_ABI = [ + { + "type": "constructor", + "inputs": [ + {"name": "_tokenImplementation", "type": "address", "internalType": "address"}, + {"name": "_bondingCurve", "type": "address", "internalType": "address"}, + ], + "stateMutability": "nonpayable", + }, + { + "type": "function", + "name": "UPGRADE_INTERFACE_VERSION", + "inputs": [], + "outputs": [{"name": "", "type": "string", "internalType": "string"}], + "stateMutability": "view", + }, + { + "type": "function", + "name": "bondingCurve", + "inputs": [], + "outputs": [{"name": "", "type": "address", "internalType": "address"}], + "stateMutability": "view", + }, + { + "type": "function", + "name": "deploy", + "inputs": [ + {"name": "_tokenCreator", "type": "address", "internalType": "address"}, + {"name": "_platformReferrer", "type": "address", "internalType": "address"}, + {"name": "_tokenURI", "type": "string", "internalType": "string"}, + {"name": "_name", "type": "string", "internalType": "string"}, + {"name": "_symbol", "type": "string", "internalType": "string"}, + ], + "outputs": [{"name": "", "type": "address", "internalType": "address"}], + "stateMutability": "payable", + }, + { + "type": "function", + "name": "implementation", + "inputs": [], + "outputs": [{"name": "", "type": "address", "internalType": "address"}], + "stateMutability": "view", + }, + { + "type": "function", + "name": "initialize", + "inputs": [{"name": "_owner", "type": "address", "internalType": "address"}], + "outputs": [], + "stateMutability": "nonpayable", + }, + { + "type": "function", + "name": "owner", + "inputs": [], + "outputs": [{"name": "", "type": "address", "internalType": "address"}], + "stateMutability": "view", + }, + { + "type": "function", + "name": "proxiableUUID", + "inputs": [], + "outputs": [{"name": "", "type": "bytes32", "internalType": "bytes32"}], + "stateMutability": "view", + }, + { + "type": "function", + "name": "renounceOwnership", + "inputs": [], + "outputs": [], + "stateMutability": "nonpayable", + }, + { + "type": "function", + "name": "tokenImplementation", + "inputs": [], + "outputs": [{"name": "", "type": "address", "internalType": "address"}], + "stateMutability": "view", + }, + { + "type": "function", + "name": "transferOwnership", + "inputs": [{"name": "newOwner", "type": "address", "internalType": "address"}], + "outputs": [], + "stateMutability": "nonpayable", + }, + { + "type": "function", + "name": "upgradeToAndCall", + "inputs": [ + {"name": "newImplementation", "type": "address", "internalType": "address"}, + {"name": "data", "type": "bytes", "internalType": "bytes"}, + ], + "outputs": [], + "stateMutability": "payable", + }, + { + "type": "event", + "name": "Initialized", + "inputs": [ + {"name": "version", "type": "uint64", "indexed": False, "internalType": "uint64"} + ], + "anonymous": False, + }, + { + "type": "event", + "name": "OwnershipTransferred", + "inputs": [ + { + "name": "previousOwner", + "type": "address", + "indexed": True, + "internalType": "address", + }, + {"name": "newOwner", "type": "address", "indexed": True, "internalType": "address"}, + ], + "anonymous": False, + }, + { + "type": "event", + "name": "Upgraded", + "inputs": [ + { + "name": "implementation", + "type": "address", + "indexed": True, + "internalType": "address", + } + ], + "anonymous": False, + }, + { + "type": "error", + "name": "AddressEmptyCode", + "inputs": [{"name": "target", "type": "address", "internalType": "address"}], + }, + {"type": "error", "name": "ERC1167FailedCreateClone", "inputs": []}, + { + "type": "error", + "name": "ERC1967InvalidImplementation", + "inputs": [{"name": "implementation", "type": "address", "internalType": "address"}], + }, + {"type": "error", "name": "ERC1967NonPayable", "inputs": []}, + {"type": "error", "name": "FailedInnerCall", "inputs": []}, + {"type": "error", "name": "InvalidInitialization", "inputs": []}, + {"type": "error", "name": "NotInitializing", "inputs": []}, + { + "type": "error", + "name": "OwnableInvalidOwner", + "inputs": [{"name": "owner", "type": "address", "internalType": "address"}], + }, + { + "type": "error", + "name": "OwnableUnauthorizedAccount", + "inputs": [{"name": "account", "type": "address", "internalType": "address"}], + }, + {"type": "error", "name": "ReentrancyGuardReentrantCall", "inputs": []}, + {"type": "error", "name": "UUPSUnauthorizedCallContext", "inputs": []}, + { + "type": "error", + "name": "UUPSUnsupportedProxiableUUID", + "inputs": [{"name": "slot", "type": "bytes32", "internalType": "bytes32"}], + }, +] + +WOW_FACTORY_CONTRACT_ADDRESSES = { + "base-sepolia": "0x04870e22fa217Cb16aa00501D7D5253B8838C1eA", + "base-mainnet": "0x997020E5F59cCB79C74D527Be492Cc610CB9fA2B", +} + + +def get_factory_address(network: str) -> str: + """Get the Zora Wow ERC20 Factory contract address for the specified network. + + Args: + network (str): The network ID to get the contract address for. + Valid networks are: base-sepolia, base-mainnet. + + Returns: + str: The contract address for the specified network. + + Raises: + ValueError: If the specified network is not supported. + + """ + network = network.lower() + if network not in WOW_FACTORY_CONTRACT_ADDRESSES: + raise ValueError( + f"Invalid network: {network}. Valid networks are: {', '.join(WOW_FACTORY_CONTRACT_ADDRESSES.keys())}" + ) + return WOW_FACTORY_CONTRACT_ADDRESSES[network] + + +GENERIC_TOKEN_METADATA_URI = "ipfs://QmY1GqprFYvojCcUEKgqHeDj9uhZD9jmYGrQTfA9vAE78J" diff --git a/cdp-agentkit-core/cdp_agentkit_core/actions/wow/create_token.py b/cdp-agentkit-core/cdp_agentkit_core/actions/wow/create_token.py new file mode 100644 index 000000000..28403e038 --- /dev/null +++ b/cdp-agentkit-core/cdp_agentkit_core/actions/wow/create_token.py @@ -0,0 +1,67 @@ +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.wow.constants import ( + GENERIC_TOKEN_METADATA_URI, + WOW_FACTORY_ABI, + get_factory_address, +) + +WOW_CREATE_TOKEN_PROMPT = """ +This tool will create a Zora Wow ERC20 memecoin using the WoW factory. This tool takes the token name and token symbol. It uses a bonding curve so there is no need to add liquidity to the pool upfront. It is only supported on Base Sepolia and Base Mainnet. +""" + + +class WowCreateTokenInput(BaseModel): + """Input argument schema for create token action.""" + + name: str = Field( + ..., + description="The name of the token to create, e.g. WowCoin", + ) + symbol: str = Field( + ..., + description="The symbol of the token to create, e.g. WOW", + ) + + +def wow_create_token(wallet: Wallet, name: str, symbol: str) -> str: + """Create a Zora Wow ERC20 memecoin. + + Args: + wallet (Wallet): The wallet to create the token from. + name (str): The name of the token to create. + symbol (str): The symbol of the token to create. + + Returns: + str: A message containing the token creation details. + + """ + factory_address = get_factory_address(wallet.network_id) + + invocation = wallet.invoke_contract( + contract_address=factory_address, + method="deploy", + abi=WOW_FACTORY_ABI, + args={ + "_tokenCreator": wallet.default_address.address_id, + "_platformReferrer": "0x0000000000000000000000000000000000000000", + "_tokenURI": GENERIC_TOKEN_METADATA_URI, + "_name": name, + "_symbol": symbol, + }, + ).wait() + + return f"Created WoW ERC20 memecoin {name} with symbol {symbol} on network {wallet.network_id}.\nTransaction hash for the token creation: {invocation.transaction.transaction_hash}\nTransaction link for the token creation: {invocation.transaction.transaction_link}" + + +class WowCreateTokenAction(CdpAction): + """Zora Wow create token action.""" + + name: str = "wow_create_token" + description: str = WOW_CREATE_TOKEN_PROMPT + args_schema: type[BaseModel] | None = WowCreateTokenInput + func: Callable[..., str] = wow_create_token diff --git a/cdp-agentkit-core/tests/actions/wow/test_create_token.py b/cdp-agentkit-core/tests/actions/wow/test_create_token.py new file mode 100644 index 000000000..e8b8f8d46 --- /dev/null +++ b/cdp-agentkit-core/tests/actions/wow/test_create_token.py @@ -0,0 +1,104 @@ +from unittest.mock import patch + +import pytest + +from cdp_agentkit_core.actions.wow.constants import ( + GENERIC_TOKEN_METADATA_URI, + WOW_FACTORY_ABI, + get_factory_address, +) +from cdp_agentkit_core.actions.wow.create_token import ( + WowCreateTokenInput, + wow_create_token, +) + +MOCK_NAME = "Test Token" +MOCK_SYMBOL = "TEST" +MOCK_NETWORK_ID = "base-sepolia" +MOCK_WALLET_ADDRESS = "0x1234567890123456789012345678901234567890" + + +def test_create_token_input_model_valid(): + """Test that CreateTokenInput accepts valid parameters.""" + input_model = WowCreateTokenInput( + name=MOCK_NAME, + symbol=MOCK_SYMBOL, + ) + + assert input_model.name == MOCK_NAME + assert input_model.symbol == MOCK_SYMBOL + + +def test_create_token_input_model_missing_params(): + """Test that CreateTokenInput raises error when params are missing.""" + with pytest.raises(ValueError): + WowCreateTokenInput() + + +def test_create_token_success(wallet_factory, contract_invocation_factory): + """Test successful token creation with valid parameters.""" + mock_wallet = wallet_factory() + mock_contract_instance = contract_invocation_factory() + mock_wallet.default_address.address_id = MOCK_WALLET_ADDRESS + mock_wallet.network_id = MOCK_NETWORK_ID + + with ( + patch.object( + mock_wallet, "invoke_contract", return_value=mock_contract_instance + ) as mock_invoke, + patch.object( + mock_contract_instance, "wait", return_value=mock_contract_instance + ) as mock_contract_wait, + ): + action_response = wow_create_token( + mock_wallet, + MOCK_NAME, + MOCK_SYMBOL, + ) + + expected_response = f"Created WoW ERC20 memecoin {MOCK_NAME} with symbol {MOCK_SYMBOL} on network {MOCK_NETWORK_ID}.\nTransaction hash for the token creation: {mock_contract_instance.transaction.transaction_hash}\nTransaction link for the token creation: {mock_contract_instance.transaction.transaction_link}" + assert action_response == expected_response + + mock_invoke.assert_called_once_with( + contract_address=get_factory_address(MOCK_NETWORK_ID), + method="deploy", + abi=WOW_FACTORY_ABI, + args={ + "_tokenCreator": MOCK_WALLET_ADDRESS, + "_platformReferrer": "0x0000000000000000000000000000000000000000", + "_tokenURI": GENERIC_TOKEN_METADATA_URI, + "_name": MOCK_NAME, + "_symbol": MOCK_SYMBOL, + }, + ) + mock_contract_wait.assert_called_once_with() + + +def test_create_token_api_error(wallet_factory): + """Test create_token when API error occurs.""" + mock_wallet = wallet_factory() + mock_wallet.default_address.address_id = MOCK_WALLET_ADDRESS + mock_wallet.network_id = MOCK_NETWORK_ID + + with patch.object( + mock_wallet, "invoke_contract", side_effect=Exception("API error") + ) as mock_invoke: + with pytest.raises(Exception, match="API error"): + wow_create_token( + mock_wallet, + MOCK_NAME, + MOCK_SYMBOL, + ) + + mock_invoke.assert_called_once_with( + contract_address=get_factory_address(MOCK_NETWORK_ID), + method="deploy", + abi=WOW_FACTORY_ABI, + args={ + "_tokenCreator": MOCK_WALLET_ADDRESS, + "_platformReferrer": "0x0000000000000000000000000000000000000000", + "_tokenURI": GENERIC_TOKEN_METADATA_URI, + "_name": MOCK_NAME, + "_symbol": MOCK_SYMBOL, + }, + ) diff --git a/cdp-langchain/CHANGELOG.md b/cdp-langchain/CHANGELOG.md index ba11e9607..9f8cb2a98 100644 --- a/cdp-langchain/CHANGELOG.md +++ b/cdp-langchain/CHANGELOG.md @@ -4,6 +4,8 @@ ### Added +- Added `wow_create_token` action to the cdp toolkit. + ## [0.0.1] - 2024-11-04 ### Added diff --git a/cdp-langchain/cdp_langchain/agent_toolkits/cdp_toolkit.py b/cdp-langchain/cdp_langchain/agent_toolkits/cdp_toolkit.py index 7fa7493b3..3737da1a1 100644 --- a/cdp-langchain/cdp_langchain/agent_toolkits/cdp_toolkit.py +++ b/cdp-langchain/cdp_langchain/agent_toolkits/cdp_toolkit.py @@ -60,6 +60,7 @@ class CdpToolkit(BaseToolkit): mint_nft deploy_nft register_basename + wow_create_token Use within an agent: .. code-block:: python