Skip to content
Merged
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
1 change: 1 addition & 0 deletions python/coinbase-agentkit/changelog.d/+fcba5356.bugfix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fixed a bug with server wallets as owner of smart wallets
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ async def initialize_accounts():
return owner, smart_account

owner, smart_account = asyncio.run(initialize_accounts())
self._account = smart_account
self._address = smart_account.address
self._owner = owner

finally:
Expand Down Expand Up @@ -151,14 +151,36 @@ def _run_async(self, coroutine):
asyncio.set_event_loop(loop)
return loop.run_until_complete(coroutine)

async def _get_smart_account(self, cdp):
"""Get the smart account, handling server wallet owners differently.

Args:
cdp: CDP client instance

Returns:
The smart account object

"""
# Check if owner is a server wallet (not an eth_account)
if not isinstance(self._owner, Account):
# For server wallets, create a fresh owner reference to avoid nested client sessions
owner = await cdp.evm.get_account(address=self._owner.address)
smart_account = await cdp.evm.get_smart_account(owner=owner, address=self._address)
else:
# Using eth_account is simpler - no nested client sessions
smart_account = await cdp.evm.get_smart_account(
owner=self._owner, address=self._address
)
return smart_account

def get_address(self) -> str:
"""Get the wallet address.

Returns:
str: The wallet's address as a hex string

"""
return self._account.address
return self._address

def get_balance(self) -> Decimal:
"""Get the wallet balance in native currency.
Expand Down Expand Up @@ -204,14 +226,16 @@ def native_transfer(self, to: str, value: Decimal) -> str:

async def _send_user_operation():
async with client as cdp:
smart_account = await self._get_smart_account(cdp)

user_operation = await cdp.evm.send_user_operation(
smart_account=self._account,
smart_account=smart_account,
network=self._network.network_id,
calls=[EncodedCall(to=to, value=value_wei, data="0x")],
paymaster_url=self._paymaster_url,
)
return await cdp.evm.wait_for_user_operation(
smart_account_address=self._account.address,
smart_account_address=self._address,
user_op_hash=user_operation.user_op_hash,
)

Expand Down Expand Up @@ -261,8 +285,9 @@ def send_transaction(self, transaction: TxParams) -> HexStr:

async def _send_user_operation():
async with client as cdp:
smart_account = await self._get_smart_account(cdp)
user_operation = await cdp.evm.send_user_operation(
smart_account=self._account,
smart_account=smart_account,
network=self._network.network_id,
calls=[
EncodedCall(
Expand All @@ -274,7 +299,7 @@ async def _send_user_operation():
paymaster_url=self._paymaster_url,
)
return await cdp.evm.wait_for_user_operation(
smart_account_address=self._account.address,
smart_account_address=self._address,
user_op_hash=user_operation.user_op_hash,
)

Expand Down Expand Up @@ -369,14 +394,15 @@ def send_user_operation(self, calls: list[EncodedCall]) -> str:

async def _send_user_operation():
async with client as cdp:
smart_account = await self._get_smart_account(cdp)
user_operation = await cdp.evm.send_user_operation(
smart_account=self._account,
smart_account=smart_account,
network=self._network.network_id,
calls=calls,
paymaster_url=self._paymaster_url,
)
return await cdp.evm.wait_for_user_operation(
smart_account_address=self._account.address,
smart_account_address=self._address,
user_op_hash=user_operation.user_op_hash,
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ async def get_smart_account_mock(*args, **kwargs):
provider._api_key_secret = MOCK_API_KEY_SECRET
provider._wallet_secret = MOCK_WALLET_SECRET
provider._account = mock_smart_account
provider._address = MOCK_ADDRESS

# Create a proper Network instance instead of a mock
provider._network = Network(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,9 @@ def test_get_balance_with_zero(mocked_wallet_provider, mock_web3):
def test_get_balance_failure(mocked_wallet_provider, mock_web3):
"""Test get_balance method when balance check fails."""
error_message = "Balance check failed"
mock_web3.return_value.eth.get_balance.side_effect = Exception(error_message)
mock_web3.return_value.eth.get_balance.side_effect = RuntimeError(error_message)

with pytest.raises(Exception, match=error_message):
with pytest.raises(RuntimeError):
mocked_wallet_provider.get_balance()


Expand Down
73 changes: 61 additions & 12 deletions python/create-onchain-agent/templates/chatbot/setup.py.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -47,15 +47,62 @@ def setup():
address=wallet_data.get("address") or os.getenv("ADDRESS"),
idempotency_key=os.getenv("IDEMPOTENCY_KEY"),
)
{% elif _wallet_provider == "smart" %}# Determine owner using priority order
owner = (
os.getenv("OWNER_PRIVATE_KEY") # First priority: Private key
or os.getenv("OWNER_SERVER_WALLET_ADDRESS") # Second priority: Server wallet address
or wallet_data.get("owner") # Third priority: Saved wallet file
)
{% elif _wallet_provider == "smart" %}# Check for environment variables first
owner_private_key = os.getenv("OWNER_PRIVATE_KEY")
owner_server_address = os.getenv("OWNER_SERVER_WALLET_ADDRESS")
smart_wallet_address_env = os.getenv("SMART_WALLET_ADDRESS")

# Determine where to get wallet configuration from (env vars or saved file)
use_env_vars = (owner_private_key or owner_server_address) and smart_wallet_address_env
use_wallet_file = wallet_data.get("owner_value") and wallet_data.get("owner_type") and wallet_data.get("smart_wallet_address")

owner_value = None
owner_type = None
smart_wallet_address = None

# Prioritize environment variables over saved wallet file
if use_env_vars:
# Use environment variables
print("Using wallet configuration from environment variables")
if owner_private_key:
owner_value = owner_private_key
owner_type = "private_key"
else:
owner_value = owner_server_address
owner_type = "server_address"
smart_wallet_address = smart_wallet_address_env
elif use_wallet_file:
# Use saved wallet file
print("Using wallet configuration from saved wallet file")
owner_value = wallet_data.get("owner_value")
owner_type = wallet_data.get("owner_type")
smart_wallet_address = wallet_data.get("smart_wallet_address")
else:
# If using just one part from env and missing the other, print a warning
if owner_private_key or owner_server_address:
print("Warning: Owner specified in environment, but no SMART_WALLET_ADDRESS found")
if owner_private_key:
owner_value = owner_private_key
owner_type = "private_key"
else:
owner_value = owner_server_address
owner_type = "server_address"
elif smart_wallet_address_env:
print("Warning: SMART_WALLET_ADDRESS specified in environment, but no owner found")
smart_wallet_address = smart_wallet_address_env

# Fall back to partial info from wallet file if available
if not owner_value and wallet_data.get("owner_value"):
print("Using owner from saved wallet file")
owner_value = wallet_data.get("owner_value")
owner_type = wallet_data.get("owner_type")

if not smart_wallet_address and wallet_data.get("smart_wallet_address"):
print("Using smart wallet address from saved wallet file")
smart_wallet_address = wallet_data.get("smart_wallet_address")

# If no owner is provided, create a new server wallet to be used as the owner
if not owner:
if not owner_value:
print("No owner provided, creating new server wallet...")
idempotency_key = os.getenv("OWNER_IDEMPOTENCY_KEY")

Expand All @@ -69,17 +116,18 @@ def setup():
async with client as cdp:
account = await cdp.evm.create_account(idempotency_key=idempotency_key)
return account.address
owner = asyncio.run(create_wallet())
print(f"Created new server wallet: {owner}")
owner_value = asyncio.run(create_wallet())
owner_type = "server_address"
print(f"Created new server wallet: {owner_value}")

# Create smart wallet config
config = CdpEvmSmartWalletProviderConfig(
api_key_id=api_key_id,
api_key_secret=api_key_secret,
wallet_secret=wallet_secret,
network_id=network_id,
address=wallet_data.get("smart_wallet_address") or os.getenv("SMART_WALLET_ADDRESS"),
owner=owner,
address=smart_wallet_address,
owner=owner_value,
paymaster_url=os.getenv("PAYMASTER_URL"),
)
{% elif _wallet_provider == "eth" %}# Get or generate private key
Expand Down Expand Up @@ -112,7 +160,8 @@ def setup():
}{% elif _wallet_provider == "smart" %}
new_wallet_data = {
"smart_wallet_address": wallet_provider.get_address(),
"owner": owner,
"owner_value": owner_value,
"owner_type": owner_type,
"network_id": network_id,
"created_at": time.strftime("%Y-%m-%d %H:%M:%S") if not wallet_data else wallet_data.get("created_at")
}{% elif _wallet_provider == "eth" %}
Expand Down
80 changes: 63 additions & 17 deletions python/examples/langchain-cdp-smart-wallet-chatbot/chatbot.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,15 +123,66 @@ def setup():
if not all([api_key_id, api_key_secret, wallet_secret]):
raise ValueError("CDP_API_KEY_ID, CDP_API_KEY_SECRET, and CDP_WALLET_SECRET are required")

# Determine owner using priority order
owner = (
os.getenv("OWNER_PRIVATE_KEY") # First priority: Private key
or os.getenv("OWNER_SERVER_WALLET_ADDRESS") # Second priority: Server wallet address
or wallet_data.get("owner") # Third priority: Saved wallet file
# Check for environment variables first
owner_private_key = os.getenv("OWNER_PRIVATE_KEY")
owner_server_address = os.getenv("OWNER_SERVER_WALLET_ADDRESS")
smart_wallet_address_env = os.getenv("SMART_WALLET_ADDRESS")

# Determine where to get wallet configuration from (env vars or saved file)
use_env_vars = (owner_private_key or owner_server_address) and smart_wallet_address_env
use_wallet_file = (
wallet_data.get("owner_value")
and wallet_data.get("owner_type")
and wallet_data.get("smart_wallet_address")
)

owner_value = None
owner_type = None
smart_wallet_address = None

# Prioritize environment variables over saved wallet file
if use_env_vars:
# Use environment variables
print("Using wallet configuration from environment variables")
if owner_private_key:
owner_value = owner_private_key
owner_type = "private_key"
else:
owner_value = owner_server_address
owner_type = "server_address"
smart_wallet_address = smart_wallet_address_env
elif use_wallet_file:
# Use saved wallet file
print("Using wallet configuration from saved wallet file")
owner_value = wallet_data.get("owner_value")
owner_type = wallet_data.get("owner_type")
smart_wallet_address = wallet_data.get("smart_wallet_address")
else:
# If using just one part from env and missing the other, print a warning
if owner_private_key or owner_server_address:
print("Warning: Owner specified in environment, but no SMART_WALLET_ADDRESS found")
if owner_private_key:
owner_value = owner_private_key
owner_type = "private_key"
else:
owner_value = owner_server_address
owner_type = "server_address"
elif smart_wallet_address_env:
print("Warning: SMART_WALLET_ADDRESS specified in environment, but no owner found")
smart_wallet_address = smart_wallet_address_env

# Fall back to partial info from wallet file if available
if not owner_value and wallet_data.get("owner_value"):
print("Using owner from saved wallet file")
owner_value = wallet_data.get("owner_value")
owner_type = wallet_data.get("owner_type")

if not smart_wallet_address and wallet_data.get("smart_wallet_address"):
print("Using smart wallet address from saved wallet file")
smart_wallet_address = wallet_data.get("smart_wallet_address")

# If no owner is provided, create a new server wallet to be used as the owner
if not owner:
if not owner_value:
print("No owner provided, creating new server wallet...")
idempotency_key = os.getenv("OWNER_IDEMPOTENCY_KEY")

Expand All @@ -147,15 +198,9 @@ async def create_wallet():
account = await cdp.evm.create_account(idempotency_key=idempotency_key)
return account.address

owner = asyncio.run(create_wallet())
print(f"Created new server wallet: {owner}")

# Determine smart wallet address using priority order
smart_wallet_address = (
wallet_data.get("smart_wallet_address") # First priority: Saved wallet file
or os.getenv("SMART_WALLET_ADDRESS") # Second priority: SMART_WALLET_ADDRESS env var
or None # Will create new if not provided
)
owner_value = asyncio.run(create_wallet())
owner_type = "server_address"
print(f"Created new server wallet: {owner_value}")

# Create the wallet provider config
config = CdpEvmSmartWalletProviderConfig(
Expand All @@ -164,7 +209,7 @@ async def create_wallet():
wallet_secret=wallet_secret,
network_id=network_id,
address=smart_wallet_address,
owner=owner,
owner=owner_value,
paymaster_url=os.getenv(
"PAYMASTER_URL"
), # Optional paymaster URL to sponsor transactions: https://docs.cdp.coinbase.com/paymaster/docs/welcome
Expand All @@ -176,7 +221,8 @@ async def create_wallet():
# Save the wallet data after successful initialization
new_wallet_data = {
"smart_wallet_address": wallet_provider.get_address(),
"owner": owner,
"owner_value": owner_value,
"owner_type": owner_type,
"network_id": network_id,
"created_at": time.strftime("%Y-%m-%d %H:%M:%S")
if not wallet_data
Expand Down
Loading
Loading