Merge #279: gui: fix spend creation steps
afc4636ad98bcff9ab2b63f3fbf08f9318031cf9 Forbid next step if amount sent > available amount (edouard) 3eddedd7a8c77acf53ebe095d85ceced7b7f7263 Reload amount left to select in choose coins step (edouard) 86896c822317ab108541acbf0622d94a9119c479 Check output amount superior to dust value (edouard) ecd35d2bd171c5975de62188d142494268e394a2 Add available balance to choose recipient step (edouard) Pull request description: close #195 close #237 close #196 ACKs for top commit: edouardparis: Self-ACK afc4636ad98bcff9ab2b63f3fbf08f9318031cf9 Tree-SHA512: 1f3e6703e3f01a5496f890fe8e8104693ab58f74510f07593b65dead7e1629f91753c1b169a2c0d481fc14d85136cbbcb3076057c9535f3afd93f290ac580692
This commit is contained in:
commit
ae331c7aec
@ -80,6 +80,7 @@ impl App {
|
||||
menu::Menu::Spend => SpendPanel::new(self.config.clone(), &self.cache.spend_txs).into(),
|
||||
menu::Menu::CreateSpendTx => CreateSpendPanel::new(
|
||||
self.config.clone(),
|
||||
self.daemon.config().main_descriptor.clone(),
|
||||
&self.cache.coins,
|
||||
self.daemon.config().main_descriptor.timelock_value(),
|
||||
self.cache.blockheight as u32,
|
||||
|
||||
@ -4,6 +4,8 @@ use std::sync::Arc;
|
||||
|
||||
use iced::{Command, Element};
|
||||
|
||||
use liana::descriptors::MultipathDescriptor;
|
||||
|
||||
use super::{redirect, State};
|
||||
use crate::{
|
||||
app::{cache::Cache, config::Config, error::Error, menu::Menu, message::Message, view},
|
||||
@ -104,13 +106,20 @@ pub struct CreateSpendPanel {
|
||||
}
|
||||
|
||||
impl CreateSpendPanel {
|
||||
pub fn new(config: Config, coins: &[Coin], timelock: u32, blockheight: u32) -> Self {
|
||||
pub fn new(
|
||||
config: Config,
|
||||
descriptor: MultipathDescriptor,
|
||||
coins: &[Coin],
|
||||
timelock: u32,
|
||||
blockheight: u32,
|
||||
) -> Self {
|
||||
Self {
|
||||
draft: step::TransactionDraft::default(),
|
||||
current: 0,
|
||||
steps: vec![
|
||||
Box::new(step::ChooseRecipients::default()),
|
||||
Box::new(step::ChooseRecipients::new(coins)),
|
||||
Box::new(step::ChooseCoins::new(
|
||||
descriptor,
|
||||
coins.to_vec(),
|
||||
timelock,
|
||||
blockheight,
|
||||
|
||||
@ -4,7 +4,7 @@ use std::sync::Arc;
|
||||
|
||||
use iced::{Command, Element};
|
||||
use liana::{
|
||||
config::Config as DaemonConfig,
|
||||
descriptors::MultipathDescriptor,
|
||||
miniscript::bitcoin::{
|
||||
self, util::psbt::Psbt, Address, Amount, Denomination, Network, OutPoint,
|
||||
},
|
||||
@ -21,6 +21,9 @@ use crate::{
|
||||
ui::component::form,
|
||||
};
|
||||
|
||||
/// See: https://github.com/wizardsardine/liana/blob/master/src/commands/mod.rs#L32
|
||||
const DUST_OUTPUT_SATS: u64 = 5_000;
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
pub struct TransactionDraft {
|
||||
inputs: Vec<Coin>,
|
||||
@ -42,22 +45,30 @@ pub trait Step {
|
||||
}
|
||||
|
||||
pub struct ChooseRecipients {
|
||||
balance_available: Amount,
|
||||
recipients: Vec<Recipient>,
|
||||
is_valid: bool,
|
||||
is_duplicate: bool,
|
||||
}
|
||||
|
||||
impl std::default::Default for ChooseRecipients {
|
||||
fn default() -> Self {
|
||||
impl ChooseRecipients {
|
||||
pub fn new(coins: &[Coin]) -> Self {
|
||||
Self {
|
||||
balance_available: coins
|
||||
.iter()
|
||||
.filter_map(|coin| {
|
||||
if coin.spend_info.is_none() {
|
||||
Some(coin.amount)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.sum(),
|
||||
recipients: vec![Recipient::default()],
|
||||
is_valid: false,
|
||||
is_duplicate: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ChooseRecipients {
|
||||
fn check_valid(&mut self) {
|
||||
self.is_valid = !self.recipients.is_empty();
|
||||
self.is_duplicate = false;
|
||||
@ -117,6 +128,7 @@ impl Step for ChooseRecipients {
|
||||
|
||||
fn view<'a>(&'a self, _cache: &'a Cache) -> Element<'a, view::Message> {
|
||||
view::spend::step::choose_recipients_view(
|
||||
&self.balance_available,
|
||||
self.recipients
|
||||
.iter()
|
||||
.enumerate()
|
||||
@ -153,6 +165,10 @@ impl Recipient {
|
||||
return Err(Error::Unexpected("Amount should be non-zero".to_string()));
|
||||
}
|
||||
|
||||
if amount.to_sat() < DUST_OUTPUT_SATS {
|
||||
return Err(Error::Unexpected("Amount should be non-zero".to_string()));
|
||||
}
|
||||
|
||||
if let Ok(address) = Address::from_str(&self.address.value) {
|
||||
if amount <= address.script_pubkey().dust_value() {
|
||||
return Err(Error::Unexpected(
|
||||
@ -207,8 +223,8 @@ impl Recipient {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ChooseCoins {
|
||||
descriptor: MultipathDescriptor,
|
||||
timelock: u32,
|
||||
coins: Vec<(Coin, bool)>,
|
||||
recipients: Vec<(Address, Amount)>,
|
||||
@ -220,7 +236,12 @@ pub struct ChooseCoins {
|
||||
}
|
||||
|
||||
impl ChooseCoins {
|
||||
pub fn new(coins: Vec<Coin>, timelock: u32, blockheight: u32) -> Self {
|
||||
pub fn new(
|
||||
descriptor: MultipathDescriptor,
|
||||
coins: Vec<Coin>,
|
||||
timelock: u32,
|
||||
blockheight: u32,
|
||||
) -> Self {
|
||||
let mut coins: Vec<(Coin, bool)> = coins
|
||||
.into_iter()
|
||||
.filter_map(|c| {
|
||||
@ -243,6 +264,7 @@ impl ChooseCoins {
|
||||
}
|
||||
});
|
||||
Self {
|
||||
descriptor,
|
||||
timelock,
|
||||
coins,
|
||||
recipients: Vec::new(),
|
||||
@ -253,7 +275,7 @@ impl ChooseCoins {
|
||||
}
|
||||
}
|
||||
|
||||
fn amount_left_to_select(&mut self, cfg: &DaemonConfig) {
|
||||
fn amount_left_to_select(&mut self) {
|
||||
// We need the feerate in order to compute the required amount of BTC to
|
||||
// select. Return early if we don't to not do unnecessary computation.
|
||||
let feerate = match self.feerate.value.parse::<u64>() {
|
||||
@ -293,7 +315,7 @@ impl ChooseCoins {
|
||||
};
|
||||
// nValue size + scriptPubKey CompactSize + OP_0 + PUSH32 + <wit program>
|
||||
const CHANGE_TXO_SIZE: usize = 8 + 1 + 1 + 1 + 32;
|
||||
let satisfaction_vsize = cfg.main_descriptor.max_sat_weight() / 4;
|
||||
let satisfaction_vsize = self.descriptor.max_sat_weight() / 4;
|
||||
let transaction_size =
|
||||
tx_template.vsize() + satisfaction_vsize * tx_template.input.len() + CHANGE_TXO_SIZE;
|
||||
|
||||
@ -317,6 +339,7 @@ impl Step for ChooseCoins {
|
||||
.iter()
|
||||
.map(|(k, v)| (k.clone(), Amount::from_sat(*v)))
|
||||
.collect();
|
||||
self.amount_left_to_select();
|
||||
}
|
||||
|
||||
fn apply(&self, draft: &mut TransactionDraft) {
|
||||
@ -342,7 +365,7 @@ impl Step for ChooseCoins {
|
||||
if s.parse::<u64>().is_ok() {
|
||||
self.feerate.value = s;
|
||||
self.feerate.valid = true;
|
||||
self.amount_left_to_select(daemon.config());
|
||||
self.amount_left_to_select();
|
||||
} else if s.is_empty() {
|
||||
self.feerate.value = "".to_string();
|
||||
self.feerate.valid = true;
|
||||
@ -384,7 +407,7 @@ impl Step for ChooseCoins {
|
||||
Message::View(view::Message::CreateSpend(view::CreateSpendMessage::SelectCoin(i))) => {
|
||||
if let Some(coin) = self.coins.get_mut(i) {
|
||||
coin.1 = !coin.1;
|
||||
self.amount_left_to_select(daemon.config());
|
||||
self.amount_left_to_select();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
|
||||
@ -23,12 +23,13 @@ use crate::{
|
||||
},
|
||||
};
|
||||
|
||||
pub fn choose_recipients_view(
|
||||
recipients: Vec<Element<Message>>,
|
||||
pub fn choose_recipients_view<'a>(
|
||||
balance_available: &'a Amount,
|
||||
recipients: Vec<Element<'a, Message>>,
|
||||
total_amount: Amount,
|
||||
is_valid: bool,
|
||||
duplicate: bool,
|
||||
) -> Element<Message> {
|
||||
) -> Element<'a, Message> {
|
||||
modal(
|
||||
false,
|
||||
None,
|
||||
@ -53,15 +54,21 @@ pub fn choose_recipients_view(
|
||||
.spacing(20)
|
||||
.align_items(Alignment::Center)
|
||||
.push(
|
||||
Container::new(text(format!("{}", total_amount)).bold())
|
||||
.width(Length::Fill),
|
||||
Container::new(
|
||||
Row::new()
|
||||
.align_items(Alignment::Center)
|
||||
.spacing(5)
|
||||
.push(text(format!("{}", total_amount)).bold())
|
||||
.push(text(format!("/ {}", balance_available))),
|
||||
)
|
||||
.width(Length::Fill),
|
||||
)
|
||||
.push_maybe(if duplicate {
|
||||
Some(text("Two recipient addresses are the same").style(color::WARNING))
|
||||
} else {
|
||||
None
|
||||
})
|
||||
.push(if is_valid {
|
||||
.push(if is_valid && total_amount < *balance_available {
|
||||
button::primary(None, "Next")
|
||||
.on_press(Message::Next)
|
||||
.width(Length::Units(100))
|
||||
@ -93,11 +100,11 @@ pub fn recipient_view<'a>(
|
||||
form::Form::new("Amount", amount, move |msg| {
|
||||
CreateSpendMessage::RecipientEdited(index, "amount", msg)
|
||||
})
|
||||
.warning("Please enter correct amount")
|
||||
.warning("Please enter correct amount (> 5000 sats)")
|
||||
.size(20)
|
||||
.padding(10),
|
||||
)
|
||||
.width(Length::Units(250)),
|
||||
.width(Length::Units(300)),
|
||||
)
|
||||
.spacing(5)
|
||||
.push(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user