nvpn 4.1.13

CLI and daemon for Nostr VPN private mesh networks
#[cfg(feature = "paid-exit")]
#[test]
fn disabled_seller_keeps_existing_channel_control_transport_alive() {
    use nostr_vpn_core::paid_route_store::{
        PaidRouteChannelRecord, PaidRouteChannelRole, PaidRouteLifecycleStatus, PaidRouteStore,
        update_paid_route_store,
    };
    use nostr_vpn_core::paid_routes::PaidExitConfig;

    let nonce = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .expect("clock is after epoch")
        .as_nanos();
    let dir = std::env::temp_dir().join(format!("nvpn-paid-exit-disabled-settlement-{nonce}"));
    std::fs::create_dir_all(&dir).expect("create test dir");
    let config_path = dir.join("config.toml");

    let mut app = AppConfig::generated();
    app.fips_host_tunnel_enabled = false;
    app.connect_to_non_roster_fips_peers = false;
    app.paid_exit.enabled = false;
    for network in &mut app.networks {
        network.listen_for_join_requests = false;
    }
    assert!(!fips_private_runtime_active(&app, false, 0));

    let buyer = Keys::generate();
    let buyer_npub = buyer.public_key().to_bech32().expect("buyer npub");
    let mut accepted_terms = PaidExitConfig {
        enabled: true,
        ..PaidExitConfig::default()
    };
    accepted_terms.channel.accepted_mints = vec!["https://mint.example".to_string()];
    let mut store = PaidRouteStore::default();
    store.channels.insert(
        "seller-channel-1".to_string(),
        PaidRouteChannelRecord {
            channel_id: "seller-channel-1".to_string(),
            offer_id: "internet-exit".to_string(),
            role: PaidRouteChannelRole::Seller,
            status: PaidRouteLifecycleStatus::Active,
            payment: Default::default(),
            accepted_terms: Some(accepted_terms),
            mint_url: "https://mint.example".to_string(),
            counterparty_npub: buyer_npub.clone(),
            created_at_unix: 100,
            expires_at_unix: 500,
            updated_at_unix: 100,
            error: String::new(),
        },
    );
    let store_path = paid_route_store_file_path(&config_path);
    update_paid_route_store(&store_path, |target| {
        *target = store.clone();
        Ok(())
    })
    .expect("persist seller channel");

    assert!(
        fips_private_runtime_active_for_config(&app, &config_path, false, 0)
            .expect("inspect seller settlement runtime"),
        "an existing seller channel must keep its payment transport alive"
    );

    let network_id = app.effective_network_id();
    let own_npub = app
        .nostr_keys()
        .expect("seller keys")
        .public_key()
        .to_bech32()
        .expect("seller npub");
    let own_pubkey = app.own_nostr_pubkey_hex().expect("seller pubkey");
    let mut recent_peers = nostr_vpn_core::recent_peers::RecentPeerEndpoints::new(
        own_npub,
        nostr_vpn_core::recent_peers::recent_peers_scope(&network_id),
    )
    .expect("recent peers cache");
    assert!(recent_peers.note_success(&buyer.public_key().to_hex(), "203.0.113.41:51821", 100));
    let config = fips_tunnel_config_from_app(FipsTunnelConfigInput {
        app: &app,
        config_path: &config_path,
        network_id: &network_id,
        iface: "utun-test".to_string(),
        underlay_interface: None,
        underlay_interface_mtu: None,
        own_pubkey: Some(&own_pubkey),
        recent_peers: Some(&recent_peers),
        live_peer_endpoints: &[],
        ethernet_underlay: None,
    })
    .expect("build disabled seller settlement config");
    let buyer_control_peer = config
        .endpoint_peers
        .iter()
        .find(|peer| peer.npub == buyer_npub)
        .expect("existing buyer remains a control peer");
    assert!(buyer_control_peer.auto_reconnect);
    assert!(config.paid_route_admissions.is_empty());
    assert!(
        config
            .peers
            .iter()
            .all(|peer| peer.participant_pubkey != buyer.public_key().to_hex()),
        "disabled selling must not restore buyer routing admission"
    );

    store
        .channels
        .get_mut("seller-channel-1")
        .expect("seller channel")
        .status = PaidRouteLifecycleStatus::Closed;
    update_paid_route_store(&store_path, |target| {
        *target = store;
        Ok(())
    })
    .expect("persist closed seller channel");
    assert!(
        !fips_private_runtime_active_for_config(&app, &config_path, false, 0)
            .expect("inspect closed seller runtime"),
        "terminal seller channels must release an otherwise idle control runtime"
    );

    let _ = std::fs::remove_dir_all(&dir);
}

#[cfg(feature = "paid-exit")]
#[tokio::test]
async fn paid_exit_settle_signs_manual_cooperative_close_from_wallet() {
    use cashu_service::{CashuSpilmanPayment, CashuSpilmanPaymentSigner};
    use nostr_sdk::prelude::{Keys, ToBech32};
    use nostr_vpn_core::paid_route_store::{
        OpenPaidRouteBuyerSessionRequest, PaidRouteBuyerPaymentUpdatesDueRequest,
        PaidRouteLifecycleStatus, PaidRouteStore, RecordPaidRouteBuyerUsageRequest,
        update_paid_route_store,
    };
    use nostr_vpn_core::paid_routes::{
        PaidExitConfig, PaidRouteUsage, signed_paid_exit_offer_from_config,
    };
    use serde_json::json;

    let nonce = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .expect("clock is after epoch")
        .as_nanos();
    let dir = std::env::temp_dir().join(format!("nvpn-paid-exit-settle-{nonce}"));
    std::fs::create_dir_all(&dir).expect("create test dir");
    let config_path = dir.join("config.toml");

    let mut app = AppConfig::generated();
    app.fips_host_tunnel_enabled = false;
    for network in &mut app.networks {
        network.listen_for_join_requests = false;
    }
    let buyer_keys = app.nostr_keys().expect("buyer keys");
    let buyer_npub = buyer_keys.public_key().to_bech32().expect("buyer npub");
    let seller = Keys::generate();
    app.select_public_paid_exit_node(&seller.public_key().to_hex())
        .expect("select seller");
    let mut offer_config = PaidExitConfig {
        enabled: true,
        ..PaidExitConfig::default()
    };
    offer_config.pricing.price_msat_per_gb = 1_000;
    offer_config.channel.accepted_mints = vec!["https://mint.example".to_string()];
    offer_config.channel.free_probe_units = 0;
    offer_config.channel.grace_units = 0;
    let signed_offer =
        signed_paid_exit_offer_from_config("internet-exit", &seller, &offer_config, None, 123)
            .expect("sign offer");

    let mut store = PaidRouteStore::default();
    store.upsert_wallet_mint("https://mint.example", "Example", None, 122);
    store
        .upsert_signed_offer(signed_offer, vec!["wss://relay.example".to_string()], 124)
        .expect("store offer");
    let session = store
        .open_buyer_session(OpenPaidRouteBuyerSessionRequest {
            offer_selector: "internet-exit".to_string(),
            buyer_npub: buyer_npub.clone(),
            mint_url: Some("https://mint.example".to_string()),
            channel_capacity_sat: Some(10),
            initial_paid_msat: 0,
            now_unix: 125,
        })
        .expect("open buyer session");
    store
        .record_buyer_usage(RecordPaidRouteBuyerUsageRequest {
            seller_pubkey: seller.public_key().to_hex(),
            usage_delta: PaidRouteUsage {
                rx_bytes: 60,
                tx_bytes: 50,
                billable_bytes: 110,
                ..PaidRouteUsage::default()
            },
            now_unix: 126,
        })
        .expect("record buyer usage")
        .expect("matched buyer session");

    let wallet_data_dir = dir.join("wallet");
    let result = paid_exit_settle_with_signer(PaidExitSettleRequest {
        app: &app,
        config_path: &config_path,
        store: &mut store,
        signer: &RuntimeFakePaymentSigner,
        session_id: &session.session_id,
        dry_run: false,
        wallet_data_dir: &wallet_data_dir,
        now_unix: 128,
    })
    .expect("settle channel");
    update_paid_route_store(&paid_route_store_file_path(&config_path), |target| {
        *target = store.clone();
        Ok(())
    })
    .expect("persist closing channel");
    let queued_payments = load_paid_exit_payment_outbox(&config_path);
    assert_eq!(queued_payments.len(), 1);
    let payment_id = queued_payments[0].id.clone();

    assert!(result.payment.changed);
    assert_eq!(result.payment.payload_type, "cooperative_close");
    assert_eq!(result.payment.session_id, session.session_id);
    assert_eq!(result.payment.channel_id, session.channel_id);
    assert_eq!(result.payment.lease_id, session.lease_id);
    assert_eq!(result.payment.delivered_units, 110);
    assert_eq!(result.payment.amount_due_msat, 1);
    assert_eq!(result.payment.paid_msat, 1_000);
    assert!(!result.dry_run);
    assert_eq!(result.queued, Some(true));
    assert!(result.persisted);

    let channel = store.channels.get(&session.channel_id).expect("channel");
    assert_eq!(channel.status, PaidRouteLifecycleStatus::Closing);
    assert_eq!(channel.payment.paid_msat, 1_000);
    assert_eq!(
        channel
            .payment
            .cashu_spilman_payment
            .as_ref()
            .map(|payment| payment.balance),
        Some(1)
    );
    let expected_signature = format!("closed-{}-1", session.channel_id);
    assert_eq!(
        channel
            .payment
            .cashu_spilman_payment
            .as_ref()
            .map(|payment| payment.signature.as_str()),
        Some(expected_signature.as_str())
    );
    let lease = store.leases.get(&session.lease_id).expect("lease");
    assert_eq!(lease.status, PaidRouteLifecycleStatus::Closing);
    assert!(
        store
            .buyer_payment_updates_due(PaidRouteBuyerPaymentUpdatesDueRequest {
                now_unix: 129,
                min_increment_msat: 1,
            })
            .is_empty()
    );

    app.set_internet_source(InternetSource::Direct);
    assert!(
        !app.connect_to_non_roster_fips_peers,
        "leaving paid mode must release implicit market discovery ownership"
    );
    assert!(!fips_private_runtime_active(&app, false, 0));
    assert!(
        fips_private_runtime_active_for_config(&app, &config_path, false, 0)
            .expect("inspect pending close runtime"),
        "queued cooperative close must keep its control transport alive"
    );
    let seller_npub = seller.public_key().to_bech32().expect("seller npub");
    let network_id = app.effective_network_id();
    let own_pubkey = app.own_nostr_pubkey_hex().expect("buyer pubkey");
    let mut recent_peers = nostr_vpn_core::recent_peers::RecentPeerEndpoints::new(
        buyer_npub,
        nostr_vpn_core::recent_peers::recent_peers_scope(&network_id),
    )
    .expect("recent peers cache");
    assert!(recent_peers.note_success(&seller.public_key().to_hex(), "203.0.113.40:51821", 128,));
    let config = fips_tunnel_config_from_app(FipsTunnelConfigInput {
        app: &app,
        config_path: &config_path,
        network_id: &network_id,
        iface: "utun-test".to_string(),
        underlay_interface: None,
        underlay_interface_mtu: None,
        own_pubkey: Some(&own_pubkey),
        recent_peers: Some(&recent_peers),
        live_peer_endpoints: &[],
        ethernet_underlay: None,
    })
    .expect("build direct-mode config with pending close");
    let seller_control_peer = config
        .endpoint_peers
        .iter()
        .find(|peer| peer.npub == seller_npub)
        .expect("pending close keeps seller as a control peer");
    assert!(seller_control_peer.auto_reconnect);
    assert!(
        seller_control_peer
            .addresses
            .iter()
            .any(|hint| hint.addr == "203.0.113.40:51821")
    );
    assert!(
        config
            .peers
            .iter()
            .all(|peer| peer.participant_pubkey != seller.public_key().to_hex()),
        "pending close must not restore the seller as an exit route"
    );

    assert!(
        acknowledge_paid_exit_payment(&config_path, &seller.public_key().to_hex(), &payment_id)
            .expect("acknowledge cooperative close"),
        "seller acknowledgment must remove the queued close"
    );
    assert!(
        !fips_private_runtime_active_for_config(&app, &config_path, false, 0)
            .expect("inspect acknowledged close runtime"),
        "idle direct mode must release the control runtime"
    );

    let _ = std::fs::remove_dir_all(&dir);

    struct RuntimeFakePaymentSigner;

    impl CashuSpilmanPaymentSigner for RuntimeFakePaymentSigner {
        fn sign_cashu_spilman_payment(
            &self,
            channel_id: &str,
            balance: u64,
            include_funding: bool,
        ) -> std::result::Result<CashuSpilmanPayment, String> {
            Ok(CashuSpilmanPayment {
                channel_id: channel_id.to_string(),
                balance,
                signature: format!("signed-{channel_id}-{balance}"),
                params: include_funding.then(|| json!({"channel": channel_id})),
                funding_proofs: include_funding.then(|| json!({"proofs": []})),
            })
        }

        fn sign_cashu_spilman_close(
            &self,
            channel_id: &str,
            final_balance: u64,
        ) -> std::result::Result<CashuSpilmanPayment, String> {
            Ok(CashuSpilmanPayment {
                channel_id: channel_id.to_string(),
                balance: final_balance,
                signature: format!("closed-{channel_id}-{final_balance}"),
                params: None,
                funding_proofs: None,
            })
        }
    }
}