Skip to content

Commit

Permalink
feat: start nwc profiles
Browse files Browse the repository at this point in the history
  • Loading branch information
Kodylow committed Jun 2, 2024
1 parent de81c33 commit 8ed8309
Show file tree
Hide file tree
Showing 10 changed files with 640 additions and 16 deletions.
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
}
}
3 changes: 2 additions & 1 deletion fedimint-nwc/src/nwc/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ 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;
Expand Down Expand Up @@ -284,7 +285,7 @@ async fn handle_get_info() -> Result<Response, NIP47Error> {
block_height: 0,
block_hash: "000000000000000000000000000000000000000000000000000000000000000000"
.to_string(),
methods: super::METHODS.iter().map(|i| i.to_string()).collect(),
methods: METHODS.iter().map(|i| i.to_string()).collect(),
})),
})
}
15 changes: 2 additions & 13 deletions fedimint-nwc/src/nwc/mod.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,4 @@
use nostr::nips::nip47::Method;

pub mod conditions;
pub mod handlers;
pub mod profiles;

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

0 comments on commit 8ed8309

Please sign in to comment.