diff --git a/README.md b/README.md index 5b5fb55dd..83fb5f8ba 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ The **Coinbase Developer Platform (CDP) Agentkit for Python** simplifies bringin - Deploying [ERC-721](https://www.coinbase.com/learn/crypto-glossary/what-is-erc-721) tokens and minting NFTs - Buying and selling [Zora Wow](https://wow.xyz/) ERC-20 coins - Deploying tokens on [Zora's Wow Launcher](https://wow.xyz/mechanics) (Bonding Curve) + - Wrapping ETH to WETH on Base Or [add your own](./CONTRIBUTING.md#adding-an-action-to-agentkit-core)! @@ -50,4 +51,4 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for more information. ## Documentation - [CDP Agentkit Documentation](https://docs.cdp.coinbase.com/agentkit/docs/welcome) - [API Reference: CDP Agentkit Core](https://coinbase.github.io/cdp-agentkit/cdp-agentkit-core/index.html) -- [API Reference: CDP Agentkit LangChain Extension](https://coinbase.github.io/cdp-agentkit/cdp-langchain/index.html) \ No newline at end of file +- [API Reference: CDP Agentkit LangChain Extension](https://coinbase.github.io/cdp-agentkit/cdp-langchain/index.html) diff --git a/cdp-agentkit-core/CHANGELOG.md b/cdp-agentkit-core/CHANGELOG.md index e603954c8..447584cb8 100644 --- a/cdp-agentkit-core/CHANGELOG.md +++ b/cdp-agentkit-core/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Added + +- Added `wrap_eth` action to wrap ETH to WETH on Base. + ## [0.0.7] - 2025-01-08 ### Added diff --git a/cdp-agentkit-core/Makefile b/cdp-agentkit-core/Makefile index 3384752bf..13f13e7ea 100644 --- a/cdp-agentkit-core/Makefile +++ b/cdp-agentkit-core/Makefile @@ -1,18 +1,18 @@ .PHONY: format format: - ruff format . + poetry run ruff format . .PHONY: lint lint: - ruff check . + poetry run ruff check . .PHONY: lint-fix lint-fix: - ruff check . --fix + poetry run ruff check . --fix .PHONY: docs docs: - sphinx-apidoc -f -o ./docs ./cdp_agentkit_core + poetry run sphinx-apidoc -f -o ./docs ./cdp_agentkit_core .PHONY: local-docs local-docs: docs diff --git a/cdp-agentkit-core/cdp_agentkit_core/actions/__init__.py b/cdp-agentkit-core/cdp_agentkit_core/actions/__init__.py index 347687039..b07f1d849 100644 --- a/cdp-agentkit-core/cdp_agentkit_core/actions/__init__.py +++ b/cdp-agentkit-core/cdp_agentkit_core/actions/__init__.py @@ -11,6 +11,7 @@ from cdp_agentkit_core.actions.wow.buy_token import WowBuyTokenAction from cdp_agentkit_core.actions.wow.create_token import WowCreateTokenAction from cdp_agentkit_core.actions.wow.sell_token import WowSellTokenAction +from cdp_agentkit_core.actions.wrap_eth import WrapEthAction # WARNING: All new CdpAction subclasses must be imported above, otherwise they will not be discovered @@ -40,4 +41,5 @@ def get_all_cdp_actions() -> list[type[CdpAction]]: "WowBuyTokenAction", "WowCreateTokenAction", "WowSellTokenAction", + "WrapEthAction", ] diff --git a/cdp-agentkit-core/cdp_agentkit_core/actions/deploy_nft.py b/cdp-agentkit-core/cdp_agentkit_core/actions/deploy_nft.py index 8ecb1c858..3538e4371 100644 --- a/cdp-agentkit-core/cdp_agentkit_core/actions/deploy_nft.py +++ b/cdp-agentkit-core/cdp_agentkit_core/actions/deploy_nft.py @@ -10,6 +10,7 @@ It takes the name of the NFT collection, the symbol of the NFT collection, and the base URI for the token metadata as inputs. """ + class DeployNftInput(BaseModel): """Input argument schema for deploy NFT action.""" diff --git a/cdp-agentkit-core/cdp_agentkit_core/actions/get_balance.py b/cdp-agentkit-core/cdp_agentkit_core/actions/get_balance.py index f74eea259..31857803d 100644 --- a/cdp-agentkit-core/cdp_agentkit_core/actions/get_balance.py +++ b/cdp-agentkit-core/cdp_agentkit_core/actions/get_balance.py @@ -10,6 +10,7 @@ It takes the asset ID as input. Always use 'eth' for the native asset ETH and 'usdc' for USDC. """ + class GetBalanceInput(BaseModel): """Input argument schema for get balance action.""" diff --git a/cdp-agentkit-core/cdp_agentkit_core/actions/mint_nft.py b/cdp-agentkit-core/cdp_agentkit_core/actions/mint_nft.py index d65d1667f..653b8c317 100644 --- a/cdp-agentkit-core/cdp_agentkit_core/actions/mint_nft.py +++ b/cdp-agentkit-core/cdp_agentkit_core/actions/mint_nft.py @@ -11,6 +11,7 @@ Do not use the contract address as the destination address. If you are unsure of the destination address, please ask the user before proceeding. """ + class MintNftInput(BaseModel): """Input argument schema for mint NFT action.""" diff --git a/cdp-agentkit-core/cdp_agentkit_core/actions/social/twitter/account_details.py b/cdp-agentkit-core/cdp_agentkit_core/actions/social/twitter/account_details.py index 01edbb920..7c1bbea06 100644 --- a/cdp-agentkit-core/cdp_agentkit_core/actions/social/twitter/account_details.py +++ b/cdp-agentkit-core/cdp_agentkit_core/actions/social/twitter/account_details.py @@ -37,10 +37,12 @@ def account_details(client: tweepy.Client) -> str: try: response = client.get_me() - data = response['data'] - data['url'] = f"https://x.com/{data['username']}" + data = response["data"] + data["url"] = f"https://x.com/{data['username']}" - message = f"""Successfully retrieved authenticated user account details:\n{dumps(response)}""" + message = ( + f"""Successfully retrieved authenticated user account details:\n{dumps(response)}""" + ) except tweepy.errors.TweepyException as e: message = f"Error retrieving authenticated user account details:\n{e}" diff --git a/cdp-agentkit-core/cdp_agentkit_core/actions/transfer.py b/cdp-agentkit-core/cdp_agentkit_core/actions/transfer.py index 883c15c17..734e03ffd 100644 --- a/cdp-agentkit-core/cdp_agentkit_core/actions/transfer.py +++ b/cdp-agentkit-core/cdp_agentkit_core/actions/transfer.py @@ -22,6 +22,7 @@ - When sending native assets (e.g. 'eth' on base-mainnet), ensure there is sufficient balance for the transfer itself AND the gas cost of this transfer """ + class TransferInput(BaseModel): """Input argument schema for transfer action.""" diff --git a/cdp-agentkit-core/cdp_agentkit_core/actions/wow/utils.py b/cdp-agentkit-core/cdp_agentkit_core/actions/wow/utils.py index 97f93e23c..dbf53764c 100644 --- a/cdp-agentkit-core/cdp_agentkit_core/actions/wow/utils.py +++ b/cdp-agentkit-core/cdp_agentkit_core/actions/wow/utils.py @@ -32,15 +32,14 @@ def get_buy_quote(network_id: str, token_address: str, amount_eth_in_wei: str): """ has_graduated = get_has_graduated(network_id, token_address) token_quote = ( - (has_graduated - and (get_uniswap_quote(network_id, token_address, amount_eth_in_wei, "buy")).amount_out) - or SmartContract.read( - network_id, - token_address, - "getEthBuyQuote", - abi=WOW_ABI, - args={"ethOrderSize": str(amount_eth_in_wei)}, - ) + has_graduated + and (get_uniswap_quote(network_id, token_address, amount_eth_in_wei, "buy")).amount_out + ) or SmartContract.read( + network_id, + token_address, + "getEthBuyQuote", + abi=WOW_ABI, + args={"ethOrderSize": str(amount_eth_in_wei)}, ) return token_quote @@ -56,14 +55,13 @@ def get_sell_quote(network_id: str, token_address: str, amount_tokens_in_wei: st """ has_graduated = get_has_graduated(network_id, token_address) token_quote = ( - (has_graduated - and (get_uniswap_quote(network_id, token_address, amount_tokens_in_wei, "sell")).amount_out) - or SmartContract.read( - network_id, - token_address, - "getTokenSellQuote", - WOW_ABI, - args={"tokenOrderSize": str(amount_tokens_in_wei)}, - ) + has_graduated + and (get_uniswap_quote(network_id, token_address, amount_tokens_in_wei, "sell")).amount_out + ) or SmartContract.read( + network_id, + token_address, + "getTokenSellQuote", + WOW_ABI, + args={"tokenOrderSize": str(amount_tokens_in_wei)}, ) return token_quote diff --git a/cdp-agentkit-core/cdp_agentkit_core/actions/wrap_eth.py b/cdp-agentkit-core/cdp_agentkit_core/actions/wrap_eth.py new file mode 100644 index 000000000..e50b477db --- /dev/null +++ b/cdp-agentkit-core/cdp_agentkit_core/actions/wrap_eth.py @@ -0,0 +1,93 @@ +from collections.abc import Callable + +from cdp import Wallet +from pydantic import BaseModel, Field + +from cdp_agentkit_core.actions import CdpAction + +WETH_ADDRESS = "0x4200000000000000000000000000000000000006" + +WETH_ABI = [ + { + "inputs": [], + "name": "deposit", + "outputs": [], + "stateMutability": "payable", + "type": "function", + }, + { + "inputs": [ + { + "name": "account", + "type": "address", + }, + ], + "name": "balanceOf", + "outputs": [ + { + "type": "uint256", + }, + ], + "stateMutability": "view", + "type": "function", + }, +] + +WRAP_ETH_PROMPT = """ +This tool can only be used to wrap ETH to WETH. +Do not use this tool for any other purpose, or trading other assets. +Inputs: +- Amount of ETH to wrap. +Important notes: +- The amount is a string and cannot have any decimal points, since the unit of measurement is wei. +- 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 WETH +- Minimum purchase amount is 100000000000000 wei (0.0000001 WETH) +- Only supported on the following networks: + - Base Sepolia (ie, 'base-sepolia') + - Base Mainnet (ie, 'base', 'base-mainnnet') +""" + + +class WrapEthInput(BaseModel): + """Input argument schema for wrapping ETH to WETH.""" + + amount_to_wrap: str = Field( + ..., + description="Amount of ETH to wrap in wei", + ) + + +def wrap_eth(wallet: Wallet, amount_to_wrap: str) -> str: + """Wrap ETH to WETH. + + Args: + wallet (Wallet): The wallet to wrap ETH from. + amount_to_wrap (str): The amount of ETH to wrap in wei. + + Returns: + str: A message containing the wrapped ETH details. + + """ + try: + invocation = wallet.invoke_contract( + contract_address=WETH_ADDRESS, + method="deposit", + abi=WETH_ABI, + args={}, + amount=amount_to_wrap, + asset_id="wei", + ) + result = invocation.wait() + return f"Wrapped ETH with transaction hash: {result.transaction.transaction_hash}" + except Exception as e: + return f"Unexpected error wrapping ETH: {e!s}" + + +class WrapEthAction(CdpAction): + """Wrap ETH to WETH action.""" + + name: str = "wrap_eth" + description: str = WRAP_ETH_PROMPT + args_schema: type[BaseModel] | None = WrapEthInput + func: Callable[..., str] = wrap_eth diff --git a/cdp-agentkit-core/tests/actions/test_wrap_eth.py b/cdp-agentkit-core/tests/actions/test_wrap_eth.py new file mode 100644 index 000000000..90f0648eb --- /dev/null +++ b/cdp-agentkit-core/tests/actions/test_wrap_eth.py @@ -0,0 +1,74 @@ +from unittest.mock import patch + +import pytest + +from cdp_agentkit_core.actions.wrap_eth import ( + WETH_ABI, + WETH_ADDRESS, + WrapEthAction, + WrapEthInput, + wrap_eth, +) + + +def test_wrap_eth_success(wallet_factory, contract_invocation_factory): + """Test successful ETH wrapping.""" + mock_wallet = wallet_factory() + mock_invocation = contract_invocation_factory() + + amount = "1000000000000000000" # 1 ETH in wei + + with ( + patch.object( + mock_wallet, "invoke_contract", return_value=mock_invocation + ) as mock_invoke_contract, + patch.object(mock_invocation, "wait", return_value=mock_invocation) as mock_invocation_wait, + ): + result = wrap_eth(mock_wallet, amount) + + mock_invoke_contract.assert_called_once_with( + contract_address=WETH_ADDRESS, + method="deposit", + abi=WETH_ABI, + args={}, + amount=amount, + asset_id="wei", + ) + mock_invocation_wait.assert_called_once_with() + + assert ( + result + == f"Wrapped ETH with transaction hash: {mock_invocation.transaction.transaction_hash}" + ) + + +def test_wrap_eth_failure(wallet_factory): + """Test ETH wrapping failure.""" + mock_wallet = wallet_factory() + mock_wallet.invoke_contract.side_effect = Exception("Test error") + + amount = "1000000000000000000" + result = wrap_eth(mock_wallet, amount) + + assert result == "Unexpected error wrapping ETH: Test error" + + +def test_wrap_eth_action_initialization(): + """Test WrapEthAction initialization and attributes.""" + action = WrapEthAction() + + assert action.name == "wrap_eth" + assert action.args_schema == WrapEthInput + assert callable(action.func) + + +def test_wrap_eth_input_model_valid(): + """Test WrapEthInput accepts valid parameters.""" + valid_input = WrapEthInput(amount_to_wrap="1000000000000000000") + assert valid_input.amount_to_wrap == "1000000000000000000" + + +def test_wrap_eth_input_model_missing_params(): + """Test WrapEthInput raises error when params are missing.""" + with pytest.raises(ValueError): + WrapEthInput() diff --git a/cdp-langchain/Makefile b/cdp-langchain/Makefile index d4d004f10..0410312ab 100644 --- a/cdp-langchain/Makefile +++ b/cdp-langchain/Makefile @@ -1,18 +1,18 @@ .PHONY: format format: - ruff format . + poetry run ruff format . .PHONY: lint lint: - ruff check . + poetry run ruff check . .PHONY: lint-fix lint-fix: - ruff check . --fix + poetry run ruff check . --fix .PHONY: docs docs: - sphinx-apidoc -f -o ./docs ./cdp_langchain + poetry run sphinx-apidoc -f -o ./docs ./cdp_langchain .PHONY: local-docs local-docs: docs diff --git a/cdp-langchain/README.md b/cdp-langchain/README.md index 22f0b8849..a1b30d51d 100644 --- a/cdp-langchain/README.md +++ b/cdp-langchain/README.md @@ -62,6 +62,7 @@ The toolkit provides the following tools: 10. **wow_create_token** - Deploy a token using Zora's Wow Launcher (Bonding Curve) 11. **wow_buy_token** - Buy Zora Wow ERC20 memecoin with ETH 12. **wow_sell_token** - Sell Zora Wow ERC20 memecoin for ETH +13. **wrap_eth** - Wrap ETH to WETH ### Using with an Agent diff --git a/cdp-langchain/cdp_langchain/agent_toolkits/cdp_toolkit.py b/cdp-langchain/cdp_langchain/agent_toolkits/cdp_toolkit.py index 8229dfd00..7880371f1 100644 --- a/cdp-langchain/cdp_langchain/agent_toolkits/cdp_toolkit.py +++ b/cdp-langchain/cdp_langchain/agent_toolkits/cdp_toolkit.py @@ -63,6 +63,7 @@ class CdpToolkit(BaseToolkit): wow_create_token wow_buy_token wow_sell_token + wrap_eth Use within an agent: .. code-block:: python diff --git a/cdp-langchain/examples/chatbot/chatbot.py b/cdp-langchain/examples/chatbot/chatbot.py index 720c96b05..6663a0d19 100644 --- a/cdp-langchain/examples/chatbot/chatbot.py +++ b/cdp-langchain/examples/chatbot/chatbot.py @@ -63,7 +63,6 @@ def initialize_agent(): "recommend they go to docs.cdp.coinbase.com for more information. Be concise and helpful with your " "responses. Refrain from restating your tools' descriptions unless it is explicitly requested." ), - ), config