Merge #478: Add refresh coins buttons

aa23bba9d48b3ce683c7090c7d7ab8c6e218868d ui: set default_features = false (edouard)
79984718bc65c3b536255e3737852de4aa3049d5 gui: add refresh coins buttons (edouard)
032cf801d5c18ba20d102b8334f09cc2cd4464c4 gui: create self send transaction (edouard)
89162262e9d72f9f78061dd890a25569a329af44 Update Liana core lib to master (Antoine Poinsot)

Pull request description:

  Fixes #371.
  Fixes #192.

ACKs for top commit:
  darosior:
    re-ACK aa23bba9d48b3ce683c7090c7d7ab8c6e218868d

Tree-SHA512: bf4685491ca404a659d99bf202bcceb103f6df89fb63e0e8ea1d25058c0022a7472fa354dc35e380d6adb06f1f0b337e6c476cf5d0c9c04e611248aacd496d6d
This commit is contained in:
Antoine Poinsot 2023-05-07 12:59:55 +02:00
commit ad5fdbae20
No known key found for this signature in database
GPG Key ID: E13FC145CD3F4304
13 changed files with 161 additions and 65 deletions

2
gui/Cargo.lock generated
View File

@ -1841,7 +1841,7 @@ dependencies = [
[[package]]
name = "liana"
version = "0.4.0"
source = "git+https://github.com/wizardsardine/liana?branch=0.4-lianad#81b81b2f789052cd8ef6b13964f9953f4fcbc0a4"
source = "git+https://github.com/wizardsardine/liana?branch=master#9a78b8cfffd5075dea3a041a9e661c27b14c57a2"
dependencies = [
"backtrace",
"base64",

View File

@ -15,7 +15,7 @@ path = "src/main.rs"
[dependencies]
async-hwi = "0.0.6"
liana = { git = "https://github.com/wizardsardine/liana", branch = "0.4-lianad", default-features = false }
liana = { git = "https://github.com/wizardsardine/liana", branch = "master", default-features = false }
liana_ui = { path = "ui" }
backtrace = "0.3"
base64 = "0.13"

View File

@ -1,3 +1,4 @@
use liana::miniscript::bitcoin::OutPoint;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Menu {
Home,
@ -8,4 +9,5 @@ pub enum Menu {
Coins,
CreateSpendTx,
Recovery,
RefreshCoins(Vec<OutPoint>),
}

View File

@ -92,6 +92,13 @@ impl App {
self.cache.blockheight as u32,
)
.into(),
menu::Menu::RefreshCoins(preselected) => CreateSpendPanel::new_self_send(
self.wallet.clone(),
&self.cache.coins,
self.cache.blockheight as u32,
preselected,
)
.into(),
};
self.state.load(self.daemon.clone())
}

View File

@ -11,7 +11,7 @@ use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
use iced::{widget::qr_code, Command, Subscription};
use liana::miniscript::bitcoin::{Address, Amount};
use liana::miniscript::bitcoin::{Address, Amount, OutPoint};
use liana_ui::widget::*;
use super::{cache::Cache, error::Error, menu::Menu, message::Message, view, wallet::Wallet};
@ -50,7 +50,7 @@ pub struct Home {
balance: Amount,
unconfirmed_balance: Amount,
remaining_sequence: Option<u32>,
number_of_expiring_coins: usize,
expiring_coins: Vec<OutPoint>,
pending_events: Vec<HistoryTransaction>,
events: Vec<HistoryTransaction>,
selected_event: Option<(usize, usize)>,
@ -76,7 +76,7 @@ impl Home {
balance,
unconfirmed_balance,
remaining_sequence: None,
number_of_expiring_coins: 0,
expiring_coins: Vec::new(),
selected_event: None,
events: Vec::new(),
pending_events: Vec::new(),
@ -103,7 +103,7 @@ impl State for Home {
&self.balance,
&self.unconfirmed_balance,
&self.remaining_sequence,
self.number_of_expiring_coins,
&self.expiring_coins,
&self.pending_events,
&self.events,
),
@ -125,7 +125,7 @@ impl State for Home {
self.balance = Amount::from_sat(0);
self.unconfirmed_balance = Amount::from_sat(0);
self.remaining_sequence = None;
self.number_of_expiring_coins = 0;
self.expiring_coins = Vec::new();
for coin in coins {
if coin.spend_info.is_none() {
if coin.block_height.is_some() {
@ -135,7 +135,7 @@ impl State for Home {
remaining_sequence(&coin, cache.blockheight as u32, timelock);
// number of block in a day
if seq <= 144 {
self.number_of_expiring_coins += 1;
self.expiring_coins.push(coin.outpoint);
}
if let Some(last) = &mut self.remaining_sequence {
if seq < *last {

View File

@ -3,6 +3,7 @@ use std::sync::Arc;
use iced::Command;
use liana::miniscript::bitcoin::OutPoint;
use liana_ui::widget::Element;
use super::{redirect, State};
@ -25,12 +26,33 @@ impl CreateSpendPanel {
draft: step::TransactionDraft::default(),
current: 0,
steps: vec![
Box::new(step::DefineSpend::new(
descriptor,
coins.to_vec(),
timelock,
blockheight,
)),
Box::new(
step::DefineSpend::new(descriptor, coins, timelock)
.with_coins_sorted(blockheight),
),
Box::new(step::SaveSpend::new(wallet)),
],
}
}
pub fn new_self_send(
wallet: Arc<Wallet>,
coins: &[Coin],
blockheight: u32,
preselected_coins: &[OutPoint],
) -> Self {
let descriptor = wallet.main_descriptor.clone();
let timelock = descriptor.first_timelock_value();
Self {
draft: step::TransactionDraft::default(),
current: 0,
steps: vec![
Box::new(
step::DefineSpend::new(descriptor, coins, timelock)
.with_preselected_coins(preselected_coins)
.with_coins_sorted(blockheight)
.self_send(),
),
Box::new(step::SaveSpend::new(wallet)),
],
}

View File

@ -60,12 +60,7 @@ pub struct DefineSpend {
}
impl DefineSpend {
pub fn new(
descriptor: LianaDescriptor,
coins: Vec<Coin>,
timelock: u16,
blockheight: u32,
) -> Self {
pub fn new(descriptor: LianaDescriptor, coins: &[Coin], timelock: u16) -> Self {
let balance_available = coins
.iter()
.filter_map(|coin| {
@ -76,27 +71,17 @@ impl DefineSpend {
}
})
.sum();
let mut coins: Vec<(Coin, bool)> = coins
.into_iter()
let coins: Vec<(Coin, bool)> = coins
.iter()
.filter_map(|c| {
if c.spend_info.is_none() {
Some((c, false))
Some((*c, false))
} else {
None
}
})
.collect();
coins.sort_by(|(a, _), (b, _)| {
if remaining_sequence(a, blockheight, timelock)
== remaining_sequence(b, blockheight, timelock)
{
// bigger amount first
b.amount.cmp(&a.amount)
} else {
// smallest blockheight (remaining_sequence) first
a.block_height.cmp(&b.block_height)
}
});
Self {
balance_available,
descriptor,
@ -111,9 +96,43 @@ impl DefineSpend {
warning: None,
}
}
pub fn with_preselected_coins(mut self, preselected_coins: &[OutPoint]) -> Self {
for (coin, selected) in &mut self.coins {
*selected = preselected_coins.contains(&coin.outpoint);
}
self
}
pub fn with_coins_sorted(mut self, blockheight: u32) -> Self {
let timelock = self.timelock;
self.coins.sort_by(|(a, a_selected), (b, b_selected)| {
if *a_selected && !b_selected || !a_selected && *b_selected {
b_selected.cmp(a_selected)
} else if remaining_sequence(a, blockheight, timelock)
== remaining_sequence(b, blockheight, timelock)
{
// bigger amount first
b.amount.cmp(&a.amount)
} else {
// smallest blockheight (remaining_sequence) first
a.block_height.cmp(&b.block_height)
}
});
self
}
pub fn self_send(mut self) -> Self {
self.recipients = Vec::new();
self
}
fn check_valid(&mut self) {
self.is_valid = !self.recipients.is_empty();
self.is_valid = self.feerate.valid && !self.feerate.value.is_empty();
self.is_duplicate = false;
if !self.coins.iter().any(|(_, selected)| *selected) {
self.is_valid = false;
}
for (i, recipient) in self.recipients.iter().enumerate() {
if !recipient.valid() {
self.is_valid = false;
@ -212,9 +231,9 @@ impl Step for DefineSpend {
}
view::CreateSpendMessage::FeerateEdited(s) => {
if s.parse::<u64>().is_ok() {
if let Ok(value) = s.parse::<u64>() {
self.feerate.value = s;
self.feerate.valid = true;
self.feerate.valid = value != 0;
self.amount_left_to_select();
} else if s.is_empty() {
self.feerate.value = "".to_string();

View File

@ -1,8 +1,8 @@
use iced::{Alignment, Length};
use iced::{widget::Space, Alignment, Length};
use liana_ui::{
color,
component::{amount::*, badge, text::*},
component::{amount::*, badge, button, text::*},
icon,
image::*,
theme,
@ -11,7 +11,7 @@ use liana_ui::{
};
use crate::{
app::{cache::Cache, view::message::Message},
app::{cache::Cache, menu::Menu, view::message::Message},
daemon::model::{remaining_sequence, Coin},
};
@ -136,7 +136,7 @@ fn coin_list_view(
.spacing(5)
})),
)
.push_maybe(coin.spend_info.map(|info| {
.push(if let Some(info) = coin.spend_info {
Column::new()
.push(
Row::new()
@ -159,7 +159,16 @@ fn coin_list_view(
)
})
.spacing(5)
})),
} else {
Column::new().push(
Row::new().push(Space::with_width(Length::Fill)).push(
button::primary(Some(icon::arrow_repeat()), "Refresh coin")
.on_press(Message::Menu(Menu::RefreshCoins(vec![
coin.outpoint,
]))),
),
)
}),
)
} else {
None

View File

@ -5,7 +5,7 @@ use iced::{alignment, Alignment, Length};
use liana::miniscript::bitcoin;
use liana_ui::{
color,
component::{amount::*, card, event, text::*},
component::{amount::*, button, card, event, text::*},
icon, theme,
util::Collection,
widget::*,
@ -27,7 +27,7 @@ pub fn home_view<'a>(
balance: &'a bitcoin::Amount,
unconfirmed_balance: &'a bitcoin::Amount,
remaining_sequence: &Option<u32>,
number_of_expiring_coins: usize,
expiring_coins: &Vec<bitcoin::OutPoint>,
pending_events: &[HistoryTransaction],
events: &Vec<HistoryTransaction>,
) -> Element<'a, Message> {
@ -48,7 +48,7 @@ pub fn home_view<'a>(
None
}),
)
.push_maybe(if number_of_expiring_coins == 0 {
.push_maybe(if expiring_coins.is_empty() {
remaining_sequence.map(|sequence| {
Container::new(
Row::new()
@ -75,13 +75,21 @@ pub fn home_view<'a>(
} else {
Some(
Container::new(
Row::new().spacing(15).align_items(Alignment::Center).push(
h4_regular(format!(
"You have {} coins that are already or about to be expired",
number_of_expiring_coins
))
.width(Length::Fill),
),
Row::new()
.spacing(15)
.align_items(Alignment::Center)
.push(
h4_regular(format!(
"You have {} coins that are already or about to be expired",
expiring_coins.len(),
))
.width(Length::Fill),
)
.push(
button::primary(Some(icon::arrow_repeat()), "Refresh coins").on_press(
Message::Menu(Menu::RefreshCoins(expiring_coins.clone())),
),
),
)
.padding(25)
.style(theme::Card::Invalid),

View File

@ -177,7 +177,7 @@ pub fn sidebar<'a>(menu: &Menu, cache: &'a Cache) -> Container<'a, Message> {
.center_x(),
)
.style(theme::Button::Menu(true))
.on_press(Message::Reload)
.on_press(Message::Menu(Menu::CreateSpendTx))
.width(iced::Length::Fill),
menu_green_bar()
)

View File

@ -93,12 +93,13 @@ pub fn create_spend_tx<'a>(
feerate: &form::Value<String>,
error: Option<&Error>,
) -> Element<'a, Message> {
let is_self_send = recipients.is_empty();
dashboard(
&Menu::CreateSpendTx,
cache,
error,
Column::new()
.push(h3("Send"))
.push(h3(if is_self_send { "Self send" } else { "Send" }))
.push(
Column::new()
.push(Column::with_children(recipients).spacing(10))
@ -116,12 +117,16 @@ pub fn create_spend_tx<'a>(
None
})
.push(Space::with_width(Length::Fill))
.push(
button::secondary(Some(icon::plus_icon()), "Add recipient")
.on_press(Message::CreateSpend(
CreateSpendMessage::AddRecipient,
)),
),
.push_maybe(if is_self_send {
None
} else {
Some(
button::secondary(Some(icon::plus_icon()), "Add recipient")
.on_press(Message::CreateSpend(
CreateSpendMessage::AddRecipient,
)),
)
}),
)
.spacing(20),
)
@ -151,7 +156,26 @@ pub fn create_spend_tx<'a>(
Row::new()
.align_items(Alignment::Center)
.push(p1_bold("Coins selection").width(Length::Fill))
.push(Container::new(if let Some(amount_left) = amount_left {
.push(if is_self_send {
Row::new()
.spacing(5)
.push(amount_with_size(
&Amount::from_sat(
coins
.iter()
.filter_map(|(coin, selected)| {
if *selected {
Some(coin.amount.to_sat())
} else {
None
}
})
.sum(),
),
P2_SIZE,
))
.push(p2_regular("selected").style(color::GREY_3))
} else if let Some(amount_left) = amount_left {
Row::new()
.spacing(5)
.push(amount_with_size(amount_left, P2_SIZE))
@ -159,7 +183,7 @@ pub fn create_spend_tx<'a>(
} else {
Row::new()
.push(text("Feerate needs to be set.").style(color::GREY_3))
}))
})
.width(Length::Fill),
)
.push(
@ -193,8 +217,9 @@ pub fn create_spend_tx<'a>(
)
.push(
if is_valid
&& total_amount < *balance_available
&& Some(&Amount::from_sat(0)) == amount_left
&& (is_self_send
|| (total_amount < *balance_available
&& Some(&Amount::from_sat(0)) == amount_left))
{
button::primary(None, "Next")
.on_press(Message::CreateSpend(CreateSpendMessage::Generate))

View File

@ -6,7 +6,7 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
iced = { version = "0.7", features = ["svg", "image"] }
iced = { version = "0.7", default_features = false, features = ["svg", "image", "glow"] }
iced_native = "0.8"
iced_lazy = { version = "0.4"}
bitcoin = "0.29"

View File

@ -22,6 +22,10 @@ pub fn arrow_right() -> Text<'static> {
icon('\u{F138}')
}
pub fn arrow_repeat() -> Text<'static> {
icon('\u{F130}')
}
pub fn arrow_return_right() -> Text<'static> {
icon('\u{F132}')
}