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]]
|
||||
name = "liana"
|
||||
version = "1.0.0"
|
||||
source = "git+https://github.com/wizardsardine/liana?branch=master#85d470dd8dd67e6726118fe6dd86f9b4c8d3b0ef"
|
||||
version = "2.0.0"
|
||||
source = "git+https://github.com/wizardsardine/liana?branch=master#605a13d4bab662f832b8fcb0d915eb17d0360c1f"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"bip39",
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use liana::{
|
||||
@ -22,6 +23,7 @@ pub enum Message {
|
||||
Info(Result<GetInfoResult, Error>),
|
||||
ReceiveAddress(Result<Address, Error>),
|
||||
Coins(Result<Vec<Coin>, Error>),
|
||||
Labels(Result<HashMap<String, String>, Error>),
|
||||
SpendTxs(Result<Vec<SpendTx>, Error>),
|
||||
Psbt(Result<Psbt, Error>),
|
||||
Recovery(Result<SpendTx, Error>),
|
||||
@ -33,4 +35,5 @@ pub enum Message {
|
||||
ConnectedHardwareWallets(Vec<HardwareWallet>),
|
||||
HistoryTransactions(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.cache.coins,
|
||||
self.cache.blockheight as u32,
|
||||
self.cache.network,
|
||||
)
|
||||
.into(),
|
||||
menu::Menu::RefreshCoins(preselected) => CreateSpendPanel::new_self_send(
|
||||
@ -104,6 +105,7 @@ impl App {
|
||||
&self.cache.coins,
|
||||
self.cache.blockheight as u32,
|
||||
preselected,
|
||||
self.cache.network,
|
||||
)
|
||||
.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::fs::OpenOptions;
|
||||
use std::io::Write;
|
||||
@ -8,8 +10,6 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
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";
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
|
||||
@ -1,18 +1,48 @@
|
||||
use std::cmp::Ordering;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::{cmp::Ordering, collections::HashSet};
|
||||
|
||||
use iced::Command;
|
||||
|
||||
use liana_ui::widget::Element;
|
||||
|
||||
use crate::{
|
||||
app::{cache::Cache, error::Error, menu::Menu, message::Message, state::State, view},
|
||||
daemon::{model::Coin, Daemon},
|
||||
app::{
|
||||
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 {
|
||||
coins: Vec<Coin>,
|
||||
coins: Coins,
|
||||
selected: Vec<usize>,
|
||||
labels_edited: LabelsEdited,
|
||||
warning: Option<Error>,
|
||||
/// timelock value to pass for the heir to consume a coin.
|
||||
timelock: u16,
|
||||
@ -21,7 +51,8 @@ pub struct CoinsPanel {
|
||||
impl CoinsPanel {
|
||||
pub fn new(coins: &[Coin], timelock: u16) -> Self {
|
||||
let mut panel = Self {
|
||||
coins: Vec::new(),
|
||||
labels_edited: LabelsEdited::default(),
|
||||
coins: Coins::default(),
|
||||
selected: Vec::new(),
|
||||
warning: None,
|
||||
timelock,
|
||||
@ -31,18 +62,14 @@ impl CoinsPanel {
|
||||
}
|
||||
|
||||
fn update_coins(&mut self, coins: &[Coin]) {
|
||||
self.coins = coins
|
||||
self.coins.list = coins
|
||||
.iter()
|
||||
.filter_map(|coin| {
|
||||
if coin.spend_info.is_none() {
|
||||
Some(coin.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.filter(|coin| coin.spend_info.is_none())
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
self.coins
|
||||
.list
|
||||
.sort_by(|a, b| match (a.block_height, b.block_height) {
|
||||
(Some(a_height), Some(b_height)) => {
|
||||
if a_height == b_height {
|
||||
@ -64,13 +91,20 @@ impl State for CoinsPanel {
|
||||
&Menu::Coins,
|
||||
cache,
|
||||
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(
|
||||
&mut self,
|
||||
_daemon: Arc<dyn Daemon + Sync + Send>,
|
||||
daemon: Arc<dyn Daemon + Sync + Send>,
|
||||
_cache: &Cache,
|
||||
message: Message,
|
||||
) -> Command<Message> {
|
||||
@ -83,6 +117,24 @@ impl State for CoinsPanel {
|
||||
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)) => {
|
||||
if let Some(position) = self.selected.iter().position(|j| *j == i) {
|
||||
self.selected.remove(position);
|
||||
@ -96,16 +148,34 @@ impl State for CoinsPanel {
|
||||
}
|
||||
|
||||
fn load(&self, daemon: Arc<dyn Daemon + Sync + Send>) -> Command<Message> {
|
||||
let daemon = daemon.clone();
|
||||
Command::perform(
|
||||
async move {
|
||||
daemon
|
||||
.list_coins()
|
||||
.map(|res| res.coins)
|
||||
.map_err(|e| e.into())
|
||||
},
|
||||
Message::Coins,
|
||||
)
|
||||
let daemon1 = daemon.clone();
|
||||
let daemon2 = daemon.clone();
|
||||
Command::batch(vec![
|
||||
Command::perform(
|
||||
async move {
|
||||
daemon1
|
||||
.list_coins()
|
||||
.map(|res| res.coins)
|
||||
.map_err(|e| e.into())
|
||||
},
|
||||
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!(
|
||||
panel
|
||||
.coins
|
||||
.list
|
||||
.iter()
|
||||
.map(|c| c.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 label;
|
||||
mod psbt;
|
||||
mod psbts;
|
||||
mod recovery;
|
||||
@ -6,6 +7,7 @@ mod settings;
|
||||
mod spend;
|
||||
mod transactions;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::convert::TryInto;
|
||||
use std::sync::Arc;
|
||||
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 crate::daemon::{
|
||||
model::{remaining_sequence, Coin, HistoryTransaction},
|
||||
model::{remaining_sequence, Coin, HistoryTransaction, LabelItem, Labelled},
|
||||
Daemon,
|
||||
};
|
||||
pub use coins::CoinsPanel;
|
||||
use label::LabelsEdited;
|
||||
pub use psbts::PsbtsPanel;
|
||||
pub use recovery::RecoveryPanel;
|
||||
pub use settings::SettingsState;
|
||||
@ -54,6 +57,7 @@ pub struct Home {
|
||||
pending_events: Vec<HistoryTransaction>,
|
||||
events: Vec<HistoryTransaction>,
|
||||
selected_event: Option<(usize, usize)>,
|
||||
labels_edited: LabelsEdited,
|
||||
warning: Option<Error>,
|
||||
}
|
||||
|
||||
@ -80,6 +84,7 @@ impl Home {
|
||||
selected_event: None,
|
||||
events: Vec::new(),
|
||||
pending_events: Vec::new(),
|
||||
labels_edited: LabelsEdited::default(),
|
||||
warning: None,
|
||||
}
|
||||
}
|
||||
@ -93,7 +98,13 @@ impl State for Home {
|
||||
} else {
|
||||
&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 {
|
||||
view::dashboard(
|
||||
&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) => {
|
||||
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)]
|
||||
pub struct ReceivePanel {
|
||||
addresses: Vec<Address>,
|
||||
addresses: Addresses,
|
||||
labels_edited: LabelsEdited,
|
||||
qr_code: Option<qr_code::State>,
|
||||
warning: Option<Error>,
|
||||
}
|
||||
@ -278,7 +325,12 @@ impl State for ReceivePanel {
|
||||
&Menu::Receive,
|
||||
cache,
|
||||
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(
|
||||
@ -288,12 +340,25 @@ impl State for ReceivePanel {
|
||||
message: Message,
|
||||
) -> Command<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) => {
|
||||
match res {
|
||||
Ok(address) => {
|
||||
self.warning = None;
|
||||
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),
|
||||
}
|
||||
@ -363,6 +428,6 @@ mod tests {
|
||||
let sandbox = sandbox.load(client, &Cache::default()).await;
|
||||
|
||||
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 iced::Command;
|
||||
@ -17,11 +17,12 @@ use crate::{
|
||||
cache::Cache,
|
||||
error::Error,
|
||||
message::Message,
|
||||
state::label::{label_item_from_str, LabelsEdited},
|
||||
view,
|
||||
wallet::{Wallet, WalletError},
|
||||
},
|
||||
daemon::{
|
||||
model::{SpendStatus, SpendTx},
|
||||
model::{LabelItem, Labelled, SpendStatus, SpendTx},
|
||||
Daemon,
|
||||
},
|
||||
hw::{list_hardware_wallets, HardwareWallet},
|
||||
@ -50,6 +51,8 @@ pub struct PsbtState {
|
||||
pub desc_policy: LianaPolicy,
|
||||
pub tx: SpendTx,
|
||||
pub saved: bool,
|
||||
pub warning: Option<Error>,
|
||||
pub labels_edited: LabelsEdited,
|
||||
pub action: Option<Box<dyn Action>>,
|
||||
}
|
||||
|
||||
@ -58,6 +61,8 @@ impl PsbtState {
|
||||
Self {
|
||||
desc_policy: wallet.main_descriptor.policy(),
|
||||
wallet,
|
||||
labels_edited: LabelsEdited::default(),
|
||||
warning: None,
|
||||
action: None,
|
||||
tx,
|
||||
saved,
|
||||
@ -84,7 +89,7 @@ impl PsbtState {
|
||||
self.action = None;
|
||||
}
|
||||
view::SpendTxMessage::Delete => {
|
||||
self.action = Some(Box::new(DeleteAction::default()));
|
||||
self.action = Some(Box::<DeleteAction>::default());
|
||||
}
|
||||
view::SpendTxMessage::Sign => {
|
||||
let action = SignAction::new(self.tx.signers(), self.wallet.clone());
|
||||
@ -99,10 +104,10 @@ impl PsbtState {
|
||||
return cmd;
|
||||
}
|
||||
view::SpendTxMessage::Broadcast => {
|
||||
self.action = Some(Box::new(BroadcastAction::default()));
|
||||
self.action = Some(Box::<BroadcastAction>::default());
|
||||
}
|
||||
view::SpendTxMessage::Save => {
|
||||
self.action = Some(Box::new(SaveAction::default()));
|
||||
self.action = Some(Box::<SaveAction>::default());
|
||||
}
|
||||
_ => {
|
||||
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(_)) => {
|
||||
self.saved = true;
|
||||
if let Some(action) = self.action.as_mut() {
|
||||
@ -132,7 +151,9 @@ impl PsbtState {
|
||||
self.saved,
|
||||
&self.desc_policy,
|
||||
&self.wallet.keys_aliases,
|
||||
self.labels_edited.cache(),
|
||||
cache.network,
|
||||
self.warning.as_ref(),
|
||||
);
|
||||
if let Some(action) = &self.action {
|
||||
modal::Modal::new(content, action.view())
|
||||
@ -161,8 +182,18 @@ impl Action for SaveAction {
|
||||
Message::View(view::Message::Spend(view::SpendTxMessage::Confirm)) => {
|
||||
let daemon = daemon.clone();
|
||||
let psbt = tx.psbt.clone();
|
||||
let mut labels = HashMap::<LabelItem, String>::new();
|
||||
for (item, label) in tx.labels() {
|
||||
labels.insert(
|
||||
label_item_from_str(item).expect("Must be a LabelItem"),
|
||||
label.clone(),
|
||||
);
|
||||
}
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
@ -140,22 +140,29 @@ impl State for RecoveryPanel {
|
||||
.recovery_paths
|
||||
.get(self.selected_path.expect("A path must be selected"))
|
||||
.map(|p| p.sequence);
|
||||
let network = cache.network;
|
||||
return Command::perform(
|
||||
async move {
|
||||
let psbt = daemon.create_recovery(address, feerate_vb, sequence)?;
|
||||
let coins = daemon.list_coins().map(|res| res.coins)?;
|
||||
let coins = coins
|
||||
.iter()
|
||||
.into_iter()
|
||||
.filter(|coin| {
|
||||
psbt.unsigned_tx
|
||||
.input
|
||||
.iter()
|
||||
.any(|input| input.previous_output == coin.outpoint)
|
||||
})
|
||||
.cloned()
|
||||
.collect();
|
||||
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,
|
||||
);
|
||||
|
||||
@ -1,15 +1,20 @@
|
||||
mod step;
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::sync::Arc;
|
||||
|
||||
use iced::Command;
|
||||
|
||||
use liana::miniscript::bitcoin::OutPoint;
|
||||
use liana::miniscript::bitcoin::{Network, OutPoint};
|
||||
use liana_ui::widget::Element;
|
||||
|
||||
use super::{redirect, State};
|
||||
use crate::{
|
||||
app::{cache::Cache, menu::Menu, message::Message, view, wallet::Wallet},
|
||||
daemon::{model::Coin, Daemon},
|
||||
app::{cache::Cache, error::Error, menu::Menu, message::Message, view, wallet::Wallet},
|
||||
daemon::{
|
||||
model::{Coin, LabelItem},
|
||||
Daemon,
|
||||
},
|
||||
};
|
||||
|
||||
pub struct CreateSpendPanel {
|
||||
@ -19,11 +24,11 @@ pub struct 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 timelock = descriptor.first_timelock_value();
|
||||
Self {
|
||||
draft: step::TransactionDraft::default(),
|
||||
draft: step::TransactionDraft::new(network),
|
||||
current: 0,
|
||||
steps: vec![
|
||||
Box::new(
|
||||
@ -40,11 +45,12 @@ impl CreateSpendPanel {
|
||||
coins: &[Coin],
|
||||
blockheight: u32,
|
||||
preselected_coins: &[OutPoint],
|
||||
network: Network,
|
||||
) -> Self {
|
||||
let descriptor = wallet.main_descriptor.clone();
|
||||
let timelock = descriptor.first_timelock_value();
|
||||
Self {
|
||||
draft: step::TransactionDraft::default(),
|
||||
draft: step::TransactionDraft::new(network),
|
||||
current: 0,
|
||||
steps: vec![
|
||||
Box::new(
|
||||
@ -99,16 +105,33 @@ impl State for CreateSpendPanel {
|
||||
}
|
||||
|
||||
fn load(&self, daemon: Arc<dyn Daemon + Sync + Send>) -> Command<Message> {
|
||||
let daemon = daemon.clone();
|
||||
Command::perform(
|
||||
async move {
|
||||
daemon
|
||||
.list_coins()
|
||||
.map(|res| res.coins)
|
||||
.map_err(|e| e.into())
|
||||
},
|
||||
Message::Coins,
|
||||
)
|
||||
let daemon1 = daemon.clone();
|
||||
let daemon2 = daemon.clone();
|
||||
Command::batch(vec![
|
||||
Command::perform(
|
||||
async move {
|
||||
daemon1
|
||||
.list_coins()
|
||||
.map(|res| res.coins)
|
||||
.map_err(|e| e.into())
|
||||
},
|
||||
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
|
||||
const DUST_OUTPUT_SATS: u64 = 5_000;
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
#[derive(Clone)]
|
||||
pub struct TransactionDraft {
|
||||
network: Network,
|
||||
inputs: Vec<Coin>,
|
||||
recipients: Vec<Recipient>,
|
||||
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 {
|
||||
@ -53,6 +70,8 @@ pub struct DefineSpend {
|
||||
descriptor: LianaDescriptor,
|
||||
timelock: u16,
|
||||
coins: Vec<(Coin, bool)>,
|
||||
coins_labels: HashMap<String, String>,
|
||||
batch_label: form::Value<String>,
|
||||
amount_left_to_select: Option<Amount>,
|
||||
feerate: form::Value<String>,
|
||||
generated: Option<Psbt>,
|
||||
@ -88,6 +107,8 @@ impl DefineSpend {
|
||||
timelock,
|
||||
generated: None,
|
||||
coins,
|
||||
coins_labels: HashMap::new(),
|
||||
batch_label: form::Value::default(),
|
||||
recipients: vec![Recipient::default()],
|
||||
is_valid: false,
|
||||
is_duplicate: false,
|
||||
@ -128,7 +149,9 @@ impl DefineSpend {
|
||||
}
|
||||
|
||||
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;
|
||||
if !self.coins.iter().any(|(_, selected)| *selected) {
|
||||
self.is_valid = false;
|
||||
@ -216,94 +239,146 @@ impl Step for DefineSpend {
|
||||
cache: &Cache,
|
||||
message: Message,
|
||||
) -> Command<Message> {
|
||||
if let Message::View(view::Message::CreateSpend(msg)) = message {
|
||||
match msg {
|
||||
view::CreateSpendMessage::AddRecipient => {
|
||||
self.recipients.push(Recipient::default());
|
||||
}
|
||||
view::CreateSpendMessage::DeleteRecipient(i) => {
|
||||
self.recipients.remove(i);
|
||||
}
|
||||
view::CreateSpendMessage::RecipientEdited(i, _, _) => {
|
||||
self.recipients
|
||||
.get_mut(i)
|
||||
.unwrap()
|
||||
.update(cache.network, msg);
|
||||
}
|
||||
|
||||
view::CreateSpendMessage::FeerateEdited(s) => {
|
||||
if let Ok(value) = s.parse::<u64>() {
|
||||
self.feerate.value = s;
|
||||
self.feerate.valid = value != 0;
|
||||
self.amount_left_to_select();
|
||||
} else if s.is_empty() {
|
||||
self.feerate.value = "".to_string();
|
||||
self.feerate.valid = true;
|
||||
self.amount_left_to_select = None;
|
||||
} else {
|
||||
self.feerate.valid = false;
|
||||
self.amount_left_to_select = None;
|
||||
match message {
|
||||
Message::View(view::Message::CreateSpend(msg)) => {
|
||||
match msg {
|
||||
view::CreateSpendMessage::BatchLabelEdited(label) => {
|
||||
self.batch_label.valid = label.len() <= 100;
|
||||
self.batch_label.value = label;
|
||||
}
|
||||
self.warning = None;
|
||||
}
|
||||
view::CreateSpendMessage::Generate => {
|
||||
let inputs: Vec<OutPoint> = self
|
||||
.coins
|
||||
.iter()
|
||||
.filter_map(
|
||||
|(coin, selected)| if *selected { Some(coin.outpoint) } else { None },
|
||||
)
|
||||
.collect();
|
||||
let mut outputs: HashMap<Address<address::NetworkUnchecked>, u64> =
|
||||
HashMap::new();
|
||||
for recipient in &self.recipients {
|
||||
outputs.insert(
|
||||
Address::from_str(&recipient.address.value).expect("Checked before"),
|
||||
recipient.amount().expect("Checked before"),
|
||||
view::CreateSpendMessage::AddRecipient => {
|
||||
self.recipients.push(Recipient::default());
|
||||
}
|
||||
view::CreateSpendMessage::DeleteRecipient(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, _, _) => {
|
||||
self.recipients
|
||||
.get_mut(i)
|
||||
.unwrap()
|
||||
.update(cache.network, msg);
|
||||
}
|
||||
|
||||
view::CreateSpendMessage::FeerateEdited(s) => {
|
||||
if let Ok(value) = s.parse::<u64>() {
|
||||
self.feerate.value = s;
|
||||
self.feerate.valid = value != 0;
|
||||
self.amount_left_to_select();
|
||||
} else if s.is_empty() {
|
||||
self.feerate.value = "".to_string();
|
||||
self.feerate.valid = true;
|
||||
self.amount_left_to_select = None;
|
||||
} else {
|
||||
self.feerate.valid = false;
|
||||
self.amount_left_to_select = None;
|
||||
}
|
||||
self.warning = None;
|
||||
}
|
||||
view::CreateSpendMessage::Generate => {
|
||||
let inputs: Vec<OutPoint> = self
|
||||
.coins
|
||||
.iter()
|
||||
.filter_map(
|
||||
|(coin, selected)| {
|
||||
if *selected {
|
||||
Some(coin.outpoint)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
},
|
||||
)
|
||||
.collect();
|
||||
let mut outputs: HashMap<Address<address::NetworkUnchecked>, u64> =
|
||||
HashMap::new();
|
||||
for recipient in &self.recipients {
|
||||
outputs.insert(
|
||||
Address::from_str(&recipient.address.value)
|
||||
.expect("Checked before"),
|
||||
recipient.amount().expect("Checked before"),
|
||||
);
|
||||
}
|
||||
let feerate_vb = self.feerate.value.parse::<u64>().unwrap_or(0);
|
||||
self.warning = None;
|
||||
return Command::perform(
|
||||
async move {
|
||||
daemon
|
||||
.create_spend_tx(&inputs, &outputs, feerate_vb)
|
||||
.map(|res| res.psbt)
|
||||
.map_err(|e| e.into())
|
||||
},
|
||||
Message::Psbt,
|
||||
);
|
||||
}
|
||||
let feerate_vb = self.feerate.value.parse::<u64>().unwrap_or(0);
|
||||
self.warning = None;
|
||||
return Command::perform(
|
||||
async move {
|
||||
daemon
|
||||
.create_spend_tx(&inputs, &outputs, feerate_vb)
|
||||
.map(|res| res.psbt)
|
||||
.map_err(|e| e.into())
|
||||
},
|
||||
Message::Psbt,
|
||||
);
|
||||
}
|
||||
view::CreateSpendMessage::SelectCoin(i) => {
|
||||
if let Some(coin) = self.coins.get_mut(i) {
|
||||
coin.1 = !coin.1;
|
||||
self.amount_left_to_select();
|
||||
view::CreateSpendMessage::SelectCoin(i) => {
|
||||
if let Some(coin) = self.coins.get_mut(i) {
|
||||
coin.1 = !coin.1;
|
||||
self.amount_left_to_select();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
_ => {}
|
||||
self.check_valid();
|
||||
}
|
||||
self.check_valid();
|
||||
Command::none()
|
||||
} else {
|
||||
if let Message::Psbt(res) = message {
|
||||
match res {
|
||||
Ok(psbt) => {
|
||||
self.generated = Some(psbt);
|
||||
return Command::perform(async {}, |_| Message::View(view::Message::Next));
|
||||
}
|
||||
Err(e) => self.warning = Some(e),
|
||||
Message::Psbt(res) => match res {
|
||||
Ok(psbt) => {
|
||||
self.generated = Some(psbt);
|
||||
return Command::perform(async {}, |_| Message::View(view::Message::Next));
|
||||
}
|
||||
}
|
||||
Command::none()
|
||||
}
|
||||
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()
|
||||
}
|
||||
|
||||
fn apply(&self, draft: &mut TransactionDraft) {
|
||||
draft.inputs = self
|
||||
.coins
|
||||
.iter()
|
||||
.filter_map(|(coin, selected)| if *selected { Some(coin.clone()) } else { None })
|
||||
.filter_map(|(coin, selected)| if *selected { Some(coin) } else { None })
|
||||
.cloned()
|
||||
.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();
|
||||
}
|
||||
|
||||
@ -326,6 +401,8 @@ impl Step for DefineSpend {
|
||||
self.is_duplicate,
|
||||
self.timelock,
|
||||
&self.coins,
|
||||
&self.coins_labels,
|
||||
&self.batch_label,
|
||||
self.amount_left_to_select.as_ref(),
|
||||
&self.feerate,
|
||||
self.warning.as_ref(),
|
||||
@ -333,8 +410,9 @@ impl Step for DefineSpend {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
#[derive(Default, Clone)]
|
||||
struct Recipient {
|
||||
label: form::Value<String>,
|
||||
address: form::Value<String>,
|
||||
amount: form::Value<String>,
|
||||
}
|
||||
@ -372,6 +450,7 @@ impl Recipient {
|
||||
&& self.address.valid
|
||||
&& !self.amount.value.is_empty()
|
||||
&& self.amount.valid
|
||||
&& self.label.valid
|
||||
}
|
||||
|
||||
fn update(&mut self, network: Network, message: view::CreateSpendMessage) {
|
||||
@ -399,12 +478,16 @@ impl Recipient {
|
||||
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> {
|
||||
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
|
||||
.partial_spend_info(&psbt)
|
||||
.unwrap();
|
||||
self.spend = Some(psbt::PsbtState::new(
|
||||
self.wallet.clone(),
|
||||
SpendTx::new(
|
||||
None,
|
||||
psbt,
|
||||
draft.inputs.clone(),
|
||||
sigs,
|
||||
self.wallet.main_descriptor.max_sat_vbytes(),
|
||||
),
|
||||
false,
|
||||
));
|
||||
|
||||
let mut tx = SpendTx::new(
|
||||
None,
|
||||
psbt,
|
||||
draft.inputs.clone(),
|
||||
sigs,
|
||||
self.wallet.main_descriptor.max_sat_vbytes(),
|
||||
draft.network,
|
||||
);
|
||||
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(
|
||||
@ -464,7 +579,9 @@ impl Step for SaveSpend {
|
||||
spend.saved,
|
||||
&spend.desc_policy,
|
||||
&spend.wallet.keys_aliases,
|
||||
spend.labels_edited.cache(),
|
||||
cache.network,
|
||||
spend.warning.as_ref(),
|
||||
);
|
||||
if let Some(action) = &spend.action {
|
||||
modal::Modal::new(content, action.view())
|
||||
|
||||
@ -1,18 +1,30 @@
|
||||
use std::convert::TryInto;
|
||||
use std::sync::Arc;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use std::{
|
||||
convert::TryInto,
|
||||
sync::Arc,
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
use iced::Command;
|
||||
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)]
|
||||
pub struct TransactionsPanel {
|
||||
pending_txs: Vec<HistoryTransaction>,
|
||||
txs: Vec<HistoryTransaction>,
|
||||
labels_edited: LabelsEdited,
|
||||
selected_tx: Option<usize>,
|
||||
warning: Option<Error>,
|
||||
}
|
||||
@ -23,6 +35,7 @@ impl TransactionsPanel {
|
||||
selected_tx: None,
|
||||
txs: Vec::new(),
|
||||
pending_txs: Vec::new(),
|
||||
labels_edited: LabelsEdited::default(),
|
||||
warning: None,
|
||||
}
|
||||
}
|
||||
@ -36,7 +49,12 @@ impl State for TransactionsPanel {
|
||||
} else {
|
||||
&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 {
|
||||
view::transactions::transactions_view(
|
||||
cache,
|
||||
@ -82,6 +100,23 @@ impl State for TransactionsPanel {
|
||||
Message::View(view::Message::Select(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) => {
|
||||
if let Some(last) = self.txs.last() {
|
||||
let daemon = daemon.clone();
|
||||
|
||||
@ -1,15 +1,21 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use iced::{widget::Space, Alignment, Length};
|
||||
|
||||
use liana_ui::{
|
||||
color,
|
||||
component::{amount::*, badge, button, text::*},
|
||||
component::{amount::*, badge, button, form, text::*},
|
||||
icon, theme,
|
||||
util::Collection,
|
||||
widget::*,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
app::{cache::Cache, menu::Menu, view::message::Message},
|
||||
app::{
|
||||
cache::Cache,
|
||||
menu::Menu,
|
||||
view::{label, message::Message},
|
||||
},
|
||||
daemon::model::{remaining_sequence, Coin},
|
||||
};
|
||||
|
||||
@ -18,6 +24,8 @@ pub fn coins_view<'a>(
|
||||
coins: &'a [Coin],
|
||||
timelock: u16,
|
||||
selected: &[usize],
|
||||
labels: &'a HashMap<String, String>,
|
||||
labels_editing: &'a HashMap<String, form::Value<String>>,
|
||||
) -> Element<'a, Message> {
|
||||
Column::new()
|
||||
.push(Container::new(h3("Coins")).width(Length::Fill))
|
||||
@ -33,6 +41,8 @@ pub fn coins_view<'a>(
|
||||
cache.blockheight as u32,
|
||||
i,
|
||||
selected.contains(&i),
|
||||
labels,
|
||||
labels_editing,
|
||||
))
|
||||
},
|
||||
)),
|
||||
@ -43,13 +53,17 @@ pub fn coins_view<'a>(
|
||||
}
|
||||
|
||||
#[allow(clippy::collapsible_else_if)]
|
||||
fn coin_list_view(
|
||||
coin: &Coin,
|
||||
fn coin_list_view<'a>(
|
||||
coin: &'a Coin,
|
||||
timelock: u16,
|
||||
blockheight: u32,
|
||||
index: usize,
|
||||
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(
|
||||
Column::new()
|
||||
.push(
|
||||
@ -58,6 +72,17 @@ fn coin_list_view(
|
||||
.push(
|
||||
Row::new()
|
||||
.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() {
|
||||
badge::spent()
|
||||
} else if coin.block_height.is_none() {
|
||||
@ -83,6 +108,18 @@ fn coin_list_view(
|
||||
Column::new()
|
||||
.padding(10)
|
||||
.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() {
|
||||
if let Some(b) = coin.block_height {
|
||||
if blockheight > b as u32 + timelock as u32 {
|
||||
@ -104,6 +141,42 @@ fn coin_list_view(
|
||||
})
|
||||
.push(
|
||||
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(
|
||||
Row::new()
|
||||
.align_items(Alignment::Center)
|
||||
@ -186,7 +259,7 @@ pub fn coin_sequence_label<'a, T: 'a>(seq: u32, timelock: u32) -> Container<'a,
|
||||
)
|
||||
.padding(10)
|
||||
.style(theme::Container::Pill(theme::Pill::Warning))
|
||||
} else if seq < timelock as u32 * 10 / 100 {
|
||||
} else if seq < timelock * 10 / 100 {
|
||||
Container::new(
|
||||
Row::new()
|
||||
.spacing(5)
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
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_ui::{
|
||||
color,
|
||||
component::{amount::*, button, card, event, text::*},
|
||||
component::{amount::*, button, card, event, form, text::*},
|
||||
icon, theme,
|
||||
util::Collection,
|
||||
widget::*,
|
||||
@ -16,7 +17,7 @@ use crate::{
|
||||
cache::Cache,
|
||||
error::Error,
|
||||
menu::Menu,
|
||||
view::{coins, dashboard, message::Message},
|
||||
view::{coins, dashboard, label, message::Message},
|
||||
},
|
||||
daemon::model::HistoryTransaction,
|
||||
};
|
||||
@ -28,8 +29,8 @@ pub fn home_view<'a>(
|
||||
unconfirmed_balance: &'a bitcoin::Amount,
|
||||
remaining_sequence: &Option<u32>,
|
||||
expiring_coins: &Vec<bitcoin::OutPoint>,
|
||||
pending_events: &[HistoryTransaction],
|
||||
events: &Vec<HistoryTransaction>,
|
||||
pending_events: &'a [HistoryTransaction],
|
||||
events: &'a Vec<HistoryTransaction>,
|
||||
) -> Element<'a, Message> {
|
||||
Column::new()
|
||||
.push(h3("Balance"))
|
||||
@ -145,21 +146,41 @@ pub fn home_view<'a>(
|
||||
.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(
|
||||
Column::new().spacing(10),
|
||||
|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.change_indexes.contains(&output_index) {
|
||||
col
|
||||
} else if let Some(t) = event.time {
|
||||
col.push(event::confirmed_incoming_event(
|
||||
label,
|
||||
NaiveDateTime::from_timestamp_opt(t as i64, 0).unwrap(),
|
||||
&Amount::from_sat(output.value),
|
||||
Message::SelectSub(i, output_index),
|
||||
))
|
||||
} else {
|
||||
col.push(event::unconfirmed_incoming_event(
|
||||
label,
|
||||
&Amount::from_sat(output.value),
|
||||
Message::SelectSub(i, output_index),
|
||||
))
|
||||
@ -168,12 +189,14 @@ fn event_list_view<'a>(i: usize, event: &HistoryTransaction) -> Column<'a, Messa
|
||||
col
|
||||
} else if let Some(t) = event.time {
|
||||
col.push(event::confirmed_outgoing_event(
|
||||
label,
|
||||
NaiveDateTime::from_timestamp_opt(t as i64, 0).unwrap(),
|
||||
&Amount::from_sat(output.value),
|
||||
Message::SelectSub(i, output_index),
|
||||
))
|
||||
} else {
|
||||
col.push(event::unconfirmed_outgoing_event(
|
||||
label,
|
||||
&Amount::from_sat(output.value),
|
||||
Message::SelectSub(i, output_index),
|
||||
))
|
||||
@ -186,8 +209,15 @@ pub fn payment_view<'a>(
|
||||
cache: &'a Cache,
|
||||
tx: &'a HistoryTransaction,
|
||||
output_index: usize,
|
||||
labels_editing: &'a HashMap<String, form::Value<String>>,
|
||||
warning: Option<&'a Error>,
|
||||
) -> 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(
|
||||
&Menu::Home,
|
||||
cache,
|
||||
@ -200,11 +230,22 @@ pub fn payment_view<'a>(
|
||||
} else {
|
||||
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(
|
||||
&Amount::from_sat(tx.tx.output[output_index].value),
|
||||
H1_SIZE,
|
||||
)))
|
||||
.push(Space::with_height(H3_SIZE))
|
||||
.push(Container::new(h3("Transaction")).width(Length::Fill))
|
||||
.push(if let Some(label) = labels_editing.get(&txid) {
|
||||
label::label_editing(txid.clone(), label, H3_SIZE)
|
||||
} else {
|
||||
label::label_editable(txid.clone(), tx.labels.get(&txid), H3_SIZE)
|
||||
})
|
||||
.push_maybe(tx.fee_amount.map(|fee_amount| {
|
||||
Row::new()
|
||||
.align_items(Alignment::Center)
|
||||
@ -259,11 +300,8 @@ pub fn payment_view<'a>(
|
||||
} else {
|
||||
Some(tx.change_indexes.clone())
|
||||
},
|
||||
if tx.is_external() {
|
||||
Some(tx.change_indexes.clone())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
&tx.labels,
|
||||
labels_editing,
|
||||
))
|
||||
.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,
|
||||
Select(usize),
|
||||
SelectSub(usize, usize),
|
||||
Label(String, LabelMessage),
|
||||
Settings(SettingsMessage),
|
||||
CreateSpend(CreateSpendMessage),
|
||||
ImportSpend(ImportSpendMessage),
|
||||
@ -18,9 +19,17 @@ pub enum Message {
|
||||
SelectHardwareWallet(usize),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum LabelMessage {
|
||||
Edited(String),
|
||||
Cancel,
|
||||
Confirm,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum CreateSpendMessage {
|
||||
AddRecipient,
|
||||
BatchLabelEdited(String),
|
||||
DeleteRecipient(usize),
|
||||
SelectCoin(usize),
|
||||
RecipientEdited(usize, &'static str, String),
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
mod label;
|
||||
mod message;
|
||||
mod warning;
|
||||
|
||||
|
||||
@ -9,14 +9,19 @@ use liana::{
|
||||
descriptors::{LianaPolicy, PathInfo, PathSpendInfo},
|
||||
miniscript::bitcoin::{
|
||||
bip32::{DerivationPath, Fingerprint},
|
||||
Address, Amount, Network, Transaction,
|
||||
blockdata::transaction::TxOut,
|
||||
Address, Amount, Network, OutPoint, Transaction, Txid,
|
||||
},
|
||||
};
|
||||
|
||||
use liana_ui::{
|
||||
color,
|
||||
component::{
|
||||
amount::*, badge, button, card, collapse::Collapse, form, hw, separation, text::*,
|
||||
amount::*,
|
||||
badge, button, card,
|
||||
collapse::Collapse,
|
||||
form, hw, separation,
|
||||
text::{self, *},
|
||||
},
|
||||
icon, theme,
|
||||
util::Collection,
|
||||
@ -28,24 +33,27 @@ use crate::{
|
||||
cache::Cache,
|
||||
error::Error,
|
||||
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},
|
||||
hw::HardwareWallet,
|
||||
};
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn psbt_view<'a>(
|
||||
cache: &'a Cache,
|
||||
tx: &'a SpendTx,
|
||||
saved: bool,
|
||||
desc_info: &'a LianaPolicy,
|
||||
key_aliases: &'a HashMap<Fingerprint, String>,
|
||||
labels_editing: &'a HashMap<String, form::Value<String>>,
|
||||
network: Network,
|
||||
warning: Option<&Error>,
|
||||
) -> Element<'a, Message> {
|
||||
dashboard(
|
||||
&Menu::PSBTs,
|
||||
cache,
|
||||
None,
|
||||
warning,
|
||||
Column::new()
|
||||
.spacing(20)
|
||||
.push(
|
||||
@ -65,14 +73,15 @@ pub fn psbt_view<'a>(
|
||||
_ => None,
|
||||
}),
|
||||
)
|
||||
.push(spend_header(tx))
|
||||
.push(spend_header(tx, labels_editing))
|
||||
.push(spend_overview_view(tx, desc_info, key_aliases))
|
||||
.push(inputs_and_outputs_view(
|
||||
&tx.coins,
|
||||
&tx.psbt.unsigned_tx,
|
||||
network,
|
||||
Some(tx.change_indexes.clone()),
|
||||
None,
|
||||
&tx.labels,
|
||||
labels_editing,
|
||||
))
|
||||
.push(if saved {
|
||||
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()
|
||||
.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(
|
||||
Column::new()
|
||||
.push(if tx.is_self_send() {
|
||||
@ -504,8 +522,10 @@ pub fn inputs_and_outputs_view<'a>(
|
||||
tx: &'a Transaction,
|
||||
network: Network,
|
||||
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> {
|
||||
let change_indexes_copy = change_indexes.clone();
|
||||
Column::new()
|
||||
.spacing(20)
|
||||
.push_maybe(if !coins.is_empty() {
|
||||
@ -551,29 +571,9 @@ pub fn inputs_and_outputs_view<'a>(
|
||||
coins
|
||||
.iter()
|
||||
.fold(
|
||||
Column::new().padding(20),
|
||||
Column::new().spacing(10).padding(20),
|
||||
|col: Column<'a, Message>, coin| {
|
||||
col.push(
|
||||
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)),
|
||||
)
|
||||
col.push(input_view(coin, labels, labels_editing))
|
||||
},
|
||||
)
|
||||
.into()
|
||||
@ -584,107 +584,373 @@ pub fn inputs_and_outputs_view<'a>(
|
||||
} else {
|
||||
None
|
||||
})
|
||||
.push(
|
||||
Container::new(Collapse::new(
|
||||
move || {
|
||||
Button::new(
|
||||
Row::new()
|
||||
.align_items(Alignment::Center)
|
||||
.push(
|
||||
h4_bold(format!(
|
||||
"{} recipient{}",
|
||||
tx.output.len(),
|
||||
if tx.output.len() == 1 { "" } else { "s" }
|
||||
))
|
||||
.width(Length::Fill),
|
||||
)
|
||||
.push(icon::collapse_icon()),
|
||||
)
|
||||
.padding(20)
|
||||
.width(Length::Fill)
|
||||
.style(theme::Button::TransparentBorder)
|
||||
},
|
||||
move || {
|
||||
Button::new(
|
||||
Row::new()
|
||||
.align_items(Alignment::Center)
|
||||
.push(
|
||||
h4_bold(format!(
|
||||
"{} recipient{}",
|
||||
tx.output.len(),
|
||||
if tx.output.len() == 1 { "" } else { "s" }
|
||||
))
|
||||
.width(Length::Fill),
|
||||
)
|
||||
.push(icon::collapsed_icon()),
|
||||
)
|
||||
.padding(20)
|
||||
.width(Length::Fill)
|
||||
.style(theme::Button::TransparentBorder)
|
||||
},
|
||||
move || {
|
||||
tx.output
|
||||
.iter()
|
||||
.enumerate()
|
||||
.fold(
|
||||
Column::new().padding(20),
|
||||
|col: Column<'a, Message>, (i, output)| {
|
||||
let addr =
|
||||
Address::from_script(&output.script_pubkey, network).unwrap();
|
||||
col.push(
|
||||
Column::new()
|
||||
.width(Length::Fill)
|
||||
.spacing(5)
|
||||
.push(
|
||||
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))),
|
||||
)
|
||||
.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),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
})
|
||||
.push_maybe(if let Some(indexes) = receive_indexes.as_ref() {
|
||||
if indexes.contains(&i) {
|
||||
Some(Container::new(text("Deposit")).padding(5).style(
|
||||
theme::Container::Pill(theme::Pill::Success),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}),
|
||||
)
|
||||
},
|
||||
.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(
|
||||
move || {
|
||||
Button::new(
|
||||
Row::new()
|
||||
.align_items(Alignment::Center)
|
||||
.push(
|
||||
h4_bold(format!(
|
||||
"{} payment{}",
|
||||
count,
|
||||
if count == 1 { "" } else { "s" }
|
||||
))
|
||||
.width(Length::Fill),
|
||||
)
|
||||
.push(icon::collapse_icon()),
|
||||
)
|
||||
.into()
|
||||
},
|
||||
))
|
||||
.style(theme::Container::Card(theme::Card::Simple)),
|
||||
.padding(20)
|
||||
.width(Length::Fill)
|
||||
.style(theme::Button::TransparentBorder)
|
||||
},
|
||||
move || {
|
||||
Button::new(
|
||||
Row::new()
|
||||
.align_items(Alignment::Center)
|
||||
.push(
|
||||
h4_bold(format!(
|
||||
"{} payment{}",
|
||||
count,
|
||||
if count == 1 { "" } else { "s" }
|
||||
))
|
||||
.width(Length::Fill),
|
||||
)
|
||||
.push(icon::collapsed_icon()),
|
||||
)
|
||||
.padding(20)
|
||||
.width(Length::Fill)
|
||||
.style(theme::Button::TransparentBorder)
|
||||
},
|
||||
move || {
|
||||
tx.output
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(i, _)| {
|
||||
if let Some(indexes) = change_indexes_copy.as_ref() {
|
||||
!indexes.contains(i)
|
||||
} else {
|
||||
true
|
||||
}
|
||||
})
|
||||
.fold(
|
||||
Column::new().padding(20),
|
||||
|col: Column<'a, Message>, (i, output)| {
|
||||
col.spacing(10).push(payment_view(
|
||||
i,
|
||||
tx.txid(),
|
||||
output,
|
||||
network,
|
||||
labels,
|
||||
labels_editing,
|
||||
))
|
||||
},
|
||||
)
|
||||
.into()
|
||||
},
|
||||
))
|
||||
.style(theme::Container::Card(theme::Card::Simple))
|
||||
} else {
|
||||
Container::new(h4_bold("0 payment"))
|
||||
.padding(20)
|
||||
.width(Length::Fill)
|
||||
.style(theme::Container::Card(theme::Card::Simple))
|
||||
}
|
||||
})
|
||||
.push_maybe(
|
||||
if change_indexes
|
||||
.as_ref()
|
||||
.map(|indexes| !indexes.is_empty())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
Some(
|
||||
Container::new(Collapse::new(
|
||||
move || {
|
||||
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()
|
||||
},
|
||||
))
|
||||
.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()
|
||||
}
|
||||
|
||||
@ -59,7 +59,7 @@ pub fn import_psbt_success_view<'a>() -> Element<'a, Message> {
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn psbts_view<'a>(spend_txs: &[SpendTx]) -> Element<'a, Message> {
|
||||
pub fn psbts_view(spend_txs: &[SpendTx]) -> Element<'_, Message> {
|
||||
Column::new()
|
||||
.push(
|
||||
Row::new()
|
||||
@ -90,7 +90,7 @@ pub fn psbts_view<'a>(spend_txs: &[SpendTx]) -> Element<'a, Message> {
|
||||
.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(
|
||||
Button::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_maybe(
|
||||
tx.labels
|
||||
.get(&tx.psbt.unsigned_tx.txid().to_string())
|
||||
.map(p1_bold),
|
||||
)
|
||||
.spacing(10)
|
||||
.align_items(Alignment::Center)
|
||||
.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()),
|
||||
_ => None,
|
||||
})
|
||||
.push_maybe(if tx.is_batch() {
|
||||
Some(badge::batch())
|
||||
} else {
|
||||
None
|
||||
})
|
||||
.push(
|
||||
Column::new()
|
||||
.align_items(Alignment::End)
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use iced::{
|
||||
widget::{
|
||||
qr_code::{self, QRCode},
|
||||
@ -10,16 +12,23 @@ use liana::miniscript::bitcoin;
|
||||
|
||||
use liana_ui::{
|
||||
color,
|
||||
component::{button, card, text::*},
|
||||
component::{
|
||||
button, card, form,
|
||||
text::{self, *},
|
||||
},
|
||||
icon, theme,
|
||||
widget::*,
|
||||
};
|
||||
|
||||
use crate::app::view::label;
|
||||
|
||||
use super::message::Message;
|
||||
|
||||
pub fn receive<'a>(
|
||||
addresses: &'a [bitcoin::Address],
|
||||
qr: Option<&'a qr_code::State>,
|
||||
labels: &'a HashMap<String, String>,
|
||||
labels_editing: &'a HashMap<String, form::Value<String>>,
|
||||
) -> Element<'a, Message> {
|
||||
Column::new()
|
||||
.push(
|
||||
@ -38,34 +47,54 @@ pub fn receive<'a>(
|
||||
.push(addresses.iter().rev().fold(
|
||||
Column::new().spacing(10).width(Length::Fill),
|
||||
|col, address| {
|
||||
let addr = address.to_string();
|
||||
col.push(
|
||||
card::simple(
|
||||
Row::new()
|
||||
.push(
|
||||
Container::new(
|
||||
scrollable(
|
||||
Column::new()
|
||||
.push(Space::with_height(Length::Fixed(10.0)))
|
||||
.push(
|
||||
p2_regular(address.to_string())
|
||||
.small()
|
||||
.style(color::GREY_3),
|
||||
)
|
||||
// Space between the address and the scrollbar
|
||||
.push(Space::with_height(Length::Fixed(10.0))),
|
||||
)
|
||||
.horizontal_scroll(
|
||||
scrollable::Properties::new().scroller_width(5),
|
||||
),
|
||||
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,
|
||||
)
|
||||
.width(Length::Fill),
|
||||
)
|
||||
})
|
||||
.push(
|
||||
Button::new(icon::clipboard_icon().style(color::GREY_3))
|
||||
.on_press(Message::Clipboard(address.to_string()))
|
||||
.style(theme::Button::TransparentBorder),
|
||||
)
|
||||
.align_items(Alignment::Center),
|
||||
Row::new()
|
||||
.push(
|
||||
Container::new(
|
||||
scrollable(
|
||||
Column::new()
|
||||
.push(Space::with_height(
|
||||
Length::Fixed(10.0),
|
||||
))
|
||||
.push(
|
||||
p2_regular(addr)
|
||||
.small()
|
||||
.style(color::GREY_3),
|
||||
)
|
||||
// Space between the address and the scrollbar
|
||||
.push(Space::with_height(
|
||||
Length::Fixed(10.0),
|
||||
)),
|
||||
)
|
||||
.horizontal_scroll(
|
||||
scrollable::Properties::new()
|
||||
.scroller_width(5),
|
||||
),
|
||||
)
|
||||
.width(Length::Fill),
|
||||
)
|
||||
.push(
|
||||
Button::new(
|
||||
icon::clipboard_icon().style(color::GREY_3),
|
||||
)
|
||||
.on_press(Message::Clipboard(address.to_string()))
|
||||
.style(theme::Button::TransparentBorder),
|
||||
)
|
||||
.align_items(Alignment::Center),
|
||||
),
|
||||
)
|
||||
.padding(20),
|
||||
)
|
||||
|
||||
@ -29,29 +29,33 @@ use crate::{
|
||||
daemon::model::{remaining_sequence, Coin, SpendTx},
|
||||
};
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn spend_view<'a>(
|
||||
cache: &'a Cache,
|
||||
tx: &'a SpendTx,
|
||||
saved: bool,
|
||||
desc_info: &'a LianaPolicy,
|
||||
key_aliases: &'a HashMap<Fingerprint, String>,
|
||||
labels_editing: &'a HashMap<String, form::Value<String>>,
|
||||
network: Network,
|
||||
warning: Option<&Error>,
|
||||
) -> Element<'a, Message> {
|
||||
dashboard(
|
||||
&Menu::CreateSpendTx,
|
||||
cache,
|
||||
None,
|
||||
warning,
|
||||
Column::new()
|
||||
.spacing(20)
|
||||
.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::inputs_and_outputs_view(
|
||||
&tx.coins,
|
||||
&tx.psbt.unsigned_tx,
|
||||
network,
|
||||
Some(tx.change_indexes.clone()),
|
||||
None,
|
||||
&tx.labels,
|
||||
labels_editing,
|
||||
))
|
||||
.push(if saved {
|
||||
Row::new()
|
||||
@ -89,6 +93,8 @@ pub fn create_spend_tx<'a>(
|
||||
duplicate: bool,
|
||||
timelock: u16,
|
||||
coins: &[(Coin, bool)],
|
||||
coins_labels: &'a HashMap<String, String>,
|
||||
batch_label: &form::Value<String>,
|
||||
amount_left: Option<&Amount>,
|
||||
feerate: &form::Value<String>,
|
||||
error: Option<&Error>,
|
||||
@ -104,6 +110,18 @@ pub fn create_spend_tx<'a>(
|
||||
} else {
|
||||
"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(
|
||||
Column::new()
|
||||
.push(Column::with_children(recipients).spacing(10))
|
||||
@ -112,7 +130,7 @@ pub fn create_spend_tx<'a>(
|
||||
.push_maybe(if duplicate {
|
||||
Some(
|
||||
Container::new(
|
||||
text("Two recipient addresses are the same")
|
||||
text("Two payment addresses are the same")
|
||||
.style(color::RED),
|
||||
)
|
||||
.padding(10),
|
||||
@ -125,7 +143,7 @@ pub fn create_spend_tx<'a>(
|
||||
None
|
||||
} else {
|
||||
Some(
|
||||
button::secondary(Some(icon::plus_icon()), "Add recipient")
|
||||
button::secondary(Some(icon::plus_icon()), "Add payment")
|
||||
.on_press(Message::CreateSpend(
|
||||
CreateSpendMessage::AddRecipient,
|
||||
)),
|
||||
@ -201,6 +219,7 @@ pub fn create_spend_tx<'a>(
|
||||
col.push(coin_list_view(
|
||||
i,
|
||||
coin,
|
||||
coins_labels,
|
||||
timelock,
|
||||
cache.blockheight as u32,
|
||||
*selected,
|
||||
@ -246,6 +265,7 @@ pub fn recipient_view<'a>(
|
||||
index: usize,
|
||||
address: &form::Value<String>,
|
||||
amount: &form::Value<String>,
|
||||
label: &form::Value<String>,
|
||||
) -> Element<'a, CreateSpendMessage> {
|
||||
Container::new(
|
||||
Column::new()
|
||||
@ -263,10 +283,10 @@ pub fn recipient_view<'a>(
|
||||
.align_items(Alignment::Start)
|
||||
.spacing(10)
|
||||
.push(
|
||||
Container::new(p1_bold("Pay to"))
|
||||
Container::new(p1_bold("Address"))
|
||||
.align_x(alignment::Horizontal::Right)
|
||||
.padding(10)
|
||||
.width(Length::Fixed(80.0)),
|
||||
.width(Length::Fixed(110.0)),
|
||||
)
|
||||
.push(
|
||||
form::Form::new_trimmed("Address", address, move |msg| {
|
||||
@ -277,6 +297,25 @@ pub fn recipient_view<'a>(
|
||||
.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(
|
||||
Row::new()
|
||||
.align_items(Alignment::Start)
|
||||
@ -285,7 +324,7 @@ pub fn recipient_view<'a>(
|
||||
Container::new(p1_bold("Amount"))
|
||||
.padding(10)
|
||||
.align_x(alignment::Horizontal::Right)
|
||||
.width(Length::Fixed(80.0)),
|
||||
.width(Length::Fixed(110.0)),
|
||||
)
|
||||
.push(
|
||||
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>(
|
||||
i: usize,
|
||||
coin: &Coin,
|
||||
coins_labels: &'a HashMap<String, String>,
|
||||
timelock: u16,
|
||||
blockheight: u32,
|
||||
selected: bool,
|
||||
@ -318,6 +358,13 @@ fn coin_list_view<'a>(
|
||||
.push(checkbox("", selected, move |_| {
|
||||
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() {
|
||||
badge::spent()
|
||||
} else if coin.block_height.is_none() {
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
use chrono::NaiveDateTime;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use iced::{alignment, Alignment, Length};
|
||||
|
||||
use liana_ui::{
|
||||
color,
|
||||
component::{amount::*, badge, card, text::*},
|
||||
component::{amount::*, badge, card, form, text::*},
|
||||
icon, theme,
|
||||
util::Collection,
|
||||
widget::*,
|
||||
@ -15,7 +16,7 @@ use crate::{
|
||||
cache::Cache,
|
||||
error::Error,
|
||||
menu::Menu,
|
||||
view::{dashboard, message::Message},
|
||||
view::{dashboard, label, message::Message},
|
||||
},
|
||||
daemon::model::HistoryTransaction,
|
||||
};
|
||||
@ -24,8 +25,8 @@ pub const HISTORY_EVENT_PAGE_SIZE: u64 = 20;
|
||||
|
||||
pub fn transactions_view<'a>(
|
||||
cache: &'a Cache,
|
||||
pending_txs: &[HistoryTransaction],
|
||||
txs: &Vec<HistoryTransaction>,
|
||||
pending_txs: &'a [HistoryTransaction],
|
||||
txs: &'a Vec<HistoryTransaction>,
|
||||
warning: Option<&'a Error>,
|
||||
) -> Element<'a, Message> {
|
||||
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(
|
||||
Button::new(
|
||||
Row::new()
|
||||
@ -96,23 +97,36 @@ fn tx_list_view<'a>(i: usize, tx: &HistoryTransaction) -> Element<'a, Message> {
|
||||
} else {
|
||||
badge::spend()
|
||||
})
|
||||
.push(if let Some(t) = tx.time {
|
||||
Container::new(
|
||||
text(format!(
|
||||
"{}",
|
||||
NaiveDateTime::from_timestamp_opt(t as i64, 0)
|
||||
.unwrap()
|
||||
.format("%b. %d, %Y - %T"),
|
||||
))
|
||||
.small(),
|
||||
)
|
||||
} else {
|
||||
badge::unconfirmed()
|
||||
})
|
||||
.push(
|
||||
Column::new()
|
||||
.push_maybe(tx.labels.get(&tx.tx.txid().to_string()).map(p1_bold))
|
||||
.push_maybe(tx.time.map(|t| {
|
||||
Container::new(
|
||||
text(format!(
|
||||
"{}",
|
||||
NaiveDateTime::from_timestamp_opt(t as i64, 0)
|
||||
.unwrap()
|
||||
.format("%b. %d, %Y - %T"),
|
||||
))
|
||||
.style(color::GREY_3)
|
||||
.small(),
|
||||
)
|
||||
})),
|
||||
)
|
||||
.spacing(10)
|
||||
.align_items(Alignment::Center)
|
||||
.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() {
|
||||
Row::new()
|
||||
.spacing(5)
|
||||
@ -142,8 +156,10 @@ fn tx_list_view<'a>(i: usize, tx: &HistoryTransaction) -> Element<'a, Message> {
|
||||
pub fn tx_view<'a>(
|
||||
cache: &'a Cache,
|
||||
tx: &'a HistoryTransaction,
|
||||
labels_editing: &'a HashMap<String, form::Value<String>>,
|
||||
warning: Option<&'a Error>,
|
||||
) -> Element<'a, Message> {
|
||||
let txid = tx.tx.txid().to_string();
|
||||
dashboard(
|
||||
&Menu::Transactions,
|
||||
cache,
|
||||
@ -156,6 +172,11 @@ pub fn tx_view<'a>(
|
||||
} else {
|
||||
Container::new(h3("Outgoing transaction")).width(Length::Fill)
|
||||
})
|
||||
.push(if let Some(label) = labels_editing.get(&txid) {
|
||||
label::label_editing(txid.clone(), label, H3_SIZE)
|
||||
} else {
|
||||
label::label_editable(txid.clone(), tx.labels.get(&txid), H1_SIZE)
|
||||
})
|
||||
.push(
|
||||
Column::new().spacing(20).push(
|
||||
Column::new()
|
||||
@ -202,10 +223,10 @@ pub fn tx_view<'a>(
|
||||
.push(
|
||||
Row::new()
|
||||
.align_items(Alignment::Center)
|
||||
.push(Container::new(text(format!("{}", tx.tx.txid())).small()))
|
||||
.push(Container::new(text(txid.clone()).small()))
|
||||
.push(
|
||||
Button::new(icon::clipboard_icon())
|
||||
.on_press(Message::Clipboard(tx.tx.txid().to_string()))
|
||||
.on_press(Message::Clipboard(txid.clone()))
|
||||
.style(theme::Button::TransparentBorder),
|
||||
)
|
||||
.width(Length::Shrink),
|
||||
@ -222,11 +243,8 @@ pub fn tx_view<'a>(
|
||||
} else {
|
||||
Some(tx.change_indexes.clone())
|
||||
},
|
||||
if tx.is_external() {
|
||||
Some(tx.change_indexes.clone())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
&tx.labels,
|
||||
labels_editing,
|
||||
))
|
||||
.spacing(20),
|
||||
)
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::fmt::Debug;
|
||||
use std::iter::FromIterator;
|
||||
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@ -10,6 +11,7 @@ pub mod error;
|
||||
pub mod jsonrpc;
|
||||
|
||||
use liana::{
|
||||
commands::LabelItem,
|
||||
config::Config,
|
||||
miniscript::bitcoin::{address, psbt::Psbt, Address, OutPoint, Txid},
|
||||
};
|
||||
@ -145,6 +147,22 @@ impl<C: Client + Debug> Daemon for Lianad<C> {
|
||||
)?;
|
||||
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)]
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use super::{model::*, Daemon, DaemonError};
|
||||
use liana::{
|
||||
commands::LabelItem,
|
||||
config::Config,
|
||||
miniscript::bitcoin::{address, psbt::Psbt, Address, OutPoint, Txid},
|
||||
DaemonControl, DaemonHandle,
|
||||
@ -59,7 +60,7 @@ impl Daemon for EmbeddedDaemon {
|
||||
}
|
||||
|
||||
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> {
|
||||
@ -126,4 +127,16 @@ impl Daemon for EmbeddedDaemon {
|
||||
.map_err(|e| DaemonError::Unexpected(e.to_string()))
|
||||
.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 model;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::fmt::Debug;
|
||||
use std::io::ErrorKind;
|
||||
|
||||
use liana::{
|
||||
commands::LabelItem,
|
||||
config::Config,
|
||||
miniscript::bitcoin::{address, psbt::Psbt, Address, OutPoint, Txid},
|
||||
StartupError,
|
||||
@ -75,6 +76,11 @@ pub trait Daemon: Debug {
|
||||
sequence: Option<u16>,
|
||||
) -> Result<Psbt, 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> {
|
||||
let info = self.get_info()?;
|
||||
@ -103,8 +109,10 @@ pub trait Daemon: Debug {
|
||||
coins,
|
||||
sigs,
|
||||
info.descriptors.main.max_sat_vbytes(),
|
||||
info.network,
|
||||
))
|
||||
}
|
||||
load_labels(self, &mut spend_txs)?;
|
||||
spend_txs.sort_by(|a, b| {
|
||||
if a.status == b.status {
|
||||
// last updated first
|
||||
@ -123,9 +131,10 @@ pub trait Daemon: Debug {
|
||||
end: u32,
|
||||
limit: u64,
|
||||
) -> Result<Vec<model::HistoryTransaction>, DaemonError> {
|
||||
let info = self.get_info()?;
|
||||
let coins = self.list_coins()?.coins;
|
||||
let txs = self.list_confirmed_txs(start, end, limit)?.transactions;
|
||||
Ok(txs
|
||||
let mut txs = txs
|
||||
.into_iter()
|
||||
.map(|tx| {
|
||||
let mut tx_coins = Vec::new();
|
||||
@ -142,12 +151,22 @@ pub trait Daemon: Debug {
|
||||
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> {
|
||||
let info = self.get_info()?;
|
||||
let coins = self.list_coins()?.coins;
|
||||
let mut txids: Vec<Txid> = Vec::new();
|
||||
for coin in &coins {
|
||||
@ -163,7 +182,7 @@ pub trait Daemon: Debug {
|
||||
}
|
||||
|
||||
let txs = self.list_txs(&txids)?.transactions;
|
||||
Ok(txs
|
||||
let mut txs = txs
|
||||
.into_iter()
|
||||
.map(|tx| {
|
||||
let mut tx_coins = Vec::new();
|
||||
@ -180,8 +199,38 @@ pub trait Daemon: Debug {
|
||||
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::{
|
||||
commands::{
|
||||
CreateSpendResult, GetAddressResult, GetInfoResult, ListCoinsEntry, ListCoinsResult,
|
||||
ListSpendEntry, ListSpendResult, ListTransactionsResult, TransactionInfo,
|
||||
CreateSpendResult, GetAddressResult, GetInfoResult, GetLabelsResult, LabelItem,
|
||||
ListCoinsEntry, ListCoinsResult, ListSpendEntry, ListSpendResult, ListTransactionsResult,
|
||||
TransactionInfo,
|
||||
},
|
||||
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;
|
||||
@ -25,7 +28,9 @@ pub fn remaining_sequence(coin: &Coin, blockheight: u32, timelock: u16) -> u32 {
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SpendTx {
|
||||
pub network: Network,
|
||||
pub coins: Vec<Coin>,
|
||||
pub labels: HashMap<String, String>,
|
||||
pub psbt: Psbt,
|
||||
pub change_indexes: Vec<usize>,
|
||||
pub spend_amount: Amount,
|
||||
@ -53,6 +58,7 @@ impl SpendTx {
|
||||
coins: Vec<Coin>,
|
||||
sigs: PartialSpendInfo,
|
||||
max_sat_vbytes: usize,
|
||||
network: Network,
|
||||
) -> Self {
|
||||
let mut change_indexes = Vec::new();
|
||||
let (change_amount, spend_amount) = psbt.unsigned_tx.output.iter().enumerate().fold(
|
||||
@ -85,6 +91,7 @@ impl SpendTx {
|
||||
}
|
||||
|
||||
Self {
|
||||
labels: HashMap::new(),
|
||||
updated_at,
|
||||
coins,
|
||||
psbt,
|
||||
@ -94,6 +101,7 @@ impl SpendTx {
|
||||
max_sat_vbytes,
|
||||
status,
|
||||
sigs,
|
||||
network,
|
||||
}
|
||||
}
|
||||
|
||||
@ -135,10 +143,48 @@ impl SpendTx {
|
||||
self.psbt.unsigned_tx.vsize() + (self.max_sat_vbytes * self.psbt.inputs.len());
|
||||
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)]
|
||||
pub struct HistoryTransaction {
|
||||
pub network: Network,
|
||||
pub labels: HashMap<String, String>,
|
||||
pub coins: Vec<Coin>,
|
||||
pub change_indexes: Vec<usize>,
|
||||
pub tx: Transaction,
|
||||
@ -156,6 +202,7 @@ impl HistoryTransaction {
|
||||
time: Option<u32>,
|
||||
coins: Vec<Coin>,
|
||||
change_indexes: Vec<usize>,
|
||||
network: Network,
|
||||
) -> Self {
|
||||
let (incoming_amount, outgoing_amount) = tx.output.iter().enumerate().fold(
|
||||
(Amount::from_sat(0), Amount::from_sat(0)),
|
||||
@ -180,6 +227,7 @@ impl HistoryTransaction {
|
||||
};
|
||||
|
||||
Self {
|
||||
labels: HashMap::new(),
|
||||
tx,
|
||||
coins,
|
||||
change_indexes,
|
||||
@ -188,6 +236,7 @@ impl HistoryTransaction {
|
||||
fee_amount,
|
||||
height,
|
||||
time,
|
||||
network,
|
||||
}
|
||||
}
|
||||
|
||||
@ -198,4 +247,54 @@ impl HistoryTransaction {
|
||||
pub fn is_self_send(&self) -> bool {
|
||||
!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> {
|
||||
Container::new(
|
||||
tooltip::Tooltip::new(
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
use crate::{
|
||||
color,
|
||||
component::{amount, badge, text},
|
||||
theme,
|
||||
util::Collection,
|
||||
widget::*,
|
||||
};
|
||||
use bitcoin::Amount;
|
||||
@ -9,14 +11,19 @@ use iced::{
|
||||
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(
|
||||
button(
|
||||
row!(
|
||||
row!(badge::spend(), badge::unconfirmed())
|
||||
row!(badge::spend(), Column::new().push_maybe(label),)
|
||||
.spacing(10)
|
||||
.align_items(Alignment::Center)
|
||||
.width(Length::Fill),
|
||||
badge::unconfirmed(),
|
||||
row!(text::p1_regular("-"), amount::amount(amount))
|
||||
.spacing(5)
|
||||
.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>(
|
||||
label: Option<iced::widget::Text<'a, iced::Renderer<theme::Theme>>>,
|
||||
date: chrono::NaiveDateTime,
|
||||
amount: &Amount,
|
||||
msg: T,
|
||||
@ -41,7 +49,10 @@ pub fn confirmed_outgoing_event<'a, T: Clone + 'a>(
|
||||
row!(
|
||||
row!(
|
||||
badge::spend(),
|
||||
text::p2_regular(date.format("%b. %d, %Y - %T").to_string())
|
||||
Column::new().push_maybe(label).push(
|
||||
text::p2_regular(date.format("%b. %d, %Y - %T").to_string())
|
||||
.style(color::GREY_3)
|
||||
)
|
||||
)
|
||||
.spacing(10)
|
||||
.align_items(Alignment::Center)
|
||||
@ -60,14 +71,19 @@ pub fn confirmed_outgoing_event<'a, T: Clone + 'a>(
|
||||
.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(
|
||||
button(
|
||||
row!(
|
||||
row!(badge::receive(), badge::unconfirmed())
|
||||
row!(badge::receive(), Column::new().push_maybe(label))
|
||||
.spacing(10)
|
||||
.align_items(Alignment::Center)
|
||||
.width(Length::Fill),
|
||||
badge::unconfirmed(),
|
||||
row!(text::p1_regular("+"), amount::amount(amount))
|
||||
.spacing(5)
|
||||
.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>(
|
||||
label: Option<iced::widget::Text<'a, iced::Renderer<theme::Theme>>>,
|
||||
date: chrono::NaiveDateTime,
|
||||
amount: &Amount,
|
||||
msg: T,
|
||||
@ -92,7 +109,10 @@ pub fn confirmed_incoming_event<'a, T: Clone + 'a>(
|
||||
row!(
|
||||
row!(
|
||||
badge::receive(),
|
||||
text::p2_regular(date.format("%b. %d, %Y - %T").to_string())
|
||||
Column::new().push_maybe(label).push(
|
||||
text::p2_regular(date.format("%b. %d, %Y - %T").to_string())
|
||||
.style(color::GREY_3)
|
||||
)
|
||||
)
|
||||
.spacing(10)
|
||||
.align_items(Alignment::Center)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user