mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-03 09:17:09 +08:00
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)
This commit is contained in:
102
wallet/chains/__init__.py
Normal file
102
wallet/chains/__init__.py
Normal file
@@ -0,0 +1,102 @@
|
||||
"""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}"
|
||||
Reference in New Issue
Block a user