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:
commit
03c37bd378
2
gui/Cargo.lock
generated
2
gui/Cargo.lock
generated
@ -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",
|
||||
|
||||
@ -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>),
|
||||
}
|
||||
|
||||
@ -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())
|
||||
|
||||
@ -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!()
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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())
|
||||
},
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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()
|
||||
}
|
||||
|
||||
|
||||
@ -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"))
|
||||
|
||||
@ -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,
|
||||
)
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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(())
|
||||
|
||||
@ -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(())
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user