From 29afd792713ab0f9720bf98a4f984865f17fe8a4 Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Wed, 30 Jul 2025 11:54:03 +0200 Subject: [PATCH 1/7] feat: execute all available claims --- src/backend_task/mod.rs | 1 + src/backend_task/tokens/claim_tokens.rs | 105 ++++++++++++++++++++---- src/backend_task/tokens/mod.rs | 2 +- src/ui/tokens/claim_tokens_screen.rs | 21 ++++- 4 files changed, 111 insertions(+), 18 deletions(-) diff --git a/src/backend_task/mod.rs b/src/backend_task/mod.rs index 1c5eb7299..002c8d51d 100644 --- a/src/backend_task/mod.rs +++ b/src/backend_task/mod.rs @@ -99,6 +99,7 @@ pub enum BackendTaskSuccessResult { }, UpdatedThemePreference(crate::ui::theme::ThemeMode), PlatformInfo(PlatformInfoTaskResult), + TokensClaimed(TokenAmount), } impl BackendTaskSuccessResult {} diff --git a/src/backend_task/tokens/claim_tokens.rs b/src/backend_task/tokens/claim_tokens.rs index b3a3b8473..341caaf69 100644 --- a/src/backend_task/tokens/claim_tokens.rs +++ b/src/backend_task/tokens/claim_tokens.rs @@ -2,6 +2,9 @@ use crate::backend_task::BackendTaskSuccessResult; use crate::context::AppContext; use crate::model::proof_log_item::{ProofLogItem, RequestType}; use crate::model::qualified_identity::QualifiedIdentity; +use dash_sdk::dpp::ProtocolError; +use dash_sdk::dpp::consensus::ConsensusError; +use dash_sdk::dpp::consensus::state::state_error::StateError; use dash_sdk::dpp::data_contract::accessors::v1::DataContractV1Getters; use dash_sdk::dpp::data_contract::associated_token::token_distribution_key::TokenDistributionType; use dash_sdk::dpp::document::DocumentV0Getters; @@ -14,8 +17,14 @@ use dash_sdk::{Error, Sdk}; use std::sync::Arc; impl AppContext { + /// Claim all pending token claims matching the provided parameters. + /// + /// This method iterates until all tokens are claimed or no more claims are available. + /// + /// If no tokens are available to claim, it returns `TokensClaimed(0)` + /// (in contrary to the [AppContext::claim_token()] method which returns error). #[allow(clippy::too_many_arguments)] - pub async fn claim_tokens( + pub async fn claim_all_tokens( &self, data_contract: Arc, token_position: u16, @@ -25,6 +34,71 @@ impl AppContext { public_note: Option, sdk: &Sdk, ) -> Result { + let mut total_claimed = 0; + loop { + let result = self + .claim_token( + data_contract.clone(), + token_position, + actor_identity, + distribution_type, + signing_key.clone(), + public_note.clone(), + sdk, + ) + .await; + + match result { + Ok(BackendTaskSuccessResult::TokensClaimed(0)) => { + // If no tokens were claimed, we can exit the loop + break; + } + Ok(BackendTaskSuccessResult::TokensClaimed(amount)) => { + total_claimed += amount; + // Continue to check for more tokens to claim + } + Err(dash_sdk::Error::Protocol(ProtocolError::ConsensusError(ce))) + if matches!( + *ce, + ConsensusError::StateError(StateError::InvalidTokenClaimNoCurrentRewards( + _ + )), + ) => + { + // No more rewards available, exit the loop + break; + } + // ConsensusError::StateError(StateError::InvalidTokenClaimNoCurrentRewards(as_refected)))) if as_expected, + + // any other result we propagate + Ok(x) => return Ok(x), + Err(e) => { + return Err(format!( + "Error claiming tokens: {}; claimed so far: {}", + e, total_claimed + )); + } + } + } + + // Return the total claimed amount. + Ok(BackendTaskSuccessResult::TokensClaimed(total_claimed)) + } + + /// Execute single token claim. + /// + /// This method will return error if no tokens were available to claim. + #[allow(clippy::too_many_arguments)] + pub async fn claim_token( + &self, + data_contract: Arc, + token_position: u16, + actor_identity: &QualifiedIdentity, + distribution_type: TokenDistributionType, + signing_key: IdentityPublicKey, + public_note: Option, + sdk: &Sdk, + ) -> Result { // Build let mut builder = TokenClaimTransitionBuilder::new( data_contract.clone(), @@ -45,7 +119,7 @@ impl AppContext { let result = sdk .token_claim(builder, &signing_key, actor_identity) .await - .map_err(|e| match e { + .inspect_err(|e|{ match e { Error::DriveProofError(proof_error, proof_bytes, block_info) => { self.db .insert_proof_log_item(ProofLogItem { @@ -54,19 +128,18 @@ impl AppContext { verification_path_query_bytes: vec![], height: block_info.height, time_ms: block_info.time_ms, - proof_bytes, + proof_bytes: proof_bytes.clone(), error: Some(proof_error.to_string()), }) .ok(); - format!( - "Error broadcasting ClaimTokens transition: {}, proof error logged", - proof_error - ) + tracing::error!(error=?proof_error, "Error broadcasting ClaimTokens transition, proof error logged"); } - e => format!("Error broadcasting ClaimTokens transition: {}", e), - })?; + e => tracing::error!(error=?e, "Error broadcasting ClaimTokens transition"), + }; + })?; // Using the result, update the balance of the claimer identity + let mut claimed_amount = 0; if let Some(token_id) = data_contract.token_id(token_position) { match result { // Standard claim result - extract claimer and amount from document @@ -77,15 +150,15 @@ impl AppContext { if let (Value::Identifier(claimer_bytes), Value::U64(amount)) = (claimer_value, amount_value) { + claimed_amount = *amount; if let Ok(claimer_id) = Identifier::from_bytes(claimer_bytes) { if let Err(e) = self.insert_token_identity_balance( &token_id, &claimer_id, *amount, ) { - eprintln!( - "Failed to update token balance from claim document: {}", - e + tracing::error!(error=?e, identity=?claimer_id, token_id=?token_id, + "Failed to update token balance from claim document", ); } } @@ -101,15 +174,15 @@ impl AppContext { if let (Value::Identifier(claimer_bytes), Value::U64(amount)) = (claimer_value, amount_value) { + claimed_amount = *amount; if let Ok(claimer_id) = Identifier::from_bytes(claimer_bytes) { if let Err(e) = self.insert_token_identity_balance( &token_id, &claimer_id, *amount, ) { - eprintln!( - "Failed to update token balance from group action document: {}", - e + tracing::error!(error=?e, identity=?claimer_id, token_id=?token_id, + "Failed to update token balance from group action document", ); } } @@ -120,6 +193,6 @@ impl AppContext { } // Return success - Ok(BackendTaskSuccessResult::Message("ClaimTokens".to_string())) + Ok(BackendTaskSuccessResult::TokensClaimed(claimed_amount)) } } diff --git a/src/backend_task/tokens/mod.rs b/src/backend_task/tokens/mod.rs index 40a1c8b80..d6c5ee0dd 100644 --- a/src/backend_task/tokens/mod.rs +++ b/src/backend_task/tokens/mod.rs @@ -488,7 +488,7 @@ impl AppContext { signing_key, public_note, } => self - .claim_tokens( + .claim_all_tokens( data_contract.clone(), *token_position, actor_identity, diff --git a/src/ui/tokens/claim_tokens_screen.rs b/src/ui/tokens/claim_tokens_screen.rs index 918ef055f..58b6fb569 100644 --- a/src/ui/tokens/claim_tokens_screen.rs +++ b/src/ui/tokens/claim_tokens_screen.rs @@ -20,7 +20,7 @@ use dash_sdk::dpp::identity::{KeyType, Purpose, SecurityLevel}; use eframe::egui::{self, Color32, Context, Ui}; use egui::RichText; use crate::app::{AppAction, BackendTasksExecutionMode}; -use crate::backend_task::BackendTask; +use crate::backend_task::{BackendTask, BackendTaskSuccessResult}; use crate::backend_task::tokens::TokenTask; use crate::context::AppContext; use crate::model::qualified_contract::QualifiedContract; @@ -248,6 +248,25 @@ impl ClaimTokensScreen { } impl ScreenLike for ClaimTokensScreen { + fn display_task_result( + &mut self, + backend_task_success_result: crate::backend_task::BackendTaskSuccessResult, + ) { + if let BackendTaskSuccessResult::TokensClaimed(amount) = backend_task_success_result { + if amount > 0 { + self.display_message( + &format!("Claimed {} tokens successfully!", amount), + MessageType::Success, + ); + self.status = ClaimTokensStatus::Complete; + } else { + // self.display_message("No tokens available to claim.", MessageType::Error); + self.status = + ClaimTokensStatus::ErrorMessage("No tokens available to claim.".to_string()); + } + } + } + fn display_message(&mut self, message: &str, message_type: MessageType) { match message_type { MessageType::Success => { From 37e29df285d751d839fffe2c38540b96752ca729 Mon Sep 17 00:00:00 2001 From: lklimek <842586+lklimek@users.noreply.github.com> Date: Mon, 4 Aug 2025 12:03:33 +0200 Subject: [PATCH 2/7] chore: remove some comments Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/backend_task/tokens/claim_tokens.rs | 2 -- src/ui/tokens/claim_tokens_screen.rs | 1 - 2 files changed, 3 deletions(-) diff --git a/src/backend_task/tokens/claim_tokens.rs b/src/backend_task/tokens/claim_tokens.rs index 341caaf69..b080f5538 100644 --- a/src/backend_task/tokens/claim_tokens.rs +++ b/src/backend_task/tokens/claim_tokens.rs @@ -68,8 +68,6 @@ impl AppContext { // No more rewards available, exit the loop break; } - // ConsensusError::StateError(StateError::InvalidTokenClaimNoCurrentRewards(as_refected)))) if as_expected, - // any other result we propagate Ok(x) => return Ok(x), Err(e) => { diff --git a/src/ui/tokens/claim_tokens_screen.rs b/src/ui/tokens/claim_tokens_screen.rs index 58b6fb569..f0c73179d 100644 --- a/src/ui/tokens/claim_tokens_screen.rs +++ b/src/ui/tokens/claim_tokens_screen.rs @@ -260,7 +260,6 @@ impl ScreenLike for ClaimTokensScreen { ); self.status = ClaimTokensStatus::Complete; } else { - // self.display_message("No tokens available to claim.", MessageType::Error); self.status = ClaimTokensStatus::ErrorMessage("No tokens available to claim.".to_string()); } From 28d83c4d1a2fc9c1d9eb5a6d3bee163ae530029b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 11:05:02 +0000 Subject: [PATCH 3/7] Initial plan From 02ab340b3c0dfae1e9a975ad7f95a0437d063ec7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 12:11:49 +0000 Subject: [PATCH 4/7] feat: add "Claim all" checkbox and complete merge with v1.0-dev - Added claim_all boolean field to ClaimTokens task - Updated handler to call claim_all_tokens or claim_token based on flag - Added claim_all checkbox to UI (enabled by default) - Added claimed_amount field to track tokens claimed - Updated display_task_result to save and display claimed amount - Updated success screen to show amount of tokens claimed - Wire checkbox state to ClaimTokens backend task Co-authored-by: lklimek <842586+lklimek@users.noreply.github.com> --- src/backend_task/tokens/mod.rs | 41 ++++++++++++++++++++-------- src/ui/tokens/claim_tokens_screen.rs | 28 +++++++++++++++---- 2 files changed, 51 insertions(+), 18 deletions(-) diff --git a/src/backend_task/tokens/mod.rs b/src/backend_task/tokens/mod.rs index d6c5ee0dd..2eefa65f6 100644 --- a/src/backend_task/tokens/mod.rs +++ b/src/backend_task/tokens/mod.rs @@ -183,6 +183,7 @@ pub enum TokenTask { distribution_type: TokenDistributionType, signing_key: IdentityPublicKey, public_note: Option, + claim_all: bool, }, EstimatePerpetualTokenRewardsWithExplanation { identity_id: Identifier, @@ -487,18 +488,34 @@ impl AppContext { distribution_type, signing_key, public_note, - } => self - .claim_all_tokens( - data_contract.clone(), - *token_position, - actor_identity, - *distribution_type, - signing_key.clone(), - public_note.clone(), - sdk, - ) - .await - .map_err(|e| format!("Failed to claim tokens: {e}")), + claim_all, + } => { + if *claim_all { + self.claim_all_tokens( + data_contract.clone(), + *token_position, + actor_identity, + *distribution_type, + signing_key.clone(), + public_note.clone(), + sdk, + ) + .await + .map_err(|e| format!("Failed to claim all tokens: {e}")) + } else { + self.claim_token( + data_contract.clone(), + *token_position, + actor_identity, + *distribution_type, + signing_key.clone(), + public_note.clone(), + sdk, + ) + .await + .map_err(|e| format!("Failed to claim tokens: {e}")) + } + } TokenTask::EstimatePerpetualTokenRewardsWithExplanation { identity_id, token_id, diff --git a/src/ui/tokens/claim_tokens_screen.rs b/src/ui/tokens/claim_tokens_screen.rs index f0c73179d..b7d444b9a 100644 --- a/src/ui/tokens/claim_tokens_screen.rs +++ b/src/ui/tokens/claim_tokens_screen.rs @@ -58,6 +58,8 @@ pub struct ClaimTokensScreen { selected_wallet: Option>>, wallet_password: String, show_password: bool, + claim_all: bool, + claimed_amount: Option, } impl ClaimTokensScreen { @@ -126,6 +128,8 @@ impl ClaimTokensScreen { selected_wallet, wallet_password: String::new(), show_password: false, + claim_all: true, + claimed_amount: None, } } @@ -211,6 +215,7 @@ impl ClaimTokensScreen { distribution_type, signing_key: self.selected_key.clone().expect("No key selected"), public_note: self.public_note.clone(), + claim_all: self.claim_all, })), BackendTask::TokenTask(Box::new(TokenTask::QueryMyTokenBalances)), ], @@ -239,6 +244,12 @@ impl ClaimTokensScreen { ui.add_space(20.0); + // Show amount claimed if available + if let Some(amount) = self.claimed_amount { + ui.label(format!("Amount claimed: {} tokens", amount)); + ui.add_space(10.0); + } + if ui.button("Back to Tokens").clicked() { action = AppAction::PopScreenAndRefresh; } @@ -253,15 +264,13 @@ impl ScreenLike for ClaimTokensScreen { backend_task_success_result: crate::backend_task::BackendTaskSuccessResult, ) { if let BackendTaskSuccessResult::TokensClaimed(amount) = backend_task_success_result { + self.claimed_amount = Some(amount); if amount > 0 { - self.display_message( - &format!("Claimed {} tokens successfully!", amount), - MessageType::Success, - ); self.status = ClaimTokensStatus::Complete; } else { - self.status = - ClaimTokensStatus::ErrorMessage("No tokens available to claim.".to_string()); + self.status = ClaimTokensStatus::ErrorMessage( + "No tokens available to claim.".to_string(), + ); } } } @@ -519,6 +528,13 @@ impl ScreenLike for ClaimTokensScreen { ui.add_space(10.0); } + // Add "Claim all" checkbox + ui.horizontal(|ui| { + ui.checkbox(&mut self.claim_all, "Claim all") + .on_hover_text("When enabled, automatically claims all available tokens by repeating claims until no more tokens are available. When disabled, performs a single claim operation."); + }); + ui.add_space(10.0); + let button = egui::Button::new(RichText::new("Claim").color(Color32::WHITE)) .fill(Color32::from_rgb(0, 128, 0)) .corner_radius(3.0); From 044ad657e6f9f0155946de2fe82ec43525d1e81a Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Wed, 11 Feb 2026 15:05:26 +0100 Subject: [PATCH 5/7] chore: post-merge fixes --- src/backend_task/mod.rs | 256 ++++++- src/backend_task/tokens/claim_tokens.rs | 46 +- src/backend_task/tokens/mod.rs | 24 +- src/context.rs | 863 ------------------------ src/database/identities.rs | 33 + src/ui/tokens/claim_tokens_screen.rs | 18 +- src/ui/wallets/import_wallet_screen.rs | 415 ------------ 7 files changed, 320 insertions(+), 1335 deletions(-) delete mode 100644 src/context.rs delete mode 100644 src/ui/wallets/import_wallet_screen.rs diff --git a/src/backend_task/mod.rs b/src/backend_task/mod.rs index 002c8d51d..97ecc2ef9 100644 --- a/src/backend_task/mod.rs +++ b/src/backend_task/mod.rs @@ -2,22 +2,33 @@ use crate::app::TaskResult; use crate::backend_task::contested_names::ContestedResourceTask; use crate::backend_task::contract::ContractTask; use crate::backend_task::core::{CoreItem, CoreTask}; +use crate::backend_task::dashpay::{DashPayTask, ContactData}; use crate::backend_task::document::DocumentTask; use crate::backend_task::identity::IdentityTask; use crate::backend_task::platform_info::{PlatformInfoTaskRequestType, PlatformInfoTaskResult}; use crate::backend_task::system_task::SystemTask; +use crate::backend_task::wallet::WalletTask; use crate::context::AppContext; +use dash_sdk::dpp::dashcore::Address; +use dash_sdk::dpp::dashcore::address::NetworkChecked; +use dash_sdk::dpp::dashcore::bls_sig_utils::BLSSignature; +use dash_sdk::dpp::dashcore::network::message_qrinfo::QRInfo; +use dash_sdk::dpp::dashcore::BlockHash; use crate::model::qualified_identity::QualifiedIdentity; +use crate::model::wallet::WalletSeedHash; +use crate::model::grovestark_prover::ProofDataOutput; use crate::ui::tokens::tokens_screen::{ ContractDescriptionInfo, IdentityTokenIdentifier, TokenInfo, }; use crate::utils::egui_mpsc::SenderAsync; use contested_names::ScheduledDPNSVote; use dash_sdk::dpp::balances::credits::TokenAmount; +use dash_sdk::dpp::dashcore::network::message_sml::MnListDiff; use dash_sdk::dpp::data_contract::associated_token::token_perpetual_distribution::distribution_function::evaluate_interval::IntervalEvaluationExplanation; use dash_sdk::dpp::group::group_action::GroupAction; use dash_sdk::dpp::prelude::DataContract; use dash_sdk::dpp::state_transition::StateTransition; +use dash_sdk::dpp::tokens::token_pricing_schedule::TokenPricingSchedule; use dash_sdk::dpp::voting::vote_choices::resource_vote_choice::ResourceVoteChoice; use dash_sdk::dpp::voting::votes::Vote; use dash_sdk::platform::proto::get_documents_request::get_documents_request_v0::Start; @@ -27,22 +38,45 @@ use futures::future::join_all; use std::collections::BTreeMap; use std::sync::Arc; use tokens::TokenTask; +use grovestark::GroveSTARKTask; pub mod broadcast_state_transition; pub mod contested_names; pub mod contract; pub mod core; +pub mod dashpay; pub mod document; +pub mod grovestark; pub mod identity; +pub mod mnlist; pub mod platform_info; pub mod register_contract; pub mod system_task; pub mod tokens; pub mod update_data_contract; +pub mod wallet; // TODO: Refactor how we handle errors and messages, and remove it from here pub(crate) const NO_IDENTITIES_FOUND: &str = "No identities found"; +/// Information about fees paid for a platform state transition +#[derive(Debug, Clone, PartialEq)] +pub struct FeeResult { + /// The fee that was estimated before the operation + pub estimated_fee: u64, + /// The actual fee that was paid (in credits) + pub actual_fee: u64, +} + +impl FeeResult { + pub fn new(estimated_fee: u64, actual_fee: u64) -> Self { + Self { + estimated_fee, + actual_fee, + } + } +} + #[derive(Debug, Clone, PartialEq)] pub enum BackendTask { IdentityTask(IdentityTask), @@ -50,26 +84,40 @@ pub enum BackendTask { ContractTask(Box), ContestedResourceTask(ContestedResourceTask), CoreTask(CoreTask), + DashPayTask(Box), BroadcastStateTransition(StateTransition), TokenTask(Box), SystemTask(SystemTask), + MnListTask(mnlist::MnListTask), PlatformInfo(PlatformInfoTaskRequestType), + GroveSTARKTask(GroveSTARKTask), + WalletTask(WalletTask), None, } #[derive(Debug, Clone, PartialEq)] #[allow(clippy::large_enum_variant)] pub enum BackendTaskSuccessResult { + // General results None, Refresh, - Message(String), + Message(String), // Used for: progress messages during long operations, placeholder messages for + // not-yet-implemented functionality, and DashPay operations that would need their own typed variants. + WalletPayment { + txid: String, + /// List of (address, amount) pairs for each recipient + recipients: Vec<(String, u64)>, + total_amount: u64, + }, + + // Specific results #[allow(dead_code)] // May be used for individual document operations Document(Document), Documents(Documents), BroadcastedDocument(Document), CoreItem(CoreItem), - RegisteredIdentity(QualifiedIdentity), - ToppedUpIdentity(QualifiedIdentity), + RegisteredIdentity(QualifiedIdentity, FeeResult), + ToppedUpIdentity(QualifiedIdentity, FeeResult), #[allow(dead_code)] // May be used for reporting successful votes SuccessfulVotes(Vec), DPNSVoteResults(Vec<(String, ResourceVoteChoice, Result<(), String>)>), @@ -95,11 +143,130 @@ pub enum BackendTaskSuccessResult { ActiveGroupActions(IndexMap), TokenPricing { token_id: Identifier, - prices: Option, + prices: Option, }, UpdatedThemePreference(crate::ui::theme::ThemeMode), PlatformInfo(PlatformInfoTaskResult), + + // DashPay related results + DashPayProfile(Option<(String, String, String)>), // (display_name, bio, avatar_url) + DashPayContactProfile(Option), // Contact's public profile document + DashPayProfileSearchResults(Vec<(Identifier, Option, String)>), // Search results: (identity_id, profile_document, username) + DashPayContactRequests { + incoming: Vec<(Identifier, Document)>, // (request_id, document) + outgoing: Vec<(Identifier, Document)>, // (request_id, document) + }, + DashPayContacts(Vec), // List of contact identity IDs + DashPayContactsWithInfo(Vec), // List of contacts with metadata + DashPayPaymentHistory(Vec<(String, String, u64, bool, String)>), // (tx_id, contact_name, amount, is_incoming, memo) + DashPayProfileUpdated(Identifier), // Identity ID of updated profile + DashPayContactRequestSent(String), // Username or ID of recipient + DashPayContactRequestAccepted(Identifier), // Request ID that was accepted + DashPayContactRequestRejected(Identifier), // Request ID that was rejected + DashPayContactAlreadyEstablished(Identifier), // Contact ID that already exists + DashPayContactInfoUpdated(Identifier), // Contact ID whose info was updated + DashPayPaymentSent(String, String, f64), // (recipient, address, amount) + GeneratedZKProof(ProofDataOutput), + VerifiedZKProof(bool, ProofDataOutput), + GeneratedReceiveAddress { + seed_hash: WalletSeedHash, + address: String, + }, + /// Platform address balances fetched from Platform + PlatformAddressBalances { + seed_hash: WalletSeedHash, + /// Map of address to (balance, nonce) + balances: BTreeMap, (u64, u32)>, + }, + /// Platform credits transferred between addresses + PlatformCreditsTransferred { + seed_hash: WalletSeedHash, + }, + /// Platform address funded from asset lock + PlatformAddressFunded { + seed_hash: WalletSeedHash, + }, + /// Withdrawal from Platform address to Core initiated + PlatformAddressWithdrawal { + seed_hash: WalletSeedHash, + }, + + // MNList-specific results + MnListFetchedDiff { + base_height: u32, + height: u32, + diff: MnListDiff, + }, + MnListFetchedQrInfo { + qr_info: QRInfo, + }, + MnListChainLockSigs { + entries: Vec<((u32, BlockHash), Option)>, + }, + MnListFetchedDiffs { + items: Vec<((u32, u32), MnListDiff)>, + }, + + // Token operation results (replacing string messages) + PausedTokens(FeeResult), + ResumedTokens(FeeResult), + MintedTokens(FeeResult), + BurnedTokens(FeeResult), + FrozeTokens(FeeResult), + UnfrozeTokens(FeeResult), + TransferredTokens(FeeResult), + PurchasedTokens(FeeResult), + SetTokenPrice(FeeResult), + DestroyedFrozenFunds(FeeResult), + ClaimedTokens(FeeResult), TokensClaimed(TokenAmount), + UpdatedTokenConfig(String, FeeResult), // The config item that was updated + FetchedTokenBalances, + SavedToken, + + // Identity operation results (replacing string messages) + AddedKeyToIdentity(FeeResult), + TransferredCredits(FeeResult), + WithdrewFromIdentity(FeeResult), + RegisteredDpnsName(FeeResult), + RefreshedIdentity(QualifiedIdentity), + LoadedIdentity(QualifiedIdentity), + + // Document operation results (replacing string messages) + DeletedDocument(Identifier, FeeResult), + ReplacedDocument(Identifier, FeeResult), + TransferredDocument(Identifier, FeeResult), + PurchasedDocument(Identifier, FeeResult), + SetDocumentPrice(Identifier, FeeResult), + + // Contract operation results (replacing string messages) + UpdatedContract(FeeResult), + RemovedContract, + FetchedNonce, + RegisteredContract(FeeResult), + RegisteredTokenContract, + SavedContract, + ContractNotFound, + TokenNotFound, + ProofErrorLogged, + + // Wallet operation results (replacing string messages) + RefreshedWallet { + /// Optional warning message (e.g., Platform sync failed but Core refresh succeeded) + warning: Option, + }, + RecoveredAssetLocks { + recovered_count: usize, + total_amount: u64, + }, + + // DPNS operation results (replacing string messages) + ScheduledVotes, + RefreshedDpnsContests, + RefreshedOwnedDpnsNames, + + // Broadcast results + BroadcastedStateTransition, } impl BackendTaskSuccessResult {} @@ -164,6 +331,9 @@ impl AppContext { self.run_document_task(*document_task, &sdk).await } BackendTask::CoreTask(core_task) => self.run_core_task(core_task).await, + BackendTask::DashPayTask(dashpay_task) => { + self.run_dashpay_task(*dashpay_task, &sdk).await + } BackendTask::BroadcastStateTransition(state_transition) => { self.broadcast_state_transition(state_transition, &sdk) .await @@ -172,10 +342,88 @@ impl AppContext { self.run_token_task(*token_task, &sdk, sender).await } BackendTask::SystemTask(system_task) => self.run_system_task(system_task, sender).await, + BackendTask::MnListTask(mnlist_task) => { + mnlist::run_mnlist_task(self, mnlist_task).await + } BackendTask::PlatformInfo(platform_info_task) => { self.run_platform_info_task(platform_info_task).await } + BackendTask::GroveSTARKTask(grovestark_task) => { + grovestark::run_grovestark_task(grovestark_task, &sdk).await + } + BackendTask::WalletTask(wallet_task) => self.run_wallet_task(wallet_task).await, BackendTask::None => Ok(BackendTaskSuccessResult::None), } } + + async fn run_wallet_task( + self: &Arc, + task: WalletTask, + ) -> Result { + match task { + WalletTask::GenerateReceiveAddress { seed_hash } => { + self.generate_receive_address(seed_hash).await + } + WalletTask::FetchPlatformAddressBalances { + seed_hash, + sync_mode, + } => { + self.fetch_platform_address_balances(seed_hash, sync_mode) + .await + } + WalletTask::TransferPlatformCredits { + seed_hash, + inputs, + outputs, + fee_payer_index, + } => { + self.transfer_platform_credits(seed_hash, inputs, outputs, fee_payer_index) + .await + } + WalletTask::FundPlatformAddressFromAssetLock { + seed_hash, + asset_lock_proof, + asset_lock_address, + outputs, + } => { + self.fund_platform_address_from_asset_lock( + seed_hash, + *asset_lock_proof, + asset_lock_address, + outputs, + ) + .await + } + WalletTask::WithdrawFromPlatformAddress { + seed_hash, + inputs, + output_script, + core_fee_per_byte, + fee_payer_index, + } => { + self.withdraw_from_platform_address( + seed_hash, + inputs, + output_script, + core_fee_per_byte, + fee_payer_index, + ) + .await + } + WalletTask::FundPlatformAddressFromWalletUtxos { + seed_hash, + amount, + destination, + fee_deduct_from_output, + } => { + self.fund_platform_address_from_wallet_utxos( + seed_hash, + amount, + destination, + fee_deduct_from_output, + ) + .await + } + } + } } diff --git a/src/backend_task/tokens/claim_tokens.rs b/src/backend_task/tokens/claim_tokens.rs index b080f5538..db7a6a4f4 100644 --- a/src/backend_task/tokens/claim_tokens.rs +++ b/src/backend_task/tokens/claim_tokens.rs @@ -144,22 +144,17 @@ impl AppContext { ClaimResult::Document(document) => { if let (Some(claimer_value), Some(amount_value)) = (document.get("claimerId"), document.get("amount")) - { - if let (Value::Identifier(claimer_bytes), Value::U64(amount)) = + && let (Value::Identifier(claimer_bytes), Value::U64(amount)) = (claimer_value, amount_value) + { + claimed_amount = *amount; + if let Ok(claimer_id) = Identifier::from_bytes(claimer_bytes) + && let Err(e) = + self.insert_token_identity_balance(&token_id, &claimer_id, *amount) { - claimed_amount = *amount; - if let Ok(claimer_id) = Identifier::from_bytes(claimer_bytes) { - if let Err(e) = self.insert_token_identity_balance( - &token_id, - &claimer_id, - *amount, - ) { - tracing::error!(error=?e, identity=?claimer_id, token_id=?token_id, - "Failed to update token balance from claim document", - ); - } - } + tracing::error!(error=?e, identity=?claimer_id, token_id=?token_id, + "Failed to update token balance from claim document", + ); } } } @@ -168,22 +163,17 @@ impl AppContext { ClaimResult::GroupActionWithDocument(_, document) => { if let (Some(claimer_value), Some(amount_value)) = (document.get("claimerId"), document.get("amount")) - { - if let (Value::Identifier(claimer_bytes), Value::U64(amount)) = + && let (Value::Identifier(claimer_bytes), Value::U64(amount)) = (claimer_value, amount_value) + { + claimed_amount = *amount; + if let Ok(claimer_id) = Identifier::from_bytes(claimer_bytes) + && let Err(e) = + self.insert_token_identity_balance(&token_id, &claimer_id, *amount) { - claimed_amount = *amount; - if let Ok(claimer_id) = Identifier::from_bytes(claimer_bytes) { - if let Err(e) = self.insert_token_identity_balance( - &token_id, - &claimer_id, - *amount, - ) { - tracing::error!(error=?e, identity=?claimer_id, token_id=?token_id, - "Failed to update token balance from group action document", - ); - } - } + tracing::error!(error=?e, identity=?claimer_id, token_id=?token_id, + "Failed to update token balance from group action document", + ); } } } diff --git a/src/backend_task/tokens/mod.rs b/src/backend_task/tokens/mod.rs index 2eefa65f6..9034babea 100644 --- a/src/backend_task/tokens/mod.rs +++ b/src/backend_task/tokens/mod.rs @@ -292,11 +292,7 @@ impl AppContext { sender, ) .await - .map(|_| { - BackendTaskSuccessResult::Message( - "Successfully registered token contract".to_string(), - ) - }) + .map(|_| BackendTaskSuccessResult::RegisteredTokenContract) .map_err(|e| format!("Failed to register token contract: {e}")) } TokenTask::QueryMyTokenBalances => self @@ -541,9 +537,7 @@ impl AppContext { Ok(Some(data_contract)) => { Ok(BackendTaskSuccessResult::FetchedContract(data_contract)) } - Ok(None) => Ok(BackendTaskSuccessResult::Message( - "Contract not found".to_string(), - )), + Ok(None) => Ok(BackendTaskSuccessResult::ContractNotFound), Err(e) => Err(format!("Error fetching contracts: {}", e)), } } @@ -569,15 +563,11 @@ impl AppContext { token_position, )) } - Ok(None) => Ok(BackendTaskSuccessResult::Message( - "Contract not found for token".to_string(), - )), + Ok(None) => Ok(BackendTaskSuccessResult::ContractNotFound), Err(e) => Err(format!("Error fetching contract for token: {}", e)), } } - Ok(None) => Ok(BackendTaskSuccessResult::Message( - "Token not found".to_string(), - )), + Ok(None) => Ok(BackendTaskSuccessResult::TokenNotFound), Err(e) => Err(format!("Error fetching token info: {}", e)), } } @@ -599,9 +589,7 @@ impl AppContext { ) .map_err(|e| format!("error saving token: {}", e))?; - Ok(BackendTaskSuccessResult::Message( - "Saved token to db".to_string(), - )) + Ok(BackendTaskSuccessResult::SavedToken) } TokenTask::UpdateTokenConfig { identity_token_info, @@ -737,6 +725,8 @@ impl AppContext { let mut validation_operations = Vec::new(); match dash_sdk::dpp::data_contract::document_type::DocumentType::try_from_schema( contract_id, + 0, + 0, &name, platform_value, None, // schema_defs diff --git a/src/context.rs b/src/context.rs deleted file mode 100644 index c71f28694..000000000 --- a/src/context.rs +++ /dev/null @@ -1,863 +0,0 @@ -use crate::app_dir::core_cookie_path; -use crate::backend_task::contested_names::ScheduledDPNSVote; -use crate::components::core_zmq_listener::ZMQConnectionEvent; -use crate::config::{Config, NetworkConfig}; -use crate::context_provider::Provider; -use crate::database::Database; -use crate::model::contested_name::ContestedName; -use crate::model::password_info::PasswordInfo; -use crate::model::qualified_contract::QualifiedContract; -use crate::model::qualified_identity::{DPNSNameInfo, QualifiedIdentity}; -use crate::model::settings::Settings; -use crate::model::wallet::{Wallet, WalletSeedHash}; -use crate::sdk_wrapper::initialize_sdk; -use crate::ui::RootScreenType; -use crate::ui::tokens::tokens_screen::{IdentityTokenBalance, IdentityTokenIdentifier}; -use crate::utils::tasks::TaskManager; -use bincode::config; -use crossbeam_channel::{Receiver, Sender}; -use dash_sdk::Sdk; -use dash_sdk::dashcore_rpc::dashcore::{InstantLock, Transaction}; -use dash_sdk::dashcore_rpc::{Auth, Client}; -use dash_sdk::dpp::dashcore::hashes::Hash; -use dash_sdk::dpp::dashcore::transaction::special_transaction::TransactionPayload::AssetLockPayloadType; -use dash_sdk::dpp::dashcore::{Address, Network, OutPoint, TxOut, Txid}; -use dash_sdk::dpp::data_contract::TokenConfiguration; -use dash_sdk::dpp::identity::accessors::IdentityGettersV0; -use dash_sdk::dpp::identity::state_transition::asset_lock_proof::InstantAssetLockProof; -use dash_sdk::dpp::identity::state_transition::asset_lock_proof::chain::ChainAssetLockProof; -use dash_sdk::dpp::prelude::{AssetLockProof, CoreBlockHeight}; -use dash_sdk::dpp::state_transition::StateTransitionSigningOptions; -use dash_sdk::dpp::state_transition::batch_transition::methods::StateTransitionCreationOptions; -use dash_sdk::dpp::system_data_contracts::{SystemDataContract, load_system_data_contract}; -use dash_sdk::dpp::version::PlatformVersion; -use dash_sdk::dpp::version::v8::PLATFORM_V8; -use dash_sdk::dpp::version::v9::PLATFORM_V9; -use dash_sdk::platform::{DataContract, Identifier}; -use dash_sdk::query_types::IndexMap; -use egui::Context; -use rusqlite::Result; -use std::collections::{BTreeMap, HashMap}; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::{Arc, Mutex, RwLock, RwLockWriteGuard}; - -const ANIMATION_REFRESH_TIME: std::time::Duration = std::time::Duration::from_millis(100); - -/// A guard that ensures settings cache invalidation happens atomically -/// -/// This guard holds a write lock on the cached settings, preventing reads -/// until the database update is complete and the cache is properly invalidated. -type SettingsCacheGuard<'a> = RwLockWriteGuard<'a, Option>; - -#[derive(Debug)] -pub struct AppContext { - pub(crate) network: Network, - developer_mode: AtomicBool, - #[allow(dead_code)] // May be used for devnet identification - pub(crate) devnet_name: Option, - pub(crate) db: Arc, - pub(crate) sdk: RwLock, - pub(crate) config: RwLock, - pub(crate) rx_zmq_status: Receiver, - pub(crate) sx_zmq_status: Sender, - pub(crate) zmq_connection_status: Mutex, - pub(crate) dpns_contract: Arc, - pub(crate) withdraws_contract: Arc, - pub(crate) token_history_contract: Arc, - pub(crate) keyword_search_contract: Arc, - pub(crate) core_client: RwLock, - pub(crate) has_wallet: AtomicBool, - pub(crate) wallets: RwLock>>>, - #[allow(dead_code)] // May be used for password validation - pub(crate) password_info: Option, - pub(crate) transactions_waiting_for_finality: Mutex>>, - /// Whether to animate the UI elements. - /// - /// This is used to control animations in the UI, such as loading spinners or transitions. - /// Disable for automated tests. - animate: AtomicBool, - /// Cached settings to avoid expensive database reads - /// Use RwLock to allow multiple readers but exclusive writers for cache invalidation - cached_settings: RwLock>, - // subtasks started by the app context, used for graceful shutdown - pub(crate) subtasks: Arc, -} - -impl AppContext { - pub fn new( - network: Network, - db: Arc, - password_info: Option, - subtasks: Arc, - ) -> Option> { - let config = match Config::load() { - Ok(config) => config, - Err(e) => { - println!("Failed to load config: {e}"); - return None; - } - }; - - let network_config = config.config_for_network(network).clone()?; - let (sx_zmq_status, rx_zmq_status) = crossbeam_channel::unbounded(); - - // we create provider, but we need to set app context to it later, as we have a circular dependency - let provider = - Provider::new(db.clone(), network, &network_config).expect("Failed to initialize SDK"); - - let sdk = initialize_sdk(&network_config, network, provider.clone()); - let platform_version = sdk.version(); - - let dpns_contract = load_system_data_contract(SystemDataContract::DPNS, platform_version) - .expect("expected to load dpns contract"); - - let withdrawal_contract = - load_system_data_contract(SystemDataContract::Withdrawals, platform_version) - .expect("expected to get withdrawal contract"); - - let token_history_contract = - load_system_data_contract(SystemDataContract::TokenHistory, platform_version) - .expect("expected to get token history contract"); - - let keyword_search_contract = - load_system_data_contract(SystemDataContract::KeywordSearch, platform_version) - .expect("expected to get keyword search contract"); - - let addr = format!( - "http://{}:{}", - network_config.core_host, network_config.core_rpc_port - ); - let cookie_path = core_cookie_path(network, &network_config.devnet_name) - .expect("expected to get cookie path"); - - // Try cookie authentication first - let core_client = match Client::new(&addr, Auth::CookieFile(cookie_path.clone())) { - Ok(client) => Ok(client), - Err(_) => { - // If cookie auth fails, try user/password authentication - tracing::info!( - "Failed to authenticate using .cookie file at {:?}, falling back to user/pass", - cookie_path, - ); - Client::new( - &addr, - Auth::UserPass( - network_config.core_rpc_user.to_string(), - network_config.core_rpc_password.to_string(), - ), - ) - } - } - .expect("Failed to create CoreClient"); - - let wallets: BTreeMap<_, _> = db - .get_wallets(&network) - .expect("expected to get wallets") - .into_iter() - .map(|w| (w.seed_hash(), Arc::new(RwLock::new(w)))) - .collect(); - - let animate = match config.developer_mode.unwrap_or(false) { - true => { - tracing::debug!("developer_mode is enabled, disabling animations"); - AtomicBool::new(false) - } - false => AtomicBool::new(true), // Animations are enabled by default - }; - - let app_context = AppContext { - network, - developer_mode: AtomicBool::new(config.developer_mode.unwrap_or(false)), - devnet_name: None, - db, - sdk: sdk.into(), - config: network_config.into(), - sx_zmq_status, - rx_zmq_status, - dpns_contract: Arc::new(dpns_contract), - withdraws_contract: Arc::new(withdrawal_contract), - token_history_contract: Arc::new(token_history_contract), - keyword_search_contract: Arc::new(keyword_search_contract), - core_client: core_client.into(), - has_wallet: (!wallets.is_empty()).into(), - wallets: RwLock::new(wallets), - password_info, - transactions_waiting_for_finality: Mutex::new(BTreeMap::new()), - zmq_connection_status: Mutex::new(ZMQConnectionEvent::Disconnected), - animate, - cached_settings: RwLock::new(None), - subtasks, - }; - - let app_context = Arc::new(app_context); - provider.bind_app_context(app_context.clone()); - - Some(app_context) - } - - /// Enables animations in the UI. - /// - /// This is used to control whether UI elements should animate, such as loading spinners or transitions. - pub fn enable_animations(&self, animate: bool) { - self.animate.store(animate, Ordering::Relaxed); - } - - pub fn enable_developer_mode(&self, enable: bool) { - self.developer_mode.store(enable, Ordering::Relaxed); - // Animations are reverse of developer mode - self.enable_animations(!enable); - } - - pub fn is_developer_mode(&self) -> bool { - self.developer_mode.load(Ordering::Relaxed) - } - - /// Repaints the UI if animations are enabled. - /// - /// Called by UI elements that need to trigger a repaint, such as loading spinners or animated icons. - pub(super) fn repaint_animation(&self, ctx: &Context) { - if self.animate.load(Ordering::Relaxed) { - // Request a repaint after a short delay to allow for animations - ctx.request_repaint_after(ANIMATION_REFRESH_TIME); - } - } - - pub fn platform_version(&self) -> &'static PlatformVersion { - default_platform_version(&self.network) - } - - pub fn state_transition_options(&self) -> Option { - if self.is_developer_mode() { - Some(StateTransitionCreationOptions { - signing_options: StateTransitionSigningOptions { - allow_signing_with_any_security_level: true, - allow_signing_with_any_purpose: true, - }, - batch_feature_version: None, - method_feature_version: None, - base_feature_version: None, - }) - } else { - None - } - } - - /// Rebuild both the Dash RPC `core_client` and the `Sdk` using the - /// updated `NetworkConfig` from `self.config`. - pub fn reinit_core_client_and_sdk(self: Arc) -> Result<(), String> { - // 1. Grab a fresh snapshot of your NetworkConfig - let cfg = { - let cfg_lock = self.config.read().unwrap(); - cfg_lock.clone() - }; - - // Note: developer_mode is now global and managed separately - - // 2. Rebuild the RPC client with the new password - let addr = format!("http://{}:{}", cfg.core_host, cfg.core_rpc_port); - let new_client = Client::new( - &addr, - Auth::UserPass(cfg.core_rpc_user.clone(), cfg.core_rpc_password.clone()), - ) - .map_err(|e| format!("Failed to create new Core RPC client: {e}"))?; - - // 3. Rebuild the Sdk with the updated config - let provider = Provider::new(self.db.clone(), self.network, &cfg) - .map_err(|e| format!("Failed to init provider: {e}"))?; - let new_sdk = initialize_sdk(&cfg, self.network, provider.clone()); - - // 4. Swap them in - { - let mut client_lock = self - .core_client - .write() - .expect("Core client lock was poisoned"); - *client_lock = new_client; - } - { - let mut sdk_lock = self.sdk.write().unwrap(); - *sdk_lock = new_sdk; - } - - // Rebind the provider to the new app context - provider.bind_app_context(self.clone()); - - Ok(()) - } - - /// Inserts a local qualified identity into the database - pub fn insert_local_qualified_identity( - &self, - qualified_identity: &QualifiedIdentity, - wallet_and_identity_id_info: &Option<(WalletSeedHash, u32)>, - ) -> Result<()> { - self.db.insert_local_qualified_identity( - qualified_identity, - wallet_and_identity_id_info, - self, - ) - } - - /// Updates a local qualified identity in the database - pub fn update_local_qualified_identity( - &self, - qualified_identity: &QualifiedIdentity, - ) -> Result<()> { - self.db - .update_local_qualified_identity(qualified_identity, self) - } - - /// Sets the alias for an identity - pub fn set_identity_alias( - &self, - identifier: &Identifier, - new_alias: Option<&str>, - ) -> Result<()> { - self.db.set_identity_alias(identifier, new_alias) - } - - pub fn set_contract_alias( - &self, - contract_id: &Identifier, - new_alias: Option<&str>, - ) -> Result<()> { - self.db.set_contract_alias(contract_id, new_alias) - } - - /// Gets the alias for an identity - pub fn get_identity_alias(&self, identifier: &Identifier) -> Result> { - self.db.get_identity_alias(identifier) - } - - /// Fetches all local qualified identities from the database - pub fn load_local_qualified_identities(&self) -> Result> { - let wallets = self.wallets.read().unwrap(); - self.db.get_local_qualified_identities(self, &wallets) - } - - /// Fetches all local qualified identities from the database - #[allow(dead_code)] // May be used for loading identities in wallets - pub fn load_local_qualified_identities_in_wallets(&self) -> Result> { - let wallets = self.wallets.read().unwrap(); - self.db - .get_local_qualified_identities_in_wallets(self, &wallets) - } - - pub fn get_identity_by_id( - &self, - identity_id: &Identifier, - ) -> Result> { - let wallets = self.wallets.read().unwrap(); - // Get the identity from the database - let result = self.db.get_identity_by_id(identity_id, self, &wallets)?; - - Ok(result) - } - - /// Fetches all voting identities from the database - pub fn load_local_voting_identities(&self) -> Result> { - self.db.get_local_voting_identities(self) - } - - /// Fetches all local user identities from the database - pub fn load_local_user_identities(&self) -> Result> { - let identities = self.db.get_local_user_identities(self)?; - - Ok(identities - .into_iter() - .map(|(mut identity, wallet_hash)| { - if let Some(wallet_id) = wallet_hash { - // Load wallets for each identity - self.load_wallet_for_identity( - &mut identity, - &[wallet_id], - ) - .unwrap_or_else(|e| { - tracing::warn!( - identity = %identity.identity.id(), - error = ?e, - "cannot load wallet for identity when loading local user identities", - ) - }) - } else { - tracing::debug!( - identity = %identity.identity.id(), - "no wallet hash found for identity when loading local user identities", - ); - } - identity - }) - .collect()) - } - - fn load_wallet_for_identity( - &self, - identity: &mut QualifiedIdentity, - wallet_hashes: &[WalletSeedHash], - ) -> Result<()> { - let wallets = self.wallets.read().unwrap(); - for wallet_hash in wallet_hashes { - if let Some(wallet) = wallets.get(wallet_hash) { - identity - .associated_wallets - .insert(*wallet_hash, wallet.clone()); - } else { - tracing::warn!( - wallet = %hex::encode(wallet_hash), - identity = %identity.identity.id(), - "wallet not found for identity when loading local user identities", - ); - } - } - - Ok(()) - } - - /// Fetches all contested names from the database including past and active ones - pub fn all_contested_names(&self) -> Result> { - self.db.get_all_contested_names(self) - } - - /// Fetches all ongoing contested names from the database - pub fn ongoing_contested_names(&self) -> Result> { - self.db.get_ongoing_contested_names(self) - } - - /// Inserts scheduled votes into the database - pub fn insert_scheduled_votes(&self, scheduled_votes: &Vec) -> Result<()> { - self.db.insert_scheduled_votes(self, scheduled_votes) - } - - /// Fetches all scheduled votes from the database - pub fn get_scheduled_votes(&self) -> Result> { - self.db.get_scheduled_votes(self) - } - - /// Clears all scheduled votes from the database - pub fn clear_all_scheduled_votes(&self) -> Result<()> { - self.db.clear_all_scheduled_votes(self) - } - - /// Clears all executed scheduled votes from the database - pub fn clear_executed_scheduled_votes(&self) -> Result<()> { - self.db.clear_executed_scheduled_votes(self) - } - - /// Deletes a scheduled vote from the database - #[allow(clippy::ptr_arg)] - pub fn delete_scheduled_vote(&self, identity_id: &[u8], contested_name: &String) -> Result<()> { - self.db - .delete_scheduled_vote(self, identity_id, contested_name) - } - - /// Marks a scheduled vote as executed in the database - pub fn mark_vote_executed(&self, identity_id: &[u8], contested_name: String) -> Result<()> { - self.db - .mark_vote_executed(self, identity_id, contested_name) - } - - /// Fetches the local identities from the database and then maps them to their DPNS names. - pub fn local_dpns_names(&self) -> Result> { - let wallets = self.wallets.read().unwrap(); - let qualified_identities = self.db.get_local_qualified_identities(self, &wallets)?; - - // Map each identity's DPNS names to (Identifier, DPNSNameInfo) tuples - let dpns_names = qualified_identities - .iter() - .flat_map(|qualified_identity| { - qualified_identity.dpns_names.iter().map(|dpns_name_info| { - ( - qualified_identity.identity.id(), - DPNSNameInfo { - name: dpns_name_info.name.clone(), - acquired_at: dpns_name_info.acquired_at, - }, - ) - }) - }) - .collect::>(); - - Ok(dpns_names) - } - - /// Updates the `start_root_screen` in the settings table - pub fn update_settings(&self, root_screen_type: RootScreenType) -> Result<()> { - let _guard = self.invalidate_settings_cache(); - - self.db - .insert_or_update_settings(self.network, root_screen_type) - } - - /// Updates the main password settings - pub fn update_main_password( - &self, - salt: &[u8], - nonce: &[u8], - password_check: &[u8], - ) -> Result<()> { - let _guard = self.invalidate_settings_cache(); - - self.db.update_main_password(salt, nonce, password_check) - } - - /// Updates the Dash Core execution settings - pub fn update_dash_core_execution_settings( - &self, - custom_dash_qt_path: Option, - overwrite_dash_conf: bool, - ) -> Result<()> { - let _guard = self.invalidate_settings_cache(); - - self.db - .update_dash_core_execution_settings(custom_dash_qt_path, overwrite_dash_conf) - } - - /// Invalidates the settings cache and returns a guard - /// - /// The cache is invalidated immediately and the guard prevents concurrent access - /// until the database operation is complete. This ensures atomicity and prevents - /// race conditions regardless of whether the database operation succeeds or fails. - pub fn invalidate_settings_cache(&self) -> SettingsCacheGuard { - let mut guard = self.cached_settings.write().unwrap(); - *guard = None; - guard - } - - /// Retrieves the current settings - /// - /// ## Cached - /// - /// This function uses a cache to avoid expensive database operations. - /// The cache is invalidated when settings are updated. - /// - /// Use [`AppContext::invalidate_settings_cache`] to invalidate the cache. - pub fn get_settings(&self) -> Result> { - // First, try to read from cache - { - let cache = self.cached_settings.read().unwrap(); - if let Some(ref settings) = *cache { - return Ok(Some(settings.clone())); - } - } - - // Cache miss, read from database - let settings = self.db.get_settings()?.map(Settings::from); - - // Update cache with the fresh data - { - let mut cache = self.cached_settings.write().unwrap(); - *cache = settings.clone(); - } - - Ok(settings) - } - - /// Retrieves all contracts from the database plus the system contracts from app context. - pub fn get_contracts( - &self, - limit: Option, - offset: Option, - ) -> Result> { - // Get contracts from the database - let mut contracts = self.db.get_contracts(self, limit, offset)?; - - // Add the DPNS contract to the list - let dpns_contract = QualifiedContract { - contract: Arc::clone(&self.dpns_contract).as_ref().clone(), - alias: Some("dpns".to_string()), - }; - - // Insert the DPNS contract at 0 - contracts.insert(0, dpns_contract); - - // Add the token history contract to the list - let token_history_contract = QualifiedContract { - contract: Arc::clone(&self.token_history_contract).as_ref().clone(), - alias: Some("token_history".to_string()), - }; - - // Insert the token history contract at 1 - contracts.insert(1, token_history_contract); - - // Add the withdrawal contract to the list - let withdraws_contract = QualifiedContract { - contract: Arc::clone(&self.withdraws_contract).as_ref().clone(), - alias: Some("withdrawals".to_string()), - }; - - // Insert the withdrawal contract at 2 - contracts.insert(2, withdraws_contract); - - // Add the keyword search contract to the list - let keyword_search_contract = QualifiedContract { - contract: Arc::clone(&self.keyword_search_contract).as_ref().clone(), - alias: Some("keyword_search".to_string()), - }; - - // Insert the keyword search contract at 3 - contracts.insert(3, keyword_search_contract); - - Ok(contracts) - } - - pub fn get_contract_by_id( - &self, - contract_id: &Identifier, - ) -> Result> { - // Get the contract from the database - self.db.get_contract_by_id(*contract_id, self) - } - - pub fn get_unqualified_contract_by_id( - &self, - contract_id: &Identifier, - ) -> Result> { - // Get the contract from the database - self.db.get_unqualified_contract_by_id(*contract_id, self) - } - - // Remove contract from the database by ID - pub fn remove_contract(&self, contract_id: &Identifier) -> Result<()> { - self.db.remove_contract(contract_id.as_bytes(), self) - } - - pub fn replace_contract( - &self, - contract_id: Identifier, - new_contract: &DataContract, - ) -> Result<()> { - self.db.replace_contract(contract_id, new_contract, self) - } - - pub(crate) fn received_transaction_finality( - &self, - tx: &Transaction, - islock: Option, - chain_locked_height: Option, - ) -> Result> { - // Initialize a vector to collect wallet outpoints - let mut wallet_outpoints = Vec::new(); - - // Identify the wallets associated with the transaction - let wallets = self.wallets.read().unwrap(); - for wallet_arc in wallets.values() { - let mut wallet = wallet_arc.write().unwrap(); - for (vout, tx_out) in tx.output.iter().enumerate() { - let address = if let Ok(output_addr) = - Address::from_script(&tx_out.script_pubkey, self.network) - { - if wallet.known_addresses.contains_key(&output_addr) { - output_addr - } else { - continue; - } - } else { - continue; - }; - self.db.insert_utxo( - tx.txid().as_byte_array(), - vout as u32, - &address, - tx_out.value, - &tx_out.script_pubkey.to_bytes(), - self.network, - )?; - self.db - .add_to_address_balance(&wallet.seed_hash(), &address, tx_out.value)?; - - // Create the OutPoint and insert it into the wallet.utxos entry - let out_point = OutPoint::new(tx.txid(), vout as u32); - wallet - .utxos - .entry(address.clone()) - .or_insert_with(HashMap::new) // Initialize inner HashMap if needed - .insert(out_point, tx_out.clone()); // Insert the TxOut at the OutPoint - - // Collect the outpoint - wallet_outpoints.push((out_point, tx_out.clone(), address.clone())); - - wallet - .address_balances - .entry(address) - .and_modify(|balance| *balance += tx_out.value) - .or_insert(tx_out.value); - } - } - if matches!( - tx.special_transaction_payload, - Some(AssetLockPayloadType(_)) - ) { - self.received_asset_lock_finality(tx, islock, chain_locked_height)?; - } - Ok(wallet_outpoints) - } - - /// Store the asset lock transaction in the database and update the wallet. - pub(crate) fn received_asset_lock_finality( - &self, - tx: &Transaction, - islock: Option, - chain_locked_height: Option, - ) -> Result<()> { - // Extract the asset lock payload from the transaction - let Some(AssetLockPayloadType(payload)) = tx.special_transaction_payload.as_ref() else { - return Ok(()); - }; - - let proof = if let Some(islock) = islock.as_ref() { - // Deserialize the InstantLock - Some(AssetLockProof::Instant(InstantAssetLockProof::new( - islock.clone(), - tx.clone(), - 0, - ))) - } else { - chain_locked_height.map(|chain_locked_height| { - AssetLockProof::Chain(ChainAssetLockProof { - core_chain_locked_height: chain_locked_height, - out_point: OutPoint::new(tx.txid(), 0), - }) - }) - }; - - { - let mut transactions = self.transactions_waiting_for_finality.lock().unwrap(); - - if let Some(asset_lock_proof) = transactions.get_mut(&tx.txid()) { - *asset_lock_proof = proof.clone(); - } - } - - // Identify the wallet associated with the transaction - let wallets = self.wallets.read().unwrap(); - for wallet_arc in wallets.values() { - let mut wallet = wallet_arc.write().unwrap(); - - // Check if any of the addresses in the transaction outputs match the wallet's known addresses - let matches_wallet = payload.credit_outputs.iter().any(|tx_out| { - if let Ok(output_addr) = Address::from_script(&tx_out.script_pubkey, self.network) { - wallet.known_addresses.contains_key(&output_addr) - } else { - false - } - }); - - if matches_wallet { - // Calculate the total amount from the credit outputs - let amount: u64 = payload - .credit_outputs - .iter() - .map(|tx_out| tx_out.value) - .sum(); - - // Store the asset lock transaction in the database - self.db.store_asset_lock_transaction( - tx, - amount, - islock.as_ref(), - &wallet.seed_hash(), - self.network, - )?; - - let first = payload - .credit_outputs - .first() - .expect("Expected at least one credit output"); - - let address = Address::from_script(&first.script_pubkey, self.network) - .expect("expected an address"); - - // Add the asset lock to the wallet's unused_asset_locks - wallet - .unused_asset_locks - .push((tx.clone(), address, amount, islock, proof)); - - break; // Exit the loop after updating the relevant wallet - } - } - - Ok(()) - } - - pub fn identity_token_balances( - &self, - ) -> Result> { - self.db.get_identity_token_balances(self) - } - - pub fn remove_token_balance( - &self, - token_id: Identifier, - identity_id: Identifier, - ) -> Result<()> { - self.db.remove_token_balance(&token_id, &identity_id, self) - } - - pub fn insert_token( - &self, - token_id: &Identifier, - token_name: &str, - token_configuration: TokenConfiguration, - contract_id: &Identifier, - token_position: u16, - ) -> Result<()> { - let config = config::standard(); - let Some(serialized_token_configuration) = - bincode::encode_to_vec(&token_configuration, config).ok() - else { - // We should always be able to serialize - return Ok(()); - }; - - self.db.insert_token( - token_id, - token_name, - serialized_token_configuration.as_slice(), - contract_id, - token_position, - self, - )?; - - Ok(()) - } - - pub fn remove_token(&self, token_id: &Identifier) -> Result<()> { - self.db.remove_token(token_id, self) - } - - #[allow(dead_code)] // May be used for storing token balances - pub fn insert_token_identity_balance( - &self, - token_id: &Identifier, - identity_id: &Identifier, - balance: u64, - ) -> Result<()> { - self.db - .insert_identity_token_balance(token_id, identity_id, balance, self)?; - - Ok(()) - } - - pub fn get_contract_by_token_id( - &self, - token_id: &Identifier, - ) -> Result> { - let contract_id = self - .db - .get_contract_id_by_token_id(token_id, self)? - .ok_or(rusqlite::Error::QueryReturnedNoRows)?; - self.db.get_contract_by_id(contract_id, self) - } -} - -/// Returns the default platform version for the given network. -pub(crate) const fn default_platform_version(network: &Network) -> &'static PlatformVersion { - // TODO: Use self.sdk.read().unwrap().version() instead of hardcoding - match network { - Network::Dash => &PLATFORM_V8, - Network::Testnet => &PLATFORM_V9, - Network::Devnet => &PLATFORM_V9, - Network::Regtest => &PLATFORM_V9, - _ => panic!("unsupported network"), - } -} diff --git a/src/database/identities.rs b/src/database/identities.rs index 0ebdce4df..524075f4f 100644 --- a/src/database/identities.rs +++ b/src/database/identities.rs @@ -114,6 +114,39 @@ impl Database { Ok(()) } + #[allow(dead_code)] // May be used for caching remote identities from network queries + pub fn insert_remote_identity_if_not_exists( + &self, + identifier: &Identifier, + qualified_identity: Option<&QualifiedIdentity>, + app_context: &AppContext, + ) -> rusqlite::Result<()> { + let id = identifier.to_vec(); + let alias = qualified_identity.and_then(|qi| qi.alias.clone()); + let identity_type = + qualified_identity.map_or("".to_string(), |qi| format!("{:?}", qi.identity_type)); + let data = qualified_identity.map(|qi| qi.to_bytes()); + + let network = app_context.network.to_string(); + + // Check if the identity already exists + let conn = self.conn.lock().unwrap(); + let mut stmt = + conn.prepare("SELECT COUNT(*) FROM identity WHERE id = ? AND network = ?")?; + let count: i64 = stmt.query_row(params![id, network], |row| row.get(0))?; + + // If the identity doesn't exist, insert it + if count == 0 { + self.execute( + "INSERT INTO identity (id, data, is_local, alias, identity_type, network) + VALUES (?, ?, 0, ?, ?, ?)", + params![id, data, alias, identity_type, network], + )?; + } + + Ok(()) + } + pub fn get_local_qualified_identities( &self, app_context: &AppContext, diff --git a/src/ui/tokens/claim_tokens_screen.rs b/src/ui/tokens/claim_tokens_screen.rs index b7d444b9a..ddce02f9a 100644 --- a/src/ui/tokens/claim_tokens_screen.rs +++ b/src/ui/tokens/claim_tokens_screen.rs @@ -268,9 +268,8 @@ impl ScreenLike for ClaimTokensScreen { if amount > 0 { self.status = ClaimTokensStatus::Complete; } else { - self.status = ClaimTokensStatus::ErrorMessage( - "No tokens available to claim.".to_string(), - ); + self.status = + ClaimTokensStatus::ErrorMessage("No tokens available to claim.".to_string()); } } } @@ -293,13 +292,12 @@ impl ScreenLike for ClaimTokensScreen { } fn refresh(&mut self) { - if let Ok(all) = self.app_context.load_local_qualified_identities() { - if let Some(updated) = all + if let Ok(all) = self.app_context.load_local_qualified_identities() + && let Some(updated) = all .into_iter() .find(|id| id.identity.id() == self.identity.identity.id()) - { - self.identity = updated; - } + { + self.identity = updated; } } @@ -606,4 +604,8 @@ impl ScreenWithWalletUnlock for ClaimTokensScreen { fn error_message(&self) -> Option<&String> { self.error_message.as_ref() } + + fn app_context(&self) -> Arc { + self.app_context.clone() + } } diff --git a/src/ui/wallets/import_wallet_screen.rs b/src/ui/wallets/import_wallet_screen.rs deleted file mode 100644 index 591b6801a..000000000 --- a/src/ui/wallets/import_wallet_screen.rs +++ /dev/null @@ -1,415 +0,0 @@ -use crate::app::AppAction; -use crate::context::AppContext; -use crate::ui::ScreenLike; -use crate::ui::components::left_panel::add_left_panel; -use crate::ui::components::styled::island_central_panel; -use crate::ui::components::top_panel::add_top_panel; -use eframe::egui::Context; - -use crate::model::wallet::encryption::{DASH_SECRET_MESSAGE, encrypt_message}; -use crate::model::wallet::{ClosedKeyItem, OpenWalletSeed, Wallet, WalletSeed}; -use crate::ui::wallets::add_new_wallet_screen::{ - DASH_BIP44_ACCOUNT_0_PATH_MAINNET, DASH_BIP44_ACCOUNT_0_PATH_TESTNET, -}; -use bip39::Mnemonic; -use dash_sdk::dashcore_rpc::dashcore::bip32::DerivationPath; -use dash_sdk::dashcore_rpc::dashcore::key::Secp256k1; -use dash_sdk::dpp::dashcore::Network; -use dash_sdk::dpp::dashcore::bip32::{ExtendedPrivKey, ExtendedPubKey}; -use egui::{Color32, ComboBox, Direction, Grid, Layout, RichText, Stroke, Ui, Vec2}; -use std::sync::atomic::Ordering; -use std::sync::{Arc, RwLock}; -use zxcvbn::zxcvbn; - -pub struct ImportWalletScreen { - seed_phrase_words: Vec, - selected_seed_phrase_length: usize, - seed_phrase: Option, - password: String, - alias_input: String, - password_strength: f64, - estimated_time_to_crack: String, - error: Option, - pub app_context: Arc, - use_password_for_app: bool, -} - -impl ImportWalletScreen { - pub fn new(app_context: &Arc) -> Self { - Self { - seed_phrase_words: vec!["".to_string(); 24], - selected_seed_phrase_length: 12, - seed_phrase: None, - password: String::new(), - alias_input: String::new(), - password_strength: 0.0, - estimated_time_to_crack: "".to_string(), - error: None, - app_context: app_context.clone(), - use_password_for_app: true, - } - } - fn save_wallet(&mut self) -> Result { - if let Some(mnemonic) = &self.seed_phrase { - let seed = mnemonic.to_seed(""); - - let (encrypted_seed, salt, nonce, uses_password) = if self.password.is_empty() { - (seed.to_vec(), vec![], vec![], false) - } else { - // Encrypt the seed to obtain encrypted_seed, salt, and nonce - let (encrypted_seed, salt, nonce) = - ClosedKeyItem::encrypt_seed(&seed, self.password.as_str())?; - if self.use_password_for_app { - let (encrypted_message, salt, nonce) = - encrypt_message(DASH_SECRET_MESSAGE, self.password.as_str())?; - self.app_context - .update_main_password(&salt, &nonce, &encrypted_message) - .map_err(|e| e.to_string())?; - } - (encrypted_seed, salt, nonce, true) - }; - - // Generate master ECDSA extended private key - let master_ecdsa_extended_private_key = - ExtendedPrivKey::new_master(self.app_context.network, &seed) - .expect("Failed to create master ECDSA extended private key"); - let bip44_root_derivation_path: DerivationPath = match self.app_context.network { - Network::Dash => DerivationPath::from(DASH_BIP44_ACCOUNT_0_PATH_MAINNET.as_slice()), - _ => DerivationPath::from(DASH_BIP44_ACCOUNT_0_PATH_TESTNET.as_slice()), - }; - let secp = Secp256k1::new(); - let master_bip44_ecdsa_extended_public_key = master_ecdsa_extended_private_key - .derive_priv(&secp, &bip44_root_derivation_path) - .map_err(|e| e.to_string())?; - - let master_bip44_ecdsa_extended_public_key = - ExtendedPubKey::from_priv(&secp, &master_bip44_ecdsa_extended_public_key); - - // Compute the seed hash - let seed_hash = ClosedKeyItem::compute_seed_hash(&seed); - - let wallet = Wallet { - wallet_seed: WalletSeed::Open(OpenWalletSeed { - seed, - wallet_info: ClosedKeyItem { - seed_hash, - encrypted_seed, - salt, - nonce, - password_hint: None, // Set a password hint if needed - }, - }), - uses_password, - master_bip44_ecdsa_extended_public_key, - address_balances: Default::default(), - known_addresses: Default::default(), - watched_addresses: Default::default(), - unused_asset_locks: Default::default(), - alias: Some(self.alias_input.clone()), - identities: Default::default(), - utxos: Default::default(), - is_main: true, - }; - - self.app_context - .db - .store_wallet(&wallet, &self.app_context.network) - .map_err(|e| { - if e.to_string().contains("UNIQUE constraint failed: wallet.seed_hash") { - "This wallet has already been imported for another network. Each wallet can only be imported once per network. If you want to use this wallet on a different network, please switch networks first.".to_string() - } else { - e.to_string() - } - })?; - - // Acquire a write lock and add the new wallet - if let Ok(mut wallets) = self.app_context.wallets.write() { - wallets.insert(wallet.seed_hash(), Arc::new(RwLock::new(wallet))); - self.app_context.has_wallet.store(true, Ordering::Relaxed); - } else { - eprintln!("Failed to acquire write lock on wallets"); - } - - Ok(AppAction::GoToMainScreen) // Navigate back to the main screen after saving - } else { - Ok(AppAction::None) // No action if no seed phrase exists - } - } - - fn render_seed_phrase_input(&mut self, ui: &mut Ui) { - ui.add_space(15.0); // Add spacing from the top - ui.vertical_centered(|ui| { - // Select the seed phrase length - ui.horizontal(|ui| { - ui.label("Seed Phrase Length:"); - - ComboBox::from_label("") - .selected_text(format!("{}", self.selected_seed_phrase_length)) - .width(100.0) - .show_ui(ui, |ui| { - for &length in &[12, 15, 18, 21, 24] { - ui.selectable_value( - &mut self.selected_seed_phrase_length, - length, - format!("{}", length), - ); - } - }); - }); - - ui.add_space(10.0); - - // Ensure the seed_phrase_words vector matches the selected length - self.seed_phrase_words - .resize(self.selected_seed_phrase_length, "".to_string()); - - // Seed phrase input grid with shorter inputs - let columns = 4; // 4 columns - let _rows = self.selected_seed_phrase_length.div_ceil(columns); - let input_width = 120.0; // Fixed width for each input - - Grid::new("seed_phrase_input_grid") - .num_columns(columns) - .spacing((15.0, 10.0)) - .show(ui, |ui| { - for i in 0..self.selected_seed_phrase_length { - ui.horizontal(|ui| { - ui.label(format!("{:2}:", i + 1)); - - let mut word = self.seed_phrase_words[i].clone(); - - let dark_mode = ui.ctx().style().visuals.dark_mode; - let response = ui.add_sized( - Vec2::new(input_width, 20.0), - egui::TextEdit::singleline(&mut word) - .text_color(crate::ui::theme::DashColors::text_primary( - dark_mode, - )) - .background_color( - crate::ui::theme::DashColors::input_background(dark_mode), - ), - ); - - if response.changed() { - // Update the seed_phrase_words[i] - self.seed_phrase_words[i] = word.clone(); - - // Check if the input contains multiple words - let words: Vec<&str> = word.split_whitespace().collect(); - - if words.len() > 1 { - // User pasted multiple words into this field - // Let's distribute them into the seed_phrase_words vector - let total_words = self.selected_seed_phrase_length; - let mut idx = i; - for word in words { - if idx < total_words { - self.seed_phrase_words[idx] = word.to_string(); - idx += 1; - } else { - break; - } - } - // Since we've updated the seed_phrase_words, the UI will reflect changes on the next frame - } - } - }); - - if (i + 1) % columns == 0 { - ui.end_row(); - } - } - }); - }); - } -} - -impl ScreenLike for ImportWalletScreen { - fn ui(&mut self, ctx: &Context) -> AppAction { - let mut action = add_top_panel( - ctx, - &self.app_context, - vec![ - ("Wallets", AppAction::GoToMainScreen), - ("Import Wallet", AppAction::None), - ], - vec![], - ); - - action |= add_left_panel( - ctx, - &self.app_context, - crate::ui::RootScreenType::RootScreenWalletsBalances, - ); - - action |= island_central_panel(ctx, |ui| { - let mut inner_action = AppAction::None; - - // Add the scroll area to make the content scrollable both vertically and horizontally - egui::ScrollArea::both() - .auto_shrink([false; 2]) // Prevent shrinking when content is less than the available area - .show(ui, |ui| { - ui.add_space(10.0); - ui.heading("Follow these steps to import your wallet."); - - ui.add_space(5.0); - - ui.heading("1. Select the seed phrase length and enter all words."); - self.render_seed_phrase_input(ui); - - // Check seed phrase validity whenever all words are filled - if self.seed_phrase_words.iter().all(|string| !string.is_empty()) { - match Mnemonic::parse_normalized(self.seed_phrase_words.join(" ").as_str()) { - Ok(mnemonic) => { - self.seed_phrase = Some(mnemonic); - // Clear any existing seed phrase error - if let Some(ref mut error) = self.error { - if error.contains("Invalid seed phrase") { - self.error = None; - } - } - } - Err(_) => { - self.seed_phrase = None; - self.error = Some("Invalid seed phrase. Please check that all words are spelled correctly and are valid BIP39 words.".to_string()); - } - } - } else { - // Clear seed phrase and error if not all words are filled - self.seed_phrase = None; - if let Some(ref mut error) = self.error { - if error.contains("Invalid seed phrase") { - self.error = None; - } - } - } - - // Display error message if seed phrase is invalid - if let Some(ref error_msg) = self.error { - if error_msg.contains("Invalid seed phrase") { - ui.add_space(10.0); - ui.colored_label(Color32::from_rgb(255, 100, 100), error_msg); - } - } - - if self.seed_phrase.is_none() { - return; - } - - ui.add_space(20.0); - - ui.heading("2. Select a wallet name to remember it. (This will not go to the blockchain)"); - - ui.add_space(8.0); - - ui.horizontal(|ui| { - ui.label("Wallet Name:"); - ui.text_edit_singleline(&mut self.alias_input); - }); - - ui.add_space(20.0); - - ui.heading("3. Add a password that must be used to unlock the wallet. (Optional but recommended)"); - - ui.add_space(8.0); - - ui.horizontal(|ui| { - ui.label("Optional Password:"); - if ui.text_edit_singleline(&mut self.password).changed() { - if !self.password.is_empty() { - let estimate = zxcvbn(&self.password, &[]); - - // Convert Score to u8 - let score_u8 = u8::from(estimate.score()); - - // Use the score to determine password strength percentage - self.password_strength = score_u8 as f64 * 25.0; // Since score ranges from 0 to 4 - - // Get the estimated crack time in seconds - let estimated_seconds = estimate.crack_times().offline_slow_hashing_1e4_per_second(); - - // Format the estimated time to a human-readable string - self.estimated_time_to_crack = estimated_seconds.to_string(); - } else { - self.password_strength = 0.0; - self.estimated_time_to_crack = String::new(); - } - } - }); - - ui.add_space(10.0); - ui.horizontal(|ui| { - ui.label("Password Strength:"); - - // Since score ranges from 0 to 4, adjust percentage accordingly - let strength_percentage = (self.password_strength / 100.0).min(1.0); - let fill_color = match self.password_strength as i32 { - 0..=25 => Color32::from_rgb(255, 182, 193), // Light pink - 26..=50 => Color32::from_rgb(255, 224, 130), // Light yellow - 51..=75 => Color32::from_rgb(144, 238, 144), // Light green - _ => Color32::from_rgb(90, 200, 90), // Medium green - }; - ui.add( - egui::ProgressBar::new(strength_percentage as f32) - .desired_width(200.0) - .show_percentage() - .text(match self.password_strength as i32 { - 0 => "None".to_string(), - 1..=25 => "Very Weak".to_string(), - 26..=50 => "Weak".to_string(), - 51..=75 => "Strong".to_string(), - _ => "Very Strong".to_string(), - }) - .fill(fill_color), - ); - }); - - ui.add_space(10.0); - ui.label(format!( - "Estimated time to crack: {}", - self.estimated_time_to_crack - )); - - // if self.app_context.password_info.is_none() { - // ui.add_space(10.0); - // ui.checkbox(&mut self.use_password_for_app, "Use password for Dash Evo Tool loose keys (recommended)"); - // } - - ui.add_space(20.0); - - ui.heading("4. Save the wallet."); - ui.add_space(5.0); - - // Centered "Save Wallet" button at the bottom - ui.with_layout(Layout::centered_and_justified(Direction::TopDown), |ui| { - let save_button = egui::Button::new( - RichText::new("Save Wallet").strong().size(30.0), - ) - .min_size(Vec2::new(300.0, 60.0)) - .corner_radius(10.0) - .stroke(Stroke::new(1.5, Color32::WHITE)) - .sense(if self.seed_phrase.is_some() { - egui::Sense::click() - } else { - egui::Sense::hover() - }); - - if ui.add(save_button).clicked() { - match self.save_wallet() { - Ok(save_wallet_action) => { - inner_action = save_wallet_action; - } - Err(e) => { - self.error = Some(e) - } - } - } - }); - }); - - inner_action - }); - - action - } -} From c1bd76e04c5df8ea8bd0841ef7cf880cefe37c3c Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Wed, 11 Feb 2026 15:06:34 +0100 Subject: [PATCH 6/7] chore: remove redundant display logic --- src/ui/tokens/claim_tokens_screen.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/ui/tokens/claim_tokens_screen.rs b/src/ui/tokens/claim_tokens_screen.rs index ddce02f9a..f08aadcd8 100644 --- a/src/ui/tokens/claim_tokens_screen.rs +++ b/src/ui/tokens/claim_tokens_screen.rs @@ -277,9 +277,7 @@ impl ScreenLike for ClaimTokensScreen { fn display_message(&mut self, message: &str, message_type: MessageType) { match message_type { MessageType::Success => { - if message.contains("Claimed") || message == "ClaimTokens" { - self.status = ClaimTokensStatus::Complete; - } + // no-op, handled in display_task_result } MessageType::Error => { self.status = ClaimTokensStatus::ErrorMessage(message.to_string()); From 96bb5b6acb6ad70acff57f3bfd6ab61a6e51e5b1 Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Tue, 17 Feb 2026 10:27:23 +0100 Subject: [PATCH 7/7] chore: fix error message --- src/backend_task/tokens/mod.rs | 38 ++++++++++++++++++++++++---------- 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/src/backend_task/tokens/mod.rs b/src/backend_task/tokens/mod.rs index 9034babea..8f7ffad12 100644 --- a/src/backend_task/tokens/mod.rs +++ b/src/backend_task/tokens/mod.rs @@ -12,6 +12,8 @@ use dash_sdk::{ Sdk, dpp::{ ProtocolError, + consensus::ConsensusError, + consensus::state::state_error::StateError, data_contract::{ TokenConfiguration, TokenContractPosition, associated_token::{ @@ -499,17 +501,31 @@ impl AppContext { .await .map_err(|e| format!("Failed to claim all tokens: {e}")) } else { - self.claim_token( - data_contract.clone(), - *token_position, - actor_identity, - *distribution_type, - signing_key.clone(), - public_note.clone(), - sdk, - ) - .await - .map_err(|e| format!("Failed to claim tokens: {e}")) + match self + .claim_token( + data_contract.clone(), + *token_position, + actor_identity, + *distribution_type, + signing_key.clone(), + public_note.clone(), + sdk, + ) + .await + { + Err(dash_sdk::Error::Protocol(ProtocolError::ConsensusError(ce))) + if matches!( + *ce, + ConsensusError::StateError( + StateError::InvalidTokenClaimNoCurrentRewards(_) + ), + ) => + { + Ok(BackendTaskSuccessResult::TokensClaimed(0)) + } + Ok(result) => Ok(result), + Err(e) => Err(format!("Failed to claim tokens: {e}")), + } } } TokenTask::EstimatePerpetualTokenRewardsWithExplanation {