From dd48135cc3dcb4fd7c4bbdb824f310a280aa662d Mon Sep 17 00:00:00 2001 From: Ash Manning Date: Mon, 25 Mar 2024 16:35:02 +0800 Subject: [PATCH] refactor parent chain & coins tabs --- Cargo.lock | 4 +- Cargo.toml | 4 +- app/app.rs | 10 +- app/gui/coins/mod.rs | 42 ++++ app/gui/coins/tx_builder.rs | 143 ++++++++++++ app/gui/coins/tx_creator.rs | 68 ++++++ app/gui/{ => coins}/utxo_creator.rs | 32 +-- app/gui/{ => coins}/utxo_selector.rs | 26 +-- app/gui/deposit.rs | 59 ----- app/gui/miner.rs | 5 +- app/gui/mod.rs | 210 ++---------------- .../{parent_chain.rs => parent_chain/info.rs} | 14 +- app/gui/parent_chain/mod.rs | 55 +++++ app/gui/parent_chain/transfer.rs | 209 +++++++++++++++++ app/gui/util.rs | 23 +- app/main.rs | 1 - app/rpc_api.rs | 11 +- app/rpc_server.rs | 31 ++- lib/types/transaction.rs | 2 +- 19 files changed, 642 insertions(+), 307 deletions(-) create mode 100644 app/gui/coins/mod.rs create mode 100644 app/gui/coins/tx_builder.rs create mode 100644 app/gui/coins/tx_creator.rs rename app/gui/{ => coins}/utxo_creator.rs (95%) rename app/gui/{ => coins}/utxo_selector.rs (79%) delete mode 100644 app/gui/deposit.rs rename app/gui/{parent_chain.rs => parent_chain/info.rs} (87%) create mode 100644 app/gui/parent_chain/mod.rs create mode 100644 app/gui/parent_chain/transfer.rs diff --git a/Cargo.lock b/Cargo.lock index 24da1c8..fa43f5e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4189,7 +4189,7 @@ dependencies = [ [[package]] name = "thunder" -version = "0.5.0" +version = "0.5.1" dependencies = [ "anyhow", "bincode", @@ -4219,7 +4219,7 @@ dependencies = [ [[package]] name = "thunder_app" -version = "0.5.0" +version = "0.5.1" dependencies = [ "anyhow", "base64 0.21.7", diff --git a/Cargo.toml b/Cargo.toml index 9ce1a1e..25147bb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ authors = [ "Nikita Chashchinskii " ] edition = "2021" -version = "0.5.0" +version = "0.5.1" [workspace.dependencies.bip300301] git = "https://github.com/Ash-L2L/bip300301.git" @@ -19,4 +19,4 @@ rev = "c6e410e702f3d22f5801f21ffdf39edece3985df" [profile.release] -lto = "fat" +# lto = "fat" diff --git a/app/app.rs b/app/app.rs index 3afaf34..00e37b9 100644 --- a/app/app.rs +++ b/app/app.rs @@ -97,16 +97,10 @@ impl App { }) } - pub fn sign_and_send(&mut self) -> Result<(), Error> { - let authorized_transaction = - self.wallet.authorize(self.transaction.read().clone())?; + pub fn sign_and_send(&self, tx: Transaction) -> Result<(), Error> { + let authorized_transaction = self.wallet.authorize(tx)?; self.runtime .block_on(self.node.submit_transaction(&authorized_transaction))?; - *self.transaction.write() = Transaction { - inputs: vec![], - proof: Proof::default(), - outputs: vec![], - }; self.update_utxos()?; Ok(()) } diff --git a/app/gui/coins/mod.rs b/app/gui/coins/mod.rs new file mode 100644 index 0000000..5d3308e --- /dev/null +++ b/app/gui/coins/mod.rs @@ -0,0 +1,42 @@ +use eframe::egui; +use strum::{EnumIter, IntoEnumIterator}; + +use crate::app::App; + +mod tx_builder; +mod tx_creator; +mod utxo_creator; +mod utxo_selector; + +use tx_builder::TxBuilder; + +#[derive(Default, EnumIter, Eq, PartialEq, strum::Display)] +enum Tab { + #[default] + #[strum(to_string = "Transaction Builder")] + TransactionBuilder, +} + +#[derive(Default)] +pub struct Coins { + tab: Tab, + tx_builder: TxBuilder, +} + +impl Coins { + pub fn show(&mut self, app: &mut App, ui: &mut egui::Ui) { + egui::TopBottomPanel::top("coins_tabs").show(ui.ctx(), |ui| { + ui.horizontal(|ui| { + Tab::iter().for_each(|tab_variant| { + let tab_name = tab_variant.to_string(); + ui.selectable_value(&mut self.tab, tab_variant, tab_name); + }) + }); + }); + egui::CentralPanel::default().show(ui.ctx(), |ui| match self.tab { + Tab::TransactionBuilder => { + let () = self.tx_builder.show(app, ui).unwrap(); + } + }); + } +} diff --git a/app/gui/coins/tx_builder.rs b/app/gui/coins/tx_builder.rs new file mode 100644 index 0000000..2f2faea --- /dev/null +++ b/app/gui/coins/tx_builder.rs @@ -0,0 +1,143 @@ +use std::collections::HashSet; + +use eframe::egui; + +use thunder::{ + bip300301::bitcoin, + types::{GetValue, Transaction}, +}; + +use super::{ + tx_creator::TxCreator, + utxo_creator::UtxoCreator, + utxo_selector::{show_utxo, UtxoSelector}, +}; +use crate::app::App; + +#[derive(Debug, Default)] +pub struct TxBuilder { + // regular tx without extra data or special inputs/outputs + base_tx: Transaction, + tx_creator: TxCreator, + utxo_creator: UtxoCreator, + utxo_selector: UtxoSelector, +} + +impl TxBuilder { + pub fn show_value_in(&mut self, app: &mut App, ui: &mut egui::Ui) { + ui.heading("Value In"); + let selected: HashSet<_> = self + .base_tx + .inputs + .iter() + .map(|(outpoint, _)| *outpoint) + .collect(); + let utxos_read = app.utxos.read(); + let mut spent_utxos: Vec<_> = utxos_read + .iter() + .filter(|(outpoint, _)| selected.contains(outpoint)) + .collect(); + let value_in: u64 = spent_utxos + .iter() + .map(|(_, output)| output.get_value()) + .sum(); + self.tx_creator.value_in = value_in; + spent_utxos.sort_by_key(|(outpoint, _)| format!("{outpoint}")); + ui.separator(); + ui.monospace(format!("Total: {}", bitcoin::Amount::from_sat(value_in))); + ui.separator(); + egui::Grid::new("utxos").striped(true).show(ui, |ui| { + ui.monospace("kind"); + ui.monospace("outpoint"); + ui.monospace("value"); + ui.end_row(); + let mut remove = None; + for (vout, (outpoint, _)) in self.base_tx.inputs.iter().enumerate() + { + let output = &utxos_read[outpoint]; + show_utxo(ui, outpoint, output); + if ui.button("remove").clicked() { + remove = Some(vout); + } + ui.end_row(); + } + if let Some(vout) = remove { + self.base_tx.inputs.remove(vout); + } + }); + } + + pub fn show_value_out(&mut self, ui: &mut egui::Ui) { + ui.heading("Value Out"); + ui.separator(); + let value_out: u64 = + self.base_tx.outputs.iter().map(GetValue::get_value).sum(); + self.tx_creator.value_out = value_out; + ui.monospace(format!( + "Total: {}", + bitcoin::Amount::from_sat(value_out) + )); + ui.separator(); + egui::Grid::new("outputs").striped(true).show(ui, |ui| { + let mut remove = None; + ui.monospace("vout"); + ui.monospace("address"); + ui.monospace("value"); + ui.end_row(); + for (vout, output) in self.base_tx.outputs.iter().enumerate() { + let address = &format!("{}", output.address)[0..8]; + let value = bitcoin::Amount::from_sat(output.get_value()); + ui.monospace(format!("{vout}")); + ui.monospace(address.to_string()); + ui.with_layout( + egui::Layout::right_to_left(egui::Align::Max), + |ui| { + ui.monospace(format!("{value}")); + }, + ); + if ui.button("remove").clicked() { + remove = Some(vout); + } + ui.end_row(); + } + if let Some(vout) = remove { + self.base_tx.outputs.remove(vout); + } + }); + } + + pub fn show( + &mut self, + app: &mut App, + ui: &mut egui::Ui, + ) -> anyhow::Result<()> { + egui::SidePanel::left("spend_utxo") + .exact_width(250.) + .resizable(false) + .show_inside(ui, |ui| { + self.utxo_selector.show(app, ui, &mut self.base_tx); + }); + egui::SidePanel::left("value_in") + .exact_width(250.) + .resizable(false) + .show_inside(ui, |ui| { + let () = self.show_value_in(app, ui); + }); + egui::SidePanel::left("value_out") + .exact_width(250.) + .resizable(false) + .show_inside(ui, |ui| { + let () = self.show_value_out(ui); + }); + egui::SidePanel::left("create_utxo") + .exact_width(450.) + .resizable(false) + .show_separator_line(false) + .show_inside(ui, |ui| { + self.utxo_creator.show(app, ui, &mut self.base_tx); + ui.separator(); + self.tx_creator.show(app, ui, &mut self.base_tx).unwrap(); + }); + Ok(()) + } +} diff --git a/app/gui/coins/tx_creator.rs b/app/gui/coins/tx_creator.rs new file mode 100644 index 0000000..d93ba77 --- /dev/null +++ b/app/gui/coins/tx_creator.rs @@ -0,0 +1,68 @@ +use eframe::egui; + +use thunder::{ + bip300301::bitcoin, + types::{Transaction, Txid}, +}; + +use crate::app::App; + +#[derive(Debug, Default)] +pub struct TxCreator { + pub value_in: u64, + pub value_out: u64, + // if the base tx has changed, need to recompute final tx + base_txid: Txid, + final_tx: Option, +} + +fn send_tx(app: &App, tx: &mut Transaction) -> anyhow::Result<()> { + app.node.regenerate_proof(tx)?; + let () = app.sign_and_send(tx.clone())?; + Ok(()) +} + +impl TxCreator { + pub fn show( + &mut self, + app: &mut App, + ui: &mut egui::Ui, + base_tx: &mut Transaction, + ) -> anyhow::Result<()> { + // if base txid has changed, store the new txid + let base_txid = base_tx.txid(); + let base_txid_changed = base_txid != self.base_txid; + if base_txid_changed { + self.base_txid = base_txid; + } + // (re)compute final tx if: + // * the tx type, tx data, or base txid has changed + // * final tx not yet set + let refresh_final_tx = base_txid_changed || self.final_tx.is_none(); + if refresh_final_tx { + self.final_tx = Some(base_tx.clone()); + } + let final_tx = match &mut self.final_tx { + None => panic!("impossible! final tx should have been set"), + Some(final_tx) => final_tx, + }; + let txid = &format!("{}", final_tx.txid())[0..8]; + ui.monospace(format!("txid: {txid}")); + if self.value_in >= self.value_out { + let fee = self.value_in - self.value_out; + let fee = bitcoin::Amount::from_sat(fee); + ui.monospace(format!("fee: {fee}")); + if ui.button("sign and send").clicked() { + if let Err(err) = send_tx(app, final_tx) { + tracing::error!("{err:#}"); + } else { + *base_tx = Transaction::default(); + self.final_tx = None; + } + } + } else { + ui.label("Not Enough Value In"); + } + Ok(()) + } +} diff --git a/app/gui/utxo_creator.rs b/app/gui/coins/utxo_creator.rs similarity index 95% rename from app/gui/utxo_creator.rs rename to app/gui/coins/utxo_creator.rs index 88c0c48..da728b7 100644 --- a/app/gui/utxo_creator.rs +++ b/app/gui/coins/utxo_creator.rs @@ -1,20 +1,12 @@ use eframe::egui; use thunder::{ bip300301::bitcoin, - types::{self, Output, OutputContent}, + types::{self, Output, OutputContent, Transaction}, }; use crate::app::App; -pub struct UtxoCreator { - utxo_type: UtxoType, - value: String, - address: String, - main_address: String, - main_fee: String, -} - -#[derive(Eq, PartialEq)] +#[derive(Debug, Eq, PartialEq)] enum UtxoType { Regular, Withdrawal, @@ -29,6 +21,15 @@ impl std::fmt::Display for UtxoType { } } +#[derive(Debug)] +pub struct UtxoCreator { + utxo_type: UtxoType, + value: String, + address: String, + main_address: String, + main_fee: String, +} + impl Default for UtxoCreator { fn default() -> Self { Self { @@ -42,7 +43,12 @@ impl Default for UtxoCreator { } impl UtxoCreator { - pub fn show(&mut self, app: &mut App, ui: &mut egui::Ui) { + pub fn show( + &mut self, + app: &mut App, + ui: &mut egui::Ui, + tx: &mut Transaction, + ) { ui.horizontal(|ui| { ui.heading("Create"); egui::ComboBox::from_id_source("utxo_type") @@ -124,7 +130,7 @@ impl UtxoCreator { value.expect("should not happen").to_sat(), ), }; - app.transaction.write().outputs.push(utxo); + tx.outputs.push(utxo); } } UtxoType::Withdrawal => { @@ -166,7 +172,7 @@ impl UtxoCreator { .to_sat(), }, }; - app.transaction.write().outputs.push(utxo); + tx.outputs.push(utxo); } } } diff --git a/app/gui/utxo_selector.rs b/app/gui/coins/utxo_selector.rs similarity index 79% rename from app/gui/utxo_selector.rs rename to app/gui/coins/utxo_selector.rs index 91b6bdd..c568234 100644 --- a/app/gui/utxo_selector.rs +++ b/app/gui/coins/utxo_selector.rs @@ -3,24 +3,24 @@ use std::collections::HashSet; use eframe::egui; use thunder::{ bip300301::bitcoin, - types::{hash, GetValue, OutPoint, Output, PointedOutput}, + types::{hash, GetValue, OutPoint, Output, PointedOutput, Transaction}, }; use crate::app::App; -#[derive(Default)] +#[derive(Debug, Default)] pub struct UtxoSelector; impl UtxoSelector { - pub fn show(&mut self, app: &mut App, ui: &mut egui::Ui) { + pub fn show( + &mut self, + app: &mut App, + ui: &mut egui::Ui, + tx: &mut Transaction, + ) { ui.heading("Spend UTXO"); - let selected: HashSet<_> = app - .transaction - .read() - .inputs - .iter() - .map(|(outpoint, _)| *outpoint) - .collect(); + let selected: HashSet<_> = + tx.inputs.iter().map(|(outpoint, _)| *outpoint).collect(); let utxos_read = app.utxos.read(); let total: u64 = utxos_read .iter() @@ -56,11 +56,7 @@ impl UtxoSelector { outpoint, output: output.clone(), }); - let mut tx_write = app.transaction.write(); - tx_write.inputs.push((outpoint, utxo_hash)); - if let Err(err) = app.node.regenerate_proof(&mut tx_write) { - tracing::error!("{err}") - } + tx.inputs.push((outpoint, utxo_hash)); } ui.end_row(); } diff --git a/app/gui/deposit.rs b/app/gui/deposit.rs deleted file mode 100644 index 441f4d9..0000000 --- a/app/gui/deposit.rs +++ /dev/null @@ -1,59 +0,0 @@ -use eframe::egui; -use thunder::bip300301::bitcoin; - -use crate::app::App; - -pub struct Deposit { - amount: String, - fee: String, -} - -impl Default for Deposit { - fn default() -> Self { - Self { - amount: "".into(), - fee: "".into(), - } - } -} - -impl Deposit { - pub fn show(&mut self, app: &mut App, ui: &mut egui::Ui) { - ui.horizontal(|ui| { - let amount_edit = egui::TextEdit::singleline(&mut self.amount) - .hint_text("amount") - .desired_width(80.); - ui.add(amount_edit); - ui.label("BTC"); - }); - ui.horizontal(|ui| { - let fee_edit = egui::TextEdit::singleline(&mut self.fee) - .hint_text("fee") - .desired_width(80.); - ui.add(fee_edit); - ui.label("BTC"); - }); - - let amount = bitcoin::Amount::from_str_in( - &self.amount, - bitcoin::Denomination::Bitcoin, - ); - let fee = bitcoin::Amount::from_str_in( - &self.fee, - bitcoin::Denomination::Bitcoin, - ); - - if ui - .add_enabled( - amount.is_ok() && fee.is_ok(), - egui::Button::new("deposit"), - ) - .clicked() - { - let _result = app.deposit( - amount.expect("should not happen"), - fee.expect("should not happen"), - ); - } - } -} diff --git a/app/gui/miner.rs b/app/gui/miner.rs index cc63f0a..7a32a57 100644 --- a/app/gui/miner.rs +++ b/app/gui/miner.rs @@ -30,7 +30,10 @@ impl Miner { let best_hash = &format!("{best_hash}")[0..8]; ui.monospace(format!("{best_hash}...")); let running = self.running.load(atomic::Ordering::SeqCst); - if ui.add_enabled(!running, Button::new("Mine")).clicked() { + if ui + .add_enabled(!running, Button::new("Mine / Refresh Block")) + .clicked() + { self.running.store(true, atomic::Ordering::SeqCst); app.runtime.spawn({ let app = app.clone(); diff --git a/app/gui/mod.rs b/app/gui/mod.rs index 5dba778..398d5c2 100644 --- a/app/gui/mod.rs +++ b/app/gui/mod.rs @@ -1,62 +1,53 @@ -use std::collections::HashSet; - use eframe::egui; -use parking_lot::lock_api::RwLockUpgradableReadGuard; use strum::{EnumIter, IntoEnumIterator}; -use thunder::{bip300301::bitcoin, types::GetValue}; use crate::{app::App, logs::LogsCapture}; mod block_explorer; -mod deposit; +mod coins; mod logs; mod mempool_explorer; mod miner; mod parent_chain; mod seed; mod util; -mod utxo_creator; -mod utxo_selector; mod withdrawals; use block_explorer::BlockExplorer; -use deposit::Deposit; +use coins::Coins; use logs::Logs; use mempool_explorer::MemPoolExplorer; use miner::Miner; use parent_chain::ParentChain; use seed::SetSeed; -use utxo_selector::{show_utxo, UtxoSelector}; - -use self::{utxo_creator::UtxoCreator, withdrawals::Withdrawals}; +use withdrawals::Withdrawals; pub struct EguiApp { app: App, block_explorer: BlockExplorer, - deposit: Deposit, + coins: Coins, logs: Logs, mempool_explorer: MemPoolExplorer, miner: Miner, parent_chain: ParentChain, set_seed: SetSeed, tab: Tab, - utxo_creator: UtxoCreator, - utxo_selector: UtxoSelector, withdrawals: Withdrawals, } -#[derive(EnumIter, Eq, PartialEq, strum::Display)] +#[derive(Default, EnumIter, Eq, PartialEq, strum::Display)] enum Tab { - #[strum(to_string = "Transaction Builder")] - TransactionBuilder, + #[default] + #[strum(to_string = "Parent Chain")] + ParentChain, + #[strum(to_string = "Coins")] + Coins, #[strum(to_string = "Mempool Explorer")] MemPoolExplorer, #[strum(to_string = "Block Explorer")] BlockExplorer, #[strum(to_string = "Withdrawals")] Withdrawals, - #[strum(to_string = "Parent Chain")] - ParentChain, #[strum(to_string = "Logs")] Logs, } @@ -76,15 +67,13 @@ impl EguiApp { Self { app, block_explorer: BlockExplorer::new(height), - deposit: Deposit::default(), + coins: Coins::default(), logs: Logs::new(logs_capture), mempool_explorer: MemPoolExplorer::default(), miner: Miner::default(), parent_chain, set_seed: SetSeed::default(), - tab: Tab::TransactionBuilder, - utxo_creator: UtxoCreator::default(), - utxo_selector: UtxoSelector, + tab: Tab::default(), withdrawals: Withdrawals::default(), } } @@ -106,8 +95,6 @@ impl EguiApp { // it up if you have multiple widgets to expand, even with different ratios. let this_target_width = this_init_max_width - last_others_width; - self.deposit.show(&mut self.app, ui); - ui.separator(); ui.add_space(this_target_width); ui.separator(); self.miner.show(&self.app, ui); @@ -141,175 +128,9 @@ impl eframe::App for EguiApp { egui::TopBottomPanel::bottom("util") .show(ctx, |ui| self.bottom_panel_content(ui)); egui::CentralPanel::default().show(ctx, |ui| match self.tab { - Tab::TransactionBuilder => { - let tx_read = self.app.transaction.read(); - let selected: HashSet<_> = tx_read - .inputs - .iter() - .map(|(outpoint, _)| *outpoint) - .collect(); - let utxos = self.app.utxos.clone(); - let value_in: u64 = utxos - .read() - .iter() - .filter(|(outpoint, _)| selected.contains(outpoint)) - .map(|(_, output)| output.get_value()) - .sum(); - let value_out: u64 = - tx_read.outputs.iter().map(GetValue::get_value).sum(); - drop(tx_read); - egui::SidePanel::left("spend_utxo") - .exact_width(250.) - .resizable(false) - .show_inside(ui, |ui| { - self.utxo_selector.show(&mut self.app.clone(), ui); - }); - egui::SidePanel::left("value_in") - .exact_width(250.) - .resizable(false) - .show_inside(ui, |ui| { - ui.heading("Value In"); - let utxos_read = utxos.read(); - let mut utxos: Vec<_> = utxos_read - .iter() - .filter(|(outpoint, _)| { - selected.contains(outpoint) - }) - .collect(); - utxos.sort_by_key(|(outpoint, _)| { - format!("{outpoint}") - }); - ui.separator(); - ui.monospace(format!( - "Total: {}", - bitcoin::Amount::from_sat(value_in) - )); - ui.separator(); - egui::Grid::new("utxos").striped(true).show( - ui, - |ui| { - ui.monospace("kind"); - ui.monospace("outpoint"); - ui.monospace("value"); - ui.end_row(); - let tx_read = - self.app.transaction.upgradable_read(); - let mut remove = None; - for (vout, (outpoint, _)) in - tx_read.inputs.iter().enumerate() - { - let output = &utxos_read[outpoint]; - show_utxo(ui, outpoint, output); - if ui.button("remove").clicked() { - remove = Some(vout); - } - ui.end_row(); - } - if let Some(vout) = remove { - let mut tx_write = - RwLockUpgradableReadGuard::upgrade( - tx_read, - ); - tx_write.inputs.remove(vout); - if let Err(err) = self - .app - .node - .regenerate_proof(&mut tx_write) - { - tracing::error!("{err}") - } - } - }, - ); - }); - egui::SidePanel::left("value_out") - .exact_width(250.) - .resizable(false) - .show_inside(ui, |ui| { - ui.heading("Value Out"); - ui.separator(); - ui.monospace(format!( - "Total: {}", - bitcoin::Amount::from_sat(value_out) - )); - ui.separator(); - egui::Grid::new("outputs").striped(true).show( - ui, - |ui| { - let mut remove = None; - ui.monospace("vout"); - ui.monospace("address"); - ui.monospace("value"); - ui.end_row(); - let tx_read = - self.app.transaction.upgradable_read(); - for (vout, output) in - tx_read.outputs.iter().enumerate() - { - let address = - &format!("{}", output.address) - [0..8]; - let value = bitcoin::Amount::from_sat( - output.get_value(), - ); - ui.monospace(format!("{vout}")); - ui.monospace(address.to_string()); - ui.with_layout( - egui::Layout::right_to_left( - egui::Align::Max, - ), - |ui| { - ui.monospace(format!( - "{value}" - )); - }, - ); - if ui.button("remove").clicked() { - remove = Some(vout); - } - ui.end_row(); - } - if let Some(vout) = remove { - let mut tx_write = - RwLockUpgradableReadGuard::upgrade( - tx_read, - ); - tx_write.outputs.remove(vout); - if let Err(err) = self - .app - .node - .regenerate_proof(&mut tx_write) - { - tracing::error!("{err}") - } - } - }, - ); - }); - egui::SidePanel::left("create_utxo") - .exact_width(450.) - .resizable(false) - .show_separator_line(false) - .show_inside(ui, |ui| { - self.utxo_creator.show(&mut self.app.clone(), ui); - ui.separator(); - ui.heading("Transaction"); - let txid = &format!( - "{}", - self.app.transaction.read().txid() - )[0..8]; - ui.monospace(format!("txid: {txid}")); - if value_in >= value_out { - let fee = value_in - value_out; - let fee = bitcoin::Amount::from_sat(fee); - ui.monospace(format!("fee: {fee}")); - if ui.button("sign and send").clicked() { - self.app.sign_and_send().unwrap_or(()); - } - } else { - ui.label("Not Enough Value In"); - } - }); + Tab::ParentChain => self.parent_chain.show(&mut self.app, ui), + Tab::Coins => { + self.coins.show(&mut self.app, ui); } Tab::MemPoolExplorer => { self.mempool_explorer.show(&mut self.app, ui); @@ -320,7 +141,6 @@ impl eframe::App for EguiApp { Tab::Withdrawals => { self.withdrawals.show(&mut self.app, ui); } - Tab::ParentChain => self.parent_chain.show(&mut self.app, ui), Tab::Logs => { self.logs.show(ui); } diff --git a/app/gui/parent_chain.rs b/app/gui/parent_chain/info.rs similarity index 87% rename from app/gui/parent_chain.rs rename to app/gui/parent_chain/info.rs index eb8e13d..1485648 100644 --- a/app/gui/parent_chain.rs +++ b/app/gui/parent_chain/info.rs @@ -5,15 +5,15 @@ use futures::TryFutureExt; use crate::{app::App, gui::util::UiExt}; #[derive(Clone, Debug)] -struct ParentChainInfo { +struct Inner { mainchain_tip: bip300301::client::Block, sidechain_wealth: bitcoin::Amount, } -pub struct ParentChain(anyhow::Result); +pub(super) struct Info(anyhow::Result); -impl ParentChain { - fn get_parent_chain_info(app: &App) -> anyhow::Result { +impl Info { + fn get_parent_chain_info(app: &App) -> anyhow::Result { let dc = app.node.drivechain(); let mainchain_tip = app.runtime.block_on(async { let mainchain_tip_blockhash = dc.get_mainchain_tip().await?; @@ -23,16 +23,16 @@ impl ParentChain { .await })?; let sidechain_wealth = app.node.get_sidechain_wealth()?; - Ok(ParentChainInfo { + Ok(Inner { mainchain_tip, sidechain_wealth, }) } pub fn new(app: &App) -> Self { - let parent_chain_info = Self::get_parent_chain_info(app) + let inner = Self::get_parent_chain_info(app) .inspect_err(|err| tracing::error!("{err:#}")); - Self(parent_chain_info) + Self(inner) } fn refresh_parent_chain_info(&mut self, app: &App) { diff --git a/app/gui/parent_chain/mod.rs b/app/gui/parent_chain/mod.rs new file mode 100644 index 0000000..2adc3b3 --- /dev/null +++ b/app/gui/parent_chain/mod.rs @@ -0,0 +1,55 @@ +use eframe::egui; +use strum::{EnumIter, IntoEnumIterator}; + +use crate::app::App; + +mod info; +mod transfer; + +use info::Info; +use transfer::Transfer; + +#[derive(Default, EnumIter, Eq, PartialEq, strum::Display)] +enum Tab { + #[default] + #[strum(to_string = "Transfer")] + Transfer, + #[strum(to_string = "Info")] + Info, +} + +pub struct ParentChain { + info: Info, + tab: Tab, + transfer: Transfer, +} + +impl ParentChain { + pub fn new(app: &App) -> Self { + let info = Info::new(app); + Self { + info, + tab: Tab::default(), + transfer: Transfer::default(), + } + } + + pub fn show(&mut self, app: &mut App, ui: &mut egui::Ui) { + egui::TopBottomPanel::top("parent_chain_tabs").show(ui.ctx(), |ui| { + ui.horizontal(|ui| { + Tab::iter().for_each(|tab_variant| { + let tab_name = tab_variant.to_string(); + ui.selectable_value(&mut self.tab, tab_variant, tab_name); + }) + }); + }); + egui::CentralPanel::default().show(ui.ctx(), |ui| match self.tab { + Tab::Transfer => { + self.transfer.show(app, ui); + } + Tab::Info => { + self.info.show(app, ui); + } + }); + } +} diff --git a/app/gui/parent_chain/transfer.rs b/app/gui/parent_chain/transfer.rs new file mode 100644 index 0000000..0f1ec61 --- /dev/null +++ b/app/gui/parent_chain/transfer.rs @@ -0,0 +1,209 @@ +use bip300301::bitcoin; +use eframe::egui; + +use crate::app::App; + +#[derive(Debug, Default)] +pub struct Deposit { + amount: String, + fee: String, +} + +impl Deposit { + pub fn show(&mut self, app: &mut App, ui: &mut egui::Ui) { + ui.add_sized((110., 10.), |ui: &mut egui::Ui| { + ui.horizontal(|ui| { + let amount_edit = egui::TextEdit::singleline(&mut self.amount) + .hint_text("amount") + .desired_width(80.); + ui.add(amount_edit); + ui.label("BTC"); + }) + .response + }); + ui.add_sized((110., 10.), |ui: &mut egui::Ui| { + ui.horizontal(|ui| { + let fee_edit = egui::TextEdit::singleline(&mut self.fee) + .hint_text("fee") + .desired_width(80.); + ui.add(fee_edit); + ui.label("BTC"); + }) + .response + }); + let amount = bitcoin::Amount::from_str_in( + &self.amount, + bitcoin::Denomination::Bitcoin, + ); + let fee = bitcoin::Amount::from_str_in( + &self.fee, + bitcoin::Denomination::Bitcoin, + ); + + if ui + .add_enabled( + amount.is_ok() && fee.is_ok(), + egui::Button::new("deposit"), + ) + .clicked() + { + if let Err(err) = app.deposit( + amount.expect("should not happen"), + fee.expect("should not happen"), + ) { + tracing::error!("{err}"); + } else { + *self = Self::default(); + } + } + } +} + +#[derive(Debug, Default)] +pub struct Withdrawal { + mainchain_address: String, + amount: String, + fee: String, + mainchain_fee: String, +} + +fn create_withdrawal( + app: &App, + mainchain_address: bitcoin::Address, + amount: bitcoin::Amount, + fee: bitcoin::Amount, + mainchain_fee: bitcoin::Amount, +) -> anyhow::Result<()> { + let accumulator = app.node.get_accumulator()?; + let tx = app.wallet.create_withdrawal( + &accumulator, + mainchain_address, + amount.to_sat(), + mainchain_fee.to_sat(), + fee.to_sat(), + )?; + app.sign_and_send(tx)?; + Ok(()) +} + +impl Withdrawal { + pub fn show(&mut self, app: &mut App, ui: &mut egui::Ui) { + ui.add_sized((250., 10.), |ui: &mut egui::Ui| { + ui.horizontal(|ui| { + let mainchain_address_edit = + egui::TextEdit::singleline(&mut self.mainchain_address) + .hint_text("mainchain address") + .desired_width(150.); + ui.add(mainchain_address_edit); + ui.label("BTC"); + if ui.button("generate").clicked() { + match app.get_new_main_address() { + Ok(main_address) => { + self.mainchain_address = main_address.to_string(); + } + Err(err) => { + let err = anyhow::Error::new(err); + tracing::error!("{err:#}") + } + }; + } + }) + .response + }); + ui.add_sized((110., 10.), |ui: &mut egui::Ui| { + ui.horizontal(|ui| { + let amount_edit = egui::TextEdit::singleline(&mut self.amount) + .hint_text("amount") + .desired_width(80.); + ui.add(amount_edit); + ui.label("BTC"); + }) + .response + }); + ui.add_sized((110., 10.), |ui: &mut egui::Ui| { + ui.horizontal(|ui| { + let fee_edit = egui::TextEdit::singleline(&mut self.fee) + .hint_text("fee") + .desired_width(80.); + ui.add(fee_edit); + ui.label("BTC"); + }) + .response + }); + ui.add_sized((110., 10.), |ui: &mut egui::Ui| { + ui.horizontal(|ui| { + let fee_edit = + egui::TextEdit::singleline(&mut self.mainchain_fee) + .hint_text("mainchain fee") + .desired_width(80.); + ui.add(fee_edit); + ui.label("BTC"); + }) + .response + }); + let mainchain_address: Option< + bitcoin::Address, + > = self.mainchain_address.parse().ok(); + let amount = bitcoin::Amount::from_str_in( + &self.amount, + bitcoin::Denomination::Bitcoin, + ); + let fee = bitcoin::Amount::from_str_in( + &self.fee, + bitcoin::Denomination::Bitcoin, + ); + let mainchain_fee = bitcoin::Amount::from_str_in( + &self.mainchain_fee, + bitcoin::Denomination::Bitcoin, + ); + + if ui + .add_enabled( + mainchain_address.is_some() + && amount.is_ok() + && fee.is_ok() + && mainchain_fee.is_ok(), + egui::Button::new("withdraw"), + ) + .clicked() + { + if let Err(err) = create_withdrawal( + app, + mainchain_address.expect("should not happen"), + amount.expect("should not happen"), + fee.expect("should not happen"), + mainchain_fee.expect("should not happen"), + ) { + tracing::error!("{err:#}"); + } else { + *self = Self::default(); + } + } + } +} + +#[derive(Default)] +pub(super) struct Transfer { + deposit: Deposit, + withdrawal: Withdrawal, +} + +impl Transfer { + pub fn show(&mut self, app: &mut App, ui: &mut egui::Ui) { + egui::SidePanel::left("deposit") + .exact_width(ui.available_width() / 2.) + .resizable(false) + .show_inside(ui, |ui| { + ui.vertical_centered(|ui| { + ui.heading("Deposit"); + self.deposit.show(app, ui); + }) + }); + egui::CentralPanel::default().show_inside(ui, |ui| { + ui.vertical_centered(|ui| { + ui.heading("Withdrawal"); + self.withdrawal.show(app, ui); + }) + }); + } +} diff --git a/app/gui/util.rs b/app/gui/util.rs index 75890bb..ccc0db8 100644 --- a/app/gui/util.rs +++ b/app/gui/util.rs @@ -1,6 +1,27 @@ use std::borrow::Borrow; -use eframe::egui::{self, Response, Ui}; +use eframe::egui::{self, InnerResponse, Response, Ui}; + +// extension for InnerResponse and InnerResponse> +pub trait InnerResponseExt { + #[allow(dead_code)] + fn join(self) -> Response; +} + +impl InnerResponseExt for InnerResponse { + fn join(self) -> Response { + self.response | self.inner + } +} + +impl InnerResponseExt for InnerResponse> { + fn join(self) -> Response { + match self.inner { + Some(inner) => self.response | inner, + None => self.response, + } + } +} /// extension trait for egui::Ui pub trait UiExt { diff --git a/app/main.rs b/app/main.rs index a291ccc..0e96987 100644 --- a/app/main.rs +++ b/app/main.rs @@ -47,7 +47,6 @@ fn main() -> anyhow::Result<()> { let app = app.clone(); async move { rpc_server::run_server(app, config.rpc_addr).await.unwrap() } }); - if config.headless { // wait for ctrlc signal let (tx, rx) = mpsc::channel(); diff --git a/app/rpc_api.rs b/app/rpc_api.rs index dafa13a..a35270b 100644 --- a/app/rpc_api.rs +++ b/app/rpc_api.rs @@ -4,7 +4,7 @@ use std::net::SocketAddr; use bip300301::bitcoin; use jsonrpsee::{core::RpcResult, proc_macros::rpc}; -use thunder::types::Address; +use thunder::types::{Address, Txid}; #[rpc(client, server)] pub trait Rpc { @@ -42,4 +42,13 @@ pub trait Rpc { #[method(name = "stop")] async fn stop(&self); + + #[method(name = "withdraw")] + async fn withdraw( + &self, + mainchain_address: bitcoin::Address, + amount_sats: u64, + fee_sats: u64, + mainchain_fee_sats: u64, + ) -> RpcResult; } diff --git a/app/rpc_server.rs b/app/rpc_server.rs index 503e2f7..ab102ac 100644 --- a/app/rpc_server.rs +++ b/app/rpc_server.rs @@ -6,7 +6,11 @@ use jsonrpsee::{ server::Server, types::ErrorObject, }; -use thunder::{node, types::Address, wallet}; +use thunder::{ + node, + types::{Address, Txid}, + wallet, +}; use thunder_app_rpc_api::RpcServer; use crate::app::{self, App}; @@ -114,6 +118,31 @@ impl RpcServer for RpcServerImpl { async fn stop(&self) { std::process::exit(0); } + + async fn withdraw( + &self, + mainchain_address: bitcoin::Address, + amount_sats: u64, + fee_sats: u64, + mainchain_fee_sats: u64, + ) -> RpcResult { + let accumulator = + self.app.node.get_accumulator().map_err(convert_node_err)?; + let tx = self + .app + .wallet + .create_withdrawal( + &accumulator, + mainchain_address, + amount_sats, + mainchain_fee_sats, + fee_sats, + ) + .map_err(convert_wallet_err)?; + let txid = tx.txid(); + self.app.sign_and_send(tx).map_err(convert_app_err)?; + Ok(txid) + } } pub async fn run_server( diff --git a/lib/types/transaction.rs b/lib/types/transaction.rs index c37596d..b65a0eb 100644 --- a/lib/types/transaction.rs +++ b/lib/types/transaction.rs @@ -156,7 +156,7 @@ impl From<&PointedOutput> for NodeHash { } } -#[derive(BorshSerialize, Clone, Debug, Deserialize, Serialize)] +#[derive(BorshSerialize, Clone, Debug, Default, Deserialize, Serialize)] pub struct Transaction { pub inputs: Vec<(OutPoint, Hash)>, /// Utreexo proof for inputs