nvpn 4.1.13

CLI and daemon for Nostr VPN private mesh networks

struct PaidExitBuyResult {
    store_path: PathBuf,
    session: OpenPaidRouteBuyerSessionResult,
    selected_exit_node: Option<String>,
    daemon_reload_attempted: bool,
}

struct PaidExitUseResult {
    config_path: PathBuf,
    store_path: PathBuf,
    session_id: String,
    seller_npub: String,
    selected_exit_node: String,
    daemon_reload_attempted: bool,
}

fn paid_exit_buy_command(args: PaidExitBuyArgs) -> Result<()> {
    let json_output = args.json;
    let result = paid_exit_buy_once(args)?;

    if json_output {
        println!(
            "{}",
            serde_json::to_string_pretty(&paid_exit_buy_result_json(&result))?
        );
    } else {
        print_paid_exit_buy_result(&result);
    }

    Ok(())
}

fn paid_exit_buy_once(args: PaidExitBuyArgs) -> Result<PaidExitBuyResult> {
    let config_path = args.config.clone().unwrap_or_else(default_config_path);
    let mut app = load_or_default_config(&config_path)?;
    let buyer_npub = app
        .nostr_keys()?
        .public_key()
        .to_bech32()
        .context("failed to encode buyer npub")?;
    let store_path = paid_route_store_file_path(&config_path);
    let no_select_exit_node = args.no_select_exit_node;
    let no_reload_daemon = args.no_reload_daemon;
    let result = update_paid_route_store(&store_path, |store| {
        let offer_selector = paid_exit_buy_offer_selector(&args, store)?;
        store.open_buyer_session(OpenPaidRouteBuyerSessionRequest {
            offer_selector,
            buyer_npub,
            mint_url: args.mint,
            channel_capacity_sat: args.channel_capacity_sat,
            initial_paid_msat: args.initial_paid_msat,
            now_unix: unix_timestamp(),
        })
    })?;

    let (selected_exit_node, daemon_reload_attempted) = if no_select_exit_node {
        (None, false)
    } else {
        let endpoint_hints = load_paid_route_store(&store_path)?
            .buyer_session_seller_fips_endpoints(&result.session_id)?;
        app.add_fips_peer_endpoint_hints(&result.seller_npub, &endpoint_hints)?;
        let selected = app.select_public_paid_exit_node(&result.seller_npub)?;
        app.save(&config_path)?;
        let daemon_reload_attempted = !no_reload_daemon;
        if daemon_reload_attempted {
            maybe_reload_running_daemon(&config_path);
        }
        (Some(selected), daemon_reload_attempted)
    };

    Ok(PaidExitBuyResult {
        store_path,
        session: result,
        selected_exit_node,
        daemon_reload_attempted,
    })
}

fn paid_exit_buy_offer_selector(args: &PaidExitBuyArgs, store: &PaidRouteStore) -> Result<String> {
    match (args.best_rated, args.offer.as_deref()) {
        (true, Some(_)) => Err(anyhow!(
            "--best-rated cannot be combined with an explicit paid-exit offer selector"
        )),
        (true, None) => store.best_rated_offer_key(),
        (false, Some(offer)) => {
            let offer = offer.trim();
            if offer.is_empty() {
                Err(anyhow!("paid exit offer selector is empty"))
            } else {
                Ok(offer.to_string())
            }
        }
        (false, None) => Err(anyhow!(
            "paid exit offer selector is required unless --best-rated is supplied"
        )),
    }
}

fn paid_exit_use_command(args: PaidExitUseArgs) -> Result<()> {
    let json_output = args.json;
    let result = paid_exit_use_once(args)?;

    if json_output {
        println!(
            "{}",
            serde_json::to_string_pretty(&paid_exit_use_result_json(&result))?
        );
    } else {
        print_paid_exit_use_result(&result);
    }

    Ok(())
}

fn paid_exit_use_once(args: PaidExitUseArgs) -> Result<PaidExitUseResult> {
    let config_path = args.config.clone().unwrap_or_else(default_config_path);
    let mut app = load_or_default_config(&config_path)?;
    let store_path = paid_route_store_file_path(&config_path);
    let session_id = args.session.trim().to_string();
    if session_id.is_empty() {
        return Err(anyhow!("paid route session id is empty"));
    }
    let now_unix = unix_timestamp();
    let (seller_npub, endpoint_hints) = update_paid_route_store(&store_path, |store| {
        store.retry_failed_funded_buyer_session(&session_id, now_unix)?;
        if !store.buyer_session_allows_routing(&session_id, now_unix)? {
            return Err(anyhow!(
                "paid route session is not ready to route yet; fund it or wait for seller admission"
            ));
        }
        store.begin_buyer_session_open_attempt(&session_id, now_unix)?;
        let seller_npub = store.buyer_session_seller_npub(&session_id)?;
        let endpoint_hints = store.buyer_session_seller_fips_endpoints(&session_id)?;
        Ok((seller_npub, endpoint_hints))
    })?;
    app.add_fips_peer_endpoint_hints(&seller_npub, &endpoint_hints)?;
    let selected_exit_node = app.select_public_paid_exit_node(&seller_npub)?;
    app.save(&config_path)?;
    let daemon_reload_attempted = !args.no_reload_daemon;
    if daemon_reload_attempted {
        maybe_reload_running_daemon(&config_path);
    }

    Ok(PaidExitUseResult {
        config_path,
        store_path,
        session_id,
        seller_npub,
        selected_exit_node,
        daemon_reload_attempted,
    })
}

fn paid_exit_buy_result_json(result: &PaidExitBuyResult) -> serde_json::Value {
    json!({
        "store_path": result.store_path.display().to_string(),
        "session": result.session,
        "selected_exit_node": result.selected_exit_node,
        "daemon_reload_attempted": result.daemon_reload_attempted,
    })
}

fn paid_exit_use_result_json(result: &PaidExitUseResult) -> serde_json::Value {
    json!({
        "config_path": result.config_path.display().to_string(),
        "store_path": result.store_path.display().to_string(),
        "session_id": result.session_id,
        "seller_npub": result.seller_npub,
        "selected_exit_node": result.selected_exit_node,
        "daemon_reload_attempted": result.daemon_reload_attempted,
    })
}

fn print_paid_exit_buy_result(result: &PaidExitBuyResult) {
    println!("paid_exit_session: {}", result.session.session_id);
    println!("seller: {}", result.session.seller_npub);
    println!("offer: {}", result.session.offer_id);
    println!("mint: {}", display_or_none(&result.session.mint_url));
    println!(
        "channel: {} capacity={} expires_at={}",
        result.session.channel_id,
        paid_exit_sat_text(result.session.channel_capacity_sat),
        result.session.expires_at_unix
    );
    println!(
        "store: {} changed={}",
        result.store_path.display(),
        result.session.changed
    );
    if let Some(selected) = result.selected_exit_node.as_deref() {
        println!("selected_exit_node: {selected}");
    } else {
        println!("selected_exit_node: unchanged");
    }
    println!(
        "daemon_reload: {}",
        if result.daemon_reload_attempted {
            "attempted"
        } else {
            "skipped"
        }
    );
}

fn print_paid_exit_use_result(result: &PaidExitUseResult) {
    println!("paid_exit_session: {}", result.session_id);
    println!("seller: {}", result.seller_npub);
    println!("selected_exit_node: {}", result.selected_exit_node);
    println!("config: {}", result.config_path.display());
    println!("store: {}", result.store_path.display());
    println!(
        "daemon_reload: {}",
        if result.daemon_reload_attempted {
            "attempted"
        } else {
            "skipped"
        }
    );
}