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, recipients: Vec, generated: Option<(Psbt, Vec)>, batch_label: Option, labels: HashMap, } 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, cache: &Cache, message: Message, ) -> Command; fn apply(&self, _draft: &mut TransactionDraft) {} fn interrupt(&mut self) {} fn load(&mut self, _draft: &TransactionDraft) {} fn subscription(&self) -> Subscription { Subscription::none() } } pub struct DefineSpend { recipients: Vec, /// If set, this is the index of a recipient that should /// receive the max amount. send_max_to_recipient: Option, /// 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, timelock: u16, coins: Vec<(Coin, bool)>, coins_labels: HashMap, batch_label: form::Value, amount_left_to_select: Option, feerate: form::Value, generated: Option<(Psbt, Vec)>, warning: Option, } 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) { 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, 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::().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 = 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, cache: &Cache, message: Message, ) -> Command { 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::>() .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::() { 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 = self .coins .iter() .filter_map( |(coin, selected)| { if *selected { Some(coin.outpoint) } else { None } }, ) .collect(); let mut outputs: HashMap, 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::().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 = 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, address: form::Value, amount: form::Value, } impl Recipient { fn amount(&self) -> Result { 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::spend::recipient_view(i, &self.address, &self.amount, &self.label, is_max_selected) } } pub struct SaveSpend { wallet: Arc, spend: Option<(psbt::PsbtState, Vec)>, curve: secp256k1::Secp256k1, } impl SaveSpend { pub fn new(wallet: Arc) -> 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 { if let Some((psbt_state, _)) = &self.spend { psbt_state.subscription() } else { Subscription::none() } } fn update( &mut self, daemon: Arc, cache: &Cache, message: Message, ) -> Command { 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 } } }