Add transaction history to home panel
This commit is contained in:
parent
dc23f3667a
commit
7e96cdd494
2
gui/Cargo.lock
generated
2
gui/Cargo.lock
generated
@ -1412,7 +1412,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "liana"
|
||||
version = "0.0.1"
|
||||
source = "git+https://github.com/revault/liana?branch=master#72a7bbea4c2bb931ff78286cff8be4fbe9c44b48"
|
||||
source = "git+https://github.com/revault/liana?branch=master#dc23f3667a977dae93cfeb13ee9694e41d020251"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"base64",
|
||||
|
||||
@ -28,4 +28,6 @@ pub enum Message {
|
||||
Updated(Result<(), Error>),
|
||||
StartRescan(Result<(), Error>),
|
||||
ConnectedHardwareWallets(Vec<HardwareWallet>),
|
||||
HistoryTransactions(Result<Vec<HistoryTransaction>, Error>),
|
||||
PendingTransactions(Result<Vec<HistoryTransaction>, Error>),
|
||||
}
|
||||
|
||||
@ -2,15 +2,20 @@ mod coins;
|
||||
mod settings;
|
||||
mod spend;
|
||||
|
||||
use std::convert::TryInto;
|
||||
use std::sync::Arc;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use iced::pure::{column, Element};
|
||||
use iced::{widget::qr_code, Command, Subscription};
|
||||
use liana::miniscript::bitcoin::{Address, Amount};
|
||||
|
||||
use super::{cache::Cache, error::Error, menu::Menu, message::Message, view};
|
||||
use crate::daemon::{model::Coin, Daemon};
|
||||
|
||||
use crate::daemon::{
|
||||
model::{Coin, HistoryTransaction},
|
||||
Daemon,
|
||||
};
|
||||
pub use coins::CoinsPanel;
|
||||
pub use settings::SettingsState;
|
||||
pub use spend::{CreateSpendPanel, SpendPanel};
|
||||
@ -33,6 +38,10 @@ pub trait State {
|
||||
|
||||
pub struct Home {
|
||||
balance: Amount,
|
||||
pending_events: Vec<HistoryTransaction>,
|
||||
events: Vec<HistoryTransaction>,
|
||||
selected_event: Option<usize>,
|
||||
warning: Option<Error>,
|
||||
}
|
||||
|
||||
impl Home {
|
||||
@ -42,7 +51,8 @@ impl Home {
|
||||
coins
|
||||
.iter()
|
||||
.map(|coin| {
|
||||
if coin.spend_info.is_none() {
|
||||
// If the coin is not spent and is its transaction is confirmed
|
||||
if coin.spend_info.is_none() && coin.block_height.is_some() {
|
||||
coin.amount.to_sat()
|
||||
} else {
|
||||
0
|
||||
@ -50,40 +60,161 @@ impl Home {
|
||||
})
|
||||
.sum(),
|
||||
),
|
||||
selected_event: None,
|
||||
events: Vec::new(),
|
||||
pending_events: Vec::new(),
|
||||
warning: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl State for Home {
|
||||
fn view<'a>(&'a self, cache: &'a Cache) -> Element<'a, view::Message> {
|
||||
if let Some(i) = self.selected_event {
|
||||
return view::modal(
|
||||
false,
|
||||
self.warning.as_ref(),
|
||||
view::home::event_view(&self.events[i]),
|
||||
);
|
||||
}
|
||||
view::dashboard(
|
||||
&Menu::Home,
|
||||
cache,
|
||||
None,
|
||||
view::home::home_view(&self.balance),
|
||||
view::home::home_view(&self.balance, &self.pending_events, &self.events),
|
||||
)
|
||||
}
|
||||
|
||||
fn update(
|
||||
&mut self,
|
||||
_daemon: Arc<dyn Daemon + Sync + Send>,
|
||||
daemon: Arc<dyn Daemon + Sync + Send>,
|
||||
_cache: &Cache,
|
||||
_message: Message,
|
||||
message: Message,
|
||||
) -> Command<Message> {
|
||||
match message {
|
||||
Message::Coins(res) => match res {
|
||||
Err(e) => self.warning = Some(e),
|
||||
Ok(coins) => {
|
||||
self.warning = None;
|
||||
self.balance = Amount::from_sat(
|
||||
coins
|
||||
.iter()
|
||||
.map(|coin| {
|
||||
// If the coin is not spent and is its transaction is confirmed
|
||||
if coin.spend_info.is_none() && coin.block_height.is_some() {
|
||||
coin.amount.to_sat()
|
||||
} else {
|
||||
0
|
||||
}
|
||||
})
|
||||
.sum(),
|
||||
);
|
||||
}
|
||||
},
|
||||
Message::HistoryTransactions(res) => match res {
|
||||
Err(e) => self.warning = Some(e),
|
||||
Ok(events) => {
|
||||
self.warning = None;
|
||||
for event in events {
|
||||
if !self.events.iter().any(|other| other.tx == event.tx) {
|
||||
self.events.push(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Message::PendingTransactions(res) => match res {
|
||||
Err(e) => self.warning = Some(e),
|
||||
Ok(events) => {
|
||||
self.warning = None;
|
||||
for event in events {
|
||||
if !self.pending_events.iter().any(|other| other.tx == event.tx) {
|
||||
self.pending_events.push(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Message::View(view::Message::Close) => {
|
||||
self.selected_event = None;
|
||||
}
|
||||
Message::View(view::Message::Select(i)) => {
|
||||
self.selected_event = Some(i);
|
||||
}
|
||||
Message::View(view::Message::Next) => {
|
||||
if let Some(last) = self.events.last() {
|
||||
let daemon = daemon.clone();
|
||||
let last_event_date = last.time.unwrap() as u32;
|
||||
return Command::perform(
|
||||
async move {
|
||||
let mut limit = view::home::HISTORY_EVENT_PAGE_SIZE;
|
||||
let mut events =
|
||||
daemon.list_history_txs(0_u32, last_event_date, limit)?;
|
||||
|
||||
// because gethistory cursor is inclusive and use blocktime
|
||||
// multiple events can occur in the same block.
|
||||
// If there is more event in the same block that the
|
||||
// HISTORY_EVENT_PAGE_SIZE they can not be retrieved by changing
|
||||
// the cursor value (blocktime) but by increasing the limit.
|
||||
//
|
||||
// 1. Check if the events retrieved have all the same blocktime
|
||||
let blocktime = if let Some(event) = events.first() {
|
||||
event.time
|
||||
} else {
|
||||
return Ok(events);
|
||||
};
|
||||
|
||||
// 2. Retrieve a larger batch of event with the same cursor but
|
||||
// a larger limit.
|
||||
while !events.iter().any(|evt| evt.time != blocktime)
|
||||
&& events.len() as u64 == limit
|
||||
{
|
||||
// increments of the equivalent of one page more.
|
||||
limit += view::home::HISTORY_EVENT_PAGE_SIZE;
|
||||
events = daemon.list_history_txs(0, last_event_date, limit)?;
|
||||
}
|
||||
Ok(events)
|
||||
},
|
||||
Message::HistoryTransactions,
|
||||
);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
Command::none()
|
||||
}
|
||||
|
||||
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();
|
||||
let daemon3 = daemon.clone();
|
||||
let now: u32 = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs()
|
||||
.try_into()
|
||||
.unwrap();
|
||||
Command::batch(vec![
|
||||
Command::perform(
|
||||
async move { daemon3.list_pending_txs().map_err(|e| e.into()) },
|
||||
Message::PendingTransactions,
|
||||
),
|
||||
Command::perform(
|
||||
async move {
|
||||
daemon1
|
||||
.list_history_txs(0, now, view::home::HISTORY_EVENT_PAGE_SIZE)
|
||||
.map_err(|e| e.into())
|
||||
},
|
||||
Message::HistoryTransactions,
|
||||
),
|
||||
Command::perform(
|
||||
async move {
|
||||
daemon2
|
||||
.list_coins()
|
||||
.map(|res| res.coins)
|
||||
.map_err(|e| e.into())
|
||||
},
|
||||
Message::Coins,
|
||||
),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -89,9 +89,15 @@ fn coin_list_view(coin: &Coin, timelock: u32, blockheight: u32) -> Element<Messa
|
||||
.width(Length::Fill),
|
||||
)
|
||||
.push(
|
||||
text(&format!("{} BTC", coin.amount.to_btc()))
|
||||
.bold()
|
||||
.width(Length::Shrink),
|
||||
row()
|
||||
.spacing(5)
|
||||
.push(
|
||||
text(&format!("{:.8}", coin.amount.to_btc()))
|
||||
.bold()
|
||||
.width(Length::Shrink),
|
||||
)
|
||||
.push(text("BTC"))
|
||||
.align_items(Alignment::Center),
|
||||
)
|
||||
.align_items(Alignment::Center)
|
||||
.spacing(20)
|
||||
|
||||
@ -1,18 +1,178 @@
|
||||
use chrono::NaiveDateTime;
|
||||
|
||||
use iced::{
|
||||
pure::{column, Element},
|
||||
Alignment,
|
||||
alignment,
|
||||
pure::{button, column, container, row, Element},
|
||||
Alignment, Length,
|
||||
};
|
||||
|
||||
use crate::ui::{
|
||||
component::{badge, button::Style, card, text::*},
|
||||
util::Collection,
|
||||
};
|
||||
use liana::miniscript::bitcoin;
|
||||
|
||||
use crate::ui::component::text::*;
|
||||
use crate::{app::view::message::Message, daemon::model::HistoryTransaction};
|
||||
|
||||
use super::message::Message;
|
||||
pub const HISTORY_EVENT_PAGE_SIZE: u64 = 20;
|
||||
|
||||
pub fn home_view(balance: &bitcoin::Amount) -> Element<Message> {
|
||||
pub fn home_view<'a>(
|
||||
balance: &'a bitcoin::Amount,
|
||||
pending_events: &[HistoryTransaction],
|
||||
events: &Vec<HistoryTransaction>,
|
||||
) -> Element<'a, Message> {
|
||||
column()
|
||||
.push(column().padding(40))
|
||||
.push(text(&format!("{} BTC", balance.to_btc())).bold().size(50))
|
||||
.push(
|
||||
column()
|
||||
.spacing(10)
|
||||
.push(
|
||||
pending_events
|
||||
.iter()
|
||||
.enumerate()
|
||||
.fold(column().spacing(10), |col, (i, event)| {
|
||||
col.push(event_list_view(i, event))
|
||||
}),
|
||||
)
|
||||
.push(
|
||||
events
|
||||
.iter()
|
||||
.enumerate()
|
||||
.fold(column().spacing(10), |col, (i, event)| {
|
||||
col.push(event_list_view(i, event))
|
||||
}),
|
||||
)
|
||||
.push_maybe(
|
||||
if events.len() % HISTORY_EVENT_PAGE_SIZE as usize == 0 && !events.is_empty() {
|
||||
Some(
|
||||
container(
|
||||
button(
|
||||
text("See more")
|
||||
.width(Length::Fill)
|
||||
.horizontal_alignment(alignment::Horizontal::Center),
|
||||
)
|
||||
.width(Length::Fill)
|
||||
.padding(15)
|
||||
.style(Style::TransparentBorder)
|
||||
.on_press(Message::Next),
|
||||
)
|
||||
.width(Length::Fill)
|
||||
.style(card::SimpleCardStyle),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
),
|
||||
)
|
||||
.align_items(Alignment::Center)
|
||||
.spacing(20)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn event_list_view<'a>(i: usize, event: &HistoryTransaction) -> Element<'a, Message> {
|
||||
container(
|
||||
button(
|
||||
row()
|
||||
.push(
|
||||
row()
|
||||
.push(if event.is_external() {
|
||||
badge::receive()
|
||||
} else {
|
||||
badge::spend()
|
||||
})
|
||||
.push(if let Some(t) = event.time {
|
||||
container(
|
||||
text(&format!("{}", NaiveDateTime::from_timestamp(t as i64, 0)))
|
||||
.small(),
|
||||
)
|
||||
} else {
|
||||
container(text(" Pending ").small())
|
||||
.padding(3)
|
||||
.style(badge::PillStyle::Success)
|
||||
})
|
||||
.spacing(10)
|
||||
.align_items(Alignment::Center)
|
||||
.width(Length::Fill),
|
||||
)
|
||||
.push(
|
||||
row()
|
||||
.push(
|
||||
text(&{
|
||||
if event.is_external() {
|
||||
format!("+ {:.8}", event.incoming_amount.to_btc())
|
||||
} else {
|
||||
format!("- {:.8}", event.outgoing_amount.to_btc())
|
||||
}
|
||||
})
|
||||
.bold()
|
||||
.width(Length::Shrink),
|
||||
)
|
||||
.push(text("BTC"))
|
||||
.spacing(5)
|
||||
.align_items(Alignment::Center),
|
||||
)
|
||||
.align_items(Alignment::Center)
|
||||
.spacing(20),
|
||||
)
|
||||
.padding(10)
|
||||
.on_press(Message::Select(i))
|
||||
.style(Style::TransparentBorder),
|
||||
)
|
||||
.style(card::SimpleCardStyle)
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn event_view<'a>(event: &HistoryTransaction) -> Element<'a, Message> {
|
||||
column()
|
||||
.push(
|
||||
row()
|
||||
.push(if event.is_external() {
|
||||
badge::receive()
|
||||
} else {
|
||||
badge::spend()
|
||||
})
|
||||
.spacing(10)
|
||||
.align_items(Alignment::Center),
|
||||
)
|
||||
.push(
|
||||
text(&{
|
||||
if event.is_external() {
|
||||
format!("+ {} BTC", event.incoming_amount.to_btc())
|
||||
} else {
|
||||
format!("- {} BTC", event.outgoing_amount.to_btc())
|
||||
}
|
||||
})
|
||||
.bold()
|
||||
.size(50)
|
||||
.width(Length::Shrink),
|
||||
)
|
||||
.push_maybe(
|
||||
event
|
||||
.fee_amount
|
||||
.map(|fee| container(text(&format!("Miner Fee: {} BTC", fee.to_btc())))),
|
||||
)
|
||||
.push(card::simple(
|
||||
column()
|
||||
.push_maybe(event.time.map(|t| {
|
||||
let date = NaiveDateTime::from_timestamp(t as i64, 0);
|
||||
row()
|
||||
.width(Length::Fill)
|
||||
.push(container(text("Date:").bold()).width(Length::Fill))
|
||||
.push(container(text(&format!("{}", date))).width(Length::Shrink))
|
||||
}))
|
||||
.push(
|
||||
row()
|
||||
.width(Length::Fill)
|
||||
.push(container(text("Txid:").bold()).width(Length::Fill))
|
||||
.push(
|
||||
container(text(&format!("{}", event.tx.txid()))).width(Length::Shrink),
|
||||
),
|
||||
)
|
||||
.spacing(5),
|
||||
))
|
||||
.align_items(Alignment::Center)
|
||||
.spacing(20)
|
||||
.max_width(750)
|
||||
.into()
|
||||
}
|
||||
|
||||
@ -30,6 +30,7 @@ pub enum SpendTxMessage {
|
||||
Confirm,
|
||||
Cancel,
|
||||
SelectHardwareWallet(usize),
|
||||
Next,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
||||
@ -267,11 +267,11 @@ pub fn dashboard<'a, T: Into<Element<'a, Message>>>(
|
||||
.height(Length::Fill),
|
||||
)
|
||||
.push(
|
||||
column().push(warn(warning)).push(
|
||||
main_section(container(scrollable(content)))
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill),
|
||||
),
|
||||
column()
|
||||
.push(warn(warning))
|
||||
.push(main_section(container(scrollable(
|
||||
container(content).padding(20),
|
||||
)))),
|
||||
)
|
||||
.width(iced::Length::Fill)
|
||||
.height(iced::Length::Fill)
|
||||
@ -280,7 +280,6 @@ pub fn dashboard<'a, T: Into<Element<'a, Message>>>(
|
||||
|
||||
fn main_section<'a, T: 'a>(menu: widget::Container<'a, T>) -> widget::Container<'a, T> {
|
||||
container(menu.max_width(1500))
|
||||
.padding(20)
|
||||
.style(MainSectionStyle)
|
||||
.center_x()
|
||||
.width(Length::Fill)
|
||||
|
||||
@ -118,6 +118,22 @@ impl<C: Client + Debug> Daemon for Lianad<C> {
|
||||
let _res: serde_json::value::Value = self.call("startrescan", Some(vec![t]))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn list_confirmed_txs(
|
||||
&self,
|
||||
start: u32,
|
||||
end: u32,
|
||||
limit: u64,
|
||||
) -> Result<ListTransactionsResult, DaemonError> {
|
||||
self.call(
|
||||
"listconfirmed",
|
||||
Some(vec![json!(start), json!(end), json!(limit)]),
|
||||
)
|
||||
}
|
||||
|
||||
fn list_txs(&self, txids: &[Txid]) -> Result<ListTransactionsResult, DaemonError> {
|
||||
self.call("list_transactions", Some(vec![txids]))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
|
||||
@ -108,6 +108,33 @@ impl Daemon for EmbeddedDaemon {
|
||||
.list_spend())
|
||||
}
|
||||
|
||||
fn list_confirmed_txs(
|
||||
&self,
|
||||
start: u32,
|
||||
end: u32,
|
||||
limit: u64,
|
||||
) -> Result<ListTransactionsResult, DaemonError> {
|
||||
Ok(self
|
||||
.handle
|
||||
.as_ref()
|
||||
.ok_or(DaemonError::NoAnswer)?
|
||||
.read()
|
||||
.unwrap()
|
||||
.control
|
||||
.list_confirmed_transactions(start, end, limit))
|
||||
}
|
||||
|
||||
fn list_txs(&self, txids: &[Txid]) -> Result<ListTransactionsResult, DaemonError> {
|
||||
Ok(self
|
||||
.handle
|
||||
.as_ref()
|
||||
.ok_or(DaemonError::NoAnswer)?
|
||||
.read()
|
||||
.unwrap()
|
||||
.control
|
||||
.list_transactions(txids))
|
||||
}
|
||||
|
||||
fn create_spend_tx(
|
||||
&self,
|
||||
coins_outpoints: &[OutPoint],
|
||||
|
||||
@ -39,22 +39,32 @@ impl std::fmt::Display for DaemonError {
|
||||
|
||||
pub trait Daemon: Debug {
|
||||
fn is_external(&self) -> bool;
|
||||
|
||||
fn load_config(&mut self, _cfg: Config) -> Result<(), DaemonError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn config(&self) -> &Config;
|
||||
|
||||
fn stop(&mut self) -> Result<(), DaemonError>;
|
||||
|
||||
fn get_info(&self) -> Result<model::GetInfoResult, DaemonError>;
|
||||
|
||||
fn get_new_address(&self) -> Result<model::GetAddressResult, DaemonError>;
|
||||
|
||||
fn list_coins(&self) -> Result<model::ListCoinsResult, DaemonError>;
|
||||
|
||||
fn list_spend_txs(&self) -> Result<model::ListSpendResult, DaemonError>;
|
||||
fn create_spend_tx(
|
||||
&self,
|
||||
coins_outpoints: &[OutPoint],
|
||||
destinations: &HashMap<Address, u64>,
|
||||
feerate_vb: u64,
|
||||
) -> Result<model::CreateSpendResult, DaemonError>;
|
||||
fn update_spend_tx(&self, psbt: &Psbt) -> Result<(), DaemonError>;
|
||||
fn delete_spend_tx(&self, txid: &Txid) -> Result<(), DaemonError>;
|
||||
fn broadcast_spend_tx(&self, txid: &Txid) -> Result<(), DaemonError>;
|
||||
fn start_rescan(&self, t: u32) -> Result<(), DaemonError>;
|
||||
fn list_confirmed_txs(
|
||||
&self,
|
||||
_start: u32,
|
||||
_end: u32,
|
||||
_limit: u64,
|
||||
) -> Result<model::ListTransactionsResult, DaemonError>;
|
||||
fn list_txs(&self, txid: &[Txid]) -> Result<model::ListTransactionsResult, DaemonError>;
|
||||
|
||||
fn list_spend_transactions(&self) -> Result<Vec<model::SpendTx>, DaemonError> {
|
||||
let coins = self.list_coins()?.coins;
|
||||
@ -78,15 +88,67 @@ pub trait Daemon: Debug {
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn create_spend_tx(
|
||||
fn list_history_txs(
|
||||
&self,
|
||||
coins_outpoints: &[OutPoint],
|
||||
destinations: &HashMap<Address, u64>,
|
||||
feerate_vb: u64,
|
||||
) -> Result<model::CreateSpendResult, DaemonError>;
|
||||
start: u32,
|
||||
end: u32,
|
||||
limit: u64,
|
||||
) -> Result<Vec<model::HistoryTransaction>, DaemonError> {
|
||||
let coins = self.list_coins()?.coins;
|
||||
let txs = self.list_confirmed_txs(start, end, limit)?.transactions;
|
||||
Ok(txs
|
||||
.into_iter()
|
||||
.map(|tx| {
|
||||
let mut tx_coins = Vec::new();
|
||||
let mut change_indexes = Vec::new();
|
||||
for coin in &coins {
|
||||
if coin.outpoint.txid == tx.tx.txid() {
|
||||
change_indexes.push(coin.outpoint.vout as usize)
|
||||
} else if tx
|
||||
.tx
|
||||
.input
|
||||
.iter()
|
||||
.any(|input| input.previous_output == coin.outpoint)
|
||||
{
|
||||
tx_coins.push(*coin);
|
||||
}
|
||||
}
|
||||
model::HistoryTransaction::new(tx.tx, tx.height, tx.time, tx_coins, change_indexes)
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn update_spend_tx(&self, psbt: &Psbt) -> Result<(), DaemonError>;
|
||||
fn delete_spend_tx(&self, txid: &Txid) -> Result<(), DaemonError>;
|
||||
fn broadcast_spend_tx(&self, txid: &Txid) -> Result<(), DaemonError>;
|
||||
fn start_rescan(&self, t: u32) -> Result<(), DaemonError>;
|
||||
fn list_pending_txs(&self) -> Result<Vec<model::HistoryTransaction>, DaemonError> {
|
||||
let coins = self.list_coins()?.coins;
|
||||
let mut txids: Vec<Txid> = Vec::new();
|
||||
for coin in &coins {
|
||||
if let Some(spend) = coin.spend_info {
|
||||
if spend.height.is_none() && !txids.contains(&spend.txid) {
|
||||
txids.push(spend.txid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let txs = self.list_txs(&txids)?.transactions;
|
||||
Ok(txs
|
||||
.into_iter()
|
||||
.map(|tx| {
|
||||
let mut tx_coins = Vec::new();
|
||||
let mut change_indexes = Vec::new();
|
||||
for coin in &coins {
|
||||
if coin.outpoint.txid == tx.tx.txid() {
|
||||
change_indexes.push(coin.outpoint.vout as usize)
|
||||
} else if tx
|
||||
.tx
|
||||
.input
|
||||
.iter()
|
||||
.any(|input| input.previous_output == coin.outpoint)
|
||||
{
|
||||
tx_coins.push(*coin);
|
||||
}
|
||||
}
|
||||
model::HistoryTransaction::new(tx.tx, tx.height, tx.time, tx_coins, change_indexes)
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
pub use liana::{
|
||||
commands::{
|
||||
CreateSpendResult, GetAddressResult, GetInfoResult, ListCoinsEntry, ListCoinsResult,
|
||||
ListSpendEntry, ListSpendResult,
|
||||
ListSpendEntry, ListSpendResult, ListTransactionsResult, TransactionInfo,
|
||||
},
|
||||
miniscript::bitcoin::{util::psbt::Psbt, Amount},
|
||||
miniscript::bitcoin::{util::psbt::Psbt, Amount, Transaction},
|
||||
};
|
||||
|
||||
pub type Coin = ListCoinsEntry;
|
||||
@ -61,3 +61,62 @@ impl SpendTx {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct HistoryTransaction {
|
||||
pub coins: Vec<Coin>,
|
||||
pub change_indexes: Vec<usize>,
|
||||
pub tx: Transaction,
|
||||
pub outgoing_amount: Amount,
|
||||
pub incoming_amount: Amount,
|
||||
pub fee_amount: Option<Amount>,
|
||||
pub height: Option<i32>,
|
||||
pub time: Option<u32>,
|
||||
}
|
||||
|
||||
impl HistoryTransaction {
|
||||
pub fn new(
|
||||
tx: Transaction,
|
||||
height: Option<i32>,
|
||||
time: Option<u32>,
|
||||
coins: Vec<Coin>,
|
||||
change_indexes: Vec<usize>,
|
||||
) -> Self {
|
||||
let (incoming_amount, outgoing_amount) = tx.output.iter().enumerate().fold(
|
||||
(Amount::from_sat(0), Amount::from_sat(0)),
|
||||
|(change, spend), (i, output)| {
|
||||
if change_indexes.contains(&i) {
|
||||
(change + Amount::from_sat(output.value), spend)
|
||||
} else {
|
||||
(change, spend + Amount::from_sat(output.value))
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
let mut inputs_amount = Amount::from_sat(0);
|
||||
for coin in &coins {
|
||||
inputs_amount += coin.amount;
|
||||
}
|
||||
|
||||
let fee_amount = if inputs_amount > outgoing_amount + incoming_amount {
|
||||
Some(inputs_amount - outgoing_amount - incoming_amount)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Self {
|
||||
tx,
|
||||
coins,
|
||||
change_indexes,
|
||||
outgoing_amount,
|
||||
incoming_amount,
|
||||
fee_amount,
|
||||
height,
|
||||
time,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_external(&self) -> bool {
|
||||
self.coins.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user