gui: auto-select coins for spend
This commit is contained in:
parent
d5b7a3a7de
commit
9e2407eb8a
8
gui/Cargo.lock
generated
8
gui/Cargo.lock
generated
@ -252,6 +252,11 @@ dependencies = [
|
||||
"byteorder",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bdk_coin_select"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/evanlinjin/bdk?branch=new_bdk_coin_select#2a06d73ac7a5dca933b19b51078f5279691364ed"
|
||||
|
||||
[[package]]
|
||||
name = "bech32"
|
||||
version = "0.9.1"
|
||||
@ -2415,9 +2420,10 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "liana"
|
||||
version = "2.0.0"
|
||||
source = "git+https://github.com/wizardsardine/liana?branch=master#87555a8da50702ebec04dbe876e7055432ca805a"
|
||||
source = "git+https://github.com/wizardsardine/liana?branch=master#2d303b139d4eeafc6b6505433e18b24cd561ab6b"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"bdk_coin_select",
|
||||
"bip39",
|
||||
"dirs 5.0.0",
|
||||
"fern",
|
||||
|
||||
@ -46,6 +46,7 @@ impl std::fmt::Display for Error {
|
||||
DaemonError::Rpc(code, e) => {
|
||||
write!(f, "[{:?}] {}", code, e)
|
||||
}
|
||||
DaemonError::CoinSelectionError => write!(f, "{}", e),
|
||||
},
|
||||
Self::Unexpected(e) => write!(f, "Unexpected error: {}", e),
|
||||
Self::HardwareWallet(e) => write!(f, "{}", e),
|
||||
|
||||
@ -19,7 +19,7 @@ use crate::{
|
||||
app::{cache::Cache, error::Error, message::Message, state::psbt, view, wallet::Wallet},
|
||||
daemon::{
|
||||
model::{remaining_sequence, Coin, SpendTx},
|
||||
Daemon,
|
||||
Daemon, DaemonError,
|
||||
},
|
||||
};
|
||||
|
||||
@ -67,6 +67,9 @@ pub trait Step {
|
||||
pub struct DefineSpend {
|
||||
balance_available: Amount,
|
||||
recipients: Vec<Recipient>,
|
||||
/// 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,
|
||||
|
||||
@ -113,6 +116,7 @@ impl DefineSpend {
|
||||
coins_labels: HashMap::new(),
|
||||
batch_label: form::Value::default(),
|
||||
recipients: vec![Recipient::default()],
|
||||
is_user_coin_selection: false, // Start with auto-selection until user edits selection.
|
||||
is_valid: false,
|
||||
is_duplicate: false,
|
||||
feerate: form::Value::default(),
|
||||
@ -151,18 +155,19 @@ impl DefineSpend {
|
||||
self
|
||||
}
|
||||
|
||||
fn check_valid(&mut self) {
|
||||
self.is_valid = self.feerate.valid
|
||||
fn form_values_are_valid(&self) -> bool {
|
||||
self.feerate.valid
|
||||
&& !self.feerate.value.is_empty()
|
||||
&& (self.batch_label.valid || self.recipients.len() < 2);
|
||||
&& (self.batch_label.valid || self.recipients.len() < 2)
|
||||
// Recipients will be empty for self-send.
|
||||
&& self.recipients.iter().all(|r| r.valid())
|
||||
}
|
||||
|
||||
fn check_valid(&mut self) {
|
||||
self.is_valid =
|
||||
self.form_values_are_valid() && self.coins.iter().any(|(_, selected)| *selected);
|
||||
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;
|
||||
}
|
||||
if !self.is_duplicate && !recipient.address.value.is_empty() {
|
||||
self.is_duplicate = self.recipients[..i]
|
||||
.iter()
|
||||
@ -170,6 +175,48 @@ impl DefineSpend {
|
||||
}
|
||||
}
|
||||
}
|
||||
fn auto_select_coins(&mut self, daemon: Arc<dyn Daemon + Sync + Send>) {
|
||||
// Set non-input values in the same way as for user selection.
|
||||
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);
|
||||
// Create a spend with empty inputs in order to use auto-selection.
|
||||
match daemon.create_spend_tx(&[], &outputs, feerate_vb) {
|
||||
Ok(spend) => {
|
||||
self.warning = None;
|
||||
let selected_coins: Vec<OutPoint> = spend
|
||||
.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));
|
||||
}
|
||||
Err(e) => {
|
||||
if let DaemonError::CoinSelectionError = e {
|
||||
// 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.
|
||||
self.amount_left_to_select();
|
||||
} else {
|
||||
self.warning = Some(e.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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.
|
||||
@ -319,11 +366,24 @@ impl Step for DefineSpend {
|
||||
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;
|
||||
self.amount_left_to_select();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// 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
|
||||
if self.form_values_are_valid()
|
||||
&& !self.recipients.is_empty()
|
||||
&& !self.is_user_coin_selection
|
||||
{
|
||||
self.auto_select_coins(daemon);
|
||||
}
|
||||
self.check_valid();
|
||||
}
|
||||
Message::Psbt(res) => match res {
|
||||
|
||||
@ -34,6 +34,9 @@ impl From<&Error> for WarningMessage {
|
||||
WarningMessage("Communication with Daemon failed".to_string())
|
||||
}
|
||||
DaemonError::DaemonStopped => WarningMessage("Daemon stopped".to_string()),
|
||||
DaemonError::CoinSelectionError => {
|
||||
WarningMessage("Error when selecting coins for spend".to_string())
|
||||
}
|
||||
},
|
||||
Error::Unexpected(_) => WarningMessage("Unknown error".to_string()),
|
||||
Error::HardwareWallet(_) => WarningMessage("Hardware wallet error".to_string()),
|
||||
|
||||
@ -2,7 +2,7 @@ use std::collections::{HashMap, HashSet};
|
||||
|
||||
use super::{model::*, Daemon, DaemonError};
|
||||
use liana::{
|
||||
commands::LabelItem,
|
||||
commands::{CommandError, LabelItem},
|
||||
config::Config,
|
||||
miniscript::bitcoin::{address, psbt::Psbt, Address, OutPoint, Txid},
|
||||
DaemonControl, DaemonHandle,
|
||||
@ -90,7 +90,10 @@ impl Daemon for EmbeddedDaemon {
|
||||
) -> Result<CreateSpendResult, DaemonError> {
|
||||
self.control()?
|
||||
.create_spend(destinations, coins_outpoints, feerate_vb)
|
||||
.map_err(|e| DaemonError::Unexpected(e.to_string()))
|
||||
.map_err(|e| match e {
|
||||
CommandError::CoinSelectionError(_) => DaemonError::CoinSelectionError,
|
||||
e => DaemonError::Unexpected(e.to_string()),
|
||||
})
|
||||
}
|
||||
|
||||
fn update_spend_tx(&self, psbt: &Psbt) -> Result<(), DaemonError> {
|
||||
|
||||
@ -30,6 +30,8 @@ pub enum DaemonError {
|
||||
Start(StartupError),
|
||||
// Error if the client is not supported.
|
||||
ClientNotSupported,
|
||||
/// Error when selecting coins for spend.
|
||||
CoinSelectionError,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for DaemonError {
|
||||
@ -42,6 +44,7 @@ impl std::fmt::Display for DaemonError {
|
||||
Self::Unexpected(e) => write!(f, "Daemon unexpected error: {}", e),
|
||||
Self::Start(e) => write!(f, "Daemon did not start: {}", e),
|
||||
Self::ClientNotSupported => write!(f, "Daemon communication is not supported"),
|
||||
Self::CoinSelectionError => write!(f, "Coin selection error"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user