Merge #825: handle psbt missing input coins

40ade751e0a00cde2a8eec3260eb267534a75f61 Fix transactions Labelled methods (edouardparis)
b4729c37286a1e1fc7584b935a47f458a2c8aef9 Split inputs and outputs view (edouardparis)
946e499b5a1f0f340169ace8228bddb451a8a7a2 Handle missing coin inputs in tx and psbt (edouardparis)

Pull request description:

ACKs for top commit:
  jp1ac4:
    ACK 40ade751e0.

Tree-SHA512: b823b78d0730a1b23a0df4834b7a622f767119a63e6f0e86cb61eb4d422fbcfcd182f5a4726d7140f444fe0cf1722c774026091ad15a11aadea8542f875dd278
This commit is contained in:
edouardparis 2023-11-28 13:44:28 +01:00
commit 7011210fc2
No known key found for this signature in database
GPG Key ID: E65F7A089C20DC8F
6 changed files with 293 additions and 196 deletions

View File

@ -314,19 +314,33 @@ pub fn payment_view<'a>(
)
.spacing(5),
))
.push(super::psbt::inputs_and_outputs_view(
&tx.coins,
&tx.tx,
cache.network,
if tx.is_external() {
None
} else {
Some(tx.change_indexes.clone())
},
&tx.labels,
labels_editing,
tx.is_single_payment().is_some(),
))
.push(
Column::new()
.spacing(20)
// We do not need to display inputs for external incoming transactions
.push_maybe(if tx.is_external() {
None
} else {
Some(super::psbt::inputs_view(
&tx.coins,
&tx.tx,
&tx.labels,
labels_editing,
))
})
.push(super::psbt::outputs_view(
&tx.tx,
cache.network,
if tx.is_external() {
None
} else {
Some(tx.change_indexes.clone())
},
&tx.labels,
labels_editing,
tx.is_single_payment().is_some(),
)),
)
.spacing(20),
)
}

View File

@ -74,15 +74,24 @@ pub fn psbt_view<'a>(
)
.push(spend_header(tx, labels_editing))
.push(spend_overview_view(tx, desc_info, key_aliases))
.push(inputs_and_outputs_view(
&tx.coins,
&tx.psbt.unsigned_tx,
network,
Some(tx.change_indexes.clone()),
&tx.labels,
labels_editing,
tx.is_single_payment().is_some(),
))
.push(
Column::new()
.spacing(20)
.push(inputs_view(
&tx.coins,
&tx.psbt.unsigned_tx,
&tx.labels,
labels_editing,
))
.push(outputs_view(
&tx.psbt.unsigned_tx,
network,
Some(tx.change_indexes.clone()),
&tx.labels,
labels_editing,
tx.is_single_payment().is_some(),
)),
)
.push(if saved {
Row::new()
.push(
@ -225,13 +234,18 @@ pub fn spend_header<'a>(
Row::new()
.align_items(Alignment::Center)
.push(h3("Miner fee: ").style(color::GREY_3))
.push(amount_with_size(&tx.fee_amount, H3_SIZE))
.push_maybe(if tx.fee_amount.is_none() {
Some(text("Missing information about transaction inputs"))
} else {
None
})
.push_maybe(tx.fee_amount.map(|fee| amount_with_size(&fee, H3_SIZE)))
.push(text(" ").size(H3_SIZE))
.push(
text(format!("(~{} sats/vbyte)", &tx.min_feerate_vb()))
.push_maybe(tx.min_feerate_vb().map(|rate| {
text(format!("(~{} sats/vbyte)", &rate))
.size(H4_SIZE)
.style(color::GREY_3),
),
.style(color::GREY_3)
})),
),
)
.into()
@ -520,8 +534,71 @@ pub fn path_view<'a>(
.into()
}
pub fn inputs_and_outputs_view<'a>(
coins: &'a [Coin],
pub fn inputs_view<'a>(
coins: &'a HashMap<OutPoint, Coin>,
tx: &'a Transaction,
labels: &'a HashMap<String, String>,
labels_editing: &'a HashMap<String, form::Value<String>>,
) -> Element<'a, Message> {
Container::new(Collapse::new(
move || {
Button::new(
Row::new()
.align_items(Alignment::Center)
.push(
h4_bold(format!(
"{} coin{} spent",
tx.input.len(),
if tx.input.len() == 1 { "" } else { "s" }
))
.width(Length::Fill),
)
.push(icon::collapse_icon()),
)
.padding(20)
.width(Length::Fill)
.style(theme::Button::TransparentBorder)
},
move || {
Button::new(
Row::new()
.align_items(Alignment::Center)
.push(
h4_bold(format!(
"{} coin{} spent",
tx.input.len(),
if tx.input.len() == 1 { "" } else { "s" }
))
.width(Length::Fill),
)
.push(icon::collapsed_icon()),
)
.padding(20)
.width(Length::Fill)
.style(theme::Button::TransparentBorder)
},
move || {
tx.input
.iter()
.fold(
Column::new().spacing(10).padding(20),
|col: Column<'a, Message>, input| {
col.push(input_view(
&input.previous_output,
coins.get(&input.previous_output),
labels,
labels_editing,
))
},
)
.into()
},
))
.style(theme::Container::Card(theme::Card::Simple))
.into()
}
pub fn outputs_view<'a>(
tx: &'a Transaction,
network: Network,
change_indexes: Option<Vec<usize>>,
@ -532,62 +609,6 @@ pub fn inputs_and_outputs_view<'a>(
let change_indexes_copy = change_indexes.clone();
Column::new()
.spacing(20)
.push_maybe(if !coins.is_empty() {
Some(
Container::new(Collapse::new(
move || {
Button::new(
Row::new()
.align_items(Alignment::Center)
.push(
h4_bold(format!(
"{} coin{} spent",
coins.len(),
if coins.len() == 1 { "" } else { "s" }
))
.width(Length::Fill),
)
.push(icon::collapse_icon()),
)
.padding(20)
.width(Length::Fill)
.style(theme::Button::TransparentBorder)
},
move || {
Button::new(
Row::new()
.align_items(Alignment::Center)
.push(
h4_bold(format!(
"{} coin{} spent",
coins.len(),
if coins.len() == 1 { "" } else { "s" }
))
.width(Length::Fill),
)
.push(icon::collapsed_icon()),
)
.padding(20)
.width(Length::Fill)
.style(theme::Button::TransparentBorder)
},
move || {
coins
.iter()
.fold(
Column::new().spacing(10).padding(20),
|col: Column<'a, Message>, coin| {
col.push(input_view(coin, labels, labels_editing))
},
)
.into()
},
))
.style(theme::Container::Card(theme::Card::Simple)),
)
} else {
None
})
.push({
let count = tx
.output
@ -729,12 +750,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(
@ -753,7 +774,7 @@ fn input_view<'a>(
})
.width(Length::Fill),
)
.push(amount(&coin.amount)),
.push_maybe(coin.map(|c| amount(&c.amount))),
)
.push(
Column::new()
@ -765,11 +786,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)
@ -782,23 +804,25 @@ fn input_view<'a>(
.push(p2_regular(addr.clone()).style(color::GREY_3))
.push(
Button::new(icon::clipboard_icon().style(color::GREY_3))
.on_press(Message::Clipboard(addr.clone()))
.on_press(Message::Clipboard(addr))
.style(theme::Button::TransparentBorder),
),
),
)
.push_maybe(labels.get(&addr).map(|label| {
Row::new()
.align_items(Alignment::Center)
.width(Length::Fill)
.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)),
)
}))
.push_maybe(coin.and_then(|c| {
labels.get(&c.address.to_string()).map(|label| {
Row::new()
.align_items(Alignment::Center)
.width(Length::Fill)
.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)),
)
})
})),
)
.spacing(5)

View File

@ -152,7 +152,7 @@ fn spend_tx_list_view(i: usize, tx: &SpendTx) -> Element<'_, Message> {
} else {
Container::new(p1_regular("Self-transfer"))
})
.push(amount_with_size(&tx.fee_amount, P2_SIZE))
.push_maybe(tx.fee_amount.map(|fee| amount_with_size(&fee, P2_SIZE)))
.width(Length::Shrink),
)
.align_items(Alignment::Center)

View File

@ -49,15 +49,24 @@ pub fn spend_view<'a>(
.push(Container::new(h3("Send")).width(Length::Fill))
.push(psbt::spend_header(tx, labels_editing))
.push(psbt::spend_overview_view(tx, desc_info, key_aliases))
.push(psbt::inputs_and_outputs_view(
&tx.coins,
&tx.psbt.unsigned_tx,
network,
Some(tx.change_indexes.clone()),
&tx.labels,
labels_editing,
tx.is_single_payment().is_some(),
))
.push(
Column::new()
.spacing(20)
.push(psbt::inputs_view(
&tx.coins,
&tx.psbt.unsigned_tx,
&tx.labels,
labels_editing,
))
.push(psbt::outputs_view(
&tx.psbt.unsigned_tx,
network,
Some(tx.change_indexes.clone()),
&tx.labels,
labels_editing,
tx.is_single_payment().is_some(),
)),
)
.push(if saved {
Row::new()
.push(

View File

@ -99,9 +99,11 @@ fn tx_list_view(i: usize, tx: &HistoryTransaction) -> Element<'_, Message> {
})
.push(
Column::new()
.push_maybe(
tx.labels.get(&tx.tx.txid().to_string()).map(p1_regular),
)
.push_maybe(if let Some(outpoint) = tx.is_single_payment() {
tx.labels.get(&outpoint.to_string()).map(p1_regular)
} else {
tx.labels.get(&tx.tx.txid().to_string()).map(p1_regular)
})
.push_maybe(tx.time.map(|t| {
Container::new(
text(format!(
@ -249,19 +251,33 @@ pub fn tx_view<'a>(
)
.spacing(5),
))
.push(super::psbt::inputs_and_outputs_view(
&tx.coins,
&tx.tx,
cache.network,
if tx.is_external() {
None
} else {
Some(tx.change_indexes.clone())
},
&tx.labels,
labels_editing,
tx.is_single_payment().is_some(),
))
.push(
Column::new()
.spacing(20)
// We do not need to display inputs for external incoming transactions
.push_maybe(if tx.is_external() {
None
} else {
Some(super::psbt::inputs_view(
&tx.coins,
&tx.tx,
&tx.labels,
labels_editing,
))
})
.push(super::psbt::outputs_view(
&tx.tx,
cache.network,
if tx.is_external() {
None
} else {
Some(tx.change_indexes.clone())
},
&tx.labels,
labels_editing,
tx.is_single_payment().is_some(),
)),
)
.spacing(20),
)
}

View File

@ -32,12 +32,12 @@ 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>,
pub spend_amount: Amount,
pub fee_amount: Amount,
pub fee_amount: Option<Amount>,
/// The maximum size difference (in virtual bytes) of
/// an input in this transaction before and after satisfaction.
pub max_sat_vbytes: usize,
@ -77,10 +77,9 @@ impl SpendTx {
},
);
let mut inputs_amount = Amount::from_sat(0);
let mut status = SpendStatus::Pending;
for coin in &coins {
inputs_amount += coin.amount;
let mut coins_map = HashMap::<OutPoint, Coin>::with_capacity(coins.len());
for coin in coins {
if let Some(info) = coin.spend_info {
if info.txid == psbt.unsigned_tx.txid() {
if info.height.is_some() {
@ -92,7 +91,40 @@ impl SpendTx {
status = SpendStatus::Deprecated
}
}
coins_map.insert(coin.outpoint, coin);
}
let inputs_amount = {
let mut inputs_amount = Amount::from_sat(0);
for (i, input) in psbt.inputs.iter().enumerate() {
if let Some(utxo) = &input.witness_utxo {
inputs_amount += Amount::from_sat(utxo.value);
// we try to have it from the coin
} else if let Some(coin) = psbt
.unsigned_tx
.input
.get(i)
.and_then(|inpt| coins_map.get(&inpt.previous_output))
{
inputs_amount += coin.amount;
// Information is missing, it is better to set inputs_amount to None.
} else {
inputs_amount = Amount::from_sat(0);
break;
}
}
if inputs_amount.to_sat() == 0 {
None
} else {
Some(inputs_amount)
}
};
// 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");
@ -125,11 +157,11 @@ impl SpendTx {
}
},
updated_at,
coins,
coins: coins_map,
psbt,
change_indexes,
spend_amount,
fee_amount: inputs_amount - spend_amount - change_amount,
fee_amount: inputs_amount.and_then(|a| a.checked_sub(spend_amount + change_amount)),
max_sat_vbytes,
status,
sigs,
@ -165,11 +197,11 @@ impl SpendTx {
}
/// Feerate obtained if all transaction inputs have the maximum satisfaction size.
pub fn min_feerate_vb(&self) -> u64 {
pub fn min_feerate_vb(&self) -> Option<u64> {
// This assumes all inputs are internal (have same max satisfaction size).
let max_tx_vbytes =
self.psbt.unsigned_tx.vsize() + (self.max_sat_vbytes * self.psbt.inputs.len());
self.fee_amount.to_sat() / max_tx_vbytes as u64
self.fee_amount.map(|a| a.to_sat() / max_tx_vbytes as u64)
}
pub fn is_send_to_self(&self) -> bool {
@ -200,9 +232,11 @@ 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));
}
for input in &self.psbt.unsigned_tx.input {
items.push(LabelItem::OutPoint(input.previous_output));
}
for (vout, output) in self.psbt.unsigned_tx.output.iter().enumerate() {
items.push(LabelItem::OutPoint(OutPoint {
@ -221,7 +255,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,
@ -252,66 +286,64 @@ impl HistoryTransaction {
},
);
let mut inputs_amount = Amount::from_sat(0);
for coin in &coins {
inputs_amount += coin.amount;
}
let fee_amount = if inputs_amount > outgoing_amount + incoming_amount {
Some(inputs_amount - outgoing_amount - incoming_amount)
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 {
None
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);
let mut coins_map = HashMap::<OutPoint, Coin>::with_capacity(coins.len());
for coin in coins {
inputs_amount += coin.amount;
coins_map.insert(coin.outpoint, coin);
}
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,
fee_amount,
fee_amount: inputs_amount.checked_sub(outgoing_amount + incoming_amount),
height,
time,
network,
@ -362,9 +394,11 @@ 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));
}
for input in &self.tx.input {
items.push(LabelItem::OutPoint(input.previous_output));
}
for (vout, output) in self.tx.output.iter().enumerate() {
items.push(LabelItem::OutPoint(OutPoint {