"""
LitcoinAgent — High-level interface for the LITCOIN protocol.

    from litcoin import Agent

    agent = Agent(bankr_key="bk_YOUR_KEY", ai_key="sk-or-...", ai_url="https://openrouter.ai/api/v1")
    agent.mine(rounds=5)   # mines (comprehension + research)
    agent.claim()

    # To also serve relay (uses your API credits for other miners' inference):
    agent = Agent(bankr_key="bk_YOUR_KEY", ai_key="sk-...", relay=True)
    agent.mine()
"""

import uuid
import time
import json
import hashlib
import logging
import threading
from dataclasses import dataclass, field
from collections import deque

from litcoin.api import CoordinatorAPI, APIError, SDK_VERSION
from litcoin.auth import BankrAuth, AuthSession
from litcoin.solver import solve
from litcoin.researcher import Researcher

log = logging.getLogger("litcoin")

# ─── Contract addresses (Base mainnet) ─────────────────────────────────────

CONTRACTS = {
    "LITCOIN":         "0x316ffb9c875f900AdCF04889E415cC86b564EBa3",
    "USDC":            "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
    "STAKING":         "0xC9584Ce1591E8EB38EdF15C28f2FDcca97A3d3B7",
    "ORACLE":          "0x4f937937A3B7Ca046d0f2B5071782aFFC675241b",
    "LITCREDIT":       "0x33e3d328F62037EB0d173705674CE713c348f0a6",
    "VAULT_MANAGER":   "0xD23a9b32e38FABE2325e1d27f94EcCf0e4a2f058",
    "LIQUIDATOR":      "0xc8095b03914a3732f07b21b4Fd66a9C55F6F1F5f",
    "CLAIMS":          "0xF703DcF2E88C0673F776870fdb12A453927C6A5e",
    "COMPUTE_ESCROW":  "0x28C351FE1A37434DD63882dA51b5f4CBade71724",
    "GUILD":           "0xC377cbD6739678E0fae16e52970755f50AF55bD1",
    "FAUCET":          "0x1659875dE16090c84C81DF1BDba3c3B4df093557",
}

BASE_RPC_URLS = [
    "https://base-mainnet.core.chainstack.com/0e8fb1d0e1b81bc55e077fc1546d4a78",
    "https://base.llamarpc.com",
    "https://base-rpc.publicnode.com",
    "https://mainnet.base.org",
    "https://base-mainnet.g.alchemy.com/v2/ASFqRTDGZhQsL9wZPYd11",
]


@dataclass
class MiningStats:
    """Tracks mining + relay session statistics."""
    rounds: int = 0
    passes: int = 0
    fails: int = 0
    total_earned: int = 0
    relay_served: int = 0
    relay_failed: int = 0
    relay_earned: int = 0
    relay_tokens: int = 0
    start_time: float = field(default_factory=time.time)
    _lock: threading.Lock = field(default_factory=threading.Lock, repr=False)

    @property
    def elapsed(self):
        return time.time() - self.start_time

    @property
    def pass_rate(self):
        return self.passes / max(1, self.rounds) * 100

    def add_relay(self, tokens=0, reward=0):
        with self._lock:
            self.relay_served += 1
            self.relay_tokens += tokens
            self.relay_earned += reward

    def add_relay_fail(self):
        with self._lock:
            self.relay_failed += 1

    def summary(self):
        return {
            "rounds": self.rounds,
            "passes": self.passes,
            "fails": self.fails,
            "passRate": round(self.pass_rate, 1),
            "totalEarned": self.total_earned,
            "relayServed": self.relay_served,
            "relayFailed": self.relay_failed,
            "relayEarned": self.relay_earned,
            "relayTokens": self.relay_tokens,
            "totalCombined": self.total_earned + self.relay_earned,
            "elapsedSeconds": round(self.elapsed, 1),
        }


# ═══════════════════════════════════════════════════════════════════════════════
#  RELAY PROVIDER — WebSocket background thread
# ═══════════════════════════════════════════════════════════════════════════════

class RelayProvider:
    """
    WebSocket-based relay provider. Connects to coordinator, receives
    inference requests, processes them using the agent's own API key,
    returns signed responses. API key never leaves this machine.
    """

    def __init__(self, wallet, coordinator_url, auth_session, bankr, ai_config, stats):
        self.wallet = wallet
        self.coordinator_url = coordinator_url
        self.auth = auth_session
        self.bankr = bankr
        self.ai = ai_config  # {key, url, model, anthropic_mode}
        self.stats = stats
        self.ws = None
        self.running = False
        self.connected = False
        self._active = 0
        self._active_lock = threading.Lock()
        self._reconnect_delay = 5
        self._max_concurrent = 3
        self._token_budget = 1_000_000
        self._tokens_today = 0
        self._day_start = time.time()

        # Auto-detect provider name
        url = self.ai["url"].lower()
        if "openrouter" in url: self.provider = "openrouter"
        elif "bankr" in url or "bnkr" in url: self.provider = "bankr"
        elif "venice" in url: self.provider = "venice"
        elif "openai" in url: self.provider = "openai"
        elif "groq" in url: self.provider = "groq"
        elif "together" in url: self.provider = "together"
        elif "anthropic" in url: self.provider = "anthropic"
        elif "localhost" in url or "127.0.0.1" in url: self.provider = "local"
        else: self.provider = "unknown"

    def start(self):
        self.running = True
        self._thread = threading.Thread(target=self._run_loop, daemon=True)
        self._thread.start()

    def stop(self):
        self.running = False
        if self.ws:
            try: self.ws.close()
            except: pass

    def update_auth(self, new_token):
        # Auth session handles this automatically
        pass

    def _run_loop(self):
        try:
            import websocket
        except ImportError:
            log.warning("[relay] Installing websocket-client...")
            import subprocess, sys
            subprocess.check_call([sys.executable, "-m", "pip", "install",
                                   "websocket-client", "--break-system-packages", "-q"])
            import websocket

        while self.running:
            try:
                self._connect(websocket)
            except Exception as e:
                if not self.running: break
                log.error(f"[relay] Connection error: {e}")
                self.connected = False

            if not self.running: break
            log.info(f"[relay] Reconnecting in {self._reconnect_delay}s...")
            time.sleep(self._reconnect_delay)
            self._reconnect_delay = min(self._reconnect_delay * 2, 300)

    def _connect(self, ws_module):
        ws_base = self.coordinator_url.replace("https://", "wss://").replace("http://", "ws://")
        ws_url = f"{ws_base}/v1/compute/relay"
        log.info(f"[relay] Connecting to relay endpoint...")

        self.ws = ws_module.WebSocketApp(
            ws_url,
            header={
                "Authorization": f"Bearer {self.auth.token}",
                "X-Relay-Wallet": self.wallet,
                "X-Litcoin-SDK": SDK_VERSION,
            },
            on_open=self._on_open,
            on_message=self._on_message,
            on_error=self._on_error,
            on_close=self._on_close,
        )
        self.ws.run_forever(ping_interval=30, ping_timeout=10)

    def _on_open(self, ws):
        self.connected = True
        self._reconnect_delay = 5
        log.info("[relay] Connected — waiting for requests")
        self._send({
            "type": "register",
            "wallet": self.wallet,
            "provider": self.provider,
            "models": [self.ai["model"]],
            "max_concurrent": self._max_concurrent,
        })

    def _on_message(self, ws, message):
        try:
            msg = json.loads(message)
        except:
            return
        if msg.get("type") == "compute_request":
            threading.Thread(target=self._handle_request, args=(msg,), daemon=True).start()
        elif msg.get("type") == "health_check":
            self._send({"type": "health_check_ack"})
        elif msg.get("type") == "reward_notification":
            reward = msg.get("reward", 0)
            self.stats.add_relay(reward=reward)
            log.info(f"[relay] +{reward:,} LITCOIN (relay reward)")
        elif msg.get("type") == "registered":
            log.info(f"[relay] Registered as provider ({self.provider})")

    def _on_error(self, ws, error):
        if self.running:
            log.warning(f"[relay] WebSocket error: {error}")

    def _on_close(self, ws, close_code, close_msg):
        self.connected = False
        if self.running:
            log.info(f"[relay] Disconnected (code={close_code})")

    def _handle_request(self, msg):
        import requests as http

        request_id = msg.get("request_id", "?")
        prompt = msg.get("prompt", "")
        model_req = msg.get("model")
        max_tokens = msg.get("max_tokens", 1024)
        system_prompt = msg.get("system_prompt")
        start = time.time()

        with self._active_lock:
            if self._active >= self._max_concurrent:
                self._send_error(request_id, "busy", "At max concurrency")
                return
            self._active += 1

        # Budget check
        elapsed = time.time() - self._day_start
        if elapsed > 86400:
            self._tokens_today = 0
            self._day_start = time.time()
        if self._token_budget > 0 and self._tokens_today >= self._token_budget:
            self._send_error(request_id, "budget_exceeded", "Daily token budget hit")
            with self._active_lock: self._active -= 1
            return

        try:
            log.info(f"[relay] Serving {request_id[:8]}... ({len(prompt)} chars)")
            model = model_req or self.ai["model"]
            text, tokens = self._call_ai(http, prompt, model, max_tokens, system_prompt)

            if not text:
                self.stats.add_relay_fail()
                self._send_error(request_id, "inference_failed", "Empty response")
                return

            elapsed_ms = int((time.time() - start) * 1000)
            self._tokens_today += tokens

            if len(text.strip()) < 1:
                self.stats.add_relay_fail()
                self._send_error(request_id, "quality_failed", "Response too short")
                return

            resp_hash = hashlib.sha256(f"{request_id}:{text}".encode()).hexdigest()
            sig_msg = f"LITCOIN Compute Receipt\nRequest: {request_id}\nHash: {resp_hash}\nTokens: {tokens}"
            sig = self.bankr.sign(sig_msg)

            self._send({
                "type": "compute_response",
                "request_id": request_id,
                "response": text,
                "tokens_used": tokens,
                "response_hash": resp_hash,
                "signature": sig,
                "model_used": model,
                "latency_ms": elapsed_ms,
            })

            self.stats.add_relay(tokens=tokens)
            log.info(f"[relay] Done {request_id[:8]} — {tokens} tokens, {elapsed_ms}ms")

        except Exception as e:
            self.stats.add_relay_fail()
            log.error(f"[relay] Request {request_id[:8]} failed: {e}")
            self._send_error(request_id, "internal_error", str(e)[:200])

        finally:
            with self._active_lock:
                self._active -= 1

    def _call_ai(self, http, prompt, model, max_tokens, system_prompt=None):
        headers = {"Content-Type": "application/json"}
        anthropic_mode = self.ai.get("anthropic_mode", False)

        if anthropic_mode:
            headers["x-api-key"] = self.ai["key"]
            headers["anthropic-version"] = "2023-06-01"
            body = {"model": model, "max_tokens": max_tokens,
                    "messages": [{"role": "user", "content": prompt}]}
            if system_prompt: body["system"] = system_prompt
            url = f"{self.ai['url']}/messages"
        else:
            headers["Authorization"] = f"Bearer {self.ai['key']}"
            messages = []
            if system_prompt: messages.append({"role": "system", "content": system_prompt})
            messages.append({"role": "user", "content": prompt})
            body = {"model": model, "max_tokens": max_tokens,
                    "messages": messages, "temperature": 0.7}
            url = f"{self.ai['url']}/chat/completions"

        try:
            resp = http.post(url, headers=headers, json=body, timeout=120)
            if resp.status_code in (429, 402, 403):
                log.warning(f"[relay] API error: {resp.status_code}")
                return None, 0
            if resp.status_code >= 500:
                log.warning(f"[relay] API server error: {resp.status_code}")
                return None, 0
            data = resp.json()
            if anthropic_mode:
                text = data.get("content", [{}])[0].get("text", "")
                usage = data.get("usage", {})
                tokens = usage.get("input_tokens", 0) + usage.get("output_tokens", 0)
            else:
                text = data.get("choices", [{}])[0].get("message", {}).get("content", "")
                usage = data.get("usage", {})
                tokens = usage.get("total_tokens", 0)
                if tokens == 0: tokens = len(prompt.split()) + len(text.split())
            return text, tokens
        except http.exceptions.Timeout:
            log.warning("[relay] API timeout")
            return None, 0
        except Exception as e:
            log.error(f"[relay] API call failed: {e}")
            return None, 0

    def _send(self, msg):
        if self.ws and self.connected:
            try: self.ws.send(json.dumps(msg))
            except: pass

    def _send_error(self, request_id, code, message):
        self._send({"type": "compute_error", "request_id": request_id,
                     "error_code": code, "message": message})


# ═══════════════════════════════════════════════════════════════════════════════
#  AGENT
# ═══════════════════════════════════════════════════════════════════════════════

class Agent:
    """
    Full-protocol LITCOIN agent.

    Handles mining, relay, claiming, staking reads, compute requests, and stats —
    all through a single clean interface.

    Relay is OPT-IN. Set relay=True to enable. When enabled, your AI API key
    is used to serve inference requests to other miners on the network,
    earning 2× mining weight per relay fulfill.

    Args:
        bankr_key:       Bankr API key (bk_...). Required.
        ai_key:          AI provider API key. Required for research mining and relay.
        ai_url:          AI provider base URL (OpenRouter, Bankr LLM, or any OpenAI-compatible).
        model:           Model name (default: llama-3.3-70b).
        anthropic_mode:  Set True for Anthropic/Claude API format.
        coordinator_url: Coordinator base URL.
        name:            Human-readable agent name (for logging/demos).
        relay:           Set True to enable relay (serves inference using your API key).
        log_level:       Logging level (default: INFO).

    Security:
        - Relay is disabled by default. When enabled, your ai_key is used to
          fulfill inference requests from other participants on the network.
          This consumes your API credits. Enable only if you understand the cost.
        - Research mining generates and executes Python code locally. Code is
          sanitized to block dangerous imports. See https://litcoiin.xyz/docs#security
    """

    def __init__(self, bankr_key, ai_key=None, ai_url=None, model=None,
                 anthropic_mode=False, coordinator_url=None, name=None,
                 relay=False, log_level=logging.INFO):
        self.name = name or "Agent"
        self.bankr = BankrAuth(bankr_key)
        self.api = CoordinatorAPI(coordinator_url)
        self.auth = AuthSession(self.bankr, self.api)
        self.stats = MiningStats()
        self._mining = False
        self._stop_event = threading.Event()
        self._relay = None

        # AI config for relay -- auto-detect URL from key prefix if not provided
        _ai_url = ai_url
        if not _ai_url and ai_key:
            if ai_key.startswith("sk-or-"): _ai_url = "https://openrouter.ai/api/v1"
            elif ai_key.startswith("bk_"): _ai_url = "https://llm.bankr.bot/v1"
            elif ai_key.startswith("sk-ant-"): _ai_url = "https://api.anthropic.com/v1"
            elif ai_key.startswith("gsk_"): _ai_url = "https://api.groq.com/openai/v1"
            elif ai_key.startswith("sk-"): _ai_url = "https://api.openai.com/v1"
        self._ai_config = {
            "key": ai_key or "",
            "url": _ai_url or "https://openrouter.ai/api/v1",
            "model": model or "google/gemini-2.5-flash",
            "anthropic_mode": anthropic_mode,
        }
        self._relay_enabled = relay
        self._researcher = None

        logging.basicConfig(
            level=log_level,
            format=f"  [%(asctime)s] [{self.name}] %(message)s",
            datefmt="%H:%M:%S",
        )

    @property
    def wallet(self):
        """The agent's wallet address (resolved from Bankr)."""
        return self.bankr.wallet

    @property
    def relay_active(self):
        """Whether the relay provider is connected."""
        return self._relay is not None and self._relay.connected

    # ─── Relay ─────────────────────────────────────────────────────────────────

    def start_relay(self):
        """
        Start the relay provider. Must be explicitly enabled via relay=True.
        
        WARNING: This uses your AI API key to serve inference requests to other
        miners on the network. This consumes your API credits.
        """
        if not self._ai_config["key"]:
            log.warning("Cannot start relay — no AI API key set")
            return None
        if self._relay and self._relay.running:
            return self._relay

        log.warning("=" * 60)
        log.warning("RELAY ENABLED — your AI API key will be used to serve")
        log.warning("inference requests to other miners on the network.")
        log.warning("This consumes your API credits. Disable with relay=False.")
        log.warning("=" * 60)

        self._relay = RelayProvider(
            wallet=self.wallet,
            coordinator_url=self.api.base_url,
            auth_session=self.auth,
            bankr=self.bankr,
            ai_config=self._ai_config,
            stats=self.stats,
        )
        self._relay.start()
        log.info(f"Relay started ({self._ai_config['url'].split('//')[1].split('/')[0]} / {self._ai_config['model']})")
        return self._relay

    def stop_relay(self):
        """Stop the relay provider."""
        if self._relay:
            self._relay.stop()
            self._relay = None

    # ─── Mining ───────────────────────────────────────────────────────────────

    def mine(self, rounds=0, max_failures=5, delay=2, on_solve=None):
        """
        Start mining. Blocks until complete.

        If ai_key was provided and relay=True, relay starts automatically.
        When the daily mining cap is reached, the agent sleeps but relay stays
        active earning from inference requests.

        Args:
            rounds:       Number of rounds (0 = mine forever)
            max_failures: Stop after N consecutive failures
            delay:        Seconds between rounds
            on_solve:     Callback(result_dict) called after each solve attempt

        Returns:
            MiningStats for this session
        """
        self._mining = True
        self._stop_event.clear()
        consecutive_fails = 0
        infra_fails = 0

        # Auto-start relay ONLY if explicitly enabled
        if self._ai_config["key"] and self._relay_enabled and not self._relay:
            self.start_relay()

        relay_str = "mine + relay" if self.relay_active else "mine only"
        log.info(f"Mining started | wallet={self.wallet[:10]}... | mode={relay_str}")

        while self._mining and not self._stop_event.is_set():
            if rounds > 0 and self.stats.rounds >= rounds:
                log.info(f"Completed {rounds} rounds")
                break
            if consecutive_fails >= max_failures:
                log.error(f"{max_failures} consecutive failures — stopping")
                break

            result = self._mine_one_round()

            if result is None:
                # Infrastructure failure
                infra_fails += 1
                wait = min(30 * infra_fails, 300)
                log.warning(f"Infra failure, retry in {wait}s")
                self._stop_event.wait(wait)
                continue

            # Cap check — coordinator tells us to back off
            if result.get("_capped"):
                cap = result.get("dailyCap", 0)
                earned = result.get("earnedToday", 0)
                resets_at = result.get("resetsAt", "next epoch")
                log.warning(f"Daily cap reached ({earned:,}/{cap:,} LITCOIN)")
                if self.relay_active:
                    log.info("Relay still active — earning from inference requests")
                log.info(f"Resets at: {resets_at} | checking again in 5 min...")
                self._stop_event.wait(300)
                continue

            infra_fails = 0
            self.stats.rounds += 1

            if result.get("pass"):
                consecutive_fails = 0
                self.stats.passes += 1
                reward = result.get("reward", 0)
                self.stats.total_earned += reward

                cooldown = result.get("cooldownSeconds", delay)

                if reward > 0:
                    log.info(f"✓ PASS +{reward:,} LITCOIN | session: {self.stats.total_earned:,}")
                else:
                    log.warning(f"⚠ PASS +0 LITCOIN (pool may be exhausted)")

                # Post-submit cap check
                if result.get("capped") or result.get("poolExhausted"):
                    cap = result.get("dailyCap", 0)
                    earned = result.get("earnedToday", 0)
                    log.warning(f"Cap reached ({earned:,}/{cap:,} LITCOIN)")
                    if self.relay_active:
                        log.info("Relay still active — earning from inference requests")
                    self._stop_event.wait(cooldown)
                else:
                    self._stop_event.wait(cooldown)
            else:
                consecutive_fails += 1
                self.stats.fails += 1
                failed = result.get("failedConstraintIndices", [])
                log.warning(f"✗ FAIL constraints={failed}")
                self._stop_event.wait(delay)

            if on_solve:
                on_solve({**result, "stats": self.stats.summary()})

        self._mining = False
        combined = self.stats.total_earned + self.stats.relay_earned
        log.info(f"Mining stopped | {self.stats.passes}/{self.stats.rounds} passed | {combined:,} LITCOIN total")
        if self.stats.relay_served > 0:
            log.info(f"Relay: {self.stats.relay_served} served | {self.stats.relay_earned:,} LITCOIN")
        return self.stats

    def mine_async(self, **kwargs):
        """Start mining in a background thread. Returns the thread."""
        t = threading.Thread(target=self.mine, kwargs=kwargs, daemon=True)
        t.start()
        return t

    def stop(self):
        """Signal the mining loop and relay to stop."""
        self._mining = False
        self._stop_event.set()
        self.stop_relay()

    def _mine_one_round(self):
        """Execute a single mine-solve-submit cycle. Returns result dict or None."""
        nonce = uuid.uuid4().hex[:32]
        try:
            # Get challenge
            challenge = self.api.get_challenge(nonce, self.auth.token)

            # Check cap before solving (saves compute)
            if challenge.get("capped") or challenge.get("poolExhausted"):
                return {
                    "_capped": True,
                    "dailyCap": challenge.get("dailyCap", 0),
                    "earnedToday": challenge.get("earnedToday", 0),
                    "resetsAt": challenge.get("resetsAt", ""),
                }

            # Solve
            artifact = solve(challenge)
            if not artifact:
                return {"pass": False, "error": "solve_failed"}

            # Submit
            result = self.api.submit_solution(
                challenge["challengeId"], artifact, nonce, self.auth.token
            )
            return result

        except APIError as e:
            if e.status == 401:
                try:
                    self.auth.refresh()
                    challenge = self.api.get_challenge(nonce, self.auth.token)
                    if challenge.get("capped") or challenge.get("poolExhausted"):
                        return {
                            "_capped": True,
                            "dailyCap": challenge.get("dailyCap", 0),
                            "earnedToday": challenge.get("earnedToday", 0),
                            "resetsAt": challenge.get("resetsAt", ""),
                        }
                    artifact = solve(challenge)
                    if not artifact:
                        return {"pass": False, "error": "solve_failed"}
                    return self.api.submit_solution(
                        challenge["challengeId"], artifact, nonce, self.auth.token
                    )
                except Exception:
                    return None
            elif e.status == 403:
                log.warning(f"Forbidden: {e.message}")
                return None
            elif e.status >= 500:
                return None
            else:
                log.error(f"API error: {e}")
                return {"pass": False, "error": str(e)}
        except Exception as e:
            log.error(f"Unexpected error: {e}")
            return None

    # ─── Claims ───────────────────────────────────────────────────────────────

    def status(self):
        """Get this agent's claim status (earned, claimed, claimable)."""
        return self.api.claims_status(self.wallet)

    def claim(self):
        """
        Claim accumulated mining rewards via Bankr.

        Returns dict with txHash on success, or None.
        """
        log.info("Claiming rewards...")

        # Use coordinator-assisted Bankr claim
        try:
            result = self.api.claims_bankr(self.wallet)
            if result.get("txHash"):
                log.info(f"✓ Claimed! tx: {result['txHash']}")
            return result
        except APIError as e:
            log.warning(f"Bankr claim route failed ({e}), trying manual...")

        status = self.api.claims_status(self.wallet)
        if not status.get("claimableWei") or status["claimableWei"] == "0":
            log.info("Nothing to claim")
            return None

        sig_data = self.api.claims_sign(self.wallet)
        total_earned_wei = sig_data.get("totalEarnedWei")
        signature = sig_data.get("signature")

        if not total_earned_wei or not signature:
            log.error("Could not get claim signature")
            return None

        calldata = self._encode_claim(total_earned_wei, signature)
        result = self.bankr.submit_tx(
            "0xF703DcF2E88C0673F776870fdb12A453927C6A5e",
            calldata
        )
        if result.get("hash"):
            log.info(f"✓ Claim tx: {result['hash']}")
        return result

    def _encode_claim(self, total_earned_wei, signature_hex):
        """ABI-encode claim(uint256, bytes) calldata."""
        selector = "38926b6d"
        sig_bytes = bytes.fromhex(signature_hex.replace("0x", ""))
        total_earned = int(total_earned_wei)

        total_earned_hex = format(total_earned, '064x')
        offset_hex = format(64, '064x')
        sig_len_hex = format(len(sig_bytes), '064x')
        sig_hex = sig_bytes.hex()
        pad_len = (32 - len(sig_bytes) % 32) % 32
        sig_padded = sig_hex + "00" * pad_len

        return "0x" + selector + total_earned_hex + offset_hex + sig_len_hex + sig_padded

    # ─── Stats ────────────────────────────────────────────────────────────────

    def network_stats(self):
        """Get global network statistics."""
        return self.api.network_stats()

    def leaderboard(self, limit=20):
        """Get mining leaderboard."""
        return self.api.leaderboard(limit)

    def health(self):
        """Check coordinator health."""
        return self.api.health()

    # ─── Staking ──────────────────────────────────────────────────────────────

    def boost(self):
        """Get this agent's staking tier and mining boost."""
        return self.api.get_boost(self.wallet)

    def staking_yield(self):
        """Get this agent's staking yield info."""
        return self.api.staking_yield(self.wallet)

    def register_staker(self):
        """Register this wallet as a known staker for yield distribution."""
        return self.api.register_staker(self.wallet)

    def stake(self, tier):
        """
        Stake LITCOIN into a tier. Approves tokens, then stakes.

        Tiers: 1=Spark (1M), 2=Circuit (5M), 3=Core (50M), 4=Architect (500M)

        Args:
            tier: Tier number (1-4)

        Returns:
            dict with tx hash, or raises on failure
        """
        tier = int(tier)
        if tier < 1 or tier > 4:
            raise ValueError("Tier must be 1-4")

        TIER_AMOUNTS = {
            1: 1_000_000,
            2: 5_000_000,
            3: 50_000_000,
            4: 500_000_000,
        }
        TIER_NAMES = {1: "Spark", 2: "Circuit", 3: "Core", 4: "Architect"}
        amount_wei = TIER_AMOUNTS[tier] * 10**18

        log.info(f"Staking {TIER_AMOUNTS[tier]:,} LITCOIN → {TIER_NAMES[tier]} (tier {tier})")

        # Approve staking contract
        log.info("Approving tokens...")
        approve_result = self._approve(CONTRACTS["LITCOIN"], CONTRACTS["STAKING"], amount_wei)
        log.info(f"✓ Approved: {approve_result.get('hash', 'ok')}")

        # stake(uint8 _tier)
        calldata = "0x604f2177" + format(tier, '064x')
        result = self.bankr.submit_tx(CONTRACTS["STAKING"], calldata)
        log.info(f"✓ Staked into {TIER_NAMES[tier]}: {result.get('hash', 'submitted')}")
        return result

    def unstake(self):
        """
        Unstake LITCOIN. Only works after lock period expires.

        Returns:
            dict with tx hash, or raises on failure
        """
        log.info("Unstaking...")
        # unstake() — no args
        calldata = "0x2def6620"
        result = self.bankr.submit_tx(CONTRACTS["STAKING"], calldata)
        log.info(f"✓ Unstaked: {result.get('hash', 'submitted')}")
        return result

    def early_unstake(self, confirm=False):
        """
        Preview or execute early unstake with penalty.

        Args:
            confirm: If False, returns penalty preview. If True, executes.

        Returns:
            dict with penaltyAmount, returnAmount, penaltyPct (preview)
            or dict with tx hash (confirmed)
        """
        wallet = self.wallet
        # previewEarlyUnstake(address) — read-only to get penalty info
        preview_cd = "0x1ffeb771" + format(int(wallet, 16), '064x')
        preview_raw = self._rpc_call(CONTRACTS["STAKING"], preview_cd)

        if preview_raw and preview_raw != "0x":
            data = preview_raw[2:] if preview_raw.startswith("0x") else preview_raw
            penalty = int(data[0:64], 16) / 1e18
            returned = int(data[64:128], 16) / 1e18
            pct = int(data[128:192], 16) / 100

            if not confirm:
                log.info(f"Early unstake preview: penalty {pct:.1f}% — lose {penalty:,.0f}, get back {returned:,.0f}")
                return {
                    "preview": True,
                    "penaltyAmount": int(penalty),
                    "returnAmount": int(returned),
                    "penaltyPct": f"{pct:.1f}%",
                }

        if not confirm:
            raise Exception("Cannot preview early unstake — are you staked?")

        log.info("Executing early unstake with penalty...")
        # earlyUnstake() — no args
        calldata = "0xfac16858"
        result = self.bankr.submit_tx(CONTRACTS["STAKING"], calldata)
        log.info(f"✓ Early unstaked: {result.get('hash', 'submitted')}")
        return result

    # ─── Vaults ────────────────────────────────────────────────────────────────

    def open_vault(self, collateral):
        """
        Open a vault with LITCOIN collateral.

        Args:
            collateral: Amount of LITCOIN (in whole tokens, not wei)

        Returns:
            dict with tx hash
        """
        collateral_wei = int(collateral) * 10**18
        log.info(f"Opening vault with {int(collateral):,} LITCOIN collateral")

        # Approve vault manager
        log.info("Approving tokens...")
        self._approve(CONTRACTS["LITCOIN"], CONTRACTS["VAULT_MANAGER"], collateral_wei)

        # openVault(uint256 _collateral)
        calldata = "0x04a2ce7e" + format(collateral_wei, '064x')
        result = self.bankr.submit_tx(CONTRACTS["VAULT_MANAGER"], calldata)
        log.info(f"✓ Vault opened: {result.get('hash', 'submitted')}")
        return result

    def mint_litcredit(self, vault_id, amount):
        """
        Mint LITCREDIT against an open vault.

        Args:
            vault_id: Vault ID (from open_vault or vault_ids())
            amount:   LITCREDIT to mint (in whole tokens, not wei)

        Returns:
            dict with tx hash
        """
        amount_wei = int(amount) * 10**18
        log.info(f"Minting {int(amount):,} LITCREDIT from vault #{vault_id}")

        # mintLitCredit(uint256 _vaultId, uint256 _amount)
        calldata = "0x91b4ffb4" + format(int(vault_id), '064x') + format(amount_wei, '064x')
        result = self.bankr.submit_tx(CONTRACTS["VAULT_MANAGER"], calldata)
        log.info(f"✓ Minted LITCREDIT: {result.get('hash', 'submitted')}")
        return result

    def repay_debt(self, vault_id, amount):
        """
        Repay LITCREDIT debt on a vault.

        Args:
            vault_id: Vault ID
            amount:   LITCREDIT to repay (in whole tokens)

        Returns:
            dict with tx hash
        """
        amount_wei = int(amount) * 10**18
        log.info(f"Repaying {int(amount):,} LITCREDIT on vault #{vault_id}")

        # Approve LITCREDIT to vault manager
        self._approve(CONTRACTS["LITCREDIT"], CONTRACTS["VAULT_MANAGER"], amount_wei)

        # repayDebt(uint256 _vaultId, uint256 _amount)
        calldata = "0x015cb0a5" + format(int(vault_id), '064x') + format(amount_wei, '064x')
        result = self.bankr.submit_tx(CONTRACTS["VAULT_MANAGER"], calldata)
        log.info(f"✓ Repaid debt: {result.get('hash', 'submitted')}")
        return result

    def withdraw_collateral(self, vault_id, amount):
        """
        Withdraw LITCOIN collateral from a vault.

        Args:
            vault_id: Vault ID
            amount:   LITCOIN to withdraw (in whole tokens)

        Returns:
            dict with tx hash
        """
        amount_wei = int(amount) * 10**18
        log.info(f"Withdrawing {int(amount):,} LITCOIN from vault #{vault_id}")

        # withdrawCollateral(uint256 _vaultId, uint256 _amount)
        calldata = "0x767a7b05" + format(int(vault_id), '064x') + format(amount_wei, '064x')
        result = self.bankr.submit_tx(CONTRACTS["VAULT_MANAGER"], calldata)
        log.info(f"✓ Withdrew collateral: {result.get('hash', 'submitted')}")
        return result

    def close_vault(self, vault_id):
        """
        Close a vault. Must repay all debt first.

        Args:
            vault_id: Vault ID

        Returns:
            dict with tx hash
        """
        log.info(f"Closing vault #{vault_id}")

        # closeVault(uint256 _vaultId)
        calldata = "0x360c3288" + format(int(vault_id), '064x')
        result = self.bankr.submit_tx(CONTRACTS["VAULT_MANAGER"], calldata)
        log.info(f"✓ Vault closed: {result.get('hash', 'submitted')}")
        return result

    def open_vault_v2(self, collateral_token, amount):
        """
        Open a vault with any supported collateral (V2 multi-collateral).

        Args:
            collateral_token: "litcoin", "usdc", or a token address
            amount: Amount in whole units (e.g. 1000 for 1000 USDC or 10000000 for 10M LITCOIN)

        Returns:
            dict with tx hash
        """
        token_map = {
            "litcoin": CONTRACTS["LITCOIN"],
            "usdc": CONTRACTS["USDC"],
        }
        token_addr = token_map.get(collateral_token.lower(), collateral_token)
        is_usdc = token_addr.lower() == CONTRACTS["USDC"].lower()

        if is_usdc:
            amount_wei = int(amount * 1e6)  # USDC has 6 decimals
            log.info(f"Opening USDC vault with {amount} USDC")
            self._approve(CONTRACTS["USDC"], CONTRACTS["VAULT_MANAGER"], amount_wei)
        else:
            amount_wei = int(amount) * 10**18
            log.info(f"Opening LITCOIN vault with {int(amount):,} LITCOIN")
            self._approve(CONTRACTS["LITCOIN"], CONTRACTS["VAULT_MANAGER"], amount_wei)

        # openVaultV2(address _collateralToken, uint256 _collateral)
        padded_token = "000000000000000000000000" + token_addr[2:].lower()
        calldata = "0x6313f5fa" + padded_token + format(amount_wei, '064x')
        result = self.bankr.submit_tx(CONTRACTS["VAULT_MANAGER"], calldata)
        log.info(f"✓ Vault opened: {result.get('hash', 'submitted')}")
        return result

    def get_vault_token(self, vault_id):
        """
        Get the collateral token address for a vault.

        Args:
            vault_id: Vault ID

        Returns:
            Token address string (LITCOIN or USDC address)
        """
        selector = "0x08ee8d6a"
        calldata = selector + format(int(vault_id), '064x')
        for rpc in BASE_RPC_URLS:
            try:
                import requests as _req
                resp = _req.post(rpc, json={
                    "jsonrpc": "2.0", "id": 1, "method": "eth_call",
                    "params": [{"to": CONTRACTS["VAULT_MANAGER"], "data": calldata}, "latest"]
                }, timeout=8)
                r = resp.json().get("result", "0x")
                if r and len(r) >= 66:
                    return "0x" + r[26:66]
            except:
                continue
        return CONTRACTS["LITCOIN"]  # default to LITCOIN for V1 vaults

    def vault_ids(self):
        """
        Get this wallet's vault IDs via RPC call.

        Returns:
            list of vault IDs, or empty list
        """
        # getOwnerVaultIds(address) — read-only call via RPC
        selector = "0xa6f83a09"
        padded = "000000000000000000000000" + self.wallet[2:]
        calldata = selector + padded

        for rpc in BASE_RPC_URLS:
            try:
                import requests as _req
                r = _req.post(rpc, json={
                    "jsonrpc": "2.0", "id": 1, "method": "eth_call",
                    "params": [{"to": CONTRACTS["VAULT_MANAGER"], "data": calldata}, "latest"]
                }, timeout=8)
                result = r.json().get("result", "0x")
                if result == "0x" or len(result) <= 2:
                    return []
                # Decode dynamic uint256[] — skip offset (32B) + length (32B), then parse IDs
                data = result[2:]  # strip 0x
                if len(data) < 128:
                    return []
                count = int(data[64:128], 16)
                ids = []
                for i in range(count):
                    start = 128 + i * 64
                    ids.append(int(data[start:start+64], 16))
                return ids
            except:
                continue
        return []

    # ─── Escrow ────────────────────────────────────────────────────────────────

    def deposit_escrow(self, amount):
        """
        Deposit LITCREDIT into ComputeEscrow for compute spending.

        Args:
            amount: LITCREDIT to deposit (in whole tokens)

        Returns:
            dict with tx hash
        """
        amount_wei = int(amount) * 10**18
        log.info(f"Depositing {int(amount):,} LITCREDIT into escrow")

        # Approve escrow contract
        self._approve(CONTRACTS["LITCREDIT"], CONTRACTS["COMPUTE_ESCROW"], amount_wei)

        # deposit(uint256 _amount)
        calldata = "0xb6b55f25" + format(amount_wei, '064x')
        result = self.bankr.submit_tx(CONTRACTS["COMPUTE_ESCROW"], calldata)
        log.info(f"✓ Deposited to escrow: {result.get('hash', 'submitted')}")
        return result

    # ─── Helpers ───────────────────────────────────────────────────────────────

    def _approve(self, token, spender, amount_wei):
        """
        Approve a spender to transfer tokens. Used internally by stake/vault/escrow.

        Args:
            token:      Token contract address
            spender:    Spender contract address
            amount_wei: Amount in wei

        Returns:
            dict with tx hash
        """
        # approve(address spender, uint256 amount)
        calldata = "0x095ea7b3" + \
            "000000000000000000000000" + spender[2:].lower() + \
            format(amount_wei, '064x')
        return self.bankr.submit_tx(token, calldata)

    # ─── Compute ──────────────────────────────────────────────────────────────

    def compute(self, prompt, model=None, max_tokens=4096):
        """
        Submit a compute request to the relay network.
        Requires LITCREDIT balance for paid requests.

        Args:
            prompt:     The inference prompt
            model:      Model to use (optional, uses relay's default)
            max_tokens: Maximum output tokens

        Returns:
            Response dict with 'response', 'tokensUsed', etc.
        """
        return self.api.compute_request(prompt, model=model,
                                        max_tokens=max_tokens, wallet=self.wallet)

    def compute_status(self):
        """Get compute network health and stats."""
        return {
            "health": self.api.compute_health(),
            "providers": self.api.compute_providers(),
            "stats": self.api.compute_stats(),
        }

    # ═══════════════════════════════════════════════════════════════════════════
    #  ON-CHAIN READS (via Base RPC)
    # ═══════════════════════════════════════════════════════════════════════════

    def _rpc_call(self, contract, calldata):
        """Raw eth_call against Base RPC. Returns hex result or None."""
        import requests as _req
        for rpc in BASE_RPC_URLS:
            try:
                r = _req.post(rpc, json={
                    "jsonrpc": "2.0", "id": 1, "method": "eth_call",
                    "params": [{"to": contract, "data": calldata}, "latest"]
                }, timeout=8)
                result = r.json().get("result", "0x")
                if result and result != "0x":
                    return result
                return "0x" + "0" * 64
            except:
                continue
        return None

    def _pad_address(self, addr=None):
        """Pad an address to 32 bytes. Defaults to self.wallet."""
        a = (addr or self.wallet).lower()
        return "000000000000000000000000" + a[2:]

    def _decode_uint(self, hex_str, offset=0):
        """Decode a uint256 from hex at the given 32-byte slot offset."""
        if not hex_str or hex_str == "0x":
            return 0
        data = hex_str[2:] if hex_str.startswith("0x") else hex_str
        start = offset * 64
        if start + 64 > len(data):
            return 0
        return int(data[start:start+64], 16)

    def _decode_bool(self, hex_str, offset=0):
        return self._decode_uint(hex_str, offset) != 0

    def _to_tokens(self, wei_amount):
        """Convert wei to whole tokens (18 decimals)."""
        return wei_amount / 10**18

    # ─── Token Balances ───────────────────────────────────────────────────────

    def litcoin_balance(self, address=None):
        """Get LITCOIN balance in whole tokens."""
        r = self._rpc_call(CONTRACTS["LITCOIN"], "0x70a08231" + self._pad_address(address))
        return self._to_tokens(self._decode_uint(r))

    def litcredit_balance(self, address=None):
        """Get LITCREDIT balance in whole tokens."""
        r = self._rpc_call(CONTRACTS["LITCREDIT"], "0x70a08231" + self._pad_address(address))
        return self._to_tokens(self._decode_uint(r))

    def balance(self):
        """Get both LITCOIN and LITCREDIT balances."""
        return {
            "litcoin": self.litcoin_balance(),
            "litcredit": self.litcredit_balance(),
        }

    def faucet(self):
        """
        Request 5,000,000 free LITCOIN from the faucet (one-time per wallet).

        Returns:
            dict with success status
        """
        log.info("Requesting faucet tokens...")
        r = self._post("/v1/faucet", {"wallet": self.wallet})
        if r.get("error"):
            log.warning(f"Faucet: {r['error']}")
        else:
            log.info(f"✓ Faucet: received 5,000,000 LITCOIN")
        return r

    # ─── Staking Reads ────────────────────────────────────────────────────────

    def tier(self):
        """Get current staking tier (0=none, 1=Spark, 2=Circuit, 3=Core, 4=Architect)."""
        r = self._rpc_call(CONTRACTS["STAKING"], "0xb45aae52" + self._pad_address())
        return self._decode_uint(r)

    def stake_info(self):
        """Get full staking info: tier, amount, stakedAt, lockUntil, locked."""
        r = self._rpc_call(CONTRACTS["STAKING"], "0xc3453153" + self._pad_address())
        if not r or r == "0x":
            return {"tier": 0, "amount": 0, "stakedAt": 0, "lockUntil": 0, "locked": False}
        return {
            "tier": self._decode_uint(r, 0),
            "amount": self._to_tokens(self._decode_uint(r, 1)),
            "stakedAt": self._decode_uint(r, 2),
            "lockUntil": self._decode_uint(r, 3),
            "locked": self._decode_bool(r, 4),
        }

    def time_until_unlock(self):
        """Seconds until staking lock expires. 0 if unlocked or not staked."""
        r = self._rpc_call(CONTRACTS["STAKING"], "0xd0228e8f" + self._pad_address())
        return self._decode_uint(r)

    def collateral_ratio(self):
        """Get collateral ratio required for this wallet (based on tier). In basis points."""
        r = self._rpc_call(CONTRACTS["STAKING"], "0x299a0ea4" + self._pad_address())
        return self._decode_uint(r)

    def mining_boost(self):
        """Get mining boost for this wallet. 10000 = 1.0x, 11000 = 1.1x, etc."""
        r = self._rpc_call(CONTRACTS["STAKING"], "0x1e9afdb1" + self._pad_address())
        return self._decode_uint(r)

    def tier_config(self, tier):
        """Get requirements for a tier: stakeRequired, lockDuration, collateralRatioBps, miningBoostBps."""
        r = self._rpc_call(CONTRACTS["STAKING"], "0xc022c523" + format(int(tier), '064x'))
        return {
            "stakeRequired": self._to_tokens(self._decode_uint(r, 0)),
            "lockDuration": self._decode_uint(r, 1),
            "collateralRatioBps": self._decode_uint(r, 2),
            "miningBoostBps": self._decode_uint(r, 3),
        }

    def total_staked(self):
        """Total LITCOIN staked across all users."""
        r = self._rpc_call(CONTRACTS["STAKING"], "0x817b1cd2")
        return self._to_tokens(self._decode_uint(r))

    # ─── Staking Writes (missing) ─────────────────────────────────────────────

    def upgrade_tier(self, new_tier):
        """
        Upgrade to a higher staking tier. Must already be staked.
        Approves additional tokens needed, then upgrades.

        Args:
            new_tier: New tier number (must be higher than current)

        Returns:
            dict with tx hash
        """
        new_tier = int(new_tier)
        TIER_AMOUNTS = {1: 1_000_000, 2: 5_000_000, 3: 50_000_000, 4: 500_000_000}
        TIER_NAMES = {1: "Spark", 2: "Circuit", 3: "Core", 4: "Architect"}

        if new_tier not in TIER_AMOUNTS:
            raise ValueError("Tier must be 1-4")

        amount_wei = TIER_AMOUNTS[new_tier] * 10**18
        log.info(f"Upgrading to {TIER_NAMES[new_tier]} (tier {new_tier})")

        # Approve the full new tier amount (contract handles the diff)
        self._approve(CONTRACTS["LITCOIN"], CONTRACTS["STAKING"], amount_wei)

        # upgradeTier(uint8 _newTier)
        calldata = "0xa2949dd7" + format(new_tier, '064x')
        result = self.bankr.submit_tx(CONTRACTS["STAKING"], calldata)
        log.info(f"✓ Upgraded to {TIER_NAMES[new_tier]}: {result.get('hash', 'submitted')}")
        return result

    # ─── Vault Reads ──────────────────────────────────────────────────────────

    def vault_info(self, vault_id):
        """Get vault details: owner, collateral, debt, ratio, active."""
        r = self._rpc_call(CONTRACTS["VAULT_MANAGER"], "0x8c64ea4a" + format(int(vault_id), '064x'))
        if not r or r == "0x":
            return None
        collateral_wei = self._decode_uint(r, 1)
        debt_wei = self._decode_uint(r, 2)
        # Calculate ratio client-side (on-chain call reverts when debt=0)
        ratio_bps = 0
        if debt_wei == 0:
            ratio_bps = 10**15  # infinity — no debt
        else:
            cpi = self._decode_uint(self._rpc_call(CONTRACTS["ORACLE"], "0x3ad868db"))
            lit_price = self._decode_uint(self._rpc_call(CONTRACTS["VAULT_MANAGER"], "0xa5964535"))
            if cpi > 0 and lit_price > 0:
                ratio_bps = (collateral_wei * lit_price * 10000) // (debt_wei * cpi)
        return {
            "vaultId": vault_id,
            "owner": "0x" + r[2+24:2+64],
            "collateral": self._to_tokens(collateral_wei),
            "debt": self._to_tokens(debt_wei),
            "ratio": ratio_bps,
            "ratioPct": round(ratio_bps / 100, 2) if ratio_bps < 10**15 else float("inf"),
            "lastInterestUpdate": self._decode_uint(r, 3),
            "active": self._decode_bool(r, 4),
        }

    def vault_health(self, vault_id):
        """Get vault collateral ratio in basis points. Higher = healthier."""
        # Try on-chain first
        r = self._rpc_call(CONTRACTS["VAULT_MANAGER"], "0x31ef9daa" + format(int(vault_id), '064x'))
        ratio = self._decode_uint(r)
        if ratio > 0:
            return ratio
        # On-chain call reverts for 0-debt vaults — calculate from vault data
        info = self.vault_info(vault_id)
        return info["ratio"] if info else 0

    def max_mintable(self, vault_id):
        """Max LITCREDIT that can be minted from this vault."""
        r = self._rpc_call(CONTRACTS["VAULT_MANAGER"], "0x0725f4cb" + format(int(vault_id), '064x'))
        raw = self._decode_uint(r)
        # Adjust for 0.5% minting fee
        adjusted = raw * 10000 // 10050
        return self._to_tokens(adjusted)

    def is_liquidatable(self, vault_id):
        """Check if a vault can be liquidated."""
        r = self._rpc_call(CONTRACTS["VAULT_MANAGER"], "0x211a4443" + format(int(vault_id), '064x'))
        return self._decode_bool(r)

    def required_ratio(self, address=None):
        """Get required collateral ratio for an address. Based on staking tier."""
        r = self._rpc_call(CONTRACTS["VAULT_MANAGER"], "0xb0da9ddd" + self._pad_address(address))
        return self._decode_uint(r)

    def system_stats(self):
        """Get protocol-wide vault stats."""
        col = self._rpc_call(CONTRACTS["VAULT_MANAGER"], "0xf7ae253b")
        debt = self._rpc_call(CONTRACTS["VAULT_MANAGER"], "0x42a21375")
        return {
            "totalCollateral": self._to_tokens(self._decode_uint(col)),
            "totalDebt": self._to_tokens(self._decode_uint(debt)),
        }

    def vaults(self):
        """Get all vault IDs and their info for this wallet."""
        ids = self.vault_ids()
        return [self.vault_info(vid) for vid in ids]

    # ─── Vault Writes (missing: addCollateral) ───────────────────────────────

    def add_collateral(self, vault_id, amount):
        """
        Add more LITCOIN collateral to an existing vault.

        Args:
            vault_id: Vault ID
            amount:   LITCOIN to add (in whole tokens)

        Returns:
            dict with tx hash
        """
        amount_wei = int(amount) * 10**18
        log.info(f"Adding {int(amount):,} LITCOIN collateral to vault #{vault_id}")

        self._approve(CONTRACTS["LITCOIN"], CONTRACTS["VAULT_MANAGER"], amount_wei)

        # addCollateral(uint256 _vaultId, uint256 _amount)
        calldata = "0xa8f35adf" + format(int(vault_id), '064x') + format(amount_wei, '064x')
        result = self.bankr.submit_tx(CONTRACTS["VAULT_MANAGER"], calldata)
        log.info(f"✓ Added collateral: {result.get('hash', 'submitted')}")
        return result

    # ─── Oracle Reads ─────────────────────────────────────────────────────────

    def oracle_prices(self):
        """Get CPI and LITCOIN prices from oracle."""
        cpi = self._rpc_call(CONTRACTS["ORACLE"], "0x3ad868db")
        lit = self._rpc_call(CONTRACTS["ORACLE"], "0x1cb870fc")
        cpi_fresh = self._rpc_call(CONTRACTS["ORACLE"], "0x3a9af351")
        lit_fresh = self._rpc_call(CONTRACTS["ORACLE"], "0xdc4febb9")
        return {
            "cpiPrice": self._decode_uint(cpi) / 10**18 if cpi else 0,
            "litcoinPrice": self._decode_uint(lit) / 10**18 if lit else 0,
            "cpiFresh": self._decode_bool(cpi_fresh),
            "litcoinFresh": self._decode_bool(lit_fresh),
        }

    # ─── Escrow Reads ─────────────────────────────────────────────────────────

    def escrow_balance(self):
        """Get LITCREDIT balance in escrow (available for compute)."""
        r = self._rpc_call(CONTRACTS["COMPUTE_ESCROW"], "0x10098ad5" + self._pad_address())
        return self._to_tokens(self._decode_uint(r))

    def escrow_stats(self):
        """Get user's escrow stats: balance, deposited, burned, withdrawn, pending."""
        r = self._rpc_call(CONTRACTS["COMPUTE_ESCROW"], "0x4e43603a" + self._pad_address())
        if not r or r == "0x":
            return {"balance": 0, "deposited": 0, "burned": 0, "withdrawn": 0, "pendingAmount": 0, "pendingAt": 0}
        return {
            "balance": self._to_tokens(self._decode_uint(r, 0)),
            "deposited": self._to_tokens(self._decode_uint(r, 1)),
            "burned": self._to_tokens(self._decode_uint(r, 2)),
            "withdrawn": self._to_tokens(self._decode_uint(r, 3)),
            "pendingAmount": self._to_tokens(self._decode_uint(r, 4)),
            "pendingAt": self._decode_uint(r, 5),
        }

    def withdrawal_status(self):
        """Get pending withdrawal info: amount, requestedAt, availableAt, ready."""
        r = self._rpc_call(CONTRACTS["COMPUTE_ESCROW"], "0x7491687e" + self._pad_address())
        if not r or r == "0x":
            return {"amount": 0, "requestedAt": 0, "availableAt": 0, "ready": False}
        return {
            "amount": self._to_tokens(self._decode_uint(r, 0)),
            "requestedAt": self._decode_uint(r, 1),
            "availableAt": self._decode_uint(r, 2),
            "ready": self._decode_bool(r, 3),
        }

    # ─── Escrow Writes (missing) ──────────────────────────────────────────────

    def request_withdraw_escrow(self, amount):
        """
        Request withdrawal from escrow. Has a 15-minute delay.

        Args:
            amount: LITCREDIT to withdraw (in whole tokens)

        Returns:
            dict with tx hash
        """
        amount_wei = int(amount) * 10**18
        log.info(f"Requesting withdrawal of {int(amount):,} LITCREDIT from escrow")

        # requestWithdraw(uint256 amount)
        calldata = "0x745400c9" + format(amount_wei, '064x')
        result = self.bankr.submit_tx(CONTRACTS["COMPUTE_ESCROW"], calldata)
        log.info(f"✓ Withdrawal requested (15-min delay): {result.get('hash', 'submitted')}")
        return result

    def cancel_withdraw_escrow(self):
        """Cancel a pending escrow withdrawal."""
        log.info("Cancelling escrow withdrawal")
        calldata = "0x84b76824"
        result = self.bankr.submit_tx(CONTRACTS["COMPUTE_ESCROW"], calldata)
        log.info(f"✓ Withdrawal cancelled: {result.get('hash', 'submitted')}")
        return result

    def complete_withdraw_escrow(self):
        """Complete escrow withdrawal after delay period."""
        log.info("Completing escrow withdrawal")
        calldata = "0xf756fa21"
        result = self.bankr.submit_tx(CONTRACTS["COMPUTE_ESCROW"], calldata)
        log.info(f"✓ Withdrawal completed: {result.get('hash', 'submitted')}")
        return result

    # ─── Guild Reads ──────────────────────────────────────────────────────────

    def guild_membership(self):
        """Get this wallet's guild membership: guildId, deposited, guildName, tier, boost."""
        r = self._rpc_call(CONTRACTS["GUILD"], "0x2ada2596" + self._pad_address())
        if not r or r == "0x" or len(r) < 130:
            return {"guildId": 0, "deposited": 0, "joinedAt": 0, "guildName": "", "guildTier": 0, "guildBoostBps": 0}
        return {
            "guildId": self._decode_uint(r, 0),
            "deposited": self._to_tokens(self._decode_uint(r, 1)),
            "joinedAt": self._decode_uint(r, 2),
            "guildTier": self._decode_uint(r, 4),
            "guildBoostBps": self._decode_uint(r, 5),
        }

    def guild_info(self, guild_id):
        """Get guild details: name, leader, totalDeposited, memberCount, stakedTier, active, boost."""
        r = self._rpc_call(CONTRACTS["GUILD"], "0xd3e96693" + format(int(guild_id), '064x'))
        if not r or r == "0x" or len(r) < 130:
            return None
        return {
            "guildId": guild_id,
            "totalDeposited": self._to_tokens(self._decode_uint(r, 2)),
            "memberCount": self._decode_uint(r, 3),
            "stakedTier": self._decode_uint(r, 4),
            "active": self._decode_bool(r, 6),
            "miningBoostBps": self._decode_uint(r, 7),
        }

    def guild_lock_status(self, guild_id):
        """Check if guild is staked, locked, and time remaining."""
        r = self._rpc_call(CONTRACTS["GUILD"], "0x9ffa873c" + format(int(guild_id), '064x'))
        return {
            "staked": self._decode_bool(r, 0),
            "locked": self._decode_bool(r, 1),
            "timeRemaining": self._decode_uint(r, 2),
        }

    def guild_count(self):
        """Total number of guilds."""
        r = self._rpc_call(CONTRACTS["GUILD"], "0x0af87227")
        return self._decode_uint(r)

    def amount_needed_for_tier(self, guild_id, tier):
        """How many more tokens a guild needs to reach a tier."""
        calldata = "0x9bf6a88a" + format(int(guild_id), '064x') + format(int(tier), '064x')
        r = self._rpc_call(CONTRACTS["GUILD"], calldata)
        return self._to_tokens(self._decode_uint(r))

    # ─── Guild Writes ─────────────────────────────────────────────────────────

    def create_guild(self, name):
        """
        Create a new mining guild. You become the leader.

        Args:
            name: Guild name (string)

        Returns:
            dict with tx hash
        """
        log.info(f"Creating guild: {name}")

        # createGuild(string) — ABI encode dynamic string
        # selector + offset + length + padded string data
        name_bytes = name.encode("utf-8")
        name_hex = name_bytes.hex()
        pad_len = (32 - len(name_bytes) % 32) % 32
        name_padded = name_hex + "00" * pad_len

        calldata = "0x1c0e61dc" + \
            format(32, '064x') + \
            format(len(name_bytes), '064x') + \
            name_padded

        result = self.bankr.submit_tx(CONTRACTS["GUILD"], calldata)
        log.info(f"✓ Guild created: {result.get('hash', 'submitted')}")
        return result

    def join_guild(self, guild_id, amount):
        """
        Join a guild with a LITCOIN deposit.

        Args:
            guild_id: Guild ID to join
            amount:   LITCOIN to deposit (whole tokens)

        Returns:
            dict with tx hash
        """
        amount_wei = int(amount) * 10**18
        log.info(f"Joining guild #{guild_id} with {int(amount):,} LITCOIN")

        self._approve(CONTRACTS["LITCOIN"], CONTRACTS["GUILD"], amount_wei)

        # joinGuild(uint256 _guildId, uint256 _amount)
        calldata = "0xa3e77e2a" + format(int(guild_id), '064x') + format(amount_wei, '064x')
        result = self.bankr.submit_tx(CONTRACTS["GUILD"], calldata)
        log.info(f"✓ Joined guild: {result.get('hash', 'submitted')}")
        return result

    def add_guild_deposit(self, amount):
        """
        Add more LITCOIN to your guild deposit.

        Args:
            amount: LITCOIN to add (whole tokens)

        Returns:
            dict with tx hash
        """
        amount_wei = int(amount) * 10**18
        log.info(f"Adding {int(amount):,} LITCOIN to guild deposit")

        self._approve(CONTRACTS["LITCOIN"], CONTRACTS["GUILD"], amount_wei)

        # addDeposit(uint256 _amount)
        calldata = "0xa6f1fd51" + format(amount_wei, '064x')
        result = self.bankr.submit_tx(CONTRACTS["GUILD"], calldata)
        log.info(f"✓ Added guild deposit: {result.get('hash', 'submitted')}")
        return result

    def leave_guild(self):
        """Leave your current guild. Returns deposited tokens."""
        log.info("Leaving guild")
        calldata = "0x0d3dfa92"
        result = self.bankr.submit_tx(CONTRACTS["GUILD"], calldata)
        log.info(f"✓ Left guild: {result.get('hash', 'submitted')}")
        return result

    def stake_guild(self, tier):
        """
        Stake the guild into a tier (leader only).

        Args:
            tier: Tier number (1-4)

        Returns:
            dict with tx hash
        """
        tier = int(tier)
        log.info(f"Staking guild into tier {tier}")

        # stakeGuild(uint8 _tier)
        calldata = "0x3deb2bb7" + format(tier, '064x')
        result = self.bankr.submit_tx(CONTRACTS["GUILD"], calldata)
        log.info(f"✓ Guild staked: {result.get('hash', 'submitted')}")
        return result

    def upgrade_guild_tier(self, new_tier):
        """
        Upgrade guild to a higher tier (leader only).

        Args:
            new_tier: New tier number

        Returns:
            dict with tx hash
        """
        new_tier = int(new_tier)
        log.info(f"Upgrading guild to tier {new_tier}")

        # upgradeGuildTier(uint8 _newTier)
        calldata = "0x40c45dcc" + format(new_tier, '064x')
        result = self.bankr.submit_tx(CONTRACTS["GUILD"], calldata)
        log.info(f"✓ Guild upgraded: {result.get('hash', 'submitted')}")
        return result

    def unstake_guild(self):
        """Unstake the guild (leader only). Lock period must have expired."""
        log.info("Unstaking guild")
        calldata = "0x7b5f7292"
        result = self.bankr.submit_tx(CONTRACTS["GUILD"], calldata)
        log.info(f"✓ Guild unstaked: {result.get('hash', 'submitted')}")
        return result

    def transfer_guild_leadership(self, new_leader):
        """
        Transfer guild leadership to another address.

        Args:
            new_leader: New leader's wallet address

        Returns:
            dict with tx hash
        """
        log.info(f"Transferring guild leadership to {new_leader[:10]}...")

        # transferLeadership(address _newLeader)
        calldata = "0xc08dd257" + "000000000000000000000000" + new_leader[2:].lower()
        result = self.bankr.submit_tx(CONTRACTS["GUILD"], calldata)
        log.info(f"✓ Leadership transferred: {result.get('hash', 'submitted')}")
        return result

    # ─── Miner Status (V3) ────────────────────────────────────────────────────

    def miner_status(self, wallet=None):
        """
        Get comprehensive miner status: mining activity, relay status,
        earnings breakdown, guild membership, and health check.

        Args:
            wallet: Wallet address to check (defaults to own wallet)

        Returns:
            dict with active, relay, earnings, guild, health fields
        """
        w = wallet or self.wallet
        return self.api.miner_status(w)

    def guild_yield(self):
        """
        Get network-wide guild yield data: history, member yields, charts.

        Returns:
            dict with totalEntries, recentHistory, memberYields, cumulativeYield
        """
        return self.api.guild_yield()

    def my_guild_yield(self):
        """
        Get your personal guild yield history and share.

        Returns:
            dict with wallet, totalYield, entries, deposited, share, guildId
        """
        return self.api.guild_member_yield(self.wallet)

    def protocol_stats(self):
        """
        Get cached protocol stats: totalStaked, litcreditSupply, prices.

        Returns:
            dict with totalStaked, litcreditSupply, cpiPrice, litcoinPrice, etc.
        """
        return self.api.protocol_stats()

    # ─── LITCREDIT Reads ──────────────────────────────────────────────────────

    def litcredit_supply(self):
        """Get LITCREDIT supply stats."""
        total = self._rpc_call(CONTRACTS["LITCREDIT"], "0x18160ddd")
        minted = self._rpc_call(CONTRACTS["LITCREDIT"], "0xa2309ff8")
        burned = self._rpc_call(CONTRACTS["LITCREDIT"], "0xd89135cd")
        return {
            "totalSupply": self._to_tokens(self._decode_uint(total)),
            "totalMinted": self._to_tokens(self._decode_uint(minted)),
            "totalBurned": self._to_tokens(self._decode_uint(burned)),
        }

    # ─── Research Mining ─────────────────────────────────────────────────────

    @property
    def researcher(self):
        """Lazy-init researcher with AI config."""
        if self._researcher is None:
            self._researcher = Researcher(self.api, self.wallet, self._ai_config)
        return self._researcher

    def research_mine(self, task_type=None, task_id=None):
        """
        Run one research mining cycle: fetch task, generate solution, test, submit.

        Args:
            task_type: Filter by type ('code_optimization', 'algorithm', 'ml_training', etc.)
            task_id: Specific task ID to work on

        Returns:
            dict with task, code, local_result, submission
        """
        return self.researcher.mine_once(task_type=task_type, task_id=task_id)

    def research_loop(self, task_type=None, task_id=None, rounds=10, delay=30):
        """
        Run autonomous research mining loop (Karpathy-style iteration).
        
        Locks onto one task and iterates on it repeatedly.
        Each round builds on the previous — this is where breakthroughs happen.

        Args:
            task_type: Filter by type (optional)
            task_id: Specific task to iterate on (recommended)
            rounds: Number of iterations (default 10)
            delay: Seconds between iterations (default 30)
        """
        self.researcher.mine_loop(task_type=task_type, task_id=task_id, rounds=rounds, delay=delay)

    def research_history(self, task_id=None):
        """Get your iteration history on research tasks."""
        return self.api.research_history(self.wallet, task_id)

    def research_tasks(self, task_type=None):
        """List available research tasks."""
        return self.researcher.list_tasks(task_type)

    def research_leaderboard(self, task_id=None):
        """Get research leaderboard."""
        return self.researcher.leaderboard(task_id)

    def research_stats(self):
        """Get global research stats."""
        return self.researcher.stats()

    # ─── Full Protocol Snapshot ───────────────────────────────────────────────

    def snapshot(self):
        """
        Full protocol snapshot for this agent. One call to see everything.

        Returns dict with: balances, staking, vaults, escrow, guild, oracle, network.
        """
        log.info("Taking protocol snapshot...")
        return {
            "wallet": self.wallet,
            "balances": self.balance(),
            "staking": self.stake_info(),
            "miningBoost": self.mining_boost(),
            "vaults": self.vaults(),
            "escrow": self.escrow_stats(),
            "guild": self.guild_membership(),
            "oracle": self.oracle_prices(),
            "network": self.network_stats(),
        }
