use std::{env, path::PathBuf};
use anyhow::{anyhow, bail, Error};
use linera_base::crypto::InMemorySigner;
use linera_client::{client_context::ClientContext, config::GenesisConfig};
use linera_execution::{WasmRuntime, WithWasmDefault as _};
use linera_persistent as persistent;
use linera_service::{
cli::command::ClientCommand,
storage::{CommonStorageOptions, Runnable, RunnableWithStore, StorageConfig},
Wallet,
};
use tracing::{debug, info};
#[derive(Clone, clap::Parser)]
#[command(
name = "linera",
version = linera_version::VersionInfo::default_clap_str(),
about = "Client implementation and command-line tool for the Linera blockchain",
)]
pub struct Options {
#[command(flatten)]
pub client_options: linera_client::Options,
#[arg(long = "wallet")]
pub wallet_state_path: Option<PathBuf>,
#[arg(long = "keystore")]
pub keystore_path: Option<PathBuf>,
#[arg(long, short = 'w', value_parser = crate::util::parse_ascii_alphanumeric_string)]
pub with_wallet: Option<String>,
#[arg(long)]
pub chrome_trace_exporter: bool,
#[arg(long, env = "LINERA_CHROME_TRACE_FILE")]
pub chrome_trace_file: Option<String>,
#[arg(long, env = "LINERA_OTLP_EXPORTER_ENDPOINT")]
pub otlp_exporter_endpoint: Option<String>,
#[arg(long = "storage", global = true)]
pub storage_config: Option<String>,
#[command(flatten)]
pub common_storage_options: CommonStorageOptions,
#[arg(long)]
pub wasm_runtime: Option<WasmRuntime>,
#[arg(long = "with-application-logs")]
pub application_logs: bool,
#[arg(long, env = "LINERA_CLIENT_TOKIO_THREADS")]
pub tokio_threads: Option<usize>,
#[arg(long, env = "LINERA_CLIENT_TOKIO_BLOCKING_THREADS")]
pub tokio_blocking_threads: Option<usize>,
#[command(subcommand)]
pub command: ClientCommand,
}
impl Options {
pub fn init() -> Self {
<Options as clap::Parser>::parse()
}
pub async fn create_client_context<S, Si>(
&self,
storage: S,
wallet: Wallet,
signer: Si,
) -> anyhow::Result<
ClientContext<linera_core::environment::Impl<S, linera_rpc::NodeProvider, Si, Wallet>>,
>
where
S: linera_core::environment::Storage,
Si: linera_core::environment::Signer,
{
let genesis_config = wallet.genesis_config().clone();
let default_chain = wallet.default_chain();
Ok(ClientContext::new(
storage,
wallet,
signer,
&self.client_options,
default_chain,
genesis_config,
)
.await?)
}
pub async fn run_with_storage<R: Runnable>(&self, job: R) -> Result<R::Output, Error> {
let storage_config = self.storage_config()?;
debug!("Running command using storage configuration: {storage_config}");
let store_config =
storage_config.add_common_storage_options(&self.common_storage_options)?;
let output = Box::pin(store_config.run_with_storage(
self.wasm_runtime.with_wasm_default(),
self.application_logs,
job,
))
.await?;
Ok(output)
}
pub async fn run_with_store<R: RunnableWithStore>(
&self,
assert_storage_v1: bool,
need_migration: bool,
job: R,
) -> Result<R::Output, Error> {
let storage_config = self.storage_config()?;
debug!("Running command using storage configuration: {storage_config}");
let store_config =
storage_config.add_common_storage_options(&self.common_storage_options)?;
if assert_storage_v1 {
store_config
.clone()
.run_with_store(crate::AssertStorageV1)
.await?;
}
if need_migration {
store_config
.clone()
.run_with_store(crate::StorageMigration)
.await?;
}
let output = Box::pin(store_config.run_with_store(job)).await?;
Ok(output)
}
pub async fn initialize_storage(&self) -> Result<(), Error> {
let storage_config = self.storage_config()?;
debug!("Initializing storage using configuration: {storage_config}");
let store_config =
storage_config.add_common_storage_options(&self.common_storage_options)?;
let wallet = self.wallet()?;
store_config.initialize(wallet.genesis_config()).await?;
Ok(())
}
pub fn wallet(&self) -> Result<Wallet, Error> {
Ok(Wallet::read(&self.wallet_path()?)?)
}
pub fn signer(&self) -> Result<persistent::File<InMemorySigner>, Error> {
Ok(persistent::File::read(&self.keystore_path()?)?)
}
pub fn suffix(&self) -> String {
self.with_wallet
.as_ref()
.map(|x| format!("_{}", x))
.unwrap_or_default()
}
pub fn config_path() -> Result<PathBuf, Error> {
let mut config_dir = dirs::config_dir().ok_or_else(|| anyhow!(
"Default wallet directory is not supported in this platform: please specify storage and wallet paths"
))?;
config_dir.push("linera");
if !config_dir.exists() {
debug!("Creating default wallet directory {}", config_dir.display());
fs_err::create_dir_all(&config_dir)?;
}
info!("Using default wallet directory {}", config_dir.display());
Ok(config_dir)
}
pub fn storage_config(&self) -> Result<StorageConfig, Error> {
if let Some(config) = &self.storage_config {
return config.parse();
}
let suffix = self.suffix();
let storage_env_var = env::var(format!("LINERA_STORAGE{suffix}")).ok();
if let Some(config) = storage_env_var {
return config.parse();
}
cfg_if::cfg_if! {
if #[cfg(feature = "rocksdb")] {
let spawn_mode =
linera_views::rocks_db::RocksDbSpawnMode::get_spawn_mode_from_runtime();
let inner_storage_config = linera_service::storage::InnerStorageConfig::RocksDb {
path: Self::config_path()?.join("wallet.db"),
spawn_mode,
};
let namespace = linera_storage::DEFAULT_NAMESPACE.to_string();
Ok(StorageConfig {
inner_storage_config,
namespace,
})
} else {
bail!("Cannot apply default storage because the feature 'rocksdb' was not selected");
}
}
}
pub fn wallet_path(&self) -> Result<PathBuf, Error> {
if let Some(path) = &self.wallet_state_path {
return Ok(path.clone());
}
let suffix = self.suffix();
let wallet_env_var = env::var(format!("LINERA_WALLET{suffix}")).ok();
if let Some(path) = wallet_env_var {
return Ok(path.parse()?);
}
let config_path = Self::config_path()?;
Ok(config_path.join("wallet.json"))
}
pub fn keystore_path(&self) -> Result<PathBuf, Error> {
if let Some(path) = &self.keystore_path {
return Ok(path.clone());
}
let suffix = self.suffix();
let keystore_env_var = env::var(format!("LINERA_KEYSTORE{suffix}")).ok();
if let Some(path) = keystore_env_var {
return Ok(path.parse()?);
}
let config_path = Self::config_path()?;
Ok(config_path.join("keystore.json"))
}
pub fn create_wallet(&self, genesis_config: GenesisConfig) -> Result<Wallet, Error> {
let wallet_path = self.wallet_path()?;
if wallet_path.exists() {
bail!("Wallet already exists: {}", wallet_path.display());
}
let wallet = Wallet::create(&wallet_path, genesis_config)?;
wallet.save()?;
Ok(wallet)
}
pub fn create_keystore(
&self,
testing_prng_seed: Option<u64>,
) -> Result<persistent::File<InMemorySigner>, Error> {
let keystore_path = self.keystore_path()?;
if keystore_path.exists() {
bail!("Keystore already exists: {}", keystore_path.display());
}
Ok(persistent::File::read_or_create(&keystore_path, || {
Ok(InMemorySigner::new(testing_prng_seed))
})?)
}
}