diff --git a/migrations/2024-02-20-210617_user_info/up.sql b/migrations/2024-02-20-210617_user_info/up.sql index e092982..3e0774d 100644 --- a/migrations/2024-02-20-210617_user_info/up.sql +++ b/migrations/2024-02-20-210617_user_info/up.sql @@ -4,7 +4,8 @@ CREATE TABLE app_user ( name VARCHAR(255) NOT NULL UNIQUE, unblinded_msg VARCHAR(255) NOT NULL UNIQUE, federation_id VARCHAR(64) NOT NULL, - federation_invite_code VARCHAR(255) NOT NULL + federation_invite_code VARCHAR(255) NOT NULL, + invoice_index INTEGER NOT NULL DEFAULT 0 ); CREATE INDEX idx_app_user_unblinded_msg ON app_user (unblinded_msg); @@ -16,6 +17,7 @@ CREATE TABLE invoice ( op_id VARCHAR(64) NOT NULL, preimage VARCHAR(64) NOT NULL, app_user_id INTEGER NOT NULL references app_user(id), + user_invoice_index INTEGER NOT NULL, bolt11 VARCHAR(2048) NOT NULL, amount BIGINT NOT NULL, state INTEGER NOT NULL DEFAULT 0 diff --git a/src/db.rs b/src/db.rs index a244d65..4ab7f7e 100644 --- a/src/db.rs +++ b/src/db.rs @@ -21,6 +21,7 @@ pub(crate) trait DBConnection { fn set_invoice_state(&self, invoice: Invoice, s: i32) -> anyhow::Result<()>; fn get_user_by_name(&self, name: String) -> anyhow::Result>; fn get_user_by_id(&self, id: i32) -> anyhow::Result>; + fn get_user_and_increment_counter(&self, name: &str) -> anyhow::Result>; fn insert_new_zap(&self, new_zap: NewZap) -> anyhow::Result; fn get_zap_by_id(&self, id: i32) -> anyhow::Result>; fn set_zap_event_id(&self, zap: Zap, event_id: String) -> anyhow::Result<()>; @@ -66,6 +67,11 @@ impl DBConnection for PostgresConnection { AppUser::get_by_id(conn, id) } + fn get_user_and_increment_counter(&self, name: &str) -> anyhow::Result> { + let conn = &mut self.db.get()?; + AppUser::get_by_name_and_increment_counter(conn, name) + } + fn insert_new_invoice(&self, new_invoice: NewInvoice) -> anyhow::Result { let conn = &mut self.db.get()?; new_invoice.insert(conn) diff --git a/src/invoice.rs b/src/invoice.rs index 84feb30..bc7a67c 100644 --- a/src/invoice.rs +++ b/src/invoice.rs @@ -67,7 +67,6 @@ pub(crate) async fn handle_pending_invoices(state: &State) -> Result<()> { spawn_invoice_subscription( state.clone(), invoice, - client.clone(), user.clone(), subscription, ) @@ -84,8 +83,7 @@ pub(crate) async fn handle_pending_invoices(state: &State) -> Result<()> { pub(crate) async fn spawn_invoice_subscription( state: State, i: Invoice, - client: ClientHandleArc, - userrelays: AppUser, + user: AppUser, subscription: UpdateStreamOrOutcome, ) { spawn("waiting for invoice being paid", async move { @@ -109,16 +107,7 @@ pub(crate) async fn spawn_invoice_subscription( } LnReceiveState::Claimed => { info!("Payment claimed"); - match notify_user( - client, - &nostr, - &state, - i.id, - i.amount as u64, - userrelays.clone(), - ) - .await - { + match notify_user(&nostr, &i, user).await { Ok(_) => { match state.db.set_invoice_state(i, InvoiceState::Settled as i32) { Ok(_) => (), @@ -140,7 +129,26 @@ pub(crate) async fn spawn_invoice_subscription( }); } -async fn notify_user( +async fn notify_user(nostr: &Client, invoice: &Invoice, user: AppUser) -> Result<()> { + let dm = nostr + .send_direct_msg( + XOnlyPublicKey::from_str(&user.pubkey)?, + json!({ + "tweak_index": invoice.user_invoice_index, + "amount": invoice.amount, + }) + .to_string(), + None, + ) + .await?; + + info!("Sent nostr dm: {dm}"); + Ok(()) +} + +// previous way we used to send ecash +#[allow(dead_code)] +async fn notify_user_with_ecash( client: ClientHandleArc, nostr: &Client, state: &State, diff --git a/src/lnurlp.rs b/src/lnurlp.rs index a02418f..44aff26 100644 --- a/src/lnurlp.rs +++ b/src/lnurlp.rs @@ -10,6 +10,7 @@ use anyhow::anyhow; use fedimint_core::{config::FederationId, Amount, BitcoinHash}; use fedimint_ln_client::LightningClientModule; use fedimint_ln_common::bitcoin::hashes::sha256; +use fedimint_ln_common::bitcoin::secp256k1::Parity; use fedimint_ln_common::lightning_invoice::{Bolt11InvoiceDescription, Sha256}; use nostr::{Event, JsonUtil, Kind}; @@ -50,7 +51,7 @@ pub async fn lnurl_callback( name: String, params: LnurlCallbackParams, ) -> anyhow::Result { - let user = state.db.get_user_by_name(name.clone())?; + let user = state.db.get_user_and_increment_counter(&name)?; if user.is_none() { return Err(anyhow!("NotFound")); } @@ -92,13 +93,15 @@ pub async fn lnurl_callback( } }; + let invoice_index = user.invoice_index; + let (op_id, pr, preimage) = ln - .create_bolt11_invoice( - Amount { - msats: params.amount, - }, + .create_bolt11_invoice_for_user_tweaked( + Amount::from_msats(params.amount), Bolt11InvoiceDescription::Hash(&desc_hash), - None, + Some(86_400), // 1 day expiry + user.pubkey().public_key(Parity::Odd), // todo is this parity correct / easy to work with? + invoice_index as u64, (), None, // todo set gateway properly ) @@ -110,6 +113,7 @@ pub async fn lnurl_callback( op_id: op_id.to_string(), preimage: hex::encode(preimage), app_user_id: user.id, + user_invoice_index: invoice_index, bolt11: pr.to_string(), amount: params.amount as i64, state: InvoiceState::Pending as i32, @@ -135,7 +139,6 @@ pub async fn lnurl_callback( spawn_invoice_subscription( state.clone(), created_invoice, - client, user.clone(), subscription, ) diff --git a/src/models/app_user.rs b/src/models/app_user.rs index f76b048..b5cb1f4 100644 --- a/src/models/app_user.rs +++ b/src/models/app_user.rs @@ -1,6 +1,8 @@ use crate::models::schema::app_user; use diesel::prelude::*; +use fedimint_ln_common::bitcoin::secp256k1::XOnlyPublicKey; use serde::{Deserialize, Serialize}; +use std::str::FromStr; #[derive( QueryableByName, Queryable, AsChangeset, Serialize, Deserialize, Debug, Clone, PartialEq, @@ -14,9 +16,14 @@ pub struct AppUser { pub unblinded_msg: String, pub federation_id: String, pub federation_invite_code: String, + pub invoice_index: i32, } impl AppUser { + pub fn pubkey(&self) -> XOnlyPublicKey { + XOnlyPublicKey::from_str(&self.pubkey).expect("invalid pubkey") + } + pub fn get_app_users(conn: &mut PgConnection) -> anyhow::Result> { Ok(app_user::table.load::(conn)?) } @@ -35,6 +42,27 @@ impl AppUser { .optional()?) } + pub fn get_by_name_and_increment_counter( + conn: &mut PgConnection, + name: &str, + ) -> anyhow::Result> { + conn.transaction(|conn| { + let user = app_user::table + .filter(app_user::name.eq(name)) + .first::(conn) + .optional()?; + + // if the user exists, increment their invoice index + if let Some(user) = &user { + diesel::update(app_user::table.filter(app_user::id.eq(user.id))) + .set(app_user::invoice_index.eq(app_user::invoice_index + 1)) + .execute(conn)?; + } + + Ok(user) + }) + } + pub fn check_available_name(conn: &mut PgConnection, name: String) -> anyhow::Result { Ok(app_user::table .filter(app_user::name.eq(name)) diff --git a/src/models/invoice.rs b/src/models/invoice.rs index 6864cf4..09f2550 100644 --- a/src/models/invoice.rs +++ b/src/models/invoice.rs @@ -13,6 +13,7 @@ pub struct Invoice { pub op_id: String, pub preimage: String, pub app_user_id: i32, + pub user_invoice_index: i32, pub bolt11: String, pub amount: i64, pub state: i32, @@ -63,6 +64,7 @@ pub struct NewInvoice { pub op_id: String, pub preimage: String, pub app_user_id: i32, + pub user_invoice_index: i32, pub bolt11: String, pub amount: i64, pub state: i32, diff --git a/src/models/schema.rs b/src/models/schema.rs index 8bea622..6a61296 100644 --- a/src/models/schema.rs +++ b/src/models/schema.rs @@ -13,6 +13,7 @@ diesel::table! { federation_id -> Varchar, #[max_length = 255] federation_invite_code -> Varchar, + invoice_index -> Int4, } } @@ -26,6 +27,7 @@ diesel::table! { #[max_length = 64] preimage -> Varchar, app_user_id -> Int4, + user_invoice_index -> Int4, #[max_length = 2048] bolt11 -> Varchar, amount -> Int8,