1use alloy_provider::network::TransactionBuilder;
2use alloy_provider::{Provider, WsConnect};
3pub use alloy_signer_local::PrivateKeySigner;
4use color_eyre::eyre::{self, Context, ContextCompat, Result};
5use gadget_blueprint_proc_macro_core::{BlueprintManager, ServiceBlueprint};
6use gadget_sdk::clients::tangle::runtime::TangleConfig;
7pub use k256;
8use std::fmt::Debug;
9use std::path::PathBuf;
10use tangle_subxt::subxt;
11use tangle_subxt::subxt::ext::sp_core;
12use tangle_subxt::subxt::tx::PairSigner;
13use tangle_subxt::tangle_testnet_runtime::api as TangleApi;
14use tangle_subxt::tangle_testnet_runtime::api::services::calls::types;
15
16pub type TanglePairSigner = PairSigner<TangleConfig, sp_core::sr25519::Pair>;
17
18#[derive(Clone)]
19pub struct Opts {
20 pub pkg_name: Option<String>,
22 pub http_rpc_url: String,
24 pub ws_rpc_url: String,
26 pub manifest_path: std::path::PathBuf,
28 pub signer: Option<TanglePairSigner>,
30 pub signer_evm: Option<PrivateKeySigner>,
32}
33
34impl Debug for Opts {
35 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36 f.debug_struct("Opts")
37 .field("pkg_name", &self.pkg_name)
38 .field("http_rpc_url", &self.http_rpc_url)
39 .field("ws_rpc_url", &self.ws_rpc_url)
40 .field("manifest_path", &self.manifest_path)
41 .finish()
42 }
43}
44
45#[derive(thiserror::Error, Debug)]
46pub enum Error {
47 #[error("Unsupported blueprint manager kind")]
48 UnsupportedBlueprintManager,
49 #[error("Contract not found at `{0}`, check the manager in your `Cargo.toml`!")]
50 ContractNotFound(PathBuf),
51 #[error("Failed to deserialize contract `{0}`: {1}")]
52 DeserializeContract(String, serde_json::Error),
53 #[error("The source at index {0} does not have a valid fetcher")]
54 MissingFetcher(usize),
55 #[error("No matching packages found in the workspace")]
56 NoPackageFound,
57 #[error("The workspace has multiple packages, please specify the package to deploy")]
58 ManyPackages,
59
60 #[error("{0}")]
61 Io(#[from] std::io::Error),
62}
63
64pub async fn generate_service_blueprint<P: Into<PathBuf>, T: AsRef<str>>(
65 manifest_metadata_path: P,
66 pkg_name: Option<&String>,
67 rpc_url: T,
68 signer_evm: Option<PrivateKeySigner>,
69) -> Result<types::create_blueprint::Blueprint> {
70 let manifest_path = manifest_metadata_path.into();
71 let metadata = cargo_metadata::MetadataCommand::new()
72 .manifest_path(manifest_path)
73 .no_deps()
74 .exec()
75 .context("Getting Metadata about the workspace")?;
76
77 let package = find_package(&metadata, pkg_name)?.clone();
78 let package_clone = &package.clone();
79 let mut blueprint = load_blueprint_metadata(&package)?;
80 build_contracts_if_needed(package_clone, &blueprint).context("Building contracts")?;
81 deploy_contracts_to_tangle(rpc_url.as_ref(), package_clone, &mut blueprint, signer_evm).await?;
82
83 bake_blueprint(blueprint)
84}
85
86pub async fn deploy_to_tangle(
87 Opts {
88 pkg_name,
89 http_rpc_url: _,
90 ws_rpc_url,
91 manifest_path,
92 signer,
93 signer_evm,
94 }: Opts,
95) -> Result<u64> {
96 let blueprint =
98 generate_service_blueprint(&manifest_path, pkg_name.as_ref(), &ws_rpc_url, signer_evm)
99 .await?;
100
101 let signer = if let Some(signer) = signer {
102 signer
103 } else {
104 crate::signer::load_signer_from_env()?
105 };
106
107 let my_account_id = signer.account_id();
108 let client = subxt::OnlineClient::from_url(ws_rpc_url.clone()).await?;
109 println!("Connected to Tangle Network at: {}", ws_rpc_url);
110 let create_blueprint_tx = TangleApi::tx().services().create_blueprint(blueprint);
111 println!("Created blueprint...");
112 let progress = client
113 .tx()
114 .sign_and_submit_then_watch_default(&create_blueprint_tx, &signer)
115 .await?;
116 let result = if cfg!(test) {
117 use gadget_sdk::tx::tangle::TxProgressExt;
118 progress.wait_for_in_block_success().await?
119 } else {
120 println!("Waiting for the transaction to be finalized...");
121 let result = progress.wait_for_finalized_success().await?;
122 println!("Transaction finalized...");
123 result
124 };
125 let event = result
126 .find::<TangleApi::services::events::BlueprintCreated>()
127 .flatten()
128 .find(|e| e.owner.0 == my_account_id.0)
129 .context("Finding the `BlueprintCreated` event")
130 .map_err(|e| {
131 eyre::eyre!(
132 "Trying to find the `BlueprintCreated` event with your account Id: {:?}",
133 e
134 )
135 })?;
136 println!(
137 "Blueprint #{} created successfully by {} with extrinsic hash: {}",
138 event.blueprint_id,
139 event.owner,
140 result.extrinsic_hash(),
141 );
142
143 Ok(event.blueprint_id)
144}
145
146pub fn load_blueprint_metadata(
147 package: &cargo_metadata::Package,
148) -> Result<ServiceBlueprint<'static>> {
149 let blueprint_json_path = package
150 .manifest_path
151 .parent()
152 .map(|p| p.join("blueprint.json"))
153 .unwrap();
154
155 if !blueprint_json_path.exists() {
156 eprintln!("Could not find blueprint.json; running `cargo build`...");
157 escargot::CargoBuild::new()
159 .manifest_path(&package.manifest_path)
160 .package(&package.name)
161 .run()
162 .context("Failed to build the package")?;
163 }
164 let blueprint_json =
166 std::fs::read_to_string(blueprint_json_path).context("Reading blueprint.json file")?;
167 let blueprint = serde_json::from_str(&blueprint_json)?;
168 Ok(blueprint)
169}
170
171async fn deploy_contracts_to_tangle(
172 rpc_url: &str,
173 package: &cargo_metadata::Package,
174 blueprint: &mut ServiceBlueprint<'_>,
175 signer_evm: Option<PrivateKeySigner>,
176) -> Result<()> {
177 enum ContractKind {
178 Manager,
179 }
180 let contract_paths = match blueprint.manager {
181 BlueprintManager::Evm(ref path) => vec![(ContractKind::Manager, path)],
182 _ => return Err(Error::UnsupportedBlueprintManager.into()),
183 };
184
185 let abs_contract_paths: Vec<_> = contract_paths
186 .into_iter()
187 .map(|(kind, path)| (kind, resolve_path_relative_to_package(package, path)))
188 .collect();
189
190 let mut contracts_raw = Vec::new();
191 for (kind, path) in abs_contract_paths {
192 if !path.exists() {
193 return Err(Error::ContractNotFound(path).into());
194 }
195
196 let content = std::fs::read_to_string(&path)?;
197 contracts_raw.push((
198 kind,
199 path.file_stem()
200 .unwrap_or_default()
201 .to_string_lossy()
202 .into_owned(),
203 content,
204 ));
205 }
206
207 let mut contracts = Vec::new();
208 for (kind, contract_name, json) in contracts_raw {
209 let contract = match serde_json::from_str::<alloy_json_abi::ContractObject>(&json) {
210 Ok(contract) => contract,
211 Err(e) => return Err(Error::DeserializeContract(contract_name, e).into()),
212 };
213
214 contracts.push((kind, contract_name, contract));
215 }
216
217 if contracts.is_empty() {
218 return Ok(());
219 }
220
221 let signer = if let Some(signer) = signer_evm {
222 signer
223 } else {
224 crate::signer::load_evm_signer_from_env()?
225 };
226
227 let wallet = alloy_provider::network::EthereumWallet::from(signer);
228 assert!(rpc_url.starts_with("ws:"));
229
230 let provider = alloy_provider::ProviderBuilder::new()
231 .with_recommended_fillers()
232 .wallet(wallet)
233 .on_ws(WsConnect::new(rpc_url))
234 .await?;
235
236 let chain_id = provider.get_chain_id().await?;
237 eprintln!("Chain ID: {chain_id}");
238
239 for (kind, name, contract) in contracts {
240 eprintln!("Deploying contract: {name} ...");
241 let Some(bytecode) = contract.bytecode.clone() else {
242 eprintln!("Contract {name} does not have deployed bytecode! Skipping ...");
243 continue;
244 };
245 let tx = alloy_rpc_types::TransactionRequest::default().with_deploy_code(bytecode);
246 let receipt = provider.send_transaction(tx).await?.get_receipt().await?;
248 if receipt.status() {
250 let contract_address =
251 alloy_network::ReceiptResponse::contract_address(&receipt).unwrap();
252 eprintln!("Contract {name} deployed at: {contract_address}");
253 match kind {
254 ContractKind::Manager => {
255 blueprint.manager = BlueprintManager::Evm(contract_address.to_string());
256 }
257 }
258 } else {
259 eprintln!("Contract {name} deployment failed!");
260 eprintln!("Receipt: {receipt:#?}");
261 }
262 }
263 Ok(())
264}
265
266fn build_contracts_if_needed(
268 package: &cargo_metadata::Package,
269 blueprint: &ServiceBlueprint,
270) -> Result<()> {
271 let pathes_to_check = match blueprint.manager {
272 BlueprintManager::Evm(ref path) => vec![path],
273 _ => return Err(Error::UnsupportedBlueprintManager.into()),
274 };
275
276 let abs_pathes_to_check: Vec<_> = pathes_to_check
277 .into_iter()
278 .map(|path| resolve_path_relative_to_package(package, path))
279 .collect();
280
281 let needs_build = abs_pathes_to_check.iter().any(|path| !path.exists());
282
283 if needs_build {
284 let foundry = crate::foundry::FoundryToolchain::new();
285 foundry.check_installed_or_exit();
286 foundry.forge.build()?;
287 }
288
289 Ok(())
290}
291
292fn bake_blueprint(
294 blueprint: ServiceBlueprint,
295) -> Result<TangleApi::runtime_types::tangle_primitives::services::ServiceBlueprint> {
296 let mut blueprint_json = serde_json::to_value(&blueprint)?;
297 convert_to_bytes_or_null(&mut blueprint_json["metadata"]["name"]);
298 convert_to_bytes_or_null(&mut blueprint_json["metadata"]["description"]);
299 convert_to_bytes_or_null(&mut blueprint_json["metadata"]["author"]);
300 convert_to_bytes_or_null(&mut blueprint_json["metadata"]["license"]);
301 convert_to_bytes_or_null(&mut blueprint_json["metadata"]["website"]);
302 convert_to_bytes_or_null(&mut blueprint_json["metadata"]["code_repository"]);
303 convert_to_bytes_or_null(&mut blueprint_json["metadata"]["category"]);
304
305 blueprint_json["registration_hook"] = serde_json::json!("None");
308 blueprint_json["request_hook"] = serde_json::json!("None");
309 for job in blueprint_json["jobs"].as_array_mut().unwrap() {
310 convert_to_bytes_or_null(&mut job["metadata"]["name"]);
311 convert_to_bytes_or_null(&mut job["metadata"]["description"]);
312 job["verifier"] = serde_json::json!("None");
314 }
315
316 let (_, gadget) = blueprint_json["gadget"]
319 .as_object_mut()
320 .expect("Bad gadget value")
321 .iter_mut()
322 .next()
323 .expect("Should be at least one gadget");
324 let sources = gadget["sources"].as_array_mut().expect("Should be a list");
325
326 for (idx, source) in sources.iter_mut().enumerate() {
329 let Some(fetchers) = source["fetcher"].as_object_mut() else {
330 return Err(Error::MissingFetcher(idx).into());
331 };
332
333 let fetcher_fields = fetchers
334 .iter_mut()
335 .next()
336 .expect("Should be at least one fetcher")
337 .1;
338 for (_key, value) in fetcher_fields
339 .as_object_mut()
340 .expect("Fetcher should be a map")
341 {
342 if value.is_array() {
343 let xs = value.as_array_mut().expect("Value should be an array");
344 for x in xs {
345 if x.is_object() {
346 convert_to_bytes_or_null(&mut x["name"]);
347 }
348 }
349 } else {
350 convert_to_bytes_or_null(value);
351 }
352 }
353 }
354
355 let blueprint = serde_json::from_value(blueprint_json)?;
356 Ok(blueprint)
357}
358
359fn convert_to_bytes_or_null(v: &mut serde_json::Value) {
363 if let serde_json::Value::String(s) = v {
364 *v = serde_json::Value::Array(s.bytes().map(serde_json::Value::from).collect());
365 return;
366 }
367
368 if let serde_json::Value::Array(vals) = v {
369 for val in vals {
370 convert_to_bytes_or_null(val);
371 }
372 }
373}
374
375fn resolve_path_relative_to_package(
377 package: &cargo_metadata::Package,
378 path: &str,
379) -> std::path::PathBuf {
380 if path.starts_with('/') {
381 std::path::PathBuf::from(path)
382 } else {
383 package.manifest_path.parent().unwrap().join(path).into()
384 }
385}
386
387fn find_package<'m>(
389 metadata: &'m cargo_metadata::Metadata,
390 pkg_name: Option<&String>,
391) -> Result<&'m cargo_metadata::Package, eyre::Error> {
392 match metadata.workspace_members.len() {
393 0 => Err(Error::NoPackageFound.into()),
394 1 => metadata
395 .packages
396 .iter()
397 .find(|p| p.id == metadata.workspace_members[0])
398 .ok_or(Error::NoPackageFound.into()),
399 _more_than_one if pkg_name.is_some() => metadata
400 .packages
401 .iter()
402 .find(|p| pkg_name.is_some_and(|v| &p.name == v))
403 .ok_or(Error::NoPackageFound.into()),
404 _otherwise => {
405 eprintln!("Please specify the package to deploy:");
406 for package in metadata.packages.iter() {
407 eprintln!("Found: {}", package.name);
408 }
409 eprintln!();
410 Err(Error::ManyPackages.into())
411 }
412 }
413}