Skip to content

Commit

Permalink
Added support for creating a Uniswap V3 liquidity pool (#5)
Browse files Browse the repository at this point in the history
  • Loading branch information
rohan-agarwal-coinbase authored Nov 5, 2024
1 parent 9d1de43 commit ac3fafc
Show file tree
Hide file tree
Showing 11 changed files with 314 additions and 1 deletion.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,5 +40,6 @@
# Environment Configurations
**/env/
**/.env/
**/.env
**/.env.local/
**/.env.test/
4 changes: 4 additions & 0 deletions cdp-agentkit-core/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

### Added

- Added `uniswap_v3_create_pool` action.

## [0.0.1] - 2024-11-04

### Added
Expand Down
8 changes: 8 additions & 0 deletions cdp-agentkit-core/cdp_agentkit_core/actions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Empty file.
130 changes: 130 additions & 0 deletions cdp-agentkit-core/cdp_agentkit_core/actions/uniswap_v3/constants.py
Original file line number Diff line number Diff line change
@@ -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]
Original file line number Diff line number Diff line change
@@ -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}"
Empty file.
85 changes: 85 additions & 0 deletions cdp-agentkit-core/tests/actions/uniswap_v3/test_create_pool.py
Original file line number Diff line number Diff line change
@@ -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,
},
)
4 changes: 4 additions & 0 deletions cdp-langchain/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

### Added

- Added `uniswap_v3_create_pool` action to the cdp toolkit.

## [0.0.1] - 2024-11-04

### Added
Expand Down
9 changes: 9 additions & 0 deletions cdp-langchain/cdp_langchain/agent_toolkits/cdp_toolkit.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
REQUEST_FAUCET_FUNDS_PROMPT,
TRADE_PROMPT,
TRANSFER_PROMPT,
UNISWAP_V3_CREATE_POOL_PROMPT,
DeployNftInput,
DeployTokenInput,
GetBalanceInput,
Expand All @@ -22,6 +23,7 @@
RequestFaucetFundsInput,
TradeInput,
TransferInput,
UniswapV3CreatePoolInput,
)
from cdp_langchain.tools import CdpAction
from cdp_langchain.utils import CdpAgentkitWrapper
Expand Down Expand Up @@ -79,6 +81,7 @@ class CdpToolkit(BaseToolkit):
mint_nft
deploy_nft
register_basename
uniswap_v3_create_pool
Use within an agent:
.. code-block:: python
Expand Down Expand Up @@ -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",
Expand Down
19 changes: 18 additions & 1 deletion cdp-langchain/cdp_langchain/utils/cdp_agentkit_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
request_faucet_funds,
trade,
transfer,
uniswap_v3_create_pool,
)


Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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":
Expand All @@ -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)

0 comments on commit ac3fafc

Please sign in to comment.