Merge #736: Enhance labelling

b88781e62828cc6fc56e49f3ad1ebdeb39788573 remove font bold for labels (edouard)
8e806404c4e12f6d67cef320a4c5485723a0f993 change labels font weight for lists (edouard)
c170f7b25ba704beaa4e37a99302ccb25e0baa77 fix psbt labelling for single payment (edouard)
28aaca919aa7a1afc63a659780c6af063578a5ef gui: update liana:master (edouard)
0873cdb944e0bc4976b0f9dc6208fe68e322f330 disable confirm button if label len > 100 (edouard)
1aae2a52da94b6a3791a7a5b058e9e6a23483089 Add From: txid label for coin selection view (edouard)
07fbc0c1b3f3b55ebd7e684e2cf15032703c54db Attach outpoint and txid labels for single payment transaction (edouard)
bdac902307c25ee02093f369aca9258edacc4a4e Change coin change labelling (edouard)

Pull request description:

  tackle most issues of #732, may need another round of external tries

ACKs for top commit:
  edouardparis:
    Self-ACK b88781e62828cc6fc56e49f3ad1ebdeb39788573

Tree-SHA512: 51ee18d8a064afb6eb96926783f13fd7dc703e3464ff5af5d6f40e15ce86e16d8afd8b5f0ca82030976664101fd340f69a8c7102e88fd2c46d5755a4b0219286
This commit is contained in:
edouard 2023-10-23 18:53:46 +02:00
commit 03c37bd378
No known key found for this signature in database
GPG Key ID: E65F7A089C20DC8F
20 changed files with 372 additions and 202 deletions

2
gui/Cargo.lock generated
View File

@ -2113,7 +2113,7 @@ dependencies = [
[[package]]
name = "liana"
version = "2.0.0"
source = "git+https://github.com/wizardsardine/liana?branch=master#4f2ff1abc0e2700d563f3077eb71c22ef9573e5c"
source = "git+https://github.com/wizardsardine/liana?branch=master#6869d8554a6214d8e73614adf3c9334c1c6188d4"
dependencies = [
"backtrace",
"bip39",

View File

@ -35,5 +35,5 @@ pub enum Message {
ConnectedHardwareWallets(Vec<HardwareWallet>),
HistoryTransactions(Result<Vec<HistoryTransaction>, Error>),
PendingTransactions(Result<Vec<HistoryTransaction>, Error>),
LabelsUpdated(Result<HashMap<String, String>, Error>),
LabelsUpdated(Result<HashMap<String, Option<String>>, Error>),
}

View File

@ -29,10 +29,13 @@ pub struct Coins {
impl Labelled for Coins {
fn labelled(&self) -> Vec<LabelItem> {
self.list
.iter()
.map(|a| LabelItem::OutPoint(a.outpoint))
.collect()
let mut items = Vec::new();
for coin in &self.list {
items.push(LabelItem::OutPoint(coin.outpoint));
items.push(LabelItem::Txid(coin.outpoint.txid));
items.push(LabelItem::Address(coin.address.clone()));
}
items
}
fn labels(&mut self) -> &mut HashMap<String, String> {
&mut self.labels
@ -169,6 +172,7 @@ impl State for CoinsPanel {
let mut targets = HashSet::<LabelItem>::new();
for coin in coins {
targets.insert(LabelItem::OutPoint(coin.outpoint));
targets.insert(LabelItem::Txid(coin.outpoint.txid));
targets.insert(LabelItem::Address(coin.address));
}
daemon2.get_labels(&targets).map_err(|e| e.into())

View File

@ -26,34 +26,51 @@ impl LabelsEdited {
targets: T,
) -> Result<Command<Message>, Error> {
match message {
Message::View(view::Message::Label(labelled, msg)) => match msg {
Message::View(view::Message::Label(items, msg)) => match msg {
view::LabelMessage::Edited(value) => {
let valid = value.len() <= 100;
if let Some(label) = self.0.get_mut(&labelled) {
label.valid = valid;
label.value = value;
} else {
self.0.insert(labelled, form::Value { valid, value });
for item in items {
if let Some(label) = self.0.get_mut(&item) {
label.valid = valid;
label.value = value.clone();
} else {
self.0.insert(
item,
form::Value {
valid,
value: value.clone(),
},
);
}
}
}
view::LabelMessage::Cancel => {
self.0.remove(&labelled);
for item in items {
self.0.remove(&item);
}
}
view::LabelMessage::Confirm => {
if let Some(label) = self.0.get(&labelled).cloned() {
return Ok(Command::perform(
async move {
if let Some(item) = label_item_from_str(&labelled) {
daemon.update_labels(&HashMap::from([(
item,
label.value.clone(),
)]))?;
}
Ok(HashMap::from([(labelled, label.value)]))
},
Message::LabelsUpdated,
));
let mut updated_labels = HashMap::<LabelItem, Option<String>>::new();
let mut updated_labels_str = HashMap::<String, Option<String>>::new();
for item in items {
if let Some(label) = self.0.get(&item).cloned() {
let item_str = label_item_from_str(&item);
if label.value.is_empty() {
updated_labels.insert(item_str, None);
updated_labels_str.insert(item, None);
} else {
updated_labels.insert(item_str, Some(label.value.clone()));
updated_labels_str.insert(item, Some(label.value));
}
}
}
return Ok(Command::perform(
async move {
daemon.update_labels(&updated_labels)?;
Ok(updated_labels_str)
},
Message::LabelsUpdated,
));
}
},
Message::LabelsUpdated(res) => match res {
@ -75,14 +92,14 @@ impl LabelsEdited {
}
}
pub fn label_item_from_str(s: &str) -> Option<LabelItem> {
pub fn label_item_from_str(s: &str) -> LabelItem {
if let Ok(addr) = bitcoin::Address::from_str(s) {
Some(LabelItem::Address(addr.assume_checked()))
LabelItem::Address(addr.assume_checked())
} else if let Ok(txid) = bitcoin::Txid::from_str(s) {
Some(LabelItem::Txid(txid))
LabelItem::Txid(txid)
} else if let Ok(outpoint) = bitcoin::OutPoint::from_str(s) {
Some(LabelItem::OutPoint(outpoint))
LabelItem::OutPoint(outpoint)
} else {
None
unreachable!()
}
}

View File

@ -182,12 +182,11 @@ impl Action for SaveAction {
Message::View(view::Message::Spend(view::SpendTxMessage::Confirm)) => {
let daemon = daemon.clone();
let psbt = tx.psbt.clone();
let mut labels = HashMap::<LabelItem, String>::new();
let mut labels = HashMap::<LabelItem, Option<String>>::new();
for (item, label) in tx.labels() {
labels.insert(
label_item_from_str(item).expect("Must be a LabelItem"),
label.clone(),
);
if !label.is_empty() {
labels.insert(label_item_from_str(item), Some(label.clone()));
}
}
return Command::perform(
async move {

View File

@ -126,6 +126,7 @@ impl State for CreateSpendPanel {
let mut targets = HashSet::<LabelItem>::new();
for coin in coins {
targets.insert(LabelItem::OutPoint(coin.outpoint));
targets.insert(LabelItem::Txid(coin.outpoint.txid));
}
daemon2.get_labels(&targets).map_err(|e| e.into())
},

View File

@ -528,30 +528,12 @@ impl Step for SaveSpend {
if let Some(label) = &draft.batch_label {
tx.labels
.insert(tx.psbt.unsigned_tx.txid().to_string(), label.clone());
for (i, output) in tx.psbt.unsigned_tx.output.iter().enumerate() {
let address_str = Address::from_script(&output.script_pubkey, tx.network)
.unwrap()
.to_string();
if tx.change_indexes.contains(&i) && tx.labels.contains_key(&address_str) {
tx.labels
.insert(address_str, format!("Change of {}", label.clone()));
}
}
}
} else if let Some(recipient) = draft.recipients.first() {
if !recipient.label.value.is_empty() {
let label = recipient.label.value.clone();
tx.labels
.insert(tx.psbt.unsigned_tx.txid().to_string(), label.clone());
for (i, output) in tx.psbt.unsigned_tx.output.iter().enumerate() {
let address_str = Address::from_script(&output.script_pubkey, tx.network)
.unwrap()
.to_string();
if tx.change_indexes.contains(&i) && tx.labels.contains_key(&address_str) {
tx.labels
.insert(address_str, format!("Change of {}", label.clone()));
}
}
.insert(tx.psbt.unsigned_tx.txid().to_string(), label);
}
}

View File

@ -64,6 +64,7 @@ fn coin_list_view<'a>(
) -> Container<'a, Message> {
let outpoint = coin.outpoint.to_string();
let address = coin.address.to_string();
let txid = coin.outpoint.txid.to_string();
Container::new(
Column::new()
.push(
@ -74,7 +75,38 @@ fn coin_list_view<'a>(
.push(badge::coin())
.push(if !collapsed {
if let Some(label) = labels.get(&outpoint) {
Container::new(p1_bold(label)).width(Length::Fill)
if !label.is_empty() {
Container::new(p1_regular(label)).width(Length::Fill)
} else if let Some(label) = labels.get(&txid) {
Container::new(
Row::new()
.spacing(5)
.push(
// It it not possible to know if a coin is a
// change coin or not so for now, From is
// enough
p1_regular("From").style(color::GREY_3),
)
.push(p1_regular(label)),
)
.width(Length::Fill)
} else {
Container::new(Space::with_width(Length::Fill))
.width(Length::Fill)
}
} else if let Some(label) = labels.get(&txid) {
Container::new(
Row::new()
.spacing(5)
.push(
// It it not possible to know if a coin is a
// change coin or not so for now, From is
// enough
p1_regular("From").style(color::GREY_3),
)
.push(p1_regular(label)),
)
.width(Length::Fill)
} else {
Container::new(Space::with_width(Length::Fill))
.width(Length::Fill)
@ -110,10 +142,10 @@ fn coin_list_view<'a>(
.spacing(5)
.push(
Container::new(if let Some(label) = labels_editing.get(&outpoint) {
label::label_editing(outpoint.clone(), label, P1_SIZE)
label::label_editing(vec![outpoint.clone()], label, P1_SIZE)
} else {
label::label_editable(
outpoint.clone(),
vec![outpoint.clone()],
labels.get(&outpoint),
P1_SIZE,
)
@ -141,6 +173,21 @@ fn coin_list_view<'a>(
})
.push(
Column::new()
.push(
Row::new()
.align_items(Alignment::Center)
.push(
p2_regular("Address label:")
.bold()
.style(color::GREY_2),
)
.push(if let Some(label) = labels.get(&address) {
p2_regular(label).style(color::GREY_2)
} else {
p2_regular("No label").style(color::GREY_2)
})
.spacing(5),
)
.push(
Row::new()
.align_items(Alignment::Center)
@ -166,11 +213,11 @@ fn coin_list_view<'a>(
Row::new()
.align_items(Alignment::Center)
.push(
p2_regular("Address label:")
p2_regular("Deposit transaction label:")
.bold()
.style(color::GREY_2),
)
.push(if let Some(label) = labels.get(&address) {
.push(if let Some(label) = labels.get(&txid) {
p2_regular(label).style(color::GREY_2)
} else {
p2_regular("No label").style(color::GREY_2)

View File

@ -19,7 +19,7 @@ use crate::{
menu::Menu,
view::{coins, dashboard, label, message::Message},
},
daemon::model::HistoryTransaction,
daemon::model::{HistoryTransaction, TransactionKind},
};
pub const HISTORY_EVENT_PAGE_SIZE: u64 = 20;
@ -103,7 +103,7 @@ pub fn home_view<'a>(
.push(pending_events.iter().enumerate().fold(
Column::new().spacing(10),
|col, (i, event)| {
if !event.is_self_send() {
if !event.is_send_to_self() {
col.push(event_list_view(i, event))
} else {
col
@ -113,7 +113,7 @@ pub fn home_view<'a>(
.push(events.iter().enumerate().fold(
Column::new().spacing(10),
|col, (i, event)| {
if !event.is_self_send() {
if !event.is_send_to_self() {
col.push(event_list_view(i + pending_events.len(), event))
} else {
col
@ -157,7 +157,7 @@ fn event_list_view(i: usize, event: &HistoryTransaction) -> Column<'_, Message>
}
.to_string(),
) {
Some(p1_bold(label))
Some(p1_regular(label))
} else {
event
.labels
@ -166,7 +166,9 @@ fn event_list_view(i: usize, event: &HistoryTransaction) -> Column<'_, Message>
.unwrap()
.to_string(),
)
.map(|label| p1_bold(format!("address label: {}", label)).style(color::GREY_3))
.map(|label| {
p1_regular(format!("address label: {}", label)).style(color::GREY_3)
})
};
if event.is_external() {
if !event.change_indexes.contains(&output_index) {
@ -223,28 +225,52 @@ pub fn payment_view<'a>(
cache,
warning,
Column::new()
.push(if tx.is_self_send() {
Container::new(h3("Payment")).width(Length::Fill)
} else if tx.is_external() {
Container::new(h3("Incoming payment")).width(Length::Fill)
} else {
Container::new(h3("Outgoing payment")).width(Length::Fill)
.push(match tx.kind {
TransactionKind::OutgoingSinglePayment(_)
| TransactionKind::OutgoingPaymentBatch(_) => {
Container::new(h3("Outgoing payment")).width(Length::Fill)
}
TransactionKind::IncomingSinglePayment(_)
| TransactionKind::IncomingPaymentBatch(_) => {
Container::new(h3("Incoming payment")).width(Length::Fill)
}
_ => Container::new(h3("Payment")).width(Length::Fill),
})
.push(if let Some(label) = labels_editing.get(&outpoint) {
label::label_editing(outpoint.clone(), label, H3_SIZE)
.push(if tx.is_single_payment().is_some() {
// if the payment is a payment of a single payment transaction then
// the label of the transaction is attached to the label of the payment outpoint
if let Some(label) = labels_editing.get(&outpoint) {
label::label_editing(vec![outpoint.clone(), txid.clone()], label, H3_SIZE)
} else {
label::label_editable(
vec![outpoint.clone(), txid.clone()],
tx.labels.get(&outpoint),
H3_SIZE,
)
}
} else if let Some(label) = labels_editing.get(&outpoint) {
label::label_editing(vec![outpoint.clone()], label, H3_SIZE)
} else {
label::label_editable(outpoint.clone(), tx.labels.get(&outpoint), H1_SIZE)
label::label_editable(vec![outpoint.clone()], tx.labels.get(&outpoint), H3_SIZE)
})
.push(Container::new(amount_with_size(
&Amount::from_sat(tx.tx.output[output_index].value),
H1_SIZE,
H3_SIZE,
)))
.push(Space::with_height(H3_SIZE))
.push(Container::new(h3("Transaction")).width(Length::Fill))
.push(if let Some(label) = labels_editing.get(&txid) {
label::label_editing(txid.clone(), label, H3_SIZE)
.push_maybe(if tx.is_batch() {
if let Some(label) = labels_editing.get(&txid) {
Some(label::label_editing(vec![txid.clone()], label, H3_SIZE))
} else {
Some(label::label_editable(
vec![txid.clone()],
tx.labels.get(&txid),
H3_SIZE,
))
}
} else {
label::label_editable(txid.clone(), tx.labels.get(&txid), H3_SIZE)
None
})
.push_maybe(tx.fee_amount.map(|fee_amount| {
Row::new()

View File

@ -3,14 +3,14 @@ use iced::{widget::row, Alignment};
use liana_ui::{
color,
component::{button, form},
font, icon,
icon,
widget::*,
};
use crate::app::view;
pub fn label_editable(
labelled: String,
labelled: Vec<String>,
label: Option<&String>,
size: u16,
) -> Element<'_, view::Message> {
@ -18,7 +18,7 @@ pub fn label_editable(
if !label.is_empty() {
return Container::new(
row!(
iced::widget::Text::new(label).size(size).font(font::BOLD),
iced::widget::Text::new(label).size(size),
button::primary(Some(icon::pencil_icon()), "Edit").on_press(
view::Message::Label(
labelled,
@ -36,7 +36,6 @@ pub fn label_editable(
row!(
iced::widget::Text::new("Add Label")
.size(size)
.font(font::BOLD)
.style(color::GREY_3),
button::primary(Some(icon::pencil_icon()), "Edit").on_press(view::Message::Label(
labelled,
@ -50,7 +49,7 @@ pub fn label_editable(
}
pub fn label_editing(
labelled: String,
labelled: Vec<String>,
label: &form::Value<String>,
size: u16,
) -> Element<view::Message> {
@ -60,7 +59,11 @@ pub fn label_editing(
.warning("Invalid label length, cannot be superior to 100")
.size(size)
.padding(10),
button::primary(None, "Save").on_press(view::message::LabelMessage::Confirm),
if label.valid {
button::primary(None, "Save").on_press(view::message::LabelMessage::Confirm)
} else {
button::primary(None, "Save")
},
button::primary(None, "Cancel").on_press(view::message::LabelMessage::Cancel)
)
.spacing(5)

View File

@ -9,7 +9,7 @@ pub enum Message {
Close,
Select(usize),
SelectSub(usize, usize),
Label(String, LabelMessage),
Label(Vec<String>, LabelMessage),
Settings(SettingsMessage),
CreateSpend(CreateSpendMessage),
ImportSpend(ImportSpendMessage),

View File

@ -198,14 +198,25 @@ pub fn spend_header<'a>(
let txid = tx.psbt.unsigned_tx.txid().to_string();
Column::new()
.spacing(20)
.push(if let Some(label) = labels_editing.get(&txid) {
label::label_editing(txid.clone(), label, H3_SIZE)
.push(if let Some(outpoint) = tx.is_single_payment() {
let outpoint = outpoint.to_string();
if let Some(label) = labels_editing.get(&outpoint) {
label::label_editing(vec![outpoint.clone(), txid.clone()], label, H3_SIZE)
} else {
label::label_editable(
vec![outpoint.clone(), txid.clone()],
tx.labels.get(&outpoint),
H3_SIZE,
)
}
} else if let Some(label) = labels_editing.get(&txid) {
label::label_editing(vec![txid.clone()], label, H3_SIZE)
} else {
label::label_editable(txid.clone(), tx.labels.get(&txid), H1_SIZE)
label::label_editable(vec![txid.clone()], tx.labels.get(&txid), H3_SIZE)
})
.push(
Column::new()
.push(if tx.is_self_send() {
.push(if tx.is_send_to_self() {
Container::new(h1("Self-transfer"))
} else {
Container::new(amount_with_size(&tx.spend_amount, H1_SIZE))
@ -707,15 +718,8 @@ pub fn inputs_and_outputs_view<'a>(
.filter(|(i, _)| change_indexes.as_ref().unwrap().contains(i))
.fold(
Column::new().padding(20),
|col: Column<'a, Message>, (i, output)| {
col.spacing(10).push(change_view(
i,
tx.txid(),
output,
network,
labels,
labels_editing,
))
|col: Column<'a, Message>, (_, output)| {
col.spacing(10).push(change_view(output, network))
},
)
.into()
@ -745,10 +749,10 @@ fn input_view<'a>(
.align_items(Alignment::Center)
.push(
Container::new(if let Some(label) = labels_editing.get(&outpoint) {
label::label_editing(outpoint.clone(), label, text::P1_SIZE)
label::label_editing(vec![outpoint.clone()], label, text::P1_SIZE)
} else {
label::label_editable(
outpoint.clone(),
vec![outpoint.clone()],
labels.get(&outpoint),
text::P1_SIZE,
)
@ -832,10 +836,10 @@ fn payment_view<'a>(
.align_items(Alignment::Center)
.push(
Container::new(if let Some(label) = labels_editing.get(&outpoint) {
label::label_editing(outpoint.clone(), label, text::P1_SIZE)
label::label_editing(vec![outpoint.clone()], label, text::P1_SIZE)
} else {
label::label_editable(
outpoint.clone(),
vec![outpoint.clone()],
labels.get(&outpoint),
text::P1_SIZE,
)
@ -881,77 +885,32 @@ fn payment_view<'a>(
.into()
}
fn change_view<'a>(
i: usize,
txid: Txid,
output: &'a TxOut,
network: Network,
labels: &'a HashMap<String, String>,
labels_editing: &'a HashMap<String, form::Value<String>>,
) -> Element<'a, Message> {
fn change_view(output: &TxOut, network: Network) -> Element<Message> {
let addr = Address::from_script(&output.script_pubkey, network)
.unwrap()
.to_string();
let outpoint = OutPoint {
txid,
vout: i as u32,
}
.to_string();
Column::new()
Row::new()
.width(Length::Fill)
.spacing(5)
.push(
Row::new()
.spacing(5)
.align_items(Alignment::Center)
.push(
Container::new(if let Some(label) = labels_editing.get(&outpoint) {
label::label_editing(outpoint.clone(), label, text::P1_SIZE)
} else {
label::label_editable(
outpoint.clone(),
labels.get(&outpoint),
text::P1_SIZE,
)
})
.width(Length::Fill),
)
.push(amount(&Amount::from_sat(output.value))),
)
.push(
Column::new()
.width(Length::Fill)
.push(
Row::new()
.align_items(Alignment::Center)
.width(Length::Fill)
.spacing(5)
.push(p1_bold("Address:").style(color::GREY_3))
.push(p2_regular(addr.clone()).style(color::GREY_3))
.push(
Row::new()
.align_items(Alignment::Center)
.width(Length::Fill)
.spacing(5)
.push(p1_bold("Address:").style(color::GREY_3))
.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()))
.style(theme::Button::TransparentBorder),
),
Button::new(icon::clipboard_icon().style(color::GREY_3))
.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(amount(&Amount::from_sat(output.value)))
.into()
}

View File

@ -96,7 +96,7 @@ fn spend_tx_list_view(i: usize, tx: &SpendTx) -> Element<'_, Message> {
Row::new()
.push(
Row::new()
.push(if tx.is_self_send() {
.push(if tx.is_send_to_self() {
badge::cycle()
} else {
badge::spend()
@ -127,7 +127,7 @@ fn spend_tx_list_view(i: usize, tx: &SpendTx) -> Element<'_, Message> {
.push_maybe(
tx.labels
.get(&tx.psbt.unsigned_tx.txid().to_string())
.map(p1_bold),
.map(p1_regular),
)
.spacing(10)
.align_items(Alignment::Center)
@ -147,7 +147,7 @@ fn spend_tx_list_view(i: usize, tx: &SpendTx) -> Element<'_, Message> {
.push(
Column::new()
.align_items(Alignment::End)
.push(if !tx.is_self_send() {
.push(if !tx.is_send_to_self() {
Container::new(amount(&tx.spend_amount))
} else {
Container::new(p1_regular("Self-transfer"))

View File

@ -52,10 +52,14 @@ pub fn receive<'a>(
card::simple(
Column::new()
.push(if let Some(label) = labels_editing.get(&addr) {
label::label_editing(addr.clone(), label, text::P1_SIZE)
label::label_editing(
vec![addr.clone()],
label,
text::P1_SIZE,
)
} else {
label::label_editable(
addr.clone(),
vec![addr.clone()],
labels.get(&addr),
text::P1_SIZE,
)

View File

@ -360,9 +360,22 @@ fn coin_list_view<'a>(
}))
.push(
if let Some(label) = coins_labels.get(&coin.outpoint.to_string()) {
Container::new(p1_bold(label)).width(Length::Fill)
Container::new(p1_regular(label)).width(Length::Fill)
} else if let Some(label) = coins_labels.get(&coin.outpoint.txid.to_string()) {
Container::new(
Row::new()
.spacing(5)
.push(
// It it not possible to know if a coin is a
// change coin or not so for now, From is
// enough
p1_regular("From").style(color::GREY_3),
)
.push(p1_regular(label)),
)
.width(Length::Fill)
} else {
Container::new(p1_bold("")).width(Length::Fill)
Container::new(p1_regular("")).width(Length::Fill)
},
)
.push(if coin.spend_info.is_some() {

View File

@ -92,14 +92,16 @@ fn tx_list_view(i: usize, tx: &HistoryTransaction) -> Element<'_, Message> {
Row::new()
.push(if tx.is_external() {
badge::receive()
} else if tx.is_self_send() {
} else if tx.is_send_to_self() {
badge::cycle()
} else {
badge::spend()
})
.push(
Column::new()
.push_maybe(tx.labels.get(&tx.tx.txid().to_string()).map(p1_bold))
.push_maybe(
tx.labels.get(&tx.tx.txid().to_string()).map(p1_regular),
)
.push_maybe(tx.time.map(|t| {
Container::new(
text(format!(
@ -165,22 +167,35 @@ pub fn tx_view<'a>(
cache,
warning,
Column::new()
.push(if tx.is_self_send() {
.push(if tx.is_send_to_self() {
Container::new(h3("Transaction")).width(Length::Fill)
} else if tx.is_external() {
Container::new(h3("Incoming transaction")).width(Length::Fill)
} else {
Container::new(h3("Outgoing transaction")).width(Length::Fill)
})
.push(if let Some(label) = labels_editing.get(&txid) {
label::label_editing(txid.clone(), label, H3_SIZE)
.push(if let Some(outpoint) = tx.is_single_payment() {
// if the payment is a payment of a single payment transaction then
// the label of the transaction is attached to the label of the payment outpoint
let outpoint = outpoint.to_string();
if let Some(label) = labels_editing.get(&outpoint) {
label::label_editing(vec![outpoint.clone(), txid.clone()], label, H3_SIZE)
} else {
label::label_editable(
vec![outpoint.clone(), txid.clone()],
tx.labels.get(&outpoint),
H3_SIZE,
)
}
} else if let Some(label) = labels_editing.get(&txid) {
label::label_editing(vec![txid.clone()], label, H3_SIZE)
} else {
label::label_editable(txid.clone(), tx.labels.get(&txid), H1_SIZE)
label::label_editable(vec![txid.clone()], tx.labels.get(&txid), H1_SIZE)
})
.push(
Column::new().spacing(20).push(
Column::new()
.push(if tx.is_self_send() {
.push(if tx.is_send_to_self() {
Container::new(h1("Self-transfer"))
} else if tx.is_external() {
Container::new(amount_with_size(&tx.incoming_amount, H1_SIZE))

View File

@ -157,8 +157,8 @@ impl<C: Client + Debug> Daemon for Lianad<C> {
Ok(res.labels)
}
fn update_labels(&self, items: &HashMap<LabelItem, String>) -> Result<(), DaemonError> {
let labels: HashMap<String, String> =
fn update_labels(&self, items: &HashMap<LabelItem, Option<String>>) -> Result<(), DaemonError> {
let labels: HashMap<String, Option<String>> =
HashMap::from_iter(items.iter().map(|(a, l)| (a.to_string(), l.clone())));
let _res: serde_json::value::Value = self.call("updatelabels", Some(vec![labels]))?;
Ok(())

View File

@ -135,7 +135,7 @@ impl Daemon for EmbeddedDaemon {
Ok(self.handle.control.get_labels(items).labels)
}
fn update_labels(&self, items: &HashMap<LabelItem, String>) -> Result<(), DaemonError> {
fn update_labels(&self, items: &HashMap<LabelItem, Option<String>>) -> Result<(), DaemonError> {
self.handle.control.update_labels(items);
Ok(())
}

View File

@ -5,6 +5,7 @@ pub mod model;
use std::collections::{HashMap, HashSet};
use std::fmt::Debug;
use std::io::ErrorKind;
use std::iter::FromIterator;
use liana::{
commands::LabelItem,
@ -80,7 +81,8 @@ pub trait Daemon: Debug {
&self,
labels: &HashSet<LabelItem>,
) -> Result<HashMap<String, String>, DaemonError>;
fn update_labels(&self, labels: &HashMap<LabelItem, String>) -> Result<(), DaemonError>;
fn update_labels(&self, labels: &HashMap<LabelItem, Option<String>>)
-> Result<(), DaemonError>;
fn list_spend_transactions(&self) -> Result<Vec<model::SpendTx>, DaemonError> {
let info = self.get_info()?;
@ -228,7 +230,12 @@ fn load_labels<T: model::Labelled, D: Daemon + ?Sized>(
items.insert(item);
}
}
let labels = daemon.get_labels(&items)?;
let labels = HashMap::from_iter(
daemon
.get_labels(&items)?
.into_iter()
.map(|(k, v)| (k, Some(v))),
);
for target in targets {
target.load_labels(&labels);
}

View File

@ -41,6 +41,7 @@ pub struct SpendTx {
pub status: SpendStatus,
pub sigs: PartialSpendInfo,
pub updated_at: Option<u32>,
pub kind: TransactionKind,
}
#[derive(PartialOrd, Ord, Debug, Clone, PartialEq, Eq)]
@ -92,6 +93,31 @@ impl SpendTx {
Self {
labels: HashMap::new(),
kind: if spend_amount == Amount::from_sat(0) {
TransactionKind::SendToSelf
} else {
let outpoints: Vec<OutPoint> = psbt
.unsigned_tx
.output
.iter()
.enumerate()
.filter_map(|(i, _)| {
if !change_indexes.contains(&i) {
Some(OutPoint {
txid: psbt.unsigned_tx.txid(),
vout: i as u32,
})
} else {
None
}
})
.collect();
if outpoints.len() == 1 {
TransactionKind::OutgoingSinglePayment(outpoints[0])
} else {
TransactionKind::OutgoingPaymentBatch(outpoints)
}
},
updated_at,
coins,
psbt,
@ -132,10 +158,6 @@ impl SpendTx {
signers
}
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).
@ -144,15 +166,23 @@ impl SpendTx {
self.fee_amount.to_sat() / max_tx_vbytes as u64
}
pub fn is_send_to_self(&self) -> bool {
matches!(self.kind, TransactionKind::SendToSelf)
}
pub fn is_single_payment(&self) -> Option<OutPoint> {
match self.kind {
TransactionKind::IncomingSinglePayment(outpoint) => Some(outpoint),
TransactionKind::OutgoingSinglePayment(outpoint) => Some(outpoint),
_ => None,
}
}
pub fn is_batch(&self) -> bool {
self.psbt
.unsigned_tx
.output
.iter()
.enumerate()
.filter(|(i, _)| !self.change_indexes.contains(i))
.count()
> 1
matches!(
self.kind,
TransactionKind::IncomingPaymentBatch(_) | TransactionKind::OutgoingPaymentBatch(_)
)
}
}
@ -193,6 +223,7 @@ pub struct HistoryTransaction {
pub fee_amount: Option<Amount>,
pub height: Option<i32>,
pub time: Option<u32>,
pub kind: TransactionKind,
}
impl HistoryTransaction {
@ -228,6 +259,47 @@ 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)
}
},
tx,
coins,
change_indexes,
@ -241,24 +313,41 @@ impl HistoryTransaction {
}
pub fn is_external(&self) -> bool {
self.coins.is_empty()
matches!(
self.kind,
TransactionKind::IncomingSinglePayment(_) | TransactionKind::IncomingPaymentBatch(_)
)
}
pub fn is_self_send(&self) -> bool {
!self.coins.is_empty() && self.outgoing_amount == Amount::from_sat(0)
pub fn is_send_to_self(&self) -> bool {
matches!(self.kind, TransactionKind::SendToSelf)
}
pub fn is_single_payment(&self) -> Option<OutPoint> {
match self.kind {
TransactionKind::IncomingSinglePayment(outpoint) => Some(outpoint),
TransactionKind::OutgoingSinglePayment(outpoint) => Some(outpoint),
_ => None,
}
}
pub fn is_batch(&self) -> bool {
self.tx
.output
.iter()
.enumerate()
.filter(|(i, _)| !self.change_indexes.contains(i))
.count()
> 1
matches!(
self.kind,
TransactionKind::IncomingPaymentBatch(_) | TransactionKind::OutgoingPaymentBatch(_)
)
}
}
#[derive(Debug, Clone)]
pub enum TransactionKind {
IncomingSinglePayment(OutPoint),
IncomingPaymentBatch(Vec<OutPoint>),
SendToSelf,
OutgoingSinglePayment(OutPoint),
OutgoingPaymentBatch(Vec<OutPoint>),
}
impl Labelled for HistoryTransaction {
fn labels(&mut self) -> &mut HashMap<String, String> {
&mut self.labels
@ -287,13 +376,17 @@ impl Labelled for HistoryTransaction {
pub trait Labelled {
fn labelled(&self) -> Vec<LabelItem>;
fn labels(&mut self) -> &mut HashMap<String, String>;
fn load_labels(&mut self, new_labels: &HashMap<String, String>) {
fn load_labels(&mut self, new_labels: &HashMap<String, Option<String>>) {
let items = self.labelled();
let labels = self.labels();
for item in items {
let item_str = item.to_string();
if let Some(l) = new_labels.get(&item_str) {
labels.insert(item_str, l.to_string());
if let Some(label) = new_labels.get(&item_str) {
if let Some(l) = label {
labels.insert(item_str, l.to_string());
} else {
labels.remove(&item_str);
}
}
}
}