diff --git a/Cargo.lock b/Cargo.lock index 2bcf909..2045501 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -751,9 +751,6 @@ name = "bitflags" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" -dependencies = [ - "serde", -] [[package]] name = "bitmaps" @@ -1173,67 +1170,18 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "config" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7328b20597b53c2454f0b1919720c25c7339051c02b72b7e05409e00b14132be" -dependencies = [ - "async-trait", - "convert_case 0.6.0", - "json5", - "lazy_static", - "nom", - "pathdiff", - "ron", - "rust-ini 0.19.0", - "serde", - "serde_json", - "toml", - "yaml-rust", -] - [[package]] name = "const-oid" version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" -[[package]] -name = "const-random" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" -dependencies = [ - "const-random-macro", -] - -[[package]] -name = "const-random-macro" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" -dependencies = [ - "getrandom", - "once_cell", - "tiny-keccak", -] - [[package]] name = "convert_case" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" -[[package]] -name = "convert_case" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" -dependencies = [ - "unicode-segmentation", -] - [[package]] name = "core-foundation" version = "0.9.4" @@ -1480,7 +1428,7 @@ dependencies = [ "detect-desktop-environment", "dirs 4.0.0", "objc", - "rust-ini 0.18.0", + "rust-ini", "web-sys", "winreg 0.10.1", "zbus", @@ -1687,7 +1635,7 @@ version = "0.99.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f33878137e4dafd7fa914ad4e259e18a4e8e532b9617a2d0150262bf53abfce" dependencies = [ - "convert_case 0.4.0", + "convert_case", "proc-macro2", "quote", "rustc_version", @@ -1840,15 +1788,6 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257" -[[package]] -name = "dlv-list" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" -dependencies = [ - "const-random", -] - [[package]] name = "downcast-rs" version = "1.2.1" @@ -3302,7 +3241,6 @@ dependencies = [ "bip39", "bitcoin 0.30.2", "chrono", - "config", "diesel", "diesel_migrations", "fedimint-api-client", @@ -3313,13 +3251,9 @@ dependencies = [ "fedimint-ln-common", "fedimint-mint-client", "fedimint-wallet-client", + "futures", "hex", - "home", - "iced", "log", - "lyon_algorithms", - "once_cell", - "palette", "rusqlite", "serde", "tempdir", @@ -3332,24 +3266,12 @@ name = "harbor-ui" version = "0.1.0" dependencies = [ "anyhow", - "async-trait", - "bincode", "bip39", "bitcoin 0.30.2", "chrono", - "config", - "diesel", - "diesel_migrations", - "fedimint-api-client", - "fedimint-bip39", - "fedimint-client", "fedimint-core", - "fedimint-ln-client", "fedimint-ln-common", - "fedimint-mint-client", - "fedimint-wallet-client", "harbor-client", - "hex", "home", "iced", "log", @@ -3357,9 +3279,6 @@ dependencies = [ "once_cell", "palette", "pretty_env_logger", - "rusqlite", - "serde", - "tempdir", "tokio", "uuid", ] @@ -3373,12 +3292,6 @@ dependencies = [ "ahash 0.7.8", ] -[[package]] -name = "hashbrown" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" - [[package]] name = "hashbrown" version = "0.14.5" @@ -4147,17 +4060,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "json5" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" -dependencies = [ - "pest", - "pest_derive", - "serde", -] - [[package]] name = "jsonrpc" version = "0.14.1" @@ -4383,12 +4285,6 @@ dependencies = [ "serde", ] -[[package]] -name = "linked-hash-map" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" - [[package]] name = "linux-raw-sys" version = "0.4.14" @@ -4609,12 +4505,6 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - [[package]] name = "miniscript" version = "10.2.0" @@ -4720,16 +4610,6 @@ dependencies = [ "memoffset", ] -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -5123,20 +5003,10 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccd746e37177e1711c20dd619a1620f34f5c8b569c53590a72dedd5344d8924a" dependencies = [ - "dlv-list 0.3.0", + "dlv-list", "hashbrown 0.12.3", ] -[[package]] -name = "ordered-multimap" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ed8acf08e98e744e5384c8bc63ceb0364e68a6854187221c18df61c4797690e" -dependencies = [ - "dlv-list 0.5.2", - "hashbrown 0.13.2", -] - [[package]] name = "ordered-stream" version = "0.2.0" @@ -5330,12 +5200,6 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" -[[package]] -name = "pathdiff" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d61c5ce1153ab5b689d0c074c4e7fc613e942dfb7dd9eea5ab202d2ad91fe361" - [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -5351,51 +5215,6 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" -[[package]] -name = "pest" -version = "2.7.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879952a81a83930934cbf1786752d6dedc3b1f29e8f8fb2ad1d0a36f377cf442" -dependencies = [ - "memchr", - "thiserror", - "ucd-trie", -] - -[[package]] -name = "pest_derive" -version = "2.7.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d214365f632b123a47fd913301e14c946c61d1c183ee245fa76eb752e59a02dd" -dependencies = [ - "pest", - "pest_generator", -] - -[[package]] -name = "pest_generator" -version = "2.7.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb55586734301717aea2ac313f50b2eb8f60d2fc3dc01d190eefa2e625f60c4e" -dependencies = [ - "pest", - "pest_meta", - "proc-macro2", - "quote", - "syn 2.0.79", -] - -[[package]] -name = "pest_meta" -version = "2.7.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b75da2a70cf4d9cb76833c990ac9cd3923c9a8905a8929789ce347c84564d03d" -dependencies = [ - "once_cell", - "pest", - "sha2", -] - [[package]] name = "phf" version = "0.11.2" @@ -6146,18 +5965,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "ron" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" -dependencies = [ - "base64 0.21.7", - "bitflags 2.6.0", - "serde", - "serde_derive", -] - [[package]] name = "roxmltree" version = "0.20.0" @@ -6207,17 +6014,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6d5f2436026b4f6e79dc829837d467cc7e9a55ee40e750d716713540715a2df" dependencies = [ "cfg-if", - "ordered-multimap 0.4.3", -] - -[[package]] -name = "rust-ini" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e2a3bcec1f113553ef1c88aae6c020a369d03d55b58de9869a0908930385091" -dependencies = [ - "cfg-if", - "ordered-multimap 0.6.0", + "ordered-multimap", ] [[package]] @@ -8311,12 +8108,6 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" -[[package]] -name = "ucd-trie" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" - [[package]] name = "uds_windows" version = "1.1.0" @@ -9424,15 +9215,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" -[[package]] -name = "yaml-rust" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" -dependencies = [ - "linked-hash-map", -] - [[package]] name = "yazi" version = "0.1.6" diff --git a/harbor-client/Cargo.toml b/harbor-client/Cargo.toml index 028a608..776a5e6 100644 --- a/harbor-client/Cargo.toml +++ b/harbor-client/Cargo.toml @@ -3,23 +3,20 @@ name = "harbor-client" version = "0.1.0" edition = "2021" -[dependencies] -# todo: remove -iced = { version = "0.13.1", features = ["debug", "tokio", "svg", "qr_code", "advanced"] } +[features] +default = [] +vendored = ["rusqlite/bundled-sqlcipher-vendored-openssl"] +[dependencies] anyhow = "1.0.89" log = "0.4" -lyon_algorithms = "1.0" -once_cell = "1.0" tokio = { version = "1", features = ["full"] } -palette = "0.7" -config = "0.14.0" serde = { version = "1.0.210", features = ["derive"] } -home = "0.5.9" chrono = "0.4.38" rusqlite = { version = "0.28.0", features = ["sqlcipher"] } diesel = { version = "2.1.6", features = ["sqlite", "chrono", "r2d2"] } diesel_migrations = { version = "2.1.0", features = ["sqlite"] } +futures = "0.3.31" uuid = { version = "1.8", features = ["v4"] } async-trait = "0.1.77" bincode = "1.3.3" diff --git a/harbor-client/src/core.rs b/harbor-client/src/core.rs index b4533dd..139597f 100644 --- a/harbor-client/src/core.rs +++ b/harbor-client/src/core.rs @@ -1,371 +1,2 @@ -use anyhow::anyhow; -use bip39::Mnemonic; -use bitcoin::address::NetworkUnchecked; -use bitcoin::{Address, Network}; -use fedimint_core::config::{ClientConfig, FederationId}; -use fedimint_core::invite_code::InviteCode; -use fedimint_core::Amount; -use fedimint_ln_client::{LightningClientModule, PayType}; -use fedimint_ln_common::config::FeeToAmount; -use fedimint_ln_common::lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription, Description}; -use fedimint_wallet_client::WalletClientModule; -use std::collections::HashMap; -use std::sync::atomic::AtomicBool; -use std::sync::Arc; -use std::time::Instant; -use iced::futures::{channel::mpsc::Sender, SinkExt}; -use log::{error, trace}; -use tokio::sync::RwLock; -use uuid::Uuid; -use crate::db::DBConnection; -use crate::fedimint_client::{ - select_gateway, spawn_internal_payment_subscription, spawn_invoice_payment_subscription, - spawn_invoice_receive_subscription, spawn_onchain_payment_subscription, - spawn_onchain_receive_subscription, FederationInviteOrId, FedimintClient, -}; -use crate::{CoreUIMsg, CoreUIMsgPacket, FederationItem}; - -#[derive(Clone)] -pub struct HarborCore { - pub network: Network, - pub mnemonic: Mnemonic, - pub tx: Sender, - pub clients: Arc>>, - pub storage: Arc, - pub stop: Arc, -} - -impl HarborCore { - pub async fn msg(&self, id: Option, msg: CoreUIMsg) { - self.tx - .clone() - .send(CoreUIMsgPacket { id, msg }) - .await - .unwrap(); - } - - // Sends updates to the UI to refelect the initial state - pub async fn init_ui_state(&self) { - for client in self.clients.read().await.values() { - let fed_balance = client.fedimint_client.get_balance().await; - self.msg( - None, - CoreUIMsg::FederationBalanceUpdated { - id: client.fedimint_client.federation_id(), - balance: fed_balance, - }, - ) - .await; - } - - let history = self.storage.get_transaction_history().unwrap(); - self.msg(None, CoreUIMsg::TransactionHistoryUpdated(history)) - .await; - - let federation_items = self.get_federation_items().await; - self.msg(None, CoreUIMsg::FederationListUpdated(federation_items)) - .await; - } - - async fn get_client(&self, federation_id: FederationId) -> FedimintClient { - let clients = self.clients.read().await; - clients - .get(&federation_id) - .expect("No client found for federation") - .clone() - } - - pub async fn send_lightning( - &self, - msg_id: Uuid, - federation_id: FederationId, - invoice: Bolt11Invoice, - ) -> anyhow::Result<()> { - if invoice.amount_milli_satoshis().is_none() { - return Err(anyhow!("Invoice must have an amount")); - } - let amount = Amount::from_msats(invoice.amount_milli_satoshis().expect("must have amount")); - - // todo go through all clients and select the first one that has enough balance - let client = self.get_client(federation_id).await.fedimint_client; - let lightning_module = client - .get_first_module::() - .expect("must have ln module"); - - let gateway = select_gateway(&client) - .await - .ok_or(anyhow!("Internal error: No gateway found for federation"))?; - - let fees = gateway.fees.to_amount(&amount); - - log::info!("Sending lightning invoice: {invoice}, paying fees: {fees}"); - - let outgoing = lightning_module - .pay_bolt11_invoice(Some(gateway), invoice.clone(), ()) - .await?; - - self.storage.create_lightning_payment( - outgoing.payment_type.operation_id(), - client.federation_id(), - invoice, - amount, - fees, - )?; - - match outgoing.payment_type { - PayType::Internal(op_id) => { - let sub = lightning_module.subscribe_internal_pay(op_id).await?; - spawn_internal_payment_subscription( - self.tx.clone(), - client.clone(), - self.storage.clone(), - op_id, - msg_id, - sub, - ) - .await; - } - PayType::Lightning(op_id) => { - let sub = lightning_module.subscribe_ln_pay(op_id).await?; - spawn_invoice_payment_subscription( - self.tx.clone(), - client.clone(), - self.storage.clone(), - op_id, - msg_id, - sub, - ) - .await; - } - } - - log::info!("Invoice sent"); - - Ok(()) - } - - pub async fn receive_lightning( - &self, - msg_id: Uuid, - federation_id: FederationId, - amount: Amount, - ) -> anyhow::Result { - let client = self.get_client(federation_id).await.fedimint_client; - let lightning_module = client - .get_first_module::() - .expect("must have ln module"); - - let gateway = select_gateway(&client) - .await - .ok_or(anyhow!("Internal error: No gateway found for federation"))?; - - let desc = Description::new(String::new()).expect("empty string is valid"); - let (op_id, invoice, preimage) = lightning_module - .create_bolt11_invoice( - amount, - Bolt11InvoiceDescription::Direct(&desc), - None, - (), - Some(gateway), - ) - .await?; - - log::info!("Invoice created: {invoice}"); - - self.storage.create_ln_receive( - op_id, - client.federation_id(), - invoice.clone(), - amount, - Amount::ZERO, // todo one day there will be receive fees - preimage, - )?; - - // Create subscription to operation if it exists - if let Ok(subscription) = lightning_module.subscribe_ln_receive(op_id).await { - spawn_invoice_receive_subscription( - self.tx.clone(), - client.clone(), - self.storage.clone(), - op_id, - msg_id, - subscription, - ) - .await; - } else { - error!("Could not create subscription to lightning receive"); - } - - Ok(invoice) - } - - /// Sends a given amount of sats to a given address, if the amount is None, send all funds - pub async fn send_onchain( - &self, - msg_id: Uuid, - federation_id: FederationId, - address: Address, - sats: Option, - ) -> anyhow::Result<()> { - // todo go through all clients and select the first one that has enough balance - let client = self.get_client(federation_id).await.fedimint_client; - let onchain = client - .get_first_module::() - .expect("must have wallet module"); - - // todo add manual fee selection - let (fees, amount) = match sats { - Some(sats) => { - let amount = bitcoin::Amount::from_sat(sats); - let fees = onchain.get_withdraw_fees(address.clone(), amount).await?; - (fees, amount) - } - None => { - let balance = client.get_balance().await; - - if balance.sats_round_down() == 0 { - return Err(anyhow!("No funds in wallet")); - } - - // get fees for the entire balance - let fees = onchain - .get_withdraw_fees( - address.clone(), - bitcoin::Amount::from_sat(balance.sats_round_down()), - ) - .await?; - - let fees_paid = Amount::from_sats(fees.amount().to_sat()); - let amount = balance.saturating_sub(fees_paid); - - if amount.sats_round_down() < 546 { - return Err(anyhow!("Not enough funds to send")); - } - - (fees, bitcoin::Amount::from_sat(amount.sats_round_down())) - } - }; - - let op_id = onchain.withdraw(address.clone(), amount, fees, ()).await?; - - self.storage.create_onchain_payment( - op_id, - client.federation_id(), - address, - amount.to_sat(), - fees.amount().to_sat(), - )?; - - let sub = onchain.subscribe_withdraw_updates(op_id).await?; - - spawn_onchain_payment_subscription( - self.tx.clone(), - client.clone(), - self.storage.clone(), - op_id, - msg_id, - sub, - ) - .await; - - Ok(()) - } - - pub async fn receive_onchain( - &self, - msg_id: Uuid, - federation_id: FederationId, - ) -> anyhow::Result
{ - let client = self.get_client(federation_id).await.fedimint_client; - let onchain = client - .get_first_module::() - .expect("must have wallet module"); - - let (op_id, address, _) = onchain.allocate_deposit_address_expert_only(()).await?; - - self.storage - .create_onchain_receive(op_id, client.federation_id(), address.clone())?; - - let sub = onchain.subscribe_deposit(op_id).await?; - - spawn_onchain_receive_subscription( - self.tx.clone(), - client.clone(), - self.storage.clone(), - op_id, - msg_id, - sub, - ) - .await; - - Ok(address) - } - - pub async fn get_federation_info( - &self, - invite_code: InviteCode, - ) -> anyhow::Result { - let download = Instant::now(); - let config = fedimint_api_client::api::net::Connector::Tor - .download_from_invite_code(&invite_code) - .await - .map_err(|e| { - error!("Could not download federation info: {e}"); - e - })?; - trace!( - "Downloaded federation info in: {}ms", - download.elapsed().as_millis() - ); - - Ok(config) - } - - pub async fn add_federation(&self, invite_code: InviteCode) -> anyhow::Result<()> { - let id = invite_code.federation_id(); - - let mut clients = self.clients.write().await; - if clients.get(&id).is_some() { - return Err(anyhow!("Federation already added")); - } - - let client = FedimintClient::new( - self.storage.clone(), - FederationInviteOrId::Invite(invite_code), - &self.mnemonic, - self.network, - self.stop.clone(), - ) - .await?; - - clients.insert(client.fedimint_client.federation_id(), client); - - Ok(()) - } - - pub async fn get_federation_items(&self) -> Vec { - let clients = self.clients.read().await; - - // Tell the UI about any clients we have - clients - .values() - .map(|c| FederationItem { - id: c.fedimint_client.federation_id(), - name: c - .fedimint_client - .get_meta("federation_name") - .unwrap_or("Unknown".to_string()), - // TODO: get the balance per fedimint - balance: 420, - guardians: None, - module_kinds: None, - }) - .collect::>() - } - - pub async fn get_seed_words(&self) -> String { - self.mnemonic.to_string() - } -} diff --git a/harbor-client/src/fedimint_client.rs b/harbor-client/src/fedimint_client.rs index 10c2c23..70543e2 100644 --- a/harbor-client/src/fedimint_client.rs +++ b/harbor-client/src/fedimint_client.rs @@ -24,8 +24,8 @@ use fedimint_ln_client::{ use fedimint_ln_common::LightningGateway; use fedimint_mint_client::MintClientInit; use fedimint_wallet_client::{DepositStateV2, WalletClientInit, WalletClientModule, WithdrawState}; -use iced::futures::channel::mpsc::Sender; -use iced::futures::{SinkExt, StreamExt}; +use futures::channel::mpsc::Sender; +use futures::{SinkExt, StreamExt}; use log::{debug, error, info, trace}; use std::fmt::Debug; use std::ops::Range; diff --git a/harbor-client/src/lib.rs b/harbor-client/src/lib.rs index cc9267a..3280d68 100644 --- a/harbor-client/src/lib.rs +++ b/harbor-client/src/lib.rs @@ -1,11 +1,29 @@ +use crate::db::DBConnection; use crate::db_models::transaction_item::TransactionItem; use crate::db_models::FederationItem; +use crate::fedimint_client::{ + select_gateway, spawn_internal_payment_subscription, spawn_invoice_payment_subscription, + spawn_invoice_receive_subscription, spawn_onchain_payment_subscription, + spawn_onchain_receive_subscription, FederationInviteOrId, FedimintClient, +}; +use anyhow::anyhow; +use bip39::Mnemonic; use bitcoin::address::NetworkUnchecked; -use bitcoin::{Address, Txid}; +use bitcoin::{Address, Network, Txid}; use fedimint_core::config::{ClientConfig, FederationId}; use fedimint_core::invite_code::InviteCode; use fedimint_core::Amount; -use fedimint_ln_common::lightning_invoice::Bolt11Invoice; +use fedimint_ln_client::{LightningClientModule, PayType}; +use fedimint_ln_common::config::FeeToAmount; +use fedimint_ln_common::lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription, Description}; +use fedimint_wallet_client::WalletClientModule; +use futures::{channel::mpsc::Sender, SinkExt}; +use log::{error, trace}; +use std::collections::HashMap; +use std::sync::atomic::AtomicBool; +use std::sync::Arc; +use std::time::Instant; +use tokio::sync::RwLock; use uuid::Uuid; pub mod core; @@ -92,3 +110,346 @@ pub enum CoreUIMsg { UnlockFailed(String), SeedWords(String), } + +#[derive(Clone)] +pub struct HarborCore { + pub network: Network, + pub mnemonic: Mnemonic, + pub tx: Sender, + pub clients: Arc>>, + pub storage: Arc, + pub stop: Arc, +} + +impl HarborCore { + pub async fn msg(&self, id: Option, msg: CoreUIMsg) { + self.tx + .clone() + .send(CoreUIMsgPacket { id, msg }) + .await + .unwrap(); + } + + // Sends updates to the UI to refelect the initial state + pub async fn init_ui_state(&self) { + for client in self.clients.read().await.values() { + let fed_balance = client.fedimint_client.get_balance().await; + self.msg( + None, + CoreUIMsg::FederationBalanceUpdated { + id: client.fedimint_client.federation_id(), + balance: fed_balance, + }, + ) + .await; + } + + let history = self.storage.get_transaction_history().unwrap(); + self.msg(None, CoreUIMsg::TransactionHistoryUpdated(history)) + .await; + + let federation_items = self.get_federation_items().await; + self.msg(None, CoreUIMsg::FederationListUpdated(federation_items)) + .await; + } + + async fn get_client(&self, federation_id: FederationId) -> FedimintClient { + let clients = self.clients.read().await; + clients + .get(&federation_id) + .expect("No client found for federation") + .clone() + } + + pub async fn send_lightning( + &self, + msg_id: Uuid, + federation_id: FederationId, + invoice: Bolt11Invoice, + ) -> anyhow::Result<()> { + if invoice.amount_milli_satoshis().is_none() { + return Err(anyhow!("Invoice must have an amount")); + } + let amount = Amount::from_msats(invoice.amount_milli_satoshis().expect("must have amount")); + + // todo go through all clients and select the first one that has enough balance + let client = self.get_client(federation_id).await.fedimint_client; + let lightning_module = client + .get_first_module::() + .expect("must have ln module"); + + let gateway = select_gateway(&client) + .await + .ok_or(anyhow!("Internal error: No gateway found for federation"))?; + + let fees = gateway.fees.to_amount(&amount); + + log::info!("Sending lightning invoice: {invoice}, paying fees: {fees}"); + + let outgoing = lightning_module + .pay_bolt11_invoice(Some(gateway), invoice.clone(), ()) + .await?; + + self.storage.create_lightning_payment( + outgoing.payment_type.operation_id(), + client.federation_id(), + invoice, + amount, + fees, + )?; + + match outgoing.payment_type { + PayType::Internal(op_id) => { + let sub = lightning_module.subscribe_internal_pay(op_id).await?; + spawn_internal_payment_subscription( + self.tx.clone(), + client.clone(), + self.storage.clone(), + op_id, + msg_id, + sub, + ) + .await; + } + PayType::Lightning(op_id) => { + let sub = lightning_module.subscribe_ln_pay(op_id).await?; + spawn_invoice_payment_subscription( + self.tx.clone(), + client.clone(), + self.storage.clone(), + op_id, + msg_id, + sub, + ) + .await; + } + } + + log::info!("Invoice sent"); + + Ok(()) + } + + pub async fn receive_lightning( + &self, + msg_id: Uuid, + federation_id: FederationId, + amount: Amount, + ) -> anyhow::Result { + let client = self.get_client(federation_id).await.fedimint_client; + let lightning_module = client + .get_first_module::() + .expect("must have ln module"); + + let gateway = select_gateway(&client) + .await + .ok_or(anyhow!("Internal error: No gateway found for federation"))?; + + let desc = Description::new(String::new()).expect("empty string is valid"); + let (op_id, invoice, preimage) = lightning_module + .create_bolt11_invoice( + amount, + Bolt11InvoiceDescription::Direct(&desc), + None, + (), + Some(gateway), + ) + .await?; + + log::info!("Invoice created: {invoice}"); + + self.storage.create_ln_receive( + op_id, + client.federation_id(), + invoice.clone(), + amount, + Amount::ZERO, // todo one day there will be receive fees + preimage, + )?; + + // Create subscription to operation if it exists + if let Ok(subscription) = lightning_module.subscribe_ln_receive(op_id).await { + spawn_invoice_receive_subscription( + self.tx.clone(), + client.clone(), + self.storage.clone(), + op_id, + msg_id, + subscription, + ) + .await; + } else { + error!("Could not create subscription to lightning receive"); + } + + Ok(invoice) + } + + /// Sends a given amount of sats to a given address, if the amount is None, send all funds + pub async fn send_onchain( + &self, + msg_id: Uuid, + federation_id: FederationId, + address: Address, + sats: Option, + ) -> anyhow::Result<()> { + // todo go through all clients and select the first one that has enough balance + let client = self.get_client(federation_id).await.fedimint_client; + let onchain = client + .get_first_module::() + .expect("must have wallet module"); + + // todo add manual fee selection + let (fees, amount) = match sats { + Some(sats) => { + let amount = bitcoin::Amount::from_sat(sats); + let fees = onchain.get_withdraw_fees(address.clone(), amount).await?; + (fees, amount) + } + None => { + let balance = client.get_balance().await; + + if balance.sats_round_down() == 0 { + return Err(anyhow!("No funds in wallet")); + } + + // get fees for the entire balance + let fees = onchain + .get_withdraw_fees( + address.clone(), + bitcoin::Amount::from_sat(balance.sats_round_down()), + ) + .await?; + + let fees_paid = Amount::from_sats(fees.amount().to_sat()); + let amount = balance.saturating_sub(fees_paid); + + if amount.sats_round_down() < 546 { + return Err(anyhow!("Not enough funds to send")); + } + + (fees, bitcoin::Amount::from_sat(amount.sats_round_down())) + } + }; + + let op_id = onchain.withdraw(address.clone(), amount, fees, ()).await?; + + self.storage.create_onchain_payment( + op_id, + client.federation_id(), + address, + amount.to_sat(), + fees.amount().to_sat(), + )?; + + let sub = onchain.subscribe_withdraw_updates(op_id).await?; + + spawn_onchain_payment_subscription( + self.tx.clone(), + client.clone(), + self.storage.clone(), + op_id, + msg_id, + sub, + ) + .await; + + Ok(()) + } + + pub async fn receive_onchain( + &self, + msg_id: Uuid, + federation_id: FederationId, + ) -> anyhow::Result
{ + let client = self.get_client(federation_id).await.fedimint_client; + let onchain = client + .get_first_module::() + .expect("must have wallet module"); + + let (op_id, address, _) = onchain.allocate_deposit_address_expert_only(()).await?; + + self.storage + .create_onchain_receive(op_id, client.federation_id(), address.clone())?; + + let sub = onchain.subscribe_deposit(op_id).await?; + + spawn_onchain_receive_subscription( + self.tx.clone(), + client.clone(), + self.storage.clone(), + op_id, + msg_id, + sub, + ) + .await; + + Ok(address) + } + + pub async fn get_federation_info( + &self, + invite_code: InviteCode, + ) -> anyhow::Result { + let download = Instant::now(); + let config = fedimint_api_client::api::net::Connector::Tor + .download_from_invite_code(&invite_code) + .await + .map_err(|e| { + error!("Could not download federation info: {e}"); + e + })?; + trace!( + "Downloaded federation info in: {}ms", + download.elapsed().as_millis() + ); + + Ok(config) + } + + pub async fn add_federation(&self, invite_code: InviteCode) -> anyhow::Result<()> { + let id = invite_code.federation_id(); + + let mut clients = self.clients.write().await; + if clients.get(&id).is_some() { + return Err(anyhow!("Federation already added")); + } + + let client = FedimintClient::new( + self.storage.clone(), + FederationInviteOrId::Invite(invite_code), + &self.mnemonic, + self.network, + self.stop.clone(), + ) + .await?; + + clients.insert(client.fedimint_client.federation_id(), client); + + Ok(()) + } + + pub async fn get_federation_items(&self) -> Vec { + let clients = self.clients.read().await; + + // Tell the UI about any clients we have + clients + .values() + .map(|c| FederationItem { + id: c.fedimint_client.federation_id(), + name: c + .fedimint_client + .get_meta("federation_name") + .unwrap_or("Unknown".to_string()), + // TODO: get the balance per fedimint + balance: 420, + guardians: None, + module_kinds: None, + }) + .collect::>() + } + + pub async fn get_seed_words(&self) -> String { + self.mnemonic.to_string() + } +} diff --git a/harbor-ui/Cargo.toml b/harbor-ui/Cargo.toml index ce5eb34..1aad857 100644 --- a/harbor-ui/Cargo.toml +++ b/harbor-ui/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" [features] default = [] -vendored = ["rusqlite/bundled-sqlcipher-vendored-openssl"] +vendored = ["harbor-client/vendored"] [dependencies] harbor-client = { version = "0.1.0", path = "../harbor-client" } @@ -18,28 +18,11 @@ lyon_algorithms = "1.0" once_cell = "1.0" tokio = { version = "1", features = ["full"] } palette = "0.7" -config = "0.14.0" -serde = { version = "1.0.210", features = ["derive"] } home = "0.5.9" chrono = "0.4.38" -rusqlite = { version = "0.28.0", features = ["sqlcipher"] } -diesel = { version = "2.1.6", features = ["sqlite", "chrono", "r2d2"] } -diesel_migrations = { version = "2.1.0", features = ["sqlite"] } uuid = { version = "1.8", features = ["v4"] } -async-trait = "0.1.77" -bincode = "1.3.3" -hex = "0.4.3" bitcoin = { version = "0.30.2", features = ["base64"] } bip39 = "2.0.0" -fedimint-api-client = { git = "https://github.com/fedimint/fedimint/", rev = "54acaa63a45e6bd14e872cdaaf020e8c100d6b33" } -fedimint-client = { git = "https://github.com/fedimint/fedimint/", rev = "54acaa63a45e6bd14e872cdaaf020e8c100d6b33" } fedimint-core = { git = "https://github.com/fedimint/fedimint/", rev = "54acaa63a45e6bd14e872cdaaf020e8c100d6b33" } -fedimint-wallet-client = { git = "https://github.com/fedimint/fedimint/", rev = "54acaa63a45e6bd14e872cdaaf020e8c100d6b33" } -fedimint-mint-client = { git = "https://github.com/fedimint/fedimint/", rev = "54acaa63a45e6bd14e872cdaaf020e8c100d6b33" } -fedimint-ln-client = { git = "https://github.com/fedimint/fedimint/", rev = "54acaa63a45e6bd14e872cdaaf020e8c100d6b33" } -fedimint-bip39 = { git = "https://github.com/fedimint/fedimint/", rev = "54acaa63a45e6bd14e872cdaaf020e8c100d6b33" } fedimint-ln-common = { git = "https://github.com/fedimint/fedimint/", rev = "54acaa63a45e6bd14e872cdaaf020e8c100d6b33" } - -[dev-dependencies] -tempdir = "0.3.7" diff --git a/harbor-ui/src/bridge.rs b/harbor-ui/src/bridge.rs index 5732f77..35cf083 100644 --- a/harbor-ui/src/bridge.rs +++ b/harbor-ui/src/bridge.rs @@ -6,9 +6,9 @@ use fedimint_core::config::FederationId; use fedimint_core::invite_code::InviteCode; use fedimint_core::Amount; use fedimint_ln_common::lightning_invoice::Bolt11Invoice; -use harbor_client::core::HarborCore; use harbor_client::db::{check_password, setup_db, DBConnection}; use harbor_client::fedimint_client::{FederationInviteOrId, FedimintClient}; +use harbor_client::HarborCore; use harbor_client::{CoreUIMsg, CoreUIMsgPacket, UICoreMsg, UICoreMsgPacket}; use iced::futures::channel::mpsc::Sender; use iced::futures::{SinkExt, Stream, StreamExt}; diff --git a/justfile b/justfile index 217530e..562ab5f 100644 --- a/justfile +++ b/justfile @@ -18,6 +18,3 @@ release: clippy: cargo clippy --all-features --tests -- -D warnings - -reset-db: - cd harbor-client && diesel migration revert --all --database-url=harbor.sqlite && diesel migration run --database-url=harbor.sqlite