Skip to content
Merged
1 change: 1 addition & 0 deletions python/create-onchain-agent/changelog.d/498.feature.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
Added revised network selection
174 changes: 120 additions & 54 deletions python/create-onchain-agent/create_onchain_agent/cli.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -39,18 +39,18 @@
# "selected": "bold cyan", # Selected option
})

NETWORK_CHOICES = [
("Ethereum Mainnet", "ethereum-mainnet"),
("Ethereum Sepolia", "ethereum-sepolia"),
("Polygon Mainnet", "polygon-mainnet"),
("Polygon Mumbai", "polygon-mumbai"),
("Base Mainnet", "base-mainnet"),
("Base Sepolia (default)", "base-sepolia"),
("Arbitrum Mainnet", "arbitrum-mainnet"),
("Arbitrum Sepolia", "arbitrum-sepolia"),
("Optimism Mainnet", "optimism-mainnet"),
("Optimism Sepolia", "optimism-sepolia"),
("Other (Enter EVM Chain ID)", "other"),
# Network constants
EVM_NETWORKS = [
("base-mainnet", "Base Mainnet"),
("base-sepolia", "Base Sepolia"),
("ethereum-mainnet", "Ethereum Mainnet"),
("ethereum-sepolia", "Ethereum Sepolia"),
("arbitrum-mainnet", "Arbitrum Mainnet"),
("arbitrum-sepolia", "Arbitrum Sepolia"),
("optimism-mainnet", "Optimism Mainnet"),
("optimism-sepolia", "Optimism Sepolia"),
("polygon-mainnet", "Polygon Mainnet"),
("polygon-mumbai", "Polygon Mumbai"),
]

CDP_SUPPORTED_NETWORKS = {
Expand All@@ -64,8 +64,26 @@

VALID_PACKAGE_NAME_REGEX = re.compile(r"^[a-zA-Z_][a-zA-Z0-9_]*$")

def download_and_extract_template():
"""Downloads and extracts the chatbot template to a persistent location."""
def get_template_path(template_path: str | None = None) -> str:
"""Gets the template path either from a local directory or downloaded from GitHub.

Args:
template_path: Optional path to local template directory

Returns:
str: Path to the template directory
"""
if template_path:
# Use provided template path
local_template_path = Path(template_path)
if not local_template_path.exists():
raise FileNotFoundError(
f"Template path not found at {local_template_path}. "
"Please provide a valid template directory path."
)
return str(local_template_path)

# No template provided - download from GitHub
LOCAL_CACHE_DIR.mkdir(parents=True, exist_ok=True)
zip_path = LOCAL_CACHE_DIR / "repo.zip"
extract_path = LOCAL_CACHE_DIR / "templates"
Expand DownExpand Up@@ -94,8 +112,17 @@ def download_and_extract_template():

return str(extract_path)

def get_network_choices(network_type: str) -> list:
"""Filter network choices based on network type (mainnet/testnet)."""
return [
(name, id) for id, name in EVM_NETWORKS
if (network_type == "mainnet" and "mainnet" in id) or
(network_type == "testnet" and any(net in id for net in ["sepolia", "mumbai", "devnet", "testnet"]))
]

@click.command()
def create_project():
@click.option('--template', type=str, help='Path to local template directory', default=None)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this new option be documented?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh nvm, just saw in the PR description that this is local-only

def create_project(template):
"""Creates a new onchain agent project with interactive prompts."""

ascii_art = """
Expand DownExpand Up@@ -131,56 +158,95 @@ def create_project():
else:
package_name = suggested_package_name

# Select network using arrow keys
# console.print("\n[cyan]Select a network:[/cyan]")

network_name = questionary.select(
"Choose a network network:",
choices=[name for name, _ in NETWORK_CHOICES],
default="Base Sepolia (default)",
style=custom_style # Apply custom styling
# Choose network type
network_type = questionary.select(
"Choose network type:",
choices=[
"Mainnet",
"Testnet",
"Custom Chain ID",
],
style=custom_style
).ask()

# Map selection to network key
network = next(n for n in NETWORK_CHOICES if n[0] == network_name)[1]
network = None
chain_id = None
rpc_url = None

# If "Other" is selected, prompt for EVM Chain ID
if network == "other":
network = questionary.text(
"Enter the EVM Chain ID for your custom network:",
if network_type == "Custom Chain ID":
# Handle custom EVM network
chain_id = questionary.text(
"Enter your chain ID:",
validate=lambda text: text.strip().isdigit() or "Chain ID must be a number",
style=custom_style
).ask().strip()

# Determine wallet provider
if network in CDP_SUPPORTED_NETWORKS:
wallet_provider = questionary.select(
"Select a wallet provider:",
choices=["CDP Wallet Provider", "Ethereum Account Wallet Provider"],
default="CDP Wallet Provider",
style=custom_style # Apply custom styling
).ask()

wallet_provider = "cdp" if wallet_provider.startswith("CDP") else "eth"
rpc_url = questionary.text(
"Enter your RPC URL:",
validate=lambda text: (
text.strip().startswith(("http://", "https://")) or
"RPC URL must start with http:// or https://"
Comment thread
0xRAG marked this conversation as resolved.
),
style=custom_style
).ask()

wallet_provider = "eth" # Default to eth wallet provider for custom networks
else:
console.print(f"[yellow]⚠️ CDP is not supported on {network}. Defaulting to Ethereum Account Wallet Provider.[/yellow]")
wallet_provider = "eth"
# Filter networks based on mainnet/testnet selection
network_choices = get_network_choices(network_type.lower())
network_name = questionary.select(
"Choose a network:",
choices=[
name + (" (default)" if id == "base-sepolia" else "")
for name, id in network_choices
],
default="Base Sepolia (default)" if network_type == "Testnet" else None,
style=custom_style
).ask()

# Remove " (default)" suffix if present
network_name = network_name.replace(" (default)", "")
network = next(id for name, id in network_choices if name == network_name)

# Determine wallet provider
if network:
if network in CDP_SUPPORTED_NETWORKS:
wallet_choices = [
"CDP Wallet Provider",
"Ethereum Account Wallet Provider"
]
wallet_selection = questionary.select(
"Select a wallet provider:",
choices=wallet_choices,
default="CDP Wallet Provider",
style=custom_style
).ask()
wallet_provider = "cdp" if wallet_selection.startswith("CDP") else "eth"
else:
console.print(f"[yellow]⚠️ CDP is not supported on {network}. Defaulting to Ethereum Account Wallet Provider.[/yellow]")
wallet_provider = "eth"

console.print(f"\n[blue]Creating your onchain agent project: {project_name}[/blue]")

template_path = download_and_extract_template()

# Run Copier with collected answers
run_copy(
template_path,
project_path,
data={
"_project_name": project_name,
"_package_name": package_name,
"_network": network,
"_wallet_provider": wallet_provider,
},
)
# Update the Copier data dict to include new fields
copier_data = {
"_project_name": project_name,
"_package_name": package_name,
"_network": network,
"_wallet_provider": wallet_provider,
}

if chain_id:
copier_data["_chain_id"] = chain_id
if rpc_url:
copier_data["_rpc_url"] = rpc_url

try:
template_path = get_template_path(template)
run_copy(template_path, project_path, data=copier_data)
except FileNotFoundError as e:
console.print(f"[red]Error: {str(e)}[/red]")
return

console.print(f"[bold blue]Successfully created your AgentKit project in {project_path}[/bold blue]")

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,7 +14,15 @@ CDP_API_KEY_PRIVATE_KEY=
PRIVATE_KEY=
{% endif %}
## Optional
{% if _network %}
NETWORK={{ _network }}
{% endif %}
{% if _chain_id %}
CHAIN_ID={{ _chain_id }}
{% endif %}
{% if _rpc_url %}
RPC_URL={{ _rpc_url }}
{% endif %}
{% if _wallet_provider == "eth" %}
# Place your CDP API key name here if you want to use the CDPApiActionProvider
CDP_API_KEY_NAME=
Expand Down
34 changes: 33 additions & 1 deletion python/create-onchain-agent/templates/chatbot/chatbot.py.jinja
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,6 +31,7 @@ from coinbase_agentkit import (
from coinbase_agentkit_langchain import get_langchain_tools
{% if _wallet_provider == "eth" %}
from eth_account import Account
from coinbase_agentkit.network import NETWORK_ID_TO_CHAIN_ID, NETWORK_ID_TO_CHAIN, CHAIN_ID_TO_NETWORK_ID
{% endif %}
"""
AgentKit Integration
Expand DownExpand Up@@ -119,10 +120,41 @@ def initialize_agent():
# Create Ethereum account from private key
account = Account.from_key(private_key)

# Get chain configuration
chain_id = os.getenv("CHAIN_ID")
rpc_url = os.getenv("RPC_URL")
network = os.getenv("NETWORK")

if chain_id and rpc_url:
pass
elif network:
chain_id = NETWORK_ID_TO_CHAIN_ID.get(network)
if not chain_id:
raise ValueError(f"Unknown network ID: {network}")

if not rpc_url:
chain = NETWORK_ID_TO_CHAIN[network]
rpc_url = chain["rpc_urls"]["default"]["http"][0]
elif chain_id:
# Try to find the network ID from the chain ID
network = CHAIN_ID_TO_NETWORK_ID.get(chain_id)
if network:
chain = NETWORK_ID_TO_CHAIN[network]
rpc_url = chain["rpc_urls"]["default"]["http"][0]
else:
raise ValueError("When using chain_id, you must also provide an RPC_URL if the chain is not recognized")
else:
print("No network configuration provided. Defaulting to Base Sepolia...")
network = "base-sepolia"
chain_id = NETWORK_ID_TO_CHAIN_ID[network]
chain = NETWORK_ID_TO_CHAIN[network]
rpc_url = chain["rpc_urls"]["default"]["http"][0]

wallet_provider = EthAccountWalletProvider(
config=EthAccountWalletProviderConfig(
account=account,
chain_id="84532"
chain_id=chain_id,
rpc_url=rpc_url
Comment thread
CarsonRoscoe marked this conversation as resolved.
)
)
{% endif %}
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,7 +11,7 @@ python = "^3.10"
python-dotenv = "^1.0.1"
langchain-openai = "^0.2.4"
langgraph = "^0.2.39"
coinbase-agentkit = "0.1.2"
coinbase-agentkit = "^0.1.4"
coinbase-agentkit-langchain = "0.1.0"

[tool.poetry.scripts]
Expand Down