diff --git a/liana-gui/src/app/message.rs b/liana-gui/src/app/message.rs index 6abcd912..b019b44b 100644 --- a/liana-gui/src/app/message.rs +++ b/liana-gui/src/app/message.rs @@ -26,6 +26,11 @@ pub enum Message { LoadWallet(Wallet), Info(Result), ReceiveAddress(Result<(Address, ChildNumber), Error>), + /// Revealed addresses. The second element contains the start index used for the request. + RevealedAddresses( + Result, + Option, // start_index + ), Coins(Result, Error>), /// When we want both coins and tip height together. CoinsTipHeight(Result, Error>, Result), diff --git a/liana-gui/src/app/state/receive.rs b/liana-gui/src/app/state/receive.rs index 9e6c77e7..2135f5eb 100644 --- a/liana-gui/src/app/state/receive.rs +++ b/liana-gui/src/app/state/receive.rs @@ -28,6 +28,8 @@ use crate::daemon::{ Daemon, }; +const PREV_ADDRESSES_PAGE_SIZE: usize = 20; + pub enum Modal { VerifyAddress(VerifyAddressModal), ShowQrCode(ShowQrCodeModal), @@ -41,6 +43,12 @@ pub struct Addresses { labels: HashMap, } +impl Addresses { + pub fn is_empty(&self) -> bool { + self.list.is_empty() && self.derivation_indexes.is_empty() && self.labels.is_empty() + } +} + impl Labelled for Addresses { fn labelled(&self) -> Vec { self.list @@ -57,9 +65,14 @@ pub struct ReceivePanel { data_dir: LianaDirectory, wallet: Arc, addresses: Addresses, + prev_addresses: Addresses, + prev_continue_from: Option, + show_prev_addresses: bool, + selected: HashSet
, labels_edited: LabelsEdited, modal: Modal, warning: Option, + processing: bool, } impl ReceivePanel { @@ -68,9 +81,34 @@ impl ReceivePanel { data_dir, wallet, addresses: Addresses::default(), + prev_addresses: Addresses::default(), + prev_continue_from: None, + show_prev_addresses: false, + selected: HashSet::new(), labels_edited: LabelsEdited::default(), modal: Modal::None, warning: None, + processing: false, + } + } + + pub fn address(&self, i: usize) -> Option<&Address> { + if i < self.addresses.list.len() { + self.addresses.list.get(i) + } else { + // i >= self.addresses.list.len() + self.prev_addresses.list.get(i - self.addresses.list.len()) + } + } + + pub fn derivation_index(&self, i: usize) -> Option<&ChildNumber> { + if i < self.addresses.list.len() { + self.addresses.derivation_indexes.get(i) + } else { + // i >= self.addresses.list.len() + self.prev_addresses + .derivation_indexes + .get(i - self.addresses.list.len()) } } } @@ -84,7 +122,13 @@ impl State for ReceivePanel { view::receive::receive( &self.addresses.list, &self.addresses.labels, + &self.prev_addresses.list, + &self.prev_addresses.labels, + self.show_prev_addresses, + &self.selected, self.labels_edited.cache(), + self.prev_continue_from.is_none(), + self.processing, ), ); @@ -118,7 +162,9 @@ impl State for ReceivePanel { match self.labels_edited.update( daemon, message, - std::iter::once(&mut self.addresses).map(|a| a as &mut dyn LabelsLoader), + std::iter::once(&mut self.addresses) + .chain(std::iter::once(&mut self.prev_addresses)) + .map(|a| a as &mut dyn LabelsLoader), ) { Ok(cmd) => cmd, Err(e) => { @@ -143,20 +189,20 @@ impl State for ReceivePanel { Task::none() } Message::View(view::Message::Select(i)) => { + let (address, index) = ( + self.address(i).expect("Must be present"), + self.derivation_index(i).expect("Must be present"), + ); self.modal = Modal::VerifyAddress(VerifyAddressModal::new( self.data_dir.clone(), self.wallet.clone(), cache.network, - self.addresses.list.get(i).expect("Must be present").clone(), - *self - .addresses - .derivation_indexes - .get(i) - .expect("Must be present"), + address.clone(), + *index, )); Task::none() } - Message::View(view::Message::Next) => { + Message::View(view::Message::NextReceiveAddress) => { let daemon = daemon.clone(); Task::perform( async move { @@ -169,11 +215,74 @@ impl State for ReceivePanel { Message::ReceiveAddress, ) } + Message::View(view::Message::ToggleShowPreviousAddresses) => { + self.show_prev_addresses = !self.show_prev_addresses; + Task::none() + } + Message::View(view::Message::SelectAddress(addr)) => { + if self.selected.contains(&addr) { + self.selected.remove(&addr); + } else { + self.selected.insert(addr); + } + Task::none() + } + Message::RevealedAddresses(res, start_index) => { + self.processing = false; + match res { + Ok(revealed) => { + self.warning = None; + // Make sure these results are for the expected continuation. + // The start index can only be None for the first request when there are no prev addresses saved. + if self.prev_continue_from == start_index + && (start_index.is_some() || self.prev_addresses.is_empty()) + { + for entry in revealed.addresses.iter() { + self.prev_addresses.list.push(entry.address.clone()); + self.prev_addresses.derivation_indexes.push(entry.index); + if let Some(label) = &entry.label { + self.prev_addresses.labels.insert( + LabelItem::from(entry.address.clone()).to_string(), + label.clone(), + ); + } + } + self.prev_continue_from = revealed.continue_from; + } + } + Err(e) => { + self.warning = Some(e); + } + }; + Task::none() + } + Message::View(view::Message::Next) => { + if self.prev_continue_from.is_some() { + self.processing = true; + let start_index = self.prev_continue_from; + Task::perform( + async move { + ( + daemon + .list_revealed_addresses( + false, + true, + PREV_ADDRESSES_PAGE_SIZE, + start_index, + ) + .await + .map_err(|e| e.into()), + start_index, + ) + }, + |(res, start_index)| Message::RevealedAddresses(res, start_index), + ) + } else { + Task::none() + } + } Message::View(view::Message::ShowQrCode(i)) => { - if let (Some(address), Some(index)) = ( - self.addresses.list.get(i), - self.addresses.derivation_indexes.get(i), - ) { + if let (Some(address), Some(index)) = (self.address(i), self.derivation_index(i)) { if let Some(modal) = ShowQrCodeModal::new(address, *index) { self.modal = Modal::ShowQrCode(modal); } @@ -192,12 +301,20 @@ impl State for ReceivePanel { fn reload( &mut self, - _daemon: Arc, + daemon: Arc, wallet: Arc, ) -> Task { - self.wallet = wallet; - self.addresses = Addresses::default(); - Task::none() + let data_dir = self.data_dir.clone(); + *self = Self::new(data_dir, wallet); + Task::perform( + async move { + daemon + .list_revealed_addresses(false, true, PREV_ADDRESSES_PAGE_SIZE, None) + .await + .map_err(|e| e.into()) + }, + |res| Message::RevealedAddresses(res, None), + ) } } @@ -348,13 +465,24 @@ mod tests { Address::from_str("tb1qkldgvljmjpxrjq2ev5qxe8dvhn0dph9q85pwtfkjeanmwdue2akqj4twxj") .unwrap() .assume_checked(); - let daemon = Daemon::new(vec![( - Some(json!({"method": "getnewaddress", "params": Option::::None})), - Ok(json!(GetAddressResult::new( - addr.clone(), - ChildNumber::from_normal_idx(0).unwrap() - ))), - )]); + let daemon = Daemon::new(vec![ + ( + Some( + json!({"method": "listrevealedaddresses", "params": [false, true, 20, Option::::None]}), + ), + Ok(json!(ListRevealedAddressesResult { + addresses: vec![], + continue_from: None, + })), + ), + ( + Some(json!({"method": "getnewaddress", "params": Option::::None})), + Ok(json!(GetAddressResult::new( + addr.clone(), + ChildNumber::from_normal_idx(0).unwrap() + ))), + ), + ]); let wallet = Arc::new(Wallet::new(LianaDescriptor::from_str(DESC).unwrap())); let sandbox: Sandbox = Sandbox::new(ReceivePanel::new( LianaDirectory::new(PathBuf::new()), @@ -364,7 +492,11 @@ mod tests { let cache = Cache::default(); let sandbox = sandbox.load(client.clone(), &cache, wallet).await; let sandbox = sandbox - .update(client, &cache, Message::View(viewMessage::Next)) + .update( + client, + &cache, + Message::View(viewMessage::NextReceiveAddress), + ) .await; let panel = sandbox.state(); diff --git a/liana-gui/src/app/view/message.rs b/liana-gui/src/app/view/message.rs index 543d8018..4d85450f 100644 --- a/liana-gui/src/app/view/message.rs +++ b/liana-gui/src/app/view/message.rs @@ -1,5 +1,5 @@ use crate::{app::menu::Menu, export::ImportExportMessage, node::bitcoind::RpcAuthType}; -use liana::miniscript::bitcoin::{bip32::Fingerprint, OutPoint}; +use liana::miniscript::bitcoin::{bip32::Fingerprint, Address, OutPoint}; pub trait Close { fn close() -> Self; @@ -14,6 +14,9 @@ pub enum Message { Select(usize), SelectPayment(OutPoint), Label(Vec, LabelMessage), + NextReceiveAddress, + ToggleShowPreviousAddresses, + SelectAddress(Address), Settings(SettingsMessage), CreateSpend(CreateSpendMessage), ImportSpend(ImportSpendMessage), diff --git a/liana-gui/src/app/view/receive.rs b/liana-gui/src/app/view/receive.rs index fd2cf077..8b76314a 100644 --- a/liana-gui/src/app/view/receive.rs +++ b/liana-gui/src/app/view/receive.rs @@ -1,9 +1,10 @@ use std::collections::{HashMap, HashSet}; use iced::{ + alignment::Horizontal, widget::{ qr_code::{self, QRCode}, - scrollable, Space, + scrollable, Button, Space, }, Alignment, Length, }; @@ -33,11 +34,75 @@ use crate::{ use super::message::Message; +fn address_card<'a>( + row_index: usize, + address: &'a bitcoin::Address, + labels: &'a HashMap, + labels_editing: &'a HashMap>, +) -> Container<'a, Message> { + let addr = address.to_string(); + card::simple( + Column::new() + .push(if let Some(label) = labels_editing.get(&addr) { + label::label_editing(vec![addr.clone()], label, text::P1_SIZE) + } else { + label::label_editable(vec![addr.clone()], labels.get(&addr), text::P1_SIZE) + }) + .push( + Row::new() + .push( + Container::new( + scrollable( + Column::new() + .push(Space::with_height(Length::Fixed(10.0))) + .push(p2_regular(address).small().style(theme::text::secondary)) + // Space between the address and the scrollbar + .push(Space::with_height(Length::Fixed(10.0))), + ) + .direction( + scrollable::Direction::Horizontal( + scrollable::Scrollbar::new().width(2).scroller_width(2), + ), + ), + ) + .width(Length::Fill), + ) + .push( + Button::new(icon::clipboard_icon().style(theme::text::secondary)) + .on_press(Message::Clipboard(addr)) + .style(theme::button::transparent_border), + ) + .align_y(Alignment::Center), + ) + .push( + Row::new() + .push( + button::secondary(None, "Verify on hardware device") + .on_press(Message::Select(row_index)), + ) + .push(Space::with_width(Length::Fill)) + .push( + button::secondary(None, "Show QR Code") + .on_press(Message::ShowQrCode(row_index)), + ), + ) + .spacing(10), + ) +} + +#[allow(clippy::too_many_arguments)] pub fn receive<'a>( addresses: &'a [bitcoin::Address], labels: &'a HashMap, + prev_addresses: &'a [bitcoin::Address], + prev_labels: &'a HashMap, + show_prev_addresses: bool, + selected: &'a HashSet, labels_editing: &'a HashMap>, + is_last_page: bool, + processing: bool, ) -> Element<'a, Message> { + let mut addresses_count = 0; // for counting number of new addresses generated Column::new() .push( Row::new() @@ -45,92 +110,146 @@ pub fn receive<'a>( .push(Container::new(h3("Receive")).width(Length::Fill)) .push( button::primary(Some(icon::plus_icon()), "Generate address") - .on_press(Message::Next), + .on_press(Message::NextReceiveAddress), ), ) - .push(p1_bold("New and never used reception addresses")) + .push(text("Always generate a new address for each deposit.")) .push( Row::new() .spacing(10) .push(addresses.iter().enumerate().rev().fold( + // iterate starting from most recently generated Column::new().spacing(10).width(Length::Fill), |col, (i, address)| { - let addr = address.to_string(); - col.push( - card::simple( - Column::new() - .push(if let Some(label) = labels_editing.get(&addr) { - label::label_editing( - vec![addr.clone()], - label, - text::P1_SIZE, - ) - } else { - label::label_editable( - vec![addr.clone()], - labels.get(&addr), - text::P1_SIZE, - ) - }) - .push( - Row::new() - .push( - Container::new( - scrollable( - Column::new() - .push(Space::with_height( - Length::Fixed(10.0), - )) - .push( - p2_regular(addr) - .small() - .style(theme::text::secondary), - ) - // Space between the address and the scrollbar - .push(Space::with_height( - Length::Fixed(10.0), - )), - ) - .direction(scrollable::Direction::Horizontal( - scrollable::Scrollbar::new() - .width(2) - .scroller_width(2), - )), - ) - .width(Length::Fill), - ) - .push( - Button::new( - icon::clipboard_icon() - .style(theme::text::secondary), - ) - .on_press(Message::Clipboard(address.to_string())) - .style(theme::button::transparent_border), - ) - .align_y(Alignment::Center), - ) - .push( - Row::new() - .push( - button::secondary( - None, - "Verify on hardware device", - ) - .on_press(Message::Select(i)), - ) - .push(Space::with_width(Length::Fill)) - .push( - button::secondary(None, "Show QR Code") - .on_press(Message::ShowQrCode(i)), - ), - ) - .spacing(10), - ) - .padding(20), - ) + addresses_count += 1; + col.push(address_card(i, address, labels, labels_editing)) }, )), ) + .push( + Container::new( + Button::new( + Row::new() + .align_y(Alignment::Center) + .push( + p1_bold("Previously generated addresses still awaiting deposit") + .width(Length::Fill), + ) + .push(if show_prev_addresses { + icon::collapsed_icon() + } else { + icon::collapse_icon() + }), + ) + .on_press(Message::ToggleShowPreviousAddresses) + .padding(20) + .width(Length::Fill) + .style(theme::button::transparent_border), + ) + .style(theme::card::simple), + ) + .push_maybe(show_prev_addresses.then_some(Row::new().spacing(10).push( + prev_addresses.iter().enumerate().fold( + // prev addresses are already ordered in descending order + Column::new().spacing(10).width(Length::Fill), + |col, (i, address)| { + col.push(if !selected.contains(address) { + Button::new( + Row::new() + .spacing(30) + .push( + Container::new( + scrollable( + Column::new() + .push(Space::with_height(Length::Fixed(10.0))) + .push( + p2_regular(address) + .small() + .style(theme::text::secondary), + ) + // Space between the address and the scrollbar + .push(Space::with_height(Length::Fixed(10.0))), + ) + .direction( + scrollable::Direction::Horizontal( + scrollable::Scrollbar::new() + .width(2) + .scroller_width(2), + ), + ), + ) + .padding(10) + .width(Length::FillPortion(13)), + ) + .push( + Container::new( + scrollable( + Column::new() + .push(Space::with_height(Length::Fixed(10.0))) + .push( + text( + prev_labels + .get(&address.to_string()) + .cloned() + .unwrap_or_default(), + ) + .small() + .style(theme::text::secondary), + ) + // Space between the label and the scrollbar + .push(Space::with_height(Length::Fixed(10.0))), + ) + .direction( + scrollable::Direction::Horizontal( + scrollable::Scrollbar::new() + .width(2) + .scroller_width(2), + ), + ), + ) + .padding(10) + .width(Length::FillPortion(7)), + ) + .align_y(Alignment::Center), + ) + .on_press(Message::SelectAddress(address.clone())) + .style(theme::button::secondary) + } else { + // Continue the row index from those of generated addresses above. + Button::new(address_card( + addresses_count + i, + address, + prev_labels, + labels_editing, + )) + .padding(0) // so that button & card borders match + .on_press(Message::SelectAddress(address.clone())) + .style(theme::button::transparent_border) + }) + }, + ), + ))) + .push_maybe( + (!is_last_page && show_prev_addresses).then_some( + Container::new( + Button::new( + text(if processing { + "Fetching ..." + } else { + "See more" + }) + .width(Length::Fill) + .align_x(Horizontal::Center), + ) + .width(Length::Fill) + .padding(15) + .style(theme::button::transparent_border) + .on_press_maybe((!processing).then_some(Message::Next)), + ) + .width(Length::Fill) + .style(theme::card::simple), + ), + ) .spacing(20) .into() } diff --git a/liana-gui/src/daemon/client/mod.rs b/liana-gui/src/daemon/client/mod.rs index 376aa3fa..6341477d 100644 --- a/liana-gui/src/daemon/client/mod.rs +++ b/liana-gui/src/daemon/client/mod.rs @@ -13,7 +13,9 @@ use tracing::{error, info}; pub mod error; pub mod jsonrpc; -use liana::miniscript::bitcoin::{address, psbt::Psbt, Address, Network, OutPoint, Txid}; +use liana::miniscript::bitcoin::{ + address, bip32::ChildNumber, psbt::Psbt, Address, Network, OutPoint, Txid, +}; use lianad::{ commands::{CoinStatus, CreateRecoveryResult, LabelItem}, config::Config, @@ -87,6 +89,24 @@ impl Daemon for Lianad { self.call("getnewaddress", Option::::None) } + async fn list_revealed_addresses( + &self, + is_change: bool, + exclude_used: bool, + limit: usize, + start_index: Option, + ) -> Result { + self.call( + "listrevealedaddresses", + Some(vec![ + json!(is_change), + json!(exclude_used), + json!(limit), + json!(start_index), // a `null` argument is parsed as `None` by the command + ]), + ) + } + async fn update_deriv_indexes( &self, receive: Option, diff --git a/liana-gui/src/daemon/embedded.rs b/liana-gui/src/daemon/embedded.rs index c86483f4..59213b2e 100644 --- a/liana-gui/src/daemon/embedded.rs +++ b/liana-gui/src/daemon/embedded.rs @@ -6,7 +6,9 @@ use tokio::sync::Mutex; use super::{model::*, node, Daemon, DaemonBackend, DaemonError}; use crate::dir::LianaDirectory; use async_trait::async_trait; -use liana::miniscript::bitcoin::{address, psbt::Psbt, Address, Network, OutPoint, Txid}; +use liana::miniscript::bitcoin::{ + address, bip32::ChildNumber, psbt::Psbt, Address, Network, OutPoint, Txid, +}; use lianad::{ commands::{CoinStatus, LabelItem}, config::Config, @@ -103,6 +105,21 @@ impl Daemon for EmbeddedDaemon { self.command(|daemon| Ok(daemon.get_new_address())).await } + async fn list_revealed_addresses( + &self, + is_change: bool, + exclude_used: bool, + limit: usize, + start_index: Option, + ) -> Result { + self.command(|daemon| { + daemon + .list_revealed_addresses(is_change, exclude_used, limit, start_index) + .map_err(|e| DaemonError::Unexpected(e.to_string())) + }) + .await + } + async fn update_deriv_indexes( &self, receive: Option, diff --git a/liana-gui/src/daemon/mod.rs b/liana-gui/src/daemon/mod.rs index 4afea243..98a3d410 100644 --- a/liana-gui/src/daemon/mod.rs +++ b/liana-gui/src/daemon/mod.rs @@ -11,7 +11,10 @@ use std::iter::FromIterator; use async_trait::async_trait; use liana::miniscript::bitcoin::{ - address, bip32::Fingerprint, psbt::Psbt, secp256k1, Address, Network, OutPoint, Txid, + address, + bip32::{ChildNumber, Fingerprint}, + psbt::Psbt, + secp256k1, Address, Network, OutPoint, Txid, }; use lianad::bip329::Labels; use lianad::commands::UpdateDerivIndexesResult; @@ -104,6 +107,13 @@ pub trait Daemon: Debug { async fn stop(&self) -> Result<(), DaemonError>; async fn get_info(&self) -> Result; async fn get_new_address(&self) -> Result; + async fn list_revealed_addresses( + &self, + is_change: bool, + exclude_used: bool, + limit: usize, + start_index: Option, + ) -> Result; async fn update_deriv_indexes( &self, receive: Option, diff --git a/liana-gui/src/daemon/model.rs b/liana-gui/src/daemon/model.rs index 9bbb0b94..53606e89 100644 --- a/liana-gui/src/daemon/model.rs +++ b/liana-gui/src/daemon/model.rs @@ -12,7 +12,8 @@ pub use liana::{ }; pub use lianad::commands::{ CreateSpendResult, GetAddressResult, GetInfoResult, GetLabelsResult, LabelItem, ListCoinsEntry, - ListCoinsResult, ListSpendEntry, ListSpendResult, ListTransactionsResult, TransactionInfo, + ListCoinsResult, ListRevealedAddressesEntry, ListRevealedAddressesResult, ListSpendEntry, + ListSpendResult, ListTransactionsResult, TransactionInfo, }; pub type Coin = ListCoinsEntry; diff --git a/liana-gui/src/services/connect/client/backend/api.rs b/liana-gui/src/services/connect/client/backend/api.rs index f795bd0e..e4dee4e3 100644 --- a/liana-gui/src/services/connect/client/backend/api.rs +++ b/liana-gui/src/services/connect/client/backend/api.rs @@ -347,6 +347,21 @@ pub struct Address { pub derivation_index: bip32::ChildNumber, } +#[derive(Deserialize)] +pub struct RevealedAddress { + #[serde(deserialize_with = "deser_addr_assume_checked")] + pub address: bitcoin::Address, + pub derivation_index: bip32::ChildNumber, + pub label: Option, + pub used_count: u32, +} + +#[derive(Deserialize)] +pub struct ListRevealedAddresses { + pub addresses: Vec, + pub continue_from: Option, +} + pub mod payload { use liana::{descriptors::LianaDescriptor, miniscript::bitcoin}; use serde::{Serialize, Serializer}; diff --git a/liana-gui/src/services/connect/client/backend/mod.rs b/liana-gui/src/services/connect/client/backend/mod.rs index ba58f713..a6426205 100644 --- a/liana-gui/src/services/connect/client/backend/mod.rs +++ b/liana-gui/src/services/connect/client/backend/mod.rs @@ -9,7 +9,9 @@ use async_trait::async_trait; use chrono::Utc; use liana::{ descriptors::LianaDescriptor, - miniscript::bitcoin::{address, psbt::Psbt, Address, Network, OutPoint, Txid}, + miniscript::bitcoin::{ + address, bip32::ChildNumber, psbt::Psbt, Address, Network, OutPoint, Txid, + }, }; use lianad::{ bip329::Labels, @@ -610,6 +612,57 @@ impl Daemon for BackendWalletClient { }) } + async fn list_revealed_addresses( + &self, + is_change: bool, + exclude_used: bool, + limit: usize, + start_index: Option, + ) -> Result { + let mut query = Vec::<(&str, String)>::new(); + query.push(("is_change_address", is_change.to_string())); + query.push(("exclude_used", exclude_used.to_string())); + query.push(("limit", limit.to_string())); + if let Some(start) = start_index { + query.push(("start_derivation_index", start.to_string())); + } + let response: Response = self + .inner + .request( + Method::GET, + &format!( + "{}/v1/wallets/{}/addresses", + self.inner.url, self.wallet_uuid + ), + ) + .await + .query(&query) + .send() + .await?; + + if !response.status().is_success() { + return Err(DaemonError::Http( + Some(response.status().into()), + response.text().await?, + )); + } + + let res: api::ListRevealedAddresses = response.json().await?; + Ok(ListRevealedAddressesResult { + addresses: res + .addresses + .into_iter() + .map(|addr| ListRevealedAddressesEntry { + index: addr.derivation_index, + address: addr.address, + label: addr.label, + used_count: addr.used_count, + }) + .collect(), + continue_from: res.continue_from, + }) + } + async fn update_deriv_indexes( &self, _receive: Option, diff --git a/lianad/src/commands/mod.rs b/lianad/src/commands/mod.rs index 77ffcf21..1ff60886 100644 --- a/lianad/src/commands/mod.rs +++ b/lianad/src/commands/mod.rs @@ -1412,11 +1412,12 @@ impl ListAddressesResult { } /// A revealed address entry in the list returned by [`DaemonControl::list_revealed_addresses`]. -#[derive(Debug, Clone, Serialize, PartialEq, Eq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct ListRevealedAddressesEntry { /// The address's derivation index. pub index: ChildNumber, /// The address. + #[serde(deserialize_with = "deser_addr_assume_checked")] pub address: bitcoin::Address, /// Label assigned to the address, if any. pub label: Option, @@ -1428,7 +1429,7 @@ pub struct ListRevealedAddressesEntry { } /// Result of a [`DaemonControl::list_revealed_addresses`] request. -#[derive(Debug, Clone, Serialize, PartialEq, Eq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct ListRevealedAddressesResult { /// Revealed addresses in order of descending derivation index. pub addresses: Vec,