Skip to content

Commit 3f16436

Browse files
authored
Merge branch 'v1.0-dev' into ci/skip-claude-review-without-auth
2 parents 77cb2e2 + cd4b52f commit 3f16436

5 files changed

Lines changed: 153 additions & 2 deletions

File tree

src/app.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1356,6 +1356,11 @@ impl App for AppState {
13561356
}
13571357

13581358
fn on_exit(&mut self, _gl: Option<&eframe::glow::Context>) {
1359+
// On macOS, order windows out before winit tears down the event
1360+
// handler. This lets AppKit properly clean up display-related KVO
1361+
// observers (TouchBar, etc.) while views are still alive.
1362+
crate::platform::order_out_all_windows();
1363+
13591364
// If shutdown_receiver is Some, the async shutdown was already initiated
13601365
// in update(). Skip the blocking fallback to avoid double-shutdown.
13611366
// The blocking path only runs when the window was force-closed without

src/backend_task/contested_names/vote_on_dpns_name.rs

Lines changed: 88 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,29 @@ use dash_sdk::dpp::data_contract::document_type::accessors::DocumentTypeV0Getter
99
use dash_sdk::dpp::identity::accessors::IdentityGettersV0;
1010
use dash_sdk::dpp::platform_value::Value;
1111
use dash_sdk::dpp::platform_value::string_encoding::Encoding;
12+
use dash_sdk::dpp::util::strings::convert_to_homograph_safe_chars;
1213
use dash_sdk::dpp::voting::vote_choices::resource_vote_choice::ResourceVoteChoice;
1314
use dash_sdk::dpp::voting::vote_polls::contested_document_resource_vote_poll::ContestedDocumentResourceVotePoll;
1415
use dash_sdk::dpp::voting::votes::Vote;
1516
use dash_sdk::dpp::voting::votes::resource_vote::ResourceVote;
1617
use dash_sdk::dpp::voting::votes::resource_vote::v0::ResourceVoteV0;
18+
use dash_sdk::drive::query::vote_polls_by_document_type_query::VotePollsByDocumentTypeQuery;
19+
use dash_sdk::platform::FetchMany;
1720
use dash_sdk::platform::transition::vote::PutVote;
21+
use dash_sdk::query_types::ContestedResource;
1822
use std::sync::Arc;
1923

24+
/// Build `[Value::from("dash"), Value::Text(normalized_label.to_owned())]` for a DPNS vote poll.
25+
///
26+
/// Caller must pre-normalize the label via `convert_to_homograph_safe_chars`
27+
/// (`alice` → `a11ce`); Platform indexes polls under the normalized form.
28+
fn dpns_vote_poll_index_values(normalized_label: &str) -> Vec<Value> {
29+
vec![
30+
Value::from("dash"),
31+
Value::Text(normalized_label.to_owned()),
32+
]
33+
}
34+
2035
impl AppContext {
2136
pub(super) async fn vote_on_dpns_name(
2237
self: &Arc<Self>,
@@ -42,15 +57,44 @@ impl AppContext {
4257
});
4358
};
4459

45-
let index_values = [Value::from("dash"), Value::Text(name.to_owned())];
60+
let normalized_label = convert_to_homograph_safe_chars(name);
61+
let index_values = dpns_vote_poll_index_values(&normalized_label);
4662

4763
let vote_poll = ContestedDocumentResourceVotePoll {
4864
index_name: contested_index.name.clone(),
49-
index_values: index_values.to_vec(),
65+
index_values,
5066
document_type_name: document_type.name().to_string(),
5167
contract_id: data_contract.id(),
5268
};
5369

70+
// Pre-flight: confirm Platform has an open poll for this label before
71+
// broadcasting — fails fast with VotePollNotFound if it doesn't.
72+
let existence_query = VotePollsByDocumentTypeQuery {
73+
contract_id: data_contract.id(),
74+
document_type_name: document_type.name().to_string(),
75+
index_name: contested_index.name.clone(),
76+
start_index_values: vec![Value::from("dash")],
77+
end_index_values: vec![],
78+
// Start exactly at our normalized label (inclusive) — a single
79+
// row is enough to confirm the poll exists.
80+
start_at_value: Some((Value::Text(normalized_label.clone()), true)),
81+
limit: Some(1),
82+
order_ascending: true,
83+
};
84+
85+
let resources = ContestedResource::fetch_many(sdk, existence_query)
86+
.await
87+
.map_err(TaskError::from)?;
88+
let poll_exists = resources
89+
.0
90+
.iter()
91+
.any(|r| r.0.as_str() == Some(normalized_label.as_str()));
92+
if !poll_exists {
93+
return Err(TaskError::VotePollNotFound {
94+
name: name.to_owned(),
95+
});
96+
}
97+
5498
let mut vote_results = vec![];
5599

56100
for qualified_identity in voters.iter() {
@@ -84,3 +128,45 @@ impl AppContext {
84128
Ok(BackendTaskSuccessResult::DPNSVoteResults(vote_results))
85129
}
86130
}
131+
132+
#[cfg(test)]
133+
mod tests {
134+
use super::*;
135+
136+
#[test]
137+
fn index_values_uses_the_given_normalized_label() {
138+
// Given: a pre-normalized DPNS label (homographs already substituted).
139+
let normalized = "a11ce";
140+
141+
// When: constructing the vote poll index values.
142+
let values = dpns_vote_poll_index_values(normalized);
143+
144+
// Then: first element is the `"dash"` parent, second is the label as-given.
145+
assert_eq!(values.len(), 2);
146+
assert_eq!(values[0], Value::from("dash"));
147+
assert_eq!(values[1], Value::Text("a11ce".to_owned()));
148+
}
149+
150+
#[test]
151+
fn index_values_do_not_renormalize_the_label() {
152+
// Given: a label that still contains homograph characters.
153+
let not_yet_normalized = "alice";
154+
155+
// When: passing it directly to the helper (violating the contract).
156+
let values = dpns_vote_poll_index_values(not_yet_normalized);
157+
158+
// Then: the helper does NOT renormalize — the raw label is returned as-is.
159+
// (Caller is responsible for normalizing before calling.)
160+
assert_eq!(values[1], Value::Text("alice".to_owned()));
161+
}
162+
163+
#[test]
164+
fn convert_to_homograph_safe_chars_maps_alice_to_a11ce() {
165+
// Given: the canonical DPNS homograph substitutions (i/l → 1, o → 0).
166+
// When: normalizing a label with i/l/o.
167+
let normalized = convert_to_homograph_safe_chars("alice");
168+
169+
// Then: the result matches the constant used by the vote poll tests.
170+
assert_eq!(normalized, "a11ce");
171+
}
172+
}

src/backend_task/error.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -842,6 +842,16 @@ pub enum TaskError {
842842
)]
843843
NoVotingIdentity { identity_id: String },
844844

845+
/// No open vote poll was found on Platform for the given DPNS name.
846+
///
847+
/// Surfaced by the pre-flight existence check in `vote_on_dpns_name`,
848+
/// before any state transition is broadcast. Short-circuits a ~70 s
849+
/// retry chain that would otherwise expire with an opaque timeout.
850+
#[error(
851+
"The contested name \"{name}\" is not currently open for voting. It may have been resolved or may not exist. Refresh the contested names list and try again."
852+
)]
853+
VotePollNotFound { name: String },
854+
845855
/// The identity does not have an authentication key required to sign documents.
846856
#[error(
847857
"This identity does not have a key for signing documents. Please add an authentication key."

src/platform/macos.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,43 @@ use objc2::MainThreadMarker;
22
use objc2::msg_send;
33
use objc2::runtime::{AnyClass, AnyObject};
44

5+
/// Orders all application windows out (hides them) so that AppKit can
6+
/// properly tear down display-related observations — in particular
7+
/// `_NSTouchBarFinderObservation` KVO observers — while the windows and
8+
/// their views are still alive.
9+
///
10+
/// Without this, winit's event handler teardown triggers a display cycle
11+
/// flush that finds KVO observers on inconsistent objects, causing a
12+
/// SIGILL crash via `_crashOnException:`.
13+
///
14+
/// Must be called from `on_exit()` before returning control to the
15+
/// eframe/winit shutdown path.
16+
pub fn order_out_all_windows(_mtm: MainThreadMarker) {
17+
unsafe {
18+
let Some(cls) = AnyClass::get(c"NSApplication") else {
19+
return;
20+
};
21+
let app: *mut AnyObject = msg_send![cls, sharedApplication];
22+
if app.is_null() {
23+
return;
24+
}
25+
26+
let windows: *mut AnyObject = msg_send![app, windows];
27+
if windows.is_null() {
28+
return;
29+
}
30+
31+
let count: usize = msg_send![windows, count];
32+
for i in 0..count {
33+
let window: *mut AnyObject = msg_send![windows, objectAtIndex: i];
34+
if !window.is_null() {
35+
let _: () = msg_send![window, orderOut: std::ptr::null::<AnyObject>()];
36+
}
37+
}
38+
tracing::debug!("Ordered out {count} windows for clean shutdown");
39+
}
40+
}
41+
542
/// Queries `accessibilityChildren` on the key window's content view to force
643
/// macOS to call the AccessKit adapter's subclassed method, which transitions
744
/// the adapter from `Inactive` to `Active`. Without this, tools like Peekaboo

src/platform/mod.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,16 @@ pub fn force_accessibility_activation() -> bool {
2929
true
3030
}
3131
}
32+
33+
/// Orders all windows out before shutdown so macOS can clean up
34+
/// display-related observations. No-op on non-macOS platforms.
35+
pub fn order_out_all_windows() {
36+
#[cfg(target_os = "macos")]
37+
{
38+
let Some(mtm) = objc2::MainThreadMarker::new() else {
39+
tracing::error!("order_out_all_windows called from non-main thread");
40+
return;
41+
};
42+
macos::order_out_all_windows(mtm);
43+
}
44+
}

0 commit comments

Comments
 (0)