Skip to content

Commit 076023d

Browse files
committed
Use create_spend to calculate amount left to select
close #822
1 parent b5a3e78 commit 076023d

File tree

7 files changed

+136
-115
lines changed

7 files changed

+136
-115
lines changed

gui/Cargo.lock

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

gui/Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ path = "src/main.rs"
1515

1616
[dependencies]
1717
async-hwi = "0.0.13"
18-
liana = { git = "https://github.com/wizardsardine/liana", branch = "master", default-features = false, features = ["nonblocking_shutdown"] }
18+
liana = { git = "https://github.com/edouardparis/liana", branch = "expose-spend-module", default-features = false, features = ["nonblocking_shutdown"] }
1919
liana_ui = { path = "ui" }
2020
backtrace = "0.3"
2121
base64 = "0.13"

gui/src/app/error.rs

+9-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use std::convert::From;
22
use std::io::ErrorKind;
33

4-
use liana::{config::ConfigError, descriptors::LianaDescError};
4+
use liana::{config::ConfigError, descriptors::LianaDescError, spend::SpendCreationError};
55

66
use crate::{
77
app::{settings::SettingsError, wallet::WalletError},
@@ -16,13 +16,15 @@ pub enum Error {
1616
Unexpected(String),
1717
HardwareWallet(async_hwi::Error),
1818
Desc(LianaDescError),
19+
Spend(SpendCreationError),
1920
}
2021

2122
impl std::fmt::Display for Error {
2223
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
2324
match self {
2425
Self::Config(e) => write!(f, "{}", e),
2526
Self::Wallet(e) => write!(f, "{}", e),
27+
Self::Spend(e) => write!(f, "{}", e),
2628
Self::Daemon(e) => match e {
2729
DaemonError::Unexpected(e) => write!(f, "{}", e),
2830
DaemonError::NoAnswer => write!(f, "Daemon did not answer"),
@@ -84,3 +86,9 @@ impl From<async_hwi::Error> for Error {
8486
Error::HardwareWallet(error)
8587
}
8688
}
89+
90+
impl From<SpendCreationError> for Error {
91+
fn from(error: SpendCreationError) -> Self {
92+
Error::Spend(error)
93+
}
94+
}

gui/src/app/state/spend/mod.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ impl CreateSpendPanel {
3232
current: 0,
3333
steps: vec![
3434
Box::new(
35-
step::DefineSpend::new(descriptor, coins, timelock)
35+
step::DefineSpend::new(network, descriptor, coins, timelock)
3636
.with_coins_sorted(blockheight),
3737
),
3838
Box::new(step::SaveSpend::new(wallet)),
@@ -54,7 +54,7 @@ impl CreateSpendPanel {
5454
current: 0,
5555
steps: vec![
5656
Box::new(
57-
step::DefineSpend::new(descriptor, coins, timelock)
57+
step::DefineSpend::new(network, descriptor, coins, timelock)
5858
.with_preselected_coins(preselected_coins)
5959
.with_coins_sorted(blockheight)
6060
.self_send(),

gui/src/app/state/spend/step.rs

+120-105
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ use iced::{Command, Subscription};
66
use liana::{
77
descriptors::LianaDescriptor,
88
miniscript::bitcoin::{
9-
self, address, psbt::Psbt, Address, Amount, Denomination, Network, OutPoint,
9+
self, address, psbt::Psbt, secp256k1, Address, Amount, Denomination, Network, OutPoint,
10+
},
11+
spend::{
12+
create_spend, CandidateCoin, SpendCreationError, SpendOutputAddress, SpendTxFees, TxGetter,
1013
},
1114
};
1215

@@ -19,7 +22,7 @@ use crate::{
1922
app::{cache::Cache, error::Error, message::Message, state::psbt, view, wallet::Wallet},
2023
daemon::{
2124
model::{remaining_sequence, Coin, SpendTx},
22-
Daemon, DaemonError,
25+
Daemon,
2326
},
2427
};
2528

@@ -73,7 +76,9 @@ pub struct DefineSpend {
7376
is_valid: bool,
7477
is_duplicate: bool,
7578

79+
network: Network,
7680
descriptor: LianaDescriptor,
81+
curve: secp256k1::Secp256k1<secp256k1::VerifyOnly>,
7782
timelock: u16,
7883
coins: Vec<(Coin, bool)>,
7984
coins_labels: HashMap<String, String>,
@@ -85,7 +90,12 @@ pub struct DefineSpend {
8590
}
8691

8792
impl DefineSpend {
88-
pub fn new(descriptor: LianaDescriptor, coins: &[Coin], timelock: u16) -> Self {
93+
pub fn new(
94+
network: Network,
95+
descriptor: LianaDescriptor,
96+
coins: &[Coin],
97+
timelock: u16,
98+
) -> Self {
8999
let balance_available = coins
90100
.iter()
91101
.filter_map(|coin| {
@@ -99,7 +109,7 @@ impl DefineSpend {
99109
let coins: Vec<(Coin, bool)> = coins
100110
.iter()
101111
.filter_map(|c| {
102-
if c.spend_info.is_none() {
112+
if c.spend_info.is_none() && !c.is_immature {
103113
Some((c.clone(), false))
104114
} else {
105115
None
@@ -109,7 +119,9 @@ impl DefineSpend {
109119

110120
Self {
111121
balance_available,
122+
network,
112123
descriptor,
124+
curve: secp256k1::Secp256k1::verification_only(),
113125
timelock,
114126
generated: None,
115127
coins,
@@ -175,110 +187,122 @@ impl DefineSpend {
175187
}
176188
}
177189
}
178-
fn auto_select_coins(&mut self, daemon: Arc<dyn Daemon + Sync + Send>) {
179-
// Set non-input values in the same way as for user selection.
180-
let mut outputs: HashMap<Address<address::NetworkUnchecked>, u64> = HashMap::new();
181-
for recipient in &self.recipients {
182-
outputs.insert(
183-
Address::from_str(&recipient.address.value).expect("Checked before"),
184-
recipient.amount().expect("Checked before"),
185-
);
190+
/// redraft calcul amount left to select and auto select coin is the user did not select a coin
191+
/// manually
192+
fn redraft(&mut self, daemon: Arc<dyn Daemon + Sync + Send>) {
193+
if !self.form_values_are_valid() || self.recipients.is_empty() {
194+
return;
186195
}
187-
let feerate_vb = self.feerate.value.parse::<u64>().unwrap_or(0);
196+
197+
let destinations: Vec<(SpendOutputAddress, Amount)> = self
198+
.recipients
199+
.iter()
200+
.map(|recipient| {
201+
(
202+
SpendOutputAddress {
203+
addr: Address::from_str(&recipient.address.value)
204+
.expect("Checked before")
205+
.assume_checked(),
206+
info: None,
207+
},
208+
Amount::from_sat(recipient.amount().expect("Checked before")),
209+
)
210+
})
211+
.collect();
212+
213+
let coins: Vec<CandidateCoin> = if self.is_user_coin_selection {
214+
self.coins
215+
.iter()
216+
.filter_map(|(c, selected)| {
217+
if *selected {
218+
Some(CandidateCoin {
219+
amount: c.amount,
220+
outpoint: c.outpoint,
221+
deriv_index: c.derivation_index,
222+
is_change: c.is_change,
223+
sequence: None,
224+
must_select: *selected,
225+
})
226+
} else {
227+
None
228+
}
229+
})
230+
.collect()
231+
} else {
232+
self.coins
233+
.iter()
234+
.map(|(c, _)| CandidateCoin {
235+
amount: c.amount,
236+
outpoint: c.outpoint,
237+
deriv_index: c.derivation_index,
238+
is_change: c.is_change,
239+
sequence: None,
240+
must_select: false,
241+
})
242+
.collect()
243+
};
244+
245+
let dummy_address = self
246+
.descriptor
247+
.change_descriptor()
248+
.derive(0.into(), &self.curve)
249+
.address(self.network);
250+
251+
let feerate_vb = self.feerate.value.parse::<u64>().expect("Checked before");
188252
// Create a spend with empty inputs in order to use auto-selection.
189-
match daemon.create_spend_tx(&[], &outputs, feerate_vb) {
253+
match create_spend(
254+
&self.descriptor,
255+
&self.curve,
256+
&mut DaemonTxGetter(&daemon),
257+
&destinations,
258+
&coins,
259+
SpendTxFees::Regular(feerate_vb),
260+
// we enter a dummy address to calculate
261+
SpendOutputAddress {
262+
addr: dummy_address,
263+
info: None,
264+
},
265+
) {
190266
Ok(spend) => {
191267
self.warning = None;
192-
let selected_coins: Vec<OutPoint> = spend
193-
.psbt
194-
.unsigned_tx
195-
.input
196-
.iter()
197-
.map(|c| c.previous_output)
198-
.collect();
199-
// Mark coins as selected.
200-
for (coin, selected) in &mut self.coins {
201-
*selected = selected_coins.contains(&coin.outpoint);
268+
if !self.is_user_coin_selection {
269+
let selected_coins: Vec<OutPoint> = spend
270+
.psbt
271+
.unsigned_tx
272+
.input
273+
.iter()
274+
.map(|c| c.previous_output)
275+
.collect();
276+
// Mark coins as selected.
277+
for (coin, selected) in &mut self.coins {
278+
*selected = selected_coins.contains(&coin.outpoint);
279+
}
202280
}
203281
// As coin selection was successful, we can assume there is nothing left to select.
204282
self.amount_left_to_select = Some(Amount::from_sat(0));
205283
}
284+
// For coin selection error (insufficient funds), do not make any changes to
285+
// selected coins on screen and just show user how much is left to select.
286+
// User can then either:
287+
// - modify recipient amounts and/or feerate and let coin selection run again, or
288+
// - select coins manually.
289+
Err(SpendCreationError::CoinSelection(amount)) => {
290+
self.amount_left_to_select = Some(Amount::from_sat(amount.missing));
291+
}
206292
Err(e) => {
207-
if let DaemonError::CoinSelectionError = e {
208-
// For coin selection error (insufficient funds), do not make any changes to
209-
// selected coins on screen and just show user how much is left to select.
210-
// User can then either:
211-
// - modify recipient amounts and/or feerate and let coin selection run again, or
212-
// - select coins manually.
213-
self.amount_left_to_select();
214-
} else {
215-
self.warning = Some(e.into());
216-
}
293+
self.warning = Some(e.into());
217294
}
218295
}
219296
}
220-
fn amount_left_to_select(&mut self) {
221-
// We need the feerate in order to compute the required amount of BTC to
222-
// select. Return early if we don't to not do unnecessary computation.
223-
let feerate = match self.feerate.value.parse::<u64>() {
224-
Ok(f) => f,
225-
Err(_) => {
226-
self.amount_left_to_select = None;
227-
return;
228-
}
229-
};
230-
231-
// The coins to be included in this transaction.
232-
let selected_coins: Vec<_> = self
233-
.coins
234-
.iter()
235-
.filter_map(|(c, selected)| if *selected { Some(c) } else { None })
236-
.collect();
297+
}
237298

238-
// A dummy representation of the transaction that will be computed, for
239-
// the purpose of computing its size in order to anticipate the fees needed.
240-
// NOTE: we make the conservative estimation a change output will always be
241-
// needed.
242-
let tx_template = bitcoin::Transaction {
243-
version: 2,
244-
lock_time: bitcoin::blockdata::locktime::absolute::LockTime::ZERO,
245-
input: selected_coins
246-
.iter()
247-
.map(|_| bitcoin::TxIn::default())
248-
.collect(),
249-
output: self
250-
.recipients
251-
.iter()
252-
.filter_map(|recipient| {
253-
if recipient.valid() {
254-
Some(bitcoin::TxOut {
255-
script_pubkey: Address::from_str(&recipient.address.value)
256-
.unwrap()
257-
.payload
258-
.script_pubkey(),
259-
value: recipient.amount().unwrap(),
260-
})
261-
} else {
262-
None
263-
}
264-
})
265-
.collect(),
266-
};
267-
// nValue size + scriptPubKey CompactSize + OP_0 + PUSH32 + <wit program>
268-
const CHANGE_TXO_SIZE: usize = 8 + 1 + 1 + 1 + 32;
269-
let satisfaction_vsize = self.descriptor.max_sat_weight() / 4;
270-
let transaction_size =
271-
tx_template.vsize() + satisfaction_vsize * tx_template.input.len() + CHANGE_TXO_SIZE;
272-
273-
// Now the calculation of the amount left to be selected by the user is a simple
274-
// substraction between the value needed by the transaction to be created and the
275-
// value that was selected already.
276-
let selected_amount = selected_coins.iter().map(|c| c.amount.to_sat()).sum();
277-
let output_sum: u64 = tx_template.output.iter().map(|o| o.value).sum();
278-
let needed_amount: u64 = transaction_size as u64 * feerate + output_sum;
279-
self.amount_left_to_select = Some(Amount::from_sat(
280-
needed_amount.saturating_sub(selected_amount),
281-
));
299+
pub struct DaemonTxGetter<'a>(&'a Arc<dyn Daemon + Sync + Send>);
300+
impl<'a> TxGetter for DaemonTxGetter<'a> {
301+
fn get_tx(&mut self, txid: &bitcoin::Txid) -> Option<bitcoin::Transaction> {
302+
self.0
303+
.list_txs(&[*txid])
304+
.ok()
305+
.and_then(|mut txs| txs.transactions.pop().map(|tx| tx.tx))
282306
}
283307
}
284308

@@ -317,14 +341,11 @@ impl Step for DefineSpend {
317341
if let Ok(value) = s.parse::<u64>() {
318342
self.feerate.value = s;
319343
self.feerate.valid = value != 0;
320-
self.amount_left_to_select();
321344
} else if s.is_empty() {
322345
self.feerate.value = "".to_string();
323346
self.feerate.valid = true;
324-
self.amount_left_to_select = None;
325347
} else {
326348
self.feerate.valid = false;
327-
self.amount_left_to_select = None;
328349
}
329350
self.warning = None;
330351
}
@@ -368,7 +389,6 @@ impl Step for DefineSpend {
368389
coin.1 = !coin.1;
369390
// Once user edits selection, auto-selection can no longer be used.
370391
self.is_user_coin_selection = true;
371-
self.amount_left_to_select();
372392
}
373393
}
374394
_ => {}
@@ -378,12 +398,7 @@ impl Step for DefineSpend {
378398
// - all form values have been added and validated
379399
// - not a self-send
380400
// - user has not yet selected coins manually
381-
if self.form_values_are_valid()
382-
&& !self.recipients.is_empty()
383-
&& !self.is_user_coin_selection
384-
{
385-
self.auto_select_coins(daemon);
386-
}
401+
self.redraft(daemon);
387402
self.check_valid();
388403
}
389404
Message::Psbt(res) => match res {

gui/src/app/view/warning.rs

+1
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ impl From<&Error> for WarningMessage {
4141
Error::Unexpected(_) => WarningMessage("Unknown error".to_string()),
4242
Error::HardwareWallet(_) => WarningMessage("Hardware wallet error".to_string()),
4343
Error::Desc(e) => WarningMessage(format!("Descriptor analysis error: '{}'.", e)),
44+
Error::Spend(e) => WarningMessage(format!("Spend creation error: '{}'.", e)),
4445
}
4546
}
4647
}

0 commit comments

Comments
 (0)