From 8221c961d191e260fd3a04af57a048f49d62700b Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Fri, 18 Jul 2025 15:11:07 +0200 Subject: [PATCH 01/11] fix: inactive button on set price group action --- .../group_actions_screen.rs | 7 +- src/ui/mod.rs | 2 +- src/ui/tokens/set_token_price_screen.rs | 82 +++++++++++++++++-- src/ui/tokens/tokens_screen/my_tokens.rs | 1 + 4 files changed, 83 insertions(+), 9 deletions(-) diff --git a/src/ui/contracts_documents/group_actions_screen.rs b/src/ui/contracts_documents/group_actions_screen.rs index 1217ff485..ca6315fe6 100644 --- a/src/ui/contracts_documents/group_actions_screen.rs +++ b/src/ui/contracts_documents/group_actions_screen.rs @@ -417,8 +417,11 @@ impl GroupActionsScreen { AppAction::AddScreen(Screen::UpdateTokenConfigScreen(Box::new(update_screen))); } TokenEvent::ChangePriceForDirectPurchase(schedule, note_opt) => { - let mut change_price_screen = - SetTokenPriceScreen::new(identity_token_info, &self.app_context); + let mut change_price_screen = SetTokenPriceScreen::new( + identity_token_info, + schedule.clone(), + &self.app_context, + ); change_price_screen.group_action_id = Some(action_id); change_price_screen.token_pricing_schedule = format!("{:?}", schedule); change_price_screen.public_note = note_opt.clone(); diff --git a/src/ui/mod.rs b/src/ui/mod.rs index b7fb41094..645ba9c9c 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -411,7 +411,7 @@ impl ScreenType { PurchaseTokenScreen::new(identity_token_info.clone(), app_context), ), ScreenType::SetTokenPriceScreen(identity_token_info) => Screen::SetTokenPriceScreen( - SetTokenPriceScreen::new(identity_token_info.clone(), app_context), + SetTokenPriceScreen::new(identity_token_info.clone(), None, app_context), ), } } diff --git a/src/ui/tokens/set_token_price_screen.rs b/src/ui/tokens/set_token_price_screen.rs index 374071c9b..ccd553230 100644 --- a/src/ui/tokens/set_token_price_screen.rs +++ b/src/ui/tokens/set_token_price_screen.rs @@ -44,6 +44,24 @@ pub enum PricingType { RemovePricing, } +impl From for PricingType { + fn from(schedule: TokenPricingSchedule) -> Self { + match schedule { + TokenPricingSchedule::SinglePrice(_) => PricingType::SinglePrice, + TokenPricingSchedule::SetPrices(_) => PricingType::TieredPricing, + } + } +} + +impl From> for PricingType { + fn from(schedule: Option) -> Self { + match schedule { + Some(schedule) => PricingType::from(schedule), + None => PricingType::RemovePricing, + } + } +} + /// Internal states for the mint process. #[derive(PartialEq)] pub enum SetTokenPriceStatus { @@ -63,6 +81,7 @@ pub struct SetTokenPriceScreen { pub group_action_id: Option, pub token_pricing_schedule: String, + /// Token pricing schedule to use; if None, we will remove the pricing schedule pricing_type: PricingType, single_price: String, tiered_prices: Vec<(String, String)>, @@ -81,14 +100,43 @@ pub struct SetTokenPriceScreen { show_password: bool, } +/// 1 Dash = 100,000,000,000 credits +const CREDITS_PER_DASH: Credits = 100_000_000_000; + impl SetTokenPriceScreen { /// Converts Dash amount to credits (1 Dash = 100,000,000,000 credits) + /// + /// ## Panics + /// + /// This function will panic if the conversion fails, which should not happen under normal circumstances fn dash_to_credits(dash_amount: f64) -> Credits { - (dash_amount * 100_000_000_000.0) as Credits + let credits_f64 = dash_amount * CREDITS_PER_DASH as f64; + + if credits_f64 < 0.0 || !credits_f64.is_finite() || credits_f64 > u64::MAX as f64 { + panic!( + "Dash amount {} after conversion to credits is not in a valid range: 0 <= {} <= {}", + dash_amount, + credits_f64, + u64::MAX + ); + } + + // Round to nearest integer to handle floating point precision issues + let credits_u64 = if credits_f64 > ((u64::MAX - 1) as f64) { + credits_f64.floor() as u64 + } else { + credits_f64.round() as u64 + }; + + credits_u64 as Credits } - pub fn new(identity_token_info: IdentityTokenInfo, app_context: &Arc) -> Self { - let possible_key = identity_token_info + pub fn new( + identity_token_info: IdentityTokenInfo, + schedule: Option, + app_context: &Arc, + ) -> Self { + let possible_key: Option<&IdentityPublicKey> = identity_token_info .identity .identity .get_first_public_key_matching( @@ -191,6 +239,28 @@ impl SetTokenPriceScreen { &mut error_message, ); + let (single_price, tiered_prices) = match &schedule { + Some(TokenPricingSchedule::SinglePrice(price)) => ( + // we store price as credits, so convert to Dash for processing + (*price as f64 / CREDITS_PER_DASH as f64).to_string(), + vec![("1".to_string(), "".to_string())], + ), + Some(TokenPricingSchedule::SetPrices(prices)) => { + let tiered_prices = prices + .iter() + .map(|(amount, price)| { + ( + amount.to_string(), + (*price as f64 / CREDITS_PER_DASH as f64).to_string(), + ) + }) + .collect::>(); + + (String::new(), tiered_prices) + } + None => (String::new(), vec![("1".to_string(), String::new())]), + }; + Self { identity_token_info: identity_token_info.clone(), selected_key: possible_key.cloned(), @@ -199,9 +269,9 @@ impl SetTokenPriceScreen { is_unilateral_group_member, group_action_id: None, token_pricing_schedule: "".to_string(), - pricing_type: PricingType::SinglePrice, - single_price: "".to_string(), - tiered_prices: vec![("1".to_string(), "".to_string())], + pricing_type: PricingType::from(schedule), + single_price, + tiered_prices, status: SetTokenPriceStatus::NotStarted, error_message: None, app_context: app_context.clone(), diff --git a/src/ui/tokens/tokens_screen/my_tokens.rs b/src/ui/tokens/tokens_screen/my_tokens.rs index 9bf36dae0..f9f1056a4 100644 --- a/src/ui/tokens/tokens_screen/my_tokens.rs +++ b/src/ui/tokens/tokens_screen/my_tokens.rs @@ -901,6 +901,7 @@ impl TokensScreen { Screen::SetTokenPriceScreen( SetTokenPriceScreen::new( info, + None, &self.app_context, ), ), From a8e31dfac203902ec45c9dd7ca58ecedcbcff933 Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Wed, 23 Jul 2025 18:11:28 +0200 Subject: [PATCH 02/11] chore: apply review feedback --- .../group_actions_screen.rs | 9 ++- src/ui/mod.rs | 2 +- src/ui/tokens/set_token_price_screen.rs | 56 ++++++++++--------- src/ui/tokens/tokens_screen/my_tokens.rs | 1 - 4 files changed, 35 insertions(+), 33 deletions(-) diff --git a/src/ui/contracts_documents/group_actions_screen.rs b/src/ui/contracts_documents/group_actions_screen.rs index ca6315fe6..ce983014d 100644 --- a/src/ui/contracts_documents/group_actions_screen.rs +++ b/src/ui/contracts_documents/group_actions_screen.rs @@ -417,11 +417,10 @@ impl GroupActionsScreen { AppAction::AddScreen(Screen::UpdateTokenConfigScreen(Box::new(update_screen))); } TokenEvent::ChangePriceForDirectPurchase(schedule, note_opt) => { - let mut change_price_screen = SetTokenPriceScreen::new( - identity_token_info, - schedule.clone(), - &self.app_context, - ); + let mut change_price_screen = + SetTokenPriceScreen::new(identity_token_info, &self.app_context) + .with_schedule(schedule.clone()); + change_price_screen.group_action_id = Some(action_id); change_price_screen.token_pricing_schedule = format!("{:?}", schedule); change_price_screen.public_note = note_opt.clone(); diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 645ba9c9c..b7fb41094 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -411,7 +411,7 @@ impl ScreenType { PurchaseTokenScreen::new(identity_token_info.clone(), app_context), ), ScreenType::SetTokenPriceScreen(identity_token_info) => Screen::SetTokenPriceScreen( - SetTokenPriceScreen::new(identity_token_info.clone(), None, app_context), + SetTokenPriceScreen::new(identity_token_info.clone(), app_context), ), } } diff --git a/src/ui/tokens/set_token_price_screen.rs b/src/ui/tokens/set_token_price_screen.rs index ccd553230..06d9e037a 100644 --- a/src/ui/tokens/set_token_price_screen.rs +++ b/src/ui/tokens/set_token_price_screen.rs @@ -82,9 +82,9 @@ pub struct SetTokenPriceScreen { pub token_pricing_schedule: String, /// Token pricing schedule to use; if None, we will remove the pricing schedule - pricing_type: PricingType, - single_price: String, - tiered_prices: Vec<(String, String)>, + pub pricing_type: PricingType, + pub single_price: String, + pub tiered_prices: Vec<(String, String)>, status: SetTokenPriceStatus, error_message: Option, @@ -101,7 +101,7 @@ pub struct SetTokenPriceScreen { } /// 1 Dash = 100,000,000,000 credits -const CREDITS_PER_DASH: Credits = 100_000_000_000; +pub const CREDITS_PER_DASH: Credits = 100_000_000_000; impl SetTokenPriceScreen { /// Converts Dash amount to credits (1 Dash = 100,000,000,000 credits) @@ -131,11 +131,7 @@ impl SetTokenPriceScreen { credits_u64 as Credits } - pub fn new( - identity_token_info: IdentityTokenInfo, - schedule: Option, - app_context: &Arc, - ) -> Self { + pub fn new(identity_token_info: IdentityTokenInfo, app_context: &Arc) -> Self { let possible_key: Option<&IdentityPublicKey> = identity_token_info .identity .identity @@ -239,7 +235,29 @@ impl SetTokenPriceScreen { &mut error_message, ); - let (single_price, tiered_prices) = match &schedule { + Self { + identity_token_info: identity_token_info.clone(), + selected_key: possible_key.cloned(), + public_note: None, + group, + is_unilateral_group_member, + group_action_id: None, + token_pricing_schedule: "".to_string(), + pricing_type: PricingType::RemovePricing, + single_price: "".to_string(), + tiered_prices: vec![("1".to_string(), "".to_string())], + status: SetTokenPriceStatus::NotStarted, + error_message: None, + app_context: app_context.clone(), + show_confirmation_popup: false, + selected_wallet, + wallet_password: String::new(), + show_password: false, + } + } + + pub fn with_schedule(self, token_pricing_schedule: Option) -> Self { + let (single_price, tiered_prices) = match &token_pricing_schedule { Some(TokenPricingSchedule::SinglePrice(price)) => ( // we store price as credits, so convert to Dash for processing (*price as f64 / CREDITS_PER_DASH as f64).to_string(), @@ -260,25 +278,11 @@ impl SetTokenPriceScreen { } None => (String::new(), vec![("1".to_string(), String::new())]), }; - Self { - identity_token_info: identity_token_info.clone(), - selected_key: possible_key.cloned(), - public_note: None, - group, - is_unilateral_group_member, - group_action_id: None, - token_pricing_schedule: "".to_string(), - pricing_type: PricingType::from(schedule), + pricing_type: PricingType::from(token_pricing_schedule), single_price, tiered_prices, - status: SetTokenPriceStatus::NotStarted, - error_message: None, - app_context: app_context.clone(), - show_confirmation_popup: false, - selected_wallet, - wallet_password: String::new(), - show_password: false, + ..self } } diff --git a/src/ui/tokens/tokens_screen/my_tokens.rs b/src/ui/tokens/tokens_screen/my_tokens.rs index f9f1056a4..9bf36dae0 100644 --- a/src/ui/tokens/tokens_screen/my_tokens.rs +++ b/src/ui/tokens/tokens_screen/my_tokens.rs @@ -901,7 +901,6 @@ impl TokensScreen { Screen::SetTokenPriceScreen( SetTokenPriceScreen::new( info, - None, &self.app_context, ), ), From 7314610407e807c26af40659b2d41ab8f4d278fa Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Thu, 31 Jul 2025 11:57:30 +0200 Subject: [PATCH 03/11] refactor: use token amount input --- src/ui/tokens/set_token_price_screen.rs | 105 +++++++++++++----------- 1 file changed, 55 insertions(+), 50 deletions(-) diff --git a/src/ui/tokens/set_token_price_screen.rs b/src/ui/tokens/set_token_price_screen.rs index 06d9e037a..581b7ad3e 100644 --- a/src/ui/tokens/set_token_price_screen.rs +++ b/src/ui/tokens/set_token_price_screen.rs @@ -3,12 +3,15 @@ use crate::app::AppAction; use crate::backend_task::BackendTask; use crate::backend_task::tokens::TokenTask; use crate::context::AppContext; +use crate::model::amount::{Amount, DASH_DECIMAL_PLACES}; use crate::model::wallet::Wallet; +use crate::ui::components::amount_input::AmountInput; use crate::ui::components::left_panel::add_left_panel; use crate::ui::components::styled::island_central_panel; use crate::ui::components::tokens_subscreen_chooser_panel::add_tokens_subscreen_chooser_panel; use crate::ui::components::top_panel::add_top_panel; use crate::ui::components::wallet_unlock::ScreenWithWalletUnlock; +use crate::ui::components::{Component, ComponentResponse}; use crate::ui::contracts_documents::group_actions_screen::GroupActionsScreen; use crate::ui::helpers::{TransactionType, add_identity_key_chooser}; use crate::ui::identities::get_selected_wallet; @@ -83,7 +86,11 @@ pub struct SetTokenPriceScreen { pub token_pricing_schedule: String, /// Token pricing schedule to use; if None, we will remove the pricing schedule pub pricing_type: PricingType, - pub single_price: String, + + // AmountInput components for pricing - following the design pattern + single_price_amount: Option, + single_price_input: Option, + pub tiered_prices: Vec<(String, String)>, status: SetTokenPriceStatus, error_message: Option, @@ -244,7 +251,8 @@ impl SetTokenPriceScreen { group_action_id: None, token_pricing_schedule: "".to_string(), pricing_type: PricingType::RemovePricing, - single_price: "".to_string(), + single_price_amount: None, + single_price_input: None, tiered_prices: vec![("1".to_string(), "".to_string())], status: SetTokenPriceStatus::NotStarted, error_message: None, @@ -257,12 +265,11 @@ impl SetTokenPriceScreen { } pub fn with_schedule(self, token_pricing_schedule: Option) -> Self { - let (single_price, tiered_prices) = match &token_pricing_schedule { - Some(TokenPricingSchedule::SinglePrice(price)) => ( - // we store price as credits, so convert to Dash for processing - (*price as f64 / CREDITS_PER_DASH as f64).to_string(), - vec![("1".to_string(), "".to_string())], - ), + let (single_price_amount, tiered_prices) = match &token_pricing_schedule { + Some(TokenPricingSchedule::SinglePrice(price)) => { + let amount = Amount::new(*price, DASH_DECIMAL_PLACES).with_unit_name("DASH"); + (Some(amount), vec![("1".to_string(), "".to_string())]) + } Some(TokenPricingSchedule::SetPrices(prices)) => { let tiered_prices = prices .iter() @@ -274,13 +281,13 @@ impl SetTokenPriceScreen { }) .collect::>(); - (String::new(), tiered_prices) + (None, tiered_prices) } - None => (String::new(), vec![("1".to_string(), String::new())]), + None => (None, vec![("1".to_string(), String::new())]), }; Self { pricing_type: PricingType::from(token_pricing_schedule), - single_price, + single_price_amount, tiered_prices, ..self } @@ -312,28 +319,35 @@ impl SetTokenPriceScreen { match self.pricing_type { PricingType::SinglePrice => { ui.label("Set a fixed price per token:"); - ui.horizontal(|ui| { - ui.label("Price per token (Dash):"); - ui.text_edit_singleline(&mut self.single_price); + + // Lazy initialization of AmountInput following the design pattern + let single_price_input = self.single_price_input.get_or_insert_with(|| { + let initial_amount = self + .single_price_amount + .as_ref() + .cloned() + .unwrap_or_else(|| Amount::new_dash(0.0)); + AmountInput::new(initial_amount) + .label("Price per token:") + .hint_text("Enter price in Dash") + .min_amount(Some(1)) // Minimum 1 credit (very small amount) }); - // Show preview - if !self.single_price.is_empty() { - if let Ok(price) = self.single_price.parse::() { - if price > 0.0 { - ui.add_space(5.0); - let credits = Self::dash_to_credits(price); - ui.colored_label( - Color32::DARK_GREEN, - format!("Price: {} Dash per token ({} credits)", price, credits), - ); - } else { - ui.colored_label(Color32::DARK_RED, "X Price must be greater than 0"); - } - } else { + let response = single_price_input.show(ui); + + // Update the domain data if there's a valid change + if response.inner.has_changed() && response.inner.is_valid() { + self.single_price_amount = response.inner.changed_value().clone(); + } + + // Show validation preview + if let Some(amount) = &self.single_price_amount { + if amount.value() > 0 { + ui.add_space(5.0); + let credits = amount.value(); ui.colored_label( - Color32::DARK_RED, - "X Invalid price - must be a positive number", + Color32::DARK_GREEN, + format!("Price: {} per token ({} credits)", amount, credits), ); } } @@ -509,19 +523,14 @@ impl SetTokenPriceScreen { fn create_pricing_schedule(&self) -> Result, String> { match self.pricing_type { PricingType::RemovePricing => Ok(None), - PricingType::SinglePrice => { - if self.single_price.trim().is_empty() { - return Err("Please enter a price".to_string()); + PricingType::SinglePrice => match &self.single_price_amount { + Some(amount) if amount.value() > 0 => { + let credits_price = amount.value(); + Ok(Some(TokenPricingSchedule::SinglePrice(credits_price))) } - match self.single_price.trim().parse::() { - Ok(dash_price) if dash_price > 0.0 => { - let credits_price = Self::dash_to_credits(dash_price); - Ok(Some(TokenPricingSchedule::SinglePrice(credits_price))) - } - Ok(_) => Err("Price must be greater than 0".to_string()), - Err(_) => Err("Invalid price - must be a positive number".to_string()), - } - } + Some(_) => Err("Price must be greater than 0".to_string()), + None => Err("Please enter a price".to_string()), + }, PricingType::TieredPricing => { let mut map = std::collections::BTreeMap::new(); @@ -600,10 +609,10 @@ impl SetTokenPriceScreen { ui.label("This will make the token unavailable for direct purchase."); } PricingType::SinglePrice => { - if let Ok(dash_price) = self.single_price.trim().parse::() { + if let Some(amount) = &self.single_price_amount { ui.label(format!( - "Are you sure you want to set a fixed price of {} Dash per token?", - dash_price + "Are you sure you want to set a fixed price of {} per token?", + amount )); } } @@ -988,11 +997,7 @@ impl ScreenLike for SetTokenPriceScreen { let can_proceed = match self.pricing_type { PricingType::RemovePricing => true, PricingType::SinglePrice => { - if let Ok(price) = self.single_price.trim().parse::() { - price > 0.0 - } else { - false - } + self.single_price_amount.is_some() }, PricingType::TieredPricing => { self.tiered_prices.iter().any(|(amount, price)| { From edcf5cfd97b6398e6739a3f28eeb7cd9419b1da9 Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Thu, 31 Jul 2025 12:17:22 +0200 Subject: [PATCH 04/11] chore: update AmountInput --- src/ui/components/amount_input.rs | 42 ++++++++++++++++++------- src/ui/components/component_trait.rs | 9 ++++++ src/ui/identities/transfer_screen.rs | 6 ++-- src/ui/identities/withdraw_screen.rs | 4 +-- src/ui/tokens/transfer_tokens_screen.rs | 4 +-- 5 files changed, 46 insertions(+), 19 deletions(-) diff --git a/src/ui/components/amount_input.rs b/src/ui/components/amount_input.rs index 8ef235d44..16b797b65 100644 --- a/src/ui/components/amount_input.rs +++ b/src/ui/components/amount_input.rs @@ -118,7 +118,7 @@ impl AmountInput { show_max_button: false, desired_width: None, show_validation_errors: true, // Default to showing validation errors - changed: false, + changed: true, // Start as changed to force initial validation } } @@ -135,13 +135,22 @@ impl AmountInput { self.decimal_places } + /// Sets decimal places for the input, preserving displayed value. Intristic value will be multiplied by 10^n, + /// where n is difference between new and old decimal places. + pub fn set_decimal_places(&mut self, decimal_places: u8) -> &mut Self { + self.decimal_places = decimal_places; + self.changed = true; + + self + } + /// Gets the unit name this input is configured for. pub fn unit_name(&self) -> Option<&str> { self.unit_name.as_deref() } /// Sets the label for the input field. - pub fn label>(mut self, label: T) -> Self { + pub fn with_label>(mut self, label: T) -> Self { self.label = Some(label.into()); self } @@ -154,7 +163,7 @@ impl AmountInput { } /// Sets the hint text for the input field. - pub fn hint_text>(mut self, hint_text: T) -> Self { + pub fn with_hint_text>(mut self, hint_text: T) -> Self { self.hint_text = Some(hint_text.into()); self } @@ -167,7 +176,7 @@ impl AmountInput { /// Sets the maximum amount allowed. If provided, a "Max" button will be shown /// when `show_max_button` is true. - pub fn max_amount(mut self, max_amount: Option) -> Self { + pub fn with_max_amount(mut self, max_amount: Option) -> Self { self.max_amount = max_amount; self } @@ -181,7 +190,7 @@ impl AmountInput { /// Sets the minimum amount allowed. Defaults to 1 (must be greater than zero). /// Set to Some(0) to allow zero amounts, or None to disable minimum validation. - pub fn min_amount(mut self, min_amount: Option) -> Self { + pub fn with_min_amount(mut self, min_amount: Option) -> Self { self.min_amount = min_amount; self } @@ -193,7 +202,7 @@ impl AmountInput { } /// Whether to show a "Max" button that sets the amount to the maximum. - pub fn max_button(mut self, show: bool) -> Self { + pub fn with_max_button(mut self, show: bool) -> Self { self.show_max_button = show; self } @@ -205,7 +214,7 @@ impl AmountInput { } /// Sets the desired width of the input field. - pub fn desired_width(mut self, width: f32) -> Self { + pub fn with_desired_width(mut self, width: f32) -> Self { self.desired_width = Some(width); self } @@ -343,6 +352,15 @@ impl Component for AmountInput { fn show(&mut self, ui: &mut Ui) -> InnerResponse { AmountInput::show_internal(self, ui) } + + fn current_value(&self) -> Option { + // Validate the current amount string and return the parsed amount + match self.validate_amount() { + Ok(Some(amount)) => Some(amount), + Ok(None) => None, // Empty input + Err(_) => None, // Invalid input returns None + } + } } #[cfg(test)] @@ -382,15 +400,15 @@ mod tests { assert_eq!(input.min_amount, Some(1)); // Custom minimum - let input = AmountInput::new(Amount::new(0, 8)).min_amount(Some(1000)); + let input = AmountInput::new(Amount::new(0, 8)).with_min_amount(Some(1000)); assert_eq!(input.min_amount, Some(1000)); // Allow zero - let input = AmountInput::new(Amount::new(0, 8)).min_amount(Some(0)); + let input = AmountInput::new(Amount::new(0, 8)).with_min_amount(Some(0)); assert_eq!(input.min_amount, Some(0)); // No minimum - let input = AmountInput::new(Amount::new(0, 8)).min_amount(None); + let input = AmountInput::new(Amount::new(0, 8)).with_min_amount(None); assert_eq!(input.min_amount, None); } @@ -478,8 +496,8 @@ mod tests { fn test_min_max_validation() { let amount = Amount::new(0, 2); let mut input = AmountInput::new(amount) - .min_amount(Some(100)) // Minimum 1.00 - .max_amount(Some(10000)); // Maximum 100.00 + .with_min_amount(Some(100)) // Minimum 1.00 + .with_max_amount(Some(10000)); // Maximum 100.00 // Test amount below minimum input.amount_str = "0.50".to_string(); // 50 (below min of 100) diff --git a/src/ui/components/component_trait.rs b/src/ui/components/component_trait.rs index 199d87e2c..26b0d6b28 100644 --- a/src/ui/components/component_trait.rs +++ b/src/ui/components/component_trait.rs @@ -92,4 +92,13 @@ pub trait Component { /// An [`InnerResponse`] containing the component's response data in [`InnerResponse::inner`] field. /// [`InnerResponse::inner`] should implement [`ComponentResponse`] trait. fn show(&mut self, ui: &mut Ui) -> InnerResponse; + + /// Returns the current value of the component. + /// + /// This method is an equivalent of binding some variable using [`ComponentResponse::update()`]. + /// + /// ## See also + /// + /// See [`ComponentResponse::update`]. + fn current_value(&self) -> Option; } diff --git a/src/ui/identities/transfer_screen.rs b/src/ui/identities/transfer_screen.rs index 4240337b1..2a0d330f5 100644 --- a/src/ui/identities/transfer_screen.rs +++ b/src/ui/identities/transfer_screen.rs @@ -115,9 +115,9 @@ impl TransferScreen { let amount_input = self.amount_input.get_or_insert_with(|| { AmountInput::new(Amount::new_dash(0.0)) - .label("Amount:") - .max_button(true) - .max_amount(Some(max_amount_credits)) + .with_label("Amount:") + .with_max_button(true) + .with_max_amount(Some(max_amount_credits)) }); // Check if input should be disabled when operation is in progress diff --git a/src/ui/identities/withdraw_screen.rs b/src/ui/identities/withdraw_screen.rs index 041daa70e..75e96342a 100644 --- a/src/ui/identities/withdraw_screen.rs +++ b/src/ui/identities/withdraw_screen.rs @@ -105,8 +105,8 @@ impl WithdrawalScreen { // Lazy initialization with basic configuration let amount_input = self.withdrawal_amount_input.get_or_insert_with(|| { AmountInput::new(Amount::new_dash(0.0)) - .label("Amount:") - .max_button(true) + .with_label("Amount:") + .with_max_button(true) }); // Check if input should be disabled when operation is in progress diff --git a/src/ui/tokens/transfer_tokens_screen.rs b/src/ui/tokens/transfer_tokens_screen.rs index e35d5b736..e8a01d1d8 100644 --- a/src/ui/tokens/transfer_tokens_screen.rs +++ b/src/ui/tokens/transfer_tokens_screen.rs @@ -120,8 +120,8 @@ impl TransferTokensScreen { .as_ref() .unwrap_or(&Amount::from(&self.identity_token_balance)), ) - .label("Amount:") - .max_button(true), + .with_label("Amount:") + .with_max_button(true), ); self.amount_input From 09dbacfda9463dbc348d82195b033232fff96d36 Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Thu, 31 Jul 2025 12:58:50 +0200 Subject: [PATCH 05/11] chore: tiered pricing --- src/backend_task/system_task/mod.rs | 2 +- src/ui/tokens/set_token_price_screen.rs | 247 +++++++++++------------ src/ui/tokens/tokens_screen/my_tokens.rs | 7 +- 3 files changed, 118 insertions(+), 138 deletions(-) diff --git a/src/backend_task/system_task/mod.rs b/src/backend_task/system_task/mod.rs index d7a6383d2..2999b43fb 100644 --- a/src/backend_task/system_task/mod.rs +++ b/src/backend_task/system_task/mod.rs @@ -49,7 +49,7 @@ impl AppContext { theme_mode: ThemeMode, ) -> Result { let _guard = self.invalidate_settings_cache(); - + self.db .update_theme_preference(theme_mode) .map_err(|e| e.to_string())?; diff --git a/src/ui/tokens/set_token_price_screen.rs b/src/ui/tokens/set_token_price_screen.rs index 581b7ad3e..bef1e976e 100644 --- a/src/ui/tokens/set_token_price_screen.rs +++ b/src/ui/tokens/set_token_price_screen.rs @@ -91,7 +91,8 @@ pub struct SetTokenPriceScreen { single_price_amount: Option, single_price_input: Option, - pub tiered_prices: Vec<(String, String)>, + // Tiered pricing with AmountInput components + pub tiered_prices: Vec<(Option, Option)>, // (amount_input, price_input) status: SetTokenPriceStatus, error_message: Option, @@ -111,33 +112,6 @@ pub struct SetTokenPriceScreen { pub const CREDITS_PER_DASH: Credits = 100_000_000_000; impl SetTokenPriceScreen { - /// Converts Dash amount to credits (1 Dash = 100,000,000,000 credits) - /// - /// ## Panics - /// - /// This function will panic if the conversion fails, which should not happen under normal circumstances - fn dash_to_credits(dash_amount: f64) -> Credits { - let credits_f64 = dash_amount * CREDITS_PER_DASH as f64; - - if credits_f64 < 0.0 || !credits_f64.is_finite() || credits_f64 > u64::MAX as f64 { - panic!( - "Dash amount {} after conversion to credits is not in a valid range: 0 <= {} <= {}", - dash_amount, - credits_f64, - u64::MAX - ); - } - - // Round to nearest integer to handle floating point precision issues - let credits_u64 = if credits_f64 > ((u64::MAX - 1) as f64) { - credits_f64.floor() as u64 - } else { - credits_f64.round() as u64 - }; - - credits_u64 as Credits - } - pub fn new(identity_token_info: IdentityTokenInfo, app_context: &Arc) -> Self { let possible_key: Option<&IdentityPublicKey> = identity_token_info .identity @@ -253,7 +227,7 @@ impl SetTokenPriceScreen { pricing_type: PricingType::RemovePricing, single_price_amount: None, single_price_input: None, - tiered_prices: vec![("1".to_string(), "".to_string())], + tiered_prices: vec![(None, None)], status: SetTokenPriceStatus::NotStarted, error_message: None, app_context: app_context.clone(), @@ -268,23 +242,34 @@ impl SetTokenPriceScreen { let (single_price_amount, tiered_prices) = match &token_pricing_schedule { Some(TokenPricingSchedule::SinglePrice(price)) => { let amount = Amount::new(*price, DASH_DECIMAL_PLACES).with_unit_name("DASH"); - (Some(amount), vec![("1".to_string(), "".to_string())]) + (Some(amount), vec![(None, None)]) } Some(TokenPricingSchedule::SetPrices(prices)) => { let tiered_prices = prices .iter() .map(|(amount, price)| { - ( - amount.to_string(), - (*price as f64 / CREDITS_PER_DASH as f64).to_string(), - ) + // Create amount input for token threshold + + let amount_input = AmountInput::new(Amount::from_token( + *amount, + &self.identity_token_info, + )) + .with_hint_text("Token amount threshold"); + + // Create price input for Dash pricing + let price = Amount::new(*price, DASH_DECIMAL_PLACES).with_unit_name("DASH"); + let price_input = AmountInput::new(price) + .with_hint_text("Enter price in Dash") + .with_min_amount(Some(1)); + (Some(amount_input), Some(price_input)) }) .collect::>(); (None, tiered_prices) } - None => (None, vec![("1".to_string(), String::new())]), + None => (None, vec![(None, None)]), }; + Self { pricing_type: PricingType::from(token_pricing_schedule), single_price_amount, @@ -328,9 +313,9 @@ impl SetTokenPriceScreen { .cloned() .unwrap_or_else(|| Amount::new_dash(0.0)); AmountInput::new(initial_amount) - .label("Price per token:") - .hint_text("Enter price in Dash") - .min_amount(Some(1)) // Minimum 1 credit (very small amount) + .with_label("Price per token:") + .with_hint_text("Enter price in Dash") + .with_min_amount(Some(1)) // Minimum 1 credit (very small amount) }); let response = single_price_input.show(ui); @@ -397,50 +382,44 @@ impl SetTokenPriceScreen { }); }) .body(|mut body| { - for (i, (amount, price)) in self.tiered_prices.iter_mut().enumerate() { - body.row(25.0, |mut row| { + for i in 0..self.tiered_prices.len() { + body.row(30.0, |mut row| { row.col(|ui| { if i == 0 { - // First tier is hardcoded to 1 token - ui.label("1"); - *amount = "1".to_string(); // Ensure it's always 1 + // First tier is hardcoded to 1 token - create AmountInput with value 1 + let amount_input = + self.tiered_prices[i].0.get_or_insert_with(|| { + AmountInput::new(Amount::from_token( + 1, + &self.identity_token_info, + )) + .with_hint_text("Token amount threshold") + }); + amount_input.show(ui); + // Make sure it's always 1 - we could disable editing or show as read-only } else { - let dark_mode = ui.ctx().style().visuals.dark_mode; - ui.add( - egui::TextEdit::singleline(amount) - .hint_text( - RichText::new("100").color(Color32::GRAY), - ) - .desired_width(100.0) - .text_color( - crate::ui::theme::DashColors::text_primary( - dark_mode, - ), - ) - .background_color( - crate::ui::theme::DashColors::input_background( - dark_mode, - ), - ), - ); + // Other tiers use AmountInput for token amounts + let amount_input = + self.tiered_prices[i].0.get_or_insert_with(|| { + AmountInput::new(Amount::from_token( + 0, + &self.identity_token_info, + )) + .with_hint_text("Token amount threshold") + }); + amount_input.show(ui); } }); row.col(|ui| { - let dark_mode = ui.ctx().style().visuals.dark_mode; - ui.add( - egui::TextEdit::singleline(price) - .hint_text(RichText::new("50").color(Color32::GRAY)) - .desired_width(120.0) - .text_color(crate::ui::theme::DashColors::text_primary( - dark_mode, - )) - .background_color( - crate::ui::theme::DashColors::input_background( - dark_mode, - ), - ), - ); - ui.label(" Dash"); + // Use AmountInput for price with lazy initialization + let price_input = + self.tiered_prices[i].1.get_or_insert_with(|| { + AmountInput::new(Amount::new_dash(0.0)) + .with_hint_text("Enter price in Dash") + .with_min_amount(Some(1)) // Minimum 1 credit + }); + + let _response = price_input.show(ui); }); row.col(|ui| { if can_remove && i > 0 && ui.small_button("X").clicked() { @@ -458,8 +437,8 @@ impl SetTokenPriceScreen { ui.add_space(10.0); ui.horizontal(|ui| { if ui.button("+ Add Tier").clicked() { - // Add empty tier - user will fill in values - self.tiered_prices.push(("".to_string(), "".to_string())); + // Add empty tier with lazy initialization + self.tiered_prices.push((None, None)); } }); @@ -478,19 +457,20 @@ impl SetTokenPriceScreen { let mut valid_tiers = Vec::new(); let mut has_errors = false; - for (amount_str, price_str) in &self.tiered_prices { - if amount_str.trim().is_empty() || price_str.trim().is_empty() { - continue; - } + for (amount_input, price_input) in &self.tiered_prices { + let Some(price) = price_input.as_ref().and_then(|input| input.current_value()) else { + continue; // Skip if no price input is available + }; - match (amount_str.parse::(), price_str.parse::()) { - (Ok(amount), Ok(price)) if price > 0.0 => { - valid_tiers.push((amount, price)); - } - _ => { - has_errors = true; - } - } + let Some(amount_value) = amount_input + .as_ref() + .and_then(|input| input.current_value()) + else { + has_errors = true; + continue; // Skip if amount is invalid + }; + + valid_tiers.push((amount_value, price)); } // Only show preview if there are valid tiers or errors @@ -498,7 +478,7 @@ impl SetTokenPriceScreen { ui.group(|ui| { // Sort tiers by amount if !valid_tiers.is_empty() { - valid_tiers.sort_by_key(|(amount, _)| *amount); + valid_tiers.sort_by_key(|(amount, _)| amount.value()); } if has_errors { @@ -508,9 +488,9 @@ impl SetTokenPriceScreen { if !valid_tiers.is_empty() { ui.colored_label(Color32::DARK_GREEN, "Pricing Structure:"); for (amount, price) in &valid_tiers { - let credits = Self::dash_to_credits(*price); + let credits = price.value(); ui.label(format!( - " - {} or more tokens: {} Dash each ({} credits)", + " - {} or more tokens: {} each ({} credits)", amount, price, credits )); } @@ -534,33 +514,21 @@ impl SetTokenPriceScreen { PricingType::TieredPricing => { let mut map = std::collections::BTreeMap::new(); - for (amount_str, price_str) in &self.tiered_prices { - if amount_str.trim().is_empty() || price_str.trim().is_empty() { + for (amount_input, price_input) in &self.tiered_prices { + let Some(price) = price_input.as_ref().and_then(|input| input.current_value()) + else { continue; - } + }; - let amount = amount_str.trim().parse::().map_err(|_| { - format!( - "Invalid amount '{}' - must be a positive number", - amount_str.trim() - ) - })?; - let dash_price = price_str.trim().parse::().map_err(|_| { - format!( - "Invalid price '{}' - must be a positive number", - price_str.trim() - ) - })?; - - if dash_price <= 0.0 { - return Err(format!( - "Price '{}' must be greater than 0", - price_str.trim() - )); - } + let Some(amount_value) = amount_input + .as_ref() + .and_then(|input| input.current_value()) + else { + continue; + }; - let credits_price = Self::dash_to_credits(dash_price); - map.insert(amount, credits_price); + let amount = amount_value.value(); + map.insert(amount, price.value()); } if map.is_empty() { @@ -619,19 +587,25 @@ impl SetTokenPriceScreen { PricingType::TieredPricing => { ui.label("Are you sure you want to set the following tiered pricing?"); ui.add_space(5.0); - for (amount_str, price_str) in &self.tiered_prices { - if amount_str.trim().is_empty() || price_str.trim().is_empty() { + for (amount_input, price_input) in &self.tiered_prices { + let Some(price) = + price_input.as_ref().and_then(|input| input.current_value()) + else { + continue; // Skip if no price input is available + }; + + let Some(amount_value) = amount_input + .as_ref() + .and_then(|input| input.current_value()) + else { continue; - } - if let (Ok(amount), Ok(dash_price)) = ( - amount_str.trim().parse::(), - price_str.trim().parse::(), - ) { - ui.label(format!( - " - {} or more tokens: {} Dash each", - amount, dash_price - )); - } + }; + + let amount = amount_value.value(); + ui.label(format!( + " - {} or more tokens: {} Dash each", + amount, price + )); } } } @@ -1000,10 +974,17 @@ impl ScreenLike for SetTokenPriceScreen { self.single_price_amount.is_some() }, PricingType::TieredPricing => { - self.tiered_prices.iter().any(|(amount, price)| { - !amount.trim().is_empty() && !price.trim().is_empty() && - amount.trim().parse::().is_ok() && - if let Ok(p) = price.trim().parse::() { p > 0.0 } else { false } + // Check if there's at least one valid tier with both amount and price + self.tiered_prices.iter().any(|(amount_input, price_input)| { + let Some(price) = price_input.as_ref().and_then(|input| input.current_value()) else { + return false; // Skip if no price input is available + }; + + let Some(amount_value) = amount_input.as_ref().and_then(|input| input.current_value()) else { + return false; // Skip if no amount input is available + }; + + amount_value.value() > 0 && price.value() > 0 }) } }; diff --git a/src/ui/tokens/tokens_screen/my_tokens.rs b/src/ui/tokens/tokens_screen/my_tokens.rs index 6b6db3fd3..a7e3a02b2 100644 --- a/src/ui/tokens/tokens_screen/my_tokens.rs +++ b/src/ui/tokens/tokens_screen/my_tokens.rs @@ -523,10 +523,9 @@ impl TokensScreen { .token_configuration .conventions() .plural_form_by_language_code_or_default("en"); - let reward_amount = Amount::new( - explanation.total_amount, - decimal_places, - ).with_unit_name(unit_name); + let reward_amount = + Amount::new(explanation.total_amount, decimal_places) + .with_unit_name(unit_name); ui.label(format!("Total Estimated Rewards: {}", reward_amount)); ui.separator(); From f4c24c9cb4d1f6fd6c1141513c1b89f0d88af8a0 Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Thu, 31 Jul 2025 14:31:00 +0200 Subject: [PATCH 06/11] chore: direct purchase --- src/ui/components/amount_input.rs | 11 ++ src/ui/tokens/direct_token_purchase_screen.rs | 138 +++++++++++++----- 2 files changed, 109 insertions(+), 40 deletions(-) diff --git a/src/ui/components/amount_input.rs b/src/ui/components/amount_input.rs index 16b797b65..1c0562f18 100644 --- a/src/ui/components/amount_input.rs +++ b/src/ui/components/amount_input.rs @@ -162,6 +162,17 @@ impl AmountInput { self } + /// Sets value of the input field. + /// + /// This will update the internal state and mark the component as changed. + pub fn set_value(&mut self, value: Amount) -> &mut Self { + self.amount_str = value.to_string_without_unit(); + self.decimal_places = value.decimal_places(); + self.unit_name = value.unit_name().map(|s| s.to_string()); + self.changed = true; // Mark as changed to trigger validation + self + } + /// Sets the hint text for the input field. pub fn with_hint_text>(mut self, hint_text: T) -> Self { self.hint_text = Some(hint_text.into()); diff --git a/src/ui/tokens/direct_token_purchase_screen.rs b/src/ui/tokens/direct_token_purchase_screen.rs index 8afabb88a..4eb556bca 100644 --- a/src/ui/tokens/direct_token_purchase_screen.rs +++ b/src/ui/tokens/direct_token_purchase_screen.rs @@ -3,6 +3,8 @@ use std::sync::{Arc, RwLock}; use std::time::{SystemTime, UNIX_EPOCH}; use dash_sdk::dpp::data_contract::accessors::v1::DataContractV1Getters; +use dash_sdk::dpp::data_contract::associated_token::token_configuration::accessors::v0::TokenConfigurationV0Getters; +use dash_sdk::dpp::data_contract::associated_token::token_configuration_convention::accessors::v0::TokenConfigurationConventionV0Getters; use dash_sdk::dpp::fee::Credits; use dash_sdk::dpp::tokens::token_pricing_schedule::TokenPricingSchedule; use eframe::egui::{self, Color32, Context, Ui}; @@ -13,7 +15,10 @@ use crate::app::{AppAction, BackendTasksExecutionMode}; use crate::backend_task::BackendTask; use crate::backend_task::tokens::TokenTask; use crate::context::AppContext; +use crate::model::amount::{Amount, DASH_DECIMAL_PLACES}; use crate::model::wallet::Wallet; +use crate::ui::components::amount_input::AmountInput; +use crate::ui::components::{Component, ComponentResponse}; use crate::ui::components::left_panel::add_left_panel; use crate::ui::components::styled::island_central_panel; use crate::ui::components::tokens_subscreen_chooser_panel::add_tokens_subscreen_chooser_panel; @@ -45,11 +50,12 @@ pub struct PurchaseTokenScreen { pub identity_token_info: IdentityTokenInfo, selected_key: Option, - // Specific to this transition - amount_to_purchase: String, - total_agreed_price: String, + // Specific to this transition - using AmountInput components following design pattern + amount_to_purchase_input: Option, + amount_to_purchase_value: Option, + total_agreed_price_input: Option, fetched_pricing_schedule: Option, - calculated_price: Option, + calculated_price_credits: Option, pricing_fetch_attempted: bool, /// Screen stuff @@ -89,10 +95,11 @@ impl PurchaseTokenScreen { Self { identity_token_info, selected_key: possible_key, - amount_to_purchase: "".to_string(), - total_agreed_price: "".to_string(), + amount_to_purchase_input: None, + amount_to_purchase_value: None, + total_agreed_price_input: None, fetched_pricing_schedule: None, - calculated_price: None, + calculated_price_credits: None, pricing_fetch_attempted: false, status: PurchaseTokensStatus::NotStarted, error_message: None, @@ -104,18 +111,34 @@ impl PurchaseTokenScreen { } } - /// Renders a text input for the user to specify an amount to purchase + /// Returns the total agreed price in credits, or 0. + fn get_total_agreed_price(&self) ->Option { + self.total_agreed_price_input + .as_ref() + .and_then(|amount| amount.current_value()) + } + + /// Renders AmountInput components for the user to specify an amount to purchase fn render_amount_input(&mut self, ui: &mut Ui) -> AppAction { let mut action = AppAction::None; ui.horizontal(|ui| { - ui.label("Amount to Purchase:"); - let response = ui.text_edit_singleline(&mut self.amount_to_purchase); - // When amount changes, recalculate the price if we have pricing schedule - if response.changed() { + // Use AmountInput for token amount with lazy initialization + let amount_input = self.amount_to_purchase_input.get_or_insert_with(|| { + AmountInput::new(Amount::new(0, self.identity_token_info.token_config.conventions().decimals()).with_unit_name(&self.identity_token_info.token_alias)) + .with_label("Amount to Purchase:") + .with_hint_text("Enter token amount to purchase") + .with_min_amount(Some(1)) + }); + + let response = amount_input.show(ui); + response.inner.update(&mut self.amount_to_purchase_value); + + // When amount changes, update domain data and recalculate the price + if response.inner.has_changed(){ self.recalculate_price(); - } + } // Fetch pricing button if ui.button("Fetch Token Price").clicked() { @@ -143,13 +166,24 @@ impl PurchaseTokenScreen { ui.add_space(5.0); ui.label("Current pricing:"); match pricing_schedule { - TokenPricingSchedule::SinglePrice(price) => { - ui.label(format!(" Fixed price: {} credits per token", price)); + TokenPricingSchedule::SinglePrice(price_credits) => { + let price = + Amount::new(*price_credits, DASH_DECIMAL_PLACES).with_unit_name("DASH"); + ui.label(format!( + " Fixed price: {} ({} credits) per token", + price, price_credits + )); } TokenPricingSchedule::SetPrices(tiers) => { ui.label(" Tiered pricing:"); - for (amount, price) in tiers { - ui.label(format!(" {} tokens: {} credits each", amount, price)); + for (amount_value, price_credits) in tiers { + let amount = Amount::from_token(*amount_value, &self.identity_token_info); + let price = + Amount::new(*price_credits, DASH_DECIMAL_PLACES).with_unit_name("DASH"); + ui.label(format!( + " {} tokens: {} ({} credits) each", + amount, price, price_credits + )); } } } @@ -160,10 +194,11 @@ impl PurchaseTokenScreen { /// Recalculates the total price based on amount and pricing schedule fn recalculate_price(&mut self) { - if let (Some(pricing_schedule), Ok(amount)) = ( + if let (Some(pricing_schedule), Some(amount_value)) = ( &self.fetched_pricing_schedule, - self.amount_to_purchase.parse::(), + &self.amount_to_purchase_value, ) { + let amount = amount_value.value(); let price_per_token = match pricing_schedule { TokenPricingSchedule::SinglePrice(price) => *price, TokenPricingSchedule::SetPrices(tiers) => { @@ -179,10 +214,23 @@ impl PurchaseTokenScreen { }; let total_price = amount.saturating_mul(price_per_token); - self.calculated_price = Some(total_price); - self.total_agreed_price = total_price.to_string(); + self.calculated_price_credits = Some(total_price); + + // Update the total agreed price AmountInput with calculated price + let price_amount = Amount::new(total_price, DASH_DECIMAL_PLACES).with_unit_name("DASH"); + + + // Update or create the price input component + let total_price_input = self.total_agreed_price_input.get_or_insert_with(|| { + AmountInput::new(price_amount.clone()) + .with_label("Total agreed price:") + .with_hint_text("Calculated total price in Dash") + }); + total_price_input.set_value(price_amount); } else { - self.calculated_price = None; + // hide and reset total agreed price when amount is invalid + self.total_agreed_price_input = None; + self.calculated_price_credits = None; } } @@ -206,27 +254,26 @@ impl PurchaseTokenScreen { ) .show(ui.ctx(), |ui| { // Validate user input - let amount_ok = self.amount_to_purchase.parse::().ok(); - if amount_ok.is_none() { + let amount_value = self.amount_to_purchase_value.as_ref(); + let Some(amount) = amount_value else { self.error_message = Some("Please enter a valid amount.".into()); self.status = PurchaseTokensStatus::ErrorMessage("Invalid amount".into()); self.show_confirmation_popup = false; return; - } + }; - let total_agreed_price_ok: Option = - self.total_agreed_price.parse::().ok(); - if total_agreed_price_ok.is_none() { + let Some( total_price) = self.get_total_agreed_price() else{ self.error_message = Some("Please enter a valid total agreed price.".into()); self.status = PurchaseTokensStatus::ErrorMessage("Invalid total agreed price".into()); self.show_confirmation_popup = false; return; - } + }; + ui.label(format!( - "Are you sure you want to purchase {} token(s) for {} Credits?", - self.amount_to_purchase, self.total_agreed_price + "Are you sure you want to purchase {} token(s) for {} ({} Credits)?", + amount, total_price, total_price.value() )); ui.add_space(10.0); @@ -250,9 +297,8 @@ impl PurchaseTokenScreen { ), token_position: self.identity_token_info.token_position, signing_key: self.selected_key.clone().expect("Expected a key"), - amount: amount_ok.expect("Expected a valid amount"), - total_agreed_price: total_agreed_price_ok - .expect("Expected a valid total agreed price"), + amount: amount.value(), + total_agreed_price: total_price.value(), })), BackendTask::TokenTask(Box::new(TokenTask::QueryMyTokenBalances)), ], @@ -474,12 +520,22 @@ impl ScreenLike for PurchaseTokenScreen { ui.add_space(10.0); - // Display calculated price - if let Some(calculated_price) = self.calculated_price { + // Display calculated price and total agreed price input + if let Some(calculated_price_credits) = self.calculated_price_credits { ui.group(|ui| { ui.heading("Calculated total price:"); - ui.label(format!("{} credits", calculated_price)); + let dash_amount = Amount::new(calculated_price_credits, DASH_DECIMAL_PLACES) + .with_unit_name("DASH"); + ui.label(format!("{} DASH ({} credits)",dash_amount, calculated_price_credits)); ui.label("Note: This is the calculated price based on the current pricing schedule."); + + ui.add_space(10.0); + ui.label("Total agreed price (you can adjust if needed):"); + + // Show the total agreed price AmountInput + if let Some(ref mut price_input) = self.total_agreed_price_input { + price_input.show(ui); + } }); } else if self.fetched_pricing_schedule.is_some() { ui.colored_label( @@ -494,9 +550,11 @@ impl ScreenLike for PurchaseTokenScreen { ui.separator(); ui.add_space(10.0); - // Purchase button (disabled if no pricing is available) - let can_purchase = - self.fetched_pricing_schedule.is_some() && self.calculated_price.is_some(); + // Purchase button (disabled if no valid amounts are available) + let can_purchase = self.fetched_pricing_schedule.is_some() + && self.calculated_price_credits.unwrap_or_default() > 0 + && self.amount_to_purchase_value.as_ref().map(|v|v.value()).unwrap_or_default() > 0 + && self.get_total_agreed_price().map(|v|v.value()).unwrap_or_default() > 0; let purchase_text = "Purchase".to_string(); if can_purchase { From aabcf380e719ffe292a8399c9e2add0e8beec009 Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Thu, 31 Jul 2025 14:36:17 +0200 Subject: [PATCH 07/11] chore: remove total agreed price --- src/ui/tokens/direct_token_purchase_screen.rs | 81 ++++++++----------- 1 file changed, 34 insertions(+), 47 deletions(-) diff --git a/src/ui/tokens/direct_token_purchase_screen.rs b/src/ui/tokens/direct_token_purchase_screen.rs index 4eb556bca..b901d0b17 100644 --- a/src/ui/tokens/direct_token_purchase_screen.rs +++ b/src/ui/tokens/direct_token_purchase_screen.rs @@ -18,12 +18,12 @@ use crate::context::AppContext; use crate::model::amount::{Amount, DASH_DECIMAL_PLACES}; use crate::model::wallet::Wallet; use crate::ui::components::amount_input::AmountInput; -use crate::ui::components::{Component, ComponentResponse}; use crate::ui::components::left_panel::add_left_panel; use crate::ui::components::styled::island_central_panel; use crate::ui::components::tokens_subscreen_chooser_panel::add_tokens_subscreen_chooser_panel; use crate::ui::components::top_panel::add_top_panel; use crate::ui::components::wallet_unlock::ScreenWithWalletUnlock; +use crate::ui::components::{Component, ComponentResponse}; use crate::ui::helpers::{TransactionType, add_identity_key_chooser}; use crate::ui::identities::get_selected_wallet; use crate::ui::identities::keys::add_key_screen::AddKeyScreen; @@ -53,7 +53,6 @@ pub struct PurchaseTokenScreen { // Specific to this transition - using AmountInput components following design pattern amount_to_purchase_input: Option, amount_to_purchase_value: Option, - total_agreed_price_input: Option, fetched_pricing_schedule: Option, calculated_price_credits: Option, pricing_fetch_attempted: bool, @@ -97,7 +96,6 @@ impl PurchaseTokenScreen { selected_key: possible_key, amount_to_purchase_input: None, amount_to_purchase_value: None, - total_agreed_price_input: None, fetched_pricing_schedule: None, calculated_price_credits: None, pricing_fetch_attempted: false, @@ -111,34 +109,35 @@ impl PurchaseTokenScreen { } } - /// Returns the total agreed price in credits, or 0. - fn get_total_agreed_price(&self) ->Option { - self.total_agreed_price_input - .as_ref() - .and_then(|amount| amount.current_value()) - } - /// Renders AmountInput components for the user to specify an amount to purchase fn render_amount_input(&mut self, ui: &mut Ui) -> AppAction { let mut action = AppAction::None; ui.horizontal(|ui| { - // Use AmountInput for token amount with lazy initialization let amount_input = self.amount_to_purchase_input.get_or_insert_with(|| { - AmountInput::new(Amount::new(0, self.identity_token_info.token_config.conventions().decimals()).with_unit_name(&self.identity_token_info.token_alias)) - .with_label("Amount to Purchase:") - .with_hint_text("Enter token amount to purchase") - .with_min_amount(Some(1)) + AmountInput::new( + Amount::new( + 0, + self.identity_token_info + .token_config + .conventions() + .decimals(), + ) + .with_unit_name(&self.identity_token_info.token_alias), + ) + .with_label("Amount to Purchase:") + .with_hint_text("Enter token amount to purchase") + .with_min_amount(Some(1)) }); let response = amount_input.show(ui); response.inner.update(&mut self.amount_to_purchase_value); // When amount changes, update domain data and recalculate the price - if response.inner.has_changed(){ + if response.inner.has_changed() { self.recalculate_price(); - } + } // Fetch pricing button if ui.button("Fetch Token Price").clicked() { @@ -215,21 +214,7 @@ impl PurchaseTokenScreen { let total_price = amount.saturating_mul(price_per_token); self.calculated_price_credits = Some(total_price); - - // Update the total agreed price AmountInput with calculated price - let price_amount = Amount::new(total_price, DASH_DECIMAL_PLACES).with_unit_name("DASH"); - - - // Update or create the price input component - let total_price_input = self.total_agreed_price_input.get_or_insert_with(|| { - AmountInput::new(price_amount.clone()) - .with_label("Total agreed price:") - .with_hint_text("Calculated total price in Dash") - }); - total_price_input.set_value(price_amount); } else { - // hide and reset total agreed price when amount is invalid - self.total_agreed_price_input = None; self.calculated_price_credits = None; } } @@ -262,18 +247,21 @@ impl PurchaseTokenScreen { return; }; - let Some( total_price) = self.get_total_agreed_price() else{ - self.error_message = Some("Please enter a valid total agreed price.".into()); - self.status = - PurchaseTokensStatus::ErrorMessage("Invalid total agreed price".into()); + let Some(total_price_credits) = self.calculated_price_credits else { + self.error_message = Some( + "Cannot calculate total price. Please fetch token pricing first.".into(), + ); + self.status = PurchaseTokensStatus::ErrorMessage("No pricing fetched".into()); self.show_confirmation_popup = false; return; }; + let total_price_dash = + Amount::new(total_price_credits, DASH_DECIMAL_PLACES).with_unit_name("DASH"); ui.label(format!( "Are you sure you want to purchase {} token(s) for {} ({} Credits)?", - amount, total_price, total_price.value() + amount, total_price_dash, total_price_credits )); ui.add_space(10.0); @@ -298,7 +286,7 @@ impl PurchaseTokenScreen { token_position: self.identity_token_info.token_position, signing_key: self.selected_key.clone().expect("Expected a key"), amount: amount.value(), - total_agreed_price: total_price.value(), + total_agreed_price: total_price_credits, })), BackendTask::TokenTask(Box::new(TokenTask::QueryMyTokenBalances)), ], @@ -528,14 +516,9 @@ impl ScreenLike for PurchaseTokenScreen { .with_unit_name("DASH"); ui.label(format!("{} DASH ({} credits)",dash_amount, calculated_price_credits)); ui.label("Note: This is the calculated price based on the current pricing schedule."); - + ui.add_space(10.0); - ui.label("Total agreed price (you can adjust if needed):"); - - // Show the total agreed price AmountInput - if let Some(ref mut price_input) = self.total_agreed_price_input { - price_input.show(ui); - } + }); } else if self.fetched_pricing_schedule.is_some() { ui.colored_label( @@ -551,10 +534,14 @@ impl ScreenLike for PurchaseTokenScreen { ui.add_space(10.0); // Purchase button (disabled if no valid amounts are available) - let can_purchase = self.fetched_pricing_schedule.is_some() + let can_purchase = self.fetched_pricing_schedule.is_some() && self.calculated_price_credits.unwrap_or_default() > 0 - && self.amount_to_purchase_value.as_ref().map(|v|v.value()).unwrap_or_default() > 0 - && self.get_total_agreed_price().map(|v|v.value()).unwrap_or_default() > 0; + && self + .amount_to_purchase_value + .as_ref() + .map(|v| v.value()) + .unwrap_or_default() + > 0; let purchase_text = "Purchase".to_string(); if can_purchase { From e668c5963da16afcf2202d956ed2885db2b67b98 Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Thu, 31 Jul 2025 14:41:50 +0200 Subject: [PATCH 08/11] chore: max amount input defaults to max_credits --- src/ui/components/amount_input.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/ui/components/amount_input.rs b/src/ui/components/amount_input.rs index 1c0562f18..fc0715be5 100644 --- a/src/ui/components/amount_input.rs +++ b/src/ui/components/amount_input.rs @@ -1,5 +1,6 @@ use crate::model::amount::Amount; use crate::ui::components::{Component, ComponentResponse}; +use dash_sdk::dpp::balances::credits::MAX_CREDITS; use dash_sdk::dpp::fee::Credits; use egui::{InnerResponse, Response, TextEdit, Ui, Vec2, WidgetText}; @@ -113,7 +114,7 @@ impl AmountInput { unit_name: amount.unit_name().map(|s| s.to_string()), label: None, hint_text: None, - max_amount: None, + max_amount: Some(MAX_CREDITS), min_amount: Some(1), // Default minimum is 1 (greater than zero) show_max_button: false, desired_width: None, @@ -187,6 +188,8 @@ impl AmountInput { /// Sets the maximum amount allowed. If provided, a "Max" button will be shown /// when `show_max_button` is true. + /// + /// Defaults to [`MAX_CREDITS`](dash_sdk::dpp::balances::credits::MAX_CREDITS). pub fn with_max_amount(mut self, max_amount: Option) -> Self { self.max_amount = max_amount; self @@ -194,6 +197,8 @@ impl AmountInput { /// Sets the maximum amount allowed (mutable reference version). /// Use this for dynamic configuration when the max amount changes at runtime (e.g., balance updates). + /// + /// Defaults to [`MAX_CREDITS`](dash_sdk::dpp::balances::credits::MAX_CREDITS). pub fn set_max_amount(&mut self, max_amount: Option) -> &mut Self { self.max_amount = max_amount; self From 692fcfd6fb61b5a3f5f0e2f678021f3fbbd38f98 Mon Sep 17 00:00:00 2001 From: pauldelucia Date: Wed, 13 Aug 2025 21:07:00 +0700 Subject: [PATCH 09/11] fmt --- src/ui/tokens/set_token_price_screen.rs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/ui/tokens/set_token_price_screen.rs b/src/ui/tokens/set_token_price_screen.rs index 9d6b79ff4..f8c16bbce 100644 --- a/src/ui/tokens/set_token_price_screen.rs +++ b/src/ui/tokens/set_token_price_screen.rs @@ -5,6 +5,7 @@ use crate::backend_task::tokens::TokenTask; use crate::context::AppContext; use crate::model::amount::{Amount, DASH_DECIMAL_PLACES}; use crate::model::wallet::Wallet; +use crate::ui::components::ComponentResponse; use crate::ui::components::amount_input::AmountInput; use crate::ui::components::component_trait::Component; use crate::ui::components::confirmation_dialog::{ConfirmationDialog, ConfirmationStatus}; @@ -13,7 +14,6 @@ use crate::ui::components::styled::island_central_panel; use crate::ui::components::tokens_subscreen_chooser_panel::add_tokens_subscreen_chooser_panel; use crate::ui::components::top_panel::add_top_panel; use crate::ui::components::wallet_unlock::ScreenWithWalletUnlock; -use crate::ui::components::ComponentResponse; use crate::ui::contracts_documents::group_actions_screen::GroupActionsScreen; use crate::ui::helpers::{TransactionType, add_identity_key_chooser}; use crate::ui::identities::get_selected_wallet; @@ -548,13 +548,11 @@ impl SetTokenPriceScreen { fn validate_pricing_configuration(&self) -> Result<(), String> { match self.pricing_type { PricingType::RemovePricing => Ok(()), - PricingType::SinglePrice => { - match &self.single_price_amount { - Some(amount) if amount.value() > 0 => Ok(()), - Some(_) => Err("Price must be greater than 0".to_string()), - None => Err("Please enter a price".to_string()), - } - } + PricingType::SinglePrice => match &self.single_price_amount { + Some(amount) if amount.value() > 0 => Ok(()), + Some(_) => Err("Price must be greater than 0".to_string()), + None => Err("Please enter a price".to_string()), + }, PricingType::TieredPricing => { let mut valid_tiers = 0; From 78a09adc24b14ecb9b94a5dc7dfd71dcf89cf778 Mon Sep 17 00:00:00 2001 From: pauldelucia Date: Wed, 13 Aug 2025 23:02:59 +0700 Subject: [PATCH 10/11] fix --- src/ui/tokens/direct_token_purchase_screen.rs | 136 ++++++++++++++++-- src/ui/tokens/set_token_price_screen.rs | 51 +++++-- 2 files changed, 165 insertions(+), 22 deletions(-) diff --git a/src/ui/tokens/direct_token_purchase_screen.rs b/src/ui/tokens/direct_token_purchase_screen.rs index 79be94ff8..16647908c 100644 --- a/src/ui/tokens/direct_token_purchase_screen.rs +++ b/src/ui/tokens/direct_token_purchase_screen.rs @@ -164,25 +164,30 @@ impl PurchaseTokenScreen { if let Some(pricing_schedule) = &self.fetched_pricing_schedule { ui.add_space(5.0); ui.label("Current pricing:"); + let token_decimals = self + .identity_token_info + .token_config + .conventions() + .decimals(); + let decimal_multiplier = 10u64.pow(token_decimals as u32); + match pricing_schedule { - TokenPricingSchedule::SinglePrice(price_credits) => { + TokenPricingSchedule::SinglePrice(price_per_smallest_unit) => { + // Convert price per smallest unit to price per token for display + let price_per_token = price_per_smallest_unit * decimal_multiplier; let price = - Amount::new(*price_credits, DASH_DECIMAL_PLACES).with_unit_name("DASH"); - ui.label(format!( - " Fixed price: {} ({} credits) per token", - price, price_credits - )); + Amount::new(price_per_token, DASH_DECIMAL_PLACES).with_unit_name("DASH"); + ui.label(format!(" Fixed price: {} per token", price)); } TokenPricingSchedule::SetPrices(tiers) => { ui.label(" Tiered pricing:"); - for (amount_value, price_credits) in tiers { + for (amount_value, price_per_smallest_unit) in tiers { let amount = Amount::from_token(&self.identity_token_info, *amount_value); - let price = - Amount::new(*price_credits, DASH_DECIMAL_PLACES).with_unit_name("DASH"); - ui.label(format!( - " {} tokens: {} ({} credits) each", - amount, price, price_credits - )); + // Convert price per smallest unit to price per token for display + let price_per_token = price_per_smallest_unit * decimal_multiplier; + let price = Amount::new(price_per_token, DASH_DECIMAL_PLACES) + .with_unit_name("DASH"); + ui.label(format!(" {} tokens: {} each", amount, price)); } } } @@ -212,6 +217,8 @@ impl PurchaseTokenScreen { } }; + // The price from Platform is per smallest unit, and amount is in smallest units + // So we just multiply them directly let total_price = amount.saturating_mul(price_per_token); self.calculated_price_credits = Some(total_price); } else { @@ -634,3 +641,106 @@ impl ScreenWithWalletUnlock for PurchaseTokenScreen { self.error_message.as_ref() } } + +#[cfg(test)] +mod tests { + use crate::model::amount::DASH_DECIMAL_PLACES; + + #[test] + fn test_token_pricing_storage_and_calculation() { + // Test how prices should be stored and calculated + + // Case 1: Token with 8 decimals (like the user's case) + let token_decimals_8 = 8u8; + let user_price_per_token_dash = 0.001; // User wants 0.001 DASH per token + let user_price_per_token_credits = + (user_price_per_token_dash * 10f64.powi(DASH_DECIMAL_PLACES as i32)) as u64; + + println!("Test 1 - Token with 8 decimals, price 0.001 DASH per token:"); + println!( + " User enters: {} DASH per token", + user_price_per_token_dash + ); + println!( + " In credits: {} credits per token", + user_price_per_token_credits + ); + + // Platform expects price per smallest unit, not per token + let decimal_divisor_8 = 10u64.pow(token_decimals_8 as u32); + let platform_price_per_smallest_unit = user_price_per_token_credits / decimal_divisor_8; + + println!( + " Platform stores: {} credits per smallest unit", + platform_price_per_smallest_unit + ); + + // When buying 1 token (100,000,000 smallest units) + let tokens_to_buy = 1u64; + let amount_smallest_units = tokens_to_buy * 10u64.pow(token_decimals_8 as u32); + let total_price = amount_smallest_units * platform_price_per_smallest_unit; + + println!( + " Buying {} token ({} smallest units)", + tokens_to_buy, amount_smallest_units + ); + println!( + " Total: {} credits (should be {} credits for 0.001 DASH)", + total_price, user_price_per_token_credits + ); + + assert_eq!( + total_price, user_price_per_token_credits, + "Total should match expected price" + ); + + // Case 2: Token with 2 decimals + let token_decimals_2 = 2u8; + let user_price_2 = 0.1; // 0.1 DASH per token + let user_price_credits_2 = (user_price_2 * 10f64.powi(DASH_DECIMAL_PLACES as i32)) as u64; + + let divisor_2 = 10u64.pow(token_decimals_2 as u32); + let platform_price_2 = user_price_credits_2 / divisor_2; + + // Buy 5 tokens + let amount_2 = 5 * 10u64.pow(token_decimals_2 as u32); // 500 smallest units + let total_2 = amount_2 * platform_price_2; + + println!("\nTest 2 - Token with 2 decimals, 5 tokens at 0.1 DASH each:"); + println!( + " Platform price: {} credits per smallest unit", + platform_price_2 + ); + println!(" Total for 5 tokens: {} credits", total_2); + + assert_eq!( + total_2, + 5 * user_price_credits_2, + "Should be 0.5 DASH total" + ); + + // Case 3: Token with 0 decimals + let _token_decimals_0 = 0u8; + let user_price_0 = 0.05; // 0.05 DASH per token + let user_price_credits_0 = (user_price_0 * 10f64.powi(DASH_DECIMAL_PLACES as i32)) as u64; + + // With 0 decimals, price per token = price per smallest unit + let platform_price_0 = user_price_credits_0; // No division needed + + let amount_0 = 10; // 10 tokens = 10 smallest units (no decimals) + let total_0 = amount_0 * platform_price_0; + + println!("\nTest 3 - Token with 0 decimals, 10 tokens at 0.05 DASH each:"); + println!( + " Platform price: {} credits per smallest unit", + platform_price_0 + ); + println!(" Total for 10 tokens: {} credits", total_0); + + assert_eq!( + total_0, + 10 * user_price_credits_0, + "Should be 0.5 DASH total" + ); + } +} diff --git a/src/ui/tokens/set_token_price_screen.rs b/src/ui/tokens/set_token_price_screen.rs index f8c16bbce..5f82ffb39 100644 --- a/src/ui/tokens/set_token_price_screen.rs +++ b/src/ui/tokens/set_token_price_screen.rs @@ -25,6 +25,7 @@ use dash_sdk::dpp::data_contract::GroupContractPosition; use dash_sdk::dpp::data_contract::accessors::v0::DataContractV0Getters; use dash_sdk::dpp::data_contract::accessors::v1::DataContractV1Getters; use dash_sdk::dpp::data_contract::associated_token::token_configuration::accessors::v0::TokenConfigurationV0Getters; +use dash_sdk::dpp::data_contract::associated_token::token_configuration_convention::accessors::v0::TokenConfigurationConventionV0Getters; use dash_sdk::dpp::data_contract::associated_token::token_distribution_rules::accessors::v0::TokenDistributionRulesV0Getters; use dash_sdk::dpp::data_contract::change_control_rules::authorized_action_takers::AuthorizedActionTakers; use dash_sdk::dpp::data_contract::group::Group; @@ -243,25 +244,36 @@ impl SetTokenPriceScreen { } pub fn with_schedule(self, token_pricing_schedule: Option) -> Self { + let token_decimals = self + .identity_token_info + .token_config + .conventions() + .decimals(); + let decimal_multiplier = 10u64.pow(token_decimals as u32); + let (single_price_amount, tiered_prices) = match &token_pricing_schedule { - Some(TokenPricingSchedule::SinglePrice(price)) => { - let amount = Amount::new(*price, DASH_DECIMAL_PLACES).with_unit_name("DASH"); + Some(TokenPricingSchedule::SinglePrice(price_per_smallest_unit)) => { + // Convert price per smallest unit back to price per token for display + let price_per_token = price_per_smallest_unit * decimal_multiplier; + let amount = + Amount::new(price_per_token, DASH_DECIMAL_PLACES).with_unit_name("DASH"); (Some(amount), vec![(None, None)]) } Some(TokenPricingSchedule::SetPrices(prices)) => { let tiered_prices = prices .iter() - .map(|(amount, price)| { + .map(|(amount, price_per_smallest_unit)| { // Create amount input for token threshold - let amount_input = AmountInput::new(Amount::from_token( &self.identity_token_info, *amount, )) .with_hint_text("Token amount threshold"); - // Create price input for Dash pricing - let price = Amount::new(*price, DASH_DECIMAL_PLACES).with_unit_name("DASH"); + // Convert price per smallest unit back to price per token for display + let price_per_token = price_per_smallest_unit * decimal_multiplier; + let price = Amount::new(price_per_token, DASH_DECIMAL_PLACES) + .with_unit_name("DASH"); let price_input = AmountInput::new(price) .with_hint_text("Enter price in Dash") .with_min_amount(Some(1)); @@ -509,14 +521,33 @@ impl SetTokenPriceScreen { PricingType::RemovePricing => Ok(None), PricingType::SinglePrice => match &self.single_price_amount { Some(amount) if amount.value() > 0 => { - let credits_price = amount.value(); - Ok(Some(TokenPricingSchedule::SinglePrice(credits_price))) + // User enters price per whole token, but Platform expects price per smallest unit + let credits_price_per_token = amount.value(); + let token_decimals = self + .identity_token_info + .token_config + .conventions() + .decimals(); + + // Convert price per token to price per smallest unit + let decimal_divisor = 10u64.pow(token_decimals as u32); + let price_per_smallest_unit = credits_price_per_token / decimal_divisor; + + Ok(Some(TokenPricingSchedule::SinglePrice( + price_per_smallest_unit, + ))) } Some(_) => Err("Price must be greater than 0".to_string()), None => Err("Please enter a price".to_string()), }, PricingType::TieredPricing => { let mut map = std::collections::BTreeMap::new(); + let token_decimals = self + .identity_token_info + .token_config + .conventions() + .decimals(); + let decimal_divisor = 10u64.pow(token_decimals as u32); for (amount_input, price_input) in &self.tiered_prices { let Some(price) = price_input.as_ref().and_then(|input| input.current_value()) @@ -532,7 +563,9 @@ impl SetTokenPriceScreen { }; let amount = amount_value.value(); - map.insert(amount, price.value()); + // Convert price per token to price per smallest unit + let price_per_smallest_unit = price.value() / decimal_divisor; + map.insert(amount, price_per_smallest_unit); } if map.is_empty() { From dd0b050ae6cf65f56c15f63f0f265e128c7b1c19 Mon Sep 17 00:00:00 2001 From: pauldelucia Date: Fri, 17 Oct 2025 18:10:44 +0700 Subject: [PATCH 11/11] clippy --- src/ui/tokens/set_token_price_screen.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/ui/tokens/set_token_price_screen.rs b/src/ui/tokens/set_token_price_screen.rs index 7c98d2029..36d699e94 100644 --- a/src/ui/tokens/set_token_price_screen.rs +++ b/src/ui/tokens/set_token_price_screen.rs @@ -390,15 +390,15 @@ impl SetTokenPriceScreen { } // Show validation preview - if let Some(amount) = &self.single_price_amount { - if amount.value() > 0 { - ui.add_space(5.0); - let credits = amount.value(); - ui.colored_label( - Color32::DARK_GREEN, - format!("Price: {} per token ({} credits)", amount, credits), - ); - } + if let Some(amount) = &self.single_price_amount + && amount.value() > 0 + { + ui.add_space(5.0); + let credits = amount.value(); + ui.colored_label( + Color32::DARK_GREEN, + format!("Price: {} per token ({} credits)", amount, credits), + ); } } PricingType::TieredPricing => {