use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::Duration;
use net_sdk::deck::{DeckClient, OperatorIdentity};
use net_sdk::meshos::{EntityKeypair, LoggingDispatcher, MeshOsConfig, MeshOsDaemonSdk};
use crate::config::Profile;
use crate::error::{generic, invalid_args, sdk, CliError};
pub struct CliContext {
_sdk: MeshOsDaemonSdk,
deck: Arc<DeckClient>,
identity: OperatorIdentity,
}
impl CliContext {
pub fn deck(&self) -> Arc<DeckClient> {
Arc::clone(&self.deck)
}
pub fn identity(&self) -> &OperatorIdentity {
&self.identity
}
pub async fn build(
profile: &Profile,
identity_override: Option<&Path>,
node_id: u64,
require_identity: bool,
) -> Result<Self, CliError> {
if let Some(endpoint) = profile.endpoint.as_deref() {
if endpoint != "in-process" {
return Err(invalid_args(format!(
"endpoint `{endpoint}` is not supported in this build; \
only `in-process` is available until the substrate \
remote-attach surface lands (see NET_CLI_PLAN.md \
Phase 5)"
)));
}
}
let keypair = match identity_override.or(profile.identity.as_deref()) {
Some(path) => load_identity_keypair(path).await?,
None => {
if require_identity {
return Err(invalid_args(
"no operator identity configured; pass --identity <PATH> \
or set `identity = \"...\"` under your profile in the \
config file. Admin / ICE commits refuse to sign with \
an ephemeral keypair.",
));
}
tracing::warn!(
"no operator identity configured; using an ephemeral \
keypair. Run `net identity generate --out <PATH>` and \
point your profile at the result for stable operator id."
);
EntityKeypair::generate()
}
};
let mut cfg = MeshOsConfig::default();
cfg.this_node = node_id;
cfg.tick_interval = Duration::from_millis(250);
let dispatcher = Arc::new(LoggingDispatcher::new());
let sdk = MeshOsDaemonSdk::start(cfg, dispatcher);
let identity = OperatorIdentity::from_keypair(keypair);
let deck = Arc::new(DeckClient::from_runtime(sdk.runtime(), identity.clone()));
Ok(Self {
_sdk: sdk,
deck,
identity,
})
}
}
pub(crate) async fn load_identity_keypair(path: &Path) -> Result<EntityKeypair, CliError> {
let text = tokio::fs::read_to_string(path).await.map_err(|e| {
generic(format!(
"failed to read identity file {}: {e}",
path.display()
))
})?;
let parsed: PartialIdentityFile = toml::from_str(&text).map_err(|e| {
invalid_args(format!(
"identity file {} failed to parse: {e}",
path.display()
))
})?;
let seed_bytes = hex::decode(&parsed.seed_hex).map_err(|e| {
invalid_args(format!(
"identity file {} `seed_hex` is not valid hex: {e}",
path.display()
))
})?;
if seed_bytes.len() != 32 {
return Err(invalid_args(format!(
"identity file {} `seed_hex` decodes to {} bytes; expected 32",
path.display(),
seed_bytes.len()
)));
}
let mut arr = [0u8; 32];
arr.copy_from_slice(&seed_bytes);
Ok(EntityKeypair::from_bytes(arr))
}
#[derive(serde::Deserialize)]
struct PartialIdentityFile {
seed_hex: String,
}
pub async fn resolve_profile(
config_path: Option<&Path>,
profile_name: &str,
) -> Result<Profile, CliError> {
let file = crate::config::ConfigFile::load(config_path)
.await
.map_err(|e| sdk(format!("config load: {e}")))?;
Ok(file.profile(profile_name))
}
#[allow(dead_code)]
pub fn resolved_identity_path(profile: &Profile, overide_: Option<&Path>) -> Option<PathBuf> {
overide_
.map(Path::to_path_buf)
.or_else(|| profile.identity.clone())
}