diff --git a/.gitignore b/.gitignore index 2dc50723d..d10ae7029 100644 --- a/.gitignore +++ b/.gitignore @@ -40,5 +40,6 @@ # Environment Configurations **/env/ **/.env/ +**/.env **/.env.local/ **/.env.test/ diff --git a/cdp-agentkit-core/CHANGELOG.md b/cdp-agentkit-core/CHANGELOG.md index d6c33f8bb..fa37898ec 100644 --- a/cdp-agentkit-core/CHANGELOG.md +++ b/cdp-agentkit-core/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Added + +- Added `uniswap_v3_create_pool` 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 387bb739e..9a5cbf53b 100644 --- a/cdp-agentkit-core/cdp_agentkit_core/actions/__init__.py +++ b/cdp-agentkit-core/cdp_agentkit_core/actions/__init__.py @@ -43,8 +43,16 @@ TransferInput, transfer, ) +from cdp_agentkit_core.actions.uniswap_v3.create_pool import ( + UNISWAP_V3_CREATE_POOL_PROMPT, + UniswapV3CreatePoolInput, + uniswap_v3_create_pool, +) __all__ = [ + "UNISWAP_V3_CREATE_POOL_PROMPT", + "UniswapV3CreatePoolInput", + "uniswap_v3_create_pool", "DEPLOY_NFT_PROMPT", "DeployNftInput", "deploy_nft", diff --git a/cdp-agentkit-core/cdp_agentkit_core/actions/uniswap_v3/__init__.py b/cdp-agentkit-core/cdp_agentkit_core/actions/uniswap_v3/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/cdp-agentkit-core/cdp_agentkit_core/actions/uniswap_v3/constants.py b/cdp-agentkit-core/cdp_agentkit_core/actions/uniswap_v3/constants.py new file mode 100644 index 000000000..4463a4c27 --- /dev/null +++ b/cdp-agentkit-core/cdp_agentkit_core/actions/uniswap_v3/constants.py @@ -0,0 +1,130 @@ +UNISWAP_V3_FACTORY_ABI = [ + {"inputs": [], "stateMutability": "nonpayable", "type": "constructor"}, + { + "anonymous": False, + "inputs": [ + {"indexed": True, "internalType": "uint24", "name": "fee", "type": "uint24"}, + {"indexed": True, "internalType": "int24", "name": "tickSpacing", "type": "int24"}, + ], + "name": "FeeAmountEnabled", + "type": "event", + }, + { + "anonymous": False, + "inputs": [ + {"indexed": True, "internalType": "address", "name": "oldOwner", "type": "address"}, + {"indexed": True, "internalType": "address", "name": "newOwner", "type": "address"}, + ], + "name": "OwnerChanged", + "type": "event", + }, + { + "anonymous": False, + "inputs": [ + {"indexed": True, "internalType": "address", "name": "token0", "type": "address"}, + {"indexed": True, "internalType": "address", "name": "token1", "type": "address"}, + {"indexed": True, "internalType": "uint24", "name": "fee", "type": "uint24"}, + {"indexed": False, "internalType": "int24", "name": "tickSpacing", "type": "int24"}, + {"indexed": False, "internalType": "address", "name": "pool", "type": "address"}, + ], + "name": "PoolCreated", + "type": "event", + }, + { + "inputs": [ + {"internalType": "address", "name": "tokenA", "type": "address"}, + {"internalType": "address", "name": "tokenB", "type": "address"}, + {"internalType": "uint24", "name": "fee", "type": "uint24"}, + ], + "name": "createPool", + "outputs": [{"internalType": "address", "name": "pool", "type": "address"}], + "stateMutability": "nonpayable", + "type": "function", + }, + { + "inputs": [ + {"internalType": "uint24", "name": "fee", "type": "uint24"}, + {"internalType": "int24", "name": "tickSpacing", "type": "int24"}, + ], + "name": "enableFeeAmount", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function", + }, + { + "inputs": [{"internalType": "uint24", "name": "", "type": "uint24"}], + "name": "feeAmountTickSpacing", + "outputs": [{"internalType": "int24", "name": "", "type": "int24"}], + "stateMutability": "view", + "type": "function", + }, + { + "inputs": [ + {"internalType": "address", "name": "", "type": "address"}, + {"internalType": "address", "name": "", "type": "address"}, + {"internalType": "uint24", "name": "", "type": "uint24"}, + ], + "name": "getPool", + "outputs": [{"internalType": "address", "name": "", "type": "address"}], + "stateMutability": "view", + "type": "function", + }, + { + "inputs": [], + "name": "owner", + "outputs": [{"internalType": "address", "name": "", "type": "address"}], + "stateMutability": "view", + "type": "function", + }, + { + "inputs": [], + "name": "parameters", + "outputs": [ + {"internalType": "address", "name": "factory", "type": "address"}, + {"internalType": "address", "name": "token0", "type": "address"}, + {"internalType": "address", "name": "token1", "type": "address"}, + {"internalType": "uint24", "name": "fee", "type": "uint24"}, + {"internalType": "int24", "name": "tickSpacing", "type": "int24"}, + ], + "stateMutability": "view", + "type": "function", + }, + { + "inputs": [{"internalType": "address", "name": "_owner", "type": "address"}], + "name": "setOwner", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function", + }, +] + +UNISWAP_V3_FACTORY_CONTRACT_ADDRESSES = { + "base-sepolia": "0x4752ba5DBc23f44D87826276BF6Fd6b1C372aD24", + "base-mainnet": "0x33128a8fC17869897dcE68Ed026d694621f6FDfD", + "ethereum-mainnet": "0x1F98431c8aD98523631AE4a59f267346ea31F984", + "arbitrum-mainnet": "0x1F98431c8aD98523631AE4a59f267346ea31F984", + "polygon-mainnet": "0x1F98431c8aD98523631AE4a59f267346ea31F984", +} + + +def get_contract_address(network: str) -> str: + """Get the Uniswap V3 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, ethereum-mainnet, + arbitrum-mainnet, polygon-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 UNISWAP_V3_FACTORY_CONTRACT_ADDRESSES: + raise ValueError( + f"Invalid network: {network}. Valid networks are: {', '.join(UNISWAP_V3_FACTORY_CONTRACT_ADDRESSES.keys())}" + ) + return UNISWAP_V3_FACTORY_CONTRACT_ADDRESSES[network] diff --git a/cdp-agentkit-core/cdp_agentkit_core/actions/uniswap_v3/create_pool.py b/cdp-agentkit-core/cdp_agentkit_core/actions/uniswap_v3/create_pool.py new file mode 100644 index 000000000..2490d5906 --- /dev/null +++ b/cdp-agentkit-core/cdp_agentkit_core/actions/uniswap_v3/create_pool.py @@ -0,0 +1,55 @@ +from cdp import Wallet +from pydantic import BaseModel, Field + +from cdp_agentkit_core.actions.uniswap_v3.constants import ( + UNISWAP_V3_FACTORY_ABI, + get_contract_address, +) + +UNISWAP_V3_CREATE_POOL_PROMPT = """ +This tool will create a Uniswap v3 pool for trading 2 tokens, one of which can be the native gas token. For native gas token, use the address 0x4200000000000000000000000000000000000006, and for ERC20 token, use its contract address. This tool takes the address of the first token, address of the second token, and the fee to charge for trades as inputs. The fee is denominated in hundredths of a bip (i.e. 1e-6) and must be passed a string. Acceptable fee values are 100, 500, 3000, and 10000. Supported networks are Base Sepolia, Base Mainnet, Ethereum Mainnet, Polygon Mainnet, and Arbitrum Mainnet.""" + + +class UniswapV3CreatePoolInput(BaseModel): + """Input argument schema for create pool action.""" + + token_a: str = Field( + ..., + description="The address of the first token to trade, e.g. 0x4200000000000000000000000000000000000006 for native gas token", + ) + token_b: str = Field( + ..., + description="The address of the second token to trade, e.g. 0x1234567890123456789012345678901234567890 for ERC20 token", + ) + fee: str = Field( + ..., + description="The fee to charge for trades, denominated in hundredths of a bip (i.e. 1e-6). Acceptable fee values are 100, 500, 3000, and 10000.", + ) + + +def uniswap_v3_create_pool(wallet: Wallet, token_a: str, token_b: str, fee: str) -> str: + """Create a Uniswap v3 pool for trading 2 tokens, one of which can be the native gas token. + + Args: + wallet (Wallet): The wallet to create the pool from. + token_a (str): The address of the first token to trade, e.g. 0x4200000000000000000000000000000000000006 for native gas token + token_b (str): The address of the second token to trade, e.g. 0x1234567890123456789012345678901234567890 for ERC20 token + fee (str): The fee to charge for trades, denominated in hundredths of a bip (i.e. 1e-6). + + Returns: + str: A message containing the pool creation details. + + """ + factory_address = get_contract_address(wallet.network_id) + + pool = wallet.invoke_contract( + contract_address=factory_address, + method="createPool", + abi=UNISWAP_V3_FACTORY_ABI, + args={ + "tokenA": token_a, + "tokenB": token_b, + "fee": fee, + }, + ).wait() + return f"Created pool for {token_a} and {token_b} with fee {fee} on network {wallet.network_id}.\nTransaction hash for the pool creation: {pool.transaction.transaction_hash}\nTransaction link for the pool creation: {pool.transaction.transaction_link}" diff --git a/cdp-agentkit-core/tests/actions/uniswap_v3/__init__.py b/cdp-agentkit-core/tests/actions/uniswap_v3/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/cdp-agentkit-core/tests/actions/uniswap_v3/test_create_pool.py b/cdp-agentkit-core/tests/actions/uniswap_v3/test_create_pool.py new file mode 100644 index 000000000..d57f0c1f2 --- /dev/null +++ b/cdp-agentkit-core/tests/actions/uniswap_v3/test_create_pool.py @@ -0,0 +1,85 @@ +from unittest.mock import patch + +import pytest + +from cdp_agentkit_core.actions.uniswap_v3.constants import UNISWAP_V3_FACTORY_ABI +from cdp_agentkit_core.actions.uniswap_v3.create_pool import ( + UniswapV3CreatePoolInput, + uniswap_v3_create_pool, +) + +MOCK_TOKEN_A = "0x4200000000000000000000000000000000000006" +MOCK_TOKEN_B = "0x1234567890123456789012345678901234567890" +MOCK_FEE = "3000" + + +def test_create_pool_input_model_valid(): + """Test that CreatePoolInput accepts valid parameters.""" + input_model = UniswapV3CreatePoolInput( + token_a=MOCK_TOKEN_A, + token_b=MOCK_TOKEN_B, + fee=MOCK_FEE, + ) + + assert input_model.token_a == MOCK_TOKEN_A + assert input_model.token_b == MOCK_TOKEN_B + assert input_model.fee == MOCK_FEE + + +def test_create_pool_input_model_missing_params(): + """Test that CreatePoolInput raises error when params are missing.""" + with pytest.raises(ValueError): + UniswapV3CreatePoolInput() + + +def test_create_pool_success(wallet_factory, contract_invocation_factory): + """Test successful pool creation with valid parameters.""" + mock_wallet = wallet_factory() + mock_contract_instance = contract_invocation_factory() + + 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 = uniswap_v3_create_pool(mock_wallet, MOCK_TOKEN_A, MOCK_TOKEN_B, MOCK_FEE) + + expected_response = f"Created pool for {MOCK_TOKEN_A} and {MOCK_TOKEN_B} with fee {MOCK_FEE} on network {mock_wallet.network_id}.\nTransaction hash for the pool creation: {mock_contract_instance.transaction.transaction_hash}\nTransaction link for the pool creation: {mock_contract_instance.transaction.transaction_link}" + assert action_response == expected_response + + mock_invoke.assert_called_once_with( + contract_address="0x4752ba5DBc23f44D87826276BF6Fd6b1C372aD24", + method="createPool", + abi=UNISWAP_V3_FACTORY_ABI, + args={ + "tokenA": MOCK_TOKEN_A, + "tokenB": MOCK_TOKEN_B, + "fee": MOCK_FEE, + }, + ) + mock_contract_wait.assert_called_once_with() + + +def test_create_pool_api_error(wallet_factory): + """Test create_pool when API error occurs.""" + mock_wallet = wallet_factory() + + with patch.object( + mock_wallet, "invoke_contract", side_effect=Exception("API error") + ) as mock_invoke: + with pytest.raises(Exception, match="API error"): + uniswap_v3_create_pool(mock_wallet, MOCK_TOKEN_A, MOCK_TOKEN_B, MOCK_FEE) + + mock_invoke.assert_called_once_with( + contract_address="0x4752ba5DBc23f44D87826276BF6Fd6b1C372aD24", + method="createPool", + abi=UNISWAP_V3_FACTORY_ABI, + args={ + "tokenA": MOCK_TOKEN_A, + "tokenB": MOCK_TOKEN_B, + "fee": MOCK_FEE, + }, + ) diff --git a/cdp-langchain/CHANGELOG.md b/cdp-langchain/CHANGELOG.md index 3c003e86b..6ce14dc14 100644 --- a/cdp-langchain/CHANGELOG.md +++ b/cdp-langchain/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Added + +- Added `uniswap_v3_create_pool` 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 1cb19e08c..85c3a6fd0 100644 --- a/cdp-langchain/cdp_langchain/agent_toolkits/cdp_toolkit.py +++ b/cdp-langchain/cdp_langchain/agent_toolkits/cdp_toolkit.py @@ -13,6 +13,7 @@ REQUEST_FAUCET_FUNDS_PROMPT, TRADE_PROMPT, TRANSFER_PROMPT, + UNISWAP_V3_CREATE_POOL_PROMPT, DeployNftInput, DeployTokenInput, GetBalanceInput, @@ -22,6 +23,7 @@ RequestFaucetFundsInput, TradeInput, TransferInput, + UniswapV3CreatePoolInput, ) from cdp_langchain.tools import CdpAction from cdp_langchain.utils import CdpAgentkitWrapper @@ -79,6 +81,7 @@ class CdpToolkit(BaseToolkit): mint_nft deploy_nft register_basename + uniswap_v3_create_pool Use within an agent: .. code-block:: python @@ -141,6 +144,12 @@ def from_cdp_agentkit_wrapper(cls, cdp_agentkit_wrapper: CdpAgentkitWrapper) -> """ actions: list[dict] = [ + { + "mode": "uniswap_v3_create_pool", + "name": "uniswap_v3_create_pool", + "description": UNISWAP_V3_CREATE_POOL_PROMPT, + "args_schema": UniswapV3CreatePoolInput, + }, { "mode": "get_wallet_details", "name": "get_wallet_details", diff --git a/cdp-langchain/cdp_langchain/utils/cdp_agentkit_wrapper.py b/cdp-langchain/cdp_langchain/utils/cdp_agentkit_wrapper.py index 348581bfc..40047e381 100644 --- a/cdp-langchain/cdp_langchain/utils/cdp_agentkit_wrapper.py +++ b/cdp-langchain/cdp_langchain/utils/cdp_agentkit_wrapper.py @@ -16,6 +16,7 @@ request_faucet_funds, trade, transfer, + uniswap_v3_create_pool, ) @@ -70,6 +71,20 @@ def export_wallet(self) -> dict[str, str]: wallet_data_dict = self.wallet.export_data().to_dict() return json.dumps(wallet_data_dict) + def uniswap_v3_create_pool_wrapper(self, token_a: str, token_b: str, fee: str) -> str: + """Create a Uniswap v3 pool for the wallet by wrapping call to CDP Agentkit Core. + + Args: + token_a (str): The contract address of the first token in the pool. + token_b (str): The contract address of the second token in the pool. + fee (str): The fee for the pool. + + Returns: + str: A message containing the pool details. + + """ + return uniswap_v3_create_pool(wallet=self.wallet, token_a=token_a, token_b=token_b, fee=fee) + def get_wallet_details_wrapper(self) -> str: """Get details about the MPC Wallet by wrapping call to CDP Agentkit Core.""" return get_wallet_details(self.wallet) @@ -212,6 +227,8 @@ def run(self, mode: str, **kwargs) -> str: return self.transfer_wrapper(**kwargs) elif mode == "trade": return self.trade_wrapper(**kwargs) + elif mode == "uniswap_v3_create_pool": + return self.uniswap_v3_create_pool_wrapper(**kwargs) elif mode == "deploy_token": return self.deploy_token_wrapper(**kwargs) elif mode == "mint_nft": @@ -221,4 +238,4 @@ def run(self, mode: str, **kwargs) -> str: elif mode == "register_basename": return self.register_basename_wrapper(**kwargs) else: - raise ValueError("Invalid mode" + mode) + raise ValueError("Invalid mode: " + mode)