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

    from litcoin import Agent

    agent = Agent(bankr_key="bk_YOUR_KEY", ai_key="sk-...", ai_url="https://api.venice.ai/api/v1")
    agent.mine(rounds=5)   # mines + relays automatically
    agent.claim()
"""

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",
    "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 "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.

    When ai_key is provided, the agent automatically relays AI inference requests
    in the background while mining, earning 2× mining weight per relay fulfill.
    Use no_relay=True to disable.

    Args:
        bankr_key:       Bankr API key (bk_...). Required.
        ai_key:          AI provider API key. Enables relay when set.
        ai_url:          AI provider base URL (default: Venice).
        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).
        no_relay:        Set True to disable relay even when ai_key is set.
        log_level:       Logging level (default: INFO).
    """

    def __init__(self, bankr_key, ai_key=None, ai_url=None, model=None,
                 anthropic_mode=False, coordinator_url=None, name=None,
                 no_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
        self._ai_config = {
            "key": ai_key or "",
            "url": ai_url or "https://api.venice.ai/api/v1",
            "model": model or "llama-3.3-70b",
            "anthropic_mode": anthropic_mode,
        }
        self._no_relay = no_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 manually. Called automatically by mine() if ai_key is set."""
        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

        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 no_relay is False, 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 if AI key is set
        if self._ai_config["key"] and not self._no_relay 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

    # ─── 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 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(),
        }

    # ─── 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

    # ─── 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(),
        }
