diff --git a/Cargo.lock b/Cargo.lock index fa43f5e..4cabb8b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2124,6 +2124,9 @@ name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +dependencies = [ + "serde", +] [[package]] name = "hex-conservative" @@ -4189,7 +4192,7 @@ dependencies = [ [[package]] name = "thunder" -version = "0.5.1" +version = "0.5.2" dependencies = [ "anyhow", "bincode", @@ -4219,7 +4222,7 @@ dependencies = [ [[package]] name = "thunder_app" -version = "0.5.1" +version = "0.5.2" dependencies = [ "anyhow", "base64 0.21.7", diff --git a/Cargo.toml b/Cargo.toml index 25147bb..0dbc1fb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ authors = [ "Nikita Chashchinskii " ] edition = "2021" -version = "0.5.1" +version = "0.5.2" [workspace.dependencies.bip300301] git = "https://github.com/Ash-L2L/bip300301.git" diff --git a/app/gui/coins/mod.rs b/app/gui/coins/mod.rs index 5d3308e..e29982d 100644 --- a/app/gui/coins/mod.rs +++ b/app/gui/coins/mod.rs @@ -3,27 +3,39 @@ use strum::{EnumIter, IntoEnumIterator}; use crate::app::App; +mod transfer_receive; mod tx_builder; mod tx_creator; mod utxo_creator; mod utxo_selector; +use transfer_receive::TransferReceive; use tx_builder::TxBuilder; #[derive(Default, EnumIter, Eq, PartialEq, strum::Display)] enum Tab { #[default] + #[strum(to_string = "Transfer & Receive")] + TransferReceive, #[strum(to_string = "Transaction Builder")] TransactionBuilder, } -#[derive(Default)] pub struct Coins { + transfer_receive: TransferReceive, tab: Tab, tx_builder: TxBuilder, } impl Coins { + pub fn new(app: &App) -> Self { + Self { + transfer_receive: TransferReceive::new(app), + tab: Tab::default(), + tx_builder: TxBuilder::default(), + } + } + pub fn show(&mut self, app: &mut App, ui: &mut egui::Ui) { egui::TopBottomPanel::top("coins_tabs").show(ui.ctx(), |ui| { ui.horizontal(|ui| { @@ -34,6 +46,9 @@ impl Coins { }); }); egui::CentralPanel::default().show(ui.ctx(), |ui| match self.tab { + Tab::TransferReceive => { + let () = self.transfer_receive.show(app, ui); + } Tab::TransactionBuilder => { let () = self.tx_builder.show(app, ui).unwrap(); } diff --git a/app/gui/coins/transfer_receive.rs b/app/gui/coins/transfer_receive.rs new file mode 100644 index 0000000..54fd628 --- /dev/null +++ b/app/gui/coins/transfer_receive.rs @@ -0,0 +1,153 @@ +use bip300301::bitcoin; +use eframe::egui; +use thunder::types::Address; + +use crate::{app::App, gui::util::UiExt}; + +#[derive(Debug, Default)] +struct Transfer { + dest: String, + amount: String, + fee: String, +} + +fn create_transfer( + app: &App, + dest: Address, + amount: bitcoin::Amount, + fee: bitcoin::Amount, +) -> anyhow::Result<()> { + let accumulator = app.node.get_accumulator()?; + let tx = app.wallet.create_transaction( + &accumulator, + dest, + amount.to_sat(), + fee.to_sat(), + )?; + app.sign_and_send(tx)?; + Ok(()) +} + +impl Transfer { + fn show(&mut self, app: &App, ui: &mut egui::Ui) { + ui.add_sized((250., 10.), |ui: &mut egui::Ui| { + ui.horizontal(|ui| { + let dest_edit = egui::TextEdit::singleline(&mut self.dest) + .hint_text("destination address") + .desired_width(150.); + ui.add(dest_edit); + }) + .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 + }); + let dest: Option
= self.dest.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, + ); + if ui + .add_enabled( + dest.is_some() && amount.is_ok() && fee.is_ok(), + egui::Button::new("transfer"), + ) + .clicked() + { + if let Err(err) = create_transfer( + app, + dest.expect("should not happen"), + amount.expect("should not happen"), + fee.expect("should not happen"), + ) { + tracing::error!("{err:#}"); + } else { + *self = Self::default(); + } + } + } +} + +#[derive(Debug)] +struct Receive { + address: anyhow::Result
, +} + +impl Receive { + fn new(app: &App) -> Self { + let address = app + .wallet + .get_new_address() + .map_err(anyhow::Error::from) + .inspect_err(|err| tracing::error!("{err:#}")); + Self { address } + } + + fn show(&mut self, app: &App, ui: &mut egui::Ui) { + match &self.address { + Ok(address) => { + ui.monospace_selectable_singleline(false, address.to_string()); + } + Err(err) => { + ui.monospace_selectable_multiline(format!("{err:#}")); + } + } + if ui.button("generate").clicked() { + *self = Self::new(app) + } + } +} + +#[derive(Debug)] +pub(super) struct TransferReceive { + transfer: Transfer, + receive: Receive, +} + +impl TransferReceive { + pub fn new(app: &App) -> Self { + Self { + transfer: Transfer::default(), + receive: Receive::new(app), + } + } + + pub fn show(&mut self, app: &mut App, ui: &mut egui::Ui) { + egui::SidePanel::left("transfer") + .exact_width(ui.available_width() / 2.) + .resizable(false) + .show_inside(ui, |ui| { + ui.vertical_centered(|ui| { + ui.heading("Transfer"); + self.transfer.show(app, ui); + }) + }); + egui::CentralPanel::default().show_inside(ui, |ui| { + ui.vertical_centered(|ui| { + ui.heading("Receive"); + self.receive.show(app, ui); + }) + }); + } +} diff --git a/app/gui/mod.rs b/app/gui/mod.rs index 398d5c2..54a6405 100644 --- a/app/gui/mod.rs +++ b/app/gui/mod.rs @@ -62,12 +62,13 @@ impl EguiApp { // Restore app state using cc.storage (requires the "persistence" feature). // Use the cc.gl (a glow::Context) to create graphics shaders and buffers that you can use // for e.g. egui::PaintCallback. + let coins = Coins::new(&app); let height = app.node.get_height().unwrap_or(0); let parent_chain = ParentChain::new(&app); Self { app, block_explorer: BlockExplorer::new(height), - coins: Coins::default(), + coins, logs: Logs::new(logs_capture), mempool_explorer: MemPoolExplorer::default(), miner: Miner::default(), diff --git a/app/gui/parent_chain/transfer.rs b/app/gui/parent_chain/transfer.rs index 0f1ec61..5deb29b 100644 --- a/app/gui/parent_chain/transfer.rs +++ b/app/gui/parent_chain/transfer.rs @@ -95,7 +95,6 @@ impl Withdrawal { .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) => { diff --git a/app/rpc_api.rs b/app/rpc_api.rs index a35270b..015946c 100644 --- a/app/rpc_api.rs +++ b/app/rpc_api.rs @@ -34,6 +34,10 @@ pub trait Rpc { #[method(name = "mine")] async fn mine(&self, fee: Option) -> RpcResult<()>; + /// Remove a tx from the mempool + #[method(name = "remove_from_mempool")] + async fn remove_from_mempool(&self, txid: Txid) -> RpcResult<()>; + #[method(name = "set_seed_from_mnemonic")] async fn set_seed_from_mnemonic(&self, mnemonic: String) -> RpcResult<()>; @@ -43,6 +47,14 @@ pub trait Rpc { #[method(name = "stop")] async fn stop(&self); + #[method(name = "transfer")] + async fn transfer( + &self, + dest: Address, + value_sats: u64, + fee_sats: u64, + ) -> RpcResult; + #[method(name = "withdraw")] async fn withdraw( &self, diff --git a/app/rpc_server.rs b/app/rpc_server.rs index ab102ac..dc43813 100644 --- a/app/rpc_server.rs +++ b/app/rpc_server.rs @@ -94,6 +94,13 @@ impl RpcServer for RpcServerImpl { self.app.mine(fee).await.map_err(convert_app_err) } + async fn remove_from_mempool(&self, txid: Txid) -> RpcResult<()> { + self.app + .node + .remove_from_mempool(txid) + .map_err(convert_node_err) + } + async fn set_seed_from_mnemonic(&self, mnemonic: String) -> RpcResult<()> { let mnemonic = bip39::Mnemonic::from_phrase(&mnemonic, bip39::Language::English) @@ -119,6 +126,24 @@ impl RpcServer for RpcServerImpl { std::process::exit(0); } + async fn transfer( + &self, + dest: Address, + value_sats: u64, + fee_sats: u64, + ) -> RpcResult { + let accumulator = + self.app.node.get_accumulator().map_err(convert_node_err)?; + let tx = self + .app + .wallet + .create_transaction(&accumulator, dest, value_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) + } + async fn withdraw( &self, mainchain_address: bitcoin::Address, diff --git a/lib/Cargo.toml b/lib/Cargo.toml index a4be1d9..6184717 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -16,7 +16,7 @@ bytes = "1.4.0" ed25519-dalek = { version = "2.1.1", features = ["batch", "serde"] } ed25519-dalek-bip32 = "0.3.0" heed = { git = "https://github.com/meilisearch/heed", tag = "v0.12.4", version = "0.12.4" } -hex = "0.4.3" +hex = { version = "0.4.3", features = ["serde"] } quinn = "0.10.1" rayon = "1.7.0" rcgen = "0.11.1" diff --git a/lib/mempool.rs b/lib/mempool.rs index cf15e56..c2c020c 100644 --- a/lib/mempool.rs +++ b/lib/mempool.rs @@ -1,5 +1,5 @@ use heed::{ - types::{OwnedType, SerdeBincode, Unit}, + types::{SerdeBincode, Unit}, Database, RoTxn, RwTxn, }; use rustreexo::accumulator::pollard::Pollard; @@ -19,7 +19,7 @@ pub enum Error { #[derive(Clone)] pub struct MemPool { pub transactions: - Database, SerdeBincode>, + Database, SerdeBincode>, pub spent_utxos: Database, Unit>, } @@ -52,14 +52,19 @@ impl MemPool { } self.transactions.put( txn, - &transaction.transaction.txid().into(), + &transaction.transaction.txid(), transaction, )?; Ok(()) } - pub fn delete(&self, txn: &mut RwTxn, txid: &Txid) -> Result<(), Error> { - self.transactions.delete(txn, txid.into())?; + pub fn delete(&self, rwtxn: &mut RwTxn, txid: &Txid) -> Result<(), Error> { + if let Some(tx) = self.transactions.get(rwtxn, txid)? { + for (outpoint, _) in &tx.transaction.inputs { + self.spent_utxos.delete(rwtxn, outpoint)?; + } + self.transactions.delete(rwtxn, txid)?; + } Ok(()) } diff --git a/lib/node.rs b/lib/node.rs index e6da055..d1977d4 100644 --- a/lib/node.rs +++ b/lib/node.rs @@ -275,6 +275,13 @@ impl Node { Ok(self.state.get_pending_withdrawal_bundle(&txn)?) } + pub fn remove_from_mempool(&self, txid: Txid) -> Result<(), Error> { + let mut rwtxn = self.env.write_txn()?; + let () = self.mempool.delete(&mut rwtxn, &txid)?; + rwtxn.commit()?; + Ok(()) + } + pub async fn submit_block( &self, header: &Header, diff --git a/lib/state.rs b/lib/state.rs index ac1ba67..52214d3 100644 --- a/lib/state.rs +++ b/lib/state.rs @@ -46,7 +46,7 @@ pub enum Error { NoUtxo { outpoint: OutPoint }, #[error("utreexo error: {0}")] Utreexo(String), - #[error("Utreexo proof verification failed")] + #[error("Utreexo proof verification failed for tx {txid}")] UtreexoProofFailed { txid: Txid }, #[error("Computed Utreexo roots do not match the header roots")] UtreexoRootsMismatch, diff --git a/lib/types/hashes.rs b/lib/types/hashes.rs index 3311f05..f10a3f2 100644 --- a/lib/types/hashes.rs +++ b/lib/types/hashes.rs @@ -3,6 +3,8 @@ use bitcoin::hashes::Hash as _; use borsh::BorshSerialize; use serde::{Deserialize, Serialize}; +use super::serde_hexstr_human_readable; + const BLAKE3_LENGTH: usize = 32; pub type Hash = [u8; BLAKE3_LENGTH]; @@ -105,7 +107,9 @@ impl std::fmt::Debug for MerkleRoot { Serialize, PartialEq, )] -pub struct Txid(pub Hash); +#[repr(transparent)] +#[serde(transparent)] +pub struct Txid(#[serde(with = "serde_hexstr_human_readable")] pub Hash); impl Txid { pub fn as_slice(&self) -> &[u8] { diff --git a/lib/types/mod.rs b/lib/types/mod.rs index 11ec2a1..6896a92 100644 --- a/lib/types/mod.rs +++ b/lib/types/mod.rs @@ -16,14 +16,37 @@ pub use transaction::{ SpentOutput, Transaction, Verify, }; -/* -// Replace () with a type (usually an enum) for output data specific for your sidechain. -pub type Output = types::Output<()>; -pub type Transaction = types::Transaction<()>; -pub type FilledTransaction = types::FilledTransaction<()>; -pub type AuthorizedTransaction = types::AuthorizedTransaction; -pub type Body = types::Body; -*/ +/// (de)serialize as hex strings for human-readable forms like json, +/// and default serialization for non human-readable formats like bincode +mod serde_hexstr_human_readable { + use hex::{FromHex, ToHex}; + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + + pub fn serialize(data: T, serializer: S) -> Result + where + S: Serializer, + T: Serialize + ToHex, + { + if serializer.is_human_readable() { + hex::serde::serialize(data, serializer) + } else { + data.serialize(serializer) + } + } + + pub fn deserialize<'de, D, T>(deserializer: D) -> Result + where + D: Deserializer<'de>, + T: Deserialize<'de> + FromHex, + ::Error: std::fmt::Display, + { + if deserializer.is_human_readable() { + hex::serde::deserialize(deserializer) + } else { + T::deserialize(deserializer) + } + } +} fn borsh_serialize_utreexo_nodehash( node_hash: &NodeHash,