mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-03 01:07:31 +08:00
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)
103 lines
2.8 KiB
Python
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}"
|