Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: nwc profiles #42

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions fedimint-nwc/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ anyhow = "1.0.75"
axum = { version = "0.7.1", features = ["json"] }
axum-macros = "0.4.0"
bincode = "1.3.3"
chrono = "0.4.38"
clap = { version = "4.5.4", features = ["derive", "env"] }
dotenv = "0.15.0"
futures-util = "0.3.30"
Expand Down
1 change: 1 addition & 0 deletions fedimint-nwc/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ pub mod nwc;
pub mod server;
pub mod services;
pub mod state;
pub mod utils;

use crate::config::Cli;
use crate::server::run_server;
Expand Down
117 changes: 117 additions & 0 deletions fedimint-nwc/src/nwc/conditions.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
use chrono::{DateTime, Duration, NaiveDateTime, Utc};
use lightning_invoice::Bolt11Invoice;
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct SingleUseSpendingConditions {
pub payment_hash: Option<String>,
pub amount_sats: u64,
}

#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct TrackedPayment {
/// Time in seconds since epoch
pub time: u64,
/// Amount in sats
pub amt: u64,
/// Payment hash
pub hash: String,
}

/// When payments for a given payment expire
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum BudgetPeriod {
/// Resets daily at midnight UTC
Day,
/// Resets every week on sunday, midnight UTC
Week,
/// Resets every month on the first, midnight UTC
Month,
/// Resets every year on the January 1st, midnight UTC
Year,
/// Payments not older than the given number of seconds are counted
Seconds(u64),
}

#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct BudgetedSpendingConditions {
/// Amount in sats for the allotted budget period
pub budget: u64,
/// Max amount in sats for a single payment
pub single_max: Option<u64>,
/// Payment history
pub payments: Vec<TrackedPayment>,
/// Time period the budget is for
pub period: BudgetPeriod,
}

impl BudgetedSpendingConditions {
pub fn add_payment(&mut self, invoice: &Bolt11Invoice) {
let time = crate::utils::now().as_secs();
let payment = TrackedPayment {
time,
amt: invoice.amount_milli_satoshis().unwrap_or_default() / 1_000,
hash: invoice.payment_hash().into_32().to_lower_hex_string(),
};

self.payments.push(payment);
}

pub fn remove_payment(&mut self, invoice: &Bolt11Invoice) {
let hex = invoice.payment_hash().into_32().to_lower_hex_string();
self.payments.retain(|p| p.hash != hex);
}

fn clean_old_payments(&mut self, now: DateTime<Utc>) {
let period_start = match self.period {
BudgetPeriod::Day => now.date_naive().and_hms_opt(0, 0, 0).unwrap_or_default(),
BudgetPeriod::Week => (now
- Duration::days((now.weekday().num_days_from_sunday()) as i64))
.date_naive()
.and_hms_opt(0, 0, 0)
.unwrap_or_default(),
BudgetPeriod::Month => now
.date_naive()
.with_day(1)
.unwrap_or_default()
.and_hms_opt(0, 0, 0)
.unwrap_or_default(),
BudgetPeriod::Year => NaiveDateTime::new(
now.date_naive().with_ordinal(1).unwrap_or_default(),
chrono::NaiveTime::from_hms_opt(0, 0, 0).unwrap_or_default(),
),
BudgetPeriod::Seconds(secs) => now
.checked_sub_signed(Duration::seconds(secs as i64))
.unwrap_or_default()
.naive_utc(),
};

self.payments
.retain(|p| p.time > period_start.timestamp() as u64)
}

pub fn sum_payments(&mut self) -> u64 {
let now = Utc::now();
self.clean_old_payments(now);
self.payments.iter().map(|p| p.amt).sum()
}

pub fn budget_remaining(&self) -> u64 {
let mut clone = self.clone();
self.budget.saturating_sub(clone.sum_payments())
}
}

#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum SpendingConditions {
SingleUse(SingleUseSpendingConditions),
/// Require approval before sending a payment
RequireApproval,
Budget(BudgetedSpendingConditions),
}

impl Default for SpendingConditions {
fn default() -> Self {
Self::RequireApproval
}
}
12 changes: 1 addition & 11 deletions fedimint-nwc/src/nwc.rs → fedimint-nwc/src/nwc/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,11 @@ use nostr_sdk::{Event, JsonUtil};
use tokio::spawn;
use tracing::info;

use super::types::METHODS;
use crate::database::Database;
use crate::services::{MultiMintService, NostrService};
use crate::state::AppState;

pub const METHODS: [Method; 8] = [
Method::GetInfo,
Method::MakeInvoice,
Method::GetBalance,
Method::LookupInvoice,
Method::PayInvoice,
Method::MultiPayInvoice,
Method::PayKeysend,
Method::MultiPayKeysend,
];

pub async fn handle_nwc_request(state: &AppState, event: Event) -> Result<(), anyhow::Error> {
let user_keys = state.nostr_service.user_keys();
let decrypted = nip04::decrypt(user_keys.secret_key()?, &event.pubkey, &event.content)?;
Expand Down
4 changes: 4 additions & 0 deletions fedimint-nwc/src/nwc/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
pub mod conditions;
pub mod handlers;
pub mod profiles;
pub mod types;
Loading
Loading