828 lines
32 KiB
Rust
828 lines
32 KiB
Rust
use std::{
|
|
cmp::Ordering,
|
|
collections::{HashMap, HashSet},
|
|
iter::FromIterator,
|
|
str::FromStr,
|
|
sync::Arc,
|
|
};
|
|
|
|
use iced::{Command, Subscription};
|
|
use liana::{
|
|
commands::ListCoinsEntry,
|
|
descriptors::LianaDescriptor,
|
|
miniscript::bitcoin::{
|
|
address, psbt::Psbt, secp256k1, Address, Amount, Denomination, Network, OutPoint,
|
|
},
|
|
spend::{SpendCreationError, MAX_FEERATE},
|
|
};
|
|
|
|
use liana_ui::{component::form, widget::Element};
|
|
|
|
use crate::{
|
|
app::{cache::Cache, error::Error, message::Message, state::psbt, view, wallet::Wallet},
|
|
daemon::{
|
|
model::{remaining_sequence, Coin, CreateSpendResult, SpendTx},
|
|
Daemon,
|
|
},
|
|
};
|
|
|
|
/// See: https://github.com/wizardsardine/liana/blob/master/src/commands/mod.rs#L32
|
|
const DUST_OUTPUT_SATS: u64 = 5_000;
|
|
|
|
#[derive(Clone)]
|
|
pub struct TransactionDraft {
|
|
network: Network,
|
|
inputs: Vec<Coin>,
|
|
recipients: Vec<Recipient>,
|
|
generated: Option<(Psbt, Vec<String>)>,
|
|
batch_label: Option<String>,
|
|
labels: HashMap<String, String>,
|
|
}
|
|
|
|
impl TransactionDraft {
|
|
pub fn new(network: Network) -> Self {
|
|
Self {
|
|
network,
|
|
inputs: Vec::new(),
|
|
recipients: Vec::new(),
|
|
generated: None,
|
|
batch_label: None,
|
|
labels: HashMap::new(),
|
|
}
|
|
}
|
|
}
|
|
|
|
pub trait Step {
|
|
fn view<'a>(&'a self, cache: &'a Cache) -> Element<'a, view::Message>;
|
|
fn update(
|
|
&mut self,
|
|
daemon: Arc<dyn Daemon + Sync + Send>,
|
|
cache: &Cache,
|
|
message: Message,
|
|
) -> Command<Message>;
|
|
fn apply(&self, _draft: &mut TransactionDraft) {}
|
|
fn interrupt(&mut self) {}
|
|
fn load(&mut self, _draft: &TransactionDraft) {}
|
|
fn subscription(&self) -> Subscription<Message> {
|
|
Subscription::none()
|
|
}
|
|
}
|
|
|
|
pub struct DefineSpend {
|
|
recipients: Vec<Recipient>,
|
|
/// If set, this is the index of a recipient that should
|
|
/// receive the max amount.
|
|
send_max_to_recipient: Option<usize>,
|
|
/// Will be `true` if coins for spend were manually selected by user.
|
|
/// Otherwise, will be `false` (including for self-send).
|
|
is_user_coin_selection: bool,
|
|
is_valid: bool,
|
|
is_duplicate: bool,
|
|
|
|
network: Network,
|
|
descriptor: LianaDescriptor,
|
|
curve: secp256k1::Secp256k1<secp256k1::VerifyOnly>,
|
|
timelock: u16,
|
|
coins: Vec<(Coin, bool)>,
|
|
coins_labels: HashMap<String, String>,
|
|
batch_label: form::Value<String>,
|
|
amount_left_to_select: Option<Amount>,
|
|
feerate: form::Value<String>,
|
|
generated: Option<(Psbt, Vec<String>)>,
|
|
warning: Option<Error>,
|
|
}
|
|
|
|
impl DefineSpend {
|
|
pub fn new(
|
|
network: Network,
|
|
descriptor: LianaDescriptor,
|
|
coins: &[Coin],
|
|
timelock: u16,
|
|
) -> Self {
|
|
let coins: Vec<(Coin, bool)> = coins
|
|
.iter()
|
|
.filter_map(|c| {
|
|
if c.spend_info.is_none() && !c.is_immature {
|
|
Some((c.clone(), false))
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.collect();
|
|
|
|
Self {
|
|
network,
|
|
descriptor,
|
|
curve: secp256k1::Secp256k1::verification_only(),
|
|
timelock,
|
|
generated: None,
|
|
coins,
|
|
coins_labels: HashMap::new(),
|
|
batch_label: form::Value::default(),
|
|
recipients: vec![Recipient::default()],
|
|
send_max_to_recipient: None,
|
|
is_user_coin_selection: false, // Start with auto-selection until user edits selection.
|
|
is_valid: false,
|
|
is_duplicate: false,
|
|
feerate: form::Value::default(),
|
|
amount_left_to_select: None,
|
|
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 {
|
|
self.sort_coins(blockheight);
|
|
self
|
|
}
|
|
|
|
fn sort_coins(&mut self, blockheight: u32) {
|
|
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)
|
|
}
|
|
});
|
|
}
|
|
|
|
pub fn self_send(mut self) -> Self {
|
|
self.recipients = Vec::new();
|
|
self
|
|
}
|
|
|
|
// If `is_redraft`, the validation of recipients will take into account
|
|
// whether any should receive the max amount. Otherwise, all recipients
|
|
// will be fully validated.
|
|
fn form_values_are_valid(&self, is_redraft: bool) -> bool {
|
|
self.feerate.valid
|
|
&& !self.feerate.value.is_empty()
|
|
&& (self.batch_label.valid || self.recipients.len() < 2)
|
|
// Recipients will be empty for self-send.
|
|
&& self.recipients.iter().enumerate().all(|(i, r)|
|
|
r.valid() || (is_redraft && self.send_max_to_recipient == Some(i) && r.address_valid()))
|
|
}
|
|
|
|
fn exists_duplicate(&self) -> bool {
|
|
for (i, recipient) in self.recipients.iter().enumerate() {
|
|
if !recipient.address.value.is_empty()
|
|
&& self.recipients[..i]
|
|
.iter()
|
|
.any(|r| r.address.value == recipient.address.value)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
false
|
|
}
|
|
|
|
fn check_valid(&mut self) {
|
|
self.is_valid =
|
|
self.form_values_are_valid(false) && self.coins.iter().any(|(_, selected)| *selected);
|
|
self.is_duplicate = self.exists_duplicate();
|
|
}
|
|
/// redraft calculates the amount left to select and auto selects coins
|
|
/// if the user did not select a coin manually
|
|
fn redraft(&mut self, daemon: Arc<dyn Daemon + Sync + Send>) {
|
|
if !self.form_values_are_valid(true)
|
|
|| self.exists_duplicate()
|
|
|| self.recipients.is_empty()
|
|
{
|
|
// The current form details are not valid to draft a spend, so remove any previously
|
|
// calculated amount as it will no longer be valid and could be misleading, e.g. if
|
|
// the user removes the amount from one of the recipients.
|
|
// We can leave any coins selected as they will either be automatically updated
|
|
// as soon as the form is valid or the user has selected these specific coins and
|
|
// so we should not touch them.
|
|
self.amount_left_to_select = None;
|
|
// Remove any max amount from a recipient as it could be misleading.
|
|
if let Some(i) = self.send_max_to_recipient {
|
|
self.recipients
|
|
.get_mut(i)
|
|
.expect("max has been requested for this recipient so it must exist")
|
|
.update(
|
|
self.network,
|
|
view::CreateSpendMessage::RecipientEdited(i, "amount", "".to_string()),
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
|
|
let destinations: HashMap<Address<address::NetworkUnchecked>, u64> = self
|
|
.recipients
|
|
.iter()
|
|
.enumerate()
|
|
.filter_map(|(i, recipient)| {
|
|
// A recipient that receives the max should be treated as change for coin selection.
|
|
// Note that we only give a change output if its value is above the dust
|
|
// threshold, but a user can only send payments above the same dust threshold,
|
|
// so using change output to determine the max amount for a recipient will
|
|
// not prevent a value that could otherwise be entered manually by the user.
|
|
if self.send_max_to_recipient == Some(i) {
|
|
None
|
|
} else {
|
|
Some((
|
|
Address::from_str(&recipient.address.value).expect("Checked before"),
|
|
recipient.amount().expect("Checked before"),
|
|
))
|
|
}
|
|
})
|
|
.collect();
|
|
|
|
let recipient_with_max = if let Some(i) = self.send_max_to_recipient {
|
|
Some((
|
|
i,
|
|
self.recipients
|
|
.get_mut(i)
|
|
.expect("max has been requested for this recipient so it must exist"),
|
|
))
|
|
} else {
|
|
None
|
|
};
|
|
|
|
let outpoints = if self.is_user_coin_selection {
|
|
let outpoints: Vec<_> = self
|
|
.coins
|
|
.iter()
|
|
.filter_map(
|
|
|(c, selected)| {
|
|
if *selected {
|
|
Some(c.outpoint)
|
|
} else {
|
|
None
|
|
}
|
|
},
|
|
)
|
|
.collect();
|
|
if outpoints.is_empty() {
|
|
// If the user has deselected all coins, set any recipient's max amount to 0.
|
|
if let Some((i, recipient)) = recipient_with_max {
|
|
recipient.update(
|
|
self.network,
|
|
view::CreateSpendMessage::RecipientEdited(i, "amount", "0".to_string()),
|
|
);
|
|
}
|
|
// Simply set the amount left to select as the total destination value. Note this
|
|
// doesn't take account of the fee, but passing an empty list to `create_spend_tx`
|
|
// would use auto-selection and so we settle for this approximation.
|
|
self.amount_left_to_select = Some(Amount::from_sat(destinations.values().sum()));
|
|
return;
|
|
}
|
|
outpoints
|
|
} else if self.send_max_to_recipient.is_some() {
|
|
// If user has not selected coins, send the max available from all coins.
|
|
self.coins.iter().map(|(c, _)| c.outpoint).collect()
|
|
} else {
|
|
Vec::new() // pass empty list for auto-selection
|
|
};
|
|
|
|
// If sending the max to a recipient, use that recipient's address as the
|
|
// change address.
|
|
// Otherwise, use a fixed change address from the user's own wallet so that
|
|
// we don't increment the change index.
|
|
let change_address = if let Some((_, recipient)) = &recipient_with_max {
|
|
Address::from_str(&recipient.address.value)
|
|
.expect("Checked before")
|
|
.as_unchecked()
|
|
.clone()
|
|
} else {
|
|
self.descriptor
|
|
.change_descriptor()
|
|
.derive(0.into(), &self.curve)
|
|
.address(self.network)
|
|
.as_unchecked()
|
|
.clone()
|
|
};
|
|
|
|
let feerate_vb = self.feerate.value.parse::<u64>().expect("Checked before");
|
|
match tokio::runtime::Handle::current().block_on(async {
|
|
daemon
|
|
.create_spend_tx(
|
|
&outpoints,
|
|
&destinations,
|
|
feerate_vb,
|
|
Some(change_address.clone()),
|
|
)
|
|
.await
|
|
}) {
|
|
Ok(CreateSpendResult::Success { psbt, .. }) => {
|
|
self.warning = None;
|
|
if !self.is_user_coin_selection {
|
|
let selected_coins: Vec<OutPoint> = psbt
|
|
.unsigned_tx
|
|
.input
|
|
.iter()
|
|
.map(|c| c.previous_output)
|
|
.collect();
|
|
// Mark coins as selected.
|
|
for (coin, selected) in &mut self.coins {
|
|
*selected = selected_coins.contains(&coin.outpoint);
|
|
}
|
|
}
|
|
// As coin selection was successful, we can assume there is nothing left to select.
|
|
self.amount_left_to_select = Some(Amount::from_sat(0));
|
|
if let Some((i, recipient)) = recipient_with_max {
|
|
// If there's no change output, any excess must be below the dust threshold
|
|
// and so the max available for this recipient is 0.
|
|
let amount = psbt
|
|
.unsigned_tx
|
|
.output
|
|
.iter()
|
|
.find(|o| {
|
|
o.script_pubkey
|
|
== change_address.clone().assume_checked().script_pubkey()
|
|
})
|
|
.map(|change_output| change_output.value.to_btc())
|
|
.unwrap_or(0.0)
|
|
.to_string();
|
|
recipient.update(
|
|
self.network,
|
|
view::CreateSpendMessage::RecipientEdited(i, "amount", amount),
|
|
);
|
|
}
|
|
}
|
|
// For coin selection error (insufficient funds), do not make any changes to
|
|
// selected coins on screen and just show user how much is left to select.
|
|
// User can then either:
|
|
// - modify recipient amounts and/or feerate and let coin selection run again, or
|
|
// - select coins manually.
|
|
Ok(CreateSpendResult::InsufficientFunds { missing }) => {
|
|
self.amount_left_to_select = Some(Amount::from_sat(missing));
|
|
if let Some((i, recipient)) = recipient_with_max {
|
|
let amount = Amount::from_sat(if destinations.is_empty() {
|
|
// If there are no other recipients, then the missing value will
|
|
// be the amount left to select in order to create an output at the dust
|
|
// threshold. Therefore, set this recipient's amount to this value so
|
|
// that the information shown is consistent.
|
|
// Otherwise, there are already insufficient funds for the other
|
|
// recipients and so the max available for this recipient is 0.
|
|
DUST_OUTPUT_SATS
|
|
} else {
|
|
0
|
|
})
|
|
.to_btc()
|
|
.to_string();
|
|
recipient.update(
|
|
self.network,
|
|
view::CreateSpendMessage::RecipientEdited(i, "amount", amount),
|
|
);
|
|
}
|
|
}
|
|
Err(e) => {
|
|
self.warning = Some(e.into());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Step for DefineSpend {
|
|
fn update(
|
|
&mut self,
|
|
daemon: Arc<dyn Daemon + Sync + Send>,
|
|
cache: &Cache,
|
|
message: Message,
|
|
) -> Command<Message> {
|
|
match message {
|
|
Message::View(view::Message::CreateSpend(msg)) => {
|
|
match msg {
|
|
view::CreateSpendMessage::BatchLabelEdited(label) => {
|
|
self.batch_label.valid = label.len() <= 100;
|
|
self.batch_label.value = label;
|
|
}
|
|
view::CreateSpendMessage::Clear => {
|
|
*self = Self::new(
|
|
self.network,
|
|
self.descriptor.clone(),
|
|
self.coins
|
|
.iter()
|
|
.map(|(c, _)| c.clone())
|
|
.collect::<Vec<ListCoinsEntry>>()
|
|
.as_slice(),
|
|
self.timelock,
|
|
);
|
|
return Command::none();
|
|
}
|
|
view::CreateSpendMessage::AddRecipient => {
|
|
self.recipients.push(Recipient::default());
|
|
}
|
|
view::CreateSpendMessage::DeleteRecipient(i) => {
|
|
self.recipients.remove(i);
|
|
if self.recipients.len() < 2 {
|
|
self.batch_label.valid = true;
|
|
self.batch_label.value = "".to_string();
|
|
}
|
|
if let Some(j) = self.send_max_to_recipient {
|
|
match j.cmp(&i) {
|
|
Ordering::Equal => {
|
|
self.send_max_to_recipient = None;
|
|
}
|
|
Ordering::Greater => {
|
|
self.send_max_to_recipient = Some(
|
|
j.checked_sub(1)
|
|
.expect("j must be greater than 0 in this case"),
|
|
);
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
view::CreateSpendMessage::RecipientEdited(i, _, _) => {
|
|
self.recipients
|
|
.get_mut(i)
|
|
.unwrap()
|
|
.update(cache.network, msg);
|
|
}
|
|
|
|
view::CreateSpendMessage::FeerateEdited(s) => {
|
|
if let Ok(value) = s.parse::<u64>() {
|
|
self.feerate.value = s;
|
|
self.feerate.valid = value != 0 && value <= MAX_FEERATE;
|
|
} else if s.is_empty() {
|
|
self.feerate.value = "".to_string();
|
|
self.feerate.valid = true;
|
|
} else {
|
|
self.feerate.valid = false;
|
|
}
|
|
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<address::NetworkUnchecked>, 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, None)
|
|
.await
|
|
.map_err(|e| e.into())
|
|
.and_then(|res| match res {
|
|
CreateSpendResult::Success { psbt, warnings } => {
|
|
Ok((psbt, warnings))
|
|
}
|
|
CreateSpendResult::InsufficientFunds { missing } => {
|
|
Err(SpendCreationError::CoinSelection(
|
|
liana::spend::InsufficientFunds { missing },
|
|
)
|
|
.into())
|
|
}
|
|
})
|
|
},
|
|
Message::Psbt,
|
|
);
|
|
}
|
|
view::CreateSpendMessage::SelectCoin(i) => {
|
|
if let Some(coin) = self.coins.get_mut(i) {
|
|
coin.1 = !coin.1;
|
|
// Once user edits selection, auto-selection can no longer be used.
|
|
self.is_user_coin_selection = true;
|
|
}
|
|
}
|
|
view::CreateSpendMessage::SendMaxToRecipient(i) => {
|
|
if self.recipients.get(i).is_some() {
|
|
if self.send_max_to_recipient == Some(i) {
|
|
// If already set to this recipient, then unset it.
|
|
self.send_max_to_recipient = None;
|
|
} else {
|
|
// Either it's set to some other recipient or not at all.
|
|
self.send_max_to_recipient = Some(i);
|
|
};
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
|
|
// Attempt to select coins automatically if:
|
|
// - all form values have been added and validated
|
|
// - not a self-send
|
|
// - user has not yet selected coins manually
|
|
self.redraft(daemon);
|
|
self.check_valid();
|
|
}
|
|
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::Labels(res) => match res {
|
|
Ok(labels) => {
|
|
self.coins_labels = labels;
|
|
}
|
|
Err(e) => self.warning = Some(e),
|
|
},
|
|
Message::Coins(res) => match res {
|
|
Ok(coins) => {
|
|
let selected: HashSet<OutPoint> =
|
|
HashSet::from_iter(self.coins.iter().filter_map(|(c, selected)| {
|
|
if *selected {
|
|
Some(c.outpoint)
|
|
} else {
|
|
None
|
|
}
|
|
}));
|
|
self.coins = coins
|
|
.into_iter()
|
|
.filter_map(|coin| {
|
|
if coin.spend_info.is_none() && !coin.is_immature {
|
|
let selected = selected.contains(&coin.outpoint);
|
|
Some((coin, selected))
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.collect();
|
|
self.sort_coins(cache.blockheight as u32);
|
|
// In case some selected coins are not spendable anymore and
|
|
// new coins make more sense to be selected. A redraft is triggered
|
|
// if all forms are valid (checked in the redraft method)
|
|
self.redraft(daemon);
|
|
self.check_valid();
|
|
}
|
|
Err(e) => self.warning = Some(e),
|
|
},
|
|
_ => {}
|
|
};
|
|
Command::none()
|
|
}
|
|
|
|
fn apply(&self, draft: &mut TransactionDraft) {
|
|
draft.inputs = self
|
|
.coins
|
|
.iter()
|
|
.filter_map(|(coin, selected)| if *selected { Some(coin) } else { None })
|
|
.cloned()
|
|
.collect();
|
|
if let Some((psbt, _)) = &self.generated {
|
|
draft.labels.clone_from(&self.coins_labels);
|
|
for (i, output) in psbt.unsigned_tx.output.iter().enumerate() {
|
|
if let Some(label) = self
|
|
.recipients
|
|
.iter()
|
|
.find(|recipient| {
|
|
!recipient.label.value.is_empty()
|
|
&& Address::from_str(&recipient.address.value)
|
|
.unwrap()
|
|
.payload()
|
|
.matches_script_pubkey(&output.script_pubkey)
|
|
&& output.value.to_sat() == recipient.amount().unwrap()
|
|
})
|
|
.map(|recipient| recipient.label.value.to_string())
|
|
{
|
|
draft.labels.insert(
|
|
OutPoint {
|
|
txid: psbt.unsigned_tx.txid(),
|
|
vout: i as u32,
|
|
}
|
|
.to_string(),
|
|
label,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
draft.recipients.clone_from(&self.recipients);
|
|
if self.recipients.len() > 1 {
|
|
draft.batch_label = Some(self.batch_label.value.clone());
|
|
}
|
|
draft.generated.clone_from(&self.generated);
|
|
}
|
|
|
|
fn view<'a>(&'a self, cache: &'a Cache) -> Element<'a, view::Message> {
|
|
view::spend::create_spend_tx(
|
|
cache,
|
|
self.recipients
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(i, recipient)| {
|
|
recipient
|
|
.view(i, self.send_max_to_recipient == Some(i))
|
|
.map(view::Message::CreateSpend)
|
|
})
|
|
.collect(),
|
|
self.is_valid,
|
|
self.is_duplicate,
|
|
self.timelock,
|
|
&self.coins,
|
|
&self.coins_labels,
|
|
&self.batch_label,
|
|
self.amount_left_to_select.as_ref(),
|
|
&self.feerate,
|
|
self.warning.as_ref(),
|
|
)
|
|
}
|
|
}
|
|
|
|
#[derive(Default, Clone)]
|
|
struct Recipient {
|
|
label: form::Value<String>,
|
|
address: form::Value<String>,
|
|
amount: form::Value<String>,
|
|
}
|
|
|
|
impl Recipient {
|
|
fn amount(&self) -> Result<u64, Error> {
|
|
if self.amount.value.is_empty() {
|
|
return Err(Error::Unexpected("Amount should be non-zero".to_string()));
|
|
}
|
|
|
|
let amount = Amount::from_str_in(&self.amount.value, Denomination::Bitcoin)
|
|
.map_err(|_| Error::Unexpected("cannot parse output amount".to_string()))?;
|
|
|
|
if amount.to_sat() == 0 {
|
|
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.payload().script_pubkey().dust_value() {
|
|
return Err(Error::Unexpected(
|
|
"Amount must be superior to script dust value".to_string(),
|
|
));
|
|
}
|
|
}
|
|
|
|
Ok(amount.to_sat())
|
|
}
|
|
|
|
fn address_valid(&self) -> bool {
|
|
!self.address.value.is_empty() && self.address.valid
|
|
}
|
|
|
|
fn valid(&self) -> bool {
|
|
self.address_valid()
|
|
&& !self.amount.value.is_empty()
|
|
&& self.amount.valid
|
|
&& self.label.valid
|
|
}
|
|
|
|
fn update(&mut self, network: Network, message: view::CreateSpendMessage) {
|
|
match message {
|
|
view::CreateSpendMessage::RecipientEdited(_, "address", address) => {
|
|
self.address.value = address;
|
|
if let Ok(address) = Address::from_str(&self.address.value) {
|
|
self.address.valid = address.is_valid_for_network(network);
|
|
if !self.amount.value.is_empty() {
|
|
self.amount.valid = self.amount().is_ok();
|
|
}
|
|
} else if self.address.value.is_empty() {
|
|
// Make the error disappear if we deleted the invalid address
|
|
self.address.valid = true;
|
|
} else {
|
|
self.address.valid = false;
|
|
}
|
|
}
|
|
view::CreateSpendMessage::RecipientEdited(_, "amount", amount) => {
|
|
self.amount.value = amount;
|
|
if !self.amount.value.is_empty() {
|
|
self.amount.valid = self.amount().is_ok();
|
|
} else {
|
|
// Make the error disappear if we deleted the invalid amount
|
|
self.amount.valid = true;
|
|
}
|
|
}
|
|
view::CreateSpendMessage::RecipientEdited(_, "label", label) => {
|
|
self.label.valid = label.len() <= 100;
|
|
self.label.value = label;
|
|
}
|
|
_ => {}
|
|
};
|
|
}
|
|
|
|
fn view(&self, i: usize, is_max_selected: bool) -> Element<view::CreateSpendMessage> {
|
|
view::spend::recipient_view(i, &self.address, &self.amount, &self.label, is_max_selected)
|
|
}
|
|
}
|
|
|
|
pub struct SaveSpend {
|
|
wallet: Arc<Wallet>,
|
|
spend: Option<(psbt::PsbtState, Vec<String>)>,
|
|
curve: secp256k1::Secp256k1<secp256k1::VerifyOnly>,
|
|
}
|
|
|
|
impl SaveSpend {
|
|
pub fn new(wallet: Arc<Wallet>) -> Self {
|
|
Self {
|
|
wallet,
|
|
spend: None,
|
|
curve: secp256k1::Secp256k1::verification_only(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Step for SaveSpend {
|
|
fn load(&mut self, draft: &TransactionDraft) {
|
|
let (psbt, warnings) = draft.generated.clone().unwrap();
|
|
let mut tx = SpendTx::new(
|
|
None,
|
|
psbt,
|
|
draft.inputs.clone(),
|
|
&self.wallet.main_descriptor,
|
|
&self.curve,
|
|
draft.network,
|
|
);
|
|
tx.labels.clone_from(&draft.labels);
|
|
|
|
if tx.is_batch() {
|
|
if let Some(label) = &draft.batch_label {
|
|
tx.labels
|
|
.insert(tx.psbt.unsigned_tx.txid().to_string(), label.clone());
|
|
}
|
|
} else if let Some(recipient) = draft.recipients.first() {
|
|
if !recipient.label.value.is_empty() {
|
|
let label = recipient.label.value.clone();
|
|
tx.labels
|
|
.insert(tx.psbt.unsigned_tx.txid().to_string(), label);
|
|
}
|
|
}
|
|
|
|
self.spend = Some((
|
|
psbt::PsbtState::new(self.wallet.clone(), tx, false),
|
|
warnings,
|
|
));
|
|
}
|
|
|
|
fn interrupt(&mut self) {
|
|
if let Some((psbt_state, _)) = &mut self.spend {
|
|
psbt_state.interrupt()
|
|
}
|
|
}
|
|
|
|
fn subscription(&self) -> Subscription<Message> {
|
|
if let Some((psbt_state, _)) = &self.spend {
|
|
psbt_state.subscription()
|
|
} else {
|
|
Subscription::none()
|
|
}
|
|
}
|
|
|
|
fn update(
|
|
&mut self,
|
|
daemon: Arc<dyn Daemon + Sync + Send>,
|
|
cache: &Cache,
|
|
message: Message,
|
|
) -> Command<Message> {
|
|
if let Some((psbt_state, _)) = &mut self.spend {
|
|
psbt_state.update(daemon, cache, message)
|
|
} else {
|
|
Command::none()
|
|
}
|
|
}
|
|
|
|
fn view<'a>(&'a self, cache: &'a Cache) -> Element<'a, view::Message> {
|
|
let (psbt_state, warnings) = self.spend.as_ref().unwrap();
|
|
let content = view::spend::spend_view(
|
|
cache,
|
|
&psbt_state.tx,
|
|
warnings,
|
|
psbt_state.saved,
|
|
&psbt_state.desc_policy,
|
|
&psbt_state.wallet.keys_aliases,
|
|
psbt_state.labels_edited.cache(),
|
|
cache.network,
|
|
psbt_state.warning.as_ref(),
|
|
);
|
|
if let Some(action) = &psbt_state.action {
|
|
action.as_ref().view(content)
|
|
} else {
|
|
content
|
|
}
|
|
}
|
|
}
|