Skip to content

Commit

Permalink
handle psbt missing input coins
Browse files Browse the repository at this point in the history
As coins deposited by an unconfirmed
transactions can be removed from
the database, GUI should not rely
on them to calculate fees, but instead
use the psbt input witness_utxo field.

When importing an external psbt, gui
checks now that the witness utxo is
present.
  • Loading branch information
edouardparis committed Nov 20, 2023
1 parent f512f3c commit 6bff5a2
Show file tree
Hide file tree
Showing 4 changed files with 120 additions and 75 deletions.
9 changes: 8 additions & 1 deletion gui/src/app/state/psbts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,14 @@ impl ImportPsbtModal {
self.imported.valid = base64::decode(&self.imported.value)
.ok()
.and_then(|bytes| Psbt::deserialize(&bytes).ok())
.is_some();
.and_then(|psbt| {
if !psbt.inputs.iter().any(|input| input.witness_utxo.is_none()) {
Some(true)
} else {
None
}
})
.is_some()
}
Message::View(view::Message::ImportSpend(view::ImportSpendMessage::Confirm)) => {
if self.imported.valid {
Expand Down
55 changes: 33 additions & 22 deletions gui/src/app/view/psbt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -521,7 +521,7 @@ pub fn path_view<'a>(
}

pub fn inputs_and_outputs_view<'a>(
coins: &'a [Coin],
coins: &'a HashMap<OutPoint, Coin>,
tx: &'a Transaction,
network: Network,
change_indexes: Option<Vec<usize>>,
Expand Down Expand Up @@ -572,12 +572,17 @@ pub fn inputs_and_outputs_view<'a>(
.style(theme::Button::TransparentBorder)
},
move || {
coins
tx.input
.iter()
.fold(
Column::new().spacing(10).padding(20),
|col: Column<'a, Message>, coin| {
col.push(input_view(coin, labels, labels_editing))
|col: Column<'a, Message>, input| {
col.push(input_view(
&input.previous_output,
coins.get(&input.previous_output),
labels,
labels_editing,
))
},
)
.into()
Expand Down Expand Up @@ -729,12 +734,12 @@ pub fn inputs_and_outputs_view<'a>(
}

fn input_view<'a>(
coin: &'a Coin,
outpoint: &'a OutPoint,
coin: Option<&'a Coin>,
labels: &'a HashMap<String, String>,
labels_editing: &'a HashMap<String, form::Value<String>>,
) -> Element<'a, Message> {
let outpoint = coin.outpoint.to_string();
let addr = coin.address.to_string();
let outpoint = outpoint.to_string();
Column::new()
.width(Length::Fill)
.push(
Expand All @@ -753,7 +758,7 @@ fn input_view<'a>(
})
.width(Length::Fill),
)
.push(amount(&coin.amount)),
.push_maybe(coin.map(|c| amount(&c.amount))),
)
.push(
Column::new()
Expand All @@ -765,11 +770,12 @@ fn input_view<'a>(
.push(p2_regular(outpoint.clone()).style(color::GREY_3))
.push(
Button::new(icon::clipboard_icon().style(color::GREY_3))
.on_press(Message::Clipboard(coin.outpoint.to_string()))
.on_press(Message::Clipboard(outpoint.clone()))
.style(theme::Button::TransparentBorder),
),
)
.push(
.push_maybe(coin.map(|c| {
let addr = c.address.to_string();
Row::new()
.align_items(Alignment::Center)
.width(Length::Fill)
Expand All @@ -785,21 +791,26 @@ fn input_view<'a>(
.on_press(Message::Clipboard(addr.clone()))
.style(theme::Button::TransparentBorder),
),
),
)
.push_maybe(labels.get(&addr).map(|label| {
Row::new()
.align_items(Alignment::Center)
.width(Length::Fill)
.push(
)
}))
.push_maybe(
coin.map(|c| {
labels.get(&c.address.to_string()).map(|label| {
Row::new()
.align_items(Alignment::Center)
.width(Length::Fill)
.spacing(5)
.push(p1_bold("Address label:").style(color::GREY_3))
.push(p2_regular(label).style(color::GREY_3)),
)
})),
.push(
Row::new()
.align_items(Alignment::Center)
.width(Length::Fill)
.spacing(5)
.push(p1_bold("Address label:").style(color::GREY_3))
.push(p2_regular(label).style(color::GREY_3)),
)
})
})
.flatten(),
),
)
.spacing(5)
.into()
Expand Down
8 changes: 7 additions & 1 deletion gui/src/app/view/psbts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use iced::{widget::Space, Alignment, Length};

use liana_ui::{
color,
component::{amount::*, badge, button, card, form, text::*},
component::{amount::*, badge, button, card, form, text::*, tooltip::tooltip},
icon, theme,
util::Collection,
widget::*,
Expand Down Expand Up @@ -34,6 +34,12 @@ pub fn import_psbt_view<'a>(
.size(20)
.padding(10),
)
.push(
Row::new()
.spacing(5)
.push(p2_regular("Psbt must follow Liana standards").small())
.push(tooltip("All inputs witness utxos must be present")),
)
.push(Row::new().push(Space::with_width(Length::Fill)).push(
if imported.valid && !imported.value.is_empty() && !processing {
button::primary(None, "Import")
Expand Down
123 changes: 72 additions & 51 deletions gui/src/daemon/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ pub fn remaining_sequence(coin: &Coin, blockheight: u32, timelock: u16) -> u32 {
#[derive(Debug, Clone)]
pub struct SpendTx {
pub network: Network,
pub coins: Vec<Coin>,
pub coins: HashMap<OutPoint, Coin>,
pub labels: HashMap<String, String>,
pub psbt: Psbt,
pub change_indexes: Vec<usize>,
Expand Down Expand Up @@ -78,9 +78,15 @@ impl SpendTx {
);

let mut inputs_amount = Amount::from_sat(0);
for input in &psbt.inputs {
if let Some(utxo) = &input.witness_utxo {
inputs_amount += Amount::from_sat(utxo.value);
}
}

let mut status = SpendStatus::Pending;
for coin in &coins {
inputs_amount += coin.amount;
let mut coins_map = HashMap::<OutPoint, Coin>::new();
for coin in coins {
if let Some(info) = coin.spend_info {
if info.txid == psbt.unsigned_tx.txid() {
if info.height.is_some() {
Expand All @@ -92,7 +98,14 @@ impl SpendTx {
status = SpendStatus::Deprecated
}
}
coins_map.insert(coin.outpoint, coin);
}

// One input coin is missing, the psbt is deprecated for now.
if coins_map.len() != psbt.inputs.len() {
status = SpendStatus::Deprecated
}

let sigs = desc
.partial_spend_info(&psbt)
.expect("PSBT must be generated by Liana");
Expand Down Expand Up @@ -125,11 +138,15 @@ impl SpendTx {
}
},
updated_at,
coins,
coins: coins_map,
psbt,
change_indexes,
spend_amount,
fee_amount: inputs_amount - spend_amount - change_amount,
fee_amount: if inputs_amount > spend_amount + change_amount {
inputs_amount - spend_amount + change_amount
} else {
Amount::from_sat(0)
},
max_sat_vbytes,
status,
sigs,
Expand Down Expand Up @@ -200,7 +217,7 @@ impl Labelled for SpendTx {
let mut items = Vec::new();
let txid = self.psbt.unsigned_tx.txid();
items.push(LabelItem::Txid(txid));
for coin in &self.coins {
for coin in self.coins.values() {
items.push(LabelItem::Address(coin.address.clone()));
items.push(LabelItem::OutPoint(coin.outpoint));
}
Expand All @@ -221,7 +238,7 @@ impl Labelled for SpendTx {
pub struct HistoryTransaction {
pub network: Network,
pub labels: HashMap<String, String>,
pub coins: Vec<Coin>,
pub coins: HashMap<OutPoint, Coin>,
pub change_indexes: Vec<usize>,
pub tx: Transaction,
pub outgoing_amount: Amount,
Expand Down Expand Up @@ -252,9 +269,53 @@ impl HistoryTransaction {
},
);

let kind = if coins.is_empty() {
if change_indexes.len() == 1 {
TransactionKind::IncomingSinglePayment(OutPoint {
txid: tx.txid(),
vout: change_indexes[0] as u32,
})
} else {
TransactionKind::IncomingPaymentBatch(
change_indexes
.iter()
.map(|i| OutPoint {
txid: tx.txid(),
vout: *i as u32,
})
.collect(),
)
}
} else if outgoing_amount == Amount::from_sat(0) {
TransactionKind::SendToSelf
} else {
let outpoints: Vec<OutPoint> = tx
.output
.iter()
.enumerate()
.filter_map(|(i, _)| {
if !change_indexes.contains(&i) {
Some(OutPoint {
txid: tx.txid(),
vout: i as u32,
})
} else {
None
}
})
.collect();
if outpoints.len() == 1 {
TransactionKind::OutgoingSinglePayment(outpoints[0])
} else {
TransactionKind::OutgoingPaymentBatch(outpoints)
}
};

let mut inputs_amount = Amount::from_sat(0);
for coin in &coins {
let mut coins_map = HashMap::<OutPoint, Coin>::new();
for coin in coins {
inputs_amount += coin.amount;
coins_map.insert(coin.outpoint, coin);
}

let fee_amount = if inputs_amount > outgoing_amount + incoming_amount {
Expand All @@ -265,49 +326,9 @@ impl HistoryTransaction {

Self {
labels: HashMap::new(),
kind: if coins.is_empty() {
if change_indexes.len() == 1 {
TransactionKind::IncomingSinglePayment(OutPoint {
txid: tx.txid(),
vout: change_indexes[0] as u32,
})
} else {
TransactionKind::IncomingPaymentBatch(
change_indexes
.iter()
.map(|i| OutPoint {
txid: tx.txid(),
vout: *i as u32,
})
.collect(),
)
}
} else if outgoing_amount == Amount::from_sat(0) {
TransactionKind::SendToSelf
} else {
let outpoints: Vec<OutPoint> = tx
.output
.iter()
.enumerate()
.filter_map(|(i, _)| {
if !change_indexes.contains(&i) {
Some(OutPoint {
txid: tx.txid(),
vout: i as u32,
})
} else {
None
}
})
.collect();
if outpoints.len() == 1 {
TransactionKind::OutgoingSinglePayment(outpoints[0])
} else {
TransactionKind::OutgoingPaymentBatch(outpoints)
}
},
kind,
tx,
coins,
coins: coins_map,
change_indexes,
outgoing_amount,
incoming_amount,
Expand Down Expand Up @@ -362,7 +383,7 @@ impl Labelled for HistoryTransaction {
let mut items = Vec::new();
let txid = self.tx.txid();
items.push(LabelItem::Txid(txid));
for coin in &self.coins {
for coin in self.coins.values() {
items.push(LabelItem::Address(coin.address.clone()));
items.push(LabelItem::OutPoint(coin.outpoint));
}
Expand Down

0 comments on commit 6bff5a2

Please sign in to comment.