feat: list unused addresses already revealed
This commit is contained in:
parent
d84c75c1ce
commit
f8e702b65e
@ -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>),
|
||||
|
||||
@ -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,16 +189,16 @@ 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()
|
||||
}
|
||||
@ -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()),
|
||||
|
||||
@ -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;
|
||||
@ -15,6 +15,8 @@ pub enum Message {
|
||||
SelectPayment(OutPoint),
|
||||
Label(Vec<String>, LabelMessage),
|
||||
NextReceiveAddress,
|
||||
ToggleShowPreviousAddresses,
|
||||
SelectAddress(Address),
|
||||
Settings(SettingsMessage),
|
||||
CreateSpend(CreateSpendMessage),
|
||||
ImportSpend(ImportSpendMessage),
|
||||
|
||||
@ -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,
|
||||
};
|
||||
@ -89,11 +90,19 @@ fn address_card<'a>(
|
||||
)
|
||||
}
|
||||
|
||||
#[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()
|
||||
@ -104,15 +113,143 @@ pub fn receive<'a>(
|
||||
.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)| col.push(address_card(i, address, labels, labels_editing)),
|
||||
|col, (i, address)| {
|
||||
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()
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user