diff --git a/icons/withdraws.png b/icons/withdraws.png new file mode 100644 index 000000000..161215184 Binary files /dev/null and b/icons/withdraws.png differ diff --git a/src/app.rs b/src/app.rs index 23b447b14..ac50709c6 100644 --- a/src/app.rs +++ b/src/app.rs @@ -7,6 +7,7 @@ use crate::ui::dpns_contested_names_screen::{DPNSContestedNamesScreen, DPNSSubsc use crate::ui::identities::identities_screen::IdentitiesScreen; use crate::ui::network_chooser_screen::NetworkChooserScreen; use crate::ui::transition_visualizer_screen::TransitionVisualizerScreen; +use crate::ui::withdraws_status_screen::WithdrawsStatusScreen; use crate::ui::{MessageType, RootScreenType, Screen, ScreenLike, ScreenType}; use dash_sdk::dpp::dashcore::Network; use derive_more::From; @@ -125,6 +126,7 @@ impl AppState { let mut transition_visualizer_screen = TransitionVisualizerScreen::new(&mainnet_app_context); let mut document_query_screen = DocumentQueryScreen::new(&mainnet_app_context); + let mut withdraws_status_screen = WithdrawsStatusScreen::new(&mainnet_app_context); let mut network_chooser_screen = NetworkChooserScreen::new( &mainnet_app_context, testnet_app_context.as_ref(), @@ -149,6 +151,7 @@ impl AppState { DPNSContestedNamesScreen::new(&testnet_app_context, DPNSSubscreen::Owned); transition_visualizer_screen = TransitionVisualizerScreen::new(testnet_app_context); document_query_screen = DocumentQueryScreen::new(testnet_app_context); + withdraws_status_screen = WithdrawsStatusScreen::new(testnet_app_context); } network_chooser_screen.current_network = chosen_network; } @@ -185,6 +188,10 @@ impl AppState { RootScreenType::RootScreenDocumentQuery, Screen::DocumentQueryScreen(document_query_screen), ), + ( + RootScreenType::RootScreenWithdrawsStatus, + Screen::WithdrawsStatusScreen(withdraws_status_screen), + ), ( RootScreenType::RootScreenNetworkChooser, Screen::NetworkChooserScreen(network_chooser_screen), @@ -297,6 +304,9 @@ impl App for AppState { BackendTaskSuccessResult::SuccessfulVotes(_) => { self.visible_screen_mut().refresh(); } + BackendTaskSuccessResult::WithdrawalStatus(_) => { + self.visible_screen_mut().display_task_result(message); + } }, TaskResult::Error(message) => { self.visible_screen_mut() diff --git a/src/context.rs b/src/context.rs index bda341a94..d6328de77 100644 --- a/src/context.rs +++ b/src/context.rs @@ -28,6 +28,7 @@ pub struct AppContext { pub(crate) sdk: Sdk, pub(crate) config: NetworkConfig, pub(crate) dpns_contract: Arc, + pub(crate) withdraws_contract: Arc, pub(crate) core_client: Client, pub(crate) has_wallet: AtomicBool, pub(crate) wallets: RwLock>>>, @@ -56,6 +57,10 @@ impl AppContext { load_system_data_contract(SystemDataContract::DPNS, PlatformVersion::latest()) .expect("expected to load dpns contract"); + let withdrawal_contract = + load_system_data_contract(SystemDataContract::Withdrawals, PlatformVersion::latest()) + .expect("expected to get withdrawal contract"); + let addr = format!( "http://{}:{}", network_config.core_host, network_config.core_rpc_port @@ -84,6 +89,7 @@ impl AppContext { sdk, config: network_config, dpns_contract: Arc::new(dpns_contract), + withdraws_contract: Arc::new(withdrawal_contract), core_client, has_wallet: (!wallets.is_empty()).into(), wallets: RwLock::new(wallets), diff --git a/src/platform/mod.rs b/src/platform/mod.rs index 396b06707..b8679a51a 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -5,6 +5,7 @@ use crate::platform::contract::ContractTask; use crate::platform::core::{CoreItem, CoreTask}; use crate::platform::document::DocumentTask; use crate::platform::identity::IdentityTask; +use crate::platform::withdrawals::{WithdrawStatusData, WithdrawalsTask}; use dash_sdk::dpp::voting::votes::Vote; use dash_sdk::query_types::Documents; use std::sync::Arc; @@ -15,6 +16,7 @@ pub mod contract; pub mod core; mod document; pub mod identity; +pub mod withdrawals; #[derive(Debug, Clone, PartialEq)] pub(crate) enum BackendTask { @@ -23,6 +25,7 @@ pub(crate) enum BackendTask { ContractTask(ContractTask), ContestedResourceTask(ContestedResourceTask), CoreTask(CoreTask), + WithdrawalTask(WithdrawalsTask), } #[derive(Debug, Clone, PartialEq)] @@ -32,6 +35,7 @@ pub(crate) enum BackendTaskSuccessResult { Documents(Documents), CoreItem(CoreItem), SuccessfulVotes(Vec), + WithdrawalStatus(WithdrawStatusData), } impl BackendTaskSuccessResult {} @@ -71,6 +75,9 @@ impl AppContext { self.run_document_task(document_task, &sdk).await } BackendTask::CoreTask(core_task) => self.run_core_task(core_task).await, + BackendTask::WithdrawalTask(withdrawal_task) => { + self.run_withdraws_task(withdrawal_task, &sdk).await + } } } } diff --git a/src/platform/withdrawals/mod.rs b/src/platform/withdrawals/mod.rs new file mode 100644 index 000000000..4eaf4dd3e --- /dev/null +++ b/src/platform/withdrawals/mod.rs @@ -0,0 +1,211 @@ +use std::sync::Arc; +use chrono::{DateTime, Local, Utc}; +use dash_sdk::dashcore_rpc::dashcore::{Address, Network, ScriptBuf}; +use dash_sdk::dpp::data_contracts::withdrawals_contract::v1::document_types::withdrawal::properties::{AMOUNT, OUTPUT_SCRIPT, STATUS}; +use dash_sdk::dpp::data_contracts::withdrawals_contract::WithdrawalStatus; +use dash_sdk::dpp::document::DocumentV0Getters; +use dash_sdk::dpp::fee::Credits; +use dash_sdk::dpp::platform_value::btreemap_extensions::BTreeValueMapHelper; +use dash_sdk::dpp::platform_value::Value; +use dash_sdk::dpp::version::PlatformVersion; +use dash_sdk::dpp::withdrawal::daily_withdrawal_limit::daily_withdrawal_limit; +use dash_sdk::drive::drive::RootTree; +use dash_sdk::drive::grovedb::Element; +use dash_sdk::drive::grovedb::element::SumValue; +use dash_sdk::drive::query::{OrderClause, WhereClause, WhereOperator}; +use dash_sdk::platform::{Document, DocumentQuery, FetchMany, Identifier}; +use dash_sdk::platform::fetch_current_no_parameters::FetchCurrent; +use dash_sdk::query_types::{KeysInPath, TotalCreditsInPlatform}; +use dash_sdk::Sdk; +use crate::context::AppContext; +use crate::platform::BackendTaskSuccessResult; + +/// constant id for subtree containing the sum of withdrawals +pub const WITHDRAWAL_TRANSACTIONS_SUM_AMOUNT_TREE_KEY: [u8; 1] = [2]; + +#[derive(Debug, Clone, PartialEq)] +pub(crate) enum WithdrawalsTask { + QueryWithdrawals(Vec), +} + +#[derive(Debug, Clone, PartialEq)] +pub struct WithdrawRecord { + pub date_time: DateTime, + pub status: WithdrawalStatus, + pub amount: Credits, + pub owner_id: Identifier, + pub address: Address, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct WithdrawStatusData { + pub withdrawals: Vec, + pub total_amount: Credits, + pub recent_withdrawal_amounts: SumValue, + pub daily_withdrawal_limit: Credits, + pub total_credits_on_platform: Credits, +} + +impl AppContext { + pub async fn run_withdraws_task( + self: &Arc, + task: WithdrawalsTask, + sdk: &Sdk, + ) -> Result { + let sdk = sdk.clone(); + match &task { + WithdrawalsTask::QueryWithdrawals(status_filter) => { + self.query_withdrawals(sdk, status_filter).await + } + } + } + + pub(super) async fn query_withdrawals( + self: &Arc, + sdk: Sdk, + status_filter: &Vec, + ) -> Result { + let queued_document_query = DocumentQuery { + data_contract: self.withdraws_contract.clone(), + document_type_name: "withdrawal".to_string(), + where_clauses: vec![WhereClause { + field: "status".to_string(), + operator: WhereOperator::In, + value: Value::Array(status_filter.to_vec()), + }], + order_by_clauses: vec![ + OrderClause { + field: "status".to_string(), + ascending: true, + }, + OrderClause { + field: "transactionIndex".to_string(), + ascending: true, + }, + ], + limit: 100, + start: None, + }; + + let documents = Document::fetch_many(&sdk, queued_document_query.clone()) + .await + .map_err(|e| e.to_string())?; + + let keys_in_path = KeysInPath { + path: vec![vec![RootTree::WithdrawalTransactions as u8]], + keys: vec![WITHDRAWAL_TRANSACTIONS_SUM_AMOUNT_TREE_KEY.to_vec()], + }; + + let elements = Element::fetch_many(&sdk, keys_in_path) + .await + .map_err(|e| e.to_string())?; + + let sum_tree_element_option = elements + .get(&WITHDRAWAL_TRANSACTIONS_SUM_AMOUNT_TREE_KEY.to_vec()) + .ok_or_else(|| { + "could not get sum tree value for current withdrawal maximum".to_string() + })?; + + let sum_tree_element = sum_tree_element_option.as_ref().ok_or_else(|| { + "could not get sum tree value for current withdrawal maximum".to_string() + })?; + + let value = if let Element::SumTree(_, value, _) = sum_tree_element { + value + } else { + return Err("could not get sum tree value for current withdrawal maximum".to_string()); + }; + + let total_credits = TotalCreditsInPlatform::fetch_current(&sdk) + .await + .map_err(|e| e.to_string())?; + + Ok(BackendTaskSuccessResult::WithdrawalStatus( + util_transform_documents_to_withdrawal_status( + *value, + total_credits.0, + &documents.values().filter_map(|a| a.clone()).collect(), + sdk.network, + )?, + )) + } +} + +fn util_transform_documents_to_withdrawal_status( + recent_withdrawal_amounts: SumValue, + total_credits_on_platform: Credits, + withdrawal_documents: &Vec, + network: Network, +) -> Result { + let total_amount = withdrawal_documents.iter().try_fold(0, |acc, document| { + document + .properties() + .get_integer::(AMOUNT) + .map_err(|_| "expected amount on withdrawal".to_string()) + .map(|amount| acc + amount) + })?; + + let daily_withdrawal_limit = + daily_withdrawal_limit(total_credits_on_platform, PlatformVersion::latest()) + .map_err(|_| "expected to get daily withdrawal limit".to_string())?; + + let mut vec_withdraws = vec![]; + for document in withdrawal_documents { + vec_withdraws.push(util_convert_document_to_record(document, network)?); + } + + Ok(WithdrawStatusData { + withdrawals: vec_withdraws, + total_amount, + recent_withdrawal_amounts, + daily_withdrawal_limit, + total_credits_on_platform, + }) +} + +fn util_convert_document_to_record( + document: &Document, + network: Network, +) -> Result { + let index = document + .created_at() + .ok_or_else(|| "expected created at".to_string())?; + + let local_datetime: DateTime = DateTime::::from_timestamp_millis(index as i64) + .ok_or_else(|| "expected date time".to_string())? + .into(); + + let amount = document + .properties() + .get_integer::(AMOUNT) + .map_err(|_| "expected amount on withdrawal".to_string())?; + + let status_int = document + .properties() + .get_integer::(STATUS) + .map_err(|_| "expected status on withdrawal".to_string())?; + + let status: WithdrawalStatus = status_int + .try_into() + .map_err(|_| "invalid withdrawal status".to_string())?; + + let owner_id = document.owner_id(); + + let address_bytes = document + .properties() + .get_bytes(OUTPUT_SCRIPT) + .map_err(|_| "expected output script".to_string())?; + + let output_script = ScriptBuf::from_bytes(address_bytes); + + let address = Address::from_script(&output_script, network) + .map_err(|_| "expected a valid address".to_string())?; + + Ok(WithdrawRecord { + date_time: local_datetime, + status, + amount, + owner_id, + address, + }) +} diff --git a/src/ui/components/left_panel.rs b/src/ui/components/left_panel.rs index cb9fdcef0..9ce7d244a 100644 --- a/src/ui/components/left_panel.rs +++ b/src/ui/components/left_panel.rs @@ -56,6 +56,11 @@ pub fn add_left_panel( RootScreenType::RootScreenTransitionVisualizerScreen, "tools.png", ), + ( + "W", + RootScreenType::RootScreenWithdrawsStatus, + "withdraws.png", + ), ("N", RootScreenType::RootScreenNetworkChooser, "config.png"), ]; diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 1d1cf8ef8..45ddcb7f5 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -12,6 +12,7 @@ use crate::ui::network_chooser_screen::NetworkChooserScreen; use crate::ui::transfers::TransferScreen; use crate::ui::transition_visualizer_screen::TransitionVisualizerScreen; use crate::ui::withdrawals::WithdrawalScreen; +use crate::ui::withdraws_status_screen::WithdrawsStatusScreen; use dash_sdk::dpp::identity::Identity; use dash_sdk::dpp::prelude::IdentityPublicKey; use dpns_contested_names_screen::DPNSSubscreen; @@ -36,6 +37,7 @@ pub mod network_chooser_screen; pub mod transfers; pub mod transition_visualizer_screen; pub mod withdrawals; +pub mod withdraws_status_screen; #[derive(Debug, Clone, Copy, Ord, PartialOrd, Eq, PartialEq, Hash)] pub enum RootScreenType { @@ -45,6 +47,7 @@ pub enum RootScreenType { RootScreenDPNSOwnedNames, RootScreenDocumentQuery, RootScreenTransitionVisualizerScreen, + RootScreenWithdrawsStatus, RootScreenNetworkChooser, } @@ -54,11 +57,12 @@ impl RootScreenType { match self { RootScreenType::RootScreenIdentities => 0, RootScreenType::RootScreenDPNSActiveContests => 1, - RootScreenType::RootScreenDocumentQuery => 2, - RootScreenType::RootScreenTransitionVisualizerScreen => 3, - RootScreenType::RootScreenNetworkChooser => 4, - RootScreenType::RootScreenDPNSPastContests => 5, - RootScreenType::RootScreenDPNSOwnedNames => 6, + RootScreenType::RootScreenDPNSPastContests => 2, + RootScreenType::RootScreenDPNSOwnedNames => 3, + RootScreenType::RootScreenDocumentQuery => 4, + RootScreenType::RootScreenTransitionVisualizerScreen => 5, + RootScreenType::RootScreenNetworkChooser => 6, + RootScreenType::RootScreenWithdrawsStatus => 7, } } @@ -67,11 +71,12 @@ impl RootScreenType { match value { 0 => Some(RootScreenType::RootScreenIdentities), 1 => Some(RootScreenType::RootScreenDPNSActiveContests), - 2 => Some(RootScreenType::RootScreenDocumentQuery), - 3 => Some(RootScreenType::RootScreenTransitionVisualizerScreen), - 4 => Some(RootScreenType::RootScreenNetworkChooser), - 5 => Some(RootScreenType::RootScreenDPNSPastContests), - 6 => Some(RootScreenType::RootScreenDPNSOwnedNames), + 2 => Some(RootScreenType::RootScreenDPNSPastContests), + 3 => Some(RootScreenType::RootScreenDPNSOwnedNames), + 4 => Some(RootScreenType::RootScreenDocumentQuery), + 5 => Some(RootScreenType::RootScreenTransitionVisualizerScreen), + 6 => Some(RootScreenType::RootScreenNetworkChooser), + 7 => Some(RootScreenType::RootScreenWithdrawsStatus), _ => None, } } @@ -88,6 +93,7 @@ impl From for ScreenType { ScreenType::TransitionVisualizer } RootScreenType::RootScreenDocumentQuery => ScreenType::DocumentQueryScreen, + RootScreenType::RootScreenWithdrawsStatus => ScreenType::WithdrawsStatus, RootScreenType::RootScreenNetworkChooser => ScreenType::NetworkChooser, } } @@ -110,6 +116,7 @@ pub enum ScreenType { KeyInfo(QualifiedIdentity, IdentityPublicKey, Option<[u8; 32]>), Keys(Identity), DocumentQueryScreen, + WithdrawsStatus, NetworkChooser, RegisterDpnsName, } @@ -165,6 +172,9 @@ impl ScreenType { ScreenType::DocumentQueryScreen => { Screen::DocumentQueryScreen(DocumentQueryScreen::new(app_context)) } + ScreenType::WithdrawsStatus => { + Screen::WithdrawsStatusScreen(WithdrawsStatusScreen::new(app_context)) + } ScreenType::AddNewWallet => { Screen::AddNewWalletScreen(AddNewWalletScreen::new(app_context)) } @@ -187,6 +197,7 @@ pub enum Screen { TransferScreen(TransferScreen), AddKeyScreen(AddKeyScreen), TransitionVisualizerScreen(TransitionVisualizerScreen), + WithdrawsStatusScreen(WithdrawsStatusScreen), NetworkChooserScreen(NetworkChooserScreen), } @@ -207,6 +218,7 @@ impl Screen { Screen::RegisterDpnsNameScreen(screen) => screen.app_context = app_context, Screen::AddNewWalletScreen(screen) => screen.app_context = app_context, Screen::TransferScreen(screen) => screen.app_context = app_context, + Screen::WithdrawsStatusScreen(screen) => screen.app_context = app_context, } } } @@ -281,6 +293,7 @@ impl Screen { Screen::RegisterDpnsNameScreen(_) => ScreenType::RegisterDpnsName, Screen::AddNewWalletScreen(_) => ScreenType::AddNewWallet, Screen::TransferScreen(screen) => ScreenType::TransferScreen(screen.identity.clone()), + Screen::WithdrawsStatusScreen(_) => ScreenType::WithdrawsStatus, } } } diff --git a/src/ui/withdraws_status_screen.rs b/src/ui/withdraws_status_screen.rs new file mode 100644 index 000000000..4ba13b5a0 --- /dev/null +++ b/src/ui/withdraws_status_screen.rs @@ -0,0 +1,372 @@ +use crate::app::{AppAction, DesiredAppAction}; +use crate::context::AppContext; +use crate::platform::withdrawals::{WithdrawRecord, WithdrawStatusData, WithdrawalsTask}; +use crate::platform::{BackendTask, BackendTaskSuccessResult}; +use crate::ui::components::left_panel::add_left_panel; +use crate::ui::components::top_panel::add_top_panel; +use crate::ui::{MessageType, RootScreenType, ScreenLike}; +use dash_sdk::dpp::dash_to_credits; +use dash_sdk::dpp::data_contracts::withdrawals_contract::WithdrawalStatus; +use dash_sdk::dpp::document::DocumentV0Getters; +use dash_sdk::dpp::platform_value::Value; +use egui::{ComboBox, Context, Ui}; +use egui_extras::{Column, TableBuilder}; +use itertools::Itertools; +use std::cell::{Cell, RefCell}; +use std::sync::{Arc, Mutex}; + +pub struct WithdrawsStatusScreen { + pub app_context: Arc, + data: Arc>>, + sort_column: Cell>, + sort_ascending: Cell, + filter_status_queued: Cell, + filter_status_pooled: Cell, + filter_status_broadcasted: Cell, + filter_status_complete: Cell, + filter_status_expired: Cell, + filter_status_mix: RefCell>, + pagination_current_page: Cell, + pagination_items_per_page: Cell, + error_message: Option, +} + +#[derive(Clone, Copy, PartialEq)] +enum SortColumn { + DateTime, + Status, + Amount, + OwnerId, + Destination, +} + +#[derive(Clone, Copy, PartialEq)] +enum PaginationItemsPerPage { + Items10 = 10, + Items15 = 15, + Items20 = 20, + Items30 = 30, + Items50 = 50, +} + +impl WithdrawsStatusScreen { + pub fn new(app_context: &Arc) -> Self { + Self { + app_context: app_context.clone(), + data: Arc::new(Mutex::new(None)), + sort_ascending: Cell::from(false), + sort_column: Cell::from(Some(SortColumn::DateTime)), + error_message: None, + filter_status_queued: Cell::new(true), + filter_status_pooled: Cell::new(true), + filter_status_broadcasted: Cell::new(true), + filter_status_complete: Cell::new(true), + filter_status_expired: Cell::new(false), + filter_status_mix: RefCell::new(vec![ + Value::U8(WithdrawalStatus::QUEUED as u8), + Value::U8(WithdrawalStatus::POOLED as u8), + Value::U8(WithdrawalStatus::BROADCASTED as u8), + Value::U8(WithdrawalStatus::COMPLETE as u8), + Value::U8(WithdrawalStatus::EXPIRED as u8), + ]), + pagination_current_page: Cell::new(0), + pagination_items_per_page: Cell::new(PaginationItemsPerPage::Items15), + } + } + + fn show_input_field(&mut self, ui: &mut Ui) {} + + fn show_output(&mut self, ui: &mut egui::Ui) { + if self.error_message.is_some() { + ui.centered_and_justified(|ui| { + ui.heading(self.error_message.as_ref().unwrap()); + }); + } else { + let mut lock_data = self.data.lock().unwrap_or_else(|poisoned| { + // Mutex is poisoned, trying to recover the inner data + poisoned.into_inner() + }); + + if let Some(ref mut data) = *lock_data { + self.sort_withdraws_data(&mut data.withdrawals); + self.show_withdraws_data(ui, data); + } + } + } + + fn sort_withdraws_data(&self, data: &mut Vec) { + if let Some(column) = self.sort_column.get() { + let compare = |a: &WithdrawRecord, b: &WithdrawRecord| -> std::cmp::Ordering { + let ord = match column { + SortColumn::DateTime => a.date_time.cmp(&b.date_time), + SortColumn::Status => (a.status as u8).cmp(&(b.status as u8)), + SortColumn::Amount => a.amount.cmp(&b.amount), + SortColumn::OwnerId => a.owner_id.cmp(&b.owner_id), + SortColumn::Destination => a.address.cmp(&b.address), + }; + if self.sort_ascending.get() { + ord + } else { + ord.reverse() + } + }; + data.sort_by(compare); + } + } + + fn handle_column_click(&self, current_sort: SortColumn) { + if self.sort_column.get() == Some(current_sort) { + self.sort_ascending.set(!self.sort_ascending.get()); + } else { + self.sort_column.set(Some(current_sort)); + self.sort_ascending.set(true); + } + } + + fn show_withdraws_data(&self, ui: &mut egui::Ui, data: &WithdrawStatusData) { + egui::Grid::new("general_info_grid") + .num_columns(2) + .spacing([20.0, 8.0]) // Adjust spacing as needed + .show(ui, |ui| { + ui.heading("General Information"); + ui.separator(); + ui.end_row(); + ui.label("Total withdrawals amount:"); + ui.label(format!( + "{:.2} DASH", + data.total_amount as f64 / (dash_to_credits!(1) as f64) + )); + ui.end_row(); + + ui.label("Recent withdrawals amount:"); + ui.label(format!( + "{:.2} DASH", + data.recent_withdrawal_amounts as f64 / (dash_to_credits!(1) as f64) + )); + ui.end_row(); + + ui.label("Daily withdrawals limit:"); + ui.label(format!( + "{:.2} DASH", + data.daily_withdrawal_limit as f64 / (dash_to_credits!(1) as f64) + )); + ui.end_row(); + + ui.label("Total credits on Platform:"); + ui.label(format!( + "{:.2} DASH", + data.total_credits_on_platform as f64 / (dash_to_credits!(1) as f64) + )); + ui.end_row(); + }); + + ui.add_space(30.0); // Optional spacing between the grids + + egui::Grid::new("filters_grid").show(ui, |ui| { + ui.heading("Filters"); + ui.end_row(); + ui.horizontal(|ui| { + ui.label("Filter by status:"); + ui.add_space(8.0); // Space after label + let mut value = self.filter_status_queued.get(); + if ui.checkbox(&mut value, "Queued").changed() { + self.filter_status_queued.set(value); + self.util_build_combined_filter_status_mix(); + } + ui.add_space(8.0); + let mut value = self.filter_status_pooled.get(); + if ui.checkbox(&mut value, "Pooled").changed() { + self.filter_status_pooled.set(value); + self.util_build_combined_filter_status_mix(); + } + ui.add_space(8.0); + let mut value = self.filter_status_broadcasted.get(); + if ui.checkbox(&mut value, "Broadcasted").changed() { + self.filter_status_broadcasted.set(value); + self.util_build_combined_filter_status_mix(); + } + ui.add_space(8.0); + let mut value = self.filter_status_complete.get(); + if ui.checkbox(&mut value, "Complete").changed() { + self.filter_status_complete.set(value); + self.util_build_combined_filter_status_mix(); + } + ui.add_space(8.0); + let mut value = self.filter_status_expired.get(); + if ui.checkbox(&mut value, "Expired").changed() { + self.filter_status_expired.set(value); + self.util_build_combined_filter_status_mix(); + } + }); + }); + ui.add_space(30.0); + ui.heading(format!("Withdrawals ({})", data.withdrawals.len())); + let mut selected = self.pagination_items_per_page.get(); + let old_selected = selected; + ComboBox::from_label("Items per page") + .selected_text(format!("{}", selected as usize)) + .show_ui(ui, |ui| { + ui.selectable_value(&mut selected, PaginationItemsPerPage::Items10, "10"); + ui.selectable_value(&mut selected, PaginationItemsPerPage::Items15, "15"); + ui.selectable_value(&mut selected, PaginationItemsPerPage::Items20, "20"); + ui.selectable_value(&mut selected, PaginationItemsPerPage::Items30, "30"); + ui.selectable_value(&mut selected, PaginationItemsPerPage::Items50, "50"); + }); + if selected != old_selected { + self.pagination_items_per_page.set(selected); + } + println!("computing with:{}", self.pagination_items_per_page.get() as usize); + let total_pages = (data.withdrawals.len() + (self.pagination_items_per_page.get() as usize) - 1) / (self.pagination_items_per_page.get() as usize); + let mut current_page = self.pagination_current_page.get().min(total_pages - 1); // Clamp to valid page range + // Calculate the slice of data for the current page + let start_index = current_page * (self.pagination_items_per_page.get() as usize); + let end_index = (start_index + (self.pagination_items_per_page.get() as usize)).min(data.withdrawals.len()); + ui.separator(); + TableBuilder::new(ui) + .striped(true) + .resizable(true) + .column(Column::initial(150.0).resizable(true)) // Date / Time + .column(Column::initial(80.0).resizable(true)) // Status + .column(Column::initial(140.0).resizable(true)) // Amount + .column(Column::initial(350.0).resizable(true)) // OwnerID + .column(Column::initial(320.0).resizable(true)) // Destination + .header(20.0, |mut header| { + header.col(|ui| { + if ui.selectable_label(false, "Date / Time").clicked() { + self.handle_column_click(SortColumn::DateTime); + } + }); + header.col(|ui| { + if ui.selectable_label(false, "Status").clicked() { + self.handle_column_click(SortColumn::Status); + } + }); + header.col(|ui| { + if ui.selectable_label(false, "Amount").clicked() { + self.handle_column_click(SortColumn::Amount); + } + }); + header.col(|ui| { + if ui.selectable_label(false, "Owner ID").clicked() { + self.handle_column_click(SortColumn::OwnerId); + } + }); + header.col(|ui| { + if ui.selectable_label(false, "Destination").clicked() { + self.handle_column_click(SortColumn::Destination); + } + }); + }) + .body(|mut body| { + for record in &data.withdrawals[start_index..end_index] { + body.row(18.0, |mut row| { + row.col(|ui| { + ui.label(&record.date_time.format("%Y-%m-%d %H:%M:%S").to_string()); + }); + row.col(|ui| { + ui.label(format!("{}", &record.status)); + }); + row.col(|ui| { + ui.label(format!( + "{:.2} DASH", + record.amount as f64 / (dash_to_credits!(1) as f64) + )); + }); + row.col(|ui| { + ui.label(format!("{}", &record.owner_id)); + }); + row.col(|ui| { + ui.label(format!("{}", &record.address)); + }); + }); + } + }); + // Pagination controls at the bottom + ui.horizontal(|ui| { + if ui.button("Previous").clicked() && current_page > 0 { + self.pagination_current_page.set(current_page - 1) + } + + ui.label(format!("Page {}/{}", current_page + 1, total_pages)); + + if ui.button("Next").clicked() && current_page < total_pages - 1 { + self.pagination_current_page.set(current_page + 1) + } + }); + } + + fn util_build_combined_filter_status_mix(&self) { + let mut res = vec![]; + if self.filter_status_queued.get() { + res.push(Value::U8(WithdrawalStatus::QUEUED as u8)); + } + if self.filter_status_pooled.get() { + res.push(Value::U8(WithdrawalStatus::POOLED as u8)); + } + if self.filter_status_broadcasted.get() { + res.push(Value::U8(WithdrawalStatus::BROADCASTED as u8)); + } + if self.filter_status_complete.get() { + res.push(Value::U8(WithdrawalStatus::COMPLETE as u8)); + } + if self.filter_status_expired.get() { + res.push(Value::U8(WithdrawalStatus::EXPIRED as u8)); + } + + self.filter_status_mix.borrow_mut().clear(); + self.filter_status_mix.borrow_mut().extend(res); + } +} + +impl ScreenLike for WithdrawsStatusScreen { + fn refresh(&mut self) { + let mut lock_data = self.data.lock().unwrap_or_else(|poisoned| { + // Mutex is poisoned, trying to recover the inner data + poisoned.into_inner() + }); + *lock_data = None; + self.error_message = None; + } + + fn display_message(&mut self, message: &str, message_type: MessageType) { + self.error_message = Some(message.to_string()); + } + fn display_task_result(&mut self, backend_task_success_result: BackendTaskSuccessResult) { + if let BackendTaskSuccessResult::WithdrawalStatus(data) = backend_task_success_result { + let mut lock_data = self.data.lock().unwrap_or_else(|poisoned| { + // Mutex is poisoned, trying to recover the inner data + poisoned.into_inner() + }); + *lock_data = Some(data); + self.error_message = None; + } + } + + fn ui(&mut self, ctx: &Context) -> AppAction { + let query = ( + "Refresh", + DesiredAppAction::BackendTask(BackendTask::WithdrawalTask( + WithdrawalsTask::QueryWithdrawals(self.filter_status_mix.borrow().clone()), + )), + ); + let mut action = add_top_panel( + ctx, + &self.app_context, + vec![("Dash Evo Tool", AppAction::None)], + vec![query], + ); + + action |= add_left_panel( + ctx, + &self.app_context, + RootScreenType::RootScreenWithdrawsStatus, + ); + + egui::CentralPanel::default().show(ctx, |ui| { + self.show_input_field(ui); + self.show_output(ui); + }); + + action + } +}