gui: merge spend creation steps
This commit is contained in:
parent
4613da4ae4
commit
6b2c9a61e0
@ -26,8 +26,7 @@ impl CreateSpendPanel {
|
||||
draft: step::TransactionDraft::default(),
|
||||
current: 0,
|
||||
steps: vec![
|
||||
Box::new(step::ChooseRecipients::new(coins)),
|
||||
Box::new(step::ChooseCoins::new(
|
||||
Box::new(step::DefineSpend::new(
|
||||
descriptor,
|
||||
coins.to_vec(),
|
||||
timelock,
|
||||
@ -72,7 +71,7 @@ impl State for CreateSpendPanel {
|
||||
}
|
||||
|
||||
if let Some(step) = self.steps.get_mut(self.current) {
|
||||
return step.update(daemon, cache, &self.draft, message);
|
||||
return step.update(daemon, cache, message);
|
||||
}
|
||||
|
||||
Command::none()
|
||||
|
||||
@ -28,7 +28,6 @@ const DUST_OUTPUT_SATS: u64 = 5_000;
|
||||
#[derive(Default, Clone)]
|
||||
pub struct TransactionDraft {
|
||||
inputs: Vec<Coin>,
|
||||
outputs: HashMap<Address, u64>,
|
||||
generated: Option<Psbt>,
|
||||
}
|
||||
|
||||
@ -38,36 +37,77 @@ pub trait Step {
|
||||
&mut self,
|
||||
daemon: Arc<dyn Daemon + Sync + Send>,
|
||||
cache: &Cache,
|
||||
draft: &TransactionDraft,
|
||||
message: Message,
|
||||
) -> Command<Message>;
|
||||
fn apply(&self, _draft: &mut TransactionDraft) {}
|
||||
fn load(&mut self, _draft: &TransactionDraft) {}
|
||||
}
|
||||
|
||||
pub struct ChooseRecipients {
|
||||
pub struct DefineSpend {
|
||||
balance_available: Amount,
|
||||
recipients: Vec<Recipient>,
|
||||
is_valid: bool,
|
||||
is_duplicate: bool,
|
||||
|
||||
descriptor: LianaDescriptor,
|
||||
timelock: u16,
|
||||
coins: Vec<(Coin, bool)>,
|
||||
amount_left_to_select: Option<Amount>,
|
||||
feerate: form::Value<String>,
|
||||
generated: Option<Psbt>,
|
||||
warning: Option<Error>,
|
||||
}
|
||||
|
||||
impl ChooseRecipients {
|
||||
pub fn new(coins: &[Coin]) -> Self {
|
||||
impl DefineSpend {
|
||||
pub fn new(
|
||||
descriptor: LianaDescriptor,
|
||||
coins: Vec<Coin>,
|
||||
timelock: u16,
|
||||
blockheight: u32,
|
||||
) -> Self {
|
||||
let balance_available = coins
|
||||
.iter()
|
||||
.filter_map(|coin| {
|
||||
if coin.spend_info.is_none() {
|
||||
Some(coin.amount)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.sum();
|
||||
let mut coins: Vec<(Coin, bool)> = coins
|
||||
.into_iter()
|
||||
.filter_map(|c| {
|
||||
if c.spend_info.is_none() {
|
||||
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: coins
|
||||
.iter()
|
||||
.filter_map(|coin| {
|
||||
if coin.spend_info.is_none() {
|
||||
Some(coin.amount)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.sum(),
|
||||
balance_available,
|
||||
descriptor,
|
||||
timelock,
|
||||
generated: None,
|
||||
coins,
|
||||
recipients: vec![Recipient::default()],
|
||||
is_valid: false,
|
||||
is_duplicate: false,
|
||||
feerate: form::Value::default(),
|
||||
amount_left_to_select: None,
|
||||
warning: None,
|
||||
}
|
||||
}
|
||||
fn check_valid(&mut self) {
|
||||
@ -84,51 +124,170 @@ impl ChooseRecipients {
|
||||
}
|
||||
}
|
||||
}
|
||||
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>() {
|
||||
Ok(f) => f,
|
||||
Err(_) => {
|
||||
self.amount_left_to_select = None;
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// The coins to be included in this transaction.
|
||||
let selected_coins: Vec<_> = self
|
||||
.coins
|
||||
.iter()
|
||||
.filter_map(|(c, selected)| if *selected { Some(c) } else { None })
|
||||
.collect();
|
||||
|
||||
// A dummy representation of the transaction that will be computed, for
|
||||
// the purpose of computing its size in order to anticipate the fees needed.
|
||||
// NOTE: we make the conservative estimation a change output will always be
|
||||
// needed.
|
||||
let tx_template = bitcoin::Transaction {
|
||||
version: 2,
|
||||
lock_time: bitcoin::PackedLockTime(0),
|
||||
input: selected_coins
|
||||
.iter()
|
||||
.map(|_| bitcoin::TxIn::default())
|
||||
.collect(),
|
||||
output: self
|
||||
.recipients
|
||||
.iter()
|
||||
.filter_map(|recipient| {
|
||||
if recipient.valid() {
|
||||
Some(bitcoin::TxOut {
|
||||
script_pubkey: Address::from_str(&recipient.address.value)
|
||||
.unwrap()
|
||||
.script_pubkey(),
|
||||
value: recipient.amount().unwrap(),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
};
|
||||
// nValue size + scriptPubKey CompactSize + OP_0 + PUSH32 + <wit program>
|
||||
const CHANGE_TXO_SIZE: usize = 8 + 1 + 1 + 1 + 32;
|
||||
let satisfaction_vsize = self.descriptor.max_sat_weight() / 4;
|
||||
let transaction_size =
|
||||
tx_template.vsize() + satisfaction_vsize * tx_template.input.len() + CHANGE_TXO_SIZE;
|
||||
|
||||
// Now the calculation of the amount left to be selected by the user is a simple
|
||||
// substraction between the value needed by the transaction to be created and the
|
||||
// value that was selected already.
|
||||
let selected_amount = selected_coins.iter().map(|c| c.amount.to_sat()).sum();
|
||||
let output_sum: u64 = tx_template.output.iter().map(|o| o.value).sum();
|
||||
let needed_amount: u64 = transaction_size as u64 * feerate + output_sum;
|
||||
self.amount_left_to_select = Some(Amount::from_sat(
|
||||
needed_amount.saturating_sub(selected_amount),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
impl Step for ChooseRecipients {
|
||||
impl Step for DefineSpend {
|
||||
fn update(
|
||||
&mut self,
|
||||
_daemon: Arc<dyn Daemon + Sync + Send>,
|
||||
daemon: Arc<dyn Daemon + Sync + Send>,
|
||||
cache: &Cache,
|
||||
_draft: &TransactionDraft,
|
||||
message: Message,
|
||||
) -> Command<Message> {
|
||||
if let Message::View(view::Message::CreateSpend(msg)) = message {
|
||||
match &msg {
|
||||
match msg {
|
||||
view::CreateSpendMessage::AddRecipient => {
|
||||
self.recipients.push(Recipient::default());
|
||||
}
|
||||
view::CreateSpendMessage::DeleteRecipient(i) => {
|
||||
self.recipients.remove(*i);
|
||||
self.recipients.remove(i);
|
||||
}
|
||||
view::CreateSpendMessage::RecipientEdited(i, _, _) => {
|
||||
self.recipients
|
||||
.get_mut(*i)
|
||||
.get_mut(i)
|
||||
.unwrap()
|
||||
.update(cache.network, msg);
|
||||
}
|
||||
|
||||
view::CreateSpendMessage::FeerateEdited(s) => {
|
||||
if s.parse::<u64>().is_ok() {
|
||||
self.feerate.value = s;
|
||||
self.feerate.valid = true;
|
||||
self.amount_left_to_select();
|
||||
} else if s.is_empty() {
|
||||
self.feerate.value = "".to_string();
|
||||
self.feerate.valid = true;
|
||||
self.amount_left_to_select = None;
|
||||
} else {
|
||||
self.feerate.valid = false;
|
||||
self.amount_left_to_select = None;
|
||||
}
|
||||
self.warning = None;
|
||||
}
|
||||
view::CreateSpendMessage::Generate => {
|
||||
let inputs: Vec<OutPoint> = self
|
||||
.coins
|
||||
.iter()
|
||||
.filter_map(
|
||||
|(coin, selected)| if *selected { Some(coin.outpoint) } else { None },
|
||||
)
|
||||
.collect();
|
||||
let mut outputs: HashMap<Address, u64> = HashMap::new();
|
||||
for recipient in &self.recipients {
|
||||
outputs.insert(
|
||||
Address::from_str(&recipient.address.value).expect("Checked before"),
|
||||
recipient.amount().expect("Checked before"),
|
||||
);
|
||||
}
|
||||
let feerate_vb = self.feerate.value.parse::<u64>().unwrap_or(0);
|
||||
self.warning = None;
|
||||
return Command::perform(
|
||||
async move {
|
||||
daemon
|
||||
.create_spend_tx(&inputs, &outputs, feerate_vb)
|
||||
.map(|res| res.psbt)
|
||||
.map_err(|e| e.into())
|
||||
},
|
||||
Message::Psbt,
|
||||
);
|
||||
}
|
||||
view::CreateSpendMessage::SelectCoin(i) => {
|
||||
if let Some(coin) = self.coins.get_mut(i) {
|
||||
coin.1 = !coin.1;
|
||||
self.amount_left_to_select();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
self.check_valid();
|
||||
Command::none()
|
||||
} else {
|
||||
if let Message::Psbt(res) = message {
|
||||
match res {
|
||||
Ok(psbt) => {
|
||||
self.generated = Some(psbt);
|
||||
return Command::perform(async {}, |_| Message::View(view::Message::Next));
|
||||
}
|
||||
Err(e) => self.warning = Some(e),
|
||||
}
|
||||
}
|
||||
Command::none()
|
||||
}
|
||||
Command::none()
|
||||
}
|
||||
|
||||
fn apply(&self, draft: &mut TransactionDraft) {
|
||||
let mut outputs: HashMap<Address, u64> = HashMap::new();
|
||||
for recipient in &self.recipients {
|
||||
outputs.insert(
|
||||
Address::from_str(&recipient.address.value).expect("Checked before"),
|
||||
recipient.amount().expect("Checked before"),
|
||||
);
|
||||
}
|
||||
draft.outputs = outputs;
|
||||
draft.inputs = self
|
||||
.coins
|
||||
.iter()
|
||||
.filter_map(|(coin, selected)| if *selected { Some(*coin) } else { None })
|
||||
.collect();
|
||||
draft.generated = self.generated.clone();
|
||||
}
|
||||
|
||||
fn view<'a>(&'a self, _cache: &'a Cache) -> Element<'a, view::Message> {
|
||||
view::spend::step::choose_recipients_view(
|
||||
fn view<'a>(&'a self, cache: &'a Cache) -> Element<'a, view::Message> {
|
||||
view::spend::step::create_spend_tx(
|
||||
cache,
|
||||
&self.balance_available,
|
||||
self.recipients
|
||||
.iter()
|
||||
@ -143,6 +302,11 @@ impl Step for ChooseRecipients {
|
||||
),
|
||||
self.is_valid,
|
||||
self.is_duplicate,
|
||||
self.timelock,
|
||||
&self.coins,
|
||||
self.amount_left_to_select.as_ref(),
|
||||
&self.feerate,
|
||||
self.warning.as_ref(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -222,211 +386,6 @@ impl Recipient {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ChooseCoins {
|
||||
descriptor: LianaDescriptor,
|
||||
timelock: u16,
|
||||
coins: Vec<(Coin, bool)>,
|
||||
recipients: Vec<(Address, Amount)>,
|
||||
|
||||
amount_left_to_select: Option<Amount>,
|
||||
feerate: form::Value<String>,
|
||||
generated: Option<Psbt>,
|
||||
warning: Option<Error>,
|
||||
}
|
||||
|
||||
impl ChooseCoins {
|
||||
pub fn new(
|
||||
descriptor: LianaDescriptor,
|
||||
coins: Vec<Coin>,
|
||||
timelock: u16,
|
||||
blockheight: u32,
|
||||
) -> Self {
|
||||
let mut coins: Vec<(Coin, bool)> = coins
|
||||
.into_iter()
|
||||
.filter_map(|c| {
|
||||
if c.spend_info.is_none() {
|
||||
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 {
|
||||
descriptor,
|
||||
timelock,
|
||||
coins,
|
||||
recipients: Vec::new(),
|
||||
feerate: form::Value::default(),
|
||||
generated: None,
|
||||
warning: None,
|
||||
amount_left_to_select: None,
|
||||
}
|
||||
}
|
||||
|
||||
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>() {
|
||||
Ok(f) => f,
|
||||
Err(_) => {
|
||||
self.amount_left_to_select = None;
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// The coins to be included in this transaction.
|
||||
let selected_coins: Vec<_> = self
|
||||
.coins
|
||||
.iter()
|
||||
.filter_map(|(c, selected)| if *selected { Some(c) } else { None })
|
||||
.collect();
|
||||
|
||||
// A dummy representation of the transaction that will be computed, for
|
||||
// the purpose of computing its size in order to anticipate the fees needed.
|
||||
// NOTE: we make the conservative estimation a change output will always be
|
||||
// needed.
|
||||
let tx_template = bitcoin::Transaction {
|
||||
version: 2,
|
||||
lock_time: bitcoin::PackedLockTime(0),
|
||||
input: selected_coins
|
||||
.iter()
|
||||
.map(|_| bitcoin::TxIn::default())
|
||||
.collect(),
|
||||
output: self
|
||||
.recipients
|
||||
.iter()
|
||||
.map(|(addr, amount)| bitcoin::TxOut {
|
||||
script_pubkey: addr.script_pubkey(),
|
||||
value: amount.to_sat(),
|
||||
})
|
||||
.collect(),
|
||||
};
|
||||
// nValue size + scriptPubKey CompactSize + OP_0 + PUSH32 + <wit program>
|
||||
const CHANGE_TXO_SIZE: usize = 8 + 1 + 1 + 1 + 32;
|
||||
let satisfaction_vsize = self.descriptor.max_sat_weight() / 4;
|
||||
let transaction_size =
|
||||
tx_template.vsize() + satisfaction_vsize * tx_template.input.len() + CHANGE_TXO_SIZE;
|
||||
|
||||
// Now the calculation of the amount left to be selected by the user is a simple
|
||||
// substraction between the value needed by the transaction to be created and the
|
||||
// value that was selected already.
|
||||
let selected_amount = selected_coins.iter().map(|c| c.amount.to_sat()).sum();
|
||||
let output_sum: u64 = tx_template.output.iter().map(|o| o.value).sum();
|
||||
let needed_amount: u64 = transaction_size as u64 * feerate + output_sum;
|
||||
self.amount_left_to_select = Some(Amount::from_sat(
|
||||
needed_amount.saturating_sub(selected_amount),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
impl Step for ChooseCoins {
|
||||
fn load(&mut self, draft: &TransactionDraft) {
|
||||
self.warning = None;
|
||||
self.recipients = draft
|
||||
.outputs
|
||||
.iter()
|
||||
.map(|(k, v)| (k.clone(), Amount::from_sat(*v)))
|
||||
.collect();
|
||||
self.amount_left_to_select();
|
||||
}
|
||||
|
||||
fn apply(&self, draft: &mut TransactionDraft) {
|
||||
draft.inputs = self
|
||||
.coins
|
||||
.iter()
|
||||
.filter_map(|(coin, selected)| if *selected { Some(*coin) } else { None })
|
||||
.collect();
|
||||
draft.generated = self.generated.clone();
|
||||
}
|
||||
|
||||
fn update(
|
||||
&mut self,
|
||||
daemon: Arc<dyn Daemon + Sync + Send>,
|
||||
_cache: &Cache,
|
||||
draft: &TransactionDraft,
|
||||
message: Message,
|
||||
) -> Command<Message> {
|
||||
match message {
|
||||
Message::View(view::Message::CreateSpend(view::CreateSpendMessage::FeerateEdited(
|
||||
s,
|
||||
))) => {
|
||||
if s.parse::<u64>().is_ok() {
|
||||
self.feerate.value = s;
|
||||
self.feerate.valid = true;
|
||||
self.amount_left_to_select();
|
||||
} else if s.is_empty() {
|
||||
self.feerate.value = "".to_string();
|
||||
self.feerate.valid = true;
|
||||
self.amount_left_to_select = None;
|
||||
} else {
|
||||
self.feerate.valid = false;
|
||||
self.amount_left_to_select = None;
|
||||
}
|
||||
self.warning = None;
|
||||
}
|
||||
Message::View(view::Message::CreateSpend(view::CreateSpendMessage::Generate)) => {
|
||||
let inputs: Vec<OutPoint> = self
|
||||
.coins
|
||||
.iter()
|
||||
.filter_map(
|
||||
|(coin, selected)| if *selected { Some(coin.outpoint) } else { None },
|
||||
)
|
||||
.collect();
|
||||
let outputs = draft.outputs.clone();
|
||||
let feerate_vb = self.feerate.value.parse::<u64>().unwrap_or(0);
|
||||
self.warning = None;
|
||||
return Command::perform(
|
||||
async move {
|
||||
daemon
|
||||
.create_spend_tx(&inputs, &outputs, feerate_vb)
|
||||
.map(|res| res.psbt)
|
||||
.map_err(|e| e.into())
|
||||
},
|
||||
Message::Psbt,
|
||||
);
|
||||
}
|
||||
Message::Psbt(res) => match res {
|
||||
Ok(psbt) => {
|
||||
self.generated = Some(psbt);
|
||||
return Command::perform(async {}, |_| Message::View(view::Message::Next));
|
||||
}
|
||||
Err(e) => self.warning = Some(e),
|
||||
},
|
||||
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();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Command::none()
|
||||
}
|
||||
|
||||
fn view<'a>(&'a self, cache: &'a Cache) -> Element<'a, view::Message> {
|
||||
view::spend::step::choose_coins_view(
|
||||
cache,
|
||||
self.timelock,
|
||||
&self.coins,
|
||||
self.amount_left_to_select.as_ref(),
|
||||
&self.feerate,
|
||||
self.warning.as_ref(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SaveSpend {
|
||||
wallet: Arc<Wallet>,
|
||||
spend: Option<detail::SpendTxState>,
|
||||
@ -460,7 +419,6 @@ impl Step for SaveSpend {
|
||||
&mut self,
|
||||
daemon: Arc<dyn Daemon + Sync + Send>,
|
||||
cache: &Cache,
|
||||
_draft: &TransactionDraft,
|
||||
message: Message,
|
||||
) -> Command<Message> {
|
||||
if let Some(spend) = &mut self.spend {
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
use iced::{Alignment, Length};
|
||||
use iced::{
|
||||
alignment,
|
||||
widget::{checkbox, scrollable, Space},
|
||||
Alignment, Length,
|
||||
};
|
||||
|
||||
use liana::miniscript::bitcoin::Amount;
|
||||
|
||||
use liana_ui::{
|
||||
color,
|
||||
component::{
|
||||
amount::*,
|
||||
badge, button, form,
|
||||
text::{text, Text},
|
||||
},
|
||||
component::{amount::*, badge, button, form, text::*},
|
||||
icon, theme,
|
||||
util::Collection,
|
||||
widget::*,
|
||||
@ -18,67 +18,139 @@ use crate::{
|
||||
app::{
|
||||
cache::Cache,
|
||||
error::Error,
|
||||
view::{coins, message::*, modal},
|
||||
menu::Menu,
|
||||
view::{coins, dashboard, message::*},
|
||||
},
|
||||
daemon::model::{remaining_sequence, Coin},
|
||||
};
|
||||
|
||||
pub fn choose_recipients_view<'a>(
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn create_spend_tx<'a>(
|
||||
cache: &'a Cache,
|
||||
balance_available: &'a Amount,
|
||||
recipients: Vec<Element<'a, Message>>,
|
||||
total_amount: Amount,
|
||||
is_valid: bool,
|
||||
duplicate: bool,
|
||||
timelock: u16,
|
||||
coins: &[(Coin, bool)],
|
||||
amount_left: Option<&Amount>,
|
||||
feerate: &form::Value<String>,
|
||||
error: Option<&Error>,
|
||||
) -> Element<'a, Message> {
|
||||
modal(
|
||||
false,
|
||||
None,
|
||||
dashboard(
|
||||
&Menu::CreateSpendTx,
|
||||
cache,
|
||||
error,
|
||||
Column::new()
|
||||
.push(text("Choose recipients").bold().size(50))
|
||||
.push(h3("Send"))
|
||||
.push(
|
||||
Column::new()
|
||||
.push(Column::with_children(recipients).spacing(10))
|
||||
.push(
|
||||
button::transparent(Some(icon::plus_icon()), "Add recipient")
|
||||
.on_press(Message::CreateSpend(CreateSpendMessage::AddRecipient)),
|
||||
Row::new()
|
||||
.push_maybe(if duplicate {
|
||||
Some(
|
||||
Container::new(
|
||||
text("Two recipient addresses are the same")
|
||||
.style(color::RED),
|
||||
)
|
||||
.padding(10),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
})
|
||||
.push(Space::with_width(Length::Fill))
|
||||
.push(
|
||||
button::secondary(Some(icon::plus_icon()), "Add recipient")
|
||||
.on_press(Message::CreateSpend(
|
||||
CreateSpendMessage::AddRecipient,
|
||||
)),
|
||||
),
|
||||
)
|
||||
.padding(10)
|
||||
.max_width(1000)
|
||||
.spacing(10),
|
||||
.spacing(20),
|
||||
)
|
||||
.spacing(20)
|
||||
.align_items(Alignment::Center),
|
||||
Some(
|
||||
Container::new(
|
||||
.push(
|
||||
Row::new()
|
||||
.push(
|
||||
Row::new()
|
||||
.push(Container::new(p1_bold("Fee rate")).padding(10))
|
||||
.spacing(10)
|
||||
.push(
|
||||
form::Form::new("Feerate (sat/vbyte)", feerate, move |msg| {
|
||||
Message::CreateSpend(CreateSpendMessage::FeerateEdited(msg))
|
||||
})
|
||||
.warning("Invalid feerate")
|
||||
.size(20)
|
||||
.padding(10),
|
||||
)
|
||||
.width(Length::FillPortion(1)),
|
||||
)
|
||||
.push(Space::with_width(Length::FillPortion(1))),
|
||||
)
|
||||
.push(
|
||||
Container::new(
|
||||
Column::new()
|
||||
.spacing(10)
|
||||
.push(
|
||||
Row::new()
|
||||
.align_items(Alignment::Center)
|
||||
.push(p1_bold("Coins selection").width(Length::Fill))
|
||||
.push(Container::new(if let Some(amount_left) = amount_left {
|
||||
Row::new()
|
||||
.spacing(5)
|
||||
.push(amount_with_size(amount_left, P2_SIZE))
|
||||
.push(p2_regular("left to select").style(color::GREY_3))
|
||||
} else {
|
||||
Row::new()
|
||||
.push(text("Feerate needs to be set.").style(color::GREY_3))
|
||||
}))
|
||||
.width(Length::Fill),
|
||||
)
|
||||
.push(
|
||||
Container::new(scrollable(coins.iter().enumerate().fold(
|
||||
Column::new().spacing(10),
|
||||
|col, (i, (coin, selected))| {
|
||||
col.push(coin_list_view(
|
||||
i,
|
||||
coin,
|
||||
timelock,
|
||||
cache.blockheight as u32,
|
||||
*selected,
|
||||
))
|
||||
},
|
||||
)))
|
||||
.max_height(300),
|
||||
),
|
||||
)
|
||||
.padding(20)
|
||||
.style(theme::Card::Simple),
|
||||
)
|
||||
.push(
|
||||
Row::new()
|
||||
.spacing(20)
|
||||
.align_items(Alignment::Center)
|
||||
.push(Space::with_width(Length::Fill))
|
||||
.push(
|
||||
Container::new(
|
||||
Row::new()
|
||||
.align_items(Alignment::Center)
|
||||
.spacing(5)
|
||||
.push(text(format!("{}", total_amount)).bold())
|
||||
.push(text(format!("/ {}", balance_available))),
|
||||
)
|
||||
.width(Length::Fill),
|
||||
button::primary(None, "Clear")
|
||||
.on_press(Message::Menu(Menu::CreateSpendTx))
|
||||
.width(Length::Units(100)),
|
||||
)
|
||||
.push_maybe(if duplicate {
|
||||
Some(text("Two recipient addresses are the same").style(color::ORANGE))
|
||||
} else {
|
||||
None
|
||||
})
|
||||
.push(if is_valid && total_amount < *balance_available {
|
||||
button::primary(None, "Next")
|
||||
.on_press(Message::Next)
|
||||
.width(Length::Units(100))
|
||||
} else {
|
||||
button::primary(None, "Next").width(Length::Units(100))
|
||||
}),
|
||||
.push(
|
||||
if is_valid
|
||||
&& total_amount < *balance_available
|
||||
&& Some(&Amount::from_sat(0)) == amount_left
|
||||
{
|
||||
button::primary(None, "Next")
|
||||
.on_press(Message::CreateSpend(CreateSpendMessage::Generate))
|
||||
.width(Length::Units(100))
|
||||
} else {
|
||||
button::primary(None, "Next").width(Length::Units(100))
|
||||
},
|
||||
),
|
||||
)
|
||||
.style(theme::Container::Foreground)
|
||||
.padding(20),
|
||||
),
|
||||
.push(Space::with_height(Length::Units(20)))
|
||||
.spacing(20),
|
||||
)
|
||||
}
|
||||
|
||||
@ -87,106 +159,60 @@ pub fn recipient_view<'a>(
|
||||
address: &form::Value<String>,
|
||||
amount: &form::Value<String>,
|
||||
) -> Element<'a, CreateSpendMessage> {
|
||||
Row::new()
|
||||
.push(
|
||||
form::Form::new("Address", address, move |msg| {
|
||||
CreateSpendMessage::RecipientEdited(index, "address", msg)
|
||||
})
|
||||
.warning("Invalid address (maybe it is for another network?)")
|
||||
.size(20)
|
||||
.padding(10),
|
||||
)
|
||||
.push(
|
||||
Container::new(
|
||||
form::Form::new("Amount", amount, move |msg| {
|
||||
CreateSpendMessage::RecipientEdited(index, "amount", msg)
|
||||
})
|
||||
.warning("Invalid amount. Must be > 0.00005000 BTC.")
|
||||
.size(20)
|
||||
.padding(10),
|
||||
)
|
||||
.width(Length::Units(300)),
|
||||
)
|
||||
.spacing(5)
|
||||
.push(
|
||||
button::transparent(Some(icon::trash_icon()), "")
|
||||
.on_press(CreateSpendMessage::DeleteRecipient(index))
|
||||
.width(Length::Shrink),
|
||||
)
|
||||
.width(Length::Fill)
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn choose_coins_view<'a>(
|
||||
cache: &Cache,
|
||||
timelock: u16,
|
||||
coins: &[(Coin, bool)],
|
||||
amount_left: Option<&Amount>,
|
||||
feerate: &form::Value<String>,
|
||||
error: Option<&Error>,
|
||||
) -> Element<'a, Message> {
|
||||
modal(
|
||||
true,
|
||||
error,
|
||||
Container::new(
|
||||
Column::new()
|
||||
.push(text("Choose coins and feerate").bold().size(50))
|
||||
.spacing(10)
|
||||
.push(
|
||||
Container::new(
|
||||
form::Form::new("Feerate (sat/vbyte)", feerate, move |msg| {
|
||||
Message::CreateSpend(CreateSpendMessage::FeerateEdited(msg))
|
||||
})
|
||||
.warning("Invalid feerate")
|
||||
.size(20)
|
||||
.padding(10),
|
||||
)
|
||||
.width(Length::Units(250)),
|
||||
Row::new().push(Space::with_width(Length::Fill)).push(
|
||||
Button::new(icon::cross_icon())
|
||||
.style(theme::Button::Transparent)
|
||||
.on_press(CreateSpendMessage::DeleteRecipient(index))
|
||||
.width(Length::Shrink),
|
||||
),
|
||||
)
|
||||
.push(
|
||||
Column::new()
|
||||
.padding(10)
|
||||
.spacing(10)
|
||||
.push(coins.iter().enumerate().fold(
|
||||
Column::new().spacing(10),
|
||||
|col, (i, (coin, selected))| {
|
||||
col.push(coin_list_view(
|
||||
i,
|
||||
coin,
|
||||
timelock,
|
||||
cache.blockheight as u32,
|
||||
*selected,
|
||||
))
|
||||
},
|
||||
)),
|
||||
)
|
||||
.spacing(20)
|
||||
.align_items(Alignment::Center),
|
||||
Some(
|
||||
Container::new(
|
||||
Row::new()
|
||||
.align_items(Alignment::Center)
|
||||
.align_items(Alignment::Start)
|
||||
.spacing(10)
|
||||
.push(
|
||||
Container::new(if let Some(amount_left) = amount_left {
|
||||
Row::new()
|
||||
.spacing(5)
|
||||
.push(text("Amount left to select:"))
|
||||
.push(text(amount_left.to_string()).bold())
|
||||
} else {
|
||||
Row::new().push(text("Feerate needs to be set."))
|
||||
})
|
||||
.width(Length::Fill),
|
||||
Container::new(p1_bold("Pay to"))
|
||||
.align_x(alignment::Horizontal::Right)
|
||||
.padding(10)
|
||||
.width(Length::Units(80)),
|
||||
)
|
||||
.push(if Some(&Amount::from_sat(0)) == amount_left {
|
||||
button::primary(None, "Next")
|
||||
.on_press(Message::CreateSpend(CreateSpendMessage::Generate))
|
||||
.width(Length::Units(100))
|
||||
} else {
|
||||
button::primary(None, "Next").width(Length::Units(100))
|
||||
}),
|
||||
.push(
|
||||
form::Form::new("Address", address, move |msg| {
|
||||
CreateSpendMessage::RecipientEdited(index, "address", msg)
|
||||
})
|
||||
.warning("Invalid address (maybe it is for another network?)")
|
||||
.size(20)
|
||||
.padding(10),
|
||||
),
|
||||
)
|
||||
.style(theme::Container::Foreground)
|
||||
.padding(20),
|
||||
),
|
||||
.push(
|
||||
Row::new()
|
||||
.align_items(Alignment::Start)
|
||||
.spacing(10)
|
||||
.push(
|
||||
Container::new(p1_bold("Amount"))
|
||||
.padding(10)
|
||||
.align_x(alignment::Horizontal::Right)
|
||||
.width(Length::Units(80)),
|
||||
)
|
||||
.push(
|
||||
form::Form::new("ex: 0.001", amount, move |msg| {
|
||||
CreateSpendMessage::RecipientEdited(index, "amount", msg)
|
||||
})
|
||||
.warning("Invalid amount. Must be > 0.00005000 BTC.")
|
||||
.size(20)
|
||||
.padding(10),
|
||||
)
|
||||
.width(Length::Fill),
|
||||
),
|
||||
)
|
||||
.padding(20)
|
||||
.style(theme::Card::Simple)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn coin_list_view<'a>(
|
||||
@ -196,37 +222,28 @@ fn coin_list_view<'a>(
|
||||
blockheight: u32,
|
||||
selected: bool,
|
||||
) -> Element<'a, Message> {
|
||||
Container::new(
|
||||
Button::new(
|
||||
Row::new()
|
||||
.push(
|
||||
Row::new()
|
||||
.push(
|
||||
Row::new()
|
||||
.push(if selected {
|
||||
icon::square_check_icon()
|
||||
} else {
|
||||
icon::square_icon()
|
||||
})
|
||||
.push(badge::coin())
|
||||
.push(if coin.spend_info.is_some() {
|
||||
badge::spent()
|
||||
} else if coin.block_height.is_none() {
|
||||
badge::unconfirmed()
|
||||
} else {
|
||||
let seq = remaining_sequence(coin, blockheight, timelock);
|
||||
coins::coin_sequence_label(seq, timelock as u32)
|
||||
})
|
||||
.spacing(10)
|
||||
.align_items(Alignment::Center)
|
||||
.width(Length::Fill),
|
||||
)
|
||||
.push(amount(&coin.amount))
|
||||
.push(checkbox("", selected, move |_| {
|
||||
Message::CreateSpend(CreateSpendMessage::SelectCoin(i))
|
||||
}))
|
||||
.push(if coin.spend_info.is_some() {
|
||||
badge::spent()
|
||||
} else if coin.block_height.is_none() {
|
||||
badge::unconfirmed()
|
||||
} else {
|
||||
let seq = remaining_sequence(coin, blockheight, timelock);
|
||||
coins::coin_sequence_label(seq, timelock as u32)
|
||||
})
|
||||
.spacing(10)
|
||||
.align_items(Alignment::Center)
|
||||
.spacing(20),
|
||||
.width(Length::Fill),
|
||||
)
|
||||
.padding(10)
|
||||
.on_press(Message::CreateSpend(CreateSpendMessage::SelectCoin(i)))
|
||||
.style(theme::Button::TransparentBorder),
|
||||
)
|
||||
.style(theme::Container::Card(theme::Card::Simple))
|
||||
.into()
|
||||
.push(amount(&coin.amount))
|
||||
// give some space for the scroll bar without using padding
|
||||
.push(Space::with_width(Length::Units(0)))
|
||||
.align_items(Alignment::Center)
|
||||
.spacing(20)
|
||||
.into()
|
||||
}
|
||||
|
||||
@ -418,20 +418,30 @@ pub struct CheckBox {}
|
||||
impl checkbox::StyleSheet for Theme {
|
||||
type Style = CheckBox;
|
||||
|
||||
fn active(&self, _style: &Self::Style, _is_selected: bool) -> checkbox::Appearance {
|
||||
checkbox::Appearance {
|
||||
background: iced::Color::TRANSPARENT.into(),
|
||||
border_width: 1.0,
|
||||
border_color: color::GREY_7,
|
||||
checkmark_color: color::GREEN,
|
||||
text_color: None,
|
||||
border_radius: 0.0,
|
||||
fn active(&self, _style: &Self::Style, is_selected: bool) -> checkbox::Appearance {
|
||||
if is_selected {
|
||||
checkbox::Appearance {
|
||||
background: color::GREEN.into(),
|
||||
border_width: 0.0,
|
||||
border_color: iced::Color::TRANSPARENT,
|
||||
checkmark_color: color::GREY_4,
|
||||
text_color: None,
|
||||
border_radius: 4.0,
|
||||
}
|
||||
} else {
|
||||
checkbox::Appearance {
|
||||
background: color::GREY_4.into(),
|
||||
border_width: 0.0,
|
||||
border_color: iced::Color::TRANSPARENT,
|
||||
checkmark_color: color::GREEN,
|
||||
text_color: None,
|
||||
border_radius: 4.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn hovered(&self, style: &Self::Style, is_selected: bool) -> checkbox::Appearance {
|
||||
let active = self.active(style, is_selected);
|
||||
checkbox::Appearance { ..active }
|
||||
self.active(style, is_selected)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user