1use std::{num::NonZeroU16, str::FromStr};
5
6use colored::Colorize as _;
7use linera_base::{data_types::Amount, listen_for_shutdown_signals, time::Duration};
8use linera_client::client_options::ResourceControlPolicyConfig;
9use linera_rpc::config::{CrossChainConfig, ExporterServiceConfig};
10#[cfg(feature = "storage-service")]
11use linera_storage_service::{
12 child::{StorageService, StorageServiceGuard},
13 common::get_service_storage_binary,
14};
15use tokio_util::sync::CancellationToken;
16use tracing::info;
17#[cfg(feature = "kubernetes")]
18use {
19 crate::cli_wrappers::local_kubernetes_net::{BuildMode, LocalKubernetesNetConfig},
20 std::path::PathBuf,
21};
22
23use crate::{
24 cli_wrappers::{
25 local_net::{
26 Database, ExportersSetup, InnerStorageConfigBuilder, LocalNetConfig, PathProvider,
27 },
28 ClientWrapper, FaucetService, LineraNet, LineraNetConfig, Network, NetworkConfig,
29 },
30 storage::{InnerStorageConfig, StorageConfig},
31};
32
33struct StorageConfigProvider {
34 config: StorageConfig,
36 #[cfg(feature = "storage-service")]
37 _service_guard: Option<StorageServiceGuard>,
38}
39
40impl StorageConfigProvider {
41 pub async fn new(storage: &Option<String>) -> anyhow::Result<StorageConfigProvider> {
42 match storage {
43 #[cfg(feature = "storage-service")]
44 None => {
45 let service_endpoint = linera_base::port::get_free_endpoint().await?;
46 let binary = get_service_storage_binary().await?.display().to_string();
47 let service = StorageService::new(&service_endpoint, binary);
48 let _service_guard = service.run().await?;
49 let _service_guard = Some(_service_guard);
50 let inner_storage_config = InnerStorageConfig::Service {
51 endpoint: service_endpoint,
52 };
53 let namespace = "table_default".to_string();
54 let config = StorageConfig {
55 inner_storage_config,
56 namespace,
57 };
58 Ok(StorageConfigProvider {
59 config,
60 _service_guard,
61 })
62 }
63 #[cfg(not(feature = "storage-service"))]
64 None => {
65 panic!("When storage is not selected, the storage-service needs to be enabled");
66 }
67 #[cfg(feature = "storage-service")]
68 Some(storage) => {
69 let config = StorageConfig::from_str(storage)?;
70 Ok(StorageConfigProvider {
71 config,
72 _service_guard: None,
73 })
74 }
75 #[cfg(not(feature = "storage-service"))]
76 Some(storage) => {
77 let config = StorageConfig::from_str(storage)?;
78 Ok(StorageConfigProvider { config })
79 }
80 }
81 }
82
83 pub fn inner_storage_config(&self) -> &InnerStorageConfig {
84 &self.config.inner_storage_config
85 }
86
87 pub fn namespace(&self) -> &str {
88 &self.config.namespace
89 }
90
91 pub fn database(&self) -> anyhow::Result<Database> {
92 match self.config.inner_storage_config {
93 InnerStorageConfig::Memory { .. } => anyhow::bail!("Not possible to work with memory"),
94 #[cfg(feature = "rocksdb")]
95 InnerStorageConfig::RocksDb { .. } => {
96 anyhow::bail!("Not possible to work with RocksDB")
97 }
98 #[cfg(feature = "storage-service")]
99 InnerStorageConfig::Service { .. } => Ok(Database::Service),
100 #[cfg(feature = "dynamodb")]
101 InnerStorageConfig::DynamoDb { .. } => Ok(Database::DynamoDb),
102 #[cfg(feature = "scylladb")]
103 InnerStorageConfig::ScyllaDb { .. } => Ok(Database::ScyllaDb),
104 #[cfg(all(feature = "rocksdb", feature = "scylladb"))]
105 InnerStorageConfig::DualRocksDbScyllaDb { .. } => Ok(Database::DualRocksDbScyllaDb),
106 }
107 }
108}
109
110#[expect(clippy::too_many_arguments)]
111#[cfg(feature = "kubernetes")]
112pub async fn handle_net_up_kubernetes(
113 num_other_initial_chains: u32,
114 initial_amount: u128,
115 num_initial_validators: usize,
116 num_shards: usize,
117 testing_prng_seed: Option<u64>,
118 binaries: &Option<Option<PathBuf>>,
119 no_build: bool,
120 docker_image_name: String,
121 build_mode: BuildMode,
122 policy_config: ResourceControlPolicyConfig,
123 with_faucet: bool,
124 faucet_chain: Option<u32>,
125 faucet_port: NonZeroU16,
126 faucet_amount: Amount,
127 dual_store: bool,
128) -> anyhow::Result<()> {
129 if num_initial_validators < 1 {
130 panic!("The local test network must have at least one validator.");
131 }
132 if num_shards < 1 {
133 panic!("The local test network must have at least one shard per validator.");
134 }
135 if faucet_chain.is_some() {
136 assert!(
137 with_faucet,
138 "--faucet-chain must be provided only with --with-faucet"
139 );
140 }
141
142 let shutdown_notifier = CancellationToken::new();
143 tokio::spawn(listen_for_shutdown_signals(shutdown_notifier.clone()));
144
145 let config = LocalKubernetesNetConfig {
146 network: Network::Grpc,
147 testing_prng_seed,
148 num_other_initial_chains,
149 initial_amount: Amount::from_tokens(initial_amount),
150 num_initial_validators,
151 num_shards,
152 binaries: binaries.clone().into(),
153 no_build,
154 docker_image_name,
155 build_mode,
156 policy_config,
157 dual_store,
158 };
159 let (mut net, client) = config.instantiate().await?;
160 let faucet_service = print_messages_and_create_faucet(
161 client,
162 with_faucet,
163 faucet_chain,
164 faucet_port,
165 faucet_amount,
166 num_other_initial_chains,
167 )
168 .await?;
169 wait_for_shutdown(shutdown_notifier, &mut net, faucet_service).await
170}
171
172#[expect(clippy::too_many_arguments)]
173pub async fn handle_net_up_service(
174 num_other_initial_chains: u32,
175 initial_amount: u128,
176 num_initial_validators: usize,
177 num_shards: usize,
178 testing_prng_seed: Option<u64>,
179 policy_config: ResourceControlPolicyConfig,
180 cross_chain_config: CrossChainConfig,
181 with_block_exporter: bool,
182 block_exporter_port: NonZeroU16,
183 path: &Option<String>,
184 storage: &Option<String>,
185 external_protocol: String,
186 with_faucet: bool,
187 faucet_chain: Option<u32>,
188 faucet_port: NonZeroU16,
189 faucet_amount: Amount,
190) -> anyhow::Result<()> {
191 if num_initial_validators < 1 {
192 panic!("The local test network must have at least one validator.");
193 }
194 if num_shards < 1 {
195 panic!("The local test network must have at least one shard per validator.");
196 }
197
198 let shutdown_notifier = CancellationToken::new();
199 tokio::spawn(listen_for_shutdown_signals(shutdown_notifier.clone()));
200
201 let storage = StorageConfigProvider::new(storage).await?;
202 let storage_config = storage.inner_storage_config().clone();
203 let namespace = storage.namespace().to_string();
204 let database = storage.database()?;
205 let storage_config_builder = InnerStorageConfigBuilder::ExistingConfig { storage_config };
206 let external = match external_protocol.as_str() {
207 "grpc" => Network::Grpc,
208 "grpcs" => Network::Grpcs,
209 _ => panic!("Only allowed options are grpc and grpcs"),
210 };
211 let internal = Network::Grpc;
212 let network = NetworkConfig { external, internal };
213 let path_provider = PathProvider::from_path_option(path)?;
214 let num_proxies = 1; let block_exporters = if with_block_exporter {
216 let exporter_config = ExporterServiceConfig {
217 host: "localhost".to_owned(),
218 port: block_exporter_port.into(),
219 };
220 ExportersSetup::Remote(vec![exporter_config])
221 } else {
222 ExportersSetup::Local(vec![])
223 };
224 let config = LocalNetConfig {
225 network,
226 database,
227 testing_prng_seed,
228 namespace,
229 num_other_initial_chains,
230 initial_amount: Amount::from_tokens(initial_amount),
231 num_initial_validators,
232 num_shards,
233 num_proxies,
234 policy_config,
235 cross_chain_config,
236 storage_config_builder,
237 path_provider,
238 block_exporters,
239 };
240 let (mut net, client) = config.instantiate().await?;
241 let faucet_service = print_messages_and_create_faucet(
242 client,
243 with_faucet,
244 faucet_chain,
245 faucet_port,
246 faucet_amount,
247 num_other_initial_chains,
248 )
249 .await?;
250
251 wait_for_shutdown(shutdown_notifier, &mut net, faucet_service).await
252}
253
254async fn wait_for_shutdown(
255 shutdown_notifier: CancellationToken,
256 net: &mut impl LineraNet,
257 faucet_service: Option<FaucetService>,
258) -> anyhow::Result<()> {
259 shutdown_notifier.cancelled().await;
260 eprintln!();
261 if let Some(service) = faucet_service {
262 eprintln!("Terminating the faucet service");
263 service.terminate().await?;
264 }
265 eprintln!("Terminating the local test network");
266 net.terminate().await?;
267 eprintln!("Done.");
268
269 Ok(())
270}
271
272async fn print_messages_and_create_faucet(
273 client: ClientWrapper,
274 with_faucet: bool,
275 faucet_chain: Option<u32>,
276 faucet_port: NonZeroU16,
277 faucet_amount: Amount,
278 num_other_initial_chains: u32,
279) -> Result<Option<FaucetService>, anyhow::Error> {
280 linera_base::time::timer::sleep(Duration::from_secs(1)).await;
282
283 info!("Local test network successfully started.");
285
286 eprintln!(
287 "To use the admin wallet of this test network, you may set \
288 the environment variables LINERA_WALLET, LINERA_KEYSTORE, \
289 and LINERA_STORAGE as follows.\n"
290 );
291 println!(
292 "{}",
293 format!(
294 "export LINERA_WALLET=\"{}\"",
295 client.wallet_path().display()
296 )
297 .bold()
298 );
299 println!(
300 "{}",
301 format!(
302 "export LINERA_KEYSTORE=\"{}\"",
303 client.keystore_path().display()
304 )
305 .bold()
306 );
307 println!(
308 "{}",
309 format!("export LINERA_STORAGE=\"{}\"\n", client.storage_path()).bold()
310 );
311
312 let wallet = client.load_wallet()?;
313 let chains = wallet.chain_ids();
314
315 let faucet_service = if with_faucet {
317 let faucet_chain_idx = faucet_chain.unwrap_or(0);
318 assert!(
319 num_other_initial_chains > faucet_chain_idx,
320 "num_other_initial_chains must be strictly greater than the faucet chain index if \
321 with_faucet is true"
322 );
323 let faucet_chain = chains
325 .into_iter()
326 .filter(|chain_id| *chain_id != wallet.genesis_admin_chain())
327 .nth(faucet_chain_idx as usize)
328 .unwrap(); let service = client
330 .run_faucet(Some(faucet_port.into()), faucet_chain, faucet_amount)
331 .await?;
332 Some(service)
333 } else {
334 None
335 };
336
337 eprintln!(
338 "\nREADY!\nPress ^C to terminate the local test network and clean the temporary directory."
339 );
340
341 Ok(faucet_service)
342}