Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/backend_task/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ pub enum BackendTaskSuccessResult {
SetTokenPrice(FeeResult),
DestroyedFrozenFunds(FeeResult),
ClaimedTokens(FeeResult),
TokensClaimed(TokenAmount),
Comment on lines 221 to +222
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if ClaimedTokens (the old variant) is still referenced anywhere
echo "=== Usages of ClaimedTokens ==="
rg -n --type=rust '\bClaimedTokens\b' -C2

echo ""
echo "=== Usages of TokensClaimed ==="
rg -n --type=rust '\bTokensClaimed\b' -C2

Repository: dashpay/dash-evo-tool

Length of output: 3259


Remove unused ClaimedTokens(FeeResult) variant.

The ClaimedTokens variant is dead code—it's defined but never used anywhere in the codebase. The active variant is TokensClaimed(TokenAmount), which is returned in claim_tokens.rs and matched in claim_tokens_screen.rs. Since they serve different semantic purposes (fee tracking vs. amount tracking), remove ClaimedTokens entirely rather than renaming.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/backend_task/mod.rs` around lines 221 - 222, Remove the dead enum variant
ClaimedTokens(FeeResult) from the backend task enum definition (leave
TokensClaimed(TokenAmount) as-is), and delete any now-unused imports or type
references tied to FeeResult; ensure no pattern matches or constructors
reference ClaimedTokens (search for ClaimedTokens and remove or refactor them)
and run tests/build to confirm nothing else depends on it.

UpdatedTokenConfig(String, FeeResult), // The config item that was updated
FetchedTokenBalances,
SavedToken,
Expand Down
129 changes: 100 additions & 29 deletions src/backend_task/tokens/claim_tokens.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<DataContract>,
token_position: u16,
Expand All @@ -25,6 +34,69 @@ impl AppContext {
public_note: Option<String>,
sdk: &Sdk,
) -> Result<BackendTaskSuccessResult, String> {
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;
}
// 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<DataContract>,
token_position: u16,
actor_identity: &QualifiedIdentity,
distribution_type: TokenDistributionType,
signing_key: IdentityPublicKey,
public_note: Option<String>,
sdk: &Sdk,
) -> Result<BackendTaskSuccessResult, dash_sdk::Error> {
// Build
let mut builder = TokenClaimTransitionBuilder::new(
data_contract.clone(),
Expand All @@ -45,7 +117,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 {
Expand All @@ -54,19 +126,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
Expand All @@ -75,14 +146,16 @@ impl AppContext {
(document.get("claimerId"), document.get("amount"))
&& let (Value::Identifier(claimer_bytes), Value::U64(amount)) =
(claimer_value, amount_value)
&& let Ok(claimer_id) = Identifier::from_bytes(claimer_bytes)
&& let Err(e) =
self.insert_token_identity_balance(&token_id, &claimer_id, *amount)
{
tracing::error!(
"Failed to update token balance from claim document: {}",
e
);
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)
{
tracing::error!(error=?e, identity=?claimer_id, token_id=?token_id,
"Failed to update token balance from claim document",
);
}
}
}

Expand All @@ -92,24 +165,22 @@ impl AppContext {
(document.get("claimerId"), document.get("amount"))
&& let (Value::Identifier(claimer_bytes), Value::U64(amount)) =
(claimer_value, amount_value)
&& let Ok(claimer_id) = Identifier::from_bytes(claimer_bytes)
&& let Err(e) =
self.insert_token_identity_balance(&token_id, &claimer_id, *amount)
{
tracing::error!(
"Failed to update token balance from group action document: {}",
e
);
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)
{
tracing::error!(error=?e, identity=?claimer_id, token_id=?token_id,
"Failed to update token balance from group action document",
);
}
}
}
}
}

// Return success with fee result
use crate::backend_task::FeeResult;
use crate::model::fee_estimation::PlatformFeeEstimator;
let estimated_fee = PlatformFeeEstimator::new().estimate_document_batch(1);
let fee_result = FeeResult::new(estimated_fee, estimated_fee);
Ok(BackendTaskSuccessResult::ClaimedTokens(fee_result))
// Return success
Ok(BackendTaskSuccessResult::TokensClaimed(claimed_amount))
}
}
57 changes: 45 additions & 12 deletions src/backend_task/tokens/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ use dash_sdk::{
Sdk,
dpp::{
ProtocolError,
consensus::ConsensusError,
consensus::state::state_error::StateError,
data_contract::{
TokenConfiguration, TokenContractPosition,
associated_token::{
Expand Down Expand Up @@ -183,6 +185,7 @@ pub enum TokenTask {
distribution_type: TokenDistributionType,
signing_key: IdentityPublicKey,
public_note: Option<String>,
claim_all: bool,
},
EstimatePerpetualTokenRewardsWithExplanation {
identity_id: Identifier,
Expand Down Expand Up @@ -483,18 +486,48 @@ impl AppContext {
distribution_type,
signing_key,
public_note,
} => self
.claim_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 {
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 {
identity_id,
token_id,
Expand Down
33 changes: 33 additions & 0 deletions src/database/identities.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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],
)?;
Comment on lines +133 to +144
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

rg -n "fn execute" src/database/identities.rs -A 10

Repository: dashpay/dash-evo-tool

Length of output: 47


🏁 Script executed:

# Find the Database struct and impl block
rg -n "impl.*Database|struct Database" src/database/ -A 5 --type rust

Repository: dashpay/dash-evo-tool

Length of output: 7347


🏁 Script executed:

# Look at the full context of the method in question
sed -n '117,148p' src/database/identities.rs

Repository: dashpay/dash-evo-tool

Length of output: 1362


🏁 Script executed:

# Check other methods that use self.execute() to see if they also lock conn first
rg -n "self\.execute|self\.conn\.lock" src/database/identities.rs --type rust -B 2 -A 2

Repository: dashpay/dash-evo-tool

Length of output: 4126


🏁 Script executed:

rg -n "fn execute\(" src/database/ --type rust -A 15 | head -60

Repository: dashpay/dash-evo-tool

Length of output: 47


🏁 Script executed:

# Specifically look in mod.rs for the execute method
cat src/database/mod.rs

Repository: dashpay/dash-evo-tool

Length of output: 4348


🏁 Script executed:

# Verify the proposed fix works by checking similar patterns in the codebase
# Look for methods that lock conn, then use conn.execute() directly
rg -n "let conn = self.conn.lock" src/database/identities.rs -A 10 | grep -E "(conn\.execute|conn\.prepare)"

Repository: dashpay/dash-evo-tool

Length of output: 583


🏁 Script executed:

# Check if 'status' column has a default or is NOT NULL in the schema
rg -n "CREATE TABLE.*identity" src/database/ -A 20 --type rust

Repository: dashpay/dash-evo-tool

Length of output: 4501


Critical deadlock: self.execute() called while holding self.conn lock.

Line 133 acquires self.conn.lock(). The guard remains in scope through the end of the function. When self.execute() is called on line 140, it internally attempts to lock self.conn again (see mod.rs lines 39–41). Rust's Mutex is not reentrant, causing a deadlock on the same thread.

Use conn.execute() directly instead, consistent with other methods that hold the lock (e.g., set_identity_alias, delete_local_qualified_identity).

🐛 Proposed fix: use local connection guard for the INSERT
         // If the identity doesn't exist, insert it
         if count == 0 {
-            self.execute(
+            conn.execute(
                 "INSERT INTO identity (id, data, is_local, alias, identity_type, network)
              VALUES (?, ?, 0, ?, ?, ?)",
                 params![id, data, alias, identity_type, network],
             )?;
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/database/identities.rs` around lines 133 - 144, The code holds
self.conn.lock() (conn guard) and then calls self.execute(...), which deadlocks
because self.execute also locks self.conn; replace the call to self.execute
inside the guarded section with a direct call on the local connection (e.g., use
conn.execute(...) or conn.prepare(...).execute(...) with the same SQL and
params![id, data, alias, identity_type, network]) so the insert runs using the
existing guard instead of attempting to re-lock self.conn; ensure you
remove/self-contained the transaction on the conn guard and keep parameter order
identical.

}

Ok(())
}

/// Returns all local identities for the current network.
///
/// Stops on the first corrupted identity blob and returns an error.
Expand Down
Loading
Loading