diff --git a/app/Cargo.toml b/app/Cargo.toml index ea3c5623..7f0f7ca9 100644 --- a/app/Cargo.toml +++ b/app/Cargo.toml @@ -36,7 +36,7 @@ tonic = { workspace = true } tonic-health = { workspace = true } # needs to line up with jsonrpsee tower version... tower = { workspace = true } -tower-http = { workspace = true, features = ["request-id", "trace"] } +tower-http = { workspace = true, features = ["cors", "request-id", "trace"] } tracing = { workspace = true } tracing-appender = { workspace = true } tracing-subscriber = { workspace = true, features = ["env-filter", "json"] } diff --git a/app/app.rs b/app/app.rs index f42ceb0c..70bdeaed 100644 --- a/app/app.rs +++ b/app/app.rs @@ -287,30 +287,29 @@ impl App { Ok(()) } - pub fn get_new_main_address( + pub async fn get_new_main_address_async( &self, ) -> Result, Error> { let Some(miner) = self.miner.as_ref() else { return Err(Error::NoCusfMainchainWalletClient); }; - let address = self.runtime.block_on({ - let miner = miner.clone(); - async move { - let mut miner_write = miner.write().await; - let cusf_mainchain = &mut miner_write.cusf_mainchain; - let mainchain_info = cusf_mainchain.get_chain_info().await?; - let cusf_mainchain_wallet = - &mut miner_write.cusf_mainchain_wallet; - let res = cusf_mainchain_wallet - .create_new_address() - .await? - .require_network(mainchain_info.network) - .unwrap(); - drop(miner_write); - Result::<_, Error>::Ok(res) - } - })?; - Ok(address) + let mut miner_write = miner.write().await; + let cusf_mainchain = &mut miner_write.cusf_mainchain; + let mainchain_info = cusf_mainchain.get_chain_info().await?; + let cusf_mainchain_wallet = &mut miner_write.cusf_mainchain_wallet; + let res = cusf_mainchain_wallet + .create_new_address() + .await? + .require_network(mainchain_info.network) + .unwrap(); + drop(miner_write); + Ok(res) + } + + pub fn get_new_main_address( + &self, + ) -> Result, Error> { + self.runtime.block_on(self.get_new_main_address_async()) } const EMPTY_BLOCK_BMM_BRIBE: bitcoin::Amount = @@ -531,7 +530,7 @@ impl App { Ok(()) } - pub fn deposit( + pub async fn deposit_async( &self, address: Address, amount: bitcoin::Amount, @@ -540,15 +539,23 @@ impl App { let Some(miner) = self.miner.as_ref() else { return Err(Error::NoCusfMainchainWalletClient); }; - self.runtime.block_on(async { - let mut miner_write = miner.write().await; - let txid = miner_write - .cusf_mainchain_wallet - .create_deposit_tx(address, amount.to_sat(), fee.to_sat()) - .await?; - drop(miner_write); - Ok(txid) - }) + let mut miner_write = miner.write().await; + let txid = miner_write + .cusf_mainchain_wallet + .create_deposit_tx(address, amount.to_sat(), fee.to_sat()) + .await?; + drop(miner_write); + Ok(txid) + } + + pub fn deposit( + &self, + address: Address, + amount: bitcoin::Amount, + fee: bitcoin::Amount, + ) -> Result { + self.runtime + .block_on(self.deposit_async(address, amount, fee)) } } diff --git a/app/gui/coins/transfer_receive.rs b/app/gui/coins/transfer_receive.rs index ddd92f66..c483ce84 100644 --- a/app/gui/coins/transfer_receive.rs +++ b/app/gui/coins/transfer_receive.rs @@ -100,7 +100,7 @@ impl Receive { }; let address = app .wallet - .get_new_address() + .get_address_or_new() .map_err(anyhow::Error::from) .inspect_err(|err| tracing::error!("{err:#}")); Self { @@ -122,7 +122,13 @@ impl Receive { .add_enabled(app.is_some(), Button::new("generate")) .clicked() { - *self = Self::new(app) + let address = app + .unwrap() + .wallet + .get_new_address() + .map_err(anyhow::Error::from) + .inspect_err(|err| tracing::error!("{err:#}")); + self.address = Some(address); } } } diff --git a/app/gui/coins/tx_builder.rs b/app/gui/coins/tx_builder.rs index 68a824b3..6ef4975c 100644 --- a/app/gui/coins/tx_builder.rs +++ b/app/gui/coins/tx_builder.rs @@ -48,7 +48,7 @@ impl TxBuilder { ui.separator(); ui.monospace(format!("Total: {value_in}")); ui.separator(); - egui::Grid::new("utxos").striped(true).show(ui, |ui| { + egui::Grid::new("spent_utxos").striped(true).show(ui, |ui| { ui.monospace("kind"); ui.monospace("outpoint"); ui.monospace("value"); @@ -111,33 +111,29 @@ impl TxBuilder { ui: &mut egui::Ui, ) -> anyhow::Result<()> { egui::ScrollArea::horizontal().show(ui, |ui| { - egui::SidePanel::left("spend_utxo") - .exact_width(250.) - .resizable(false) - .show_inside(ui, |ui| { + ui.horizontal(|ui| { + ui.vertical(|ui| { + ui.set_width(250.0); self.utxo_selector.show(app, ui, &mut self.base_tx); }); - egui::SidePanel::left("value_in") - .exact_width(250.) - .resizable(false) - .show_inside(ui, |ui| { + ui.separator(); + ui.vertical(|ui| { + ui.set_width(250.0); let () = self.show_value_in(app, ui); }); - egui::SidePanel::left("value_out") - .exact_width(250.) - .resizable(false) - .show_inside(ui, |ui| { + ui.separator(); + ui.vertical(|ui| { + ui.set_width(250.0); let () = self.show_value_out(ui); }); - egui::SidePanel::left("create_utxo") - .exact_width(450.) - .resizable(false) - .show_separator_line(false) - .show_inside(ui, |ui| { + ui.separator(); + ui.vertical(|ui| { + ui.set_width(450.0); 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/mempool_explorer.rs b/app/gui/mempool_explorer.rs index 50444fad..50c75d86 100644 --- a/app/gui/mempool_explorer.rs +++ b/app/gui/mempool_explorer.rs @@ -17,165 +17,198 @@ impl MemPoolExplorer { let utxos = app .and_then(|app| app.wallet.get_utxos().ok()) .unwrap_or_default(); - egui::SidePanel::left("transaction_picker") - .resizable(false) - .show_inside(ui, |ui| { - ui.heading("Transactions"); - ui.separator(); - egui::Grid::new("transactions") - .striped(true) - .show(ui, |ui| { - ui.monospace("txid"); - ui.monospace("value out"); - ui.monospace("fee"); - ui.end_row(); - for (index, transaction) in - transactions.iter().enumerate() - { - let value_out: bitcoin::Amount = transaction - .transaction - .outputs - .iter() - .map(GetValue::get_value) - .sum(); - let value_in: bitcoin::Amount = transaction - .transaction - .inputs - .iter() - .map(|(outpoint, _)| { - utxos.get(outpoint).map(GetValue::get_value) - }) - .sum::>() - .unwrap_or(bitcoin::Amount::ZERO); - let txid = - &format!("{}", transaction.transaction.txid()) - [0..8]; - if value_in >= value_out { - let fee = value_in - value_out; - ui.selectable_value( - &mut self.current, - index, - txid.to_string(), - ); - ui.with_layout( - egui::Layout::right_to_left( - egui::Align::Max, - ), - |ui| { - ui.monospace(format!("{value_out}")); - }, - ); - ui.with_layout( - egui::Layout::right_to_left( - egui::Align::Max, - ), - |ui| { - ui.monospace(format!("{fee}")); - }, - ); - ui.end_row(); - } else { - ui.selectable_value( - &mut self.current, - index, - txid.to_string(), - ); - ui.monospace("invalid"); - ui.end_row(); + egui::ScrollArea::horizontal().show(ui, |ui| { + ui.horizontal(|ui| { + ui.vertical(|ui| { + ui.set_width(250.0); + ui.heading("Transactions"); + ui.separator(); + egui::Grid::new("transactions").striped(true).show( + ui, + |ui| { + ui.monospace("txid"); + ui.monospace("value out"); + ui.monospace("fee"); + ui.end_row(); + for (index, transaction) in + transactions.iter().enumerate() + { + let value_out: bitcoin::Amount = transaction + .transaction + .outputs + .iter() + .map(GetValue::get_value) + .sum(); + let value_in: bitcoin::Amount = transaction + .transaction + .inputs + .iter() + .map(|(outpoint, _)| { + utxos + .get(outpoint) + .map(GetValue::get_value) + }) + .sum::>() + .unwrap_or(bitcoin::Amount::ZERO); + let txid = &format!( + "{}", + transaction.transaction.txid() + )[0..8]; + if value_in >= value_out { + let fee = value_in - value_out; + ui.selectable_value( + &mut self.current, + index, + txid.to_string(), + ); + ui.with_layout( + egui::Layout::right_to_left( + egui::Align::Max, + ), + |ui| { + ui.monospace(format!( + "{value_out}" + )); + }, + ); + ui.with_layout( + egui::Layout::right_to_left( + egui::Align::Max, + ), + |ui| { + ui.monospace(format!("{fee}")); + }, + ); + ui.end_row(); + } else { + ui.selectable_value( + &mut self.current, + index, + txid.to_string(), + ); + ui.monospace("invalid"); + ui.end_row(); + } } - } + }, + ); + }); + if let Some(transaction) = transactions.get(self.current) { + ui.separator(); + ui.vertical(|ui| { + ui.set_width(250.0); + ui.heading("Inputs"); + ui.separator(); + egui::Grid::new("inputs").striped(true).show( + ui, + |ui| { + ui.monospace("kind"); + ui.monospace("outpoint"); + ui.monospace("value"); + ui.end_row(); + for (outpoint, _) in + &transaction.transaction.inputs + { + let (kind, hash, vout) = match outpoint { + OutPoint::Regular { txid, vout } => ( + "regular", + format!("{txid}"), + *vout, + ), + OutPoint::Deposit(outpoint) => ( + "deposit", + format!("{}", outpoint.txid), + outpoint.vout, + ), + OutPoint::Coinbase { + merkle_root, + vout, + } => ( + "coinbase", + format!("{merkle_root}"), + *vout, + ), + }; + let output = &utxos[outpoint]; + let hash = &hash[0..8]; + let value = output.get_value(); + ui.monospace(kind.to_string()); + ui.monospace(format!("{hash}:{vout}",)); + ui.monospace(format!("{value}",)); + ui.end_row(); + } + }, + ); }); - }); - if let Some(transaction) = transactions.get(self.current) { - egui::SidePanel::left("inputs") - .resizable(false) - .show_inside(ui, |ui| { - ui.heading("Inputs"); ui.separator(); - egui::Grid::new("inputs").striped(true).show(ui, |ui| { - ui.monospace("kind"); - ui.monospace("outpoint"); - ui.monospace("value"); - ui.end_row(); - for (outpoint, _) in &transaction.transaction.inputs { - let (kind, hash, vout) = match outpoint { - OutPoint::Regular { txid, vout } => { - ("regular", format!("{txid}"), *vout) + ui.vertical(|ui| { + ui.set_width(250.0); + ui.heading("Outputs"); + ui.separator(); + egui::Grid::new("outputs").striped(true).show( + ui, + |ui| { + ui.monospace("vout"); + ui.monospace("address"); + ui.monospace("value"); + ui.end_row(); + for (vout, output) in transaction + .transaction + .outputs + .iter() + .enumerate() + { + let address = + &format!("{}", output.address)[0..8]; + let value = output.get_value(); + ui.monospace(format!("{vout}")); + ui.monospace(address.to_string()); + ui.monospace(format!("{value}")); + ui.end_row(); } - OutPoint::Deposit(outpoint) => ( - "deposit", - format!("{}", outpoint.txid), - outpoint.vout, - ), - OutPoint::Coinbase { merkle_root, vout } => ( - "coinbase", - format!("{merkle_root}"), - *vout, - ), - }; - let output = &utxos[outpoint]; - let hash = &hash[0..8]; - let value = output.get_value(); - ui.monospace(kind.to_string()); - ui.monospace(format!("{hash}:{vout}",)); - ui.monospace(format!("{value}",)); - ui.end_row(); - } + }, + ); }); - }); - egui::SidePanel::left("outputs") - .resizable(false) - .show_inside(ui, |ui| { - ui.heading("Outputs"); ui.separator(); - egui::Grid::new("inputs").striped(true).show(ui, |ui| { - ui.monospace("vout"); - ui.monospace("address"); - ui.monospace("value"); - ui.end_row(); - for (vout, output) in - transaction.transaction.outputs.iter().enumerate() + ui.vertical(|ui| { + ui.set_width(400.0); + ui.heading("Viewing"); + ui.separator(); + let txid = transaction.transaction.txid(); + ui.monospace(format!("Txid: {txid}")); + let transaction_size = bincode::serialize(&transaction) + .unwrap_or(vec![]) + .len(); + let transaction_size = if let Ok(transaction_size) = + SpecificSize::new(transaction_size as f64, Byte) { - let address = &format!("{}", output.address)[0..8]; - let value = output.get_value(); - ui.monospace(format!("{vout}")); - ui.monospace(address.to_string()); - ui.monospace(format!("{value}")); - ui.end_row(); - } + let bytes = transaction_size.to_bytes(); + if bytes < 1024 { + format!("{transaction_size}") + } else if bytes < 1024 * 1024 { + let transaction_size: SpecificSize = + transaction_size.into(); + format!("{transaction_size}") + } else { + let transaction_size: SpecificSize = + transaction_size.into(); + format!("{transaction_size}") + } + } else { + "".into() + }; + ui.monospace(format!( + "Transaction size: {transaction_size}" + )); }); - }); - egui::CentralPanel::default().show_inside(ui, |ui| { - ui.heading("Viewing"); - ui.separator(); - let txid = transaction.transaction.txid(); - ui.monospace(format!("Txid: {txid}")); - let transaction_size = - bincode::serialize(&transaction).unwrap_or(vec![]).len(); - let transaction_size = if let Ok(transaction_size) = - SpecificSize::new(transaction_size as f64, Byte) - { - let bytes = transaction_size.to_bytes(); - if bytes < 1024 { - format!("{transaction_size}") - } else if bytes < 1024 * 1024 { - let transaction_size: SpecificSize = - transaction_size.into(); - format!("{transaction_size}") - } else { - let transaction_size: SpecificSize = - transaction_size.into(); - format!("{transaction_size}") - } } else { - "".into() - }; - ui.monospace(format!("Transaction size: {transaction_size}")); - }); - } else { - egui::CentralPanel::default().show_inside(ui, |ui| { - ui.heading("No transactions in mempool"); + ui.separator(); + ui.vertical(|ui| { + ui.set_width(400.0); + ui.heading("No transactions in mempool"); + }); + } }); - } + }); } } diff --git a/app/gui/parent_chain/info.rs b/app/gui/parent_chain/info.rs index e794c77b..1ac07d92 100644 --- a/app/gui/parent_chain/info.rs +++ b/app/gui/parent_chain/info.rs @@ -10,44 +10,67 @@ struct Inner { sidechain_wealth: bitcoin::Amount, } -pub(super) struct Info(Option>); +pub(super) struct Info { + promise: Option>>, + last_value: Option>, +} impl Info { - fn get_parent_chain_info(app: &App) -> anyhow::Result { - let mainchain_tip_info = - app.runtime.block_on(app.node.with_cusf_mainchain( - |cusf_mainchain| cusf_mainchain.get_chain_tip().boxed(), - ))?; - let sidechain_wealth = app.node.get_sidechain_wealth()?; - Ok(Inner { - mainchain_tip_info, - sidechain_wealth, - }) - } - pub fn new(app: Option<&App>) -> Self { - let inner = app.map(|app| { - Self::get_parent_chain_info(app) - .inspect_err(|err| tracing::error!("{err:#}")) - }); - Self(inner) + let mut this = Self { + promise: None, + last_value: None, + }; + if let Some(app) = app { + this.refresh_parent_chain_info(app); + } + this } fn refresh_parent_chain_info(&mut self, app: &App) { - self.0 = Some( - Self::get_parent_chain_info(app) - .inspect_err(|err| tracing::error!("{err:#}")), - ); + let app = app.clone(); + self.promise = Some(poll_promise::Promise::spawn_async(async move { + let mainchain_tip_info = app + .node + .with_cusf_mainchain(|cusf_mainchain| { + cusf_mainchain.get_chain_tip().boxed() + }) + .await?; + let sidechain_wealth = app.node.get_sidechain_wealth()?; + Ok(Inner { + mainchain_tip_info, + sidechain_wealth, + }) + })); } pub fn show(&mut self, app: Option<&App>, ui: &mut egui::Ui) { - if ui - .add_enabled(app.is_some(), Button::new("Refresh")) - .clicked() - { - let () = self.refresh_parent_chain_info(app.unwrap()); + if let Some(result) = self.promise.as_ref().and_then(|p| p.ready()) { + self.last_value = Some(match result { + Ok(inner) => Ok(inner.clone()), + Err(err) => Err(anyhow::anyhow!("{err:#}")), + }); + self.promise = None; } - let parent_chain_info = match self.0.as_ref() { + + ui.horizontal(|ui| { + let is_refreshing = self.promise.is_some(); + if ui + .add_enabled( + app.is_some() && !is_refreshing, + Button::new("Refresh"), + ) + .clicked() + { + self.refresh_parent_chain_info(app.unwrap()); + } + if is_refreshing { + ui.spinner(); + ui.label("Refreshing parent chain info..."); + } + }); + + let parent_chain_info = match self.last_value.as_ref() { Some(Ok(parent_chain_info)) => parent_chain_info, Some(Err(err)) => { ui.monospace_selectable_multiline(format!("{err:#}")); diff --git a/app/gui/parent_chain/transfer.rs b/app/gui/parent_chain/transfer.rs index f64ee66b..0c6dcdc3 100644 --- a/app/gui/parent_chain/transfer.rs +++ b/app/gui/parent_chain/transfer.rs @@ -2,14 +2,55 @@ use eframe::egui::{self, Button}; use crate::app::App; -#[derive(Debug, Default)] +#[derive(Default)] pub struct Deposit { amount: String, fee: String, + promise: Option>>, +} + +impl std::fmt::Debug for Deposit { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Deposit") + .field("amount", &self.amount) + .field("fee", &self.fee) + .field("promise_active", &self.promise.is_some()) + .finish() + } } impl Deposit { pub fn show(&mut self, app: Option<&App>, ui: &mut egui::Ui) { + if let Some(promise) = &self.promise { + match promise.ready() { + None => { + ui.horizontal(|ui| { + ui.spinner(); + ui.label( + "Creating deposit transaction on parent chain...", + ); + }); + return; + } + Some(Ok(txid)) => { + tracing::info!("Deposit transaction created: {}", txid); + self.promise = None; + *self = Self::default(); + return; + } + Some(Err(err)) => { + ui.colored_label( + egui::Color32::RED, + format!("Error: {err}"), + ); + if ui.button("Dismiss").clicked() { + self.promise = None; + } + return; + } + } + } + ui.add_sized((110., 10.), |ui: &mut egui::Ui| { ui.horizontal(|ui| { let amount_edit = egui::TextEdit::singleline(&mut self.amount) @@ -46,26 +87,50 @@ impl Deposit { ) .clicked() { - let app = app.unwrap(); - if let Err(err) = app.deposit( - app.wallet.get_new_address().expect("should not happen"), - amount.expect("should not happen"), - fee.expect("should not happen"), - ) { - tracing::error!("{err}"); - } else { - *self = Self::default(); + let app = app.unwrap().clone(); + let amount = amount.expect("should not happen"); + let fee = fee.expect("should not happen"); + + match app.wallet.get_new_address() { + Ok(address) => { + self.promise = + Some(poll_promise::Promise::spawn_async(async move { + app.deposit_async(address, amount, fee) + .await + .map_err(|e| format!("{e:#}")) + })); + } + Err(err) => { + tracing::error!("Failed to get new address: {err}"); + } } } } } -#[derive(Debug, Default)] +#[derive(Default)] pub struct Withdrawal { mainchain_address: String, amount: String, fee: String, mainchain_fee: String, + generate_promise: Option< + poll_promise::Promise< + Result, String>, + >, + >, +} + +impl std::fmt::Debug for Withdrawal { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Withdrawal") + .field("mainchain_address", &self.mainchain_address) + .field("amount", &self.amount) + .field("fee", &self.fee) + .field("mainchain_fee", &self.mainchain_fee) + .field("generate_active", &self.generate_promise.is_some()) + .finish() + } } fn create_withdrawal( @@ -89,6 +154,22 @@ fn create_withdrawal( impl Withdrawal { pub fn show(&mut self, app: Option<&App>, ui: &mut egui::Ui) { + if let Some(promise) = &self.generate_promise { + match promise.ready() { + None => {} + Some(Ok(address)) => { + self.mainchain_address = address.to_string(); + self.generate_promise = None; + } + Some(Err(err)) => { + tracing::error!( + "Failed to generate mainchain address: {err}" + ); + self.generate_promise = None; + } + } + } + ui.add_sized((250., 10.), |ui: &mut egui::Ui| { ui.horizontal(|ui| { let mainchain_address_edit = @@ -96,19 +177,24 @@ impl Withdrawal { .hint_text("mainchain address") .desired_width(150.); ui.add(mainchain_address_edit); + + let is_generating = self.generate_promise.is_some(); + let generate_btn = if is_generating { + Button::new("generating...") + } else { + Button::new("generate") + }; if ui - .add_enabled(app.is_some(), Button::new("generate")) + .add_enabled(app.is_some() && !is_generating, generate_btn) .clicked() { - match app.unwrap().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:#}") - } - }; + let app = app.unwrap().clone(); + self.generate_promise = + Some(poll_promise::Promise::spawn_async(async move { + app.get_new_main_address_async() + .await + .map_err(|e| format!("{e:#}")) + })); } }) .response @@ -180,7 +266,16 @@ impl Withdrawal { ) { tracing::error!("{err:#}"); } else { + let is_generating = self.generate_promise.is_some(); *self = Self::default(); + if is_generating { + // Keep the generation promise active if it was running + // (though unlikely to happen during a withdrawal) + self.generate_promise = + Some(poll_promise::Promise::spawn_async(async move { + unreachable!() + })); + } } } } diff --git a/app/main.rs b/app/main.rs index 4b7ec87e..f0d238c7 100644 --- a/app/main.rs +++ b/app/main.rs @@ -179,7 +179,11 @@ fn run_egui_app( line_buffer: LineBuffer, app: Result, ) -> Result<(), eframe::Error> { - let native_options = eframe::NativeOptions::default(); + let native_options = eframe::NativeOptions { + viewport: eframe::egui::ViewportBuilder::default() + .with_inner_size(eframe::egui::vec2(1280.0, 720.0)), + ..Default::default() + }; let rpc_addr = url::Url::parse(&format!("http://{}", config.rpc_addr)) .expect("failed to parse rpc addr"); eframe::run_native( @@ -225,6 +229,17 @@ fn main() -> anyhow::Result<()> { }); if !config.headless { + let fallback_rt; + let _guard = match &app { + Ok(app) => Some(app.runtime.enter()), + Err(_) => { + fallback_rt = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .ok(); + fallback_rt.as_ref().map(|rt| rt.enter()) + } + }; // For GUI mode we want the GUI to start, even if the app fails to start. return run_egui_app(&config, line_buffer, app) .map_err(|e| anyhow::anyhow!("failed to run egui app: {e:#}")); diff --git a/app/rpc_server.rs b/app/rpc_server.rs index 8b69a127..aa8955d2 100644 --- a/app/rpc_server.rs +++ b/app/rpc_server.rs @@ -13,6 +13,7 @@ use thunder::{ }; use thunder_app_rpc_api::RpcServer; use tower_http::{ + cors::CorsLayer, request_id::{ MakeRequestId, PropagateRequestIdLayer, RequestId, SetRequestIdLayer, }, @@ -356,7 +357,9 @@ pub async fn run_server( ))) .into_inner(); - let http_middleware = tower::ServiceBuilder::new().layer(tracer); + let http_middleware = tower::ServiceBuilder::new() + .layer(tracer) + .layer(CorsLayer::permissive()); let rpc_middleware = RpcServiceBuilder::new().rpc_logger(1024); let server = Server::builder() diff --git a/lib/wallet.rs b/lib/wallet.rs index 027810b3..a53cf94a 100644 --- a/lib/wallet.rs +++ b/lib/wallet.rs @@ -510,6 +510,20 @@ impl Wallet { Ok(address) } + pub fn get_last_address(&self) -> Result, Error> { + let txn = self.env.read_txn().map_err(EnvError::from)?; + let last = self.index_to_address.last(&txn).map_err(DbError::from)?; + Ok(last.map(|(_, address)| address)) + } + + pub fn get_address_or_new(&self) -> Result { + if let Some(address) = self.get_last_address()? { + Ok(address) + } else { + self.get_new_address() + } + } + pub fn get_num_addresses(&self) -> Result { let txn = self.env.read_txn().map_err(EnvError::from)?; let (last_index, _) = self @@ -575,3 +589,63 @@ impl Watchable<()> for Wallet { }) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_get_address_or_new() -> Result<(), Error> { + let test_dir = std::env::temp_dir().join(format!( + "thunder_test_wallet_{}", + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_nanos() + )); + + // Ensure clean state + if test_dir.exists() { + let _unused = std::fs::remove_dir_all(&test_dir); + } + + let wallet = Wallet::new(&test_dir)?; + + // Seed must be set before we can generate addresses + assert!(!wallet.has_seed()?); + let seed = [1u8; 64]; + wallet.set_seed(&seed)?; + assert!(wallet.has_seed()?); + + // Get last address when none have been generated + let last = wallet.get_last_address()?; + assert!(last.is_none()); + + // Get address or new should generate the first address + let addr1 = wallet.get_address_or_new()?; + + // Now last address should be addr1 + let last = wallet.get_last_address()?; + assert_eq!(last, Some(addr1)); + + // Subsequent get_address_or_new calls should return the same addr1 + let addr2 = wallet.get_address_or_new()?; + assert_eq!(addr1, addr2); + + // Generating a new address explicitly should give a new one + let addr3 = wallet.get_new_address()?; + assert_ne!(addr1, addr3); + + // Now last address should be addr3 + let last = wallet.get_last_address()?; + assert_eq!(last, Some(addr3)); + + // And get_address_or_new should return addr3 + let addr4 = wallet.get_address_or_new()?; + assert_eq!(addr3, addr4); + + // Clean up + let _unused = std::fs::remove_dir_all(&test_dir); + Ok(()) + } +}