Merge #1704: feat: List unused addresses on Receive panel

f8e702b65ea894131b212fdfed89b16068699fe9 feat: list unused addresses already revealed (Michael Mallan)
d84c75c1ce548c2c7dfecf98960e9a19409cb9e7 refactor: extract address card logic to function (Michael Mallan)
daa3daa938567e6d76e7dce02bddd725b97a72d9 refactor: add message for next receive address (Michael Mallan)
8b8b48142f60ede2b8ce702c89a61f3fd32a1842 feat: add list addresses method to gui daemon interface (Michael Mallan)

Pull request description:

  This implements #809 by listing unused addresses on the Receive panel.

ACKs for top commit:
  edouardparis:
    ACK f8e702b65ea894131b212fdfed89b16068699fe9

Tree-SHA512: 05538d099433bad33c947694e91bc082147159bebd4607ac2d37f9127013656c928a2a332f1212fc5f9d6a2d68e3047e7c3da5523233b92d47b2802811a706d8
This commit is contained in:
edouardparis 2025-05-15 12:18:11 +02:00
commit e910c06027
No known key found for this signature in database
GPG Key ID: E65F7A089C20DC8F
11 changed files with 484 additions and 108 deletions

View File

@ -26,6 +26,11 @@ pub enum Message {
LoadWallet(Wallet),
Info(Result<GetInfoResult, Error>),
ReceiveAddress(Result<(Address, ChildNumber), Error>),
/// Revealed addresses. The second element contains the start index used for the request.
RevealedAddresses(
Result<ListRevealedAddressesResult, Error>,
Option<ChildNumber>, // start_index
),
Coins(Result<Vec<Coin>, Error>),
/// When we want both coins and tip height together.
CoinsTipHeight(Result<Vec<Coin>, Error>, Result<i32, Error>),

View File

@ -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<String, String>,
}
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<LabelItem> {
self.list
@ -57,9 +65,14 @@ pub struct ReceivePanel {
data_dir: LianaDirectory,
wallet: Arc<Wallet>,
addresses: Addresses,
prev_addresses: Addresses,
prev_continue_from: Option<ChildNumber>,
show_prev_addresses: bool,
selected: HashSet<Address>,
labels_edited: LabelsEdited,
modal: Modal,
warning: Option<Error>,
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<dyn Daemon + Sync + Send>,
daemon: Arc<dyn Daemon + Sync + Send>,
wallet: Arc<Wallet>,
) -> Task<Message> {
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::<Request>::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::<ChildNumber>::None]}),
),
Ok(json!(ListRevealedAddressesResult {
addresses: vec![],
continue_from: None,
})),
),
(
Some(json!({"method": "getnewaddress", "params": Option::<Request>::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<ReceivePanel> = 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();

View File

@ -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<String>, LabelMessage),
NextReceiveAddress,
ToggleShowPreviousAddresses,
SelectAddress(Address),
Settings(SettingsMessage),
CreateSpend(CreateSpendMessage),
ImportSpend(ImportSpendMessage),

View File

@ -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<String, String>,
labels_editing: &'a HashMap<String, form::Value<String>>,
) -> 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<String, String>,
prev_addresses: &'a [bitcoin::Address],
prev_labels: &'a HashMap<String, String>,
show_prev_addresses: bool,
selected: &'a HashSet<bitcoin::Address>,
labels_editing: &'a HashMap<String, form::Value<String>>,
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()
}

View File

@ -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<C: Client + Send + Sync + Debug> Daemon for Lianad<C> {
self.call("getnewaddress", Option::<Request>::None)
}
async fn list_revealed_addresses(
&self,
is_change: bool,
exclude_used: bool,
limit: usize,
start_index: Option<ChildNumber>,
) -> Result<ListRevealedAddressesResult, DaemonError> {
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<u32>,

View File

@ -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<ChildNumber>,
) -> Result<ListRevealedAddressesResult, DaemonError> {
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<u32>,

View File

@ -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<model::GetInfoResult, DaemonError>;
async fn get_new_address(&self) -> Result<model::GetAddressResult, DaemonError>;
async fn list_revealed_addresses(
&self,
is_change: bool,
exclude_used: bool,
limit: usize,
start_index: Option<ChildNumber>,
) -> Result<model::ListRevealedAddressesResult, DaemonError>;
async fn update_deriv_indexes(
&self,
receive: Option<u32>,

View File

@ -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;

View File

@ -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<String>,
pub used_count: u32,
}
#[derive(Deserialize)]
pub struct ListRevealedAddresses {
pub addresses: Vec<RevealedAddress>,
pub continue_from: Option<bip32::ChildNumber>,
}
pub mod payload {
use liana::{descriptors::LianaDescriptor, miniscript::bitcoin};
use serde::{Serialize, Serializer};

View File

@ -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<ChildNumber>,
) -> Result<ListRevealedAddressesResult, DaemonError> {
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<u32>,

View File

@ -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<String>,
@ -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<ListRevealedAddressesEntry>,