Merge #621: Gui labels
757b53ebab43c05a444ce22c8dd666fb367e6ee0 cargo update -p liana (edouard)
aeff735ac320e4c3addb11460869fc0cb0146802 fix unconfirmed payments layouts with labels (edouard)
dbb91464c82bb11be4fab679a673c3695248dd7d fix update labels for pending_txs (edouard)
9edcdd9a4e4170ce48ca656e0e8f36dcbe42ff07 Add labels to change outputs according to main label (edouard)
2354ac9175b5766286852a035c106e44425ffb72 cargo clippy --fix --lib -p liana_gui (edouard)
9db4541952561de8d50bdee35fa7ec737c3e65e8 Add labels support to gui (edouard)
Pull request description:
This PR use the lianad update_labels and get_labels commands.
It also introduce new concepts from talks with Kevin and Antoine:
- User spending is creating a payment, when he does not add multiple recipients anymore, he is doing multiple payment in one bitcoin transaction.
- a transaction that have multiple outgoing outputs (multiple payment) is tagged as a 'Batch'.
ACKs for top commit:
edouardparis:
Self-ACK 757b53ebab43c05a444ce22c8dd666fb367e6ee0
Tree-SHA512: 2208009fcc0ab8f587929a347d191d428156984e87b732fb6efa5f08a3962c85510c82fcdeba687a6d2bd2fa45f5a17384d8a9b709e1407ebbbe0f451fe69e90
This commit is contained in:
commit
2fbafd9325
4
gui/Cargo.lock
generated
4
gui/Cargo.lock
generated
@ -2112,8 +2112,8 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "liana"
|
name = "liana"
|
||||||
version = "1.0.0"
|
version = "2.0.0"
|
||||||
source = "git+https://github.com/wizardsardine/liana?branch=master#85d470dd8dd67e6726118fe6dd86f9b4c8d3b0ef"
|
source = "git+https://github.com/wizardsardine/liana?branch=master#605a13d4bab662f832b8fcb0d915eb17d0360c1f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"backtrace",
|
"backtrace",
|
||||||
"bip39",
|
"bip39",
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use liana::{
|
use liana::{
|
||||||
@ -22,6 +23,7 @@ pub enum Message {
|
|||||||
Info(Result<GetInfoResult, Error>),
|
Info(Result<GetInfoResult, Error>),
|
||||||
ReceiveAddress(Result<Address, Error>),
|
ReceiveAddress(Result<Address, Error>),
|
||||||
Coins(Result<Vec<Coin>, Error>),
|
Coins(Result<Vec<Coin>, Error>),
|
||||||
|
Labels(Result<HashMap<String, String>, Error>),
|
||||||
SpendTxs(Result<Vec<SpendTx>, Error>),
|
SpendTxs(Result<Vec<SpendTx>, Error>),
|
||||||
Psbt(Result<Psbt, Error>),
|
Psbt(Result<Psbt, Error>),
|
||||||
Recovery(Result<SpendTx, Error>),
|
Recovery(Result<SpendTx, Error>),
|
||||||
@ -33,4 +35,5 @@ pub enum Message {
|
|||||||
ConnectedHardwareWallets(Vec<HardwareWallet>),
|
ConnectedHardwareWallets(Vec<HardwareWallet>),
|
||||||
HistoryTransactions(Result<Vec<HistoryTransaction>, Error>),
|
HistoryTransactions(Result<Vec<HistoryTransaction>, Error>),
|
||||||
PendingTransactions(Result<Vec<HistoryTransaction>, Error>),
|
PendingTransactions(Result<Vec<HistoryTransaction>, Error>),
|
||||||
|
LabelsUpdated(Result<HashMap<String, String>, Error>),
|
||||||
}
|
}
|
||||||
|
|||||||
@ -97,6 +97,7 @@ impl App {
|
|||||||
self.wallet.clone(),
|
self.wallet.clone(),
|
||||||
&self.cache.coins,
|
&self.cache.coins,
|
||||||
self.cache.blockheight as u32,
|
self.cache.blockheight as u32,
|
||||||
|
self.cache.network,
|
||||||
)
|
)
|
||||||
.into(),
|
.into(),
|
||||||
menu::Menu::RefreshCoins(preselected) => CreateSpendPanel::new_self_send(
|
menu::Menu::RefreshCoins(preselected) => CreateSpendPanel::new_self_send(
|
||||||
@ -104,6 +105,7 @@ impl App {
|
|||||||
&self.cache.coins,
|
&self.cache.coins,
|
||||||
self.cache.blockheight as u32,
|
self.cache.blockheight as u32,
|
||||||
preselected,
|
preselected,
|
||||||
|
self.cache.network,
|
||||||
)
|
)
|
||||||
.into(),
|
.into(),
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
//! Settings is the module to handle the GUI settings file.
|
||||||
|
//! The settings file is used by the GUI to store useful information.
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fs::OpenOptions;
|
use std::fs::OpenOptions;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
@ -8,8 +10,6 @@ use serde::{Deserialize, Serialize};
|
|||||||
|
|
||||||
use crate::{app::wallet::Wallet, hw::HardwareWalletConfig};
|
use crate::{app::wallet::Wallet, hw::HardwareWalletConfig};
|
||||||
|
|
||||||
///! Settings is the module to handle the GUI settings file.
|
|
||||||
///! The settings file is used by the GUI to store useful information.
|
|
||||||
pub const DEFAULT_FILE_NAME: &str = "settings.json";
|
pub const DEFAULT_FILE_NAME: &str = "settings.json";
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
|
|||||||
@ -1,18 +1,48 @@
|
|||||||
use std::cmp::Ordering;
|
use std::collections::HashMap;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use std::{cmp::Ordering, collections::HashSet};
|
||||||
|
|
||||||
use iced::Command;
|
use iced::Command;
|
||||||
|
|
||||||
use liana_ui::widget::Element;
|
use liana_ui::widget::Element;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
app::{cache::Cache, error::Error, menu::Menu, message::Message, state::State, view},
|
app::{
|
||||||
daemon::{model::Coin, Daemon},
|
cache::Cache,
|
||||||
|
error::Error,
|
||||||
|
menu::Menu,
|
||||||
|
message::Message,
|
||||||
|
state::{label::LabelsEdited, State},
|
||||||
|
view,
|
||||||
|
},
|
||||||
|
daemon::{
|
||||||
|
model::{Coin, LabelItem, Labelled},
|
||||||
|
Daemon,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct Coins {
|
||||||
|
list: Vec<Coin>,
|
||||||
|
labels: HashMap<String, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Labelled for Coins {
|
||||||
|
fn labelled(&self) -> Vec<LabelItem> {
|
||||||
|
self.list
|
||||||
|
.iter()
|
||||||
|
.map(|a| LabelItem::OutPoint(a.outpoint))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
fn labels(&mut self) -> &mut HashMap<String, String> {
|
||||||
|
&mut self.labels
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct CoinsPanel {
|
pub struct CoinsPanel {
|
||||||
coins: Vec<Coin>,
|
coins: Coins,
|
||||||
selected: Vec<usize>,
|
selected: Vec<usize>,
|
||||||
|
labels_edited: LabelsEdited,
|
||||||
warning: Option<Error>,
|
warning: Option<Error>,
|
||||||
/// timelock value to pass for the heir to consume a coin.
|
/// timelock value to pass for the heir to consume a coin.
|
||||||
timelock: u16,
|
timelock: u16,
|
||||||
@ -21,7 +51,8 @@ pub struct CoinsPanel {
|
|||||||
impl CoinsPanel {
|
impl CoinsPanel {
|
||||||
pub fn new(coins: &[Coin], timelock: u16) -> Self {
|
pub fn new(coins: &[Coin], timelock: u16) -> Self {
|
||||||
let mut panel = Self {
|
let mut panel = Self {
|
||||||
coins: Vec::new(),
|
labels_edited: LabelsEdited::default(),
|
||||||
|
coins: Coins::default(),
|
||||||
selected: Vec::new(),
|
selected: Vec::new(),
|
||||||
warning: None,
|
warning: None,
|
||||||
timelock,
|
timelock,
|
||||||
@ -31,18 +62,14 @@ impl CoinsPanel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn update_coins(&mut self, coins: &[Coin]) {
|
fn update_coins(&mut self, coins: &[Coin]) {
|
||||||
self.coins = coins
|
self.coins.list = coins
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|coin| {
|
.filter(|coin| coin.spend_info.is_none())
|
||||||
if coin.spend_info.is_none() {
|
.cloned()
|
||||||
Some(coin.clone())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
self.coins
|
self.coins
|
||||||
|
.list
|
||||||
.sort_by(|a, b| match (a.block_height, b.block_height) {
|
.sort_by(|a, b| match (a.block_height, b.block_height) {
|
||||||
(Some(a_height), Some(b_height)) => {
|
(Some(a_height), Some(b_height)) => {
|
||||||
if a_height == b_height {
|
if a_height == b_height {
|
||||||
@ -64,13 +91,20 @@ impl State for CoinsPanel {
|
|||||||
&Menu::Coins,
|
&Menu::Coins,
|
||||||
cache,
|
cache,
|
||||||
self.warning.as_ref(),
|
self.warning.as_ref(),
|
||||||
view::coins::coins_view(cache, &self.coins, self.timelock, &self.selected),
|
view::coins::coins_view(
|
||||||
|
cache,
|
||||||
|
&self.coins.list,
|
||||||
|
self.timelock,
|
||||||
|
&self.selected,
|
||||||
|
&self.coins.labels,
|
||||||
|
self.labels_edited.cache(),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update(
|
fn update(
|
||||||
&mut self,
|
&mut self,
|
||||||
_daemon: Arc<dyn Daemon + Sync + Send>,
|
daemon: Arc<dyn Daemon + Sync + Send>,
|
||||||
_cache: &Cache,
|
_cache: &Cache,
|
||||||
message: Message,
|
message: Message,
|
||||||
) -> Command<Message> {
|
) -> Command<Message> {
|
||||||
@ -83,6 +117,24 @@ impl State for CoinsPanel {
|
|||||||
self.update_coins(&coins);
|
self.update_coins(&coins);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
Message::Labels(res) => match res {
|
||||||
|
Err(e) => self.warning = Some(e),
|
||||||
|
Ok(labels) => {
|
||||||
|
self.coins.labels = labels;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Message::View(view::Message::Label(_, _)) | Message::LabelsUpdated(_) => {
|
||||||
|
match self.labels_edited.update(
|
||||||
|
daemon,
|
||||||
|
message,
|
||||||
|
std::iter::once(&mut self.coins).map(|a| a as &mut dyn Labelled),
|
||||||
|
) {
|
||||||
|
Ok(cmd) => return cmd,
|
||||||
|
Err(e) => {
|
||||||
|
self.warning = Some(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Message::View(view::Message::Select(i)) => {
|
Message::View(view::Message::Select(i)) => {
|
||||||
if let Some(position) = self.selected.iter().position(|j| *j == i) {
|
if let Some(position) = self.selected.iter().position(|j| *j == i) {
|
||||||
self.selected.remove(position);
|
self.selected.remove(position);
|
||||||
@ -96,16 +148,34 @@ impl State for CoinsPanel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn load(&self, daemon: Arc<dyn Daemon + Sync + Send>) -> Command<Message> {
|
fn load(&self, daemon: Arc<dyn Daemon + Sync + Send>) -> Command<Message> {
|
||||||
let daemon = daemon.clone();
|
let daemon1 = daemon.clone();
|
||||||
|
let daemon2 = daemon.clone();
|
||||||
|
Command::batch(vec![
|
||||||
Command::perform(
|
Command::perform(
|
||||||
async move {
|
async move {
|
||||||
daemon
|
daemon1
|
||||||
.list_coins()
|
.list_coins()
|
||||||
.map(|res| res.coins)
|
.map(|res| res.coins)
|
||||||
.map_err(|e| e.into())
|
.map_err(|e| e.into())
|
||||||
},
|
},
|
||||||
Message::Coins,
|
Message::Coins,
|
||||||
)
|
),
|
||||||
|
Command::perform(
|
||||||
|
async move {
|
||||||
|
let coins = daemon2
|
||||||
|
.list_coins()
|
||||||
|
.map(|res| res.coins)
|
||||||
|
.map_err(Error::from)?;
|
||||||
|
let mut targets = HashSet::<LabelItem>::new();
|
||||||
|
for coin in coins {
|
||||||
|
targets.insert(LabelItem::OutPoint(coin.outpoint));
|
||||||
|
targets.insert(LabelItem::Address(coin.address));
|
||||||
|
}
|
||||||
|
daemon2.get_labels(&targets).map_err(|e| e.into())
|
||||||
|
},
|
||||||
|
Message::Labels,
|
||||||
|
),
|
||||||
|
])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -172,6 +242,7 @@ mod tests {
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
panel
|
panel
|
||||||
.coins
|
.coins
|
||||||
|
.list
|
||||||
.iter()
|
.iter()
|
||||||
.map(|c| c.outpoint)
|
.map(|c| c.outpoint)
|
||||||
.collect::<Vec<bitcoin::OutPoint>>(),
|
.collect::<Vec<bitcoin::OutPoint>>(),
|
||||||
|
|||||||
88
gui/src/app/state/label.rs
Normal file
88
gui/src/app/state/label.rs
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
use liana::miniscript::bitcoin;
|
||||||
|
use std::str::FromStr;
|
||||||
|
use std::{collections::HashMap, iter::IntoIterator, sync::Arc};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
app::{error::Error, message::Message, view},
|
||||||
|
daemon::{
|
||||||
|
model::{LabelItem, Labelled},
|
||||||
|
Daemon,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use iced::Command;
|
||||||
|
use liana_ui::component::form;
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct LabelsEdited(HashMap<String, form::Value<String>>);
|
||||||
|
|
||||||
|
impl LabelsEdited {
|
||||||
|
pub fn cache(&self) -> &HashMap<String, form::Value<String>> {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
pub fn update<'a, T: IntoIterator<Item = &'a mut dyn Labelled>>(
|
||||||
|
&mut self,
|
||||||
|
daemon: Arc<dyn Daemon + Sync + Send>,
|
||||||
|
message: Message,
|
||||||
|
targets: T,
|
||||||
|
) -> Result<Command<Message>, Error> {
|
||||||
|
match message {
|
||||||
|
Message::View(view::Message::Label(labelled, 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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
view::LabelMessage::Cancel => {
|
||||||
|
self.0.remove(&labelled);
|
||||||
|
}
|
||||||
|
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,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Message::LabelsUpdated(res) => match res {
|
||||||
|
Ok(new_labels) => {
|
||||||
|
for target in targets {
|
||||||
|
target.load_labels(&new_labels);
|
||||||
|
}
|
||||||
|
for (labelled, _) in new_labels {
|
||||||
|
self.0.remove(&labelled);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => {}
|
||||||
|
};
|
||||||
|
Ok(Command::none())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn label_item_from_str(s: &str) -> Option<LabelItem> {
|
||||||
|
if let Ok(addr) = bitcoin::Address::from_str(s) {
|
||||||
|
Some(LabelItem::Address(addr.assume_checked()))
|
||||||
|
} else if let Ok(txid) = bitcoin::Txid::from_str(s) {
|
||||||
|
Some(LabelItem::Txid(txid))
|
||||||
|
} else if let Ok(outpoint) = bitcoin::OutPoint::from_str(s) {
|
||||||
|
Some(LabelItem::OutPoint(outpoint))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,4 +1,5 @@
|
|||||||
mod coins;
|
mod coins;
|
||||||
|
mod label;
|
||||||
mod psbt;
|
mod psbt;
|
||||||
mod psbts;
|
mod psbts;
|
||||||
mod recovery;
|
mod recovery;
|
||||||
@ -6,6 +7,7 @@ mod settings;
|
|||||||
mod spend;
|
mod spend;
|
||||||
mod transactions;
|
mod transactions;
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::convert::TryInto;
|
use std::convert::TryInto;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
@ -17,10 +19,11 @@ use liana_ui::widget::*;
|
|||||||
use super::{cache::Cache, error::Error, menu::Menu, message::Message, view, wallet::Wallet};
|
use super::{cache::Cache, error::Error, menu::Menu, message::Message, view, wallet::Wallet};
|
||||||
|
|
||||||
use crate::daemon::{
|
use crate::daemon::{
|
||||||
model::{remaining_sequence, Coin, HistoryTransaction},
|
model::{remaining_sequence, Coin, HistoryTransaction, LabelItem, Labelled},
|
||||||
Daemon,
|
Daemon,
|
||||||
};
|
};
|
||||||
pub use coins::CoinsPanel;
|
pub use coins::CoinsPanel;
|
||||||
|
use label::LabelsEdited;
|
||||||
pub use psbts::PsbtsPanel;
|
pub use psbts::PsbtsPanel;
|
||||||
pub use recovery::RecoveryPanel;
|
pub use recovery::RecoveryPanel;
|
||||||
pub use settings::SettingsState;
|
pub use settings::SettingsState;
|
||||||
@ -54,6 +57,7 @@ pub struct Home {
|
|||||||
pending_events: Vec<HistoryTransaction>,
|
pending_events: Vec<HistoryTransaction>,
|
||||||
events: Vec<HistoryTransaction>,
|
events: Vec<HistoryTransaction>,
|
||||||
selected_event: Option<(usize, usize)>,
|
selected_event: Option<(usize, usize)>,
|
||||||
|
labels_edited: LabelsEdited,
|
||||||
warning: Option<Error>,
|
warning: Option<Error>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -80,6 +84,7 @@ impl Home {
|
|||||||
selected_event: None,
|
selected_event: None,
|
||||||
events: Vec::new(),
|
events: Vec::new(),
|
||||||
pending_events: Vec::new(),
|
pending_events: Vec::new(),
|
||||||
|
labels_edited: LabelsEdited::default(),
|
||||||
warning: None,
|
warning: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -93,7 +98,13 @@ impl State for Home {
|
|||||||
} else {
|
} else {
|
||||||
&self.events[i - self.pending_events.len()]
|
&self.events[i - self.pending_events.len()]
|
||||||
};
|
};
|
||||||
view::home::payment_view(cache, event, output_index, self.warning.as_ref())
|
view::home::payment_view(
|
||||||
|
cache,
|
||||||
|
event,
|
||||||
|
output_index,
|
||||||
|
self.labels_edited.cache(),
|
||||||
|
self.warning.as_ref(),
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
view::dashboard(
|
view::dashboard(
|
||||||
&Menu::Home,
|
&Menu::Home,
|
||||||
@ -174,6 +185,23 @@ impl State for Home {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
Message::View(view::Message::Label(_, _)) | Message::LabelsUpdated(_) => {
|
||||||
|
match self.labels_edited.update(
|
||||||
|
daemon,
|
||||||
|
message,
|
||||||
|
self.pending_events
|
||||||
|
.iter_mut()
|
||||||
|
.map(|tx| tx as &mut dyn Labelled)
|
||||||
|
.chain(self.events.iter_mut().map(|tx| tx as &mut dyn Labelled)),
|
||||||
|
) {
|
||||||
|
Ok(cmd) => {
|
||||||
|
return cmd;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
self.warning = Some(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
Message::View(view::Message::Close) => {
|
Message::View(view::Message::Close) => {
|
||||||
self.selected_event = None;
|
self.selected_event = None;
|
||||||
}
|
}
|
||||||
@ -265,9 +293,28 @@ impl From<Home> for Box<dyn State> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct Addresses {
|
||||||
|
list: Vec<Address>,
|
||||||
|
labels: HashMap<String, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Labelled for Addresses {
|
||||||
|
fn labelled(&self) -> Vec<LabelItem> {
|
||||||
|
self.list
|
||||||
|
.iter()
|
||||||
|
.map(|a| LabelItem::Address(a.clone()))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
fn labels(&mut self) -> &mut HashMap<String, String> {
|
||||||
|
&mut self.labels
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct ReceivePanel {
|
pub struct ReceivePanel {
|
||||||
addresses: Vec<Address>,
|
addresses: Addresses,
|
||||||
|
labels_edited: LabelsEdited,
|
||||||
qr_code: Option<qr_code::State>,
|
qr_code: Option<qr_code::State>,
|
||||||
warning: Option<Error>,
|
warning: Option<Error>,
|
||||||
}
|
}
|
||||||
@ -278,7 +325,12 @@ impl State for ReceivePanel {
|
|||||||
&Menu::Receive,
|
&Menu::Receive,
|
||||||
cache,
|
cache,
|
||||||
self.warning.as_ref(),
|
self.warning.as_ref(),
|
||||||
view::receive::receive(&self.addresses, self.qr_code.as_ref()),
|
view::receive::receive(
|
||||||
|
&self.addresses.list,
|
||||||
|
self.qr_code.as_ref(),
|
||||||
|
&self.addresses.labels,
|
||||||
|
self.labels_edited.cache(),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
fn update(
|
fn update(
|
||||||
@ -288,12 +340,25 @@ impl State for ReceivePanel {
|
|||||||
message: Message,
|
message: Message,
|
||||||
) -> Command<Message> {
|
) -> Command<Message> {
|
||||||
match message {
|
match message {
|
||||||
|
Message::View(view::Message::Label(_, _)) | Message::LabelsUpdated(_) => {
|
||||||
|
match self.labels_edited.update(
|
||||||
|
daemon,
|
||||||
|
message,
|
||||||
|
std::iter::once(&mut self.addresses).map(|a| a as &mut dyn Labelled),
|
||||||
|
) {
|
||||||
|
Ok(cmd) => cmd,
|
||||||
|
Err(e) => {
|
||||||
|
self.warning = Some(e);
|
||||||
|
Command::none()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Message::ReceiveAddress(res) => {
|
Message::ReceiveAddress(res) => {
|
||||||
match res {
|
match res {
|
||||||
Ok(address) => {
|
Ok(address) => {
|
||||||
self.warning = None;
|
self.warning = None;
|
||||||
self.qr_code = Some(qr_code::State::new(address.to_qr_uri()).unwrap());
|
self.qr_code = Some(qr_code::State::new(address.to_qr_uri()).unwrap());
|
||||||
self.addresses.push(address);
|
self.addresses.list.push(address);
|
||||||
}
|
}
|
||||||
Err(e) => self.warning = Some(e),
|
Err(e) => self.warning = Some(e),
|
||||||
}
|
}
|
||||||
@ -363,6 +428,6 @@ mod tests {
|
|||||||
let sandbox = sandbox.load(client, &Cache::default()).await;
|
let sandbox = sandbox.load(client, &Cache::default()).await;
|
||||||
|
|
||||||
let panel = sandbox.state();
|
let panel = sandbox.state();
|
||||||
assert_eq!(panel.addresses, vec![addr]);
|
assert_eq!(panel.addresses.list, vec![addr]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
use std::collections::HashSet;
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use iced::Command;
|
use iced::Command;
|
||||||
@ -17,11 +17,12 @@ use crate::{
|
|||||||
cache::Cache,
|
cache::Cache,
|
||||||
error::Error,
|
error::Error,
|
||||||
message::Message,
|
message::Message,
|
||||||
|
state::label::{label_item_from_str, LabelsEdited},
|
||||||
view,
|
view,
|
||||||
wallet::{Wallet, WalletError},
|
wallet::{Wallet, WalletError},
|
||||||
},
|
},
|
||||||
daemon::{
|
daemon::{
|
||||||
model::{SpendStatus, SpendTx},
|
model::{LabelItem, Labelled, SpendStatus, SpendTx},
|
||||||
Daemon,
|
Daemon,
|
||||||
},
|
},
|
||||||
hw::{list_hardware_wallets, HardwareWallet},
|
hw::{list_hardware_wallets, HardwareWallet},
|
||||||
@ -50,6 +51,8 @@ pub struct PsbtState {
|
|||||||
pub desc_policy: LianaPolicy,
|
pub desc_policy: LianaPolicy,
|
||||||
pub tx: SpendTx,
|
pub tx: SpendTx,
|
||||||
pub saved: bool,
|
pub saved: bool,
|
||||||
|
pub warning: Option<Error>,
|
||||||
|
pub labels_edited: LabelsEdited,
|
||||||
pub action: Option<Box<dyn Action>>,
|
pub action: Option<Box<dyn Action>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,6 +61,8 @@ impl PsbtState {
|
|||||||
Self {
|
Self {
|
||||||
desc_policy: wallet.main_descriptor.policy(),
|
desc_policy: wallet.main_descriptor.policy(),
|
||||||
wallet,
|
wallet,
|
||||||
|
labels_edited: LabelsEdited::default(),
|
||||||
|
warning: None,
|
||||||
action: None,
|
action: None,
|
||||||
tx,
|
tx,
|
||||||
saved,
|
saved,
|
||||||
@ -84,7 +89,7 @@ impl PsbtState {
|
|||||||
self.action = None;
|
self.action = None;
|
||||||
}
|
}
|
||||||
view::SpendTxMessage::Delete => {
|
view::SpendTxMessage::Delete => {
|
||||||
self.action = Some(Box::new(DeleteAction::default()));
|
self.action = Some(Box::<DeleteAction>::default());
|
||||||
}
|
}
|
||||||
view::SpendTxMessage::Sign => {
|
view::SpendTxMessage::Sign => {
|
||||||
let action = SignAction::new(self.tx.signers(), self.wallet.clone());
|
let action = SignAction::new(self.tx.signers(), self.wallet.clone());
|
||||||
@ -99,10 +104,10 @@ impl PsbtState {
|
|||||||
return cmd;
|
return cmd;
|
||||||
}
|
}
|
||||||
view::SpendTxMessage::Broadcast => {
|
view::SpendTxMessage::Broadcast => {
|
||||||
self.action = Some(Box::new(BroadcastAction::default()));
|
self.action = Some(Box::<BroadcastAction>::default());
|
||||||
}
|
}
|
||||||
view::SpendTxMessage::Save => {
|
view::SpendTxMessage::Save => {
|
||||||
self.action = Some(Box::new(SaveAction::default()));
|
self.action = Some(Box::<SaveAction>::default());
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
if let Some(action) = self.action.as_mut() {
|
if let Some(action) = self.action.as_mut() {
|
||||||
@ -110,6 +115,20 @@ impl PsbtState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
Message::View(view::Message::Label(_, _)) | Message::LabelsUpdated(_) => {
|
||||||
|
match self.labels_edited.update(
|
||||||
|
daemon,
|
||||||
|
message,
|
||||||
|
std::iter::once(&mut self.tx).map(|tx| tx as &mut dyn Labelled),
|
||||||
|
) {
|
||||||
|
Ok(cmd) => {
|
||||||
|
return cmd;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
self.warning = Some(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
Message::Updated(Ok(_)) => {
|
Message::Updated(Ok(_)) => {
|
||||||
self.saved = true;
|
self.saved = true;
|
||||||
if let Some(action) = self.action.as_mut() {
|
if let Some(action) = self.action.as_mut() {
|
||||||
@ -132,7 +151,9 @@ impl PsbtState {
|
|||||||
self.saved,
|
self.saved,
|
||||||
&self.desc_policy,
|
&self.desc_policy,
|
||||||
&self.wallet.keys_aliases,
|
&self.wallet.keys_aliases,
|
||||||
|
self.labels_edited.cache(),
|
||||||
cache.network,
|
cache.network,
|
||||||
|
self.warning.as_ref(),
|
||||||
);
|
);
|
||||||
if let Some(action) = &self.action {
|
if let Some(action) = &self.action {
|
||||||
modal::Modal::new(content, action.view())
|
modal::Modal::new(content, action.view())
|
||||||
@ -161,8 +182,18 @@ impl Action for SaveAction {
|
|||||||
Message::View(view::Message::Spend(view::SpendTxMessage::Confirm)) => {
|
Message::View(view::Message::Spend(view::SpendTxMessage::Confirm)) => {
|
||||||
let daemon = daemon.clone();
|
let daemon = daemon.clone();
|
||||||
let psbt = tx.psbt.clone();
|
let psbt = tx.psbt.clone();
|
||||||
|
let mut labels = HashMap::<LabelItem, String>::new();
|
||||||
|
for (item, label) in tx.labels() {
|
||||||
|
labels.insert(
|
||||||
|
label_item_from_str(item).expect("Must be a LabelItem"),
|
||||||
|
label.clone(),
|
||||||
|
);
|
||||||
|
}
|
||||||
return Command::perform(
|
return Command::perform(
|
||||||
async move { daemon.update_spend_tx(&psbt).map_err(|e| e.into()) },
|
async move {
|
||||||
|
daemon.update_spend_tx(&psbt)?;
|
||||||
|
daemon.update_labels(&labels).map_err(|e| e.into())
|
||||||
|
},
|
||||||
Message::Updated,
|
Message::Updated,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -140,22 +140,29 @@ impl State for RecoveryPanel {
|
|||||||
.recovery_paths
|
.recovery_paths
|
||||||
.get(self.selected_path.expect("A path must be selected"))
|
.get(self.selected_path.expect("A path must be selected"))
|
||||||
.map(|p| p.sequence);
|
.map(|p| p.sequence);
|
||||||
|
let network = cache.network;
|
||||||
return Command::perform(
|
return Command::perform(
|
||||||
async move {
|
async move {
|
||||||
let psbt = daemon.create_recovery(address, feerate_vb, sequence)?;
|
let psbt = daemon.create_recovery(address, feerate_vb, sequence)?;
|
||||||
let coins = daemon.list_coins().map(|res| res.coins)?;
|
let coins = daemon.list_coins().map(|res| res.coins)?;
|
||||||
let coins = coins
|
let coins = coins
|
||||||
.iter()
|
.into_iter()
|
||||||
.filter(|coin| {
|
.filter(|coin| {
|
||||||
psbt.unsigned_tx
|
psbt.unsigned_tx
|
||||||
.input
|
.input
|
||||||
.iter()
|
.iter()
|
||||||
.any(|input| input.previous_output == coin.outpoint)
|
.any(|input| input.previous_output == coin.outpoint)
|
||||||
})
|
})
|
||||||
.cloned()
|
|
||||||
.collect();
|
.collect();
|
||||||
let sigs = desc.partial_spend_info(&psbt).unwrap();
|
let sigs = desc.partial_spend_info(&psbt).unwrap();
|
||||||
Ok(SpendTx::new(None, psbt, coins, sigs, desc.max_sat_vbytes()))
|
Ok(SpendTx::new(
|
||||||
|
None,
|
||||||
|
psbt,
|
||||||
|
coins,
|
||||||
|
sigs,
|
||||||
|
desc.max_sat_vbytes(),
|
||||||
|
network,
|
||||||
|
))
|
||||||
},
|
},
|
||||||
Message::Recovery,
|
Message::Recovery,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,15 +1,20 @@
|
|||||||
mod step;
|
mod step;
|
||||||
|
|
||||||
|
use std::collections::HashSet;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use iced::Command;
|
use iced::Command;
|
||||||
|
|
||||||
use liana::miniscript::bitcoin::OutPoint;
|
use liana::miniscript::bitcoin::{Network, OutPoint};
|
||||||
use liana_ui::widget::Element;
|
use liana_ui::widget::Element;
|
||||||
|
|
||||||
use super::{redirect, State};
|
use super::{redirect, State};
|
||||||
use crate::{
|
use crate::{
|
||||||
app::{cache::Cache, menu::Menu, message::Message, view, wallet::Wallet},
|
app::{cache::Cache, error::Error, menu::Menu, message::Message, view, wallet::Wallet},
|
||||||
daemon::{model::Coin, Daemon},
|
daemon::{
|
||||||
|
model::{Coin, LabelItem},
|
||||||
|
Daemon,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct CreateSpendPanel {
|
pub struct CreateSpendPanel {
|
||||||
@ -19,11 +24,11 @@ pub struct CreateSpendPanel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl CreateSpendPanel {
|
impl CreateSpendPanel {
|
||||||
pub fn new(wallet: Arc<Wallet>, coins: &[Coin], blockheight: u32) -> Self {
|
pub fn new(wallet: Arc<Wallet>, coins: &[Coin], blockheight: u32, network: Network) -> Self {
|
||||||
let descriptor = wallet.main_descriptor.clone();
|
let descriptor = wallet.main_descriptor.clone();
|
||||||
let timelock = descriptor.first_timelock_value();
|
let timelock = descriptor.first_timelock_value();
|
||||||
Self {
|
Self {
|
||||||
draft: step::TransactionDraft::default(),
|
draft: step::TransactionDraft::new(network),
|
||||||
current: 0,
|
current: 0,
|
||||||
steps: vec![
|
steps: vec![
|
||||||
Box::new(
|
Box::new(
|
||||||
@ -40,11 +45,12 @@ impl CreateSpendPanel {
|
|||||||
coins: &[Coin],
|
coins: &[Coin],
|
||||||
blockheight: u32,
|
blockheight: u32,
|
||||||
preselected_coins: &[OutPoint],
|
preselected_coins: &[OutPoint],
|
||||||
|
network: Network,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let descriptor = wallet.main_descriptor.clone();
|
let descriptor = wallet.main_descriptor.clone();
|
||||||
let timelock = descriptor.first_timelock_value();
|
let timelock = descriptor.first_timelock_value();
|
||||||
Self {
|
Self {
|
||||||
draft: step::TransactionDraft::default(),
|
draft: step::TransactionDraft::new(network),
|
||||||
current: 0,
|
current: 0,
|
||||||
steps: vec![
|
steps: vec![
|
||||||
Box::new(
|
Box::new(
|
||||||
@ -99,16 +105,33 @@ impl State for CreateSpendPanel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn load(&self, daemon: Arc<dyn Daemon + Sync + Send>) -> Command<Message> {
|
fn load(&self, daemon: Arc<dyn Daemon + Sync + Send>) -> Command<Message> {
|
||||||
let daemon = daemon.clone();
|
let daemon1 = daemon.clone();
|
||||||
|
let daemon2 = daemon.clone();
|
||||||
|
Command::batch(vec![
|
||||||
Command::perform(
|
Command::perform(
|
||||||
async move {
|
async move {
|
||||||
daemon
|
daemon1
|
||||||
.list_coins()
|
.list_coins()
|
||||||
.map(|res| res.coins)
|
.map(|res| res.coins)
|
||||||
.map_err(|e| e.into())
|
.map_err(|e| e.into())
|
||||||
},
|
},
|
||||||
Message::Coins,
|
Message::Coins,
|
||||||
)
|
),
|
||||||
|
Command::perform(
|
||||||
|
async move {
|
||||||
|
let coins = daemon
|
||||||
|
.list_coins()
|
||||||
|
.map(|res| res.coins)
|
||||||
|
.map_err(Error::from)?;
|
||||||
|
let mut targets = HashSet::<LabelItem>::new();
|
||||||
|
for coin in coins {
|
||||||
|
targets.insert(LabelItem::OutPoint(coin.outpoint));
|
||||||
|
}
|
||||||
|
daemon2.get_labels(&targets).map_err(|e| e.into())
|
||||||
|
},
|
||||||
|
Message::Labels,
|
||||||
|
),
|
||||||
|
])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -26,10 +26,27 @@ use crate::{
|
|||||||
/// See: https://github.com/wizardsardine/liana/blob/master/src/commands/mod.rs#L32
|
/// See: https://github.com/wizardsardine/liana/blob/master/src/commands/mod.rs#L32
|
||||||
const DUST_OUTPUT_SATS: u64 = 5_000;
|
const DUST_OUTPUT_SATS: u64 = 5_000;
|
||||||
|
|
||||||
#[derive(Default, Clone)]
|
#[derive(Clone)]
|
||||||
pub struct TransactionDraft {
|
pub struct TransactionDraft {
|
||||||
|
network: Network,
|
||||||
inputs: Vec<Coin>,
|
inputs: Vec<Coin>,
|
||||||
|
recipients: Vec<Recipient>,
|
||||||
generated: Option<Psbt>,
|
generated: Option<Psbt>,
|
||||||
|
batch_label: Option<String>,
|
||||||
|
labels: HashMap<String, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TransactionDraft {
|
||||||
|
pub fn new(network: Network) -> Self {
|
||||||
|
Self {
|
||||||
|
network,
|
||||||
|
inputs: Vec::new(),
|
||||||
|
recipients: Vec::new(),
|
||||||
|
generated: None,
|
||||||
|
batch_label: None,
|
||||||
|
labels: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait Step {
|
pub trait Step {
|
||||||
@ -53,6 +70,8 @@ pub struct DefineSpend {
|
|||||||
descriptor: LianaDescriptor,
|
descriptor: LianaDescriptor,
|
||||||
timelock: u16,
|
timelock: u16,
|
||||||
coins: Vec<(Coin, bool)>,
|
coins: Vec<(Coin, bool)>,
|
||||||
|
coins_labels: HashMap<String, String>,
|
||||||
|
batch_label: form::Value<String>,
|
||||||
amount_left_to_select: Option<Amount>,
|
amount_left_to_select: Option<Amount>,
|
||||||
feerate: form::Value<String>,
|
feerate: form::Value<String>,
|
||||||
generated: Option<Psbt>,
|
generated: Option<Psbt>,
|
||||||
@ -88,6 +107,8 @@ impl DefineSpend {
|
|||||||
timelock,
|
timelock,
|
||||||
generated: None,
|
generated: None,
|
||||||
coins,
|
coins,
|
||||||
|
coins_labels: HashMap::new(),
|
||||||
|
batch_label: form::Value::default(),
|
||||||
recipients: vec![Recipient::default()],
|
recipients: vec![Recipient::default()],
|
||||||
is_valid: false,
|
is_valid: false,
|
||||||
is_duplicate: false,
|
is_duplicate: false,
|
||||||
@ -128,7 +149,9 @@ impl DefineSpend {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn check_valid(&mut self) {
|
fn check_valid(&mut self) {
|
||||||
self.is_valid = self.feerate.valid && !self.feerate.value.is_empty();
|
self.is_valid = self.feerate.valid
|
||||||
|
&& !self.feerate.value.is_empty()
|
||||||
|
&& (self.batch_label.valid || self.recipients.len() < 2);
|
||||||
self.is_duplicate = false;
|
self.is_duplicate = false;
|
||||||
if !self.coins.iter().any(|(_, selected)| *selected) {
|
if !self.coins.iter().any(|(_, selected)| *selected) {
|
||||||
self.is_valid = false;
|
self.is_valid = false;
|
||||||
@ -216,13 +239,22 @@ impl Step for DefineSpend {
|
|||||||
cache: &Cache,
|
cache: &Cache,
|
||||||
message: Message,
|
message: Message,
|
||||||
) -> Command<Message> {
|
) -> Command<Message> {
|
||||||
if let Message::View(view::Message::CreateSpend(msg)) = message {
|
match message {
|
||||||
|
Message::View(view::Message::CreateSpend(msg)) => {
|
||||||
match msg {
|
match msg {
|
||||||
|
view::CreateSpendMessage::BatchLabelEdited(label) => {
|
||||||
|
self.batch_label.valid = label.len() <= 100;
|
||||||
|
self.batch_label.value = label;
|
||||||
|
}
|
||||||
view::CreateSpendMessage::AddRecipient => {
|
view::CreateSpendMessage::AddRecipient => {
|
||||||
self.recipients.push(Recipient::default());
|
self.recipients.push(Recipient::default());
|
||||||
}
|
}
|
||||||
view::CreateSpendMessage::DeleteRecipient(i) => {
|
view::CreateSpendMessage::DeleteRecipient(i) => {
|
||||||
self.recipients.remove(i);
|
self.recipients.remove(i);
|
||||||
|
if self.recipients.len() < 2 {
|
||||||
|
self.batch_label.valid = true;
|
||||||
|
self.batch_label.value = "".to_string();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
view::CreateSpendMessage::RecipientEdited(i, _, _) => {
|
view::CreateSpendMessage::RecipientEdited(i, _, _) => {
|
||||||
self.recipients
|
self.recipients
|
||||||
@ -251,14 +283,21 @@ impl Step for DefineSpend {
|
|||||||
.coins
|
.coins
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(
|
.filter_map(
|
||||||
|(coin, selected)| if *selected { Some(coin.outpoint) } else { None },
|
|(coin, selected)| {
|
||||||
|
if *selected {
|
||||||
|
Some(coin.outpoint)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
},
|
||||||
)
|
)
|
||||||
.collect();
|
.collect();
|
||||||
let mut outputs: HashMap<Address<address::NetworkUnchecked>, u64> =
|
let mut outputs: HashMap<Address<address::NetworkUnchecked>, u64> =
|
||||||
HashMap::new();
|
HashMap::new();
|
||||||
for recipient in &self.recipients {
|
for recipient in &self.recipients {
|
||||||
outputs.insert(
|
outputs.insert(
|
||||||
Address::from_str(&recipient.address.value).expect("Checked before"),
|
Address::from_str(&recipient.address.value)
|
||||||
|
.expect("Checked before"),
|
||||||
recipient.amount().expect("Checked before"),
|
recipient.amount().expect("Checked before"),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -283,27 +322,63 @@ impl Step for DefineSpend {
|
|||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
self.check_valid();
|
self.check_valid();
|
||||||
Command::none()
|
}
|
||||||
} else {
|
Message::Psbt(res) => match res {
|
||||||
if let Message::Psbt(res) = message {
|
|
||||||
match res {
|
|
||||||
Ok(psbt) => {
|
Ok(psbt) => {
|
||||||
self.generated = Some(psbt);
|
self.generated = Some(psbt);
|
||||||
return Command::perform(async {}, |_| Message::View(view::Message::Next));
|
return Command::perform(async {}, |_| Message::View(view::Message::Next));
|
||||||
}
|
}
|
||||||
Err(e) => self.warning = Some(e),
|
Err(e) => self.warning = Some(e),
|
||||||
|
},
|
||||||
|
Message::Labels(res) => match res {
|
||||||
|
Ok(labels) => {
|
||||||
|
self.coins_labels = labels;
|
||||||
}
|
}
|
||||||
}
|
Err(e) => self.warning = Some(e),
|
||||||
|
},
|
||||||
|
_ => {}
|
||||||
|
};
|
||||||
Command::none()
|
Command::none()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fn apply(&self, draft: &mut TransactionDraft) {
|
fn apply(&self, draft: &mut TransactionDraft) {
|
||||||
draft.inputs = self
|
draft.inputs = self
|
||||||
.coins
|
.coins
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|(coin, selected)| if *selected { Some(coin.clone()) } else { None })
|
.filter_map(|(coin, selected)| if *selected { Some(coin) } else { None })
|
||||||
|
.cloned()
|
||||||
.collect();
|
.collect();
|
||||||
|
if let Some(psbt) = &self.generated {
|
||||||
|
draft.labels = self.coins_labels.clone();
|
||||||
|
for (i, output) in psbt.unsigned_tx.output.iter().enumerate() {
|
||||||
|
if let Some(label) = self
|
||||||
|
.recipients
|
||||||
|
.iter()
|
||||||
|
.find(|recipient| {
|
||||||
|
!recipient.label.value.is_empty()
|
||||||
|
&& Address::from_str(&recipient.address.value)
|
||||||
|
.unwrap()
|
||||||
|
.payload
|
||||||
|
.matches_script_pubkey(&output.script_pubkey)
|
||||||
|
&& output.value == recipient.amount().unwrap()
|
||||||
|
})
|
||||||
|
.map(|recipient| recipient.label.value.to_string())
|
||||||
|
{
|
||||||
|
draft.labels.insert(
|
||||||
|
OutPoint {
|
||||||
|
txid: psbt.unsigned_tx.txid(),
|
||||||
|
vout: i as u32,
|
||||||
|
}
|
||||||
|
.to_string(),
|
||||||
|
label,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
draft.recipients = self.recipients.clone();
|
||||||
|
if self.recipients.len() > 1 {
|
||||||
|
draft.batch_label = Some(self.batch_label.value.clone());
|
||||||
|
}
|
||||||
draft.generated = self.generated.clone();
|
draft.generated = self.generated.clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -326,6 +401,8 @@ impl Step for DefineSpend {
|
|||||||
self.is_duplicate,
|
self.is_duplicate,
|
||||||
self.timelock,
|
self.timelock,
|
||||||
&self.coins,
|
&self.coins,
|
||||||
|
&self.coins_labels,
|
||||||
|
&self.batch_label,
|
||||||
self.amount_left_to_select.as_ref(),
|
self.amount_left_to_select.as_ref(),
|
||||||
&self.feerate,
|
&self.feerate,
|
||||||
self.warning.as_ref(),
|
self.warning.as_ref(),
|
||||||
@ -333,8 +410,9 @@ impl Step for DefineSpend {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default, Clone)]
|
||||||
struct Recipient {
|
struct Recipient {
|
||||||
|
label: form::Value<String>,
|
||||||
address: form::Value<String>,
|
address: form::Value<String>,
|
||||||
amount: form::Value<String>,
|
amount: form::Value<String>,
|
||||||
}
|
}
|
||||||
@ -372,6 +450,7 @@ impl Recipient {
|
|||||||
&& self.address.valid
|
&& self.address.valid
|
||||||
&& !self.amount.value.is_empty()
|
&& !self.amount.value.is_empty()
|
||||||
&& self.amount.valid
|
&& self.amount.valid
|
||||||
|
&& self.label.valid
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update(&mut self, network: Network, message: view::CreateSpendMessage) {
|
fn update(&mut self, network: Network, message: view::CreateSpendMessage) {
|
||||||
@ -399,12 +478,16 @@ impl Recipient {
|
|||||||
self.amount.valid = true;
|
self.amount.valid = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
view::CreateSpendMessage::RecipientEdited(_, "label", label) => {
|
||||||
|
self.label.valid = label.len() <= 100;
|
||||||
|
self.label.value = label;
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn view(&self, i: usize) -> Element<view::CreateSpendMessage> {
|
fn view(&self, i: usize) -> Element<view::CreateSpendMessage> {
|
||||||
view::spend::recipient_view(i, &self.address, &self.amount)
|
view::spend::recipient_view(i, &self.address, &self.amount, &self.label)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -430,17 +513,49 @@ impl Step for SaveSpend {
|
|||||||
.main_descriptor
|
.main_descriptor
|
||||||
.partial_spend_info(&psbt)
|
.partial_spend_info(&psbt)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
self.spend = Some(psbt::PsbtState::new(
|
|
||||||
self.wallet.clone(),
|
let mut tx = SpendTx::new(
|
||||||
SpendTx::new(
|
|
||||||
None,
|
None,
|
||||||
psbt,
|
psbt,
|
||||||
draft.inputs.clone(),
|
draft.inputs.clone(),
|
||||||
sigs,
|
sigs,
|
||||||
self.wallet.main_descriptor.max_sat_vbytes(),
|
self.wallet.main_descriptor.max_sat_vbytes(),
|
||||||
),
|
draft.network,
|
||||||
false,
|
);
|
||||||
));
|
tx.labels = draft.labels.clone();
|
||||||
|
|
||||||
|
if tx.is_batch() {
|
||||||
|
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()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.spend = Some(psbt::PsbtState::new(self.wallet.clone(), tx, false));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update(
|
fn update(
|
||||||
@ -464,7 +579,9 @@ impl Step for SaveSpend {
|
|||||||
spend.saved,
|
spend.saved,
|
||||||
&spend.desc_policy,
|
&spend.desc_policy,
|
||||||
&spend.wallet.keys_aliases,
|
&spend.wallet.keys_aliases,
|
||||||
|
spend.labels_edited.cache(),
|
||||||
cache.network,
|
cache.network,
|
||||||
|
spend.warning.as_ref(),
|
||||||
);
|
);
|
||||||
if let Some(action) = &spend.action {
|
if let Some(action) = &spend.action {
|
||||||
modal::Modal::new(content, action.view())
|
modal::Modal::new(content, action.view())
|
||||||
|
|||||||
@ -1,18 +1,30 @@
|
|||||||
use std::convert::TryInto;
|
use std::{
|
||||||
use std::sync::Arc;
|
convert::TryInto,
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
sync::Arc,
|
||||||
|
time::{SystemTime, UNIX_EPOCH},
|
||||||
|
};
|
||||||
|
|
||||||
use iced::Command;
|
use iced::Command;
|
||||||
use liana_ui::widget::*;
|
use liana_ui::widget::*;
|
||||||
|
|
||||||
use crate::app::{cache::Cache, error::Error, message::Message, view, State};
|
use crate::app::{
|
||||||
|
cache::Cache,
|
||||||
|
error::Error,
|
||||||
|
message::Message,
|
||||||
|
state::{label::LabelsEdited, State},
|
||||||
|
view,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::daemon::{model::HistoryTransaction, Daemon};
|
use crate::daemon::{
|
||||||
|
model::{HistoryTransaction, Labelled},
|
||||||
|
Daemon,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct TransactionsPanel {
|
pub struct TransactionsPanel {
|
||||||
pending_txs: Vec<HistoryTransaction>,
|
pending_txs: Vec<HistoryTransaction>,
|
||||||
txs: Vec<HistoryTransaction>,
|
txs: Vec<HistoryTransaction>,
|
||||||
|
labels_edited: LabelsEdited,
|
||||||
selected_tx: Option<usize>,
|
selected_tx: Option<usize>,
|
||||||
warning: Option<Error>,
|
warning: Option<Error>,
|
||||||
}
|
}
|
||||||
@ -23,6 +35,7 @@ impl TransactionsPanel {
|
|||||||
selected_tx: None,
|
selected_tx: None,
|
||||||
txs: Vec::new(),
|
txs: Vec::new(),
|
||||||
pending_txs: Vec::new(),
|
pending_txs: Vec::new(),
|
||||||
|
labels_edited: LabelsEdited::default(),
|
||||||
warning: None,
|
warning: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -36,7 +49,12 @@ impl State for TransactionsPanel {
|
|||||||
} else {
|
} else {
|
||||||
&self.txs[i - self.pending_txs.len()]
|
&self.txs[i - self.pending_txs.len()]
|
||||||
};
|
};
|
||||||
view::transactions::tx_view(cache, tx, self.warning.as_ref())
|
view::transactions::tx_view(
|
||||||
|
cache,
|
||||||
|
tx,
|
||||||
|
self.labels_edited.cache(),
|
||||||
|
self.warning.as_ref(),
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
view::transactions::transactions_view(
|
view::transactions::transactions_view(
|
||||||
cache,
|
cache,
|
||||||
@ -82,6 +100,23 @@ impl State for TransactionsPanel {
|
|||||||
Message::View(view::Message::Select(i)) => {
|
Message::View(view::Message::Select(i)) => {
|
||||||
self.selected_tx = Some(i);
|
self.selected_tx = Some(i);
|
||||||
}
|
}
|
||||||
|
Message::View(view::Message::Label(_, _)) | Message::LabelsUpdated(_) => {
|
||||||
|
match self.labels_edited.update(
|
||||||
|
daemon,
|
||||||
|
message,
|
||||||
|
self.pending_txs
|
||||||
|
.iter_mut()
|
||||||
|
.map(|tx| tx as &mut dyn Labelled)
|
||||||
|
.chain(self.txs.iter_mut().map(|tx| tx as &mut dyn Labelled)),
|
||||||
|
) {
|
||||||
|
Ok(cmd) => {
|
||||||
|
return cmd;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
self.warning = Some(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
Message::View(view::Message::Next) => {
|
Message::View(view::Message::Next) => {
|
||||||
if let Some(last) = self.txs.last() {
|
if let Some(last) = self.txs.last() {
|
||||||
let daemon = daemon.clone();
|
let daemon = daemon.clone();
|
||||||
|
|||||||
@ -1,15 +1,21 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use iced::{widget::Space, Alignment, Length};
|
use iced::{widget::Space, Alignment, Length};
|
||||||
|
|
||||||
use liana_ui::{
|
use liana_ui::{
|
||||||
color,
|
color,
|
||||||
component::{amount::*, badge, button, text::*},
|
component::{amount::*, badge, button, form, text::*},
|
||||||
icon, theme,
|
icon, theme,
|
||||||
util::Collection,
|
util::Collection,
|
||||||
widget::*,
|
widget::*,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
app::{cache::Cache, menu::Menu, view::message::Message},
|
app::{
|
||||||
|
cache::Cache,
|
||||||
|
menu::Menu,
|
||||||
|
view::{label, message::Message},
|
||||||
|
},
|
||||||
daemon::model::{remaining_sequence, Coin},
|
daemon::model::{remaining_sequence, Coin},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -18,6 +24,8 @@ pub fn coins_view<'a>(
|
|||||||
coins: &'a [Coin],
|
coins: &'a [Coin],
|
||||||
timelock: u16,
|
timelock: u16,
|
||||||
selected: &[usize],
|
selected: &[usize],
|
||||||
|
labels: &'a HashMap<String, String>,
|
||||||
|
labels_editing: &'a HashMap<String, form::Value<String>>,
|
||||||
) -> Element<'a, Message> {
|
) -> Element<'a, Message> {
|
||||||
Column::new()
|
Column::new()
|
||||||
.push(Container::new(h3("Coins")).width(Length::Fill))
|
.push(Container::new(h3("Coins")).width(Length::Fill))
|
||||||
@ -33,6 +41,8 @@ pub fn coins_view<'a>(
|
|||||||
cache.blockheight as u32,
|
cache.blockheight as u32,
|
||||||
i,
|
i,
|
||||||
selected.contains(&i),
|
selected.contains(&i),
|
||||||
|
labels,
|
||||||
|
labels_editing,
|
||||||
))
|
))
|
||||||
},
|
},
|
||||||
)),
|
)),
|
||||||
@ -43,13 +53,17 @@ pub fn coins_view<'a>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::collapsible_else_if)]
|
#[allow(clippy::collapsible_else_if)]
|
||||||
fn coin_list_view(
|
fn coin_list_view<'a>(
|
||||||
coin: &Coin,
|
coin: &'a Coin,
|
||||||
timelock: u16,
|
timelock: u16,
|
||||||
blockheight: u32,
|
blockheight: u32,
|
||||||
index: usize,
|
index: usize,
|
||||||
collapsed: bool,
|
collapsed: bool,
|
||||||
) -> Container<Message> {
|
labels: &'a HashMap<String, String>,
|
||||||
|
labels_editing: &'a HashMap<String, form::Value<String>>,
|
||||||
|
) -> Container<'a, Message> {
|
||||||
|
let outpoint = coin.outpoint.to_string();
|
||||||
|
let address = coin.address.to_string();
|
||||||
Container::new(
|
Container::new(
|
||||||
Column::new()
|
Column::new()
|
||||||
.push(
|
.push(
|
||||||
@ -58,6 +72,17 @@ fn coin_list_view(
|
|||||||
.push(
|
.push(
|
||||||
Row::new()
|
Row::new()
|
||||||
.push(badge::coin())
|
.push(badge::coin())
|
||||||
|
.push(if !collapsed {
|
||||||
|
if let Some(label) = labels.get(&outpoint) {
|
||||||
|
Container::new(p1_bold(label)).width(Length::Fill)
|
||||||
|
} else {
|
||||||
|
Container::new(Space::with_width(Length::Fill))
|
||||||
|
.width(Length::Fill)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Container::new(Space::with_width(Length::Fill))
|
||||||
|
.width(Length::Fill)
|
||||||
|
})
|
||||||
.push(if coin.spend_info.is_some() {
|
.push(if coin.spend_info.is_some() {
|
||||||
badge::spent()
|
badge::spent()
|
||||||
} else if coin.block_height.is_none() {
|
} else if coin.block_height.is_none() {
|
||||||
@ -83,6 +108,18 @@ fn coin_list_view(
|
|||||||
Column::new()
|
Column::new()
|
||||||
.padding(10)
|
.padding(10)
|
||||||
.spacing(5)
|
.spacing(5)
|
||||||
|
.push(
|
||||||
|
Container::new(if let Some(label) = labels_editing.get(&outpoint) {
|
||||||
|
label::label_editing(outpoint.clone(), label, P1_SIZE)
|
||||||
|
} else {
|
||||||
|
label::label_editable(
|
||||||
|
outpoint.clone(),
|
||||||
|
labels.get(&outpoint),
|
||||||
|
P1_SIZE,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.width(Length::Fill),
|
||||||
|
)
|
||||||
.push_maybe(if coin.spend_info.is_none() {
|
.push_maybe(if coin.spend_info.is_none() {
|
||||||
if let Some(b) = coin.block_height {
|
if let Some(b) = coin.block_height {
|
||||||
if blockheight > b as u32 + timelock as u32 {
|
if blockheight > b as u32 + timelock as u32 {
|
||||||
@ -104,6 +141,42 @@ fn coin_list_view(
|
|||||||
})
|
})
|
||||||
.push(
|
.push(
|
||||||
Column::new()
|
Column::new()
|
||||||
|
.push(
|
||||||
|
Row::new()
|
||||||
|
.align_items(Alignment::Center)
|
||||||
|
.push(p2_regular("Address:").bold().style(color::GREY_2))
|
||||||
|
.push(
|
||||||
|
Row::new()
|
||||||
|
.align_items(Alignment::Center)
|
||||||
|
.push(
|
||||||
|
p2_regular(address.clone())
|
||||||
|
.style(color::GREY_2),
|
||||||
|
)
|
||||||
|
.push(
|
||||||
|
Button::new(icon::clipboard_icon())
|
||||||
|
.on_press(Message::Clipboard(
|
||||||
|
address.clone(),
|
||||||
|
))
|
||||||
|
.style(theme::Button::TransparentBorder),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.spacing(5),
|
||||||
|
)
|
||||||
|
.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(
|
.push(
|
||||||
Row::new()
|
Row::new()
|
||||||
.align_items(Alignment::Center)
|
.align_items(Alignment::Center)
|
||||||
@ -186,7 +259,7 @@ pub fn coin_sequence_label<'a, T: 'a>(seq: u32, timelock: u32) -> Container<'a,
|
|||||||
)
|
)
|
||||||
.padding(10)
|
.padding(10)
|
||||||
.style(theme::Container::Pill(theme::Pill::Warning))
|
.style(theme::Container::Pill(theme::Pill::Warning))
|
||||||
} else if seq < timelock as u32 * 10 / 100 {
|
} else if seq < timelock * 10 / 100 {
|
||||||
Container::new(
|
Container::new(
|
||||||
Row::new()
|
Row::new()
|
||||||
.spacing(5)
|
.spacing(5)
|
||||||
|
|||||||
@ -1,11 +1,12 @@
|
|||||||
use chrono::NaiveDateTime;
|
use chrono::NaiveDateTime;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use iced::{alignment, Alignment, Length};
|
use iced::{alignment, widget::Space, Alignment, Length};
|
||||||
|
|
||||||
use liana::miniscript::bitcoin;
|
use liana::miniscript::bitcoin;
|
||||||
use liana_ui::{
|
use liana_ui::{
|
||||||
color,
|
color,
|
||||||
component::{amount::*, button, card, event, text::*},
|
component::{amount::*, button, card, event, form, text::*},
|
||||||
icon, theme,
|
icon, theme,
|
||||||
util::Collection,
|
util::Collection,
|
||||||
widget::*,
|
widget::*,
|
||||||
@ -16,7 +17,7 @@ use crate::{
|
|||||||
cache::Cache,
|
cache::Cache,
|
||||||
error::Error,
|
error::Error,
|
||||||
menu::Menu,
|
menu::Menu,
|
||||||
view::{coins, dashboard, message::Message},
|
view::{coins, dashboard, label, message::Message},
|
||||||
},
|
},
|
||||||
daemon::model::HistoryTransaction,
|
daemon::model::HistoryTransaction,
|
||||||
};
|
};
|
||||||
@ -28,8 +29,8 @@ pub fn home_view<'a>(
|
|||||||
unconfirmed_balance: &'a bitcoin::Amount,
|
unconfirmed_balance: &'a bitcoin::Amount,
|
||||||
remaining_sequence: &Option<u32>,
|
remaining_sequence: &Option<u32>,
|
||||||
expiring_coins: &Vec<bitcoin::OutPoint>,
|
expiring_coins: &Vec<bitcoin::OutPoint>,
|
||||||
pending_events: &[HistoryTransaction],
|
pending_events: &'a [HistoryTransaction],
|
||||||
events: &Vec<HistoryTransaction>,
|
events: &'a Vec<HistoryTransaction>,
|
||||||
) -> Element<'a, Message> {
|
) -> Element<'a, Message> {
|
||||||
Column::new()
|
Column::new()
|
||||||
.push(h3("Balance"))
|
.push(h3("Balance"))
|
||||||
@ -145,21 +146,41 @@ pub fn home_view<'a>(
|
|||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn event_list_view<'a>(i: usize, event: &HistoryTransaction) -> Column<'a, Message> {
|
fn event_list_view(i: usize, event: &HistoryTransaction) -> Column<'_, Message> {
|
||||||
event.tx.output.iter().enumerate().fold(
|
event.tx.output.iter().enumerate().fold(
|
||||||
Column::new().spacing(10),
|
Column::new().spacing(10),
|
||||||
|col, (output_index, output)| {
|
|col, (output_index, output)| {
|
||||||
|
let label = if let Some(label) = event.labels.get(
|
||||||
|
&bitcoin::OutPoint {
|
||||||
|
txid: event.tx.txid(),
|
||||||
|
vout: output_index as u32,
|
||||||
|
}
|
||||||
|
.to_string(),
|
||||||
|
) {
|
||||||
|
Some(p1_bold(label))
|
||||||
|
} else {
|
||||||
|
event
|
||||||
|
.labels
|
||||||
|
.get(
|
||||||
|
&bitcoin::Address::from_script(&output.script_pubkey, event.network)
|
||||||
|
.unwrap()
|
||||||
|
.to_string(),
|
||||||
|
)
|
||||||
|
.map(|label| p1_bold(format!("address label: {}", label)).style(color::GREY_3))
|
||||||
|
};
|
||||||
if event.is_external() {
|
if event.is_external() {
|
||||||
if !event.change_indexes.contains(&output_index) {
|
if !event.change_indexes.contains(&output_index) {
|
||||||
col
|
col
|
||||||
} else if let Some(t) = event.time {
|
} else if let Some(t) = event.time {
|
||||||
col.push(event::confirmed_incoming_event(
|
col.push(event::confirmed_incoming_event(
|
||||||
|
label,
|
||||||
NaiveDateTime::from_timestamp_opt(t as i64, 0).unwrap(),
|
NaiveDateTime::from_timestamp_opt(t as i64, 0).unwrap(),
|
||||||
&Amount::from_sat(output.value),
|
&Amount::from_sat(output.value),
|
||||||
Message::SelectSub(i, output_index),
|
Message::SelectSub(i, output_index),
|
||||||
))
|
))
|
||||||
} else {
|
} else {
|
||||||
col.push(event::unconfirmed_incoming_event(
|
col.push(event::unconfirmed_incoming_event(
|
||||||
|
label,
|
||||||
&Amount::from_sat(output.value),
|
&Amount::from_sat(output.value),
|
||||||
Message::SelectSub(i, output_index),
|
Message::SelectSub(i, output_index),
|
||||||
))
|
))
|
||||||
@ -168,12 +189,14 @@ fn event_list_view<'a>(i: usize, event: &HistoryTransaction) -> Column<'a, Messa
|
|||||||
col
|
col
|
||||||
} else if let Some(t) = event.time {
|
} else if let Some(t) = event.time {
|
||||||
col.push(event::confirmed_outgoing_event(
|
col.push(event::confirmed_outgoing_event(
|
||||||
|
label,
|
||||||
NaiveDateTime::from_timestamp_opt(t as i64, 0).unwrap(),
|
NaiveDateTime::from_timestamp_opt(t as i64, 0).unwrap(),
|
||||||
&Amount::from_sat(output.value),
|
&Amount::from_sat(output.value),
|
||||||
Message::SelectSub(i, output_index),
|
Message::SelectSub(i, output_index),
|
||||||
))
|
))
|
||||||
} else {
|
} else {
|
||||||
col.push(event::unconfirmed_outgoing_event(
|
col.push(event::unconfirmed_outgoing_event(
|
||||||
|
label,
|
||||||
&Amount::from_sat(output.value),
|
&Amount::from_sat(output.value),
|
||||||
Message::SelectSub(i, output_index),
|
Message::SelectSub(i, output_index),
|
||||||
))
|
))
|
||||||
@ -186,8 +209,15 @@ pub fn payment_view<'a>(
|
|||||||
cache: &'a Cache,
|
cache: &'a Cache,
|
||||||
tx: &'a HistoryTransaction,
|
tx: &'a HistoryTransaction,
|
||||||
output_index: usize,
|
output_index: usize,
|
||||||
|
labels_editing: &'a HashMap<String, form::Value<String>>,
|
||||||
warning: Option<&'a Error>,
|
warning: Option<&'a Error>,
|
||||||
) -> Element<'a, Message> {
|
) -> Element<'a, Message> {
|
||||||
|
let txid = tx.tx.txid().to_string();
|
||||||
|
let outpoint = bitcoin::OutPoint {
|
||||||
|
txid: tx.tx.txid(),
|
||||||
|
vout: output_index as u32,
|
||||||
|
}
|
||||||
|
.to_string();
|
||||||
dashboard(
|
dashboard(
|
||||||
&Menu::Home,
|
&Menu::Home,
|
||||||
cache,
|
cache,
|
||||||
@ -200,11 +230,22 @@ pub fn payment_view<'a>(
|
|||||||
} else {
|
} else {
|
||||||
Container::new(h3("Outgoing payment")).width(Length::Fill)
|
Container::new(h3("Outgoing payment")).width(Length::Fill)
|
||||||
})
|
})
|
||||||
|
.push(if let Some(label) = labels_editing.get(&outpoint) {
|
||||||
|
label::label_editing(outpoint.clone(), label, H3_SIZE)
|
||||||
|
} else {
|
||||||
|
label::label_editable(outpoint.clone(), tx.labels.get(&outpoint), H1_SIZE)
|
||||||
|
})
|
||||||
.push(Container::new(amount_with_size(
|
.push(Container::new(amount_with_size(
|
||||||
&Amount::from_sat(tx.tx.output[output_index].value),
|
&Amount::from_sat(tx.tx.output[output_index].value),
|
||||||
H1_SIZE,
|
H1_SIZE,
|
||||||
)))
|
)))
|
||||||
|
.push(Space::with_height(H3_SIZE))
|
||||||
.push(Container::new(h3("Transaction")).width(Length::Fill))
|
.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)
|
||||||
|
} else {
|
||||||
|
label::label_editable(txid.clone(), tx.labels.get(&txid), H3_SIZE)
|
||||||
|
})
|
||||||
.push_maybe(tx.fee_amount.map(|fee_amount| {
|
.push_maybe(tx.fee_amount.map(|fee_amount| {
|
||||||
Row::new()
|
Row::new()
|
||||||
.align_items(Alignment::Center)
|
.align_items(Alignment::Center)
|
||||||
@ -259,11 +300,8 @@ pub fn payment_view<'a>(
|
|||||||
} else {
|
} else {
|
||||||
Some(tx.change_indexes.clone())
|
Some(tx.change_indexes.clone())
|
||||||
},
|
},
|
||||||
if tx.is_external() {
|
&tx.labels,
|
||||||
Some(tx.change_indexes.clone())
|
labels_editing,
|
||||||
} else {
|
|
||||||
None
|
|
||||||
},
|
|
||||||
))
|
))
|
||||||
.spacing(20),
|
.spacing(20),
|
||||||
)
|
)
|
||||||
|
|||||||
71
gui/src/app/view/label.rs
Normal file
71
gui/src/app/view/label.rs
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
use iced::{widget::row, Alignment};
|
||||||
|
|
||||||
|
use liana_ui::{
|
||||||
|
color,
|
||||||
|
component::{button, form},
|
||||||
|
font, icon,
|
||||||
|
widget::*,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::app::view;
|
||||||
|
|
||||||
|
pub fn label_editable(
|
||||||
|
labelled: String,
|
||||||
|
label: Option<&String>,
|
||||||
|
size: u16,
|
||||||
|
) -> Element<'_, view::Message> {
|
||||||
|
if let Some(label) = label {
|
||||||
|
if !label.is_empty() {
|
||||||
|
return Container::new(
|
||||||
|
row!(
|
||||||
|
iced::widget::Text::new(label).size(size).font(font::BOLD),
|
||||||
|
button::primary(Some(icon::pencil_icon()), "Edit").on_press(
|
||||||
|
view::Message::Label(
|
||||||
|
labelled,
|
||||||
|
view::message::LabelMessage::Edited(label.to_string())
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.spacing(5)
|
||||||
|
.align_items(Alignment::Center),
|
||||||
|
)
|
||||||
|
.into();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Container::new(
|
||||||
|
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,
|
||||||
|
view::message::LabelMessage::Edited(String::default())
|
||||||
|
))
|
||||||
|
)
|
||||||
|
.spacing(5)
|
||||||
|
.align_items(Alignment::Center),
|
||||||
|
)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn label_editing(
|
||||||
|
labelled: String,
|
||||||
|
label: &form::Value<String>,
|
||||||
|
size: u16,
|
||||||
|
) -> Element<view::Message> {
|
||||||
|
let e: Element<view::LabelMessage> = Container::new(
|
||||||
|
row!(
|
||||||
|
form::Form::new("Label", label, view::LabelMessage::Edited)
|
||||||
|
.warning("Invalid label length, cannot be superior to 100")
|
||||||
|
.size(size)
|
||||||
|
.padding(10),
|
||||||
|
button::primary(None, "Save").on_press(view::message::LabelMessage::Confirm),
|
||||||
|
button::primary(None, "Cancel").on_press(view::message::LabelMessage::Cancel)
|
||||||
|
)
|
||||||
|
.spacing(5)
|
||||||
|
.align_items(Alignment::Center),
|
||||||
|
)
|
||||||
|
.into();
|
||||||
|
e.map(move |msg| view::Message::Label(labelled.clone(), msg))
|
||||||
|
}
|
||||||
@ -9,6 +9,7 @@ pub enum Message {
|
|||||||
Close,
|
Close,
|
||||||
Select(usize),
|
Select(usize),
|
||||||
SelectSub(usize, usize),
|
SelectSub(usize, usize),
|
||||||
|
Label(String, LabelMessage),
|
||||||
Settings(SettingsMessage),
|
Settings(SettingsMessage),
|
||||||
CreateSpend(CreateSpendMessage),
|
CreateSpend(CreateSpendMessage),
|
||||||
ImportSpend(ImportSpendMessage),
|
ImportSpend(ImportSpendMessage),
|
||||||
@ -18,9 +19,17 @@ pub enum Message {
|
|||||||
SelectHardwareWallet(usize),
|
SelectHardwareWallet(usize),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum LabelMessage {
|
||||||
|
Edited(String),
|
||||||
|
Cancel,
|
||||||
|
Confirm,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum CreateSpendMessage {
|
pub enum CreateSpendMessage {
|
||||||
AddRecipient,
|
AddRecipient,
|
||||||
|
BatchLabelEdited(String),
|
||||||
DeleteRecipient(usize),
|
DeleteRecipient(usize),
|
||||||
SelectCoin(usize),
|
SelectCoin(usize),
|
||||||
RecipientEdited(usize, &'static str, String),
|
RecipientEdited(usize, &'static str, String),
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
mod label;
|
||||||
mod message;
|
mod message;
|
||||||
mod warning;
|
mod warning;
|
||||||
|
|
||||||
|
|||||||
@ -9,14 +9,19 @@ use liana::{
|
|||||||
descriptors::{LianaPolicy, PathInfo, PathSpendInfo},
|
descriptors::{LianaPolicy, PathInfo, PathSpendInfo},
|
||||||
miniscript::bitcoin::{
|
miniscript::bitcoin::{
|
||||||
bip32::{DerivationPath, Fingerprint},
|
bip32::{DerivationPath, Fingerprint},
|
||||||
Address, Amount, Network, Transaction,
|
blockdata::transaction::TxOut,
|
||||||
|
Address, Amount, Network, OutPoint, Transaction, Txid,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
use liana_ui::{
|
use liana_ui::{
|
||||||
color,
|
color,
|
||||||
component::{
|
component::{
|
||||||
amount::*, badge, button, card, collapse::Collapse, form, hw, separation, text::*,
|
amount::*,
|
||||||
|
badge, button, card,
|
||||||
|
collapse::Collapse,
|
||||||
|
form, hw, separation,
|
||||||
|
text::{self, *},
|
||||||
},
|
},
|
||||||
icon, theme,
|
icon, theme,
|
||||||
util::Collection,
|
util::Collection,
|
||||||
@ -28,24 +33,27 @@ use crate::{
|
|||||||
cache::Cache,
|
cache::Cache,
|
||||||
error::Error,
|
error::Error,
|
||||||
menu::Menu,
|
menu::Menu,
|
||||||
view::{dashboard, hw::hw_list_view, message::*, warning::warn},
|
view::{dashboard, hw::hw_list_view, label, message::*, warning::warn},
|
||||||
},
|
},
|
||||||
daemon::model::{Coin, SpendStatus, SpendTx},
|
daemon::model::{Coin, SpendStatus, SpendTx},
|
||||||
hw::HardwareWallet,
|
hw::HardwareWallet,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub fn psbt_view<'a>(
|
pub fn psbt_view<'a>(
|
||||||
cache: &'a Cache,
|
cache: &'a Cache,
|
||||||
tx: &'a SpendTx,
|
tx: &'a SpendTx,
|
||||||
saved: bool,
|
saved: bool,
|
||||||
desc_info: &'a LianaPolicy,
|
desc_info: &'a LianaPolicy,
|
||||||
key_aliases: &'a HashMap<Fingerprint, String>,
|
key_aliases: &'a HashMap<Fingerprint, String>,
|
||||||
|
labels_editing: &'a HashMap<String, form::Value<String>>,
|
||||||
network: Network,
|
network: Network,
|
||||||
|
warning: Option<&Error>,
|
||||||
) -> Element<'a, Message> {
|
) -> Element<'a, Message> {
|
||||||
dashboard(
|
dashboard(
|
||||||
&Menu::PSBTs,
|
&Menu::PSBTs,
|
||||||
cache,
|
cache,
|
||||||
None,
|
warning,
|
||||||
Column::new()
|
Column::new()
|
||||||
.spacing(20)
|
.spacing(20)
|
||||||
.push(
|
.push(
|
||||||
@ -65,14 +73,15 @@ pub fn psbt_view<'a>(
|
|||||||
_ => None,
|
_ => None,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.push(spend_header(tx))
|
.push(spend_header(tx, labels_editing))
|
||||||
.push(spend_overview_view(tx, desc_info, key_aliases))
|
.push(spend_overview_view(tx, desc_info, key_aliases))
|
||||||
.push(inputs_and_outputs_view(
|
.push(inputs_and_outputs_view(
|
||||||
&tx.coins,
|
&tx.coins,
|
||||||
&tx.psbt.unsigned_tx,
|
&tx.psbt.unsigned_tx,
|
||||||
network,
|
network,
|
||||||
Some(tx.change_indexes.clone()),
|
Some(tx.change_indexes.clone()),
|
||||||
None,
|
&tx.labels,
|
||||||
|
labels_editing,
|
||||||
))
|
))
|
||||||
.push(if saved {
|
.push(if saved {
|
||||||
Row::new()
|
Row::new()
|
||||||
@ -182,9 +191,18 @@ pub fn delete_action<'a>(warning: Option<&Error>, deleted: bool) -> Element<'a,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn spend_header<'a>(tx: &SpendTx) -> Element<'a, Message> {
|
pub fn spend_header<'a>(
|
||||||
|
tx: &'a SpendTx,
|
||||||
|
labels_editing: &'a HashMap<String, form::Value<String>>,
|
||||||
|
) -> Element<'a, Message> {
|
||||||
|
let txid = tx.psbt.unsigned_tx.txid().to_string();
|
||||||
Column::new()
|
Column::new()
|
||||||
.spacing(20)
|
.spacing(20)
|
||||||
|
.push(if let Some(label) = labels_editing.get(&txid) {
|
||||||
|
label::label_editing(txid.clone(), label, H3_SIZE)
|
||||||
|
} else {
|
||||||
|
label::label_editable(txid.clone(), tx.labels.get(&txid), H1_SIZE)
|
||||||
|
})
|
||||||
.push(
|
.push(
|
||||||
Column::new()
|
Column::new()
|
||||||
.push(if tx.is_self_send() {
|
.push(if tx.is_self_send() {
|
||||||
@ -504,8 +522,10 @@ pub fn inputs_and_outputs_view<'a>(
|
|||||||
tx: &'a Transaction,
|
tx: &'a Transaction,
|
||||||
network: Network,
|
network: Network,
|
||||||
change_indexes: Option<Vec<usize>>,
|
change_indexes: Option<Vec<usize>>,
|
||||||
receive_indexes: Option<Vec<usize>>,
|
labels: &'a HashMap<String, String>,
|
||||||
|
labels_editing: &'a HashMap<String, form::Value<String>>,
|
||||||
) -> Element<'a, Message> {
|
) -> Element<'a, Message> {
|
||||||
|
let change_indexes_copy = change_indexes.clone();
|
||||||
Column::new()
|
Column::new()
|
||||||
.spacing(20)
|
.spacing(20)
|
||||||
.push_maybe(if !coins.is_empty() {
|
.push_maybe(if !coins.is_empty() {
|
||||||
@ -551,29 +571,9 @@ pub fn inputs_and_outputs_view<'a>(
|
|||||||
coins
|
coins
|
||||||
.iter()
|
.iter()
|
||||||
.fold(
|
.fold(
|
||||||
Column::new().padding(20),
|
Column::new().spacing(10).padding(20),
|
||||||
|col: Column<'a, Message>, coin| {
|
|col: Column<'a, Message>, coin| {
|
||||||
col.push(
|
col.push(input_view(coin, labels, labels_editing))
|
||||||
Row::new()
|
|
||||||
.align_items(Alignment::Center)
|
|
||||||
.width(Length::Fill)
|
|
||||||
.push(
|
|
||||||
Row::new()
|
|
||||||
.width(Length::Fill)
|
|
||||||
.align_items(Alignment::Center)
|
|
||||||
.push(p2_regular(coin.outpoint.to_string()).style(color::GREY_3))
|
|
||||||
.push(
|
|
||||||
Button::new(icon::clipboard_icon().style(color::GREY_3))
|
|
||||||
.on_press(Message::Clipboard(
|
|
||||||
coin.outpoint.to_string(),
|
|
||||||
))
|
|
||||||
.style(
|
|
||||||
theme::Button::TransparentBorder,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.push(amount(&coin.amount)),
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.into()
|
.into()
|
||||||
@ -584,7 +584,20 @@ pub fn inputs_and_outputs_view<'a>(
|
|||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
})
|
})
|
||||||
.push(
|
.push({
|
||||||
|
let count = tx
|
||||||
|
.output
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter(|(i, _)| {
|
||||||
|
if let Some(indexes) = change_indexes_copy.as_ref() {
|
||||||
|
!indexes.contains(i)
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.count();
|
||||||
|
if count > 0 {
|
||||||
Container::new(Collapse::new(
|
Container::new(Collapse::new(
|
||||||
move || {
|
move || {
|
||||||
Button::new(
|
Button::new(
|
||||||
@ -592,9 +605,9 @@ pub fn inputs_and_outputs_view<'a>(
|
|||||||
.align_items(Alignment::Center)
|
.align_items(Alignment::Center)
|
||||||
.push(
|
.push(
|
||||||
h4_bold(format!(
|
h4_bold(format!(
|
||||||
"{} recipient{}",
|
"{} payment{}",
|
||||||
tx.output.len(),
|
count,
|
||||||
if tx.output.len() == 1 { "" } else { "s" }
|
if count == 1 { "" } else { "s" }
|
||||||
))
|
))
|
||||||
.width(Length::Fill),
|
.width(Length::Fill),
|
||||||
)
|
)
|
||||||
@ -610,9 +623,9 @@ pub fn inputs_and_outputs_view<'a>(
|
|||||||
.align_items(Alignment::Center)
|
.align_items(Alignment::Center)
|
||||||
.push(
|
.push(
|
||||||
h4_bold(format!(
|
h4_bold(format!(
|
||||||
"{} recipient{}",
|
"{} payment{}",
|
||||||
tx.output.len(),
|
count,
|
||||||
if tx.output.len() == 1 { "" } else { "s" }
|
if count == 1 { "" } else { "s" }
|
||||||
))
|
))
|
||||||
.width(Length::Fill),
|
.width(Length::Fill),
|
||||||
)
|
)
|
||||||
@ -626,59 +639,83 @@ pub fn inputs_and_outputs_view<'a>(
|
|||||||
tx.output
|
tx.output
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
|
.filter(|(i, _)| {
|
||||||
|
if let Some(indexes) = change_indexes_copy.as_ref() {
|
||||||
|
!indexes.contains(i)
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
})
|
||||||
.fold(
|
.fold(
|
||||||
Column::new().padding(20),
|
Column::new().padding(20),
|
||||||
|col: Column<'a, Message>, (i, output)| {
|
|col: Column<'a, Message>, (i, output)| {
|
||||||
let addr =
|
col.spacing(10).push(payment_view(
|
||||||
Address::from_script(&output.script_pubkey, network).unwrap();
|
i,
|
||||||
col.push(
|
tx.txid(),
|
||||||
Column::new()
|
output,
|
||||||
.width(Length::Fill)
|
network,
|
||||||
.spacing(5)
|
labels,
|
||||||
.push(
|
labels_editing,
|
||||||
Row::new()
|
|
||||||
.align_items(Alignment::Center)
|
|
||||||
.width(Length::Fill)
|
|
||||||
.push(
|
|
||||||
Row::new()
|
|
||||||
.align_items(Alignment::Center)
|
|
||||||
.width(Length::Fill)
|
|
||||||
.push(p2_regular(addr.to_string()).style(color::GREY_3))
|
|
||||||
.push(
|
|
||||||
Button::new(icon::clipboard_icon().style(color::GREY_3))
|
|
||||||
.on_press(Message::Clipboard(
|
|
||||||
addr.to_string(),
|
|
||||||
))
|
))
|
||||||
.style(
|
},
|
||||||
theme::Button::TransparentBorder,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.push(amount(&Amount::from_sat(output.value))),
|
.into()
|
||||||
)
|
},
|
||||||
.push_maybe(if let Some(indexes) = change_indexes.as_ref() {
|
|
||||||
if indexes.contains(&i) {
|
|
||||||
Some(Container::new(text("Change")).padding(5).style(
|
|
||||||
theme::Container::Pill(theme::Pill::Success),
|
|
||||||
))
|
))
|
||||||
|
.style(theme::Container::Card(theme::Card::Simple))
|
||||||
} else {
|
} else {
|
||||||
None
|
Container::new(h4_bold("0 payment"))
|
||||||
|
.padding(20)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.style(theme::Container::Card(theme::Card::Simple))
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
None
|
|
||||||
})
|
})
|
||||||
.push_maybe(if let Some(indexes) = receive_indexes.as_ref() {
|
.push_maybe(
|
||||||
if indexes.contains(&i) {
|
if change_indexes
|
||||||
Some(Container::new(text("Deposit")).padding(5).style(
|
.as_ref()
|
||||||
theme::Container::Pill(theme::Pill::Success),
|
.map(|indexes| !indexes.is_empty())
|
||||||
))
|
.unwrap_or(false)
|
||||||
} else {
|
{
|
||||||
None
|
Some(
|
||||||
}
|
Container::new(Collapse::new(
|
||||||
} else {
|
move || {
|
||||||
None
|
Button::new(
|
||||||
}),
|
Row::new()
|
||||||
|
.align_items(Alignment::Center)
|
||||||
|
.push(h4_bold("Change").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("Change").width(Length::Fill))
|
||||||
|
.push(icon::collapsed_icon()),
|
||||||
|
)
|
||||||
|
.padding(20)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.style(theme::Button::TransparentBorder)
|
||||||
|
},
|
||||||
|
move || {
|
||||||
|
tx.output
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.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,
|
||||||
|
))
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.into()
|
.into()
|
||||||
@ -686,6 +723,235 @@ pub fn inputs_and_outputs_view<'a>(
|
|||||||
))
|
))
|
||||||
.style(theme::Container::Card(theme::Card::Simple)),
|
.style(theme::Container::Card(theme::Card::Simple)),
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn input_view<'a>(
|
||||||
|
coin: &'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();
|
||||||
|
Column::new()
|
||||||
|
.width(Length::Fill)
|
||||||
|
.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(&coin.amount)),
|
||||||
|
)
|
||||||
|
.push(
|
||||||
|
Column::new()
|
||||||
|
.push(
|
||||||
|
Row::new()
|
||||||
|
.align_items(Alignment::Center)
|
||||||
|
.spacing(5)
|
||||||
|
.push(p1_bold("Outpoint:").style(color::GREY_3))
|
||||||
|
.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()))
|
||||||
|
.style(theme::Button::TransparentBorder),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.push(
|
||||||
|
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:").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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.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)),
|
||||||
|
)
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.spacing(5)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn payment_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> {
|
||||||
|
let addr = Address::from_script(&output.script_pubkey, network)
|
||||||
|
.unwrap()
|
||||||
|
.to_string();
|
||||||
|
let outpoint = OutPoint {
|
||||||
|
txid,
|
||||||
|
vout: i as u32,
|
||||||
|
}
|
||||||
|
.to_string();
|
||||||
|
Column::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()
|
||||||
|
.push(
|
||||||
|
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:").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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.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)),
|
||||||
|
)
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.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> {
|
||||||
|
let addr = Address::from_script(&output.script_pubkey, network)
|
||||||
|
.unwrap()
|
||||||
|
.to_string();
|
||||||
|
let outpoint = OutPoint {
|
||||||
|
txid,
|
||||||
|
vout: i as u32,
|
||||||
|
}
|
||||||
|
.to_string();
|
||||||
|
Column::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()
|
||||||
|
.push(
|
||||||
|
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:").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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.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)),
|
||||||
|
)
|
||||||
|
})),
|
||||||
|
)
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -59,7 +59,7 @@ pub fn import_psbt_success_view<'a>() -> Element<'a, Message> {
|
|||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn psbts_view<'a>(spend_txs: &[SpendTx]) -> Element<'a, Message> {
|
pub fn psbts_view(spend_txs: &[SpendTx]) -> Element<'_, Message> {
|
||||||
Column::new()
|
Column::new()
|
||||||
.push(
|
.push(
|
||||||
Row::new()
|
Row::new()
|
||||||
@ -90,7 +90,7 @@ pub fn psbts_view<'a>(spend_txs: &[SpendTx]) -> Element<'a, Message> {
|
|||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn spend_tx_list_view<'a>(i: usize, tx: &SpendTx) -> Element<'a, Message> {
|
fn spend_tx_list_view(i: usize, tx: &SpendTx) -> Element<'_, Message> {
|
||||||
Container::new(
|
Container::new(
|
||||||
Button::new(
|
Button::new(
|
||||||
Row::new()
|
Row::new()
|
||||||
@ -124,6 +124,11 @@ fn spend_tx_list_view<'a>(i: usize, tx: &SpendTx) -> Element<'a, Message> {
|
|||||||
.push(icon::key_icon().style(color::GREY_3)),
|
.push(icon::key_icon().style(color::GREY_3)),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
.push_maybe(
|
||||||
|
tx.labels
|
||||||
|
.get(&tx.psbt.unsigned_tx.txid().to_string())
|
||||||
|
.map(p1_bold),
|
||||||
|
)
|
||||||
.spacing(10)
|
.spacing(10)
|
||||||
.align_items(Alignment::Center)
|
.align_items(Alignment::Center)
|
||||||
.width(Length::Fill),
|
.width(Length::Fill),
|
||||||
@ -134,6 +139,11 @@ fn spend_tx_list_view<'a>(i: usize, tx: &SpendTx) -> Element<'a, Message> {
|
|||||||
SpendStatus::Spent => Some(badge::spent()),
|
SpendStatus::Spent => Some(badge::spent()),
|
||||||
_ => None,
|
_ => None,
|
||||||
})
|
})
|
||||||
|
.push_maybe(if tx.is_batch() {
|
||||||
|
Some(badge::batch())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
})
|
||||||
.push(
|
.push(
|
||||||
Column::new()
|
Column::new()
|
||||||
.align_items(Alignment::End)
|
.align_items(Alignment::End)
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use iced::{
|
use iced::{
|
||||||
widget::{
|
widget::{
|
||||||
qr_code::{self, QRCode},
|
qr_code::{self, QRCode},
|
||||||
@ -10,16 +12,23 @@ use liana::miniscript::bitcoin;
|
|||||||
|
|
||||||
use liana_ui::{
|
use liana_ui::{
|
||||||
color,
|
color,
|
||||||
component::{button, card, text::*},
|
component::{
|
||||||
|
button, card, form,
|
||||||
|
text::{self, *},
|
||||||
|
},
|
||||||
icon, theme,
|
icon, theme,
|
||||||
widget::*,
|
widget::*,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use crate::app::view::label;
|
||||||
|
|
||||||
use super::message::Message;
|
use super::message::Message;
|
||||||
|
|
||||||
pub fn receive<'a>(
|
pub fn receive<'a>(
|
||||||
addresses: &'a [bitcoin::Address],
|
addresses: &'a [bitcoin::Address],
|
||||||
qr: Option<&'a qr_code::State>,
|
qr: Option<&'a qr_code::State>,
|
||||||
|
labels: &'a HashMap<String, String>,
|
||||||
|
labels_editing: &'a HashMap<String, form::Value<String>>,
|
||||||
) -> Element<'a, Message> {
|
) -> Element<'a, Message> {
|
||||||
Column::new()
|
Column::new()
|
||||||
.push(
|
.push(
|
||||||
@ -38,34 +47,54 @@ pub fn receive<'a>(
|
|||||||
.push(addresses.iter().rev().fold(
|
.push(addresses.iter().rev().fold(
|
||||||
Column::new().spacing(10).width(Length::Fill),
|
Column::new().spacing(10).width(Length::Fill),
|
||||||
|col, address| {
|
|col, address| {
|
||||||
|
let addr = address.to_string();
|
||||||
col.push(
|
col.push(
|
||||||
card::simple(
|
card::simple(
|
||||||
|
Column::new()
|
||||||
|
.push(if let Some(label) = labels_editing.get(&addr) {
|
||||||
|
label::label_editing(addr.clone(), label, text::P1_SIZE)
|
||||||
|
} else {
|
||||||
|
label::label_editable(
|
||||||
|
addr.clone(),
|
||||||
|
labels.get(&addr),
|
||||||
|
text::P1_SIZE,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.push(
|
||||||
Row::new()
|
Row::new()
|
||||||
.push(
|
.push(
|
||||||
Container::new(
|
Container::new(
|
||||||
scrollable(
|
scrollable(
|
||||||
Column::new()
|
Column::new()
|
||||||
.push(Space::with_height(Length::Fixed(10.0)))
|
.push(Space::with_height(
|
||||||
|
Length::Fixed(10.0),
|
||||||
|
))
|
||||||
.push(
|
.push(
|
||||||
p2_regular(address.to_string())
|
p2_regular(addr)
|
||||||
.small()
|
.small()
|
||||||
.style(color::GREY_3),
|
.style(color::GREY_3),
|
||||||
)
|
)
|
||||||
// Space between the address and the scrollbar
|
// Space between the address and the scrollbar
|
||||||
.push(Space::with_height(Length::Fixed(10.0))),
|
.push(Space::with_height(
|
||||||
|
Length::Fixed(10.0),
|
||||||
|
)),
|
||||||
)
|
)
|
||||||
.horizontal_scroll(
|
.horizontal_scroll(
|
||||||
scrollable::Properties::new().scroller_width(5),
|
scrollable::Properties::new()
|
||||||
|
.scroller_width(5),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.width(Length::Fill),
|
.width(Length::Fill),
|
||||||
)
|
)
|
||||||
.push(
|
.push(
|
||||||
Button::new(icon::clipboard_icon().style(color::GREY_3))
|
Button::new(
|
||||||
|
icon::clipboard_icon().style(color::GREY_3),
|
||||||
|
)
|
||||||
.on_press(Message::Clipboard(address.to_string()))
|
.on_press(Message::Clipboard(address.to_string()))
|
||||||
.style(theme::Button::TransparentBorder),
|
.style(theme::Button::TransparentBorder),
|
||||||
)
|
)
|
||||||
.align_items(Alignment::Center),
|
.align_items(Alignment::Center),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.padding(20),
|
.padding(20),
|
||||||
)
|
)
|
||||||
|
|||||||
@ -29,29 +29,33 @@ use crate::{
|
|||||||
daemon::model::{remaining_sequence, Coin, SpendTx},
|
daemon::model::{remaining_sequence, Coin, SpendTx},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub fn spend_view<'a>(
|
pub fn spend_view<'a>(
|
||||||
cache: &'a Cache,
|
cache: &'a Cache,
|
||||||
tx: &'a SpendTx,
|
tx: &'a SpendTx,
|
||||||
saved: bool,
|
saved: bool,
|
||||||
desc_info: &'a LianaPolicy,
|
desc_info: &'a LianaPolicy,
|
||||||
key_aliases: &'a HashMap<Fingerprint, String>,
|
key_aliases: &'a HashMap<Fingerprint, String>,
|
||||||
|
labels_editing: &'a HashMap<String, form::Value<String>>,
|
||||||
network: Network,
|
network: Network,
|
||||||
|
warning: Option<&Error>,
|
||||||
) -> Element<'a, Message> {
|
) -> Element<'a, Message> {
|
||||||
dashboard(
|
dashboard(
|
||||||
&Menu::CreateSpendTx,
|
&Menu::CreateSpendTx,
|
||||||
cache,
|
cache,
|
||||||
None,
|
warning,
|
||||||
Column::new()
|
Column::new()
|
||||||
.spacing(20)
|
.spacing(20)
|
||||||
.push(Container::new(h3("Send")).width(Length::Fill))
|
.push(Container::new(h3("Send")).width(Length::Fill))
|
||||||
.push(psbt::spend_header(tx))
|
.push(psbt::spend_header(tx, labels_editing))
|
||||||
.push(psbt::spend_overview_view(tx, desc_info, key_aliases))
|
.push(psbt::spend_overview_view(tx, desc_info, key_aliases))
|
||||||
.push(psbt::inputs_and_outputs_view(
|
.push(psbt::inputs_and_outputs_view(
|
||||||
&tx.coins,
|
&tx.coins,
|
||||||
&tx.psbt.unsigned_tx,
|
&tx.psbt.unsigned_tx,
|
||||||
network,
|
network,
|
||||||
Some(tx.change_indexes.clone()),
|
Some(tx.change_indexes.clone()),
|
||||||
None,
|
&tx.labels,
|
||||||
|
labels_editing,
|
||||||
))
|
))
|
||||||
.push(if saved {
|
.push(if saved {
|
||||||
Row::new()
|
Row::new()
|
||||||
@ -89,6 +93,8 @@ pub fn create_spend_tx<'a>(
|
|||||||
duplicate: bool,
|
duplicate: bool,
|
||||||
timelock: u16,
|
timelock: u16,
|
||||||
coins: &[(Coin, bool)],
|
coins: &[(Coin, bool)],
|
||||||
|
coins_labels: &'a HashMap<String, String>,
|
||||||
|
batch_label: &form::Value<String>,
|
||||||
amount_left: Option<&Amount>,
|
amount_left: Option<&Amount>,
|
||||||
feerate: &form::Value<String>,
|
feerate: &form::Value<String>,
|
||||||
error: Option<&Error>,
|
error: Option<&Error>,
|
||||||
@ -104,6 +110,18 @@ pub fn create_spend_tx<'a>(
|
|||||||
} else {
|
} else {
|
||||||
"Send"
|
"Send"
|
||||||
}))
|
}))
|
||||||
|
.push_maybe(if recipients.len() > 1 {
|
||||||
|
Some(
|
||||||
|
form::Form::new("Batch label", batch_label, |s| {
|
||||||
|
Message::CreateSpend(CreateSpendMessage::BatchLabelEdited(s))
|
||||||
|
})
|
||||||
|
.warning("Invalid label length, cannot be superior to 100")
|
||||||
|
.size(30)
|
||||||
|
.padding(10),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
})
|
||||||
.push(
|
.push(
|
||||||
Column::new()
|
Column::new()
|
||||||
.push(Column::with_children(recipients).spacing(10))
|
.push(Column::with_children(recipients).spacing(10))
|
||||||
@ -112,7 +130,7 @@ pub fn create_spend_tx<'a>(
|
|||||||
.push_maybe(if duplicate {
|
.push_maybe(if duplicate {
|
||||||
Some(
|
Some(
|
||||||
Container::new(
|
Container::new(
|
||||||
text("Two recipient addresses are the same")
|
text("Two payment addresses are the same")
|
||||||
.style(color::RED),
|
.style(color::RED),
|
||||||
)
|
)
|
||||||
.padding(10),
|
.padding(10),
|
||||||
@ -125,7 +143,7 @@ pub fn create_spend_tx<'a>(
|
|||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
Some(
|
Some(
|
||||||
button::secondary(Some(icon::plus_icon()), "Add recipient")
|
button::secondary(Some(icon::plus_icon()), "Add payment")
|
||||||
.on_press(Message::CreateSpend(
|
.on_press(Message::CreateSpend(
|
||||||
CreateSpendMessage::AddRecipient,
|
CreateSpendMessage::AddRecipient,
|
||||||
)),
|
)),
|
||||||
@ -201,6 +219,7 @@ pub fn create_spend_tx<'a>(
|
|||||||
col.push(coin_list_view(
|
col.push(coin_list_view(
|
||||||
i,
|
i,
|
||||||
coin,
|
coin,
|
||||||
|
coins_labels,
|
||||||
timelock,
|
timelock,
|
||||||
cache.blockheight as u32,
|
cache.blockheight as u32,
|
||||||
*selected,
|
*selected,
|
||||||
@ -246,6 +265,7 @@ pub fn recipient_view<'a>(
|
|||||||
index: usize,
|
index: usize,
|
||||||
address: &form::Value<String>,
|
address: &form::Value<String>,
|
||||||
amount: &form::Value<String>,
|
amount: &form::Value<String>,
|
||||||
|
label: &form::Value<String>,
|
||||||
) -> Element<'a, CreateSpendMessage> {
|
) -> Element<'a, CreateSpendMessage> {
|
||||||
Container::new(
|
Container::new(
|
||||||
Column::new()
|
Column::new()
|
||||||
@ -263,10 +283,10 @@ pub fn recipient_view<'a>(
|
|||||||
.align_items(Alignment::Start)
|
.align_items(Alignment::Start)
|
||||||
.spacing(10)
|
.spacing(10)
|
||||||
.push(
|
.push(
|
||||||
Container::new(p1_bold("Pay to"))
|
Container::new(p1_bold("Address"))
|
||||||
.align_x(alignment::Horizontal::Right)
|
.align_x(alignment::Horizontal::Right)
|
||||||
.padding(10)
|
.padding(10)
|
||||||
.width(Length::Fixed(80.0)),
|
.width(Length::Fixed(110.0)),
|
||||||
)
|
)
|
||||||
.push(
|
.push(
|
||||||
form::Form::new_trimmed("Address", address, move |msg| {
|
form::Form::new_trimmed("Address", address, move |msg| {
|
||||||
@ -277,6 +297,25 @@ pub fn recipient_view<'a>(
|
|||||||
.padding(10),
|
.padding(10),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
.push(
|
||||||
|
Row::new()
|
||||||
|
.align_items(Alignment::Start)
|
||||||
|
.spacing(10)
|
||||||
|
.push(
|
||||||
|
Container::new(p1_bold("Description"))
|
||||||
|
.align_x(alignment::Horizontal::Right)
|
||||||
|
.padding(10)
|
||||||
|
.width(Length::Fixed(110.0)),
|
||||||
|
)
|
||||||
|
.push(
|
||||||
|
form::Form::new("Payment label", label, move |msg| {
|
||||||
|
CreateSpendMessage::RecipientEdited(index, "label", msg)
|
||||||
|
})
|
||||||
|
.warning("Label length is too long (> 100 char)")
|
||||||
|
.size(20)
|
||||||
|
.padding(10),
|
||||||
|
),
|
||||||
|
)
|
||||||
.push(
|
.push(
|
||||||
Row::new()
|
Row::new()
|
||||||
.align_items(Alignment::Start)
|
.align_items(Alignment::Start)
|
||||||
@ -285,7 +324,7 @@ pub fn recipient_view<'a>(
|
|||||||
Container::new(p1_bold("Amount"))
|
Container::new(p1_bold("Amount"))
|
||||||
.padding(10)
|
.padding(10)
|
||||||
.align_x(alignment::Horizontal::Right)
|
.align_x(alignment::Horizontal::Right)
|
||||||
.width(Length::Fixed(80.0)),
|
.width(Length::Fixed(110.0)),
|
||||||
)
|
)
|
||||||
.push(
|
.push(
|
||||||
form::Form::new_trimmed("0.001 (in BTC)", amount, move |msg| {
|
form::Form::new_trimmed("0.001 (in BTC)", amount, move |msg| {
|
||||||
@ -308,6 +347,7 @@ pub fn recipient_view<'a>(
|
|||||||
fn coin_list_view<'a>(
|
fn coin_list_view<'a>(
|
||||||
i: usize,
|
i: usize,
|
||||||
coin: &Coin,
|
coin: &Coin,
|
||||||
|
coins_labels: &'a HashMap<String, String>,
|
||||||
timelock: u16,
|
timelock: u16,
|
||||||
blockheight: u32,
|
blockheight: u32,
|
||||||
selected: bool,
|
selected: bool,
|
||||||
@ -318,6 +358,13 @@ fn coin_list_view<'a>(
|
|||||||
.push(checkbox("", selected, move |_| {
|
.push(checkbox("", selected, move |_| {
|
||||||
Message::CreateSpend(CreateSpendMessage::SelectCoin(i))
|
Message::CreateSpend(CreateSpendMessage::SelectCoin(i))
|
||||||
}))
|
}))
|
||||||
|
.push(
|
||||||
|
if let Some(label) = coins_labels.get(&coin.outpoint.to_string()) {
|
||||||
|
Container::new(p1_bold(label)).width(Length::Fill)
|
||||||
|
} else {
|
||||||
|
Container::new(p1_bold("")).width(Length::Fill)
|
||||||
|
},
|
||||||
|
)
|
||||||
.push(if coin.spend_info.is_some() {
|
.push(if coin.spend_info.is_some() {
|
||||||
badge::spent()
|
badge::spent()
|
||||||
} else if coin.block_height.is_none() {
|
} else if coin.block_height.is_none() {
|
||||||
|
|||||||
@ -1,10 +1,11 @@
|
|||||||
use chrono::NaiveDateTime;
|
use chrono::NaiveDateTime;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use iced::{alignment, Alignment, Length};
|
use iced::{alignment, Alignment, Length};
|
||||||
|
|
||||||
use liana_ui::{
|
use liana_ui::{
|
||||||
color,
|
color,
|
||||||
component::{amount::*, badge, card, text::*},
|
component::{amount::*, badge, card, form, text::*},
|
||||||
icon, theme,
|
icon, theme,
|
||||||
util::Collection,
|
util::Collection,
|
||||||
widget::*,
|
widget::*,
|
||||||
@ -15,7 +16,7 @@ use crate::{
|
|||||||
cache::Cache,
|
cache::Cache,
|
||||||
error::Error,
|
error::Error,
|
||||||
menu::Menu,
|
menu::Menu,
|
||||||
view::{dashboard, message::Message},
|
view::{dashboard, label, message::Message},
|
||||||
},
|
},
|
||||||
daemon::model::HistoryTransaction,
|
daemon::model::HistoryTransaction,
|
||||||
};
|
};
|
||||||
@ -24,8 +25,8 @@ pub const HISTORY_EVENT_PAGE_SIZE: u64 = 20;
|
|||||||
|
|
||||||
pub fn transactions_view<'a>(
|
pub fn transactions_view<'a>(
|
||||||
cache: &'a Cache,
|
cache: &'a Cache,
|
||||||
pending_txs: &[HistoryTransaction],
|
pending_txs: &'a [HistoryTransaction],
|
||||||
txs: &Vec<HistoryTransaction>,
|
txs: &'a Vec<HistoryTransaction>,
|
||||||
warning: Option<&'a Error>,
|
warning: Option<&'a Error>,
|
||||||
) -> Element<'a, Message> {
|
) -> Element<'a, Message> {
|
||||||
dashboard(
|
dashboard(
|
||||||
@ -83,7 +84,7 @@ pub fn transactions_view<'a>(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn tx_list_view<'a>(i: usize, tx: &HistoryTransaction) -> Element<'a, Message> {
|
fn tx_list_view(i: usize, tx: &HistoryTransaction) -> Element<'_, Message> {
|
||||||
Container::new(
|
Container::new(
|
||||||
Button::new(
|
Button::new(
|
||||||
Row::new()
|
Row::new()
|
||||||
@ -96,7 +97,10 @@ fn tx_list_view<'a>(i: usize, tx: &HistoryTransaction) -> Element<'a, Message> {
|
|||||||
} else {
|
} else {
|
||||||
badge::spend()
|
badge::spend()
|
||||||
})
|
})
|
||||||
.push(if let Some(t) = tx.time {
|
.push(
|
||||||
|
Column::new()
|
||||||
|
.push_maybe(tx.labels.get(&tx.tx.txid().to_string()).map(p1_bold))
|
||||||
|
.push_maybe(tx.time.map(|t| {
|
||||||
Container::new(
|
Container::new(
|
||||||
text(format!(
|
text(format!(
|
||||||
"{}",
|
"{}",
|
||||||
@ -104,15 +108,25 @@ fn tx_list_view<'a>(i: usize, tx: &HistoryTransaction) -> Element<'a, Message> {
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
.format("%b. %d, %Y - %T"),
|
.format("%b. %d, %Y - %T"),
|
||||||
))
|
))
|
||||||
|
.style(color::GREY_3)
|
||||||
.small(),
|
.small(),
|
||||||
)
|
)
|
||||||
} else {
|
})),
|
||||||
badge::unconfirmed()
|
)
|
||||||
})
|
|
||||||
.spacing(10)
|
.spacing(10)
|
||||||
.align_items(Alignment::Center)
|
.align_items(Alignment::Center)
|
||||||
.width(Length::Fill),
|
.width(Length::Fill),
|
||||||
)
|
)
|
||||||
|
.push_maybe(if tx.time.is_none() {
|
||||||
|
Some(badge::unconfirmed())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
})
|
||||||
|
.push_maybe(if tx.is_batch() {
|
||||||
|
Some(badge::batch())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
})
|
||||||
.push(if tx.is_external() {
|
.push(if tx.is_external() {
|
||||||
Row::new()
|
Row::new()
|
||||||
.spacing(5)
|
.spacing(5)
|
||||||
@ -142,8 +156,10 @@ fn tx_list_view<'a>(i: usize, tx: &HistoryTransaction) -> Element<'a, Message> {
|
|||||||
pub fn tx_view<'a>(
|
pub fn tx_view<'a>(
|
||||||
cache: &'a Cache,
|
cache: &'a Cache,
|
||||||
tx: &'a HistoryTransaction,
|
tx: &'a HistoryTransaction,
|
||||||
|
labels_editing: &'a HashMap<String, form::Value<String>>,
|
||||||
warning: Option<&'a Error>,
|
warning: Option<&'a Error>,
|
||||||
) -> Element<'a, Message> {
|
) -> Element<'a, Message> {
|
||||||
|
let txid = tx.tx.txid().to_string();
|
||||||
dashboard(
|
dashboard(
|
||||||
&Menu::Transactions,
|
&Menu::Transactions,
|
||||||
cache,
|
cache,
|
||||||
@ -156,6 +172,11 @@ pub fn tx_view<'a>(
|
|||||||
} else {
|
} else {
|
||||||
Container::new(h3("Outgoing transaction")).width(Length::Fill)
|
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)
|
||||||
|
} else {
|
||||||
|
label::label_editable(txid.clone(), tx.labels.get(&txid), H1_SIZE)
|
||||||
|
})
|
||||||
.push(
|
.push(
|
||||||
Column::new().spacing(20).push(
|
Column::new().spacing(20).push(
|
||||||
Column::new()
|
Column::new()
|
||||||
@ -202,10 +223,10 @@ pub fn tx_view<'a>(
|
|||||||
.push(
|
.push(
|
||||||
Row::new()
|
Row::new()
|
||||||
.align_items(Alignment::Center)
|
.align_items(Alignment::Center)
|
||||||
.push(Container::new(text(format!("{}", tx.tx.txid())).small()))
|
.push(Container::new(text(txid.clone()).small()))
|
||||||
.push(
|
.push(
|
||||||
Button::new(icon::clipboard_icon())
|
Button::new(icon::clipboard_icon())
|
||||||
.on_press(Message::Clipboard(tx.tx.txid().to_string()))
|
.on_press(Message::Clipboard(txid.clone()))
|
||||||
.style(theme::Button::TransparentBorder),
|
.style(theme::Button::TransparentBorder),
|
||||||
)
|
)
|
||||||
.width(Length::Shrink),
|
.width(Length::Shrink),
|
||||||
@ -222,11 +243,8 @@ pub fn tx_view<'a>(
|
|||||||
} else {
|
} else {
|
||||||
Some(tx.change_indexes.clone())
|
Some(tx.change_indexes.clone())
|
||||||
},
|
},
|
||||||
if tx.is_external() {
|
&tx.labels,
|
||||||
Some(tx.change_indexes.clone())
|
labels_editing,
|
||||||
} else {
|
|
||||||
None
|
|
||||||
},
|
|
||||||
))
|
))
|
||||||
.spacing(20),
|
.spacing(20),
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
|
use std::iter::FromIterator;
|
||||||
|
|
||||||
use serde::de::DeserializeOwned;
|
use serde::de::DeserializeOwned;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@ -10,6 +11,7 @@ pub mod error;
|
|||||||
pub mod jsonrpc;
|
pub mod jsonrpc;
|
||||||
|
|
||||||
use liana::{
|
use liana::{
|
||||||
|
commands::LabelItem,
|
||||||
config::Config,
|
config::Config,
|
||||||
miniscript::bitcoin::{address, psbt::Psbt, Address, OutPoint, Txid},
|
miniscript::bitcoin::{address, psbt::Psbt, Address, OutPoint, Txid},
|
||||||
};
|
};
|
||||||
@ -145,6 +147,22 @@ impl<C: Client + Debug> Daemon for Lianad<C> {
|
|||||||
)?;
|
)?;
|
||||||
Ok(res.psbt)
|
Ok(res.psbt)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_labels(
|
||||||
|
&self,
|
||||||
|
items: &HashSet<LabelItem>,
|
||||||
|
) -> Result<HashMap<String, String>, DaemonError> {
|
||||||
|
let items = items.iter().map(|a| a.to_string()).collect::<Vec<String>>();
|
||||||
|
let res: GetLabelsResult = self.call("getlabels", Some(vec![items]))?;
|
||||||
|
Ok(res.labels)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_labels(&self, items: &HashMap<LabelItem, String>) -> Result<(), DaemonError> {
|
||||||
|
let labels: HashMap<String, 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(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
use super::{model::*, Daemon, DaemonError};
|
use super::{model::*, Daemon, DaemonError};
|
||||||
use liana::{
|
use liana::{
|
||||||
|
commands::LabelItem,
|
||||||
config::Config,
|
config::Config,
|
||||||
miniscript::bitcoin::{address, psbt::Psbt, Address, OutPoint, Txid},
|
miniscript::bitcoin::{address, psbt::Psbt, Address, OutPoint, Txid},
|
||||||
DaemonControl, DaemonHandle,
|
DaemonControl, DaemonHandle,
|
||||||
@ -59,7 +60,7 @@ impl Daemon for EmbeddedDaemon {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn list_coins(&self) -> Result<ListCoinsResult, DaemonError> {
|
fn list_coins(&self) -> Result<ListCoinsResult, DaemonError> {
|
||||||
Ok(self.control()?.list_coins())
|
Ok(self.control()?.list_coins(&[], &[]))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn list_spend_txs(&self) -> Result<ListSpendResult, DaemonError> {
|
fn list_spend_txs(&self) -> Result<ListSpendResult, DaemonError> {
|
||||||
@ -126,4 +127,16 @@ impl Daemon for EmbeddedDaemon {
|
|||||||
.map_err(|e| DaemonError::Unexpected(e.to_string()))
|
.map_err(|e| DaemonError::Unexpected(e.to_string()))
|
||||||
.map(|res| res.psbt)
|
.map(|res| res.psbt)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_labels(
|
||||||
|
&self,
|
||||||
|
items: &HashSet<LabelItem>,
|
||||||
|
) -> Result<HashMap<String, String>, DaemonError> {
|
||||||
|
Ok(self.handle.control.get_labels(items).labels)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_labels(&self, items: &HashMap<LabelItem, String>) -> Result<(), DaemonError> {
|
||||||
|
self.handle.control.update_labels(items);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,11 +2,12 @@ pub mod client;
|
|||||||
pub mod embedded;
|
pub mod embedded;
|
||||||
pub mod model;
|
pub mod model;
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
use std::io::ErrorKind;
|
use std::io::ErrorKind;
|
||||||
|
|
||||||
use liana::{
|
use liana::{
|
||||||
|
commands::LabelItem,
|
||||||
config::Config,
|
config::Config,
|
||||||
miniscript::bitcoin::{address, psbt::Psbt, Address, OutPoint, Txid},
|
miniscript::bitcoin::{address, psbt::Psbt, Address, OutPoint, Txid},
|
||||||
StartupError,
|
StartupError,
|
||||||
@ -75,6 +76,11 @@ pub trait Daemon: Debug {
|
|||||||
sequence: Option<u16>,
|
sequence: Option<u16>,
|
||||||
) -> Result<Psbt, DaemonError>;
|
) -> Result<Psbt, DaemonError>;
|
||||||
fn list_txs(&self, txid: &[Txid]) -> Result<model::ListTransactionsResult, DaemonError>;
|
fn list_txs(&self, txid: &[Txid]) -> Result<model::ListTransactionsResult, DaemonError>;
|
||||||
|
fn get_labels(
|
||||||
|
&self,
|
||||||
|
labels: &HashSet<LabelItem>,
|
||||||
|
) -> Result<HashMap<String, String>, DaemonError>;
|
||||||
|
fn update_labels(&self, labels: &HashMap<LabelItem, String>) -> Result<(), DaemonError>;
|
||||||
|
|
||||||
fn list_spend_transactions(&self) -> Result<Vec<model::SpendTx>, DaemonError> {
|
fn list_spend_transactions(&self) -> Result<Vec<model::SpendTx>, DaemonError> {
|
||||||
let info = self.get_info()?;
|
let info = self.get_info()?;
|
||||||
@ -103,8 +109,10 @@ pub trait Daemon: Debug {
|
|||||||
coins,
|
coins,
|
||||||
sigs,
|
sigs,
|
||||||
info.descriptors.main.max_sat_vbytes(),
|
info.descriptors.main.max_sat_vbytes(),
|
||||||
|
info.network,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
load_labels(self, &mut spend_txs)?;
|
||||||
spend_txs.sort_by(|a, b| {
|
spend_txs.sort_by(|a, b| {
|
||||||
if a.status == b.status {
|
if a.status == b.status {
|
||||||
// last updated first
|
// last updated first
|
||||||
@ -123,9 +131,10 @@ pub trait Daemon: Debug {
|
|||||||
end: u32,
|
end: u32,
|
||||||
limit: u64,
|
limit: u64,
|
||||||
) -> Result<Vec<model::HistoryTransaction>, DaemonError> {
|
) -> Result<Vec<model::HistoryTransaction>, DaemonError> {
|
||||||
|
let info = self.get_info()?;
|
||||||
let coins = self.list_coins()?.coins;
|
let coins = self.list_coins()?.coins;
|
||||||
let txs = self.list_confirmed_txs(start, end, limit)?.transactions;
|
let txs = self.list_confirmed_txs(start, end, limit)?.transactions;
|
||||||
Ok(txs
|
let mut txs = txs
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|tx| {
|
.map(|tx| {
|
||||||
let mut tx_coins = Vec::new();
|
let mut tx_coins = Vec::new();
|
||||||
@ -142,12 +151,22 @@ pub trait Daemon: Debug {
|
|||||||
tx_coins.push(coin.clone());
|
tx_coins.push(coin.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
model::HistoryTransaction::new(tx.tx, tx.height, tx.time, tx_coins, change_indexes)
|
model::HistoryTransaction::new(
|
||||||
|
tx.tx,
|
||||||
|
tx.height,
|
||||||
|
tx.time,
|
||||||
|
tx_coins,
|
||||||
|
change_indexes,
|
||||||
|
info.network,
|
||||||
|
)
|
||||||
})
|
})
|
||||||
.collect())
|
.collect();
|
||||||
|
load_labels(self, &mut txs)?;
|
||||||
|
Ok(txs)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn list_pending_txs(&self) -> Result<Vec<model::HistoryTransaction>, DaemonError> {
|
fn list_pending_txs(&self) -> Result<Vec<model::HistoryTransaction>, DaemonError> {
|
||||||
|
let info = self.get_info()?;
|
||||||
let coins = self.list_coins()?.coins;
|
let coins = self.list_coins()?.coins;
|
||||||
let mut txids: Vec<Txid> = Vec::new();
|
let mut txids: Vec<Txid> = Vec::new();
|
||||||
for coin in &coins {
|
for coin in &coins {
|
||||||
@ -163,7 +182,7 @@ pub trait Daemon: Debug {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let txs = self.list_txs(&txids)?.transactions;
|
let txs = self.list_txs(&txids)?.transactions;
|
||||||
Ok(txs
|
let mut txs = txs
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|tx| {
|
.map(|tx| {
|
||||||
let mut tx_coins = Vec::new();
|
let mut tx_coins = Vec::new();
|
||||||
@ -180,8 +199,38 @@ pub trait Daemon: Debug {
|
|||||||
tx_coins.push(coin.clone());
|
tx_coins.push(coin.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
model::HistoryTransaction::new(tx.tx, tx.height, tx.time, tx_coins, change_indexes)
|
model::HistoryTransaction::new(
|
||||||
|
tx.tx,
|
||||||
|
tx.height,
|
||||||
|
tx.time,
|
||||||
|
tx_coins,
|
||||||
|
change_indexes,
|
||||||
|
info.network,
|
||||||
|
)
|
||||||
})
|
})
|
||||||
.collect())
|
.collect();
|
||||||
|
|
||||||
|
load_labels(self, &mut txs)?;
|
||||||
|
Ok(txs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn load_labels<T: model::Labelled, D: Daemon + ?Sized>(
|
||||||
|
daemon: &D,
|
||||||
|
targets: &mut Vec<T>,
|
||||||
|
) -> Result<(), DaemonError> {
|
||||||
|
if targets.is_empty() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let mut items = HashSet::<LabelItem>::new();
|
||||||
|
for target in &*targets {
|
||||||
|
for item in target.labelled() {
|
||||||
|
items.insert(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let labels = daemon.get_labels(&items)?;
|
||||||
|
for target in targets {
|
||||||
|
target.load_labels(&labels);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|||||||
@ -1,12 +1,15 @@
|
|||||||
use std::collections::HashSet;
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
pub use liana::{
|
pub use liana::{
|
||||||
commands::{
|
commands::{
|
||||||
CreateSpendResult, GetAddressResult, GetInfoResult, ListCoinsEntry, ListCoinsResult,
|
CreateSpendResult, GetAddressResult, GetInfoResult, GetLabelsResult, LabelItem,
|
||||||
ListSpendEntry, ListSpendResult, ListTransactionsResult, TransactionInfo,
|
ListCoinsEntry, ListCoinsResult, ListSpendEntry, ListSpendResult, ListTransactionsResult,
|
||||||
|
TransactionInfo,
|
||||||
},
|
},
|
||||||
descriptors::{PartialSpendInfo, PathSpendInfo},
|
descriptors::{PartialSpendInfo, PathSpendInfo},
|
||||||
miniscript::bitcoin::{bip32::Fingerprint, psbt::Psbt, Amount, Transaction},
|
miniscript::bitcoin::{
|
||||||
|
bip32::Fingerprint, psbt::Psbt, Address, Amount, Network, OutPoint, Transaction, Txid,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub type Coin = ListCoinsEntry;
|
pub type Coin = ListCoinsEntry;
|
||||||
@ -25,7 +28,9 @@ pub fn remaining_sequence(coin: &Coin, blockheight: u32, timelock: u16) -> u32 {
|
|||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct SpendTx {
|
pub struct SpendTx {
|
||||||
|
pub network: Network,
|
||||||
pub coins: Vec<Coin>,
|
pub coins: Vec<Coin>,
|
||||||
|
pub labels: HashMap<String, String>,
|
||||||
pub psbt: Psbt,
|
pub psbt: Psbt,
|
||||||
pub change_indexes: Vec<usize>,
|
pub change_indexes: Vec<usize>,
|
||||||
pub spend_amount: Amount,
|
pub spend_amount: Amount,
|
||||||
@ -53,6 +58,7 @@ impl SpendTx {
|
|||||||
coins: Vec<Coin>,
|
coins: Vec<Coin>,
|
||||||
sigs: PartialSpendInfo,
|
sigs: PartialSpendInfo,
|
||||||
max_sat_vbytes: usize,
|
max_sat_vbytes: usize,
|
||||||
|
network: Network,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let mut change_indexes = Vec::new();
|
let mut change_indexes = Vec::new();
|
||||||
let (change_amount, spend_amount) = psbt.unsigned_tx.output.iter().enumerate().fold(
|
let (change_amount, spend_amount) = psbt.unsigned_tx.output.iter().enumerate().fold(
|
||||||
@ -85,6 +91,7 @@ impl SpendTx {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
|
labels: HashMap::new(),
|
||||||
updated_at,
|
updated_at,
|
||||||
coins,
|
coins,
|
||||||
psbt,
|
psbt,
|
||||||
@ -94,6 +101,7 @@ impl SpendTx {
|
|||||||
max_sat_vbytes,
|
max_sat_vbytes,
|
||||||
status,
|
status,
|
||||||
sigs,
|
sigs,
|
||||||
|
network,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -135,10 +143,48 @@ impl SpendTx {
|
|||||||
self.psbt.unsigned_tx.vsize() + (self.max_sat_vbytes * self.psbt.inputs.len());
|
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.to_sat() / max_tx_vbytes as u64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_batch(&self) -> bool {
|
||||||
|
self.psbt
|
||||||
|
.unsigned_tx
|
||||||
|
.output
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter(|(i, _)| !self.change_indexes.contains(i))
|
||||||
|
.count()
|
||||||
|
> 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Labelled for SpendTx {
|
||||||
|
fn labels(&mut self) -> &mut HashMap<String, String> {
|
||||||
|
&mut self.labels
|
||||||
|
}
|
||||||
|
fn labelled(&self) -> Vec<LabelItem> {
|
||||||
|
let mut items = Vec::new();
|
||||||
|
let txid = self.psbt.unsigned_tx.txid();
|
||||||
|
items.push(LabelItem::Txid(txid));
|
||||||
|
for coin in &self.coins {
|
||||||
|
items.push(LabelItem::Address(coin.address.clone()));
|
||||||
|
items.push(LabelItem::OutPoint(coin.outpoint));
|
||||||
|
}
|
||||||
|
for (vout, output) in self.psbt.unsigned_tx.output.iter().enumerate() {
|
||||||
|
items.push(LabelItem::OutPoint(OutPoint {
|
||||||
|
txid,
|
||||||
|
vout: vout as u32,
|
||||||
|
}));
|
||||||
|
items.push(LabelItem::Address(
|
||||||
|
Address::from_script(&output.script_pubkey, self.network).unwrap(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
items
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct HistoryTransaction {
|
pub struct HistoryTransaction {
|
||||||
|
pub network: Network,
|
||||||
|
pub labels: HashMap<String, String>,
|
||||||
pub coins: Vec<Coin>,
|
pub coins: Vec<Coin>,
|
||||||
pub change_indexes: Vec<usize>,
|
pub change_indexes: Vec<usize>,
|
||||||
pub tx: Transaction,
|
pub tx: Transaction,
|
||||||
@ -156,6 +202,7 @@ impl HistoryTransaction {
|
|||||||
time: Option<u32>,
|
time: Option<u32>,
|
||||||
coins: Vec<Coin>,
|
coins: Vec<Coin>,
|
||||||
change_indexes: Vec<usize>,
|
change_indexes: Vec<usize>,
|
||||||
|
network: Network,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let (incoming_amount, outgoing_amount) = tx.output.iter().enumerate().fold(
|
let (incoming_amount, outgoing_amount) = tx.output.iter().enumerate().fold(
|
||||||
(Amount::from_sat(0), Amount::from_sat(0)),
|
(Amount::from_sat(0), Amount::from_sat(0)),
|
||||||
@ -180,6 +227,7 @@ impl HistoryTransaction {
|
|||||||
};
|
};
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
|
labels: HashMap::new(),
|
||||||
tx,
|
tx,
|
||||||
coins,
|
coins,
|
||||||
change_indexes,
|
change_indexes,
|
||||||
@ -188,6 +236,7 @@ impl HistoryTransaction {
|
|||||||
fee_amount,
|
fee_amount,
|
||||||
height,
|
height,
|
||||||
time,
|
time,
|
||||||
|
network,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -198,4 +247,54 @@ impl HistoryTransaction {
|
|||||||
pub fn is_self_send(&self) -> bool {
|
pub fn is_self_send(&self) -> bool {
|
||||||
!self.coins.is_empty() && self.outgoing_amount == Amount::from_sat(0)
|
!self.coins.is_empty() && self.outgoing_amount == Amount::from_sat(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_batch(&self) -> bool {
|
||||||
|
self.tx
|
||||||
|
.output
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter(|(i, _)| !self.change_indexes.contains(i))
|
||||||
|
.count()
|
||||||
|
> 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Labelled for HistoryTransaction {
|
||||||
|
fn labels(&mut self) -> &mut HashMap<String, String> {
|
||||||
|
&mut self.labels
|
||||||
|
}
|
||||||
|
fn labelled(&self) -> Vec<LabelItem> {
|
||||||
|
let mut items = Vec::new();
|
||||||
|
let txid = self.tx.txid();
|
||||||
|
items.push(LabelItem::Txid(txid));
|
||||||
|
for coin in &self.coins {
|
||||||
|
items.push(LabelItem::Address(coin.address.clone()));
|
||||||
|
items.push(LabelItem::OutPoint(coin.outpoint));
|
||||||
|
}
|
||||||
|
for (vout, output) in self.tx.output.iter().enumerate() {
|
||||||
|
items.push(LabelItem::OutPoint(OutPoint {
|
||||||
|
txid,
|
||||||
|
vout: vout as u32,
|
||||||
|
}));
|
||||||
|
items.push(LabelItem::Address(
|
||||||
|
Address::from_script(&output.script_pubkey, self.network).unwrap(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
items
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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>) {
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -100,6 +100,19 @@ pub fn unconfirmed<'a, T: 'a>() -> Container<'a, T> {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn batch<'a, T: 'a>() -> Container<'a, T> {
|
||||||
|
Container::new(
|
||||||
|
tooltip::Tooltip::new(
|
||||||
|
Container::new(text::p2_regular(" Batch "))
|
||||||
|
.padding(10)
|
||||||
|
.style(theme::Container::Pill(theme::Pill::Simple)),
|
||||||
|
"This transaction contains multiple payments",
|
||||||
|
tooltip::Position::Top,
|
||||||
|
)
|
||||||
|
.style(theme::Container::Card(theme::Card::Simple)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn deprecated<'a, T: 'a>() -> Container<'a, T> {
|
pub fn deprecated<'a, T: 'a>() -> Container<'a, T> {
|
||||||
Container::new(
|
Container::new(
|
||||||
tooltip::Tooltip::new(
|
tooltip::Tooltip::new(
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
|
color,
|
||||||
component::{amount, badge, text},
|
component::{amount, badge, text},
|
||||||
theme,
|
theme,
|
||||||
|
util::Collection,
|
||||||
widget::*,
|
widget::*,
|
||||||
};
|
};
|
||||||
use bitcoin::Amount;
|
use bitcoin::Amount;
|
||||||
@ -9,14 +11,19 @@ use iced::{
|
|||||||
Alignment, Length,
|
Alignment, Length,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn unconfirmed_outgoing_event<'a, T: Clone + 'a>(amount: &Amount, msg: T) -> Container<'a, T> {
|
pub fn unconfirmed_outgoing_event<'a, T: Clone + 'a>(
|
||||||
|
label: Option<iced::widget::Text<'a, iced::Renderer<theme::Theme>>>,
|
||||||
|
amount: &Amount,
|
||||||
|
msg: T,
|
||||||
|
) -> Container<'a, T> {
|
||||||
Container::new(
|
Container::new(
|
||||||
button(
|
button(
|
||||||
row!(
|
row!(
|
||||||
row!(badge::spend(), badge::unconfirmed())
|
row!(badge::spend(), Column::new().push_maybe(label),)
|
||||||
.spacing(10)
|
.spacing(10)
|
||||||
.align_items(Alignment::Center)
|
.align_items(Alignment::Center)
|
||||||
.width(Length::Fill),
|
.width(Length::Fill),
|
||||||
|
badge::unconfirmed(),
|
||||||
row!(text::p1_regular("-"), amount::amount(amount))
|
row!(text::p1_regular("-"), amount::amount(amount))
|
||||||
.spacing(5)
|
.spacing(5)
|
||||||
.align_items(Alignment::Center),
|
.align_items(Alignment::Center),
|
||||||
@ -32,6 +39,7 @@ pub fn unconfirmed_outgoing_event<'a, T: Clone + 'a>(amount: &Amount, msg: T) ->
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn confirmed_outgoing_event<'a, T: Clone + 'a>(
|
pub fn confirmed_outgoing_event<'a, T: Clone + 'a>(
|
||||||
|
label: Option<iced::widget::Text<'a, iced::Renderer<theme::Theme>>>,
|
||||||
date: chrono::NaiveDateTime,
|
date: chrono::NaiveDateTime,
|
||||||
amount: &Amount,
|
amount: &Amount,
|
||||||
msg: T,
|
msg: T,
|
||||||
@ -41,7 +49,10 @@ pub fn confirmed_outgoing_event<'a, T: Clone + 'a>(
|
|||||||
row!(
|
row!(
|
||||||
row!(
|
row!(
|
||||||
badge::spend(),
|
badge::spend(),
|
||||||
|
Column::new().push_maybe(label).push(
|
||||||
text::p2_regular(date.format("%b. %d, %Y - %T").to_string())
|
text::p2_regular(date.format("%b. %d, %Y - %T").to_string())
|
||||||
|
.style(color::GREY_3)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
.spacing(10)
|
.spacing(10)
|
||||||
.align_items(Alignment::Center)
|
.align_items(Alignment::Center)
|
||||||
@ -60,14 +71,19 @@ pub fn confirmed_outgoing_event<'a, T: Clone + 'a>(
|
|||||||
.style(theme::Container::Card(theme::Card::Simple))
|
.style(theme::Container::Card(theme::Card::Simple))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn unconfirmed_incoming_event<'a, T: Clone + 'a>(amount: &Amount, msg: T) -> Container<'a, T> {
|
pub fn unconfirmed_incoming_event<'a, T: Clone + 'a>(
|
||||||
|
label: Option<iced::widget::Text<'a, iced::Renderer<theme::Theme>>>,
|
||||||
|
amount: &Amount,
|
||||||
|
msg: T,
|
||||||
|
) -> Container<'a, T> {
|
||||||
Container::new(
|
Container::new(
|
||||||
button(
|
button(
|
||||||
row!(
|
row!(
|
||||||
row!(badge::receive(), badge::unconfirmed())
|
row!(badge::receive(), Column::new().push_maybe(label))
|
||||||
.spacing(10)
|
.spacing(10)
|
||||||
.align_items(Alignment::Center)
|
.align_items(Alignment::Center)
|
||||||
.width(Length::Fill),
|
.width(Length::Fill),
|
||||||
|
badge::unconfirmed(),
|
||||||
row!(text::p1_regular("+"), amount::amount(amount))
|
row!(text::p1_regular("+"), amount::amount(amount))
|
||||||
.spacing(5)
|
.spacing(5)
|
||||||
.align_items(Alignment::Center),
|
.align_items(Alignment::Center),
|
||||||
@ -83,6 +99,7 @@ pub fn unconfirmed_incoming_event<'a, T: Clone + 'a>(amount: &Amount, msg: T) ->
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn confirmed_incoming_event<'a, T: Clone + 'a>(
|
pub fn confirmed_incoming_event<'a, T: Clone + 'a>(
|
||||||
|
label: Option<iced::widget::Text<'a, iced::Renderer<theme::Theme>>>,
|
||||||
date: chrono::NaiveDateTime,
|
date: chrono::NaiveDateTime,
|
||||||
amount: &Amount,
|
amount: &Amount,
|
||||||
msg: T,
|
msg: T,
|
||||||
@ -92,7 +109,10 @@ pub fn confirmed_incoming_event<'a, T: Clone + 'a>(
|
|||||||
row!(
|
row!(
|
||||||
row!(
|
row!(
|
||||||
badge::receive(),
|
badge::receive(),
|
||||||
|
Column::new().push_maybe(label).push(
|
||||||
text::p2_regular(date.format("%b. %d, %Y - %T").to_string())
|
text::p2_regular(date.format("%b. %d, %Y - %T").to_string())
|
||||||
|
.style(color::GREY_3)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
.spacing(10)
|
.spacing(10)
|
||||||
.align_items(Alignment::Center)
|
.align_items(Alignment::Center)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user