use std::path::{Path, PathBuf};
use std::time::{Duration, Instant};
use anyhow::{Context, Result, bail};
use serde_json::{Value, json};
use crate::account::{PublicId, first_public_id};
use crate::chain_config::{self, ChainOptions};
use crate::envfile::{self, Env};
use crate::home::{Home, RuntimeKind, Settings, write_json};
use crate::rpc::Rpc;
use crate::runtime::{Docker, Native, Runtime, WalletArg};
use crate::session::{self, Session, SessionInfo};
const FAUCET_BALANCE: u64 = 1_000_000_000_000_000_000;
const FAUCET_LABEL: &str = "faucet";
pub const DEFAULT_FUND: u128 = 10_000_000_000;
const SEGMENT_LEN: u64 = 96 * 1024;
const READY_TIMEOUT: Duration = Duration::from_secs(180);
const WALLET_POLL_TIMEOUT: &str = "2s";
#[derive(Debug, Default, Clone, clap::Args)]
pub struct UpOptions {
#[arg(long, conflicts_with = "native")]
pub docker: bool,
#[arg(long)]
pub native: bool,
#[arg(long)]
pub lez: Option<String>,
#[arg(long, value_parser = clap::value_parser!(u16).range(1..))]
pub port: Option<u16>,
#[arg(long)]
pub block_time: Option<String>,
#[arg(long)]
pub rebuild: bool,
}
impl UpOptions {
fn apply(&self, mut settings: Settings) -> Settings {
if self.docker {
settings.runtime = RuntimeKind::Docker;
}
if self.native {
settings.runtime = RuntimeKind::Native;
}
if let Some(lez) = &self.lez {
settings.lez.clone_from(lez);
}
if let Some(port) = self.port {
settings.port = port;
}
if let Some(block_time) = &self.block_time {
settings.block_time.clone_from(block_time);
}
settings
}
}
pub struct Devnet {
home: Home,
settings: Settings,
runtime: Box<dyn Runtime>,
}
impl Devnet {
pub fn open(home: Home) -> Result<Self> {
let settings = home.load_settings()?;
Ok(Self::with(home, settings))
}
fn with(home: Home, settings: Settings) -> Self {
let runtime: Box<dyn Runtime> = match settings.runtime {
RuntimeKind::Docker => Box::new(Docker::new(home.clone(), settings.clone())),
RuntimeKind::Native => Box::new(Native::new(home.clone(), settings.clone())),
};
Self {
home,
settings,
runtime,
}
}
fn rpc(&self) -> Rpc {
Rpc::new(self.settings.sequencer_url())
}
fn wallet(&self, args: Vec<WalletArg>) -> Result<String> {
std::fs::create_dir_all(self.home.wallet_dir())?;
write_json(
&self.home.wallet_dir().join("wallet_config.json"),
&wallet_config(&self.runtime.wallet_sequencer_url()),
)?;
self.runtime.wallet(args)
}
fn faucet(&self) -> Result<&PublicId> {
self.settings
.faucet
.as_ref()
.context("no faucet yet: run `logos-devnet up` first")
}
fn require_running(&self) -> Result<()> {
if !self.runtime.running() {
bail!("the devnet is not running: run `logos-devnet up`");
}
Ok(())
}
pub fn up(self, opts: &UpOptions) -> Result<()> {
self.launch(opts, false)
}
pub fn reset(self, opts: &UpOptions) -> Result<()> {
self.launch(opts, true)
}
fn launch(self, opts: &UpOptions, wipe: bool) -> Result<()> {
let settings = opts.apply(self.settings.clone());
chain_config::block_time_secs(&settings.block_time)?;
let continuing = !wipe && self.home.has_chain();
if continuing && settings.lez != self.settings.lez {
bail!(
"this chain runs LEZ {}. `logos-devnet reset --lez {}` starts a new chain on it",
self.settings.lez,
settings.lez
);
}
let unchanged = settings == self.settings && self.home.settings_file().exists();
if !wipe && unchanged && !opts.rebuild && self.runtime.running() {
println!("Already running.");
let resumed = self.resume_sessions(false);
self.status()?;
return resumed;
}
if !(self.runtime.running() && settings.port == self.settings.port) {
port_free(settings.port)?;
}
let mut next = Self::with(self.home.clone(), settings);
next.runtime.prepare(opts.rebuild)?;
let faucet = next.dev_faucet()?;
if continuing && funds_another_faucet(self.settings.faucet.as_ref(), &faucet) {
bail!(
"this chain's genesis funds another faucet than the dev wallet holds. \
`logos-devnet reset` starts a chain that funds it"
);
}
next.settings.faucet = Some(faucet);
let config = next.chain_config()?;
self.runtime.stop()?;
if wipe {
next.runtime.wipe_chain()?;
remove_if_present(&self.home.env_file(), |p| std::fs::remove_file(p))?;
}
next.home.save_settings(&next.settings)?;
std::fs::create_dir_all(next.home.chain_dir())?;
write_json(&next.home.chain_config(), &config)?;
next.runtime.start()?;
next.wait_ready()?;
let existing = std::fs::read_to_string(next.home.env_file()).ok();
let env = carried_env(
continuing,
existing.as_deref(),
next.settings.sequencer_url(),
)?;
next.write_env(&env)?;
let resumed = next.resume_sessions(!continuing);
next.status()?;
resumed
}
fn resume_sessions(&self, refund: bool) -> Result<()> {
let mut failed = Vec::new();
for session in self.sessions() {
let resumed = session.info().and_then(|info| {
let funded = if refund {
self.send(&info.account, info.funded).map(drop)
} else {
Ok(())
};
self.write_session_wallet_config(&session)?;
let ctl = session.logosctl();
ctl.start()?;
ctl.load(&info.modules)?;
ctl.ensure_wallet(
&session.wallet_config(),
&session.wallet_storage(),
&self.settings.sequencer_url(),
)?;
funded
});
if let Err(e) = resumed {
eprintln!("session {}: {e:#}", session.name);
failed.push(session.name);
}
}
if !failed.is_empty() {
bail!(
"the devnet is up, but sessions {} failed. If one keeps failing, \
`logos-devnet session rm <name>` and make it again",
failed.join(", ")
);
}
Ok(())
}
fn sessions(&self) -> Vec<Session> {
session::list(&self.home.sessions_dir()).unwrap_or_else(|e| {
eprintln!("sessions: {e:#}");
Vec::new()
})
}
fn write_session_wallet_config(&self, session: &Session) -> Result<()> {
std::fs::create_dir_all(session.wallet_dir())?;
write_json(
&session.wallet_config(),
&wallet_config(&self.settings.sequencer_url()),
)
}
fn send(&self, to: &PublicId, amount: u128) -> Result<u128> {
let faucet = self.faucet()?;
self.wallet(vec![
"auth-transfer".into(),
"send".into(),
"--from".into(),
faucet.wallet_form().into(),
"--to".into(),
to.wallet_form().into(),
"--amount".into(),
amount.to_string().into(),
])?;
self.rpc().balance(to)
}
fn dev_faucet(&self) -> Result<PublicId> {
match self.labelled_account(FAUCET_LABEL)? {
Some(faucet) => Ok(faucet),
None => self.new_account(Some(FAUCET_LABEL)),
}
}
fn chain_config(&self) -> Result<Value> {
chain_config::patch(
self.runtime.upstream_config()?,
&ChainOptions {
home: &self.runtime.chain_home(),
block_time: &self.settings.block_time,
faucet: self.faucet()?,
faucet_balance: FAUCET_BALANCE,
},
)
}
fn wait_ready(&self) -> Result<()> {
let rpc = self.rpc();
let deadline = Instant::now() + READY_TIMEOUT;
loop {
if rpc.last_block_id().is_ok_and(|head| head >= 1) {
return Ok(());
}
if !self.runtime.running() {
bail!("the sequencer stopped while starting. See `logos-devnet logs`");
}
if Instant::now() > deadline {
bail!(
"no block within {}s. See `logos-devnet logs`",
READY_TIMEOUT.as_secs()
);
}
std::thread::sleep(Duration::from_millis(500));
}
}
fn write_env(&self, env: &Env) -> Result<()> {
std::fs::write(self.home.env_file(), env.render()).context("writing the env file")
}
fn read_env(&self) -> Result<Env> {
let text = std::fs::read_to_string(self.home.env_file())
.context("no env file: run `logos-devnet up` first")?;
Env::parse(&text)
}
pub fn down(&self) -> Result<()> {
for session in self.sessions() {
if let Err(e) = session.logosctl().stop() {
eprintln!("session {}: {e:#}", session.name);
}
}
self.runtime.stop()?;
println!("Stopped. The chain and sessions are kept; `logos-devnet up` resumes them.");
Ok(())
}
pub fn status(&self) -> Result<()> {
let s = &self.settings;
println!("lez {} ({})", s.lez, self.runtime.describe());
let running = self.runtime.running();
let head = running.then(|| self.rpc().last_block_id().ok()).flatten();
match head {
Some(head) => println!("sequencer {} running, block {head}", s.sequencer_url()),
None if running => println!("sequencer {} starting", s.sequencer_url()),
None => println!("sequencer {} stopped", s.sequencer_url()),
}
if let Some(faucet) = &s.faucet {
let balance = head.and_then(|_| self.rpc().balance(faucet).ok());
let balance = balance.map_or(String::new(), |b| format!(" {b} lepta"));
println!("faucet {}{balance}", faucet.wallet_form());
}
if let Ok(env) = self.read_env() {
for (name, id) in &env.programs {
println!(
"program {name} {} ({})",
id.as_str(),
envfile::program_var(name)
);
}
println!("env {}", self.home.env_file().display());
}
let sessions = self.sessions();
if !sessions.is_empty() {
let names: Vec<_> = sessions.iter().map(|s| s.name.as_str()).collect();
println!("sessions {}", names.join(", "));
}
Ok(())
}
pub fn fund(&self, account: &str, amount: u128) -> Result<()> {
let to = PublicId::parse(account)?;
self.faucet()?;
self.require_running()?;
let balance = self.send(&to, amount)?;
println!("{} {balance} lepta", to.wallet_form());
Ok(())
}
pub fn session_new(&self, name: &str, extra: &[String], amount: u128) -> Result<()> {
let session = Session::new(&self.home.sessions_dir(), name)?;
if session.exists() {
bail!("session {name} exists. `logos-devnet session rm {name}` first");
}
let modules = session::modules_with(extra)?;
self.faucet()?;
self.require_running()?;
let made = self.make_session(&session, modules, amount);
if made.is_err() {
match session.logosctl().stop() {
Ok(()) => {
let _ = std::fs::remove_dir_all(session.dir());
}
Err(e) => eprintln!(
"session {name}: its daemon didn't stop ({e:#}), so {} stays",
session.dir().display()
),
}
}
let info = made?;
println!("session {name} ready, with {}", info.modules.join(", "));
for (var, value) in session.vars(&info) {
println!(" {var}={value}");
}
println!("Load it with: source <(logos-devnet session env {name})");
Ok(())
}
fn make_session(
&self,
session: &Session,
modules: Vec<String>,
amount: u128,
) -> Result<SessionInfo> {
self.write_session_wallet_config(session)?;
let ctl = session.logosctl();
eprintln!(
"Starting session {} and installing {}...",
session.name,
modules.join(", ")
);
ctl.start()?;
ctl.install(&modules)?;
ctl.load(&modules)?;
ctl.create_wallet(&session.wallet_config(), &session.wallet_storage())?;
let account = ctl.call("lez_core", "create_account_public", &[])?;
let account =
PublicId::from_hex(account.as_str().context("lez_core returned no account")?)?;
ctl.call("lez_core", "save", &[])?;
let info = SessionInfo {
modules,
account,
funded: amount,
};
session.save_info(&info)?;
self.send(&info.account, amount)?;
Ok(info)
}
pub fn session_list(&self) -> Result<()> {
let sessions = session::list(&self.home.sessions_dir())?;
if sessions.is_empty() {
println!("No sessions. `logos-devnet session new <name>` makes one.");
}
let chain_up = self.runtime.running();
for session in sessions {
let state = if session.logosctl().running() {
"running"
} else {
"stopped"
};
match session.info() {
Ok(info) => {
let balance = chain_up
.then(|| self.rpc().balance(&info.account).ok())
.flatten()
.map_or(String::new(), |b| format!(" {b} lepta"));
println!(
"{:<16} {state:<8} {}{balance}",
session.name,
info.account.wallet_form()
);
}
Err(e) => println!("{:<16} {state:<8} {e:#}", session.name),
}
}
Ok(())
}
pub fn session_env(&self, name: &str) -> Result<()> {
let session = self.existing_session(name)?;
let info = session.info()?;
let devnet = self.read_env().map(|env| env.render()).unwrap_or_else(|e| {
eprintln!("{e:#}");
String::new()
});
print!("{devnet}{}", session.env_lines(&info));
Ok(())
}
pub fn session_rm(&self, name: &str) -> Result<()> {
let session = self.existing_session(name)?;
session.logosctl().stop()?;
std::fs::remove_dir_all(session.dir())
.with_context(|| format!("deleting {}", session.dir().display()))?;
println!("Removed session {name} and its wallet.");
Ok(())
}
fn existing_session(&self, name: &str) -> Result<Session> {
let session = Session::new(&self.home.sessions_dir(), name)?;
if !session.exists() {
bail!("no session {name}. `logos-devnet session ls` lists them");
}
Ok(session)
}
pub fn deploy(&self, name: &str, elf: &Path) -> Result<()> {
envfile::check_program_name(name)?;
let size = std::fs::metadata(elf)
.with_context(|| format!("{} does not exist", elf.display()))?
.len();
let faucet = self.faucet()?;
self.require_running()?;
let mut env = self.read_env()?;
let label = format!("program-{name}");
let header = match self.labelled_account(&label)? {
Some(header) => header,
None => self.new_account(Some(&label))?,
};
if self.rpc().account_in_use(&header)? {
println!(
"{name} is already deployed on this chain. `logos-devnet reset` to deploy a new build."
);
} else {
let mut args: Vec<WalletArg> = vec![
"program-loader".into(),
"deploy".into(),
"--elf".into(),
WalletArg::File(elf.to_path_buf()),
"--header".into(),
header.wallet_form().into(),
"--segments".into(),
];
for _ in 0..segment_count(size) {
args.push(self.new_account(None)?.wallet_form().into());
}
args.extend([
"--immutable".into(),
"--payer".into(),
faucet.wallet_form().into(),
]);
let output = self.wallet(args)?;
let deployed = output
.lines()
.find_map(|line| line.strip_prefix("Program deployed. Header account id: "))
.with_context(|| format!("the wallet did not report a deploy:\n{output}"))?;
if PublicId::parse(deployed.trim())? != header {
bail!(
"the wallet deployed at {deployed}, not at {}",
header.as_str()
);
}
}
env.programs.insert(name.to_owned(), header.clone());
self.write_env(&env)?;
println!("{}={}", envfile::program_var(name), header.as_str());
Ok(())
}
fn new_account(&self, label: Option<&str>) -> Result<PublicId> {
let mut args: Vec<WalletArg> = vec!["account".into(), "new".into(), "public".into()];
if let Some(label) = label {
args.extend(["--label".into(), label.into()]);
}
let output = self.wallet(args)?;
first_public_id(&output).with_context(|| format!("the wallet made no account:\n{output}"))
}
fn labelled_account(&self, label: &str) -> Result<Option<PublicId>> {
let list = self.wallet(vec!["account".into(), "list".into()])?;
Ok(labelled(&list, label))
}
pub fn print_env(&self) -> Result<()> {
print!("{}", self.read_env()?.render());
Ok(())
}
pub fn logs(&self, follow: bool) -> Result<()> {
self.runtime.logs(follow)
}
pub fn write_project_wallet_config(&self, dir: &Path) -> Result<PathBuf> {
let path = dir.join("wallet_config.json");
for existing in [&path, &dir.join("storage.json")] {
if existing.exists() {
bail!(
"{} exists. Give the devnet wallet a new directory",
existing.display()
);
}
}
std::fs::create_dir_all(dir)?;
write_json(&path, &wallet_config(&self.settings.sequencer_url()))?;
Ok(path)
}
}
fn port_free(port: u16) -> Result<()> {
std::net::TcpListener::bind(("127.0.0.1", port))
.map(drop)
.with_context(|| format!("port {port} on 127.0.0.1 is taken. Pick another with --port"))
}
fn funds_another_faucet(stored: Option<&PublicId>, wallet: &PublicId) -> bool {
stored.is_some_and(|stored| stored != wallet)
}
fn carried_env(continuing: bool, existing: Option<&str>, sequencer: String) -> Result<Env> {
let mut env = match existing {
Some(text) if continuing => Env::parse(text)
.context("the env file is damaged. Delete it, then `logos-devnet up`")?,
_ => Env::default(),
};
env.sequencer = sequencer;
Ok(env)
}
fn remove_if_present(path: &Path, remove: fn(&Path) -> std::io::Result<()>) -> Result<()> {
match remove(path) {
Err(e) if e.kind() != std::io::ErrorKind::NotFound => {
Err(e).with_context(|| format!("deleting {}", path.display()))
}
_ => Ok(()),
}
}
fn wallet_config(sequencer: &str) -> Value {
json!({
"gas_limit": 200_000,
"multi_sequencer_client_config": { "calibration_limit": 3, "distribution_limit": 1 },
"seq_block_poll_max_amount": 100,
"seq_poll_max_retries": 10,
"seq_poll_timeout": WALLET_POLL_TIMEOUT,
"seq_tx_poll_max_blocks": 15,
"sequencer": { "sequencer_addr": sequencer },
"sequencer_addr": sequencer,
"sequencers": [{ "sequencer_addr": sequencer }]
})
}
fn segment_count(elf_len: u64) -> u64 {
elf_len.div_ceil(SEGMENT_LEN).max(1)
}
fn labelled(list: &str, label: &str) -> Option<PublicId> {
let tag = format!("[{label}]");
list.lines()
.filter(|line| line.split_whitespace().any(|word| word == tag))
.find_map(first_public_id)
}
#[cfg(test)]
mod tests {
use super::*;
const ID: &str = "HpavNGFYxjyhLsy171aXsvv146yUeXmRWiho22uvT43d";
#[test]
fn counts_segments() {
assert_eq!(segment_count(1), 1);
assert_eq!(segment_count(SEGMENT_LEN), 1);
assert_eq!(segment_count(SEGMENT_LEN + 1), 2);
assert_eq!(segment_count(241_064), 3);
}
#[test]
fn finds_a_labelled_account() {
let list = "Public/CbgR6tj5kWx5oziiFptM7jMvrQeYY3Mzaao6ciuhSr2r\n\
/0 Public/Xuuz5DXfz3knxKf7jTvbhQBrrH5HzpwZnEzTEv6N3Pn [faucet]\n\
/1 Public/6YvesSEDWW25kpi8SSki2c8o5m3uACsigvGnNQ6wMruh [program-counter]\n";
let found = labelled(list, "program-counter").unwrap();
assert_eq!(
found.as_str(),
"6YvesSEDWW25kpi8SSki2c8o5m3uACsigvGnNQ6wMruh"
);
assert!(
labelled(list, "program-count").is_none(),
"labels match whole"
);
assert!(labelled(list, "missing").is_none());
}
#[test]
fn wallet_configs_point_every_address_field_at_the_devnet() {
let config = wallet_config("http://127.0.0.1:3040");
assert_eq!(
config["sequencer"]["sequencer_addr"],
"http://127.0.0.1:3040"
);
assert_eq!(config["sequencer_addr"], "http://127.0.0.1:3040");
assert_eq!(
config["sequencers"][0]["sequencer_addr"],
"http://127.0.0.1:3040"
);
assert_eq!(config["gas_limit"], 200_000);
assert_eq!(config["seq_poll_timeout"], "2s");
}
#[test]
fn options_change_only_what_they_name() {
let base = Settings::default();
let opts = UpOptions {
port: Some(4000),
native: true,
..UpOptions::default()
};
let applied = opts.apply(base.clone());
assert_eq!(applied.port, 4000);
assert_eq!(applied.runtime, RuntimeKind::Native);
let back = UpOptions {
docker: true,
..UpOptions::default()
}
.apply(applied.clone());
assert_eq!(back.runtime, RuntimeKind::Docker);
assert_eq!(
Settings {
port: base.port,
runtime: base.runtime,
..applied
},
base
);
}
#[test]
fn a_lost_settings_file_keeps_the_wallets_faucet() {
let wallet = PublicId::parse(ID).unwrap();
let other = PublicId::parse("6YvesSEDWW25kpi8SSki2c8o5m3uACsigvGnNQ6wMruh").unwrap();
assert!(!funds_another_faucet(None, &wallet), "settings lost");
assert!(!funds_another_faucet(Some(&wallet), &wallet));
assert!(
funds_another_faucet(Some(&other), &wallet),
"wallet replaced"
);
}
#[test]
fn a_continuing_chain_keeps_its_programs_and_a_new_one_drops_them() {
let mut before = Env {
sequencer: "http://127.0.0.1:3040".into(),
..Env::default()
};
before
.programs
.insert("counter".into(), PublicId::parse(ID).unwrap());
let text = before.render();
let url = "http://127.0.0.1:3041".to_owned();
let kept = carried_env(true, Some(&text), url.clone()).unwrap();
assert_eq!(kept.programs, before.programs);
assert_eq!(kept.sequencer, url);
assert!(
carried_env(false, Some(&text), url.clone())
.unwrap()
.programs
.is_empty()
);
assert!(
carried_env(true, None, url.clone())
.unwrap()
.programs
.is_empty()
);
assert!(
carried_env(true, Some("garbage"), url).is_err(),
"never drops programs silently"
);
}
fn devnet_in(dir: &Path) -> Devnet {
Devnet::open(Home::locate(Some(dir.into())).unwrap()).unwrap()
}
#[test]
fn project_wallet_configs_never_touch_a_wallet() {
let dir = tempfile::tempdir().unwrap();
let devnet = devnet_in(&dir.path().join("home"));
let wallet = dir.path().join("provider");
std::fs::create_dir_all(&wallet).unwrap();
std::fs::write(wallet.join("storage.json"), "{}").unwrap();
assert!(devnet.write_project_wallet_config(&wallet).is_err());
assert!(!wallet.join("wallet_config.json").exists());
}
#[test]
fn writes_a_project_wallet_config_into_a_new_directory_once() {
let dir = tempfile::tempdir().unwrap();
let devnet = devnet_in(&dir.path().join("home"));
let target = dir.path().join("new/buyer");
let path = devnet.write_project_wallet_config(&target).unwrap();
let config: Value = serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
assert_eq!(config["sequencer_addr"], "http://127.0.0.1:3040");
assert!(devnet.write_project_wallet_config(&target).is_err());
}
#[test]
fn fund_and_deploy_refuse_before_up() {
let dir = tempfile::tempdir().unwrap();
let devnet = devnet_in(dir.path());
let err = devnet.fund(ID, 1).unwrap_err();
assert!(err.to_string().contains("logos-devnet up"), "{err}");
let elf = dir.path().join("program.bin");
std::fs::write(&elf, [0u8; 8]).unwrap();
let err = devnet.deploy("counter", &elf).unwrap_err();
assert!(err.to_string().contains("logos-devnet up"), "{err}");
assert!(
devnet.deploy("Counter", &elf).is_err(),
"bad names are refused first"
);
}
#[test]
fn up_refuses_another_lez_tag_on_an_existing_chain() {
let dir = tempfile::tempdir().unwrap();
let home = Home::locate(Some(dir.path().into())).unwrap();
std::fs::create_dir_all(home.chain_dir().join("rocksdb-0101")).unwrap();
let opts = UpOptions {
lez: Some("v9.9.9".into()),
..UpOptions::default()
};
let err = Devnet::open(home.clone()).unwrap().up(&opts).unwrap_err();
assert!(err.to_string().contains("reset --lez v9.9.9"), "{err}");
assert!(!home.settings_file().exists(), "nothing saved");
}
#[test]
fn a_taken_port_changes_nothing() {
let dir = tempfile::tempdir().unwrap();
let home = Home::locate(Some(dir.path().into())).unwrap();
std::fs::create_dir_all(home.chain_dir().join("rocksdb-0101")).unwrap();
let taken = std::net::TcpListener::bind(("127.0.0.1", 0)).unwrap();
let opts = UpOptions {
port: Some(taken.local_addr().unwrap().port()),
..UpOptions::default()
};
let err = Devnet::open(home.clone())
.unwrap()
.reset(&opts)
.unwrap_err();
assert!(err.to_string().contains("is taken"), "{err}");
assert!(!home.settings_file().exists(), "nothing saved");
assert!(home.has_chain(), "nothing deleted");
}
#[test]
fn a_bad_block_time_changes_nothing() {
let dir = tempfile::tempdir().unwrap();
let home = Home::locate(Some(dir.path().into())).unwrap();
std::fs::create_dir_all(home.chain_dir().join("rocksdb-0101")).unwrap();
for command in [Devnet::up, Devnet::reset] {
let opts = UpOptions {
block_time: Some("1".into()),
..UpOptions::default()
};
assert!(command(Devnet::open(home.clone()).unwrap(), &opts).is_err());
assert!(!home.settings_file().exists(), "nothing saved");
assert!(home.has_chain(), "nothing deleted");
}
}
}