Skip to content
Open
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
337 changes: 228 additions & 109 deletions backend/app/agents/executor.py

Large diffs are not rendered by default.

26 changes: 7 additions & 19 deletions backend/app/agents/onchain/swap.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@


def execute_swap(
agent_id: str,
wallet_address: str,
wallet_private_key: str,
from_token: Literal["USDC", "WETH", "CBBTC"],
to_token: Literal["USDC", "WETH", "CBBTC"],
amount: float,
Expand All @@ -48,7 +49,8 @@ def execute_swap(
Execute a swap using the uniswap-python library.

Args:
agent_id: Agent identifier (e.g., "agent_1")
wallet_address: Wallet address to execute swap from
wallet_private_key: Private key for signing (plain, not encrypted)
from_token: Source token symbol
to_token: Destination token symbol
amount: Amount to swap (in token units, not wei)
Expand All @@ -62,22 +64,8 @@ def execute_swap(
Raises:
ValueError: If swap fails or invalid parameters provided
"""
# Get agent private key from env
private_key_var = f"{agent_id.upper()}_PRIVATE_KEY"
agent_private_key = os.getenv(private_key_var)

if not agent_private_key:
raise ValueError(f"Missing environment variable: {private_key_var}")

# Get agent address
address_var = f"{agent_id.upper()}_ADDRESS"
agent_address = os.getenv(address_var)

if not agent_address:
raise ValueError(f"Missing environment variable: {address_var}")

# Get RPC URL
rpc_url = os.getenv("RPC_LOCAL", "http://127.0.0.1:8545")
rpc_url = os.getenv("RPC_URL")

# Validate tokens
from_token = from_token.upper()
Expand All @@ -103,8 +91,8 @@ def execute_swap(
try:
# Initialize Uniswap instance with V3
uniswap = Uniswap(
address=agent_address,
private_key=agent_private_key,
address=wallet_address,
private_key=wallet_private_key,
version=3,
provider=rpc_url,
web3=None # Let the library create the web3 instance
Expand Down
173 changes: 173 additions & 0 deletions backend/app/agents/onchain/tenderly.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
"""
Tenderly RPC integration for Virtual TestNet wallet funding.

Uses Tenderly's JSON-RPC API to fund wallets with ETH and ERC-20 tokens.
"""

import os
import requests


# Base mainnet token addresses
USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
WETH_ADDRESS = "0x4200000000000000000000000000000000000006"
CBBTC_ADDRESS = "0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf"


def get_rpc_url() -> str:
"""Get Tenderly Virtual TestNet RPC URL from environment."""
rpc_url = os.getenv("RPC_URL")
if not rpc_url:
raise ValueError("RPC_URL not set in environment")
return rpc_url


def fund_wallet_with_eth(wallet_address: str, amount_eth: float = 10.0) -> dict:
"""
Fund a wallet with native ETH on Tenderly Virtual TestNet.

Args:
wallet_address: The wallet address to fund (0x...)
amount_eth: Amount of ETH to send (default 10.0)

Returns:
Response dict with success status
"""
rpc_url = get_rpc_url()

# Convert ETH to Wei (1 ETH = 10^18 Wei)
amount_wei = int(amount_eth * 10**18)

payload = {
"jsonrpc": "2.0",
"method": "tenderly_setBalance",
"params": [[wallet_address], hex(amount_wei)],
"id": "1"
}

response = requests.post(rpc_url, json=payload, headers={"Content-Type": "application/json"})

if response.status_code == 200:
print(f"Funded {wallet_address} with {amount_eth} ETH")
return {"success": True, "amount": amount_eth, "address": wallet_address}
else:
print(f"Failed to fund wallet: {response.status_code} - {response.text}")
response.raise_for_status()


def fund_wallet_with_token(
wallet_address: str,
token_address: str,
amount: float,
decimals: int = 6
) -> dict:
"""
Fund a wallet with ERC-20 tokens on Tenderly Virtual TestNet.

Args:
wallet_address: The wallet address to fund (0x...)
token_address: The token contract address (0x...)
amount: Amount of tokens (human readable, e.g., 1000 for 1000 USDC)
decimals: Token decimals (USDC=6, WETH=18, CBBTC=8)

Returns:
Response dict with success status
"""
rpc_url = get_rpc_url()

# Convert amount to smallest unit (e.g., 1000 USDC -> 1000000000 for 6 decimals)
amount_smallest_unit = int(amount * 10**decimals)

payload = {
"jsonrpc": "2.0",
"method": "tenderly_setErc20Balance",
"params": [token_address, wallet_address, hex(amount_smallest_unit)],
"id": "1"
}

response = requests.post(rpc_url, json=payload, headers={"Content-Type": "application/json"})

if response.status_code == 200:
print(f"Funded {wallet_address} with {amount} tokens")
return {
"success": True,
"amount": amount,
"token": token_address,
"recipient": wallet_address
}
else:
print(f"Failed to fund wallet with tokens: {response.status_code} - {response.text}")
response.raise_for_status()


def fund_wallet_with_usdc(wallet_address: str, amount_usdc: float = 10000.0) -> dict:
"""
Fund a wallet with USDC on Tenderly Virtual TestNet.

Args:
wallet_address: The wallet address to fund (0x...)
amount_usdc: Amount of USDC (default 10,000)

Returns:
Response dict with success status
"""
return fund_wallet_with_token(
wallet_address=wallet_address,
token_address=USDC_ADDRESS,
amount=amount_usdc,
decimals=6 # USDC has 6 decimals
)


def reset_wallet_balance(wallet_address: str, usdc_amount: float = 10000.0, eth_amount: float = 1.0) -> dict:
"""
Reset a wallet's balance back to initial amounts.

Useful for testing - resets both ETH and USDC to starting values.

Args:
wallet_address: The wallet address to reset (0x...)
usdc_amount: USDC amount to reset to (default 10,000)
eth_amount: ETH amount to reset to (default 1.0)

Returns:
Dict with reset results
"""
return setup_agent_wallet(wallet_address, usdc_amount, eth_amount)


def setup_agent_wallet(wallet_address: str, initial_usdc: float = 10000.0, initial_eth: float = 1.0) -> dict:
"""
Complete wallet setup: fund with both ETH (for gas) and USDC (for trading).

Args:
wallet_address: The wallet address to setup (0x...)
initial_usdc: Starting USDC balance (default 10,000)
initial_eth: Starting ETH balance for gas (default 1.0)

Returns:
Dict with funding results
"""
print(f"Setting up wallet: {wallet_address}")

try:
# Fund with ETH for gas fees
eth_result = fund_wallet_with_eth(wallet_address, initial_eth)

# Fund with USDC for trading
usdc_result = fund_wallet_with_usdc(wallet_address, initial_usdc)

print(f"Wallet setup complete!")
print(f" ETH: {initial_eth}")
print(f" USDC: {initial_usdc}")

return {
"success": True,
"wallet": wallet_address,
"eth_funded": eth_result,
"usdc_funded": usdc_result
}

except Exception as e:
print(f"Failed to setup wallet: {str(e)}")
raise
14 changes: 6 additions & 8 deletions backend/app/agents/onchain/ts_swap_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,17 @@


def execute_ts_swap(
agent_id: str,
private_key: str,
from_token: Literal["USDC", "WETH", "CBBTC"],
to_token: Literal["USDC", "WETH", "CBBTC"],
amount: float,
slippage: int = 50
slippage: int = 50,
) -> Dict[str, any]:
"""
Execute a swap using the TypeScript implementation.

Args:
agent_id: Agent identifier (e.g., "agent_1")
private_key: The agent's private key for signing transactions
from_token: Source token symbol
to_token: Destination token symbol
amount: Amount to swap (in token units, not wei)
Expand All @@ -36,12 +36,10 @@ def execute_ts_swap(
Raises:
ValueError: If swap fails
"""
# Get agent private key from env
private_key_var = f"{agent_id.upper()}_PRIVATE_KEY"
agent_private_key = os.getenv(private_key_var)
if not private_key:
raise ValueError("private_key is required")

if not agent_private_key:
raise ValueError(f"Missing environment variable: {private_key_var}")
agent_private_key = private_key

# Get RPC URL (prioritize RPC_URL for testnet/mainnet, fallback to RPC_LOCAL for local dev)
rpc_url = os.getenv("RPC_URL") or os.getenv("RPC_LOCAL", "http://127.0.0.1:8545")
Expand Down
103 changes: 103 additions & 0 deletions backend/app/agents/onchain/wallet_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
"""
Wallet utility functions for agent wallet management.

Handles wallet generation, encryption, and transaction signing.
"""

from eth_account import Account
from cryptography.fernet import Fernet
import os
from typing import Dict


def generate_wallet() -> Dict[str, str]:
"""
Generate a new Ethereum wallet with private key and address.

Returns:
Dict with 'address' and 'private_key' keys
"""
account = Account.create()

# Get private key as bytes, then convert to hex (always 64 chars)
private_key_bytes = account.key
private_key_hex = private_key_bytes.hex()

return {
"address": account.address,
"private_key": private_key_hex
}


def get_encryption_key() -> bytes:
"""
Get the Fernet encryption key from environment.

Raises:
ValueError: If WALLET_ENCRYPTION_KEY not set in environment

Returns:
Encryption key as bytes
"""
encryption_key = os.getenv("WALLET_ENCRYPTION_KEY")

if not encryption_key:
new_key = Fernet.generate_key()
print(f"\n⚠️ WALLET_ENCRYPTION_KEY not found!")
print(f"Add this to your .env file:")
print(f"WALLET_ENCRYPTION_KEY={new_key.decode()}\n")
raise ValueError("WALLET_ENCRYPTION_KEY not set in environment")

return encryption_key.encode()


def encrypt_private_key(private_key: str) -> str:
"""
Encrypt a private key for secure database storage.

Args:
private_key: Plain private key as hex string

Returns:
Encrypted private key as string
"""
key = get_encryption_key()
cipher = Fernet(key)
encrypted = cipher.encrypt(private_key.encode())
return encrypted.decode()


def decrypt_private_key(encrypted_private_key: str) -> str:
"""
Decrypt a private key from database storage.

Args:
encrypted_private_key: Encrypted private key from database

Returns:
Plain private key as hex string
"""
key = get_encryption_key()
cipher = Fernet(key)
decrypted = cipher.decrypt(encrypted_private_key.encode())
return decrypted.decode()


def private_key_to_address(private_key: str) -> str:
"""
Derive Ethereum address from a private key.

Args:
private_key: Private key as hex string (with or without 0x prefix)

Returns:
Ethereum address (0x...)
"""
# Add 0x prefix if not present
if not private_key.startswith("0x"):
private_key = "0x" + private_key

account = Account.from_key(private_key)
return account.address


11 changes: 5 additions & 6 deletions backend/app/agents/tools/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,22 @@
)

from .market_data_tool import MarketDataTool
from .make_trade_tool import MakeTradeTool
from .trade_tool import TradeTool
from .tweet_post_tool import TweetPostTool
from .database_tool import DatabaseTool
from .plan_tool import PlanTool

# Deprecated: These tools are kept for backwards compatibility
# Use MakeTradeTool instead for simulated trading
from .trade_tool import TradeTool # Deprecated: on-chain execution
from .portfolio_tool import PortfolioTool # Deprecated: use MakeTradeTool
from .make_trade_tool import MakeTradeTool # Deprecated: use TradeTool for on-chain execution
from .portfolio_tool import PortfolioTool # Deprecated: use TradeTool

__all__ = [
"MarketDataTool",
"MakeTradeTool",
"TradeTool",
"TweetPostTool",
"DatabaseTool",
"PlanTool",
# Deprecated
"TradeTool",
"MakeTradeTool",
"PortfolioTool",
]
Loading