vios_app 0.1.1

Small JSON vaults: Argon2id + AES-GCM, with optional AAD binding.
Documentation
use std::{env, fs, process::exit};
use sha2::{Digest, Sha256};
use serde_json::json;

fn to_hex_ascii(s: &str) -> String {
    s.as_bytes().iter().map(|b| format!("{:02x}", b)).collect()
}
fn to_hex_ascii_bytes(bytes: &[u8]) -> String {
    bytes.iter().map(|b| format!("{:02x}", b)).collect()
}

fn main() {
    // --- Args ---
    let mut args = std::env::args().skip(1).collect::<Vec<_>>();
    if args.is_empty() {
        eprintln!("Usage: license_exporter <vault_path>");
        exit(2);
    }
    let vault_path = &args[0];

    // --- Env ---
    let rpc_url   = env::var("XRPL_RPC_URL").unwrap_or_else(|_| "https://s.altnet.rippletest.net:51234".to_string());
    let account   = env::var("XRPL_ADDRESS").unwrap_or_else(|_| {
        eprintln!("XRPL_ADDRESS is required in environment.");
        exit(2);
    });
    let secret    = env::var("XRPL_SEED").unwrap_or_else(|_| {
        eprintln!("XRPL_SEED is required in environment.");
        exit(2);
    });
    // Optional UX bits (not used by rippled directly, we keep here for extensibility)
    let license_text = env::var("VIOS_LICENSE_TEXT").unwrap_or_else(|_| "Emotionprint License v1".to_string());

    // --- Hash file ---
    let data = match fs::read(vault_path) {
        Ok(b) => b,
        Err(e) => {
            eprintln!("Failed to read {}: {}", vault_path, e);
            exit(1);
        }
    };
    let hash_hex = format!("{:x}", Sha256::digest(&data)); // 64 lowercase hex chars

    // --- Build memos ---
    // XRPL requires hex strings for MemoType and MemoData
    let memo_type_license = to_hex_ascii("license");
    let memo_type_sha256  = to_hex_ascii("sha256");
    let memo_data_license = to_hex_ascii(&license_text);
    // We want the literal ascii of the 64-char hex (e.g. "a3b4...") as memo data, so hex-encode those ascii bytes:
    let memo_data_sha256  = to_hex_ascii(&hash_hex);

    // --- Construct submit request ---
    let tx_json = json!({
        "TransactionType": "Payment",
        "Account": account,
        "Destination": account,
        "Amount": "1", // 1 drop
        "Memos": [
            { "Memo": { "MemoType": memo_type_license, "MemoData": memo_data_license } },
            { "Memo": { "MemoType": memo_type_sha256,  "MemoData": memo_data_sha256  } }
        ]
    });

    let req = json!({
        "method": "submit",
        "params": [{
            "secret": secret,
            "tx_json": tx_json
        }]
    });

    // --- Submit to XRPL ---
    let resp = match ureq::post(&rpc_url).send_json(req) {
        Ok(r) => match r.into_string() {
            Ok(s) => s,
            Err(e) => {
                eprintln!("XRPL response read error: {}", e);
                exit(1);
            }
        },
        Err(e) => {
            eprintln!("XRPL submit error: {}", e);
            exit(1);
        }
    };

    // --- Parse result & print a clean summary ---
    let v: serde_json::Value = match serde_json::from_str(&resp) {
        Ok(v) => v,
        Err(e) => {
            eprintln!("XRPL JSON parse error: {}\nRaw: {}", e, resp);
            exit(1);
        }
    };

    let engine = v.pointer("/result/engine_result").and_then(|x| x.as_str()).unwrap_or("<unknown>");
    let accepted = v.pointer("/result/engine_result_code").and_then(|x| x.as_i64()).unwrap_or(-999);
    let hash = v.pointer("/result/tx_json/hash").and_then(|x| x.as_str()).unwrap_or("");
    println!("--- VIOS License Anchor ---");
    println!("Vault: {}", vault_path);
    println!("SHA256: {}", hash_hex);
    println!("License: {}", license_text);
    println!("XRPL engine_result: {} (code {})", engine, accepted);
    if !hash.is_empty() {
        println!("TX Hash: {}", hash);
        println!("Explorer: https://testnet.xrpl.org/transactions/{}", hash);
    } else {
        // Fallback: try to find any 64-hex substring
        if let Some(h) = find_any_hex64(&v) {
            println!("TX Hash: {}", h);
            println!("Explorer: https://testnet.xrpl.org/transactions/{}", h);
        } else {
            println!("No TX hash field in response. Full response below:\n{}", resp);
            exit(1);
        }
    }
}

// fallback finder for any 64-hex token in the response JSON
fn find_any_hex64(v: &serde_json::Value) -> Option<String> {
    let s = v.to_string();
    let mut cur = String::new();
    for c in s.chars() {
        if c.is_ascii_hexdigit() {
            cur.push(c);
            if cur.len() == 64 { return Some(cur); }
        } else {
            cur.clear();
        }
    }
    None
}