Add explicit vocabulary and named operation contexts
This commit is contained in:
+107
-44
@@ -287,17 +287,30 @@ pub trait OperationExecutor: Send + 'static {
|
||||
}
|
||||
}
|
||||
|
||||
enum DeliveryMode {
|
||||
FirstIssue,
|
||||
Reissue,
|
||||
}
|
||||
|
||||
struct PreparedDestinationContext {
|
||||
identity: PublicKey,
|
||||
include_qr: bool,
|
||||
mode: DeliveryMode,
|
||||
attempt: FirstIssueAttempt,
|
||||
generation: u64,
|
||||
destination: PreparedDestination,
|
||||
}
|
||||
|
||||
struct RestorePreviewContext {
|
||||
_archive_name: String,
|
||||
generation: u64,
|
||||
preview: RestorePreview,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ApplicationExecutor {
|
||||
prepared_destination: Option<(
|
||||
PublicKey,
|
||||
bool,
|
||||
bool,
|
||||
FirstIssueAttempt,
|
||||
u64,
|
||||
PreparedDestination,
|
||||
)>,
|
||||
restore_preview: Option<(String, u64, RestorePreview)>,
|
||||
prepared_destination: Option<PreparedDestinationContext>,
|
||||
restore_preview: Option<RestorePreviewContext>,
|
||||
}
|
||||
|
||||
impl OperationExecutor for ApplicationExecutor {
|
||||
@@ -379,14 +392,18 @@ impl OperationExecutor for ApplicationExecutor {
|
||||
attempt,
|
||||
prepared_generation: generation,
|
||||
};
|
||||
self.prepared_destination = Some((
|
||||
self.prepared_destination = Some(PreparedDestinationContext {
|
||||
identity,
|
||||
include_qr,
|
||||
reissue,
|
||||
mode: if reissue {
|
||||
DeliveryMode::Reissue
|
||||
} else {
|
||||
DeliveryMode::FirstIssue
|
||||
},
|
||||
attempt,
|
||||
generation,
|
||||
destination,
|
||||
));
|
||||
});
|
||||
CommandResult::DeliveryDestination(summary)
|
||||
}),
|
||||
UiCommand::DeliverFirstIssue {
|
||||
@@ -394,23 +411,16 @@ impl OperationExecutor for ApplicationExecutor {
|
||||
overwrite_confirmed,
|
||||
prepared_generation,
|
||||
} => {
|
||||
let (
|
||||
identity,
|
||||
include_qr,
|
||||
reissue,
|
||||
prepared_attempt,
|
||||
destination_generation,
|
||||
mut destination,
|
||||
) = self
|
||||
let prepared = self
|
||||
.prepared_destination
|
||||
.take()
|
||||
.ok_or_else(|| OperationError {
|
||||
message: "No prepared output destination is available.".to_owned(),
|
||||
})?;
|
||||
if reissue
|
||||
|| prepared_attempt != attempt
|
||||
if !matches!(prepared.mode, DeliveryMode::FirstIssue)
|
||||
|| prepared.attempt != attempt
|
||||
|| !destination_generation_matches(
|
||||
destination_generation,
|
||||
prepared.generation,
|
||||
prepared_generation,
|
||||
generation,
|
||||
)
|
||||
@@ -420,6 +430,9 @@ impl OperationExecutor for ApplicationExecutor {
|
||||
.to_owned(),
|
||||
});
|
||||
}
|
||||
let identity = prepared.identity;
|
||||
let include_qr = prepared.include_qr;
|
||||
let mut destination = prepared.destination;
|
||||
if destination.overwrite_confirmation_required() {
|
||||
if !overwrite_confirmed {
|
||||
return Err(OperationError {
|
||||
@@ -519,7 +532,11 @@ impl OperationExecutor for ApplicationExecutor {
|
||||
changes,
|
||||
preview_generation: generation,
|
||||
};
|
||||
self.restore_preview = Some((file_name, generation, preview));
|
||||
self.restore_preview = Some(RestorePreviewContext {
|
||||
_archive_name: file_name,
|
||||
generation,
|
||||
preview,
|
||||
});
|
||||
CommandResult::RestorePreview(summary)
|
||||
})
|
||||
}
|
||||
@@ -532,14 +549,12 @@ impl OperationExecutor for ApplicationExecutor {
|
||||
Ok(CommandResult::Cancelled)
|
||||
}
|
||||
UiCommand::ConfirmRestore { preview_generation } => {
|
||||
let (_, stored_generation, preview) =
|
||||
self.restore_preview.take().ok_or_else(|| OperationError {
|
||||
message:
|
||||
"Restore preview is no longer available; select the archive again."
|
||||
.to_owned(),
|
||||
})?;
|
||||
let context = self.restore_preview.take().ok_or_else(|| OperationError {
|
||||
message: "Restore preview is no longer available; select the archive again."
|
||||
.to_owned(),
|
||||
})?;
|
||||
if !restore_preview_generation_matches(
|
||||
stored_generation,
|
||||
context.generation,
|
||||
preview_generation,
|
||||
generation,
|
||||
) {
|
||||
@@ -547,7 +562,8 @@ impl OperationExecutor for ApplicationExecutor {
|
||||
message: "Restore preview is stale; select the archive again.".to_owned(),
|
||||
});
|
||||
}
|
||||
app.confirm_restore(preview).map(CommandResult::Restored)
|
||||
app.confirm_restore(context.preview)
|
||||
.map(CommandResult::Restored)
|
||||
}
|
||||
};
|
||||
result.map_err(|error| OperationError {
|
||||
@@ -573,28 +589,23 @@ impl OperationExecutor for ApplicationExecutor {
|
||||
overwrite_confirmed,
|
||||
prepared_generation,
|
||||
} => {
|
||||
let (
|
||||
prepared_identity,
|
||||
_include_qr,
|
||||
reissue,
|
||||
_attempt,
|
||||
destination_generation,
|
||||
mut destination,
|
||||
) = self
|
||||
let prepared = self
|
||||
.prepared_destination
|
||||
.take()
|
||||
.ok_or_else(|| OperationError {
|
||||
message: "No prepared output destination is available.".to_owned(),
|
||||
})?;
|
||||
if prepared_identity != identity
|
||||
|| !reissue
|
||||
|| destination_generation != prepared_generation
|
||||
if prepared.identity != identity
|
||||
|| !matches!(prepared.mode, DeliveryMode::Reissue)
|
||||
|| prepared.generation != prepared_generation
|
||||
{
|
||||
return Err(OperationError {
|
||||
message: "Prepared output destination is stale; select it again."
|
||||
.to_owned(),
|
||||
});
|
||||
}
|
||||
let include_qr = prepared.include_qr;
|
||||
let mut destination = prepared.destination;
|
||||
if destination.overwrite_confirmation_required() {
|
||||
if !overwrite_confirmed {
|
||||
return Err(OperationError {
|
||||
@@ -604,7 +615,7 @@ impl OperationExecutor for ApplicationExecutor {
|
||||
destination = destination.confirm_overwrite().map_err(operation_error)?;
|
||||
}
|
||||
let delivery = app
|
||||
.reissue_client_file(identity, password.expose(), destination, _include_qr)
|
||||
.reissue_client_file(identity, password.expose(), destination, include_qr)
|
||||
.map_err(|error| OperationError {
|
||||
message: error.to_string(),
|
||||
})?;
|
||||
@@ -1088,6 +1099,58 @@ mod tests {
|
||||
assert!(!restore_preview_generation_matches(8, 8, 8));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prepared_delivery_is_cleared_by_cancel_and_unrelated_commands() {
|
||||
let (_root, mut app) = application();
|
||||
let identity = PublicKey::from_bytes([44; 32]);
|
||||
let mut executor = ApplicationExecutor::default();
|
||||
let path = _root.path().join("prepared.conf");
|
||||
let prepare = || UiCommand::PrepareDelivery {
|
||||
identity,
|
||||
path: path.clone(),
|
||||
include_qr: false,
|
||||
reissue: false,
|
||||
attempt: FirstIssueAttempt::Initial,
|
||||
};
|
||||
let deliver = || UiCommand::DeliverFirstIssue {
|
||||
attempt: FirstIssueAttempt::Initial,
|
||||
overwrite_confirmed: false,
|
||||
prepared_generation: 7,
|
||||
};
|
||||
|
||||
assert!(matches!(
|
||||
executor.execute_at(&mut app, prepare(), 7),
|
||||
Ok(CommandResult::DeliveryDestination(_))
|
||||
));
|
||||
assert!(matches!(
|
||||
executor.execute_at(&mut app, UiCommand::CancelPreparedDelivery, 7),
|
||||
Ok(CommandResult::Cancelled)
|
||||
));
|
||||
assert_eq!(
|
||||
executor
|
||||
.execute_at(&mut app, deliver(), 7)
|
||||
.unwrap_err()
|
||||
.message,
|
||||
"No prepared output destination is available."
|
||||
);
|
||||
|
||||
assert!(matches!(
|
||||
executor.execute_at(&mut app, prepare(), 7),
|
||||
Ok(CommandResult::DeliveryDestination(_))
|
||||
));
|
||||
assert!(matches!(
|
||||
executor.execute_at(&mut app, UiCommand::ListBackups, 7),
|
||||
Ok(CommandResult::Backups(_))
|
||||
));
|
||||
assert_eq!(
|
||||
executor
|
||||
.execute_at(&mut app, deliver(), 7)
|
||||
.unwrap_err()
|
||||
.message,
|
||||
"No prepared output destination is available."
|
||||
);
|
||||
}
|
||||
|
||||
struct FakeStatistics {
|
||||
started: Option<mpsc::SyncSender<()>>,
|
||||
release: Option<Receiver<()>>,
|
||||
|
||||
+20
-20
@@ -654,9 +654,9 @@ fn qr_state_summary(state: &UiState, width: usize) -> Option<String> {
|
||||
.filter(|row| row.saved.is_some())
|
||||
.count(),
|
||||
match view.applied {
|
||||
super::state::AppliedState::Verified => "verified",
|
||||
super::state::AppliedState::Unknown => "not verified",
|
||||
super::state::AppliedState::Unavailable => "observation unavailable",
|
||||
crate::domain::ApplyStatus::Verified => "verified",
|
||||
crate::domain::ApplyStatus::Unknown => "not verified",
|
||||
crate::domain::ApplyStatus::Unavailable => "observation unavailable",
|
||||
}
|
||||
))
|
||||
}
|
||||
@@ -1210,9 +1210,9 @@ fn persistent_state_line(view: &SessionView) -> String {
|
||||
.filter(|row| row.saved.is_some())
|
||||
.count();
|
||||
let applied = match view.applied {
|
||||
super::state::AppliedState::Verified => "verified",
|
||||
super::state::AppliedState::Unknown => "not verified",
|
||||
super::state::AppliedState::Unavailable => "unavailable",
|
||||
crate::domain::ApplyStatus::Verified => "verified",
|
||||
crate::domain::ApplyStatus::Unknown => "not verified",
|
||||
crate::domain::ApplyStatus::Unavailable => "unavailable",
|
||||
};
|
||||
format!(
|
||||
"Draft: {draft}; Saved peers: {saved_peers} / managed {saved_managed}; Applied(saved): {applied}"
|
||||
@@ -1797,16 +1797,16 @@ fn saved_status(view: &SessionView) -> String {
|
||||
|
||||
fn applied_status(view: &SessionView) -> String {
|
||||
match view.applied {
|
||||
super::state::AppliedState::Verified if view.has_draft => {
|
||||
crate::domain::ApplyStatus::Verified if view.has_draft => {
|
||||
"Applied: verified for saved configuration (draft excluded)".to_owned()
|
||||
}
|
||||
super::state::AppliedState::Verified => {
|
||||
crate::domain::ApplyStatus::Verified => {
|
||||
"Applied: verified for saved configuration".to_owned()
|
||||
}
|
||||
super::state::AppliedState::Unknown => {
|
||||
crate::domain::ApplyStatus::Unknown => {
|
||||
"Applied: not verified for saved configuration".to_owned()
|
||||
}
|
||||
super::state::AppliedState::Unavailable => "Applied: observation unavailable".to_owned(),
|
||||
crate::domain::ApplyStatus::Unavailable => "Applied: observation unavailable".to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2329,7 +2329,7 @@ mod tests {
|
||||
client_states: BTreeMap::new(),
|
||||
saved_peer_count: Some(0),
|
||||
document_issues: Vec::new(),
|
||||
applied: super::super::state::AppliedState::Unknown,
|
||||
applied: crate::domain::ApplyStatus::Unknown,
|
||||
reserved_addresses: Default::default(),
|
||||
statistics: RuntimeObservation::unknown(),
|
||||
has_draft: true,
|
||||
@@ -2545,7 +2545,7 @@ mod tests {
|
||||
)]),
|
||||
saved_peer_count: Some(1),
|
||||
document_issues: Vec::new(),
|
||||
applied: super::super::state::AppliedState::Unknown,
|
||||
applied: crate::domain::ApplyStatus::Unknown,
|
||||
reserved_addresses: Default::default(),
|
||||
statistics: RuntimeObservation::unknown(),
|
||||
has_draft: false,
|
||||
@@ -2558,13 +2558,13 @@ mod tests {
|
||||
let identity = PublicKey::from_bytes([61; 32]);
|
||||
let mut view = saved_client_view(identity);
|
||||
view.has_draft = false;
|
||||
view.applied = super::super::state::AppliedState::Verified;
|
||||
view.applied = crate::domain::ApplyStatus::Verified;
|
||||
let summary = state_summary(&view);
|
||||
assert!(summary.contains("Draft: no changes"));
|
||||
assert!(summary.contains("Saved: 1 peer sections; 1 managed clients"));
|
||||
assert!(summary.contains("Applied: verified for saved configuration"));
|
||||
|
||||
view.applied = super::super::state::AppliedState::Unknown;
|
||||
view.applied = crate::domain::ApplyStatus::Unknown;
|
||||
assert_eq!(
|
||||
applied_status(&view),
|
||||
"Applied: not verified for saved configuration"
|
||||
@@ -2572,7 +2572,7 @@ mod tests {
|
||||
assert!(!state_summary(&view).contains("restart pending"));
|
||||
|
||||
view.has_draft = true;
|
||||
view.applied = super::super::state::AppliedState::Verified;
|
||||
view.applied = crate::domain::ApplyStatus::Verified;
|
||||
assert!(
|
||||
draft_status(&view)
|
||||
.contains("Draft: pending; draft peers: 1; managed-client changes: 0")
|
||||
@@ -2619,13 +2619,13 @@ mod tests {
|
||||
);
|
||||
assert!(draft_status(&view).contains("managed-client changes: 2"));
|
||||
|
||||
view.applied = super::super::state::AppliedState::Unknown;
|
||||
view.applied = crate::domain::ApplyStatus::Unknown;
|
||||
assert_eq!(
|
||||
applied_status(&view),
|
||||
"Applied: not verified for saved configuration"
|
||||
);
|
||||
assert!(!state_summary(&view).contains("restart pending"));
|
||||
view.applied = super::super::state::AppliedState::Unavailable;
|
||||
view.applied = crate::domain::ApplyStatus::Unavailable;
|
||||
assert_eq!(applied_status(&view), "Applied: observation unavailable");
|
||||
|
||||
let mut state = UiState {
|
||||
@@ -2637,7 +2637,7 @@ mod tests {
|
||||
assert_eq!(state_summary(state.view.as_ref().unwrap()), before);
|
||||
assert_eq!(
|
||||
state.view.as_ref().unwrap().applied,
|
||||
super::super::state::AppliedState::Unavailable
|
||||
crate::domain::ApplyStatus::Unavailable
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3932,7 +3932,7 @@ mod tests {
|
||||
)]),
|
||||
saved_peer_count: Some(1),
|
||||
document_issues: Vec::new(),
|
||||
applied: super::super::state::AppliedState::Unknown,
|
||||
applied: crate::domain::ApplyStatus::Unknown,
|
||||
reserved_addresses: Default::default(),
|
||||
statistics: RuntimeObservation::unknown(),
|
||||
has_draft: false,
|
||||
@@ -4055,7 +4055,7 @@ mod tests {
|
||||
]),
|
||||
saved_peer_count: Some(2),
|
||||
document_issues: vec![],
|
||||
applied: super::super::state::AppliedState::Unknown,
|
||||
applied: crate::domain::ApplyStatus::Unknown,
|
||||
reserved_addresses: Default::default(),
|
||||
statistics: RuntimeObservation::unknown(),
|
||||
has_draft: true,
|
||||
|
||||
+12
-24
@@ -4,8 +4,8 @@ use std::collections::BTreeSet;
|
||||
use std::net::IpAddr;
|
||||
|
||||
use crate::app::{Application, ClientDisplay};
|
||||
use crate::domain::{Client, PublicKey, RuntimeObservation};
|
||||
use crate::runtime::RuntimeObservation as Statistics;
|
||||
use crate::domain::{ApplyStatus, Client, PublicKey};
|
||||
use crate::runtime::RuntimeObservation;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum Screen {
|
||||
@@ -34,13 +34,6 @@ pub enum DialogKind {
|
||||
Exit,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum AppliedState {
|
||||
Unknown,
|
||||
Unavailable,
|
||||
Verified,
|
||||
}
|
||||
|
||||
/// Public client values for both states. No configuration bytes or secrets are
|
||||
/// retained in a view snapshot.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
@@ -58,9 +51,9 @@ pub struct SessionView {
|
||||
/// saved bytes can be projected. This includes nonmanaged peers.
|
||||
pub saved_peer_count: Option<usize>,
|
||||
pub document_issues: Vec<crate::wireguard::DocumentIssue>,
|
||||
pub applied: AppliedState,
|
||||
pub applied: ApplyStatus,
|
||||
pub reserved_addresses: BTreeSet<IpAddr>,
|
||||
pub statistics: Statistics,
|
||||
pub statistics: RuntimeObservation,
|
||||
pub has_draft: bool,
|
||||
pub vault_initialized: bool,
|
||||
}
|
||||
@@ -83,11 +76,6 @@ impl SessionView {
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
let applied = match states.applied() {
|
||||
RuntimeObservation::Unknown => AppliedState::Unknown,
|
||||
RuntimeObservation::Unavailable => AppliedState::Unavailable,
|
||||
RuntimeObservation::Verified => AppliedState::Verified,
|
||||
};
|
||||
let projection = app.display_projection();
|
||||
let saved_peer_count =
|
||||
crate::wireguard::ConfigDocument::parse(&app.saved_bytes().server_config)
|
||||
@@ -99,9 +87,9 @@ impl SessionView {
|
||||
client_states,
|
||||
saved_peer_count,
|
||||
document_issues: projection.document_issues,
|
||||
applied,
|
||||
applied: *states.applied(),
|
||||
reserved_addresses: states.reserved_addresses().clone(),
|
||||
statistics: Statistics::unknown(),
|
||||
statistics: RuntimeObservation::unknown(),
|
||||
has_draft: app.has_draft_changes(),
|
||||
vault_initialized: app.vault_initialized(),
|
||||
}
|
||||
@@ -318,7 +306,7 @@ impl UiState {
|
||||
|
||||
/// Apply only statistics sampled from the still-current saved/draft view.
|
||||
/// This never changes the applied-state marker.
|
||||
pub fn apply_statistics(&mut self, generation: u64, statistics: Statistics) -> bool {
|
||||
pub fn apply_statistics(&mut self, generation: u64, statistics: RuntimeObservation) -> bool {
|
||||
if generation != self.generation {
|
||||
return false;
|
||||
}
|
||||
@@ -521,9 +509,9 @@ mod tests {
|
||||
client_states: Default::default(),
|
||||
saved_peer_count: None,
|
||||
document_issues: Vec::new(),
|
||||
applied: AppliedState::Unknown,
|
||||
applied: ApplyStatus::Unknown,
|
||||
reserved_addresses: BTreeSet::new(),
|
||||
statistics: Statistics::unknown(),
|
||||
statistics: RuntimeObservation::unknown(),
|
||||
has_draft: false,
|
||||
vault_initialized: true,
|
||||
}
|
||||
@@ -669,9 +657,9 @@ mod tests {
|
||||
let key = PublicKey::from_bytes([4; 32]);
|
||||
let mut state = UiState::default();
|
||||
assert!(state.apply_view(view(0, vec![row(key, "client", true)])));
|
||||
assert!(state.apply_statistics(0, Statistics::unknown()));
|
||||
assert_eq!(state.view.as_ref().unwrap().applied, AppliedState::Unknown);
|
||||
assert!(state.apply_statistics(0, RuntimeObservation::unknown()));
|
||||
assert_eq!(state.view.as_ref().unwrap().applied, ApplyStatus::Unknown);
|
||||
state.generation = 1;
|
||||
assert!(!state.apply_statistics(0, Statistics::unknown()));
|
||||
assert!(!state.apply_statistics(0, RuntimeObservation::unknown()));
|
||||
}
|
||||
}
|
||||
|
||||
+8
-7
@@ -960,9 +960,10 @@ mod tests {
|
||||
use ratatui::backend::TestBackend;
|
||||
|
||||
use crate::app::ClientDisplay;
|
||||
use crate::domain::ApplyStatus;
|
||||
use crate::runtime::RuntimeObservation;
|
||||
use crate::tui::events::OperationError;
|
||||
use crate::tui::state::{AppliedState, OperationKind, SessionView};
|
||||
use crate::tui::state::{OperationKind, SessionView};
|
||||
use crate::wireguard::PeerIssue;
|
||||
|
||||
struct FakeWorker {
|
||||
@@ -1032,7 +1033,7 @@ mod tests {
|
||||
client_states: Default::default(),
|
||||
saved_peer_count: Some(0),
|
||||
document_issues: Vec::new(),
|
||||
applied: AppliedState::Unknown,
|
||||
applied: ApplyStatus::Unknown,
|
||||
reserved_addresses: Default::default(),
|
||||
statistics: RuntimeObservation::unknown(),
|
||||
has_draft: false,
|
||||
@@ -1163,7 +1164,7 @@ mod tests {
|
||||
let identity = crate::domain::PublicKey::from_bytes([1; 32]);
|
||||
let mut view = navigation_view();
|
||||
view.has_draft = true;
|
||||
view.applied = AppliedState::Verified;
|
||||
view.applied = ApplyStatus::Verified;
|
||||
let mut state = UiState {
|
||||
screen: Screen::Help,
|
||||
selected_identity: Some(identity),
|
||||
@@ -1192,7 +1193,7 @@ mod tests {
|
||||
assert_eq!(state.selected_peer_index, selected_peer_index);
|
||||
assert_eq!(state.view, view_before);
|
||||
assert!(state.view.as_ref().unwrap().has_draft);
|
||||
assert_eq!(state.view.as_ref().unwrap().applied, AppliedState::Verified);
|
||||
assert_eq!(state.view.as_ref().unwrap().applied, ApplyStatus::Verified);
|
||||
// The scrolling helper accepts no worker or command sink, so Help input
|
||||
// cannot submit work or mutate saved/applied application state.
|
||||
assert!(!handle_help_navigation_key(
|
||||
@@ -1313,7 +1314,7 @@ mod tests {
|
||||
document_issues: vec![crate::wireguard::DocumentIssue::MalformedSectionHeader {
|
||||
line: 1,
|
||||
}],
|
||||
applied: AppliedState::Unknown,
|
||||
applied: ApplyStatus::Unknown,
|
||||
reserved_addresses: Default::default(),
|
||||
statistics: RuntimeObservation::unknown(),
|
||||
has_draft: false,
|
||||
@@ -1344,7 +1345,7 @@ mod tests {
|
||||
client_states: Default::default(),
|
||||
saved_peer_count: Some(2),
|
||||
document_issues: Vec::new(),
|
||||
applied: AppliedState::Unknown,
|
||||
applied: ApplyStatus::Unknown,
|
||||
reserved_addresses: Default::default(),
|
||||
statistics: RuntimeObservation::unknown(),
|
||||
has_draft: false,
|
||||
@@ -1578,7 +1579,7 @@ mod tests {
|
||||
client_states: Default::default(),
|
||||
saved_peer_count: Some(0),
|
||||
document_issues: Vec::new(),
|
||||
applied: AppliedState::Unknown,
|
||||
applied: ApplyStatus::Unknown,
|
||||
reserved_addresses: Default::default(),
|
||||
statistics: RuntimeObservation::unknown(),
|
||||
has_draft: false,
|
||||
|
||||
Reference in New Issue
Block a user