Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Gui labels #621

Merged
merged 6 commits into from
Oct 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions gui/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions gui/src/app/message.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use std::collections::HashMap;
use std::sync::Arc;

use liana::{
Expand All @@ -22,6 +23,7 @@ pub enum Message {
Info(Result<GetInfoResult, Error>),
ReceiveAddress(Result<Address, Error>),
Coins(Result<Vec<Coin>, Error>),
Labels(Result<HashMap<String, String>, Error>),
SpendTxs(Result<Vec<SpendTx>, Error>),
Psbt(Result<Psbt, Error>),
Recovery(Result<SpendTx, Error>),
Expand All @@ -33,4 +35,5 @@ pub enum Message {
ConnectedHardwareWallets(Vec<HardwareWallet>),
HistoryTransactions(Result<Vec<HistoryTransaction>, Error>),
PendingTransactions(Result<Vec<HistoryTransaction>, Error>),
LabelsUpdated(Result<HashMap<String, String>, Error>),
}
2 changes: 2 additions & 0 deletions gui/src/app/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,13 +97,15 @@ impl App {
self.wallet.clone(),
&self.cache.coins,
self.cache.blockheight as u32,
self.cache.network,
)
.into(),
menu::Menu::RefreshCoins(preselected) => CreateSpendPanel::new_self_send(
self.wallet.clone(),
&self.cache.coins,
self.cache.blockheight as u32,
preselected,
self.cache.network,
)
.into(),
};
Expand Down
4 changes: 2 additions & 2 deletions gui/src/app/settings.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//! Settings is the module to handle the GUI settings file.
//! The settings file is used by the GUI to store useful information.
use std::collections::HashMap;
use std::fs::OpenOptions;
use std::io::Write;
Expand All @@ -8,8 +10,6 @@ use serde::{Deserialize, Serialize};

use crate::{app::wallet::Wallet, hw::HardwareWalletConfig};

///! Settings is the module to handle the GUI settings file.
///! The settings file is used by the GUI to store useful information.
pub const DEFAULT_FILE_NAME: &str = "settings.json";

#[derive(Debug, Clone, Deserialize, Serialize)]
Expand Down
121 changes: 96 additions & 25 deletions gui/src/app/state/coins.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,48 @@
use std::cmp::Ordering;
use std::collections::HashMap;
use std::sync::Arc;
use std::{cmp::Ordering, collections::HashSet};

use iced::Command;

use liana_ui::widget::Element;

use crate::{
app::{cache::Cache, error::Error, menu::Menu, message::Message, state::State, view},
daemon::{model::Coin, Daemon},
app::{
cache::Cache,
error::Error,
menu::Menu,
message::Message,
state::{label::LabelsEdited, State},
view,
},
daemon::{
model::{Coin, LabelItem, Labelled},
Daemon,
},
};

#[derive(Debug, Default)]
pub struct Coins {
list: Vec<Coin>,
labels: HashMap<String, String>,
}

impl Labelled for Coins {
fn labelled(&self) -> Vec<LabelItem> {
self.list
.iter()
.map(|a| LabelItem::OutPoint(a.outpoint))
.collect()
}
fn labels(&mut self) -> &mut HashMap<String, String> {
&mut self.labels
}
}

pub struct CoinsPanel {
coins: Vec<Coin>,
coins: Coins,
selected: Vec<usize>,
labels_edited: LabelsEdited,
warning: Option<Error>,
/// timelock value to pass for the heir to consume a coin.
timelock: u16,
Expand All @@ -21,7 +51,8 @@ pub struct CoinsPanel {
impl CoinsPanel {
pub fn new(coins: &[Coin], timelock: u16) -> Self {
let mut panel = Self {
coins: Vec::new(),
labels_edited: LabelsEdited::default(),
coins: Coins::default(),
selected: Vec::new(),
warning: None,
timelock,
Expand All @@ -31,18 +62,14 @@ impl CoinsPanel {
}

fn update_coins(&mut self, coins: &[Coin]) {
self.coins = coins
self.coins.list = coins
.iter()
.filter_map(|coin| {
if coin.spend_info.is_none() {
Some(coin.clone())
} else {
None
}
})
.filter(|coin| coin.spend_info.is_none())
.cloned()
.collect();

self.coins
.list
.sort_by(|a, b| match (a.block_height, b.block_height) {
(Some(a_height), Some(b_height)) => {
if a_height == b_height {
Expand All @@ -64,13 +91,20 @@ impl State for CoinsPanel {
&Menu::Coins,
cache,
self.warning.as_ref(),
view::coins::coins_view(cache, &self.coins, self.timelock, &self.selected),
view::coins::coins_view(
cache,
&self.coins.list,
self.timelock,
&self.selected,
&self.coins.labels,
self.labels_edited.cache(),
),
)
}

fn update(
&mut self,
_daemon: Arc<dyn Daemon + Sync + Send>,
daemon: Arc<dyn Daemon + Sync + Send>,
_cache: &Cache,
message: Message,
) -> Command<Message> {
Expand All @@ -83,6 +117,24 @@ impl State for CoinsPanel {
self.update_coins(&coins);
}
},
Message::Labels(res) => match res {
Err(e) => self.warning = Some(e),
Ok(labels) => {
self.coins.labels = labels;
}
},
Message::View(view::Message::Label(_, _)) | Message::LabelsUpdated(_) => {
match self.labels_edited.update(
daemon,
message,
std::iter::once(&mut self.coins).map(|a| a as &mut dyn Labelled),
) {
Ok(cmd) => return cmd,
Err(e) => {
self.warning = Some(e);
}
}
}
Message::View(view::Message::Select(i)) => {
if let Some(position) = self.selected.iter().position(|j| *j == i) {
self.selected.remove(position);
Expand All @@ -96,16 +148,34 @@ impl State for CoinsPanel {
}

fn load(&self, daemon: Arc<dyn Daemon + Sync + Send>) -> Command<Message> {
let daemon = daemon.clone();
Command::perform(
async move {
daemon
.list_coins()
.map(|res| res.coins)
.map_err(|e| e.into())
},
Message::Coins,
)
let daemon1 = daemon.clone();
let daemon2 = daemon.clone();
Command::batch(vec![
Command::perform(
async move {
daemon1
.list_coins()
.map(|res| res.coins)
.map_err(|e| e.into())
},
Message::Coins,
),
Command::perform(
async move {
let coins = daemon2
.list_coins()
.map(|res| res.coins)
.map_err(Error::from)?;
let mut targets = HashSet::<LabelItem>::new();
for coin in coins {
targets.insert(LabelItem::OutPoint(coin.outpoint));
targets.insert(LabelItem::Address(coin.address));
}
daemon2.get_labels(&targets).map_err(|e| e.into())
},
Message::Labels,
),
])
}
}

Expand Down Expand Up @@ -172,6 +242,7 @@ mod tests {
assert_eq!(
panel
.coins
.list
.iter()
.map(|c| c.outpoint)
.collect::<Vec<bitcoin::OutPoint>>(),
Expand Down
88 changes: 88 additions & 0 deletions gui/src/app/state/label.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
use liana::miniscript::bitcoin;
use std::str::FromStr;
use std::{collections::HashMap, iter::IntoIterator, sync::Arc};

use crate::{
app::{error::Error, message::Message, view},
daemon::{
model::{LabelItem, Labelled},
Daemon,
},
};
use iced::Command;
use liana_ui::component::form;

#[derive(Default)]
pub struct LabelsEdited(HashMap<String, form::Value<String>>);

impl LabelsEdited {
pub fn cache(&self) -> &HashMap<String, form::Value<String>> {
&self.0
}
pub fn update<'a, T: IntoIterator<Item = &'a mut dyn Labelled>>(
&mut self,
daemon: Arc<dyn Daemon + Sync + Send>,
message: Message,
targets: T,
) -> Result<Command<Message>, Error> {
match message {
Message::View(view::Message::Label(labelled, msg)) => match msg {
view::LabelMessage::Edited(value) => {
let valid = value.len() <= 100;
if let Some(label) = self.0.get_mut(&labelled) {
label.valid = valid;
label.value = value;
} else {
self.0.insert(labelled, form::Value { valid, value });
}
}
view::LabelMessage::Cancel => {
self.0.remove(&labelled);
}
view::LabelMessage::Confirm => {
if let Some(label) = self.0.get(&labelled).cloned() {
return Ok(Command::perform(
async move {
if let Some(item) = label_item_from_str(&labelled) {
daemon.update_labels(&HashMap::from([(
item,
label.value.clone(),
)]))?;
}
Ok(HashMap::from([(labelled, label.value)]))
},
Message::LabelsUpdated,
));
}
}
},
Message::LabelsUpdated(res) => match res {
Ok(new_labels) => {
for target in targets {
target.load_labels(&new_labels);
}
for (labelled, _) in new_labels {
self.0.remove(&labelled);
}
}
Err(e) => {
return Err(e);
}
},
_ => {}
};
Ok(Command::none())
}
}

pub fn label_item_from_str(s: &str) -> Option<LabelItem> {
if let Ok(addr) = bitcoin::Address::from_str(s) {
Some(LabelItem::Address(addr.assume_checked()))
} else if let Ok(txid) = bitcoin::Txid::from_str(s) {
Some(LabelItem::Txid(txid))
} else if let Ok(outpoint) = bitcoin::OutPoint::from_str(s) {
Some(LabelItem::OutPoint(outpoint))
} else {
None
}
}
Loading
Loading