Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
use crate::types::SDKHandle;
use crate::{DashSDKError, DashSDKErrorCode, DashSDKResult, DashSDKResultDataType};
use dash_sdk::dpp::data_contract::accessors::v0::DataContractV0Getters;
use dash_sdk::dpp::document::DocumentV0Getters;
use dash_sdk::dpp::platform_value::Value;
use dash_sdk::dpp::voting::contender_structs::ContenderWithSerializedDocument;
use dash_sdk::dpp::voting::vote_info_storage::contested_document_vote_poll_winner_info::ContestedDocumentVotePollWinnerInfo;
use dash_sdk::dpp::voting::vote_polls::contested_document_resource_vote_poll::ContestedDocumentResourceVotePoll;
use dash_sdk::drive::query::vote_poll_vote_state_query::ContestedDocumentVotePollDriveQuery;
use dash_sdk::platform::FetchMany;
use dash_sdk::platform::{DataContract, Fetch};
use dash_sdk::query_types::Contenders;
use std::ffi::{c_char, c_void, CStr, CString};

Expand Down Expand Up @@ -235,6 +238,19 @@ fn get_contested_resource_vote_state(
}
// Add contenders
if result_type.has_documents() {
// Decode each contender's document so callers get the
// label the requester actually typed ("pizza") next to the
// homograph-normalized index value ("p1zza"). Without this
// a UI can only show the normalized form, which reads as a
// typo to the person who submitted it.
//
// Best-effort: the contract fetch or a single decode
// failing must not fail the whole query, so `label` is
// simply absent for rows that could not be decoded and the
// caller falls back to the normalized value.
let contract: Option<DataContract> =
DataContract::fetch(&sdk, contract_id).await.ok().flatten();

Comment on lines 238 to +253

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟡 Suggestion: New label-decoding logic has no Rust-side test coverage

Verified: the only tests in this file's #[cfg(test)] module (lines 306-347) cover null-handle and null-contract-id guard paths — nothing exercises the new label-decoding logic added in this PR (contract fetch, document_type_for_name, try_to_contender, JSON-embedded label extraction), nor the mirrored logic in dpns/queries/contested.rs. The new Swift tests inject already-decoded label JSON directly, so they validate JSON consumption but not the Rust-side production logic — e.g. that a document lacking a label field, or one that fails to deserialize against the fetched contract, is silently omitted rather than producing malformed JSON or panicking.

source: ['claude', 'codex']

let contenders_json: Vec<String> = contenders.contenders
.iter()
.map(|(id, contender)| {
Expand All @@ -245,13 +261,36 @@ fn get_contested_resource_vote_state(
r#""document":null"#.to_string()
};

let label_json = contract
.as_ref()
.and_then(|contract| {
let doc_type = contract
.document_type_for_name(document_type_name_str)
.ok()?;
let decoded = contender
.try_to_contender(doc_type, sdk.version())
.ok()?;
let label = decoded
.document()
.as_ref()?
.get("label")?
.as_str()?
.to_string();
Some(format!(
r#","label":{}"#,
serde_json::to_string(&label).ok()?
))
})
.unwrap_or_default();
Comment on lines +264 to +284

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

💬 Nitpick: document_type_for_name looked up per contender instead of once per query

contract.document_type_for_name(document_type_name_str) is called inside the per-contender .map() closure (verified at line ~264), repeating a name-keyed lookup into the contract's document type map for every contender instead of resolving it once before the loop. Contests are typically small so the practical cost is negligible, but hoisting it out avoids the repeated lookup.

source: ['claude']


let vote_count = contender.vote_tally().unwrap_or(0);

format!(
r#"{{"identity_id":"{}","vote_count":{},{}}}"#,
r#"{{"identity_id":"{}","vote_count":{},{}{}}}"#,
bs58::encode(id.as_bytes()).into_string(),
vote_count,
document_json
document_json,
label_json
)
})
.collect();
Expand Down
109 changes: 109 additions & 0 deletions packages/rs-sdk-ffi/src/dpns/queries/contested.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
//! FFI bindings for contested DPNS username queries

use dash_sdk::dpp::data_contract::accessors::v0::DataContractV0Getters;
use dash_sdk::dpp::document::DocumentV0Getters;
use dash_sdk::dpp::system_data_contracts::{load_system_data_contract, SystemDataContract};
use std::ffi::{CStr, CString};
use std::os::raw::c_char;

Expand Down Expand Up @@ -488,6 +491,14 @@ pub unsafe extern "C" fn dash_sdk_dpns_get_non_resolved_contests_for_identity(

match result {
Ok(names_with_contest_info) => {
// Baked-in system contract: no network round trip, and it is the
// schema these documents were written against.
let platform_version = sdk.version();
let dpns_domain_type =
load_system_data_contract(SystemDataContract::DPNS, platform_version)
.ok()
.and_then(|contract| contract.document_type_cloned_for_name("domain").ok());

let count = names_with_contest_info.len();
let mut names = Vec::with_capacity(count);

Expand All @@ -512,12 +523,55 @@ pub unsafe extern "C" fn dash_sdk_dpns_get_non_resolved_contests_for_identity(
// Extract actual vote tally from ContenderWithSerializedDocument
let vote_count = votes.vote_tally().unwrap_or(0);

// The requester's own spelling ("pizza"), which the
// normalized contest name ("p1zza") does not preserve.
// Null when the document can't be decoded — callers fall
// back to the normalized name rather than guessing.
let c_label = dpns_domain_type
.as_ref()
.and_then(|doc_type| {
let decoded = votes
.try_to_contender(doc_type.as_ref(), platform_version)
.ok()?;
let label = decoded.document().as_ref()?.get("label")?.as_str()?;
CString::new(label).ok().map(|s| s.into_raw())
})
.unwrap_or(std::ptr::null_mut());

contenders.push(DashSDKContender {
identity_id: c_id,
vote_count,
label: c_label,
});
}

// Ordered, de-duplicated requested labels. Derived here so the
// display policy lives in one place rather than being
// re-implemented by each language binding.
let mut requested_labels: Vec<*mut c_char> = Vec::new();
let mut seen_labels: Vec<String> = Vec::new();
for contender in &contenders {
if contender.label.is_null() {
continue;
}
let label = CStr::from_ptr(contender.label)
.to_string_lossy()
.into_owned();
if label.is_empty() || seen_labels.contains(&label) {
continue;
}
if let Ok(c_label) = CString::new(label.clone()) {
seen_labels.push(label);
requested_labels.push(c_label.into_raw());
}
}
let requested_label_count = requested_labels.len();
let requested_labels_ptr = if requested_labels.is_empty() {
std::ptr::null_mut()
} else {
Box::into_raw(requested_labels.into_boxed_slice()) as *mut *mut c_char
};

let contender_count = contenders.len();
let contenders_ptr = if contenders.is_empty() {
std::ptr::null_mut()
Expand All @@ -529,6 +583,8 @@ pub unsafe extern "C" fn dash_sdk_dpns_get_non_resolved_contests_for_identity(
let contest_info_c = DashSDKContestInfo {
contenders: contenders_ptr,
contender_count,
requested_labels: requested_labels_ptr,
requested_label_count,
abstain_votes: contest_info.contenders.abstain_vote_tally.unwrap_or(0),
lock_votes: contest_info.contenders.lock_vote_tally.unwrap_or(0),
end_time: contest_info.end_time,
Expand Down Expand Up @@ -587,6 +643,14 @@ pub unsafe extern "C" fn dash_sdk_dpns_get_contested_non_resolved_usernames(

match result {
Ok(names_with_contest_info) => {
// Baked-in system contract: no network round trip, and it is the
// schema these documents were written against.
let platform_version = sdk.version();
let dpns_domain_type =
load_system_data_contract(SystemDataContract::DPNS, platform_version)
.ok()
.and_then(|contract| contract.document_type_cloned_for_name("domain").ok());

let count = names_with_contest_info.len();
let mut names = Vec::with_capacity(count);

Expand All @@ -611,12 +675,55 @@ pub unsafe extern "C" fn dash_sdk_dpns_get_contested_non_resolved_usernames(
// Extract actual vote tally from ContenderWithSerializedDocument
let vote_count = votes.vote_tally().unwrap_or(0);

// The requester's own spelling ("pizza"), which the
// normalized contest name ("p1zza") does not preserve.
// Null when the document can't be decoded — callers fall
// back to the normalized name rather than guessing.
let c_label = dpns_domain_type
.as_ref()
.and_then(|doc_type| {
let decoded = votes
.try_to_contender(doc_type.as_ref(), platform_version)
.ok()?;
let label = decoded.document().as_ref()?.get("label")?.as_str()?;
CString::new(label).ok().map(|s| s.into_raw())
})
.unwrap_or(std::ptr::null_mut());

contenders.push(DashSDKContender {
identity_id: c_id,
vote_count,
label: c_label,
});
}

// Ordered, de-duplicated requested labels. Derived here so the
// display policy lives in one place rather than being
// re-implemented by each language binding.
let mut requested_labels: Vec<*mut c_char> = Vec::new();
let mut seen_labels: Vec<String> = Vec::new();
for contender in &contenders {
if contender.label.is_null() {
continue;
}
let label = CStr::from_ptr(contender.label)
.to_string_lossy()
.into_owned();
if label.is_empty() || seen_labels.contains(&label) {
continue;
}
if let Ok(c_label) = CString::new(label.clone()) {
seen_labels.push(label);
requested_labels.push(c_label.into_raw());
}
}
let requested_label_count = requested_labels.len();
let requested_labels_ptr = if requested_labels.is_empty() {
std::ptr::null_mut()
} else {
Box::into_raw(requested_labels.into_boxed_slice()) as *mut *mut c_char
};

let contender_count = contenders.len();
let contenders_ptr = if contenders.is_empty() {
std::ptr::null_mut()
Expand All @@ -628,6 +735,8 @@ pub unsafe extern "C" fn dash_sdk_dpns_get_contested_non_resolved_usernames(
let contest_info_c = DashSDKContestInfo {
contenders: contenders_ptr,
contender_count,
requested_labels: requested_labels_ptr,
requested_label_count,
abstain_votes: contest_info.contenders.abstain_vote_tally.unwrap_or(0),
lock_votes: contest_info.contenders.lock_vote_tally.unwrap_or(0),
end_time: contest_info.end_time,
Expand Down
Loading
Loading