nvpn 4.1.1

CLI and daemon for Nostr VPN private mesh networks
Documentation
#[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,
    };
    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 mut app = AppConfig::generated();
    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 = 1_000;
    offer_config.pricing.per_units = 100;
    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: &dir.join("config.toml"),
        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");

    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_100);
    assert_eq!(result.payment.paid_msat, 2_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::Closed);
    assert_eq!(channel.payment.paid_msat, 2_000);
    assert_eq!(
        channel
            .payment
            .cashu_spilman_payment
            .as_ref()
            .map(|payment| payment.balance),
        Some(2)
    );
    let expected_signature = format!("closed-{}-2", 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::Closed);
    assert!(
        store
            .buyer_payment_updates_due(PaidRouteBuyerPaymentUpdatesDueRequest {
                now_unix: 129,
                min_increment_msat: 1,
            })
            .is_empty()
    );

    app.set_internet_source(InternetSource::Direct);
    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: &dir.join("config.toml"),
        network_id: &network_id,
        iface: "utun-test".to_string(),
        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"
    );

    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,
            })
        }
    }
}