cipher-gate 0.3.0

Proxy RPC that routes signing requests to a browser wallet UI
//! Styled console output for cipher-gate.
//! Uses ANSI colors — degrades gracefully on dumb terminals.

// Colors
const RESET: &str = "\x1b[0m";
const BOLD: &str = "\x1b[1m";
const DIM: &str = "\x1b[2m";
const GREEN: &str = "\x1b[32m";
const YELLOW: &str = "\x1b[33m";
const BLUE: &str = "\x1b[34m";
const MAGENTA: &str = "\x1b[35m";
const CYAN: &str = "\x1b[36m";
const RED: &str = "\x1b[31m";
const WHITE: &str = "\x1b[97m";

fn timestamp() -> String {
    let now = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap_or_default();
    let secs = now.as_secs() % 86400;
    let h = secs / 3600;
    let m = (secs % 3600) / 60;
    let s = secs % 60;
    format!("{h:02}:{m:02}:{s:02}")
}

/// Print the startup banner with server config.
pub fn banner(
    rpc_url: &str,
    host: &str,
    proxy_port: u16,
    ui_port: u16,
    separate_ui: bool,
    exposed: bool,
) {
    // Display loopback binds as "localhost" but show the real address when exposed.
    let display_host = if exposed { host } else { "localhost" };
    eprintln!();
    eprintln!(
        "  {BOLD}{MAGENTA}cipher-gate{RESET}  {DIM}Proxy RPC with browser wallet signing{RESET}"
    );
    eprintln!("  {DIM}─────────────────────────────────────────{RESET}");
    eprintln!("  {DIM}Upstream   {RESET}{WHITE}{rpc_url}{RESET}");
    eprintln!("  {DIM}Proxy      {RESET}{CYAN}http://{display_host}:{proxy_port}{RESET}");
    if separate_ui {
        eprintln!("  {DIM}UI         {RESET}{CYAN}http://{display_host}:{ui_port}{RESET}");
    }
    if exposed {
        eprintln!(
            "  {RED}{RESET}  {RED}{BOLD}Exposed on {host} — reachable beyond this machine{RESET}"
        );
    }
    eprintln!();
}

/// Print chain ID after fetching from upstream.
pub fn chain_id(cid: &str) {
    let decimal = u64::from_str_radix(cid.trim_start_matches("0x"), 16)
        .map(|n| n.to_string())
        .unwrap_or_default();
    let name = chain_name(&decimal);
    if name.is_empty() {
        eprintln!("  {DIM}Chain      {RESET}{WHITE}{cid}{RESET} {DIM}({decimal}){RESET}");
    } else {
        eprintln!("  {DIM}Chain      {RESET}{WHITE}{name}{RESET} {DIM}({cid}){RESET}");
    }
    eprintln!();
}

pub fn chain_id_failed() {
    eprintln!("  {YELLOW}{RESET}  {DIM}Could not fetch chain ID from upstream{RESET}");
    eprintln!();
}

pub fn ready() {
    eprintln!("  {GREEN}{RESET}  {BOLD}Ready{RESET} {DIM}— waiting for connections{RESET}");
    eprintln!();
}

// ── Runtime events ──────────────────────────────────────────

pub fn ws_connected() {
    eprintln!(
        "  {DIM}{}{RESET}  {GREEN}{RESET}  {GREEN}Frontend connected{RESET}",
        timestamp()
    );
}

pub fn ws_disconnected() {
    eprintln!(
        "  {DIM}{}{RESET}  {RED}{RESET}  {DIM}Frontend disconnected{RESET}",
        timestamp()
    );
}

pub fn wallet_connected(address: &str) {
    let short = shorten_address(address);
    eprintln!(
        "  {DIM}{}{RESET}  {GREEN}{RESET}  Wallet connected  {CYAN}{short}{RESET}",
        timestamp()
    );
}

pub fn wallet_disconnected() {
    eprintln!(
        "  {DIM}{}{RESET}  {DIM}{RESET}  {DIM}Wallet disconnected{RESET}",
        timestamp()
    );
}

pub fn intercepted(method: &str) {
    eprintln!(
        "  {DIM}{}{RESET}  {YELLOW}{RESET}  Intercepted  {BOLD}{method}{RESET}",
        timestamp()
    );
}

pub fn simulation_passed(gas: &str) {
    eprintln!(
        "  {DIM}{}{RESET}  {GREEN}{RESET}  Simulation passed  {DIM}gas={gas}{RESET}",
        timestamp()
    );
}

pub fn simulation_failed(reason: &str) {
    eprintln!(
        "  {DIM}{}{RESET}  {RED}{RESET}  Simulation failed  {RED}{reason}{RESET}",
        timestamp()
    );
}

pub fn decoded(signature: &str) {
    eprintln!(
        "  {DIM}{}{RESET}  {BLUE}ƒ{RESET}  Decoded  {BLUE}{signature}{RESET}",
        timestamp()
    );
}

pub fn calldata_warning(msg: &str) {
    eprintln!(
        "  {DIM}{}{RESET}  {RED}{RESET}  {RED}{BOLD}{msg}{RESET}",
        timestamp()
    );
}

pub fn signed(method: &str) {
    eprintln!(
        "  {DIM}{}{RESET}  {GREEN}{RESET}  Signed  {GREEN}{method}{RESET}",
        timestamp()
    );
}

pub fn rejected(method: &str) {
    eprintln!(
        "  {DIM}{}{RESET}  {YELLOW}{RESET}  Rejected  {YELLOW}{method}{RESET}",
        timestamp()
    );
}

pub fn timeout(method: &str) {
    eprintln!(
        "  {DIM}{}{RESET}  {RED}{RESET}  Timed out  {DIM}{method}{RESET}",
        timestamp()
    );
}

pub fn waiting_for_frontend() {
    eprintln!(
        "  {DIM}{}{RESET}  {YELLOW}{RESET}  {DIM}Waiting for signing UI to connect{RESET}",
        timestamp()
    );
}

// ── Helpers ─────────────────────────────────────────────────

fn shorten_address(addr: &str) -> String {
    if addr.len() > 12 {
        format!("{}...{}", &addr[..6], &addr[addr.len() - 4..])
    } else {
        addr.to_string()
    }
}

fn chain_name(decimal: &str) -> &'static str {
    match decimal {
        "1" => "Ethereum",
        "5" => "Goerli",
        "10" => "Optimism",
        "56" => "BSC",
        "100" => "Gnosis",
        "137" => "Polygon",
        "250" => "Fantom",
        "8453" => "Base",
        "42161" => "Arbitrum",
        "42170" => "Arbitrum Nova",
        "43114" => "Avalanche",
        "11155111" => "Sepolia",
        "11155420" => "OP Sepolia",
        "84532" => "Base Sepolia",
        "421614" => "Arb Sepolia",
        _ => "",
    }
}