Skip to content
Open
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
102 changes: 82 additions & 20 deletions src/ui/tools/transition_visualizer_screen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,40 @@ use dash_sdk::dpp::state_transition::StateTransition;
use dash_sdk::platform::Identifier;
use eframe::egui::{self, Color32, Context, ScrollArea, TextEdit, Ui, Window};
use egui::RichText;
use serde_json::Value;
use serde_json::{Value, json};
use std::sync::Arc;
use std::time::{Duration, Instant};

fn state_transition_to_pretty_json(state_transition: &StateTransition) -> Result<String, String> {
match serde_json::to_string_pretty(state_transition) {
Ok(json) => Ok(json),
Err(e) => {
let error = e.to_string();
let debug_output = format!("{:#?}", state_transition);
tracing::warn!(
error = %error,
"parsed state transition but failed to serialize it to JSON; using debug fallback"
);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
tracing::debug!(
state_transition = %debug_output,
"debug representation of state transition that failed JSON serialization"
);

serde_json::to_string_pretty(&json!({
"warning": "State transition parsed successfully, but JSON serialization failed. Showing debug representation instead.",
"serialization_error": error,
"debug": debug_output,
}))
.map_err(|fallback_error| {
format!(
"Failed to serialize state transition to JSON ({}) and failed to build debug fallback ({})",
e, fallback_error
)
})
}
}
}

#[derive(PartialEq)]
enum TransitionBroadcastStatus {
NotStarted,
Expand All @@ -33,6 +63,7 @@ pub struct TransitionVisualizerScreen {
pub app_context: Arc<AppContext>,
input_data: String,
parsed_json: Option<String>,
parsed_state_transition: Option<StateTransition>,
parse_error: Option<(String, Instant)>,
broadcast_status: TransitionBroadcastStatus,
submit_banner: Option<BannerHandle>,
Expand All @@ -48,6 +79,7 @@ impl TransitionVisualizerScreen {
app_context: app_context.clone(),
input_data: String::new(),
parsed_json: None,
parsed_state_transition: None,
parse_error: None,
broadcast_status: TransitionBroadcastStatus::NotStarted,
submit_banner: None,
Expand Down Expand Up @@ -87,6 +119,7 @@ impl TransitionVisualizerScreen {
fn parse_input(&mut self) {
// Clear previous parse results...
self.parsed_json = None;
self.parsed_state_transition = None;
self.parse_error = None;
self.detected_contract_ids.clear();

Expand Down Expand Up @@ -117,10 +150,12 @@ impl TransitionVisualizerScreen {
// Try to deserialize into a StateTransition
match StateTransition::deserialize_from_bytes(&bytes) {
Ok(state_transition) => {
// Convert to JSON
match serde_json::to_string_pretty(&state_transition) {
// Convert to JSON, falling back to a debug view for transition variants
// whose map keys cannot currently be represented as JSON object keys.
match state_transition_to_pretty_json(&state_transition) {
Ok(json) => {
self.parsed_json = Some(json.clone());
self.parsed_state_transition = Some(state_transition);

// Extract contract IDs from the JSON
if let Ok(json_value) = serde_json::from_str::<Value>(&json) {
Expand All @@ -131,14 +166,15 @@ impl TransitionVisualizerScreen {
}
}
Err(e) => {
self.parse_error = Some((
format!("Failed to serialize to JSON: {}", e),
Instant::now(),
));
self.parse_error = Some((e, Instant::now()));
}
}
}
Err(e) => {
tracing::warn!(
error = %e,
"failed to deserialize state transition input"
);
self.parse_error =
Some((format!("Failed to parse: {}", e), Instant::now()));
}
Expand Down Expand Up @@ -228,23 +264,27 @@ impl TransitionVisualizerScreen {
if ComponentStyles::add_primary_button(ui, "Broadcast Transition to Platform")
.clicked()
{
// Mark as submitting
self.submit_banner.take_and_clear();
let handle = MessageBanner::set_global(
ui.ctx(),
"Submitting transition...",
MessageType::Info,
);
handle.with_elapsed();
self.submit_banner = Some(handle);
self.broadcast_status = TransitionBroadcastStatus::Submitting;
if let Some(state_transition) = self.parsed_state_transition.clone() {
// Mark as submitting
self.submit_banner.take_and_clear();
let handle = MessageBanner::set_global(
ui.ctx(),
"Submitting transition...",
MessageType::Info,
);
handle.with_elapsed();
self.submit_banner = Some(handle);
self.broadcast_status = TransitionBroadcastStatus::Submitting;

if let Some(json) = &self.parsed_json
&& let Ok(state_transition) = serde_json::from_str(json)
{
app_action = AppAction::BackendTask(
BackendTask::BroadcastStateTransition(state_transition),
);
} else {
MessageBanner::set_global(
ui.ctx(),
"Parsed transition is unavailable for broadcast",
MessageType::Error,
);
}
}
}
Expand Down Expand Up @@ -518,3 +558,25 @@ impl ScreenLike for TransitionVisualizerScreen {
action
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn platform_address_transition_with_non_string_map_keys_uses_debug_fallback() {
let input = "DAABAEZJ8lqSWSBK4JRgM6A4o7EoAM/IDPwAD0JAAQA2I7Ax2quQXVnyAKnezuNPE75q7vwAD0JAAQAAAAEAQR/vseNQooUa8QVJbujMgP22M8EzL3C9AAEibx6iMtmWHA1ou89lGCCtHgJmKcieCtdhZMhqcnU9O+fc4Y575ryF";
let bytes = STANDARD
.decode(input)
.expect("issue #573 base64 should decode");
let state_transition = StateTransition::deserialize_from_bytes(&bytes)
.expect("issue #573 transition should deserialize");

let json = state_transition_to_pretty_json(&state_transition)
.expect("debug fallback should produce valid JSON");

assert!(json.contains("parsed successfully"));
assert!(json.contains("key must be a string"));
assert!(json.contains("AddressFundsTransfer"));
}
}
Loading