From 4cb1323a2fd3d5f7c9781d074cede7c1bdaa1f42 Mon Sep 17 00:00:00 2001 From: "kody.low" Date: Wed, 10 Jan 2024 14:06:19 -0800 Subject: [PATCH] hold: multimint support via federation Id map --- migrations/20231224194609_create_tables.sql | 13 +++-- src/main.rs | 19 +++--- src/model/app_user.rs | 3 + src/model/app_user_relays.rs | 4 ++ src/model/invoice.rs | 1 + src/router/handlers/lnurlp/callback.rs | 39 ++++++++++--- src/router/handlers/nostr/mod.rs | 1 + src/router/handlers/nostr/register.rs | 2 + src/state.rs | 65 ++++++++++++++++++--- 9 files changed, 119 insertions(+), 28 deletions(-) diff --git a/migrations/20231224194609_create_tables.sql b/migrations/20231224194609_create_tables.sql index 74d40e8..316d356 100644 --- a/migrations/20231224194609_create_tables.sql +++ b/migrations/20231224194609_create_tables.sql @@ -3,7 +3,8 @@ CREATE TABLE app_user ( id SERIAL PRIMARY KEY, pubkey VARCHAR(64) NOT NULL, name VARCHAR(20) NOT NULL, - dm_type VARCHAR(5) NOT NULL + dm_type VARCHAR(5) NOT NULL, + federation_id VARCHAR(64) NOT NULL ); CREATE TABLE relay ( id SERIAL PRIMARY KEY, @@ -16,15 +17,15 @@ CREATE TABLE app_user_relays ( ); CREATE TABLE invoice ( id SERIAL PRIMARY KEY, + federation_id VARCHAR(64) NOT NULL, op_id VARCHAR(64) NOT NULL, app_user_id INTEGER NOT NULL references app_user(id), bolt11 VARCHAR(2048) NOT NULL, amount BIGINT NOT NULL, state INTEGER NOT NULL DEFAULT 0 ); -CREATE TABLE zaps -( - id INTEGER NOT NULL PRIMARY KEY references invoice (id), - request TEXT NOT NULL, +CREATE TABLE zaps ( + id INTEGER NOT NULL PRIMARY KEY references invoice (id), + request TEXT NOT NULL, event_id VARCHAR(64) -); \ No newline at end of file +); diff --git a/src/main.rs b/src/main.rs index 592f10d..f01549b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -22,7 +22,7 @@ async fn main() -> Result<()> { tracing_subscriber::fmt::init(); let state = AppState { - fm: load_fedimint_client().await?, + fm_clients: load_existing_clients().await, mm: ModelManager::new().await?, nostr: load_nostr_client().await?, }; @@ -48,8 +48,6 @@ async fn main() -> Result<()> { /// Starts subscription for all pending invoices from previous run async fn handle_pending_invoices(state: AppState) -> Result<()> { let invoices = InvoiceBmc::get_pending(&state.mm).await?; - let ln = state.fm.get_first_module::(); - // sort invoices by user for efficiency let invoices_by_user = invoices .into_iter() @@ -59,22 +57,29 @@ async fn handle_pending_invoices(state: AppState) -> Result<()> { .map(|(user, invs)| (user, invs.collect::>())) .collect::>(); - for (user, invoices) in invoices_by_user { - let nip05relays = AppUserRelaysBmc::get_by_id(&state.mm, user).await?; - for invoice in invoices { - // create subscription to operation if it exists + let fm_clients = state.fm_clients.lock().await; + for invoice in invoices { + let nip05relays = AppUserRelaysBmc::get_by_id(&state.mm, invoice.app_user_id).await?; + if let Some(client) = fm_clients.get(&nip05relays.federation_id) { + let ln = client.get_first_module::(); if let Ok(subscription) = ln .subscribe_ln_receive(invoice.op_id.parse().expect("invalid op_id")) .await { spawn_invoice_subscription( state.clone(), + nip05relays.federation_id, invoice.id, nip05relays.clone(), subscription, ) .await; } + } else { + error!( + "No client found for federation_id: {}", + nip05relays.federation_id + ); } } diff --git a/src/model/app_user.rs b/src/model/app_user.rs index 00c6116..d5b6405 100644 --- a/src/model/app_user.rs +++ b/src/model/app_user.rs @@ -17,6 +17,7 @@ pub struct AppUser { pub pubkey: String, pub name: String, pub dm_type: String, + pub federation_id: String, } #[derive(Debug, Clone, Fields, FromRow, Serialize)] @@ -24,6 +25,7 @@ pub struct AppUserForCreate { pub pubkey: String, pub name: String, pub dm_type: String, + pub federation_id: String, } #[derive(Debug, Clone, Fields, FromRow, Serialize)] @@ -31,6 +33,7 @@ pub struct AppUserForUpdate { pub pubkey: Option, pub name: Option, pub dm_type: Option, + pub federation_id: Option, } pub struct AppUserBmc; diff --git a/src/model/app_user_relays.rs b/src/model/app_user_relays.rs index 39e99e1..49a03dd 100644 --- a/src/model/app_user_relays.rs +++ b/src/model/app_user_relays.rs @@ -25,6 +25,7 @@ pub struct AppUserRelaysForCreate { pub name: String, pub dm_type: String, pub relays: Vec, + pub federation_id: String, } #[derive(Debug, Clone, FromRow, Serialize)] @@ -51,6 +52,7 @@ impl AppUserRelaysBmc { pubkey: app_user_relays_c.pubkey, name: app_user_relays_c.name, dm_type: app_user_relays_c.dm_type, + federation_id: app_user_relays_c.federation_id, }; let user_id = base::create::(mm, user_c).await?; @@ -88,6 +90,7 @@ impl AppUserRelaysBmc { let userrelays = AppUserRelays { app_user_id: user.id, + federation_id: user.federation_id, pubkey: user.pubkey, name: user.name, dm_type: user.dm_type, @@ -124,6 +127,7 @@ impl AppUserRelaysBmc { let userrelays = AppUserRelays { app_user_id: user.id, + federation_id: user.federation_id, pubkey: user.pubkey, name: user.name, dm_type: user.dm_type, diff --git a/src/model/invoice.rs b/src/model/invoice.rs index 165f340..793fbd7 100644 --- a/src/model/invoice.rs +++ b/src/model/invoice.rs @@ -22,6 +22,7 @@ pub struct Invoice { #[derive(Debug, Clone, Fields, FromRow, Serialize)] pub struct InvoiceForCreate { pub op_id: String, + pub federation_id: String, pub app_user_id: i32, pub bolt11: String, pub amount: i64, diff --git a/src/router/handlers/lnurlp/callback.rs b/src/router/handlers/lnurlp/callback.rs index 28f9547..82e6f74 100644 --- a/src/router/handlers/lnurlp/callback.rs +++ b/src/router/handlers/lnurlp/callback.rs @@ -7,7 +7,7 @@ use axum::{ Json, }; use fedimint_client::oplog::UpdateStreamOrOutcome; -use fedimint_core::{core::OperationId, task::spawn, Amount}; +use fedimint_core::{config::FederationId, core::OperationId, task::spawn, Amount}; use fedimint_ln_client::{LightningClientModule, LnReceiveState}; use fedimint_mint_client::{MintClientModule, OOBNotes}; use futures::StreamExt; @@ -105,8 +105,12 @@ pub async fn handle_callback( } let nip05relays = AppUserRelaysBmc::get_by(&state.mm, NameOrPubkey::Name, &username).await?; - - let ln = state.fm.get_first_module::(); + let fm_client: ClientArc = state + .fm_clients + .lock() + .await + .get(FederationId::from_str(&nip05relays.federation_id))?; + let ln = fm_client.get_first_module::(); let (op_id, pr) = ln .create_bolt11_invoice( Amount { @@ -123,6 +127,7 @@ pub async fn handle_callback( &state.mm, InvoiceForCreate { op_id: op_id.to_string(), + federation_id: nip05relays.federation_id, app_user_id: nip05relays.app_user_id, amount: params.amount as i64, bolt11: pr.to_string(), @@ -149,7 +154,14 @@ pub async fn handle_callback( .await .expect("subscribing to a just created operation can't fail"); - spawn_invoice_subscription(state, id, nip05relays, subscription).await; + spawn_invoice_subscription( + state, + FederationId::from_str(&nip05relays.federation_id), + id, + nip05relays, + subscription, + ) + .await; let verify_url = format!( "http://{}:{}/lnurlp/{}/verify/{}", @@ -170,6 +182,7 @@ pub async fn handle_callback( pub(crate) async fn spawn_invoice_subscription( state: AppState, + federation_id: FederationId, id: i32, userrelays: AppUserRelays, subscription: UpdateStreamOrOutcome, @@ -190,9 +203,15 @@ pub(crate) async fn spawn_invoice_subscription( let invoice = InvoiceBmc::set_state(&state.mm, id, InvoiceState::Settled) .await .expect("settling invoice can't fail"); - notify_user(&state, id, invoice.amount as u64, userrelays.clone()) - .await - .expect("notifying user can't fail"); + notify_user( + &state, + federation_id, + id, + invoice.amount as u64, + userrelays.clone(), + ) + .await + .expect("notifying user can't fail"); break; } _ => {} @@ -203,10 +222,16 @@ pub(crate) async fn spawn_invoice_subscription( async fn notify_user( state: &AppState, + federation_id: FederationId, id: i32, amount: u64, app_user_relays: AppUserRelays, ) -> Result<(), Box> { + let fm_client: ClientArc = state + .fm_clients + .lock() + .await + .get(FederationId::from_str(&federation_id))?; let mint = state.fm.get_first_module::(); let (operation_id, notes) = mint .spend_notes(Amount::from_msats(amount), Duration::from_secs(604800), ()) diff --git a/src/router/handlers/nostr/mod.rs b/src/router/handlers/nostr/mod.rs index ac8fc4c..af578dd 100644 --- a/src/router/handlers/nostr/mod.rs +++ b/src/router/handlers/nostr/mod.rs @@ -6,6 +6,7 @@ pub mod well_known; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AppUserRelays { pub app_user_id: i32, + pub federation_id: String, pub pubkey: String, pub name: String, pub dm_type: String, diff --git a/src/router/handlers/nostr/register.rs b/src/router/handlers/nostr/register.rs index 32646aa..9aaa755 100644 --- a/src/router/handlers/nostr/register.rs +++ b/src/router/handlers/nostr/register.rs @@ -18,6 +18,7 @@ pub struct UserParams { pub name: String, pub dm_type: SupportedDmType, pub relays: Option>, + pub federation_id: FederationId, } #[axum_macros::debug_handler] @@ -50,6 +51,7 @@ pub async fn handle_register( name: params.name, dm_type: params.dm_type.to_string(), relays, + federation_id: params.federation_id.to_string(), }; match AppUserRelaysBmc::register(&state.mm, nip05relays_c).await { diff --git a/src/state.rs b/src/state.rs index 4e8c55c..7ee909b 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,3 +1,5 @@ +use std::{collections::BTreeMap, path::PathBuf}; + use fedimint_client::ClientArc; use nostr_sdk::Client; @@ -5,7 +7,6 @@ use crate::{config, model::ModelManager}; use anyhow::Result; use config::CONFIG; -use fedimint_client::{get_config_from_db, FederationInfo}; use fedimint_core::db::Database; use fedimint_ln_client::LightningClientInit; use fedimint_mint_client::MintClientInit; @@ -13,31 +14,79 @@ use fedimint_wallet_client::WalletClientInit; #[derive(Clone)] pub struct AppState { - pub fm: ClientArc, + pub fm_clients: Arc>>, pub mm: ModelManager, pub nostr: Client, } -pub async fn load_fedimint_client() -> Result { +pub async fn load_existing_clients() -> Arc>> { + let mut clients = BTreeMap::new(); + let fm_dbs = CONFIG + .fm_db_path + .read_dir() + .expect("Failed to read fm db path") + .flatten(); + for db in fm_dbs { + let client_db_path = db.path(); + let client = load_fedimint_client(client_db_path) + .await + .expect("Failed to load client"); + let federation_id = client.federation_id(); + clients.insert(federation_id, client); + } + + Arc::new(Mutex::new(clients)) +} + +pub async fn load_fedimint_client(file_path: PathBuf) -> Result { + let fedimint_client_path = CONFIG.fm_db_path.join(file_path); + let db = Database::new( fedimint_rocksdb::RocksDb::open(CONFIG.fm_db_path.clone())?, Default::default(), ); let mut client_builder = fedimint_client::Client::builder(); - if get_config_from_db(&db).await.is_none() { - let federation_info = FederationInfo::from_invite_code(CONFIG.invite_code.clone()).await?; - client_builder.with_federation_info(federation_info); - }; + client_builder.with_database(db); client_builder.with_module(WalletClientInit(None)); client_builder.with_module(MintClientInit); client_builder.with_module(LightningClientInit); client_builder.with_primary_module(1); - let client_res = client_builder.build(CONFIG.root_secret.clone()).await?; + let client_secret = match client_builder.load_decodable_client_secret().await { + Ok(secret) => secret, + Err(_) => { + let secret = PlainRootSecretStrategy::random(&mut thread_rng()); + store_encodable_client_secret(client_builder.db(), secret) + .await + .map_err(|e| anyhow!("Failed to store client secret: {}", e))?; + secret + } + }; + let root_secret = PlainRootSecretStrategy::to_root_secret(&client_secret); + let client_res = client_builder.build(root_secret).await?; Ok(client_res) } +pub async fn store_encodable_client_secret( + db: &Database, + secret: T, +) -> anyhow::Result<()> { + let mut dbtx = db.begin_transaction().await; + + // Don't overwrite an existing secret + match dbtx.get_value(&EncodedClientSecretKey).await { + Some(_) => bail!("Encoded client secret already exists, cannot overwrite"), + None => { + let encoded_secret = consensus_encode_to_vec(&secret); + dbtx.insert_entry(&EncodedClientSecretKey, &encoded_secret) + .await; + dbtx.commit_tx().await; + Ok(()) + } + } +} + pub async fn load_nostr_client() -> Result { let client = nostr_sdk::Client::new(&CONFIG.nostr_sk);