From be52a49c02bccf629d104bd1fd912f01596e607b Mon Sep 17 00:00:00 2001 From: kieran Date: Tue, 4 Feb 2025 15:59:47 +0000 Subject: [PATCH] wip --- Cargo.toml | 4 +- config.yaml | 6 +- migrations/20250202135844_payments.sql | 8 +-- src/background/mod.rs | 29 ++++++++- src/background/payments.rs | 18 ++++++ src/bin/main.rs | 34 ++++++++-- src/db.rs | 30 +++++++++ src/lib.rs | 3 +- src/payments.rs | 53 ++++++++++++++++ src/routes/payment.rs | 88 ++++++++++++++++++++++++-- src/settings.rs | 37 +---------- 11 files changed, 252 insertions(+), 58 deletions(-) create mode 100644 src/background/payments.rs create mode 100644 src/payments.rs diff --git a/Cargo.toml b/Cargo.toml index 6e456aa..3a0a7c0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,7 +35,7 @@ sha2 = "0.10.8" sqlx = { version = "0.8.1", features = ["mysql", "runtime-tokio", "chrono", "uuid"] } config = { version = "0.15.7", features = ["yaml"] } chrono = { version = "0.4.38", features = ["serde"] } -reqwest = { version = "0.12.8", features = ["stream"] } +reqwest = { version = "0.12.8", features = ["stream", "http2"] } clap = { version = "4.5.18", features = ["derive"] } mime2ext = "0.1.53" infer = "0.19.0" @@ -48,4 +48,4 @@ ffmpeg-rs-raw = { git = "https://git.v0l.io/Kieran/ffmpeg-rs-raw.git", rev = "a6 candle-core = { git = "https://git.v0l.io/huggingface/candle.git", tag = "0.8.1", optional = true } candle-nn = { git = "https://git.v0l.io/huggingface/candle.git", tag = "0.8.1", optional = true } candle-transformers = { git = "https://git.v0l.io/huggingface/candle.git", tag = "0.8.1", optional = true } -fedimint-tonic-lnd = { version = "0.2.0", optional = true, features = ["invoicesrpc"] } \ No newline at end of file +fedimint-tonic-lnd = { version = "0.2.0", optional = true, default-features = false, features = ["invoicesrpc", "lightningrpc"] } \ No newline at end of file diff --git a/config.yaml b/config.yaml index 539e425..6f4341d 100644 --- a/config.yaml +++ b/config.yaml @@ -35,9 +35,9 @@ payments: fiat: "USD" # LND node config lnd: - endpoint: "https://mylnd:10002" - tls: "tls.crt" - macaroon: "admin.macaroon" + endpoint: "https://127.0.0.1:10001" + tls: "/home/kieran/.polar/networks/3/volumes/lnd/alice/tls.cert" + macaroon: "/home/kieran/.polar/networks/3/volumes/lnd/alice/data/chain/bitcoin/regtest/admin.macaroon" # Cost per unit (BTC/USD/EUR/AUD/CAD/JPY/GBP) cost: currency: "BTC" diff --git a/migrations/20250202135844_payments.sql b/migrations/20250202135844_payments.sql index f31c40e..387ea04 100644 --- a/migrations/20250202135844_payments.sql +++ b/migrations/20250202135844_payments.sql @@ -9,13 +9,13 @@ create table payments user_id integer unsigned not null, created timestamp default current_timestamp, amount integer unsigned not null, - is_paid bit(1) not null, + is_paid bit(1) not null default 0, days_value integer unsigned not null, size_value integer unsigned not null, - index integer unsigned not null, - rate double not null, + settle_index integer unsigned, + rate float, - constraint fk_payments_user + constraint fk_payments_user_id foreign key (user_id) references users (id) on delete cascade on update restrict diff --git a/src/background/mod.rs b/src/background/mod.rs index 39dbbef..0b3b691 100644 --- a/src/background/mod.rs +++ b/src/background/mod.rs @@ -1,24 +1,47 @@ use crate::db::Database; use crate::filesystem::FileStore; use anyhow::Result; -use log::info; +use log::{info, warn}; use tokio::task::JoinHandle; #[cfg(feature = "media-compression")] mod media_metadata; -pub fn start_background_tasks(db: Database, file_store: FileStore) -> Vec>> { +#[cfg(feature = "payments")] +mod payments; + +pub fn start_background_tasks( + db: Database, + file_store: FileStore, + #[cfg(feature = "payments")] client: Option, +) -> Vec>> { let mut ret = vec![]; #[cfg(feature = "media-compression")] { + let db = db.clone(); ret.push(tokio::spawn(async move { info!("Starting MediaMetadata background task"); - let mut m = media_metadata::MediaMetadata::new(db.clone(), file_store.clone()); + let mut m = media_metadata::MediaMetadata::new(db, file_store.clone()); m.process().await?; info!("MediaMetadata background task completed"); Ok(()) })); } + #[cfg(feature = "payments")] + { + if let Some(client) = client { + let db = db.clone(); + ret.push(tokio::spawn(async move { + info!("Starting PaymentsHandler background task"); + let mut m = payments::PaymentsHandler::new(client, db); + m.process().await?; + info!("PaymentsHandler background task completed"); + Ok(()) + })); + } else { + warn!("Not starting PaymentsHandler, configuration missing") + } + } ret } diff --git a/src/background/payments.rs b/src/background/payments.rs new file mode 100644 index 0000000..9595418 --- /dev/null +++ b/src/background/payments.rs @@ -0,0 +1,18 @@ +use crate::db::Database; +use anyhow::Result; +use fedimint_tonic_lnd::Client; + +pub struct PaymentsHandler { + client: Client, + database: Database, +} + +impl PaymentsHandler { + pub fn new(client: Client, database: Database) -> Self { + PaymentsHandler { client, database } + } + + pub async fn process(&mut self) -> Result<()> { + Ok(()) + } +} diff --git a/src/bin/main.rs b/src/bin/main.rs index 3017743..ce6cc3b 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -3,6 +3,8 @@ use std::net::{IpAddr, SocketAddr}; use anyhow::Error; use clap::Parser; use config::Config; +#[cfg(feature = "payments")] +use fedimint_tonic_lnd::lnrpc::GetInfoRequest; use log::{error, info}; use rocket::config::Ident; use rocket::data::{ByteUnit, Limits}; @@ -102,11 +104,35 @@ async fn main() -> Result<(), Error> { rocket = rocket.mount("/", routes![routes::get_blob_thumb]); } #[cfg(feature = "payments")] - { - rocket = rocket.mount("/", routes::payment::routes()); - } + let lnd = { + if let Some(lnd) = settings.payments.as_ref().map(|p| &p.lnd) { + let lnd = fedimint_tonic_lnd::connect( + lnd.endpoint.clone(), + lnd.tls.clone(), + lnd.macaroon.clone(), + ) + .await?; - let jh = start_background_tasks(db, fs); + let info = { + let mut lnd = lnd.clone(); + lnd.lightning().get_info(GetInfoRequest::default()).await? + }; + + info!( + "LND connected: {} v{}", + info.get_ref().alias, + info.get_ref().version + ); + rocket = rocket + .manage(lnd.clone()) + .mount("/", routes::payment::routes()); + Some(lnd) + } else { + None + } + }; + + let jh = start_background_tasks(db, fs, lnd); if let Err(e) = rocket.launch().await { error!("Rocker error {}", e); diff --git a/src/db.rs b/src/db.rs index a9bd06b..63b5dd6 100644 --- a/src/db.rs +++ b/src/db.rs @@ -94,6 +94,20 @@ pub struct UserStats { pub total_size: u64, } +#[cfg(feature = "payments")] +#[derive(Clone, FromRow, Serialize)] +pub struct Payment { + pub payment_hash: Vec, + pub user_id: u64, + pub created: DateTime, + pub amount: u64, + pub is_paid: bool, + pub days_value: u64, + pub size_value: u64, + pub settle_index: u64, + pub rate: f32, +} + #[derive(Clone)] pub struct Database { pub(crate) pool: sqlx::pool::Pool, @@ -274,3 +288,19 @@ impl Database { Ok((results, count)) } } + +#[cfg(feature = "payments")] +impl Database { + pub async fn insert_payment(&self, payment: &Payment) -> Result<(), Error> { + sqlx::query("insert into payments(payment_hash,user_id,amount,days_value,size_value,rate) values(?,?,?,?,?,?)") + .bind(&payment.payment_hash) + .bind(&payment.user_id) + .bind(&payment.amount) + .bind(&payment.days_value) + .bind(&payment.size_value) + .bind(&payment.rate) + .execute(&self.pool) + .await?; + Ok(()) + } +} diff --git a/src/lib.rs b/src/lib.rs index 77bc648..2553422 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,13 +5,14 @@ pub mod background; pub mod cors; pub mod db; pub mod filesystem; +#[cfg(feature = "payments")] +pub mod payments; #[cfg(feature = "media-compression")] pub mod processing; pub mod routes; pub mod settings; pub mod void_file; - pub fn can_compress(mime_type: &str) -> bool { mime_type.starts_with("image/") } diff --git a/src/payments.rs b/src/payments.rs new file mode 100644 index 0000000..ab95db0 --- /dev/null +++ b/src/payments.rs @@ -0,0 +1,53 @@ +use serde::{Deserialize, Serialize}; +use std::fmt::{Display, Formatter}; + +#[cfg(feature = "payments")] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PaymentAmount { + pub currency: Currency, + pub amount: f32, +} + +#[cfg(feature = "payments")] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum Currency { + BTC, + USD, + EUR, + GBP, + JPY, + CAD, + AUD, +} + +#[cfg(feature = "payments")] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum PaymentUnit { + GBSpace, + GBEgress, +} + +impl PaymentUnit { + /// Get the total size from a number of units + pub fn to_size(&self, units: f32) -> u64 { + (1000f32 * 1000f32 * 1000f32 * units) as u64 + } +} + +impl Display for PaymentUnit { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + PaymentUnit::GBSpace => write!(f, "GB Space"), + PaymentUnit::GBEgress => write!(f, "GB Egress"), + } + } +} + +#[cfg(feature = "payments")] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum PaymentInterval { + Day(u16), + Month(u16), + Year(u16), +} diff --git a/src/routes/payment.rs b/src/routes/payment.rs index a84175f..53fc88b 100644 --- a/src/routes/payment.rs +++ b/src/routes/payment.rs @@ -1,12 +1,18 @@ use crate::auth::nip98::Nip98Auth; -use crate::db::Database; -use crate::settings::{PaymentAmount, PaymentInterval, PaymentUnit, Settings}; +use crate::db::{Database, Payment}; +use crate::payments::{Currency, PaymentAmount, PaymentInterval, PaymentUnit}; +use crate::settings::Settings; +use chrono::{Months, Utc}; +use fedimint_tonic_lnd::lnrpc::Invoice; +use fedimint_tonic_lnd::Client; +use log::{error, info}; use rocket::serde::json::Json; use rocket::{routes, Route, State}; use serde::{Deserialize, Serialize}; +use std::ops::{Add, Deref}; pub fn routes() -> Vec { - routes![get_payment] + routes![get_payment, req_payment] } #[derive(Deserialize, Serialize)] @@ -31,7 +37,9 @@ struct PaymentRequest { } #[derive(Deserialize, Serialize)] -struct PaymentResponse {} +struct PaymentResponse { + pub pr: String, +} #[rocket::get("/payment")] async fn get_payment(settings: &State) -> Option> { @@ -49,7 +57,75 @@ async fn req_payment( auth: Nip98Auth, db: &State, settings: &State, + lnd: &State, req: Json, -) -> Json { - Json::from(PaymentResponse {}) +) -> Result, String> { + let cfg = if let Some(p) = &settings.payments { + p + } else { + return Err("Payment not enabled, missing configuration option(s)".to_string()); + }; + + let btc_amount = match cfg.cost.currency { + Currency::BTC => cfg.cost.amount, + _ => return Err("Currency not supported".to_string()), + }; + + let amount = btc_amount * req.units * req.quantity as f32; + + let pubkey_vec = auth.event.pubkey.to_bytes().to_vec(); + let uid = db + .upsert_user(&pubkey_vec) + .await + .map_err(|_| "Failed to get user account".to_string())?; + + let mut lnd = lnd.deref().clone(); + let c = lnd.lightning(); + let msat = (amount * 1e11f32) as u64; + let memo = format!( + "{}x {} {} for {}", + req.quantity, req.units, cfg.unit, auth.event.pubkey + ); + info!("Requesting {} msats: {}", msat, memo); + let invoice = c + .add_invoice(Invoice { + value_msat: msat as i64, + memo, + ..Default::default() + }) + .await + .map_err(|e| e.message().to_string())?; + + let days_value = match cfg.interval { + PaymentInterval::Day(d) => d as u64, + PaymentInterval::Month(m) => { + let now = Utc::now(); + (now.add(Months::new(m as u32)) - now).num_days() as u64 + } + PaymentInterval::Year(y) => { + let now = Utc::now(); + (now.add(Months::new(12 * y as u32)) - now).num_days() as u64 + } + }; + + let record = Payment { + payment_hash: invoice.get_ref().payment_addr.clone(), + user_id: uid, + created: Default::default(), + amount: msat, + is_paid: false, + days_value, + size_value: cfg.unit.to_size(req.units), + settle_index: 0, + rate: 0.0, + }; + + if let Err(e) = db.insert_payment(&record).await { + error!("Failed to insert payment: {}", e); + return Err("Failed to insert payment".to_string()); + } + + Ok(Json(PaymentResponse { + pr: invoice.get_ref().payment_request.clone(), + })) } diff --git a/src/settings.rs b/src/settings.rs index 94cf19c..3c54334 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -1,3 +1,5 @@ +#[cfg(feature = "payments")] +use crate::payments::{Currency, PaymentAmount, PaymentInterval, PaymentUnit}; use serde::{Deserialize, Serialize}; use std::path::PathBuf; @@ -63,41 +65,6 @@ pub struct PaymentConfig { pub fiat: Option, } -#[cfg(feature = "payments")] -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct PaymentAmount { - pub currency: Currency, - pub amount: f32, -} - -#[cfg(feature = "payments")] -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum Currency { - BTC, - USD, - EUR, - GBP, - JPY, - CAD, - AUD, -} - -#[cfg(feature = "payments")] -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum PaymentUnit { - GBSpace, - GBEgress, -} - -#[cfg(feature = "payments")] -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "lowercase")] -pub enum PaymentInterval { - Day(u16), - Month(u16), - Year(u16), -} - #[cfg(feature = "payments")] #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LndConfig {