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;
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_proxies: usize,
117 num_shards: usize,
118 testing_prng_seed: Option<u64>,
119 binaries: &Option<Option<PathBuf>>,
120 no_build: bool,
121 docker_image_name: String,
122 build_mode: BuildMode,
123 policy_config: ResourceControlPolicyConfig,
124 with_faucet: bool,
125 faucet_chain: Option<u32>,
126 faucet_port: NonZeroU16,
127 faucet_amount: Amount,
128 with_block_exporter: bool,
129 num_block_exporters: usize,
130 indexer_image_name: String,
131 explorer_image_name: String,
132 dual_store: bool,
133 path: &Option<String>,
134) -> anyhow::Result<()> {
135 assert!(
136 num_initial_validators >= 1,
137 "The local test network must have at least one validator."
138 );
139 assert!(
140 num_proxies >= 1,
141 "The local test network must have at least one proxy."
142 );
143 assert!(
144 num_shards >= 1,
145 "The local test network must have at least one shard per validator."
146 );
147 if faucet_chain.is_some() {
148 assert!(
149 with_faucet,
150 "--faucet-chain must be provided only with --with-faucet"
151 );
152 }
153
154 let shutdown_notifier = CancellationToken::new();
155 tokio::spawn(listen_for_shutdown_signals(shutdown_notifier.clone()));
156
157 let num_block_exporters = if with_block_exporter {
158 assert!(
159 num_block_exporters > 0,
160 "If --with-block-exporter is provided, --num-block-exporters must be greater than 0"
161 );
162 num_block_exporters
163 } else {
164 0
165 };
166
167 let config = LocalKubernetesNetConfig {
168 network: Network::Grpc,
169 testing_prng_seed,
170 num_other_initial_chains,
171 initial_amount: Amount::from_tokens(initial_amount),
172 num_initial_validators,
173 num_proxies,
174 num_shards,
175 binaries: binaries.clone().into(),
176 no_build,
177 docker_image_name,
178 build_mode,
179 policy_config,
180 num_block_exporters,
181 indexer_image_name,
182 explorer_image_name,
183 dual_store,
184 path_provider: PathProvider::from_path_option(path)?,
185 };
186 let (mut net, client) = config.instantiate().await?;
187 let faucet_service = print_messages_and_create_faucet(
188 client,
189 with_faucet,
190 faucet_chain,
191 faucet_port,
192 faucet_amount,
193 num_other_initial_chains,
194 )
195 .await?;
196 wait_for_shutdown(shutdown_notifier, &mut net, faucet_service).await
197}
198
199#[expect(clippy::too_many_arguments)]
200pub async fn handle_net_up_service(
201 num_other_initial_chains: u32,
202 initial_amount: u128,
203 num_initial_validators: usize,
204 num_shards: usize,
205 testing_prng_seed: Option<u64>,
206 policy_config: ResourceControlPolicyConfig,
207 cross_chain_config: CrossChainConfig,
208 with_block_exporter: bool,
209 block_exporter_address: String,
210 block_exporter_port: NonZeroU16,
211 path: &Option<String>,
212 storage: &Option<String>,
213 external_protocol: String,
214 with_faucet: bool,
215 faucet_chain: Option<u32>,
216 faucet_port: NonZeroU16,
217 faucet_amount: Amount,
218) -> anyhow::Result<()> {
219 assert!(
220 num_initial_validators >= 1,
221 "The local test network must have at least one validator."
222 );
223 assert!(
224 num_shards >= 1,
225 "The local test network must have at least one shard per validator."
226 );
227
228 let shutdown_notifier = CancellationToken::new();
229 tokio::spawn(listen_for_shutdown_signals(shutdown_notifier.clone()));
230
231 let storage = StorageConfigProvider::new(storage).await?;
232 let storage_config = storage.inner_storage_config().clone();
233 let namespace = storage.namespace().to_string();
234 let database = storage.database()?;
235 let storage_config_builder = InnerStorageConfigBuilder::ExistingConfig { storage_config };
236 let external = match external_protocol.as_str() {
237 "grpc" => Network::Grpc,
238 "grpcs" => Network::Grpcs,
239 _ => panic!("Only allowed options are grpc and grpcs"),
240 };
241 let internal = Network::Grpc;
242 let network = NetworkConfig { external, internal };
243 let path_provider = PathProvider::from_path_option(path)?;
244 let num_proxies = 1; let block_exporters = ExportersSetup::new(
246 with_block_exporter,
247 block_exporter_address,
248 block_exporter_port,
249 );
250 let config = LocalNetConfig {
251 network,
252 database,
253 testing_prng_seed,
254 namespace,
255 num_other_initial_chains,
256 initial_amount: Amount::from_tokens(initial_amount),
257 num_initial_validators,
258 num_shards,
259 num_proxies,
260 policy_config,
261 cross_chain_config,
262 storage_config_builder,
263 path_provider,
264 block_exporters,
265 };
266 let (mut net, client) = config.instantiate().await?;
267 let faucet_service = print_messages_and_create_faucet(
268 client,
269 with_faucet,
270 faucet_chain,
271 faucet_port,
272 faucet_amount,
273 num_other_initial_chains,
274 )
275 .await?;
276
277 wait_for_shutdown(shutdown_notifier, &mut net, faucet_service).await
278}
279
280async fn wait_for_shutdown(
281 shutdown_notifier: CancellationToken,
282 net: &mut impl LineraNet,
283 faucet_service: Option<FaucetService>,
284) -> anyhow::Result<()> {
285 shutdown_notifier.cancelled().await;
286 eprintln!();
287 if let Some(service) = faucet_service {
288 eprintln!("Terminating the faucet service");
289 service.terminate().await?;
290 }
291 eprintln!("Terminating the local test network");
292 net.terminate().await?;
293 eprintln!("Done.");
294
295 Ok(())
296}
297
298async fn print_messages_and_create_faucet(
299 client: ClientWrapper,
300 with_faucet: bool,
301 faucet_chain: Option<u32>,
302 faucet_port: NonZeroU16,
303 faucet_amount: Amount,
304 num_other_initial_chains: u32,
305) -> Result<Option<FaucetService>, anyhow::Error> {
306 linera_base::time::timer::sleep(Duration::from_secs(1)).await;
308
309 info!("Local test network successfully started.");
311
312 eprintln!(
313 "To use the admin wallet of this test network, you may set \
314 the environment variables LINERA_WALLET, LINERA_KEYSTORE, \
315 and LINERA_STORAGE as follows.\n"
316 );
317 println!(
318 "{}",
319 format!(
320 "export LINERA_WALLET=\"{}\"",
321 client.wallet_path().display(),
322 )
323 .bold(),
324 );
325 println!(
326 "{}",
327 format!(
328 "export LINERA_KEYSTORE=\"{}\"",
329 client.keystore_path().display(),
330 )
331 .bold()
332 );
333 println!(
334 "{}",
335 format!("export LINERA_STORAGE=\"{}\"", client.storage_path()).bold(),
336 );
337
338 let wallet = client.load_wallet()?;
339 let chains = wallet.chain_ids();
340
341 let faucet_service = if with_faucet {
343 let faucet_chain = if let Some(faucet_chain_idx) = faucet_chain {
344 assert!(
345 num_other_initial_chains > faucet_chain_idx,
346 "num_other_initial_chains must be strictly greater than the faucet chain index if \
347 with_faucet is true"
348 );
349
350 eprintln!("To connect to this network, you can use the following faucet URL:");
351 println!(
352 "{}",
353 format!("export LINERA_FAUCET_URL=\"http://localhost:{faucet_port}\"").bold(),
354 );
355
356 Some(
358 chains
359 .into_iter()
360 .filter(|chain_id| *chain_id != wallet.genesis_admin_chain())
361 .nth(faucet_chain_idx as usize)
362 .expect("there should be at least one non-admin chain"),
363 )
364 } else {
365 None
366 };
367 let service = client
368 .run_faucet(Some(faucet_port.into()), faucet_chain, faucet_amount)
369 .await?;
370 Some(service)
371 } else {
372 None
373 };
374
375 println!();
376
377 eprintln!(
378 "\nREADY!\nPress ^C to terminate the local test network and clean the temporary directory."
379 );
380
381 Ok(faucet_service)
382}