-
Notifications
You must be signed in to change notification settings - Fork 20
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
cf20139
commit 1be0f37
Showing
3 changed files
with
364 additions
and
372 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 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<CoreUIMsgPacket>, | ||
pub clients: Arc<RwLock<HashMap<FederationId, FedimintClient>>>, | ||
pub storage: Arc<dyn DBConnection + Send + Sync>, | ||
pub stop: Arc<AtomicBool>, | ||
} | ||
|
||
impl HarborCore { | ||
pub async fn msg(&self, id: Option<Uuid>, 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::<LightningClientModule>() | ||
.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<Bolt11Invoice> { | ||
let client = self.get_client(federation_id).await.fedimint_client; | ||
let lightning_module = client | ||
.get_first_module::<LightningClientModule>() | ||
.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<NetworkUnchecked>, | ||
sats: Option<u64>, | ||
) -> 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::<WalletClientModule>() | ||
.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<Address> { | ||
let client = self.get_client(federation_id).await.fedimint_client; | ||
let onchain = client | ||
.get_first_module::<WalletClientModule>() | ||
.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<ClientConfig> { | ||
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<FederationItem> { | ||
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::<Vec<FederationItem>>() | ||
} | ||
|
||
pub async fn get_seed_words(&self) -> String { | ||
self.mnemonic.to_string() | ||
} | ||
} |
Oops, something went wrong.