Skip to content

Commit 8f98180

Browse files
feat(sdk): expose the label each contender actually requested (#4331)
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent 14e2419 commit 8f98180

7 files changed

Lines changed: 558 additions & 54 deletions

File tree

packages/rs-sdk-ffi/src/contested_resource/queries/vote_state.rs

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
use crate::types::SDKHandle;
22
use crate::{DashSDKError, DashSDKErrorCode, DashSDKResult, DashSDKResultDataType};
3+
use dash_sdk::dpp::data_contract::accessors::v0::DataContractV0Getters;
4+
use dash_sdk::dpp::document::DocumentV0Getters;
35
use dash_sdk::dpp::platform_value::Value;
46
use dash_sdk::dpp::voting::contender_structs::ContenderWithSerializedDocument;
57
use dash_sdk::dpp::voting::vote_info_storage::contested_document_vote_poll_winner_info::ContestedDocumentVotePollWinnerInfo;
68
use dash_sdk::dpp::voting::vote_polls::contested_document_resource_vote_poll::ContestedDocumentResourceVotePoll;
79
use dash_sdk::drive::query::vote_poll_vote_state_query::ContestedDocumentVotePollDriveQuery;
810
use dash_sdk::platform::FetchMany;
11+
use dash_sdk::platform::{DataContract, Fetch};
912
use dash_sdk::query_types::Contenders;
1013
use std::ffi::{c_char, c_void, CStr, CString};
1114

@@ -235,6 +238,19 @@ fn get_contested_resource_vote_state(
235238
}
236239
// Add contenders
237240
if result_type.has_documents() {
241+
// Decode each contender's document so callers get the
242+
// label the requester actually typed ("pizza") next to the
243+
// homograph-normalized index value ("p1zza"). Without this
244+
// a UI can only show the normalized form, which reads as a
245+
// typo to the person who submitted it.
246+
//
247+
// Best-effort: the contract fetch or a single decode
248+
// failing must not fail the whole query, so `label` is
249+
// simply absent for rows that could not be decoded and the
250+
// caller falls back to the normalized value.
251+
let contract: Option<DataContract> =
252+
DataContract::fetch(&sdk, contract_id).await.ok().flatten();
253+
238254
let contenders_json: Vec<String> = contenders.contenders
239255
.iter()
240256
.map(|(id, contender)| {
@@ -245,13 +261,36 @@ fn get_contested_resource_vote_state(
245261
r#""document":null"#.to_string()
246262
};
247263

264+
let label_json = contract
265+
.as_ref()
266+
.and_then(|contract| {
267+
let doc_type = contract
268+
.document_type_for_name(document_type_name_str)
269+
.ok()?;
270+
let decoded = contender
271+
.try_to_contender(doc_type, sdk.version())
272+
.ok()?;
273+
let label = decoded
274+
.document()
275+
.as_ref()?
276+
.get("label")?
277+
.as_str()?
278+
.to_string();
279+
Some(format!(
280+
r#","label":{}"#,
281+
serde_json::to_string(&label).ok()?
282+
))
283+
})
284+
.unwrap_or_default();
285+
248286
let vote_count = contender.vote_tally().unwrap_or(0);
249287

250288
format!(
251-
r#"{{"identity_id":"{}","vote_count":{},{}}}"#,
289+
r#"{{"identity_id":"{}","vote_count":{},{}{}}}"#,
252290
bs58::encode(id.as_bytes()).into_string(),
253291
vote_count,
254-
document_json
292+
document_json,
293+
label_json
255294
)
256295
})
257296
.collect();

packages/rs-sdk-ffi/src/dpns/queries/contested.rs

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
//! FFI bindings for contested DPNS username queries
22
3+
use dash_sdk::dpp::data_contract::accessors::v0::DataContractV0Getters;
4+
use dash_sdk::dpp::document::DocumentV0Getters;
5+
use dash_sdk::dpp::system_data_contracts::{load_system_data_contract, SystemDataContract};
36
use std::ffi::{CStr, CString};
47
use std::os::raw::c_char;
58

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

489492
match result {
490493
Ok(names_with_contest_info) => {
494+
// Baked-in system contract: no network round trip, and it is the
495+
// schema these documents were written against.
496+
let platform_version = sdk.version();
497+
let dpns_domain_type =
498+
load_system_data_contract(SystemDataContract::DPNS, platform_version)
499+
.ok()
500+
.and_then(|contract| contract.document_type_cloned_for_name("domain").ok());
501+
491502
let count = names_with_contest_info.len();
492503
let mut names = Vec::with_capacity(count);
493504

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

526+
// The requester's own spelling ("pizza"), which the
527+
// normalized contest name ("p1zza") does not preserve.
528+
// Null when the document can't be decoded — callers fall
529+
// back to the normalized name rather than guessing.
530+
let c_label = dpns_domain_type
531+
.as_ref()
532+
.and_then(|doc_type| {
533+
let decoded = votes
534+
.try_to_contender(doc_type.as_ref(), platform_version)
535+
.ok()?;
536+
let label = decoded.document().as_ref()?.get("label")?.as_str()?;
537+
CString::new(label).ok().map(|s| s.into_raw())
538+
})
539+
.unwrap_or(std::ptr::null_mut());
540+
515541
contenders.push(DashSDKContender {
516542
identity_id: c_id,
517543
vote_count,
544+
label: c_label,
518545
});
519546
}
520547

548+
// Ordered, de-duplicated requested labels. Derived here so the
549+
// display policy lives in one place rather than being
550+
// re-implemented by each language binding.
551+
let mut requested_labels: Vec<*mut c_char> = Vec::new();
552+
let mut seen_labels: Vec<String> = Vec::new();
553+
for contender in &contenders {
554+
if contender.label.is_null() {
555+
continue;
556+
}
557+
let label = CStr::from_ptr(contender.label)
558+
.to_string_lossy()
559+
.into_owned();
560+
if label.is_empty() || seen_labels.contains(&label) {
561+
continue;
562+
}
563+
if let Ok(c_label) = CString::new(label.clone()) {
564+
seen_labels.push(label);
565+
requested_labels.push(c_label.into_raw());
566+
}
567+
}
568+
let requested_label_count = requested_labels.len();
569+
let requested_labels_ptr = if requested_labels.is_empty() {
570+
std::ptr::null_mut()
571+
} else {
572+
Box::into_raw(requested_labels.into_boxed_slice()) as *mut *mut c_char
573+
};
574+
521575
let contender_count = contenders.len();
522576
let contenders_ptr = if contenders.is_empty() {
523577
std::ptr::null_mut()
@@ -529,6 +583,8 @@ pub unsafe extern "C" fn dash_sdk_dpns_get_non_resolved_contests_for_identity(
529583
let contest_info_c = DashSDKContestInfo {
530584
contenders: contenders_ptr,
531585
contender_count,
586+
requested_labels: requested_labels_ptr,
587+
requested_label_count,
532588
abstain_votes: contest_info.contenders.abstain_vote_tally.unwrap_or(0),
533589
lock_votes: contest_info.contenders.lock_vote_tally.unwrap_or(0),
534590
end_time: contest_info.end_time,
@@ -587,6 +643,14 @@ pub unsafe extern "C" fn dash_sdk_dpns_get_contested_non_resolved_usernames(
587643

588644
match result {
589645
Ok(names_with_contest_info) => {
646+
// Baked-in system contract: no network round trip, and it is the
647+
// schema these documents were written against.
648+
let platform_version = sdk.version();
649+
let dpns_domain_type =
650+
load_system_data_contract(SystemDataContract::DPNS, platform_version)
651+
.ok()
652+
.and_then(|contract| contract.document_type_cloned_for_name("domain").ok());
653+
590654
let count = names_with_contest_info.len();
591655
let mut names = Vec::with_capacity(count);
592656

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

678+
// The requester's own spelling ("pizza"), which the
679+
// normalized contest name ("p1zza") does not preserve.
680+
// Null when the document can't be decoded — callers fall
681+
// back to the normalized name rather than guessing.
682+
let c_label = dpns_domain_type
683+
.as_ref()
684+
.and_then(|doc_type| {
685+
let decoded = votes
686+
.try_to_contender(doc_type.as_ref(), platform_version)
687+
.ok()?;
688+
let label = decoded.document().as_ref()?.get("label")?.as_str()?;
689+
CString::new(label).ok().map(|s| s.into_raw())
690+
})
691+
.unwrap_or(std::ptr::null_mut());
692+
614693
contenders.push(DashSDKContender {
615694
identity_id: c_id,
616695
vote_count,
696+
label: c_label,
617697
});
618698
}
619699

700+
// Ordered, de-duplicated requested labels. Derived here so the
701+
// display policy lives in one place rather than being
702+
// re-implemented by each language binding.
703+
let mut requested_labels: Vec<*mut c_char> = Vec::new();
704+
let mut seen_labels: Vec<String> = Vec::new();
705+
for contender in &contenders {
706+
if contender.label.is_null() {
707+
continue;
708+
}
709+
let label = CStr::from_ptr(contender.label)
710+
.to_string_lossy()
711+
.into_owned();
712+
if label.is_empty() || seen_labels.contains(&label) {
713+
continue;
714+
}
715+
if let Ok(c_label) = CString::new(label.clone()) {
716+
seen_labels.push(label);
717+
requested_labels.push(c_label.into_raw());
718+
}
719+
}
720+
let requested_label_count = requested_labels.len();
721+
let requested_labels_ptr = if requested_labels.is_empty() {
722+
std::ptr::null_mut()
723+
} else {
724+
Box::into_raw(requested_labels.into_boxed_slice()) as *mut *mut c_char
725+
};
726+
620727
let contender_count = contenders.len();
621728
let contenders_ptr = if contenders.is_empty() {
622729
std::ptr::null_mut()
@@ -628,6 +735,8 @@ pub unsafe extern "C" fn dash_sdk_dpns_get_contested_non_resolved_usernames(
628735
let contest_info_c = DashSDKContestInfo {
629736
contenders: contenders_ptr,
630737
contender_count,
738+
requested_labels: requested_labels_ptr,
739+
requested_label_count,
631740
abstain_votes: contest_info.contenders.abstain_vote_tally.unwrap_or(0),
632741
lock_votes: contest_info.contenders.lock_vote_tally.unwrap_or(0),
633742
end_time: contest_info.end_time,

0 commit comments

Comments
 (0)