diff --git a/gui/src/app/state/recovery.rs b/gui/src/app/state/recovery.rs index 5daecece..dbe904e9 100644 --- a/gui/src/app/state/recovery.rs +++ b/gui/src/app/state/recovery.rs @@ -155,7 +155,7 @@ impl State for RecoveryPanel { .cloned() .collect(); let sigs = desc.partial_spend_info(&psbt).unwrap(); - Ok(SpendTx::new(None, psbt, coins, sigs)) + Ok(SpendTx::new(None, psbt, coins, sigs, desc.max_sat_vbytes())) }, Message::Recovery, ); diff --git a/gui/src/app/state/spend/step.rs b/gui/src/app/state/spend/step.rs index 00d226a2..dcbe53e1 100644 --- a/gui/src/app/state/spend/step.rs +++ b/gui/src/app/state/spend/step.rs @@ -432,7 +432,13 @@ impl Step for SaveSpend { .unwrap(); self.spend = Some(psbt::PsbtState::new( self.wallet.clone(), - SpendTx::new(None, psbt, draft.inputs.clone(), sigs), + SpendTx::new( + None, + psbt, + draft.inputs.clone(), + sigs, + self.wallet.main_descriptor.max_sat_vbytes(), + ), false, )); } diff --git a/gui/src/app/view/home.rs b/gui/src/app/view/home.rs index 7e59e74e..0a1a28cd 100644 --- a/gui/src/app/view/home.rs +++ b/gui/src/app/view/home.rs @@ -210,6 +210,15 @@ pub fn payment_view<'a>( .align_items(Alignment::Center) .push(h3("Miner fee: ").style(color::GREY_3)) .push(amount_with_size(&fee_amount, H3_SIZE)) + .push(text(" ").size(H3_SIZE)) + .push( + text(format!( + "({} sats/vbyte)", + fee_amount.to_sat() / tx.tx.vsize() as u64 + )) + .size(H4_SIZE) + .style(color::GREY_3), + ) })) .push(card::simple( Column::new() diff --git a/gui/src/app/view/psbt.rs b/gui/src/app/view/psbt.rs index f314ab36..587d17e8 100644 --- a/gui/src/app/view/psbt.rs +++ b/gui/src/app/view/psbt.rs @@ -196,7 +196,13 @@ pub fn spend_header<'a>(tx: &SpendTx) -> Element<'a, Message> { Row::new() .align_items(Alignment::Center) .push(h3("Miner fee: ").style(color::GREY_3)) - .push(amount_with_size(&tx.fee_amount, H3_SIZE)), + .push(amount_with_size(&tx.fee_amount, H3_SIZE)) + .push(text(" ").size(H3_SIZE)) + .push( + text(format!("(~{} sats/vbyte)", &tx.min_feerate_vb())) + .size(H4_SIZE) + .style(color::GREY_3), + ), ), ) .into() diff --git a/gui/src/app/view/transactions.rs b/gui/src/app/view/transactions.rs index 127cb194..20491080 100644 --- a/gui/src/app/view/transactions.rs +++ b/gui/src/app/view/transactions.rs @@ -171,6 +171,15 @@ pub fn tx_view<'a>( .align_items(Alignment::Center) .push(h3("Miner fee: ").style(color::GREY_3)) .push(amount_with_size(&fee_amount, H3_SIZE)) + .push(text(" ").size(H3_SIZE)) + .push( + text(format!( + "({} sats/vbyte)", + fee_amount.to_sat() / tx.tx.vsize() as u64 + )) + .size(H4_SIZE) + .style(color::GREY_3), + ) })), ), ) diff --git a/gui/src/daemon/mod.rs b/gui/src/daemon/mod.rs index 8b3a9426..1c4e975f 100644 --- a/gui/src/daemon/mod.rs +++ b/gui/src/daemon/mod.rs @@ -97,7 +97,13 @@ pub trait Daemon: Debug { .main .partial_spend_info(&tx.psbt) .map_err(|e| DaemonError::Unexpected(e.to_string()))?; - spend_txs.push(model::SpendTx::new(tx.updated_at, tx.psbt, coins, sigs)) + spend_txs.push(model::SpendTx::new( + tx.updated_at, + tx.psbt, + coins, + sigs, + info.descriptors.main.max_sat_vbytes(), + )) } spend_txs.sort_by(|a, b| { if a.status == b.status { diff --git a/gui/src/daemon/model.rs b/gui/src/daemon/model.rs index 2f6b53d1..b36d9e45 100644 --- a/gui/src/daemon/model.rs +++ b/gui/src/daemon/model.rs @@ -30,6 +30,9 @@ pub struct SpendTx { pub change_indexes: Vec, pub spend_amount: Amount, pub fee_amount: Amount, + /// The maximum size difference (in virtual bytes) of + /// an input in this transaction before and after satisfaction. + pub max_sat_vbytes: usize, pub status: SpendStatus, pub sigs: PartialSpendInfo, pub updated_at: Option, @@ -49,6 +52,7 @@ impl SpendTx { psbt: Psbt, coins: Vec, sigs: PartialSpendInfo, + max_sat_vbytes: usize, ) -> Self { let mut change_indexes = Vec::new(); let (change_amount, spend_amount) = psbt.unsigned_tx.output.iter().enumerate().fold( @@ -87,6 +91,7 @@ impl SpendTx { change_indexes, spend_amount, fee_amount: inputs_amount - spend_amount - change_amount, + max_sat_vbytes, status, sigs, } @@ -122,6 +127,14 @@ impl SpendTx { pub fn is_self_send(&self) -> bool { !self.coins.is_empty() && self.spend_amount == Amount::from_sat(0) } + + /// Feerate obtained if all transaction inputs have the maximum satisfaction size. + pub fn min_feerate_vb(&self) -> 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 + } } #[derive(Debug, Clone)]