blueprint_chain_setup_tangle/
deploy.rs1use alloy_network::{AnyNetwork, ReceiptResponse};
2use alloy_provider::network::TransactionBuilder;
3use alloy_provider::{Provider, WsConnect};
4use alloy_rpc_types::serde_helpers::WithOtherFields;
5use alloy_rpc_types_eth::TransactionRequest;
6use alloy_signer_local::PrivateKeySigner;
7use blueprint_chain_setup_common::signer::{load_evm_signer_from_env, load_signer_from_env};
8use blueprint_crypto::tangle_pair_signer::TanglePairSigner;
9use blueprint_std::fmt::Debug;
10use blueprint_std::io::{BufRead, BufReader};
11use blueprint_std::path::Path;
12use blueprint_std::path::PathBuf;
13use blueprint_std::process::{Command, Stdio};
14use blueprint_std::{env, rand, thread};
15use blueprint_tangle_extra::metadata::types::blueprint::BlueprintServiceManager;
16use color_eyre::eyre::{self, Context, ContextCompat, Result, eyre};
17use dialoguer::console::style;
18use indicatif::{ProgressBar, ProgressStyle};
19use std::sync::{
20 Arc,
21 atomic::{AtomicBool, Ordering},
22};
23use subxt::tx::Signer;
24use tangle_subxt::subxt;
25use tangle_subxt::tangle_testnet_runtime::api as TangleApi;
26use tangle_subxt::tangle_testnet_runtime::api::services::calls::types::create_blueprint::Blueprint;
27
28#[derive(Clone)]
29pub struct Opts {
30 pub pkg_name: Option<String>,
32 pub http_rpc_url: String,
34 pub ws_rpc_url: String,
36 pub manifest_path: blueprint_std::path::PathBuf,
38 pub signer: Option<TanglePairSigner<sp_core::sr25519::Pair>>,
40 pub signer_evm: Option<PrivateKeySigner>,
42}
43
44impl Debug for Opts {
45 fn fmt(&self, f: &mut blueprint_std::fmt::Formatter<'_>) -> blueprint_std::fmt::Result {
46 f.debug_struct("Opts")
47 .field("pkg_name", &self.pkg_name)
48 .field("http_rpc_url", &self.http_rpc_url)
49 .field("ws_rpc_url", &self.ws_rpc_url)
50 .field("manifest_path", &self.manifest_path)
51 .finish_non_exhaustive()
52 }
53}
54
55#[derive(thiserror::Error, Debug)]
56pub enum Error {
57 #[error("Unsupported blueprint manager kind")]
58 UnsupportedBlueprintManager,
59 #[error("Contract not found at `{0}`, check the manager in your `Cargo.toml`!")]
60 ContractNotFound(PathBuf),
61 #[error("Failed to deserialize contract `{0}`: {1}")]
62 DeserializeContract(String, serde_json::Error),
63 #[error("The source at index {0} does not have a valid fetcher")]
64 MissingFetcher(usize),
65 #[error("No matching packages found in the workspace")]
66 NoPackageFound,
67 #[error("The workspace has multiple packages, please specify the package to deploy")]
68 ManyPackages,
69
70 #[error("{0}")]
71 Io(#[from] std::io::Error),
72}
73
74async fn generate_service_blueprint<P: Into<PathBuf>, T: AsRef<str>>(
75 manifest_metadata_path: P,
76 pkg_name: Option<&String>,
77 rpc_url: T,
78 signer_evm: Option<PrivateKeySigner>,
79) -> Result<Blueprint> {
80 let manifest_path = manifest_metadata_path.into();
81 let metadata = cargo_metadata::MetadataCommand::new()
82 .manifest_path(manifest_path)
83 .no_deps()
84 .exec()
85 .context("Getting Metadata about the workspace")?;
86
87 let package = find_package(&metadata, pkg_name)?.clone();
88
89 let mut blueprint = load_blueprint_metadata(&package)?;
90 build_contracts_if_needed(&package, &blueprint).context("Building contracts")?;
91 deploy_contracts_to_tangle(rpc_url.as_ref(), &package, &mut blueprint, signer_evm).await?;
92
93 Ok(blueprint.try_into()?)
94}
95
96pub async fn deploy_to_tangle(
105 Opts {
106 pkg_name,
107 http_rpc_url: _,
108 ws_rpc_url,
109 manifest_path,
110 signer,
111 signer_evm,
112 }: Opts,
113) -> Result<u64> {
114 let progress_bar = ProgressBar::new(100);
116 progress_bar.set_style(
117 ProgressStyle::default_bar()
118 .template("{spinner:.green} [{bar:40.cyan/blue}] {pos}% {msg}")?
119 .progress_chars("#>-"),
120 );
121
122 let update_progress = |percent: u64, message: &str| {
124 progress_bar.set_position(percent);
125 progress_bar.set_message(message.to_string());
126 };
127
128 update_progress(0, "Generating blueprint");
130
131 let should_stop = Arc::new(AtomicBool::new(false));
133 let should_stop_clone = should_stop.clone();
134
135 let progress_clone = progress_bar.clone();
137 let _progress_handle = std::thread::spawn(move || {
138 let mut current = 0;
139 let target = 80;
140 let increment = rand::random::<u64>() % 2 + 1;
141 let sleep_duration = std::time::Duration::from_millis(1000);
142
143 while current < target && !should_stop_clone.load(Ordering::Relaxed) {
144 std::thread::sleep(sleep_duration);
145 current += increment;
146 progress_clone.set_position(current);
147 progress_clone.set_message(format!("Generating blueprint... ({current}%)"));
148 }
149 });
150
151 update_progress(60, "Generating blueprint");
153 let blueprint = generate_service_blueprint(
154 manifest_path,
155 pkg_name.as_ref(),
156 ws_rpc_url.clone(),
157 signer_evm,
158 )
159 .await?;
160
161 should_stop.store(true, Ordering::Relaxed);
163
164 if progress_bar.position() < 80 {
166 update_progress(80, "Blueprint generated");
167 } else {
168 update_progress(progress_bar.position(), "Blueprint generated");
169 }
170
171 std::thread::sleep(std::time::Duration::from_millis(100));
173
174 let signer = if let Some(signer) = signer {
175 signer
176 } else {
177 load_signer_from_env()?
178 };
179
180 let my_account_id = signer.account_id();
181 update_progress(85, &format!("Connected to Tangle Network at: {ws_rpc_url}"));
182 let client = subxt::OnlineClient::from_url(ws_rpc_url.clone()).await?;
183
184 update_progress(90, "Creating blueprint transaction");
185 let create_blueprint_tx = TangleApi::tx().services().create_blueprint(blueprint);
186
187 update_progress(93, "Signing and submitting transaction");
188 let progress = client
189 .tx()
190 .sign_and_submit_then_watch_default(&create_blueprint_tx, &signer)
191 .await?;
192
193 update_progress(95, "Waiting for transaction confirmation");
194 let result = if cfg!(test) {
195 use blueprint_tangle_extra::util::TxProgressExt;
196 progress.wait_for_in_block_success().await?
197 } else {
198 blueprint_core::debug!("Waiting for the transaction to be finalized...");
199 let result = progress.wait_for_finalized_success().await?;
200 blueprint_core::debug!("Transaction finalized...");
201 result
202 };
203
204 update_progress(98, "Verifying blueprint creation");
205 let event = result
206 .find::<TangleApi::services::events::BlueprintCreated>()
207 .flatten()
208 .find(|e| e.owner.0 == my_account_id.0)
209 .context("Finding the `BlueprintCreated` event")
210 .map_err(|e| {
211 eyre::eyre!(
212 "Trying to find the `BlueprintCreated` event with your account Id: {:?}",
213 e
214 )
215 })?;
216
217 update_progress(100, "Deployment complete");
218 progress_bar.finish_with_message("Blueprint deployed successfully!");
219
220 blueprint_core::info!(
221 "Blueprint #{} created successfully by {} with extrinsic hash: {}",
222 event.blueprint_id,
223 event.owner,
224 result.extrinsic_hash(),
225 );
226
227 Ok(event.blueprint_id)
228}
229
230fn workspace_or_package_manifest_path(package: &cargo_metadata::Package) -> PathBuf {
234 env::var("CARGO_WORKSPACE_DIR").map_or_else(|_| find_workspace_root(package), PathBuf::from)
235}
236
237fn find_workspace_root(package: &cargo_metadata::Package) -> PathBuf {
240 let package_dir = package
241 .manifest_path
242 .parent()
243 .unwrap()
244 .as_std_path()
245 .to_path_buf();
246 let mut current_dir = package_dir.clone();
247 let mut workspace_root = package_dir;
248
249 while let Some(parent) = current_dir.parent() {
251 let potential_cargo_toml = parent.join("Cargo.toml");
252 blueprint_core::debug!(
253 "Looking for Cargo.toml at: {}",
254 potential_cargo_toml.display()
255 );
256 if potential_cargo_toml.exists() {
257 blueprint_core::debug!("Found Cargo.toml");
258 if let Ok(content) = std::fs::read_to_string(&potential_cargo_toml) {
260 if content.contains("[workspace]") {
261 blueprint_core::debug!(
262 "Found [workspace] section, using this directory as workspace root"
263 );
264 workspace_root = parent.to_path_buf();
265 break;
266 }
267 }
268 }
269 current_dir = parent.to_path_buf();
270
271 if current_dir.parent().is_none() {
273 break;
274 }
275 }
276
277 blueprint_core::debug!("Identified workspace root: {:?}", workspace_root);
278 workspace_root
279}
280
281fn do_cargo_build(manifest_path: &Path) -> Result<()> {
282 let mut cmd = Command::new("cargo");
283 cmd.arg("build")
284 .arg("--manifest-path")
285 .arg(manifest_path)
286 .arg("--all")
287 .stdout(Stdio::piped())
288 .stderr(Stdio::piped());
289
290 let mut child = cmd.spawn().expect("Failed to start cargo build");
291 let stdout = child.stdout.take().expect("Failed to capture stdout");
292 let stderr = child.stderr.take().expect("Failed to capture stderr");
293
294 let stdout_thread = thread::spawn(move || {
295 let reader = BufReader::new(stdout);
296 for line in reader.lines().map_while(Result::ok) {
297 blueprint_core::debug!(target: "build-output", "{}", line);
298 }
299 });
300
301 let stderr_thread = thread::spawn(move || {
302 let reader = BufReader::new(stderr);
303 for line in reader.lines().map_while(Result::ok) {
304 blueprint_core::debug!(target: "build-output", "{}", line);
305 }
306 });
307
308 let status = child.wait().expect("Failed to wait on cargo build");
309
310 stdout_thread.join().expect("Stdout thread panicked");
311 stderr_thread.join().expect("Stderr thread panicked");
312
313 if !status.success() {
314 blueprint_core::error!("Cargo build failed");
315 blueprint_core::error!("NOTE: Use `RUST_LOG=build-output=debug` to see more details");
316 return Err(eyre!("Cargo build failed"));
317 }
318
319 Ok(())
320}
321
322fn load_blueprint_metadata(
323 package: &cargo_metadata::Package,
324) -> Result<blueprint_tangle_extra::metadata::types::blueprint::ServiceBlueprint<'static>> {
325 let workspace_root = find_workspace_root(package);
327 let package_dir = package
328 .manifest_path
329 .parent()
330 .unwrap()
331 .as_std_path()
332 .to_path_buf();
333
334 let mut blueprint_json_path = workspace_root.join("blueprint.json");
336
337 if !blueprint_json_path.exists() {
339 blueprint_json_path = package_dir.join("blueprint.json");
340 }
341
342 if !blueprint_json_path.exists() {
343 blueprint_core::warn!(
344 "Could not find blueprint.json in workspace root or binary directory; running `cargo build`..."
345 );
346 blueprint_core::debug!(
347 "Looked for blueprint.json at workspace root: {:?}",
348 workspace_root.join("blueprint.json")
349 );
350 blueprint_core::debug!(
351 "Looked for blueprint.json at package dir: {:?}",
352 package_dir.join("blueprint.json")
353 );
354
355 do_cargo_build(&workspace_root.join("Cargo.toml"))?;
358 }
359
360 blueprint_json_path = workspace_root.join("blueprint.json");
362
363 if !blueprint_json_path.exists() {
365 blueprint_json_path = package_dir.join("blueprint.json");
366 }
367
368 blueprint_core::debug!("Found blueprint.json at: {:?}", blueprint_json_path);
369
370 let blueprint_json = std::fs::read_to_string(&blueprint_json_path).context(format!(
372 "Reading blueprint.json file at {blueprint_json_path:?}",
373 ))?;
374 let blueprint = serde_json::from_str(&blueprint_json)?;
375 Ok(blueprint)
376}
377
378async fn deploy_contracts_to_tangle(
379 rpc_url: &str,
380 package: &cargo_metadata::Package,
381 blueprint: &mut blueprint_tangle_extra::metadata::types::blueprint::ServiceBlueprint<'static>,
382 signer_evm: Option<PrivateKeySigner>,
383) -> Result<()> {
384 enum ContractKind {
385 Manager,
386 }
387 let contract_paths = match blueprint.manager {
388 BlueprintServiceManager::Evm(ref path) => vec![(ContractKind::Manager, path)],
389 _ => return Err(Error::UnsupportedBlueprintManager.into()),
390 };
391
392 let abs_contract_paths: Vec<_> = contract_paths
393 .into_iter()
394 .map(|(kind, path)| (kind, resolve_path_relative_to_package(package, path)))
395 .collect();
396
397 let mut contracts_raw = Vec::new();
398 for (kind, path) in abs_contract_paths {
399 if !path.exists() {
400 return Err(Error::ContractNotFound(path).into());
401 }
402
403 let content = std::fs::read_to_string(&path)?;
404 contracts_raw.push((
405 kind,
406 path.file_stem()
407 .unwrap_or_default()
408 .to_string_lossy()
409 .into_owned(),
410 content,
411 ));
412 }
413
414 let mut contracts = Vec::new();
415 for (kind, contract_name, json) in contracts_raw {
416 let contract = match serde_json::from_str::<alloy_json_abi::ContractObject>(&json) {
417 Ok(contract) => contract,
418 Err(e) => return Err(Error::DeserializeContract(contract_name, e).into()),
419 };
420
421 contracts.push((kind, contract_name, contract));
422 }
423
424 if contracts.is_empty() {
426 return Ok(());
427 }
428
429 let signer = if let Some(signer) = signer_evm {
430 signer
431 } else {
432 load_evm_signer_from_env()?
433 };
434
435 let wallet = alloy_provider::network::EthereumWallet::from(signer);
436 assert!(rpc_url.starts_with("ws://") || rpc_url.starts_with("wss://"));
437
438 let provider = alloy_provider::ProviderBuilder::new()
439 .network::<AnyNetwork>()
440 .wallet(wallet)
441 .connect_ws(WsConnect::new(rpc_url))
442 .await?;
443
444 let chain_id = provider.get_chain_id().await?;
445 blueprint_core::debug!("Chain ID: {chain_id}");
446
447 for (kind, name, contract) in contracts {
448 blueprint_core::info!("Deploying contract: {name} ...");
449 let Some(bytecode) = contract.bytecode.clone() else {
450 blueprint_core::warn!("Contract {name} does not have deployed bytecode! Skipping ...");
451 continue;
452 };
453
454 let tx = TransactionRequest::default().with_deploy_code(bytecode);
455 let receipt = provider
457 .send_transaction(WithOtherFields::new(tx))
458 .await?
459 .get_receipt()
460 .await?;
461 if receipt.status() {
463 let contract_address =
464 alloy_network::ReceiptResponse::contract_address(&receipt).unwrap();
465 blueprint_core::info!("Contract {name} deployed at: {contract_address}");
466 println!(
467 " {}",
468 style(format!("Contract {name} deployed at: {contract_address}")).yellow()
469 );
470
471 match kind {
472 ContractKind::Manager => {
473 blueprint.manager = BlueprintServiceManager::Evm(contract_address.to_string());
474 }
475 }
476 } else {
477 blueprint_core::error!("Contract {name} deployment failed!");
478 blueprint_core::debug!("Receipt: {receipt:#?}");
479 }
480 }
481 Ok(())
482}
483
484fn build_contracts_if_needed(
486 package: &cargo_metadata::Package,
487 blueprint: &blueprint_tangle_extra::metadata::types::blueprint::ServiceBlueprint<'static>,
488) -> Result<()> {
489 let pathes_to_check = match blueprint.manager {
490 BlueprintServiceManager::Evm(ref path) => vec![path],
491 _ => return Err(Error::UnsupportedBlueprintManager.into()),
492 };
493
494 blueprint_core::debug!("Checking for contracts to build: {pathes_to_check:?}");
495
496 let abs_pathes_to_check: Vec<_> = pathes_to_check
497 .into_iter()
498 .map(|path| resolve_path_relative_to_package(package, path))
499 .collect();
500
501 blueprint_core::debug!("Absolute paths to check: {abs_pathes_to_check:?}");
502
503 let needs_build = abs_pathes_to_check.iter().any(|path| !path.exists());
504 if !needs_build {
505 blueprint_core::debug!("All contracts are already built");
506 return Ok(());
507 }
508
509 blueprint_core::debug!("Contracts need to be built");
510
511 let workspace_root = find_workspace_root(package);
513 let package_dir = package
514 .manifest_path
515 .parent()
516 .unwrap()
517 .as_std_path()
518 .to_path_buf();
519
520 blueprint_core::debug!("Workspace root directory: {workspace_root:?}");
521
522 let mut contracts_dir = workspace_root.join("contracts");
524 if !contracts_dir.exists() {
525 blueprint_core::debug!(
526 "Contracts directory not found in workspace root: {contracts_dir:?}"
527 );
528
529 let package_contracts_dir = package_dir.join("contracts");
531 if !package_contracts_dir.exists() {
532 blueprint_core::debug!(
533 "Contracts directory not found in package directory: {package_contracts_dir:?}"
534 );
535 return Err(Error::ContractNotFound(contracts_dir).into());
536 }
537
538 blueprint_core::debug!("Using contracts directory from package: {package_contracts_dir:?}");
539 contracts_dir = package_contracts_dir;
540 }
541
542 blueprint_core::debug!("Contracts directory: {contracts_dir:?}");
543
544 let foundry = crate::foundry::FoundryToolchain::new();
545 foundry.check_installed_or_exit();
546
547 blueprint_core::debug!("Changing to workspace root directory: {workspace_root:?}");
549 std::env::set_current_dir(workspace_root)?;
550 foundry.forge.install_dependencies()?;
551 foundry.forge.build()?;
552
553 for path in &abs_pathes_to_check {
555 if !path.exists() {
556 return Err(Error::ContractNotFound(path.clone()).into());
557 }
558 }
559
560 Ok(())
561}
562
563fn resolve_path_relative_to_package(
565 package: &cargo_metadata::Package,
566 path: &str,
567) -> std::path::PathBuf {
568 if path.starts_with('/') {
569 std::path::PathBuf::from(path)
570 } else {
571 workspace_or_package_manifest_path(package).join(path)
572 }
573}
574
575fn find_package<'m>(
577 metadata: &'m cargo_metadata::Metadata,
578 pkg_name: Option<&String>,
579) -> Result<&'m cargo_metadata::Package, eyre::Error> {
580 match metadata.workspace_members.len() {
581 0 => Err(Error::NoPackageFound.into()),
582 1 => metadata
583 .packages
584 .iter()
585 .find(|p| p.id == metadata.workspace_members[0])
586 .ok_or(Error::NoPackageFound.into()),
587 _more_than_one if pkg_name.is_some() => metadata
588 .packages
589 .iter()
590 .find(|p| pkg_name.is_some_and(|v| &p.name == v))
591 .ok_or(Error::NoPackageFound.into()),
592 _otherwise => {
593 let bin_packages: Vec<&cargo_metadata::Package> = metadata
595 .packages
596 .iter()
597 .filter(|p| {
598 p.targets
599 .iter()
600 .any(|t| t.kind.contains(&"bin".to_string()))
601 })
602 .collect();
603
604 match bin_packages.len() {
605 0 => {
606 eprintln!("No binary packages found in the workspace.");
607 Err(Error::NoPackageFound.into())
608 }
609 1 => {
610 blueprint_core::info!(
612 "Automatically selecting the only binary package: {}",
613 bin_packages[0].name
614 );
615 Ok(bin_packages[0])
616 }
617 _ => {
618 eprintln!(
620 "Multiple binary packages found. Please specify the package to deploy:"
621 );
622 for package in &bin_packages {
623 eprintln!("Found: {}", package.name);
624 }
625 eprintln!();
626 Err(Error::ManyPackages.into())
627 }
628 }
629 }
630 }
631}