Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(py): aerodrome deposit & withdraw #174

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions cdp-agentkit-core/python/Makefile
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
ifneq (,$(wildcard ./.env))
include .env
export
endif

.PHONY: format
format:
poetry run ruff format .
Expand All @@ -21,3 +26,19 @@ local-docs: docs
.PHONY: test
test:
poetry run pytest

.PHONY: test-utils
test-utils:
poetry run pytest tests/actions/aerodrome/test_utils_e2e.py -s

.PHONY: test-deposit
test-deposit:
poetry run pytest tests/actions/aerodrome/test_deposit_e2e.py -s

.PHONY: test-quote
test-quote:
poetry run pytest tests/actions/aerodrome/test_quote_e2e.py -s

.PHONY: test-withdraw
test-withdraw:
poetry run pytest tests/actions/aerodrome/test_withdraw_e2e.py -s
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
AERODROME_FACTORY_ADDRESS = "0x420DD381b31aEf6683db6B902084cB0FFECe40Da"
AERODROME_ROUTER_ADDRESS = "0xcF77a3Ba9A5CA399B7c97c74d54e5b1Beb874E43"
AERODROME_POOL_ADDRESS = "0xA4e46b4f701c62e14DF11B48dCe76A7d793CD6d7"
BASE_RPC_URL = "https://mainnet.base.org"

AERODROME_FACTORY_ABI = [
{
"inputs": [
{"internalType": "address", "name": "tokenA", "type": "address"},
{"internalType": "address", "name": "tokenB", "type": "address"},
{"internalType": "bool", "name": "stable", "type": "bool"}
],
"name": "getPool",
"outputs": [{"internalType": "address", "name": "", "type": "address"}],
"stateMutability": "view",
"type": "function"
}
]

AERODROME_ROUTER_ABI = [
{
"inputs": [
{"internalType": "address", "name": "tokenA", "type": "address"},
{"internalType": "address", "name": "tokenB", "type": "address"},
{"internalType": "bool", "name": "stable", "type": "bool"},
{"internalType": "uint256", "name": "amountADesired", "type": "uint256"},
{"internalType": "uint256", "name": "amountBDesired", "type": "uint256"},
{"internalType": "uint256", "name": "amountAMin", "type": "uint256"},
{"internalType": "uint256", "name": "amountBMin", "type": "uint256"},
{"internalType": "address", "name": "to", "type": "address"},
{"internalType": "uint256", "name": "deadline", "type": "uint256"}
],
"name": "addLiquidity",
"outputs": [
{"internalType": "uint256", "name": "amountA", "type": "uint256"},
{"internalType": "uint256", "name": "amountB", "type": "uint256"},
{"internalType": "uint256", "name": "liquidity", "type": "uint256"}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{"internalType": "address", "name": "tokenA", "type": "address"},
{"internalType": "address", "name": "tokenB", "type": "address"},
{"internalType": "bool", "name": "stable", "type": "bool"},
{"internalType": "address", "name": "_factory", "type": "address"},
{"internalType": "uint256", "name": "amountADesired", "type": "uint256"},
{"internalType": "uint256", "name": "amountBDesired", "type": "uint256"}
],
"name": "quoteAddLiquidity",
"outputs": [
{"internalType": "uint256", "name": "amountA", "type": "uint256"},
{"internalType": "uint256", "name": "amountB", "type": "uint256"},
{"internalType": "uint256", "name": "liquidity", "type": "uint256"}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{"internalType": "address", "name": "tokenA", "type": "address"},
{"internalType": "address", "name": "tokenB", "type": "address"},
{"internalType": "bool", "name": "stable", "type": "bool"},
{"internalType": "uint256", "name": "liquidity", "type": "uint256"},
{"internalType": "uint256", "name": "amountAMin", "type": "uint256"},
{"internalType": "uint256", "name": "amountBMin", "type": "uint256"},
{"internalType": "address", "name": "to", "type": "address"},
{"internalType": "uint256", "name": "deadline", "type": "uint256"}
],
"name": "removeLiquidity",
"outputs": [
{"internalType": "uint256", "name": "amountA", "type": "uint256"},
{"internalType": "uint256", "name": "amountB", "type": "uint256"}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{"internalType": "address", "name": "token", "type": "address"},
{"internalType": "bool", "name": "stable", "type": "bool"},
{"internalType": "uint256", "name": "amountTokenDesired", "type": "uint256"},
{"internalType": "uint256", "name": "amountTokenMin", "type": "uint256"},
{"internalType": "uint256", "name": "amountETHMin", "type": "uint256"},
{"internalType": "address", "name": "to", "type": "address"},
{"internalType": "uint256", "name": "deadline", "type": "uint256"}
],
"name": "addLiquidityETH",
"outputs": [
{"internalType": "uint256", "name": "amountToken", "type": "uint256"},
{"internalType": "uint256", "name": "amountETH", "type": "uint256"},
{"internalType": "uint256", "name": "liquidity", "type": "uint256"}
],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [
{"internalType": "address", "name": "token", "type": "address"},
{"internalType": "bool", "name": "stable", "type": "bool"},
{"internalType": "uint256", "name": "liquidity", "type": "uint256"},
{"internalType": "uint256", "name": "amountTokenMin", "type": "uint256"},
{"internalType": "uint256", "name": "amountETHMin", "type": "uint256"},
{"internalType": "address", "name": "to", "type": "address"},
{"internalType": "uint256", "name": "deadline", "type": "uint256"}
],
"name": "removeLiquidityETH",
"outputs": [
{"internalType": "uint256", "name": "amountToken", "type": "uint256"},
{"internalType": "uint256", "name": "amountETH", "type": "uint256"}
],
"stateMutability": "nonpayable",
"type": "function"
}
]

AERODROME_POOL_ABI = [
{
"inputs": [],
"name": "getReserves",
"outputs": [
{
"internalType": "uint256",
"name": "_reserve0",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "_reserve1",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "_blockTimestampLast",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "totalSupply",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "balanceOf",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
from collections.abc import Callable
from decimal import Decimal

from cdp import Asset, Wallet
from pydantic import BaseModel, Field

from cdp_agentkit_core.actions import CdpAction
from cdp_agentkit_core.actions.aerodrome.constants import AERODROME_ROUTER_ABI, AERODROME_ROUTER_ADDRESS
from cdp_agentkit_core.actions.utils import approve


class AerodromeAddLiquidityInput(BaseModel):
"""Input schema for Aerodrome liquidity deposit action."""

token_a: str = Field(..., description="The address of the first token in the pair")
token_b: str = Field(..., description="The address of the second token in the pair")
stable: bool = Field(..., description="Whether this is a stable or volatile pair")
amount_a_desired: str = Field(..., description="The amount of first token to add as liquidity")
amount_b_desired: str = Field(..., description="The amount of second token to add as liquidity")
amount_a_min: str = Field(..., description="The minimum amount of first token to add as liquidity")
amount_b_min: str = Field(..., description="The minimum amount of second token to add as liquidity")
to: str = Field(..., description="The address that will receive the liquidity tokens")
deadline: str = Field(..., description="The timestamp deadline for the transaction to be executed by")


DEPOSIT_PROMPT = """
This tool allows adding liquidity to Aerodrome pools.
It takes:

- token_a: The address of the first token in the pair
- token_b: The address of the second token in the pair
- stable: Whether this is a stable or volatile pair (true/false)
- amount_a_desired: The amount of first token to add as liquidity in whole units
Examples:
- 1 TOKEN
- 0.1 TOKEN
- 0.01 TOKEN
- amount_b_desired: The amount of second token to add as liquidity in whole units
- amount_a_min: The minimum amount of first token (to protect against slippage)
- amount_b_min: The minimum amount of second token (to protect against slippage)
- to: The address to receive the LP tokens
- deadline: The timestamp deadline for the transaction (in seconds since Unix epoch)

Important notes:
- Make sure to use exact amounts provided. Do not convert units for amounts in this action.
- Please use token addresses (example 0x4200000000000000000000000000000000000006). If unsure of token addresses, please clarify before continuing.
- The deadline should be a future timestamp (current time + some buffer, e.g., 20 minutes)
- For stable pairs, the ratio between tokens should be close to 1:1 in USD value
"""


def deposit(
wallet: Wallet,
token_a: str,
token_b: str,
stable: bool,
amount_a_desired: str,
amount_b_desired: str,
amount_a_min: str,
amount_b_min: str,
to: str,
deadline: str,
) -> str:
"""Add liquidity to an Aerodrome pool.

Args:
wallet (Wallet): The wallet to execute the deposit from
token_a (str): The address of the first token
token_b (str): The address of the second token
stable (bool): Whether this is a stable or volatile pair
amount_a_desired (str): The desired amount of first token to add
amount_b_desired (str): The desired amount of second token to add
amount_a_min (str): The minimum amount of first token to add
amount_b_min (str): The minimum amount of second token to add
to (str): The address to receive LP tokens
deadline (str): The timestamp deadline for the transaction

Returns:
str: A success message with transaction hash or error message
"""
try:
# Validate inputs
if float(amount_a_desired) <= 0 or float(amount_b_desired) <= 0:
return "Error: Desired amounts must be greater than 0"

if float(amount_a_min) <= 0 or float(amount_b_min) <= 0:
return "Error: Minimum amounts must be greater than 0"

# Convert amounts to atomic units
token_a_asset = Asset.fetch(wallet.network_id, token_a)
token_b_asset = Asset.fetch(wallet.network_id, token_b)

atomic_amount_a_desired = str(int(token_a_asset.to_atomic_amount(Decimal(amount_a_desired))))
atomic_amount_b_desired = str(int(token_b_asset.to_atomic_amount(Decimal(amount_b_desired))))
atomic_amount_a_min = str(int(token_a_asset.to_atomic_amount(Decimal(amount_a_min))))
atomic_amount_b_min = str(int(token_b_asset.to_atomic_amount(Decimal(amount_b_min))))

# atomic_amount_a_approved = str(int(token_a_asset.to_atomic_amount(Decimal(amount_a_desired)*Decimal(10))))
# atomic_amount_b_approved = str(int(token_b_asset.to_atomic_amount(Decimal(amount_b_desired) * Decimal(10))))

# Approve router for both tokens
approval_a = approve(wallet, token_a, AERODROME_ROUTER_ADDRESS, atomic_amount_a_desired)
if approval_a.startswith("Error"):
return f"Error approving token A: {approval_a}"

approval_b = approve(wallet, token_b, AERODROME_ROUTER_ADDRESS, atomic_amount_b_desired)
if approval_b.startswith("Error"):
return f"Error approving token B: {approval_b}"

print(f"\ndesired a: {atomic_amount_a_desired}\ndesired b: {atomic_amount_b_desired}")
print(f"\nmin a: {atomic_amount_a_min}\nmin b: {atomic_amount_b_min}")
add_liquidity_args = {
"tokenA": token_a,
"tokenB": token_b,
"stable": stable,
"amountADesired": atomic_amount_a_desired,
"amountBDesired": atomic_amount_b_desired,
"amountAMin": atomic_amount_a_min,
"amountBMin": atomic_amount_b_min,
"to": to,
"deadline": deadline,
}

invocation = wallet.invoke_contract(
contract_address=AERODROME_ROUTER_ADDRESS,
method="addLiquidity",
abi=AERODROME_ROUTER_ABI,
args=add_liquidity_args,
).wait()

return f"Added liquidity to Aerodrome pool with transaction hash: {invocation.transaction_hash} and transaction link: {invocation.transaction_link}"

except Exception as e:
return f"Error adding liquidity to Aerodrome: {e!s}"


class AerodromeAddLiquidityAction(CdpAction):
"""Aerodrome liquidity deposit action."""

name: str = "aerodrome_add_liquidity"
description: str = DEPOSIT_PROMPT
args_schema: type[BaseModel] = AerodromeAddLiquidityInput
func: Callable[..., str] = deposit
Loading
Loading