use std::path::PathBuf;
use serde::Serialize;
pub trait Report: Serialize {
fn human(&self) -> String;
}
pub fn emit<R: Report>(report: &R, json: bool) -> anyhow::Result<()> {
if json {
println!("{}", serde_json::to_string_pretty(report)?);
} else {
print!("{}", report.human());
}
Ok(())
}
#[derive(Debug, Serialize)]
pub struct Status {
pub lez: String,
pub runtime: String,
pub sequencer: Sequencer,
pub faucet: Option<Balance>,
pub programs: Vec<Program>,
pub account_groups: Vec<GroupSummary>,
pub sessions: Vec<String>,
pub messaging: Option<Messaging>,
pub env_file: Option<PathBuf>,
}
#[derive(Debug, Serialize)]
pub struct Messaging {
pub running: bool,
pub delivery_node: bool,
pub discovery_node: bool,
#[serde(flatten)]
pub addresses: Option<crate::messaging::Addresses>,
}
#[derive(Debug, Serialize)]
pub struct Sequencer {
pub url: String,
pub running: bool,
pub block: Option<u64>,
pub max_txs_per_block: u32,
pub block_time: String,
pub dev_mode: bool,
}
#[derive(Debug, Serialize)]
pub struct Balance {
pub account: String,
pub balance: Option<u128>,
}
#[derive(Debug, Serialize)]
pub struct Program {
pub name: String,
pub id: String,
pub hex: String,
pub var: String,
pub deployed_now: bool,
pub updated: bool,
}
#[derive(Debug, Serialize)]
pub struct GroupSummary {
pub name: String,
pub accounts: usize,
pub amount: u64,
pub file: PathBuf,
pub wallet: PathBuf,
pub var: String,
}
#[derive(Debug, Serialize)]
pub struct SessionMade {
pub name: String,
pub modules: Vec<String>,
pub vars: Vec<(String, String)>,
}
#[derive(Debug, Serialize)]
pub struct SessionRow {
pub name: String,
pub running: bool,
pub account: Option<String>,
pub balance: Option<u128>,
pub error: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct Sessions(pub Vec<SessionRow>);
#[derive(Debug, Serialize)]
pub struct Groups(pub Vec<GroupSummary>);
#[derive(Debug)]
pub struct EnvVars(Vec<(String, String)>);
impl EnvVars {
pub fn new(vars: Vec<(String, String)>) -> Self {
Self(vars)
}
}
impl Serialize for EnvVars {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
use serde::ser::SerializeMap;
let mut map = serializer.serialize_map(Some(self.0.len()))?;
for (var, value) in &self.0 {
map.serialize_entry(var, value)?;
}
map.end()
}
}
#[derive(Debug, Serialize)]
pub struct Done {
pub message: String,
}
#[derive(Debug, Serialize)]
pub struct WalletConfig {
pub path: PathBuf,
}
impl Report for WalletConfig {
fn human(&self) -> String {
format!("{}\n", self.path.display())
}
}
impl Report for Status {
fn human(&self) -> String {
let s = &self.sequencer;
let mut out = format!("lez {} ({})\n", self.lez, self.runtime);
out += &match (s.running, s.block) {
(true, Some(block)) => format!("sequencer {} running, block {block}", s.url),
(true, None) => format!("sequencer {} starting", s.url),
(false, _) => format!("sequencer {} stopped", s.url),
};
out += if s.dev_mode {
", dev mode (fake proofs)\n"
} else {
"\n"
};
if let Some(faucet) = &self.faucet {
out += &format!("faucet {}{}\n", faucet.account, lepta(faucet.balance));
}
for p in &self.programs {
out += &format!("program {} {} ({})\n", p.name, p.id, p.var);
}
for g in &self.account_groups {
out += &format!(
"accounts {} {} x {} lepta ({})\n",
g.name, g.accounts, g.amount, g.var
);
}
if !self.sessions.is_empty() {
out += &format!("sessions {}\n", self.sessions.join(", "));
}
if let Some(m) = &self.messaging {
let down: Vec<&str> = [
(m.delivery_node, "delivery"),
(m.discovery_node, "discovery"),
]
.into_iter()
.filter_map(|(up, node)| (!up).then_some(node))
.collect();
out += &match (m.running, down.as_slice()) {
(false, _) => "messaging stopped\n".to_owned(),
(true, []) => "messaging running\n".to_owned(),
(true, down) => format!(
"messaging running, but its {} node{} down. `logos-devnet up` starts {}\n",
down.join(" and "),
if down.len() == 1 { " is" } else { "s are" },
if down.len() == 1 { "it" } else { "them" },
),
};
if let Some(a) = &m.addresses {
out += &format!(" delivery {}\n", a.delivery);
out += &format!(" discovery {}\n", a.discovery);
}
}
if let Some(env) = &self.env_file {
out += &format!("env {}\n", env.display());
}
out
}
}
impl Report for Balance {
fn human(&self) -> String {
format!("{}{}\n", self.account, lepta(self.balance))
}
}
impl Report for Program {
fn human(&self) -> String {
format!("{}={}\n", self.var, self.id)
}
}
impl Report for SessionMade {
fn human(&self) -> String {
let mut out = format!(
"session {} ready, with {}\n",
self.name,
self.modules.join(", ")
);
for (var, value) in &self.vars {
out += &format!(" {var}={value}\n");
}
out += &format!(
"Load it with: source <(logos-devnet session env {})\n",
self.name
);
out
}
}
impl Report for Sessions {
fn human(&self) -> String {
if self.0.is_empty() {
return "No sessions. `logos-devnet session new <name>` makes one.\n".into();
}
self.0
.iter()
.map(|s| {
let state = if s.running { "running" } else { "stopped" };
match (&s.account, &s.error) {
(Some(account), _) => {
format!("{:<16} {state:<8} {account}{}\n", s.name, lepta(s.balance))
}
(None, error) => format!(
"{:<16} {state:<8} {}\n",
s.name,
error.as_deref().unwrap_or("")
),
}
})
.collect()
}
}
impl Report for GroupSummary {
fn human(&self) -> String {
format!(
"{} {} accounts x {} lepta\n accounts {}\n wallet {}\n env {}\n",
self.name,
self.accounts,
self.amount,
self.file.display(),
self.wallet.display(),
self.var
)
}
}
impl Report for Groups {
fn human(&self) -> String {
if self.0.is_empty() {
return "No account groups. `logos-devnet accounts new <name> --count <n>` makes one.\n"
.into();
}
self.0.iter().map(Report::human).collect()
}
}
impl Report for EnvVars {
fn human(&self) -> String {
let mut out = String::from(crate::envfile::HEADER);
for (var, value) in &self.0 {
out += &crate::envfile::export_line(var, value);
}
out
}
}
impl Report for Done {
fn human(&self) -> String {
format!("{}\n", self.message)
}
}
fn lepta(balance: Option<u128>) -> String {
balance.map_or(String::new(), |b| format!(" {b} lepta"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn status_fields_are_stable() {
let status = Status {
lez: "v0.3.1".into(),
runtime: "docker".into(),
sequencer: Sequencer {
url: "http://127.0.0.1:3040".into(),
running: true,
block: Some(7),
max_txs_per_block: 20,
block_time: "1s".into(),
dev_mode: true,
},
faucet: None,
programs: vec![],
account_groups: vec![],
sessions: vec![],
messaging: Some(Messaging {
running: true,
delivery_node: true,
discovery_node: false,
addresses: Some(crate::messaging::Addresses {
delivery: "/ip4/127.0.0.1/tcp/61310/p2p/16Uiu2HAmA".into(),
discovery: "/ip4/127.0.0.1/tcp/61320/p2p/16Uiu2HAmB".into(),
}),
}),
env_file: None,
};
let json = serde_json::to_value(&status).unwrap();
for key in [
"lez",
"runtime",
"sequencer",
"faucet",
"programs",
"account_groups",
"sessions",
"messaging",
"env_file",
] {
assert!(json.get(key).is_some(), "{key}");
}
assert_eq!(json["sequencer"]["block"], 7);
assert_eq!(json["sequencer"]["dev_mode"], true);
assert!(status.human().contains("running, block 7, dev mode"));
assert_eq!(json["messaging"]["running"], true);
assert_eq!(
json["messaging"]["delivery"],
"/ip4/127.0.0.1/tcp/61310/p2p/16Uiu2HAmA"
);
assert!(
status.human().contains(
"messaging running, but its discovery node is down. `logos-devnet up` starts it\n delivery /ip4/127.0.0.1/tcp/61310"
),
"{}",
status.human()
);
assert_eq!(json["messaging"]["discovery_node"], false);
}
#[test]
fn env_vars_keep_their_order_and_quote_for_the_shell() {
let env = EnvVars::new(vec![("B".into(), "it's".into()), ("A".into(), "x".into())]);
let json = serde_json::to_string(&env).unwrap();
assert_eq!(json, r#"{"B":"it's","A":"x"}"#, "in order, not sorted");
assert_eq!(
env.human(),
"# Written by logos-devnet. Load it with: source <this file>\nexport B='it'\\''s'\nexport A='x'\n"
);
}
}