zing-cli 0.0.2

CLI tool + MCP server for paid semantic search on Zing
Documentation
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use std::str::FromStr;

pub const DEFAULT_API_URL: &str = "https://search.zing.services";
pub const DEFAULT_PLATFORM_USDC_ADDRESS: &str =
    "0x9b1b8ff37a5fdc77141c58ca43a4800a82d6ce91cfaceb7ae7c62c7c80458299";

fn zing_config_dir() -> String {
    let home = std::env::var("HOME").unwrap_or_else(|_| "~".to_string());
    format!("{}/.zing/zing_config", home)
}

#[derive(Debug)]
pub struct ZingConfig {
    pub rpc_url: String,
    pub active_address: sui_sdk_types::Address,
    pub api_base_url: String,
    pub platform_usdc_address: sui_sdk_types::Address,
    pub keystore_path: PathBuf,
}

#[derive(Debug, Deserialize, Serialize)]
pub struct ZingClientConfig {
    pub keystore: ZingKeystorePath,
    pub active_address: Option<String>,
    #[serde(default)]
    pub active_env: Option<String>,
    #[serde(default)]
    pub envs: Vec<ZingEnv>,
}

#[derive(Debug, Deserialize, Serialize)]
pub struct ZingKeystorePath {
    #[serde(rename = "File")]
    pub file: String,
}

#[derive(Debug, Deserialize, Serialize)]
pub struct ZingEnv {
    pub alias: String,
    pub rpc: String,
}

pub fn load_config() -> anyhow::Result<ZingConfig> {
    let config_dir = zing_config_dir();
    let client_yaml_path = PathBuf::from(&config_dir).join("client.yaml");

    if !client_yaml_path.exists() {
        init_config(&config_dir, &client_yaml_path)?;
    }

    let client_yaml = std::fs::read_to_string(&client_yaml_path)
        .map_err(|e| anyhow::anyhow!("Cannot read Zing config at {}: {}", client_yaml_path.display(), e))?;
    let zing_config: ZingClientConfig = serde_yaml::from_str(&client_yaml)?;

    let rpc_url = zing_config
        .active_env
        .as_deref()
        .and_then(|alias| zing_config.envs.iter().find(|e| e.alias == alias))
        .map(|e| e.rpc.clone())
        .unwrap_or_else(|| "https://fullnode.mainnet.sui.io:443".to_string());

    let addr_str = zing_config
        .active_address
        .ok_or_else(|| anyhow::anyhow!("No active_address set in Zing config at {}", client_yaml_path.display()))?;
    let active_address = sui_sdk_types::Address::from_str(&addr_str)?;

    let keystore_path = PathBuf::from(&config_dir).join(&zing_config.keystore.file);

    let api_base_url = std::env::var("ZING_API_URL")
        .unwrap_or_else(|_| DEFAULT_API_URL.to_string());

    let platform_usdc_addr_str = std::env::var("ZING_PLATFORM_USDC_ADDRESS")
        .unwrap_or_else(|_| DEFAULT_PLATFORM_USDC_ADDRESS.to_string());
    let platform_usdc_address = sui_sdk_types::Address::from_str(&platform_usdc_addr_str)?;

    Ok(ZingConfig {
        rpc_url,
        active_address,
        api_base_url,
        platform_usdc_address,
        keystore_path,
    })
}

fn init_config(config_dir: &str, client_yaml_path: &Path) -> anyhow::Result<()> {
    use base64ct::{Base64, Encoding};
    use rand::RngCore;
    use sui_crypto::ed25519::Ed25519PrivateKey;

    std::fs::create_dir_all(config_dir)
        .map_err(|e| anyhow::anyhow!("Cannot create config directory {}: {}", config_dir, e))?;

    let mut key_bytes = [0u8; 32];
    rand::rngs::OsRng.fill_bytes(&mut key_bytes);
    let keypair = Ed25519PrivateKey::new(key_bytes);
    let address = keypair.public_key().derive_address();

    let mut raw = vec![0x00u8];
    raw.extend_from_slice(&key_bytes);
    let encoded = Base64::encode_string(&raw);
    let keystore_json = serde_json::to_string_pretty(&vec![encoded])?;
    let keystore_path = PathBuf::from(config_dir).join("zing.keystore");
    std::fs::write(&keystore_path, keystore_json).map_err(|e| {
        anyhow::anyhow!("Cannot write keystore to {}: {}", keystore_path.display(), e)
    })?;

    let config = ZingClientConfig {
        keystore: ZingKeystorePath {
            file: "zing.keystore".to_string(),
        },
        active_address: Some(address.to_string()),
        active_env: Some("mainnet".to_string()),
        envs: vec![ZingEnv {
            alias: "mainnet".to_string(),
            rpc: "https://fullnode.mainnet.sui.io:443".to_string(),
        }],
    };
    let config_yaml = serde_yaml::to_string(&config)?;
    std::fs::write(client_yaml_path, config_yaml).map_err(|e| {
        anyhow::anyhow!("Cannot write config to {}: {}", client_yaml_path.display(), e)
    })?;

    eprintln!(
        "Created new Zing wallet.\n  Address: {}\n\n\
         Fund this address with at least 0.01 USDC on Sui mainnet to use paid search.",
        address
    );

    Ok(())
}