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