nvpn 4.1.4

CLI and daemon for Nostr VPN private mesh networks
Documentation
use super::*;

pub(super) async fn fund_automatic_paid_exit(
    app: &AppConfig,
    config_path: &Path,
    session_id: &str,
    now_unix: u64,
) -> Result<StreamingRoutePaymentEnvelope> {
    if !PaidExitAutomaticBuyer::enabled(app) {
        return Err(anyhow!(
            "automatic paid exit funding cancelled by internet mode"
        ));
    }
    let store_path = paid_route_store_file_path(config_path);
    let mut store = load_paid_route_store(&store_path)?;
    let session = store
        .sessions
        .get(session_id)
        .cloned()
        .ok_or_else(|| anyhow!("automatic paid exit session {session_id} does not exist"))?;
    let lease = store
        .leases
        .get(&session.session.lease_id)
        .cloned()
        .ok_or_else(|| anyhow!("automatic paid exit session has no lease"))?;
    let channel = store
        .channels
        .get(&session.session.payment.channel_id)
        .cloned()
        .ok_or_else(|| anyhow!("automatic paid exit session has no channel"))?;
    let quote = store
        .quotes
        .get(&lease.lease.quote_id)
        .cloned()
        .ok_or_else(|| anyhow!("automatic paid exit session has no quote"))?;
    let opened = open_streaming_route_cashu_spilman_channel_from_wallet(
        &paid_exit_wallet_data_dir(config_path),
        StreamingRouteOpenCashuSpilmanChannelFromWalletRequest {
            mint_url: channel.mint_url,
            receiver_pubkey_hex: quote.quote.receiver_pubkey_hex,
            capacity_sat: session.session.payment.capacity_sat,
            expiry_unix: channel.expires_at_unix,
            max_amount_per_output: 0,
            unit: "sat".to_string(),
            opening_paid_msat: 0,
            keyset_id: None,
            keyset_info_json: None,
        },
    )
    .await?;
    store.attach_buyer_spilman_channel(AttachPaidRouteBuyerSpilmanChannelRequest {
        session_id: session_id.to_string(),
        channel_id: opened.channel.channel_id.clone(),
        cashu_unit: opened.channel.unit.clone(),
        capacity_sat: opened.channel.capacity_sat,
        paid_msat: Some(opened.channel.opening_paid_msat),
        payment: opened.channel.payment.clone(),
        now_unix,
    })?;
    let buyer_npub = app
        .nostr_keys()?
        .public_key()
        .to_bech32()
        .context("failed to encode automatic paid exit buyer npub")?;
    let payment =
        store.build_buyer_payment_envelope(BuildPaidRouteBuyerPaymentEnvelopeRequest {
            session_id: session_id.to_string(),
            buyer_npub,
            kind: BuildPaidRouteBuyerPaymentEnvelopeKind::ChannelOpen,
            payment: opened.channel.payment,
            delivered_units: None,
            paid_msat: Some(opened.channel.opening_paid_msat),
            now_unix,
        })?;
    write_paid_route_store(&store_path, &store)?;
    Ok(payment.envelope)
}

pub(crate) async fn finalize_automatic_paid_exit(
    automatic: &PaidExitAutomaticBuyer,
    runtime: &crate::fips_private_mesh::FipsPrivateTunnelRuntime,
    app: &AppConfig,
    config_path: &Path,
    now_unix: u64,
) -> Result<()> {
    let Some(candidate) = automatic.candidate.as_ref() else {
        return Ok(());
    };
    drain_paid_exit_buyer_usage(runtime, config_path, &candidate.seller_pubkey, now_unix)?;
    if candidate.funded {
        let wallet_data_dir = paid_exit_wallet_data_dir(config_path);
        let signer =
            FileSpilmanPaymentSigner::load(&wallet_data_dir).map_err(|error| anyhow!("{error}"))?;
        let store_path = paid_route_store_file_path(config_path);
        let mut store = load_paid_route_store(&store_path)?;
        let result = paid_exit_settle_with_signer(PaidExitSettleRequest {
            app,
            config_path,
            store: &mut store,
            signer: &signer,
            session_id: &candidate.session_id,
            dry_run: false,
            wallet_data_dir: &wallet_data_dir,
            now_unix,
        })?;
        if result.persisted && result.payment.changed {
            write_paid_route_store(&store_path, &store)?;
        }
        let flushed = flush_paid_exit_payment_outbox(runtime, config_path).await;
        if flushed.errors > 0 {
            eprintln!(
                "paid-exit: automatic seller finalization queued with {} send error(s)",
                flushed.errors
            );
        }
    }
    Ok(())
}

pub(super) fn suspend_automatic_paid_exit(
    automatic: &PaidExitAutomaticBuyer,
    runtime: &crate::fips_private_mesh::FipsPrivateTunnelRuntime,
    config_path: &Path,
    now_unix: u64,
) -> Result<()> {
    let Some(candidate) = automatic.candidate.as_ref() else {
        return Ok(());
    };
    drain_paid_exit_buyer_usage(runtime, config_path, &candidate.seller_pubkey, now_unix)?;
    Ok(())
}

pub(super) fn queue_recovered_automatic_channel_open(
    app: &AppConfig,
    config_path: &Path,
    store: &mut PaidRouteStore,
    session_id: &str,
    now_unix: u64,
) -> Result<()> {
    let signer = FileSpilmanPaymentSigner::load(&paid_exit_wallet_data_dir(config_path))
        .map_err(|error| anyhow!("{error}"))?;
    let buyer_npub = app
        .nostr_keys()?
        .public_key()
        .to_bech32()
        .context("failed to encode automatic paid exit buyer npub")?;
    let payment = store.build_buyer_signed_payment_envelope(
        &signer,
        BuildPaidRouteBuyerSignedPaymentEnvelopeRequest {
            session_id: session_id.to_string(),
            buyer_npub,
            kind: BuildPaidRouteBuyerPaymentEnvelopeKind::ChannelOpen,
            delivered_units: None,
            paid_msat: None,
            now_unix,
        },
    )?;
    if payment.changed {
        write_paid_route_store(&paid_route_store_file_path(config_path), store)?;
    }
    queue_paid_exit_payment(app, config_path, &payment.envelope)?;
    Ok(())
}

fn drain_paid_exit_buyer_usage(
    runtime: &crate::fips_private_mesh::FipsPrivateTunnelRuntime,
    config_path: &Path,
    seller_pubkey: &str,
    now_unix: u64,
) -> Result<PaidRouteUsage> {
    let delta = runtime.drain_paid_route_usage(seller_pubkey)?;
    if delta.is_empty() {
        return Ok(delta);
    }
    let store_path = paid_route_store_file_path(config_path);
    let mut store = load_paid_route_store(&store_path)?;
    let changed = store
        .record_buyer_usage(RecordPaidRouteBuyerUsageRequest {
            seller_pubkey: seller_pubkey.to_string(),
            usage_delta: delta.clone(),
            now_unix,
        })?
        .is_some_and(|result| result.changed);
    if changed {
        write_paid_route_store(&store_path, &store)?;
    }
    Ok(delta)
}