Skip to content

Commit

Permalink
Merge #621: Gui labels
Browse files Browse the repository at this point in the history
757b53e cargo update -p liana (edouard)
aeff735 fix unconfirmed payments layouts with labels (edouard)
dbb9146 fix update labels for pending_txs (edouard)
9edcdd9 Add labels to change outputs according to main label (edouard)
2354ac9 cargo clippy --fix --lib -p liana_gui (edouard)
9db4541 Add labels support to gui (edouard)

Pull request description:

  This PR use the lianad update_labels and get_labels commands.
  It also introduce new concepts from talks with Kevin and Antoine:
  - User spending is creating a payment, when he does not add multiple recipients anymore, he is doing multiple payment in one bitcoin transaction.
  - a transaction that have multiple outgoing outputs (multiple payment) is tagged as a 'Batch'.

ACKs for top commit:
  edouardparis:
    Self-ACK 757b53e

Tree-SHA512: 2208009fcc0ab8f587929a347d191d428156984e87b732fb6efa5f08a3962c85510c82fcdeba687a6d2bd2fa45f5a17384d8a9b709e1407ebbbe0f451fe69e90
  • Loading branch information
edouardparis committed Oct 19, 2023
2 parents 605a13d + 757b53e commit 2fbafd9
Show file tree
Hide file tree
Showing 28 changed files with 1,591 additions and 375 deletions.
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

0 comments on commit 2fbafd9

Please sign in to comment.