Files
hermes-agent/wallet/chains/__init__.py
Shannon Sands ffefd57719 feat: add wallet module — manager, policy engine, chain providers, tools, CLI
Phase 2 of the wallet architecture — crypto wallet functionality built
on top of the keystore.

Core components:
- wallet/manager.py: Wallet CRUD, balance checks, transaction execution.
  Private keys stored as sealed keystore secrets — only the manager reads
  them, and only to pass to chain providers for signing.
- wallet/policy.py: Transaction policy engine with spending limits, daily
  limits, rate limits, cooldown, recipient allow/blocklists, approval
  thresholds, and a kill switch (freeze/unfreeze).
- wallet/chains/: Abstract ChainProvider interface + EVM and Solana impls.
  EVM supports Ethereum, Base, Polygon, Arbitrum, Optimism + testnets.
  Solana supports mainnet + devnet.

Agent integration:
- tools/wallet_tool.py: 5 agent-facing tools (wallet_list, wallet_balance,
  wallet_send, wallet_history, wallet_estimate_gas). All return JSON,
  none expose private keys. wallet_send goes through the policy engine.
- toolsets.py: New 'wallet' toolset
- model_tools.py: wallet_tool added to discovery list

CLI:
- wallet/cli.py: Full CLI — create, create-agent, import, list, balance,
  send (with interactive confirmation), fund, history, freeze, unfreeze, status
- hermes_cli/main.py: 'hermes wallet' subcommand registered

Policy defaults:
- Agent wallets: 1.0 native/tx max, 5.0/day, 5 txns/hour, 30s cooldown,
  approval required above 0.5 native
- User wallets: owner approval required for all transactions

Tests: 100 passing (28 wallet + 72 keystore)
2026-03-29 08:38:21 +10:00

103 lines
2.8 KiB
Python

"""Abstract chain provider interface.
Each blockchain (EVM, Solana, etc.) implements this interface.
The wallet manager dispatches through it without knowing chain specifics.
"""
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from decimal import Decimal
from typing import List, Optional
@dataclass
class Balance:
"""Native token balance for a wallet."""
chain: str
address: str
balance: Decimal # In native units (ETH, SOL)
balance_raw: int # In smallest unit (wei, lamports)
symbol: str # "ETH", "SOL"
decimals: int # 18 for ETH, 9 for SOL
@dataclass
class TransactionResult:
"""Result of a submitted transaction."""
tx_hash: str
chain: str
status: str # "submitted" | "confirmed" | "failed"
explorer_url: str = ""
gas_used: Optional[int] = None
error: Optional[str] = None
@dataclass
class GasEstimate:
"""Gas/fee estimate for a transaction."""
chain: str
estimated_fee: Decimal # In native units
estimated_fee_raw: int # In smallest unit
symbol: str
@dataclass
class ChainConfig:
"""Configuration for a blockchain."""
chain_id: str # "ethereum", "base", "solana", etc.
display_name: str
symbol: str # Native token symbol
decimals: int
rpc_url: str
explorer_url: str # Base URL for tx explorer
is_testnet: bool = False
class ChainProvider(ABC):
"""Abstract interface for blockchain interaction."""
def __init__(self, config: ChainConfig):
self.config = config
@property
def chain_id(self) -> str:
return self.config.chain_id
@abstractmethod
def get_balance(self, address: str) -> Balance:
"""Get native token balance for an address."""
...
@abstractmethod
def send_transaction(
self,
from_private_key: str,
to_address: str,
amount: Decimal,
) -> TransactionResult:
"""Sign and broadcast a native token transfer.
Args:
from_private_key: Hex-encoded private key (provided by daemon, never by agent)
to_address: Recipient address
amount: Amount in native units (ETH, SOL)
Returns:
TransactionResult with tx hash and status
"""
...
@abstractmethod
def estimate_fee(self, from_address: str, to_address: str, amount: Decimal) -> GasEstimate:
"""Estimate transaction fee."""
...
@abstractmethod
def validate_address(self, address: str) -> bool:
"""Check if an address is valid for this chain."""
...
def explorer_tx_url(self, tx_hash: str) -> str:
"""Return the block explorer URL for a transaction."""
return f"{self.config.explorer_url}/tx/{tx_hash}"