From e633557e1bb3467e39523bb14c77bead6a5b4345 Mon Sep 17 00:00:00 2001 From: benthecarman Date: Wed, 15 May 2024 18:04:15 -0500 Subject: [PATCH 1/3] Hookup database to tx items --- src/bridge.rs | 3 + src/components/transaction_item.rs | 8 +- src/core.rs | 114 ++++++++++-- src/db.rs | 278 ++++++++++++++++++++++++++++- src/db_models/lightning_payment.rs | 18 ++ src/db_models/lightning_receive.rs | 18 ++ src/db_models/onchain_payment.rs | 18 ++ src/db_models/onchain_receive.rs | 22 +++ src/fedimint_client.rs | 98 +++++++++- src/main.rs | 4 + 10 files changed, 556 insertions(+), 25 deletions(-) diff --git a/src/bridge.rs b/src/bridge.rs index 0d743a2..66aa93c 100644 --- a/src/bridge.rs +++ b/src/bridge.rs @@ -1,3 +1,4 @@ +use crate::components::TransactionItem; use bitcoin::{Address, Txid}; use fedimint_core::api::InviteCode; use fedimint_core::Amount; @@ -37,6 +38,8 @@ pub enum CoreUIMsg { ReceiveSuccess(ReceiveSuccessMsg), ReceiveFailed(String), BalanceUpdated(Amount), + // todo probably want a way to incrementally add items to the history + TransactionHistoryUpdated(Vec), AddFederationFailed(String), AddFederationSuccess, Unlocking, diff --git a/src/components/transaction_item.rs b/src/components/transaction_item.rs index 301d568..a9bd493 100644 --- a/src/components/transaction_item.rs +++ b/src/components/transaction_item.rs @@ -19,10 +19,10 @@ pub enum TransactionDirection { #[derive(Debug, Clone, Copy)] pub struct TransactionItem { - kind: TransactionItemKind, - amount: u64, - direction: TransactionDirection, - timestamp: u64, + pub kind: TransactionItemKind, + pub amount: u64, + pub direction: TransactionDirection, + pub timestamp: u64, } impl TransactionItem { diff --git a/src/core.rs b/src/core.rs index 29c97dd..31bca5d 100644 --- a/src/core.rs +++ b/src/core.rs @@ -5,6 +5,7 @@ use fedimint_core::api::InviteCode; use fedimint_core::config::FederationId; 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; @@ -42,7 +43,6 @@ use crate::{ const PEG_IN_TIMEOUT_YEAR: Duration = Duration::from_secs(86400 * 365); struct HarborCore { - balance: Amount, network: Network, mnemonic: Mnemonic, tx: Sender, @@ -62,7 +62,16 @@ impl HarborCore { // Sends updates to the UI to refelect the initial state async fn init_ui_state(&self) { - self.msg(CoreUIMsg::BalanceUpdated(self.balance)).await; + let mut balance = Amount::ZERO; + for client in self.clients.read().await.values() { + balance += client.fedimint_client.get_balance().await; + } + + self.msg(CoreUIMsg::BalanceUpdated(balance)).await; + + let history = self.storage.get_transaction_history().unwrap(); + self.msg(CoreUIMsg::TransactionHistoryUpdated(history)) + .await; } // todo for now just use the first client, but eventually we'll want to have a way to select a client @@ -71,6 +80,11 @@ impl HarborCore { } async fn send_lightning(&self, 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().await.fedimint_client; let lightning_module = client.get_first_module::(); @@ -79,18 +93,44 @@ impl HarborCore { .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, ()) + .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(), sub).await; + spawn_internal_payment_subscription( + self.tx.clone(), + client.clone(), + self.storage.clone(), + op_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(), sub).await; + spawn_invoice_payment_subscription( + self.tx.clone(), + client.clone(), + self.storage.clone(), + op_id, + sub, + ) + .await; } } @@ -108,7 +148,7 @@ impl HarborCore { .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, _) = lightning_module + let (op_id, invoice, preimage) = lightning_module .create_bolt11_invoice( amount, Bolt11InvoiceDescription::Direct(&desc), @@ -118,11 +158,27 @@ impl HarborCore { ) .await?; - println!("{}", invoice); + 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(), subscription).await; + spawn_invoice_receive_subscription( + self.tx.clone(), + client.clone(), + self.storage.clone(), + op_id, + subscription, + ) + .await; } else { error!("Could not create subscription to lightning receive"); } @@ -141,12 +197,27 @@ impl HarborCore { let fees = onchain.get_withdraw_fees(address.clone(), amount).await?; let op_id = onchain - .withdraw(address, bitcoin::Amount::from_sat(sats), fees, ()) + .withdraw(address.clone(), bitcoin::Amount::from_sat(sats), 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(), sub).await; + spawn_onchain_payment_subscription( + self.tx.clone(), + client.clone(), + self.storage.clone(), + op_id, + sub, + ) + .await; Ok(()) } @@ -161,9 +232,24 @@ impl HarborCore { let (op_id, address) = onchain.get_deposit_address(valid_until, ()).await?; + self.storage.create_onchain_receive( + op_id, + client.federation_id(), + address.clone(), + 0, // fixme this should be nullable + 0, // fixme this should be nullable + )?; + let sub = onchain.subscribe_deposit_updates(op_id).await?; - spawn_onchain_receive_subscription(self.tx.clone(), client.clone(), sub).await; + spawn_onchain_receive_subscription( + self.tx.clone(), + client.clone(), + self.storage.clone(), + op_id, + sub, + ) + .await; Ok(address) } @@ -268,14 +354,8 @@ pub fn run_core() -> Subscription { clients.insert(client.fedimint_client.federation_id(), client); } - let mut balance = Amount::ZERO; - for client in clients.values() { - balance += client.fedimint_client.get_balance().await; - } - let core = HarborCore { storage: db.clone(), - balance, tx: tx.clone(), mnemonic, network, diff --git a/src/db.rs b/src/db.rs index 3a0de51..c8e9ef4 100644 --- a/src/db.rs +++ b/src/db.rs @@ -1,10 +1,19 @@ -use crate::db_models::{Fedimint, NewFedimint, NewProfile, Profile}; +use crate::components::TransactionItem; +use crate::db_models::{ + Fedimint, LightningPayment, LightningReceive, NewFedimint, NewProfile, OnChainPayment, + OnChainReceive, Profile, +}; +use bitcoin::{Address, Txid}; use diesel::{ connection::SimpleConnection, r2d2::{ConnectionManager, Pool}, SqliteConnection, }; use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness}; +use fedimint_core::config::FederationId; +use fedimint_core::core::OperationId; +use fedimint_core::Amount; +use fedimint_ln_common::lightning_invoice::Bolt11Invoice; use std::{sync::Arc, time::Duration}; pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!(); @@ -51,6 +60,69 @@ pub trait DBConnection { // updates the federation data fn update_fedimint_data(&self, id: String, value: Vec) -> anyhow::Result<()>; + + fn create_ln_receive( + &self, + operation_id: OperationId, + fedimint_id: FederationId, + bolt11: Bolt11Invoice, + amount: Amount, + fee: Amount, + preimage: [u8; 32], + ) -> anyhow::Result<()>; + + fn mark_ln_receive_as_success(&self, operation_id: OperationId) -> anyhow::Result<()>; + + fn mark_ln_receive_as_failed(&self, operation_id: OperationId) -> anyhow::Result<()>; + + fn create_lightning_payment( + &self, + operation_id: OperationId, + fedimint_id: FederationId, + bolt11: Bolt11Invoice, + amount: Amount, + fee: Amount, + ) -> anyhow::Result<()>; + + fn set_lightning_payment_preimage( + &self, + operation_id: OperationId, + preimage: [u8; 32], + ) -> anyhow::Result<()>; + + fn mark_lightning_payment_as_failed(&self, operation_id: OperationId) -> anyhow::Result<()>; + + fn create_onchain_payment( + &self, + operation_id: OperationId, + fedimint_id: FederationId, + address: Address, + amount_sats: u64, + fee_sats: u64, + ) -> anyhow::Result<()>; + + fn set_onchain_payment_txid(&self, operation_id: OperationId, txid: Txid) + -> anyhow::Result<()>; + + fn mark_onchain_payment_as_failed(&self, operation_id: OperationId) -> anyhow::Result<()>; + + fn create_onchain_receive( + &self, + operation_id: OperationId, + fedimint_id: FederationId, + address: Address, + amount_sats: u64, + fee_sats: u64, + ) -> anyhow::Result<()>; + + fn mark_onchain_receive_as_failed(&self, operation_id: OperationId) -> anyhow::Result<()>; + + fn set_onchain_receive_txid(&self, operation_id: OperationId, txid: Txid) + -> anyhow::Result<()>; + + fn mark_onchain_receive_as_confirmed(&self, operation_id: OperationId) -> anyhow::Result<()>; + + fn get_transaction_history(&self) -> anyhow::Result>; } pub(crate) struct SQLConnection { @@ -91,6 +163,210 @@ impl DBConnection for SQLConnection { let f = Fedimint { id, value }; f.update(conn) } + + fn create_ln_receive( + &self, + operation_id: OperationId, + fedimint_id: FederationId, + bolt11: Bolt11Invoice, + amount: Amount, + fee: Amount, + preimage: [u8; 32], + ) -> anyhow::Result<()> { + let conn = &mut self.db.get()?; + + LightningReceive::create( + conn, + operation_id, + fedimint_id, + bolt11, + amount, + fee, + preimage, + )?; + + Ok(()) + } + + fn mark_ln_receive_as_success(&self, operation_id: OperationId) -> anyhow::Result<()> { + let conn = &mut self.db.get()?; + + LightningReceive::mark_as_success(conn, operation_id)?; + + Ok(()) + } + + fn mark_ln_receive_as_failed(&self, operation_id: OperationId) -> anyhow::Result<()> { + let conn = &mut self.db.get()?; + + LightningReceive::mark_as_failed(conn, operation_id)?; + + Ok(()) + } + + fn create_lightning_payment( + &self, + operation_id: OperationId, + fedimint_id: FederationId, + bolt11: Bolt11Invoice, + amount: Amount, + fee: Amount, + ) -> anyhow::Result<()> { + let conn = &mut self.db.get()?; + + LightningPayment::create(conn, operation_id, fedimint_id, bolt11, amount, fee)?; + + Ok(()) + } + + fn set_lightning_payment_preimage( + &self, + operation_id: OperationId, + preimage: [u8; 32], + ) -> anyhow::Result<()> { + let conn = &mut self.db.get()?; + + LightningPayment::set_preimage(conn, operation_id, preimage)?; + + Ok(()) + } + + fn mark_lightning_payment_as_failed(&self, operation_id: OperationId) -> anyhow::Result<()> { + let conn = &mut self.db.get()?; + + LightningPayment::mark_as_failed(conn, operation_id)?; + + Ok(()) + } + + fn create_onchain_receive( + &self, + operation_id: OperationId, + fedimint_id: FederationId, + address: Address, + amount_sats: u64, + fee_sats: u64, + ) -> anyhow::Result<()> { + let conn = &mut self.db.get()?; + + OnChainReceive::create( + conn, + operation_id, + fedimint_id, + address, + amount_sats, + fee_sats, + )?; + + Ok(()) + } + + fn create_onchain_payment( + &self, + operation_id: OperationId, + fedimint_id: FederationId, + address: Address, + amount_sats: u64, + fee_sats: u64, + ) -> anyhow::Result<()> { + let conn = &mut self.db.get()?; + + OnChainPayment::create( + conn, + operation_id, + fedimint_id, + address, + amount_sats, + fee_sats, + )?; + + Ok(()) + } + + fn set_onchain_payment_txid( + &self, + operation_id: OperationId, + txid: Txid, + ) -> anyhow::Result<()> { + let conn = &mut self.db.get()?; + + OnChainPayment::set_txid(conn, operation_id, txid)?; + + Ok(()) + } + + fn mark_onchain_payment_as_failed(&self, operation_id: OperationId) -> anyhow::Result<()> { + let conn = &mut self.db.get()?; + + OnChainPayment::mark_as_failed(conn, operation_id)?; + + Ok(()) + } + + fn mark_onchain_receive_as_failed(&self, operation_id: OperationId) -> anyhow::Result<()> { + let conn = &mut self.db.get()?; + + OnChainReceive::mark_as_failed(conn, operation_id)?; + + Ok(()) + } + + fn set_onchain_receive_txid( + &self, + operation_id: OperationId, + txid: Txid, + ) -> anyhow::Result<()> { + let conn = &mut self.db.get()?; + + OnChainReceive::set_txid(conn, operation_id, txid)?; + + Ok(()) + } + + fn mark_onchain_receive_as_confirmed(&self, operation_id: OperationId) -> anyhow::Result<()> { + let conn = &mut self.db.get()?; + + OnChainReceive::mark_as_confirmed(conn, operation_id)?; + + Ok(()) + } + + fn get_transaction_history(&self) -> anyhow::Result> { + let conn = &mut self.db.get()?; + + let onchain_payments = OnChainPayment::get_history(conn)?; + let onchain_receives = OnChainReceive::get_history(conn)?; + let lightning_payments = LightningPayment::get_history(conn)?; + let lightning_receives = LightningReceive::get_history(conn)?; + + let mut items: Vec = Vec::with_capacity( + onchain_payments.len() + + onchain_receives.len() + + lightning_payments.len() + + lightning_receives.len(), + ); + + for onchain_payment in onchain_payments { + items.push(onchain_payment.into()); + } + + for onchain_receive in onchain_receives { + items.push(onchain_receive.into()); + } + + for lightning_payment in lightning_payments { + items.push(lightning_payment.into()); + } + + for lightning_receive in lightning_receives { + items.push(lightning_receive.into()); + } + + // sort by timestamp so that the most recent items are at the top + items.sort_by(|a, b| b.timestamp.cmp(&a.timestamp)); + + Ok(items) + } } #[derive(Debug)] diff --git a/src/db_models/lightning_payment.rs b/src/db_models/lightning_payment.rs index 8eb9f02..7049179 100644 --- a/src/db_models/lightning_payment.rs +++ b/src/db_models/lightning_payment.rs @@ -1,3 +1,4 @@ +use crate::components::{TransactionDirection, TransactionItem, TransactionItemKind}; use crate::db_models::schema::lightning_payments; use crate::db_models::PaymentStatus; use bitcoin::hashes::hex::{FromHex, ToHex}; @@ -145,4 +146,21 @@ impl LightningPayment { Ok(()) } + + pub fn get_history(conn: &mut SqliteConnection) -> anyhow::Result> { + Ok(lightning_payments::table + .filter(lightning_payments::status.eq(PaymentStatus::Success as i32)) + .load::(conn)?) + } +} + +impl From for TransactionItem { + fn from(payment: LightningPayment) -> Self { + Self { + kind: TransactionItemKind::Lightning, + amount: payment.amount().sats_round_down(), + direction: TransactionDirection::Outgoing, + timestamp: payment.created_at.and_utc().timestamp() as u64, + } + } } diff --git a/src/db_models/lightning_receive.rs b/src/db_models/lightning_receive.rs index f3da374..aaede55 100644 --- a/src/db_models/lightning_receive.rs +++ b/src/db_models/lightning_receive.rs @@ -1,3 +1,4 @@ +use crate::components::{TransactionDirection, TransactionItem, TransactionItemKind}; use crate::db_models::schema::lightning_receives; use crate::db_models::PaymentStatus; use bitcoin::hashes::hex::{FromHex, ToHex}; @@ -142,4 +143,21 @@ impl LightningReceive { Ok(()) } + + pub fn get_history(conn: &mut SqliteConnection) -> anyhow::Result> { + Ok(lightning_receives::table + .filter(lightning_receives::status.eq(PaymentStatus::Success as i32)) + .load::(conn)?) + } +} + +impl From for TransactionItem { + fn from(payment: LightningReceive) -> Self { + Self { + kind: TransactionItemKind::Lightning, + amount: payment.amount().sats_round_down(), + direction: TransactionDirection::Incoming, + timestamp: payment.created_at.and_utc().timestamp() as u64, + } + } } diff --git a/src/db_models/onchain_payment.rs b/src/db_models/onchain_payment.rs index bab519a..538d290 100644 --- a/src/db_models/onchain_payment.rs +++ b/src/db_models/onchain_payment.rs @@ -1,3 +1,4 @@ +use crate::components::{TransactionDirection, TransactionItem, TransactionItemKind}; use crate::db_models::schema::on_chain_payments; use crate::db_models::PaymentStatus; use bitcoin::hashes::hex::{FromHex, ToHex}; @@ -121,4 +122,21 @@ impl OnChainPayment { Ok(()) } + + pub fn get_history(conn: &mut SqliteConnection) -> anyhow::Result> { + Ok(on_chain_payments::table + .filter(on_chain_payments::status.eq(PaymentStatus::Success as i32)) + .load::(conn)?) + } +} + +impl From for TransactionItem { + fn from(payment: OnChainPayment) -> Self { + Self { + kind: TransactionItemKind::Onchain, + amount: payment.amount_sats as u64, + direction: TransactionDirection::Outgoing, + timestamp: payment.created_at.and_utc().timestamp() as u64, + } + } } diff --git a/src/db_models/onchain_receive.rs b/src/db_models/onchain_receive.rs index 8c0c157..e224fa1 100644 --- a/src/db_models/onchain_receive.rs +++ b/src/db_models/onchain_receive.rs @@ -1,3 +1,4 @@ +use crate::components::{TransactionDirection, TransactionItem, TransactionItemKind}; use crate::db_models::schema::on_chain_receives; use crate::db_models::PaymentStatus; use bitcoin::hashes::hex::{FromHex, ToHex}; @@ -135,4 +136,25 @@ impl OnChainReceive { Ok(()) } + + pub fn get_history(conn: &mut SqliteConnection) -> anyhow::Result> { + Ok(on_chain_receives::table + .filter( + on_chain_receives::status + .eq(PaymentStatus::Success as i32) + .or(on_chain_receives::status.eq(PaymentStatus::WaitingConfirmation as i32)), + ) + .load::(conn)?) + } +} + +impl From for TransactionItem { + fn from(payment: OnChainReceive) -> Self { + Self { + kind: TransactionItemKind::Onchain, + amount: payment.amount_sats as u64, + direction: TransactionDirection::Incoming, + timestamp: payment.created_at.and_utc().timestamp() as u64, + } + } } diff --git a/src/fedimint_client.rs b/src/fedimint_client.rs index 7aab8e7..9412522 100644 --- a/src/fedimint_client.rs +++ b/src/fedimint_client.rs @@ -11,6 +11,7 @@ use fedimint_client::oplog::UpdateStreamOrOutcome; use fedimint_client::secret::{get_default_client_secret, RootSecretStrategy}; use fedimint_client::ClientHandleArc; use fedimint_core::config::{ClientConfig, FederationId}; +use fedimint_core::core::OperationId; use fedimint_core::db::mem_impl::MemDatabase; use fedimint_core::db::mem_impl::MemTransaction; use fedimint_core::db::IDatabaseTransactionOps; @@ -224,9 +225,25 @@ pub(crate) async fn select_gateway(client: &ClientHandleArc) -> Option, + sender: &mut Sender, +) { + if let Ok(history) = storage.get_transaction_history() { + sender + .send(Message::CoreMessage(CoreUIMsg::TransactionHistoryUpdated( + history, + ))) + .await + .unwrap(); + } +} + pub(crate) async fn spawn_invoice_receive_subscription( mut sender: Sender, client: ClientHandleArc, + storage: Arc, + operation_id: OperationId, subscription: UpdateStreamOrOutcome, ) { spawn(async move { @@ -241,6 +258,10 @@ pub(crate) async fn spawn_invoice_receive_subscription( ))) .await .unwrap(); + + if let Err(e) = storage.mark_ln_receive_as_failed(operation_id) { + error!("Could not mark lightning receive as failed: {e}"); + } } LnReceiveState::Claimed => { info!("Payment claimed"); @@ -251,12 +272,18 @@ pub(crate) async fn spawn_invoice_receive_subscription( .await .unwrap(); + if let Err(e) = storage.mark_ln_receive_as_success(operation_id) { + error!("Could not mark lightning receive as success: {e}"); + } + let new_balance = client.get_balance().await; sender .send(Message::CoreMessage(CoreUIMsg::BalanceUpdated(new_balance))) .await .unwrap(); + update_history(storage.clone(), &mut sender).await; + break; } _ => {} @@ -268,6 +295,8 @@ pub(crate) async fn spawn_invoice_receive_subscription( pub(crate) async fn spawn_invoice_payment_subscription( mut sender: Sender, client: ClientHandleArc, + storage: Arc, + operation_id: OperationId, subscription: UpdateStreamOrOutcome, ) { spawn(async move { @@ -282,6 +311,11 @@ pub(crate) async fn spawn_invoice_payment_subscription( ))) .await .unwrap(); + + if let Err(e) = storage.mark_lightning_payment_as_failed(operation_id) { + error!("Could not mark lightning payment as failed: {e}"); + } + break; } LnPayState::UnexpectedError { error_message } => { error!("Unexpected payment error: {:?}", error_message); @@ -289,6 +323,11 @@ pub(crate) async fn spawn_invoice_payment_subscription( .send(Message::CoreMessage(CoreUIMsg::SendFailure(error_message))) .await .unwrap(); + + if let Err(e) = storage.mark_lightning_payment_as_failed(operation_id) { + error!("Could not mark lightning payment as failed: {e}"); + } + break; } LnPayState::Success { preimage } => { info!("Payment success"); @@ -300,12 +339,18 @@ pub(crate) async fn spawn_invoice_payment_subscription( .await .unwrap(); + if let Err(e) = storage.set_lightning_payment_preimage(operation_id, preimage) { + error!("Could not mark lightning payment as success: {e}"); + } + let new_balance = client.get_balance().await; sender .send(Message::CoreMessage(CoreUIMsg::BalanceUpdated(new_balance))) .await .unwrap(); + update_history(storage.clone(), &mut sender).await; + break; } _ => {} @@ -317,6 +362,8 @@ pub(crate) async fn spawn_invoice_payment_subscription( pub(crate) async fn spawn_internal_payment_subscription( mut sender: Sender, client: ClientHandleArc, + storage: Arc, + operation_id: OperationId, subscription: UpdateStreamOrOutcome, ) { spawn(async move { @@ -331,6 +378,10 @@ pub(crate) async fn spawn_internal_payment_subscription( ))) .await .unwrap(); + if let Err(e) = storage.mark_lightning_payment_as_failed(operation_id) { + error!("Could not mark lightning payment as failed: {e}"); + } + break; } InternalPayState::UnexpectedError(error_message) => { error!("Unexpected payment error: {error_message:?}"); @@ -338,6 +389,10 @@ pub(crate) async fn spawn_internal_payment_subscription( .send(Message::CoreMessage(CoreUIMsg::SendFailure(error_message))) .await .unwrap(); + if let Err(e) = storage.mark_lightning_payment_as_failed(operation_id) { + error!("Could not mark lightning payment as failed: {e}"); + } + break; } InternalPayState::Preimage(preimage) => { info!("Payment success"); @@ -349,12 +404,19 @@ pub(crate) async fn spawn_internal_payment_subscription( .await .unwrap(); + if let Err(e) = storage.set_lightning_payment_preimage(operation_id, preimage.0) + { + error!("Could not mark lightning payment as success: {e}"); + } + let new_balance = client.get_balance().await; sender .send(Message::CoreMessage(CoreUIMsg::BalanceUpdated(new_balance))) .await .unwrap(); + update_history(storage, &mut sender).await; + break; } _ => {} @@ -366,6 +428,8 @@ pub(crate) async fn spawn_internal_payment_subscription( pub(crate) async fn spawn_onchain_payment_subscription( mut sender: Sender, client: ClientHandleArc, + storage: Arc, + operation_id: OperationId, subscription: UpdateStreamOrOutcome, ) { spawn(async move { @@ -381,6 +445,10 @@ pub(crate) async fn spawn_onchain_payment_subscription( ))) .await .unwrap(); + if let Err(e) = storage.mark_onchain_payment_as_failed(operation_id) { + error!("Could not mark onchain payment as failed: {e}"); + } + break; } WithdrawState::Succeeded(txid) => { @@ -391,12 +459,18 @@ pub(crate) async fn spawn_onchain_payment_subscription( .await .unwrap(); + if let Err(e) = storage.set_onchain_payment_txid(operation_id, txid) { + error!("Could not mark onchain payment txid: {e}"); + } + let new_balance = client.get_balance().await; sender .send(Message::CoreMessage(CoreUIMsg::BalanceUpdated(new_balance))) .await .unwrap(); + update_history(storage.clone(), &mut sender).await; + break; } } @@ -407,6 +481,8 @@ pub(crate) async fn spawn_onchain_payment_subscription( pub(crate) async fn spawn_onchain_receive_subscription( mut sender: Sender, client: ClientHandleArc, + storage: Arc, + operation_id: OperationId, subscription: UpdateStreamOrOutcome, ) { spawn(async move { @@ -422,17 +498,27 @@ pub(crate) async fn spawn_onchain_receive_subscription( ))) .await .unwrap(); + + if let Err(e) = storage.mark_onchain_receive_as_failed(operation_id) { + error!("Could not mark onchain receive as failed: {e}"); + } + break; } DepositState::WaitingForConfirmation(data) => { info!("Onchain receive waiting for confirmation: {data:?}"); - let params = ReceiveSuccessMsg::Onchain { - txid: data.btc_transaction.txid(), - }; + let txid = data.btc_transaction.txid(); + let params = ReceiveSuccessMsg::Onchain { txid }; sender .send(Message::CoreMessage(CoreUIMsg::ReceiveSuccess(params))) .await .unwrap(); + + if let Err(e) = storage.set_onchain_receive_txid(operation_id, txid) { + error!("Could not mark onchain payment txid: {e}"); + } + + update_history(storage.clone(), &mut sender).await; } DepositState::Confirmed(data) => { info!("Onchain receive confirmed: {data:?}"); @@ -445,6 +531,12 @@ pub(crate) async fn spawn_onchain_receive_subscription( .await .unwrap(); + if let Err(e) = storage.mark_onchain_receive_as_confirmed(operation_id) { + error!("Could not mark onchain payment txid: {e}"); + } + + update_history(storage.clone(), &mut sender).await; + break; } } diff --git a/src/main.rs b/src/main.rs index f728883..0c626e3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -377,6 +377,10 @@ impl HarborWallet { self.balance_sats = balance.sats_round_down(); Command::none() } + CoreUIMsg::TransactionHistoryUpdated(history) => { + self.transaction_history = history; + Command::none() + } CoreUIMsg::ReceiveGenerating => { self.receive_status = ReceiveStatus::Generating; Command::none() From 82fc5b2802fb7240d463779168a6b59f29c68dc0 Mon Sep 17 00:00:00 2001 From: benthecarman Date: Wed, 15 May 2024 18:12:41 -0500 Subject: [PATCH 2/3] Fix onchain receive table to only have amount after a receieve --- .../2024-05-13-234832_create_config/up.sql | 4 +- src/core.rs | 9 +---- src/db.rs | 38 +++++++++---------- src/db_models/onchain_receive.rs | 16 ++++---- src/db_models/schema.rs | 4 +- src/fedimint_client.rs | 7 +++- 6 files changed, 37 insertions(+), 41 deletions(-) diff --git a/migrations/2024-05-13-234832_create_config/up.sql b/migrations/2024-05-13-234832_create_config/up.sql index 969fd05..877d2ab 100644 --- a/migrations/2024-05-13-234832_create_config/up.sql +++ b/migrations/2024-05-13-234832_create_config/up.sql @@ -56,8 +56,8 @@ CREATE TABLE on_chain_receives operation_id TEXT PRIMARY KEY NOT NULL, fedimint_id TEXT NOT NULL REFERENCES fedimint (id), address TEXT NOT NULL, - amount_sats BIGINT NOT NULL, - fee_sats BIGINT NOT NULL, + amount_sats BIGINT, + fee_sats BIGINT, txid TEXT, status INTEGER NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, diff --git a/src/core.rs b/src/core.rs index 31bca5d..a69e9f5 100644 --- a/src/core.rs +++ b/src/core.rs @@ -232,13 +232,8 @@ impl HarborCore { let (op_id, address) = onchain.get_deposit_address(valid_until, ()).await?; - self.storage.create_onchain_receive( - op_id, - client.federation_id(), - address.clone(), - 0, // fixme this should be nullable - 0, // fixme this should be nullable - )?; + self.storage + .create_onchain_receive(op_id, client.federation_id(), address.clone())?; let sub = onchain.subscribe_deposit_updates(op_id).await?; diff --git a/src/db.rs b/src/db.rs index c8e9ef4..4fe4e29 100644 --- a/src/db.rs +++ b/src/db.rs @@ -111,14 +111,17 @@ pub trait DBConnection { operation_id: OperationId, fedimint_id: FederationId, address: Address, - amount_sats: u64, - fee_sats: u64, ) -> anyhow::Result<()>; fn mark_onchain_receive_as_failed(&self, operation_id: OperationId) -> anyhow::Result<()>; - fn set_onchain_receive_txid(&self, operation_id: OperationId, txid: Txid) - -> anyhow::Result<()>; + fn set_onchain_receive_txid( + &self, + operation_id: OperationId, + txid: Txid, + amount_sats: u64, + fee_sats: u64, + ) -> anyhow::Result<()>; fn mark_onchain_receive_as_confirmed(&self, operation_id: OperationId) -> anyhow::Result<()>; @@ -244,19 +247,10 @@ impl DBConnection for SQLConnection { operation_id: OperationId, fedimint_id: FederationId, address: Address, - amount_sats: u64, - fee_sats: u64, ) -> anyhow::Result<()> { let conn = &mut self.db.get()?; - OnChainReceive::create( - conn, - operation_id, - fedimint_id, - address, - amount_sats, - fee_sats, - )?; + OnChainReceive::create(conn, operation_id, fedimint_id, address)?; Ok(()) } @@ -315,10 +309,12 @@ impl DBConnection for SQLConnection { &self, operation_id: OperationId, txid: Txid, + amount_sats: u64, + fee_sats: u64, ) -> anyhow::Result<()> { let conn = &mut self.db.get()?; - OnChainReceive::set_txid(conn, operation_id, txid)?; + OnChainReceive::set_txid(conn, operation_id, txid, amount_sats, fee_sats)?; Ok(()) } @@ -667,8 +663,6 @@ mod tests { operation_id, FederationId::from_str(FEDERATION_ID).unwrap(), address.clone(), - amount, - fee, ) .unwrap(); @@ -682,15 +676,15 @@ mod tests { FederationId::from_str(FEDERATION_ID).unwrap() ); assert_eq!(payment.address(), address); - assert_eq!(payment.amount_sats as u64, amount); - assert_eq!(payment.fee_sats as u64, fee); + assert!(payment.amount_sats.is_none()); + assert!(payment.fee_sats.is_none()); assert_eq!(payment.txid(), None); assert_eq!(payment.status(), PaymentStatus::Pending); // sleep for a second to make sure the timestamps are different std::thread::sleep(Duration::from_secs(1)); - OnChainReceive::set_txid(&mut conn, operation_id, Txid::all_zeros()).unwrap(); + OnChainReceive::set_txid(&mut conn, operation_id, Txid::all_zeros(), amount, fee).unwrap(); let with_txid = OnChainReceive::get_by_operation_id(&mut conn, operation_id) .unwrap() @@ -698,6 +692,8 @@ mod tests { assert_eq!(with_txid.status(), PaymentStatus::WaitingConfirmation); assert_eq!(with_txid.txid(), Some(Txid::all_zeros())); + assert_eq!(with_txid.amount_sats, Some(amount as i64)); + assert_eq!(with_txid.fee_sats, Some(fee as i64)); assert_ne!(with_txid.updated_at, with_txid.created_at); assert_ne!(with_txid.updated_at, payment.updated_at); @@ -712,6 +708,8 @@ mod tests { assert_eq!(confirmed.status(), PaymentStatus::Success); assert_eq!(confirmed.txid(), Some(Txid::all_zeros())); + assert_eq!(with_txid.amount_sats, Some(amount as i64)); + assert_eq!(with_txid.fee_sats, Some(fee as i64)); assert_ne!(confirmed.updated_at, confirmed.created_at); assert_ne!(confirmed.updated_at, with_txid.updated_at); } diff --git a/src/db_models/onchain_receive.rs b/src/db_models/onchain_receive.rs index e224fa1..2429149 100644 --- a/src/db_models/onchain_receive.rs +++ b/src/db_models/onchain_receive.rs @@ -14,8 +14,8 @@ pub struct OnChainReceive { operation_id: String, fedimint_id: String, address: String, - pub amount_sats: i64, - pub fee_sats: i64, + pub amount_sats: Option, + pub fee_sats: Option, txid: Option, status: i32, pub created_at: chrono::NaiveDateTime, @@ -28,8 +28,6 @@ struct NewOnChainReceive { operation_id: String, fedimint_id: String, address: String, - amount_sats: i64, - fee_sats: i64, status: i32, } @@ -61,15 +59,11 @@ impl OnChainReceive { operation_id: OperationId, fedimint_id: FederationId, address: Address, - amount_sats: u64, - fee_sats: u64, ) -> anyhow::Result<()> { let new = NewOnChainReceive { operation_id: operation_id.to_string(), fedimint_id: fedimint_id.to_string(), address: address.to_string(), - amount_sats: amount_sats as i64, - fee_sats: fee_sats as i64, status: PaymentStatus::Pending as i32, }; @@ -94,6 +88,8 @@ impl OnChainReceive { conn: &mut SqliteConnection, operation_id: OperationId, txid: Txid, + amount_sats: u64, + fee_sats: u64, ) -> anyhow::Result<()> { diesel::update( on_chain_receives::table @@ -101,6 +97,8 @@ impl OnChainReceive { ) .set(( on_chain_receives::txid.eq(Some(txid.to_hex())), + on_chain_receives::amount_sats.eq(Some(amount_sats as i64)), + on_chain_receives::fee_sats.eq(Some(fee_sats as i64)), on_chain_receives::status.eq(PaymentStatus::WaitingConfirmation as i32), )) .execute(conn)?; @@ -152,7 +150,7 @@ impl From for TransactionItem { fn from(payment: OnChainReceive) -> Self { Self { kind: TransactionItemKind::Onchain, - amount: payment.amount_sats as u64, + amount: payment.amount_sats.unwrap_or(0) as u64, // todo handle this better direction: TransactionDirection::Incoming, timestamp: payment.created_at.and_utc().timestamp() as u64, } diff --git a/src/db_models/schema.rs b/src/db_models/schema.rs index 2eed2d8..76b79f3 100644 --- a/src/db_models/schema.rs +++ b/src/db_models/schema.rs @@ -56,8 +56,8 @@ diesel::table! { operation_id -> Text, fedimint_id -> Text, address -> Text, - amount_sats -> BigInt, - fee_sats -> BigInt, + amount_sats -> Nullable, + fee_sats -> Nullable, txid -> Nullable, status -> Integer, created_at -> Timestamp, diff --git a/src/fedimint_client.rs b/src/fedimint_client.rs index 9412522..550dc13 100644 --- a/src/fedimint_client.rs +++ b/src/fedimint_client.rs @@ -508,13 +508,18 @@ pub(crate) async fn spawn_onchain_receive_subscription( DepositState::WaitingForConfirmation(data) => { info!("Onchain receive waiting for confirmation: {data:?}"); let txid = data.btc_transaction.txid(); + let index = data.out_idx as usize; + let amount = data.btc_transaction.output[index].value; let params = ReceiveSuccessMsg::Onchain { txid }; sender .send(Message::CoreMessage(CoreUIMsg::ReceiveSuccess(params))) .await .unwrap(); - if let Err(e) = storage.set_onchain_receive_txid(operation_id, txid) { + let fee_sats = 0; // fees for receives may exist one day + if let Err(e) = + storage.set_onchain_receive_txid(operation_id, txid, amount, fee_sats) + { error!("Could not mark onchain payment txid: {e}"); } From c7f8bfe4437f134ba0e2b67ddb4b349fa67d09dd Mon Sep 17 00:00:00 2001 From: benthecarman Date: Wed, 15 May 2024 18:13:26 -0500 Subject: [PATCH 3/3] Remove fake tx button --- src/main.rs | 11 ----------- src/routes/history.rs | 7 ++----- 2 files changed, 2 insertions(+), 16 deletions(-) diff --git a/src/main.rs b/src/main.rs index 0c626e3..94ada4e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -94,8 +94,6 @@ pub enum Message { Donate, // Core messages we get from core CoreMessage(CoreUIMsg), - // Fake stuff for testing - FakeAddTransaction, } // This is the UI state. It should only contain data that is directly rendered by the UI @@ -338,15 +336,6 @@ impl HarborWallet { println!("Copying to clipboard: {s}"); clipboard::write(s) } - Message::FakeAddTransaction => { - if self.transaction_history.len() % 2 == 0 { - self.transaction_history - .push(TransactionItem::make_dummy_onchain()); - } else { - self.transaction_history.push(TransactionItem::make_dummy()); - } - Command::none() - } // Handle any messages we get from core Message::CoreMessage(msg) => match msg { CoreUIMsg::Sending => { diff --git a/src/routes/history.rs b/src/routes/history.rs index 6cf6fe7..a5975ce 100644 --- a/src/routes/history.rs +++ b/src/routes/history.rs @@ -2,15 +2,12 @@ use iced::widget::{column, container, scrollable}; use iced::Element; use iced::{Length, Padding}; -use crate::components::{h_button, h_header, h_transaction_item, SvgIcon}; +use crate::components::{h_header, h_transaction_item}; use crate::{HarborWallet, Message}; pub fn history(harbor: &HarborWallet) -> Element { let header = h_header("History", "Here's what's happened so far."); - let fake_button = - h_button("Add Transaction", SvgIcon::Squirrel, false).on_press(Message::FakeAddTransaction); - let transactions = harbor .transaction_history .iter() @@ -18,7 +15,7 @@ pub fn history(harbor: &HarborWallet) -> Element { column.push(h_transaction_item(item)) }); - let column = column![header, fake_button, transactions].spacing(48); + let column = column![header, transactions].spacing(48); container(scrollable( column