diff --git a/Cargo.lock b/Cargo.lock index 0ae3822..a81b9ef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2359,6 +2359,7 @@ dependencies = [ "serde", "sha2 0.10.8", "tangle-subxt", + "thiserror 2.0.7", "tokio", "toml", "tracing", @@ -4853,6 +4854,7 @@ dependencies = [ "gadget-client-evm", "gadget-client-networking", "gadget-client-tangle", + "thiserror 2.0.7", ] [[package]] diff --git a/crates/blueprint/manager/Cargo.toml b/crates/blueprint/manager/Cargo.toml index 20a7a70..8fe8e0e 100644 --- a/crates/blueprint/manager/Cargo.toml +++ b/crates/blueprint/manager/Cargo.toml @@ -32,6 +32,7 @@ reqwest = { workspace = true } sha2 = { workspace = true } futures = { workspace = true } itertools = { workspace = true } +thiserror.workspace = true tracing = { workspace = true, features = ["log"] } tracing-subscriber = { workspace = true, features = ["env-filter", "ansi", "tracing-log"] } libp2p = { workspace = true } diff --git a/crates/blueprint/manager/src/error.rs b/crates/blueprint/manager/src/error.rs index 74d0130..01ac696 100644 --- a/crates/blueprint/manager/src/error.rs +++ b/crates/blueprint/manager/src/error.rs @@ -1,30 +1,39 @@ -use std::fmt::{Display, Formatter}; +pub type Result = std::result::Result; -#[derive(Debug, Clone)] -pub struct Error { - pub message: String, -} +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("No fetchers found for blueprint")] + NoFetchers, + #[error("Multiple fetchers found for blueprint")] + MultipleFetchers, + #[error("No testing fetcher found for blueprint, despite operating in test mode")] + NoTestFetcher, + #[error("Blueprint does not contain a supported fetcher")] + UnsupportedGadget, -impl Error { - pub fn msg>(msg: T) -> Self { - Self { - message: msg.into(), - } - } -} + #[error("Unable to find matching binary")] + NoMatchingBinary, + #[error("Binary hash {expected} mismatched expected hash of {actual}")] + HashMismatch { expected: String, actual: String }, + #[error("Failed to build binary: {0:?}")] + BuildBinary(std::process::Output), + #[error("Failed to fetch git root: {0:?}")] + FetchGitRoot(std::process::Output), -impl Display for Error { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - std::fmt::Debug::fmt(self, f) - } -} + #[error("Failed to get initial block hash")] + InitialBlock, + #[error("Finality Notification stream died")] + ClientDied, + #[error("{0}")] + Other(String), -impl std::error::Error for Error {} + #[error(transparent)] + Io(#[from] std::io::Error), + #[error(transparent)] + Utf8(#[from] std::string::FromUtf8Error), -impl From for Error { - fn from(error: std::io::Error) -> Self { - Error { - message: error.to_string(), - } - } + #[error(transparent)] + Request(#[from] reqwest::Error), + #[error(transparent)] + TangleClient(#[from] gadget_clients::tangle::error::Error), } diff --git a/crates/blueprint/manager/src/executor/event_handler.rs b/crates/blueprint/manager/src/executor/event_handler.rs index 5931c9d..5e01e22 100644 --- a/crates/blueprint/manager/src/executor/event_handler.rs +++ b/crates/blueprint/manager/src/executor/event_handler.rs @@ -1,10 +1,12 @@ use crate::config::BlueprintManagerConfig; +use crate::error::{Error, Result}; use crate::gadget::native::FilteredBlueprint; use crate::gadget::ActiveGadgets; -use crate::sdk::utils::bounded_string_to_string; +use crate::sdk::utils::{ + bounded_string_to_string, generate_running_process_status_handle, make_executable, +}; use crate::sources::github::GithubBinaryFetcher; -use crate::sources::BinarySourceFetcher; -use color_eyre::eyre::OptionExt; +use crate::sources::{process_arguments_and_env, BinarySourceFetcher}; use gadget_clients::tangle::client::{TangleConfig, TangleEvent}; use gadget_clients::tangle::services::{RpcServicesWithBlueprint, TangleServicesClient}; use gadget_config::{GadgetConfiguration, Protocol}; @@ -24,6 +26,85 @@ pub struct VerifiedBlueprint<'a> { pub(crate) blueprint: FilteredBlueprint, } +impl VerifiedBlueprint<'_> { + pub async fn start_services_if_needed( + &self, + gadget_config: &GadgetConfiguration, + blueprint_manager_opts: &BlueprintManagerConfig, + active_gadgets: &mut ActiveGadgets, + ) -> Result<()> { + let blueprint_source = &self.fetcher; + let blueprint = &self.blueprint; + + let blueprint_id = blueprint_source.blueprint_id(); + if active_gadgets.contains_key(&blueprint_id) { + return Ok(()); + } + + let mut binary_download_path = blueprint_source.get_binary().await?; + + // Ensure the binary is executable + if cfg!(target_family = "windows") { + if binary_download_path.extension().is_none() { + binary_download_path.set_extension("exe"); + } + } else if let Err(err) = make_executable(&binary_download_path) { + let msg = format!("Failed to make the binary executable: {err}"); + warn!("{}", msg); + return Err(Error::Other(msg)); + } + + let service_str = blueprint_source.name(); + for service_id in &blueprint.services { + let sub_service_str = format!("{service_str}-{service_id}"); + let (arguments, env_vars) = process_arguments_and_env( + gadget_config, + blueprint_manager_opts, + blueprint_id, + *service_id, + blueprint, + &sub_service_str, + ); + + info!("Starting protocol: {sub_service_str} with args: {arguments:?}"); + + // Now that the file is loaded, spawn the process + let process_handle = tokio::process::Command::new(&binary_download_path) + .kill_on_drop(true) + .stdin(std::process::Stdio::null()) + .current_dir(&std::env::current_dir()?) + .envs(env_vars) + .args(arguments) + .spawn()?; + + if blueprint.registration_mode { + // We must wait for the process to exit successfully + let status = process_handle.wait_with_output().await?; + if status.status.success() { + info!("***Protocol (registration mode) {sub_service_str} executed successfully***"); + } else { + error!( + "Protocol (registration mode) {sub_service_str} failed to execute: {status:?}" + ); + } + continue; + } + + // A normal running gadget binary. Store the process handle and let the event loop handle the rest + + let (status_handle, abort) = + generate_running_process_status_handle(process_handle, &sub_service_str); + + active_gadgets + .entry(blueprint_id) + .or_default() + .insert(*service_id, (status_handle, Some(abort))); + } + + Ok(()) + } +} + impl Debug for VerifiedBlueprint<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { format!( @@ -34,28 +115,6 @@ impl Debug for VerifiedBlueprint<'_> { } } -pub async fn handle_services( - blueprints: &[VerifiedBlueprint<'_>], - gadget_config: &GadgetConfiguration, - blueprint_manager_opts: &BlueprintManagerConfig, - active_gadgets: &mut ActiveGadgets, -) -> color_eyre::Result<()> { - for blueprint in blueprints { - if let Err(err) = crate::sources::handle( - blueprint, - gadget_config, - blueprint_manager_opts, - active_gadgets, - ) - .await - { - error!("{err}"); - } - } - - Ok(()) -} - #[derive(Default, Debug)] pub struct EventPollResult { pub needs_update: bool, @@ -63,7 +122,7 @@ pub struct EventPollResult { pub blueprint_registrations: Vec, } -pub(crate) async fn check_blueprint_events( +pub(crate) fn check_blueprint_events( event: &TangleEvent, active_gadgets: &mut ActiveGadgets, account_id: &AccountId32, @@ -166,11 +225,11 @@ pub(crate) async fn handle_tangle_event( event: &TangleEvent, blueprints: &[RpcServicesWithBlueprint], gadget_config: &GadgetConfiguration, - gadget_manager_opts: &BlueprintManagerConfig, + manager_opts: &BlueprintManagerConfig, active_gadgets: &mut ActiveGadgets, poll_result: EventPollResult, client: &TangleServicesClient, -) -> color_eyre::Result<()> { +) -> Result<()> { info!("Received notification {}", event.number); const DEFAULT_PROTOCOL: Protocol = Protocol::Tangle; warn!("Using Tangle protocol as default over Eigen. This is a temporary development workaround. You can alter this behavior here"); @@ -185,7 +244,11 @@ pub(crate) async fn handle_tangle_event( let blueprint = client .get_blueprint_by_id(event.hash, *blueprint_id) .await? - .ok_or_eyre("Unable to retrieve blueprint for registration mode")?; + .ok_or_else(|| { + Error::Other(String::from( + "Unable to retrieve blueprint for registration mode", + )) + })?; let general_blueprint = FilteredBlueprint { blueprint_id: *blueprint_id, @@ -215,86 +278,14 @@ pub(crate) async fn handle_tangle_event( }) .chain(registration_blueprints) { - let mut test_fetcher_idx = None; - let mut fetcher_candidates: Vec> = vec![]; - - if let Gadget::Native(gadget) = &blueprint.gadget { - for (source_idx, gadget_source) in gadget.sources.0.iter().enumerate() { - match &gadget_source.fetcher { - GadgetSourceFetcher::Github(gh) => { - let fetcher = GithubBinaryFetcher { - fetcher: gh.clone(), - blueprint_id: blueprint.blueprint_id, - gadget_name: blueprint.name.clone(), - }; - - fetcher_candidates.push(Box::new(fetcher)); - } - - GadgetSourceFetcher::Testing(test) => { - // TODO: demote to TRACE once proven to work - if !gadget_manager_opts.test_mode { - warn!("Ignoring testing fetcher as we are not in test mode"); - continue; - } - - let fetcher = crate::sources::testing::TestSourceFetcher { - fetcher: test.clone(), - blueprint_id: blueprint.blueprint_id, - gadget_name: blueprint.name.clone(), - }; - - test_fetcher_idx = Some(source_idx); - fetcher_candidates.push(Box::new(fetcher)); - } - - _ => { - warn!("Blueprint does not contain a supported fetcher"); - continue; - } - } - } - - // A bunch of sanity checks to enforce structure - - // Ensure that we have at least one fetcher - if fetcher_candidates.is_empty() { - warn!("No fetchers found for blueprint: {}", blueprint.name,); - continue; - } - - // Ensure that we have a test fetcher if we are in test mode - if gadget_manager_opts.test_mode && test_fetcher_idx.is_none() { - return Err(color_eyre::Report::msg(format!( - "No testing fetcher found for blueprint `{}` despite operating in TEST MODE", - blueprint.name, - ))); - } + let mut fetcher_candidates = get_fetcher_candidates(&blueprint, manager_opts)?; - // Ensure that we have only one fetcher if we are in test mode - if gadget_manager_opts.test_mode { - fetcher_candidates = - vec![fetcher_candidates.remove(test_fetcher_idx.expect("Should exist"))]; - } - - // Ensure there is only a single candidate fetcher - if fetcher_candidates.len() != 1 { - warn!( - "Multiple fetchers found for blueprint: {}. Invalidating blueprint", - blueprint.name, - ); - continue; - } - - let verified_blueprint = VerifiedBlueprint { - fetcher: fetcher_candidates.pop().expect("Should exist"), - blueprint, - }; + let verified_blueprint = VerifiedBlueprint { + fetcher: fetcher_candidates.pop().expect("Should exist"), + blueprint, + }; - verified_blueprints.push(verified_blueprint); - } else { - warn!("Blueprint does not contain a native gadget and thus currently unsupported"); - } + verified_blueprints.push(verified_blueprint); } trace!( @@ -306,20 +297,18 @@ pub(crate) async fn handle_tangle_event( ); // Step 3: Check to see if we need to start any new services - handle_services( - &verified_blueprints, - gadget_config, - gadget_manager_opts, - active_gadgets, - ) - .await?; + for blueprint in &verified_blueprints { + blueprint + .start_services_if_needed(gadget_config, manager_opts, active_gadgets) + .await?; + } // Check to see if local is running services that are not on-chain let mut to_remove: Vec<(u64, u64)> = vec![]; // Loop through every (blueprint_id, service_id) running. See if the service is still on-chain. If not, kill it and add it to to_remove for (blueprint_id, process_handles) in &mut *active_gadgets { - for service_id in process_handles.keys() { + for (service_id, process_handle) in process_handles { info!( "Checking service for on-chain termination: bid={blueprint_id}//sid={service_id}" ); @@ -335,12 +324,8 @@ pub(crate) async fn handle_tangle_event( to_remove.push((*blueprint_id, *service_id)); } } - } - } - // Check to see if any process handles have died - for (blueprint_id, process_handles) in &mut *active_gadgets { - for (service_id, process_handle) in process_handles { + // Check to see if any process handles have died if !to_remove.contains(&(*blueprint_id, *service_id)) && !process_handle.0.load(Ordering::Relaxed) { @@ -377,3 +362,87 @@ pub(crate) async fn handle_tangle_event( Ok(()) } + +fn get_fetcher_candidates( + blueprint: &FilteredBlueprint, + manager_opts: &BlueprintManagerConfig, +) -> Result>> { + let mut test_fetcher_idx = None; + let mut fetcher_candidates: Vec> = vec![]; + + let sources; + match &blueprint.gadget { + Gadget::Native(gadget) => { + sources = &gadget.sources.0; + } + Gadget::Wasm(_) => { + warn!("WASM gadgets are not supported yet"); + return Err(Error::UnsupportedGadget); + } + Gadget::Container(_) => { + warn!("Container gadgets are not supported yet"); + return Err(Error::UnsupportedGadget); + } + } + + for (source_idx, gadget_source) in sources.iter().enumerate() { + match &gadget_source.fetcher { + GadgetSourceFetcher::Github(gh) => { + let fetcher = GithubBinaryFetcher { + fetcher: gh.clone(), + blueprint_id: blueprint.blueprint_id, + gadget_name: blueprint.name.clone(), + }; + + fetcher_candidates.push(Box::new(fetcher)); + } + + GadgetSourceFetcher::Testing(test) => { + // TODO: demote to TRACE once proven to work + if !manager_opts.test_mode { + warn!("Ignoring testing fetcher as we are not in test mode"); + continue; + } + + let fetcher = crate::sources::testing::TestSourceFetcher { + fetcher: test.clone(), + blueprint_id: blueprint.blueprint_id, + gadget_name: blueprint.name.clone(), + }; + + test_fetcher_idx = Some(source_idx); + fetcher_candidates.push(Box::new(fetcher)); + } + + _ => { + warn!("Blueprint does not contain a supported fetcher"); + continue; + } + } + } + + // A bunch of sanity checks to enforce structure + + // Ensure that we have at least one fetcher + if fetcher_candidates.is_empty() { + return Err(Error::NoFetchers); + } + + // Ensure there is only a single candidate fetcher + if fetcher_candidates.len() != 1 { + return Err(Error::MultipleFetchers); + } + + // Ensure that we have a test fetcher if we are in test mode + if manager_opts.test_mode && test_fetcher_idx.is_none() { + return Err(Error::NoTestFetcher); + } + + // Ensure that we have only one fetcher if we are in test mode + if manager_opts.test_mode { + fetcher_candidates = + vec![fetcher_candidates.remove(test_fetcher_idx.expect("Should exist"))]; + } + + Ok(fetcher_candidates) +} diff --git a/crates/blueprint/manager/src/executor/mod.rs b/crates/blueprint/manager/src/executor/mod.rs index 526afcb..b51434b 100644 --- a/crates/blueprint/manager/src/executor/mod.rs +++ b/crates/blueprint/manager/src/executor/mod.rs @@ -1,8 +1,8 @@ use crate::config::BlueprintManagerConfig; +use crate::error::Error; +use crate::error::Result; use crate::gadget::ActiveGadgets; use crate::sdk::entry::SendFuture; -use crate::sdk::utils; -use crate::sdk::utils::msg_to_error; use color_eyre::eyre::OptionExt; use color_eyre::Report; use gadget_clients::tangle::client::{TangleClient, TangleConfig}; @@ -17,29 +17,13 @@ use std::collections::HashMap; use std::future::Future; use std::pin::Pin; use std::task::{Context, Poll}; -use tangle_subxt::subxt::blocks::BlockRef; -use tangle_subxt::subxt::ext::sp_core::{ecdsa, sr25519, H256}; +use tangle_subxt::subxt::ext::sp_core::{ecdsa, sr25519}; use tangle_subxt::subxt::tx::Signer; use tangle_subxt::subxt::utils::AccountId32; -use tangle_subxt::subxt::Config; use tokio::task::JoinHandle; pub(crate) mod event_handler; -pub async fn get_blueprints( - runtime: &TangleServicesClient, - block_hash: [u8; 32], - account_id: AccountId32, -) -> color_eyre::Result> -where - BlockRef<::Hash>: From>, -{ - runtime - .query_operator_blueprints(block_hash, account_id) - .await - .map_err(|err| msg_to_error(err.to_string())) -} - pub struct BlueprintManagerHandle { shutdown_call: Option>, start_tx: Option>, @@ -215,8 +199,7 @@ pub async fn run_blueprint_manager>( if result.needs_update { operator_subscribed_blueprints = services_client .query_operator_blueprints(event.hash, sub_account_id.clone()) - .await - .map_err(|err| msg_to_error(err.to_string()))?; + .await?; } event_handler::handle_tangle_event( @@ -226,12 +209,12 @@ pub async fn run_blueprint_manager>( &blueprint_manager_config, &mut active_gadgets, result, - &services_client, + services_client, ) .await?; } - Err::<(), _>(utils::msg_to_error("Finality Notification stream died")) + Err::<(), _>(Error::ClientDied) }; let (tx_stop, rx_stop) = tokio::sync::oneshot::channel::<()>(); @@ -293,20 +276,16 @@ async fn handle_init( active_gadgets: &mut ActiveGadgets, gadget_config: &GadgetConfiguration, blueprint_manager_config: &BlueprintManagerConfig, -) -> color_eyre::Result> { +) -> Result> { info!("Beginning initialization of Blueprint Manager"); - let (operator_subscribed_blueprints, init_event) = - if let Some(event) = tangle_runtime.next_event().await { - ( - get_blueprints(services_client, event.hash, sub_account_id.clone()) - .await - .map_err(|err| Report::msg(format!("Failed to obtain blueprints: {err}")))?, - event, - ) - } else { - return Err(Report::msg("Failed to get initial block hash")); - }; + let Some(init_event) = tangle_runtime.next_event().await else { + return Err(Error::InitialBlock); + }; + + let operator_subscribed_blueprints = services_client + .query_operator_blueprints(init_event.hash, sub_account_id.clone()) + .await?; info!( "Received {} initial blueprints this operator is registered to", diff --git a/crates/blueprint/manager/src/lib.rs b/crates/blueprint/manager/src/lib.rs index 7168062..70e8063 100644 --- a/crates/blueprint/manager/src/lib.rs +++ b/crates/blueprint/manager/src/lib.rs @@ -2,7 +2,6 @@ pub mod config; pub mod error; pub mod executor; pub mod gadget; -pub mod protocols; pub mod sdk; pub mod sources; pub use executor::run_blueprint_manager; diff --git a/crates/blueprint/manager/src/main.rs b/crates/blueprint/manager/src/main.rs index c405488..0e06d14 100644 --- a/crates/blueprint/manager/src/main.rs +++ b/crates/blueprint/manager/src/main.rs @@ -1,6 +1,5 @@ use blueprint_manager::config::BlueprintManagerConfig; use blueprint_manager::sdk; -use blueprint_manager::sdk::utils::msg_to_error; use clap::Parser; use sdk::entry; @@ -19,7 +18,8 @@ async fn main() -> color_eyre::Result<()> { )?; // TODO: blueprint-manager CLI mode - return Err(msg_to_error("TODO: blueprint-manager CLI mode".to_string())); + eprintln!("TODO: blueprint-manager CLI mode"); + return Ok(()); // let gadget_config_settings = std::fs::read_to_string(gadget_config)?; // let gadget_config: GadgetConfig = diff --git a/crates/blueprint/manager/src/protocols/mod.rs b/crates/blueprint/manager/src/protocols/mod.rs deleted file mode 100644 index e755804..0000000 --- a/crates/blueprint/manager/src/protocols/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod resolver; diff --git a/crates/blueprint/manager/src/protocols/resolver.rs b/crates/blueprint/manager/src/protocols/resolver.rs deleted file mode 100644 index 95c5158..0000000 --- a/crates/blueprint/manager/src/protocols/resolver.rs +++ /dev/null @@ -1,14 +0,0 @@ -use tangle_subxt::tangle_testnet_runtime::api::runtime_types::tangle_primitives::services::{ - GadgetBinary, GithubFetcher, -}; - -#[derive(Debug)] -pub struct NativeGithubMetadata { - pub git: String, - pub tag: String, - pub owner: String, - pub repo: String, - pub gadget_binaries: Vec, - pub blueprint_id: u64, - pub fetcher: GithubFetcher, -} diff --git a/crates/blueprint/manager/src/sdk/utils.rs b/crates/blueprint/manager/src/sdk/utils.rs index dd4ca8d..278b3d0 100644 --- a/crates/blueprint/manager/src/sdk/utils.rs +++ b/crates/blueprint/manager/src/sdk/utils.rs @@ -1,8 +1,7 @@ -use crate::protocols::resolver::NativeGithubMetadata; +use crate::error::Result; use gadget_logging::{info, warn}; use sha2::Digest; use std::path::Path; -use std::string::FromUtf8Error; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use tangle_subxt::tangle_testnet_runtime::api::runtime_types::tangle_primitives::services::field::BoundedString; @@ -10,9 +9,10 @@ use tangle_subxt::tangle_testnet_runtime::api::runtime_types::tangle_primitives: GadgetBinary, GithubFetcher, }; -pub fn bounded_string_to_string(string: BoundedString) -> Result { +pub fn bounded_string_to_string(string: BoundedString) -> Result { let bytes: &Vec = &string.0 .0; - String::from_utf8(bytes.clone()) + let ret = String::from_utf8(bytes.clone())?; + Ok(ret) } pub fn hash_bytes_to_hex>(input: T) -> String { @@ -57,32 +57,18 @@ pub fn get_download_url(binary: &GadgetBinary, fetcher: &GithubFetcher) -> Strin format!("https://github.com/{owner}/{repo}/releases/download/v{tag}/{binary_name}-{os_name}-{arch_name}{ext}") } -pub fn msg_to_error>(msg: T) -> color_eyre::Report { - color_eyre::Report::msg(msg.into()) -} - -pub async fn chmod_x_file>(path: P) -> color_eyre::Result<()> { - let success = tokio::process::Command::new("chmod") - .arg("+x") - .arg(format!("{}", path.as_ref().display())) - .spawn()? - .wait_with_output() - .await? - .status - .success(); +pub fn make_executable>(path: P) -> Result<()> { + #[cfg(target_family = "unix")] + { + use std::os::unix::fs::PermissionsExt; - if success { - Ok(()) - } else { - Err(color_eyre::eyre::eyre!( - "Failed to chmod +x {}", - path.as_ref().display() - )) + let f = std::fs::File::open(path)?; + let mut perms = f.metadata()?.permissions(); + perms.set_mode(perms.mode() | 0o111); + f.set_permissions(perms)?; } -} -pub fn is_windows() -> bool { - std::env::consts::OS == "windows" + Ok(()) } pub fn generate_running_process_status_handle( diff --git a/crates/blueprint/manager/src/sources/github.rs b/crates/blueprint/manager/src/sources/github.rs index dfba067..a317a60 100644 --- a/crates/blueprint/manager/src/sources/github.rs +++ b/crates/blueprint/manager/src/sources/github.rs @@ -1,12 +1,10 @@ +use crate::error::{Error, Result}; use crate::gadget::native::get_gadget_binary; use crate::sdk; -use crate::sdk::utils::{ - get_download_url, hash_bytes_to_hex, is_windows, msg_to_error, valid_file_exists, -}; +use crate::sdk::utils::{get_download_url, hash_bytes_to_hex, valid_file_exists}; use crate::sources::BinarySourceFetcher; use async_trait::async_trait; -use color_eyre::eyre::OptionExt; -use gadget_logging::{error, info}; +use gadget_logging::info; use std::path::PathBuf; use tangle_subxt::tangle_testnet_runtime::api::runtime_types::tangle_primitives::services::GithubFetcher; use tokio::io::AsyncWriteExt; @@ -19,54 +17,43 @@ pub struct GithubBinaryFetcher { #[async_trait] impl BinarySourceFetcher for GithubBinaryFetcher { - async fn get_binary(&self) -> color_eyre::Result { - let relevant_binary = get_gadget_binary(&self.fetcher.binaries.0) - .ok_or_eyre("Unable to find matching binary")?; + async fn get_binary(&self) -> Result { + let relevant_binary = + get_gadget_binary(&self.fetcher.binaries.0).ok_or(Error::NoMatchingBinary)?; let expected_hash = sdk::utils::slice_32_to_sha_hex_string(relevant_binary.sha256); let current_dir = std::env::current_dir()?; let mut binary_download_path = format!("{}/protocol-{:?}", current_dir.display(), self.fetcher.tag); - if is_windows() { + if cfg!(target_family = "windows") { binary_download_path += ".exe" } info!("Downloading to {binary_download_path}"); // Check if the binary exists, if not download it - let retrieved_hash = if !valid_file_exists(&binary_download_path, &expected_hash).await { - let url = get_download_url(relevant_binary, &self.fetcher); + if !valid_file_exists(&binary_download_path, &expected_hash).await { + return Ok(PathBuf::from(binary_download_path)); + } + + let url = get_download_url(relevant_binary, &self.fetcher); - let download = reqwest::get(&url) - .await - .map_err(|err| msg_to_error(err.to_string()))? - .bytes() - .await - .map_err(|err| msg_to_error(err.to_string()))?; - let retrieved_hash = hash_bytes_to_hex(&download); + let download = reqwest::get(&url).await?.bytes().await?; + let retrieved_hash = hash_bytes_to_hex(&download); - // Write the binary to disk - let mut file = tokio::fs::File::create(&binary_download_path).await?; - file.write_all(&download).await?; - file.flush().await?; - Some(retrieved_hash) - } else { - None - }; + // Write the binary to disk + let mut file = tokio::fs::File::create(&binary_download_path).await?; + file.write_all(&download).await?; + file.flush().await?; - if let Some(retrieved_hash) = retrieved_hash { - if retrieved_hash.trim() != expected_hash.trim() { - error!( - "Binary hash {} mismatched expected hash of {} for protocol: {}", - retrieved_hash, expected_hash, self.gadget_name - ); - return Ok(PathBuf::from(binary_download_path)); - } + if retrieved_hash.trim() != expected_hash.trim() { + return Err(Error::HashMismatch { + expected: expected_hash, + actual: retrieved_hash, + }); } - Err(color_eyre::Report::msg( - "The hash of the downloaded binary did not match", - )) + Ok(PathBuf::from(binary_download_path)) } fn blueprint_id(&self) -> u64 { diff --git a/crates/blueprint/manager/src/sources/mod.rs b/crates/blueprint/manager/src/sources/mod.rs index 07c8d02..c223c7c 100644 --- a/crates/blueprint/manager/src/sources/mod.rs +++ b/crates/blueprint/manager/src/sources/mod.rs @@ -1,10 +1,8 @@ use crate::config::BlueprintManagerConfig; -use crate::executor::event_handler::VerifiedBlueprint; -use crate::gadget::ActiveGadgets; -use crate::sdk::utils::{chmod_x_file, generate_running_process_status_handle, is_windows}; +use crate::error::Result; +use crate::gadget::native::FilteredBlueprint; use async_trait::async_trait; -use gadget_config::{GadgetConfiguration, Protocol}; -use gadget_logging::{error, info, warn}; +use gadget_config::GadgetConfiguration; use std::path::PathBuf; pub mod github; @@ -13,134 +11,27 @@ pub mod testing; #[async_trait] #[auto_impl::auto_impl(Box)] pub trait BinarySourceFetcher: Send + Sync { - async fn get_binary(&self) -> color_eyre::Result; + async fn get_binary(&self) -> Result; fn blueprint_id(&self) -> u64; fn name(&self) -> String; } -pub async fn handle( - blueprint: &VerifiedBlueprint<'_>, +pub fn process_arguments_and_env( gadget_config: &GadgetConfiguration, - blueprint_manager_opts: &BlueprintManagerConfig, - active_gadgets: &mut ActiveGadgets, -) -> color_eyre::Result<()> { - let blueprint_source = &blueprint.fetcher; - let blueprint = &blueprint.blueprint; - - let blueprint_id = blueprint_source.blueprint_id(); - let service_str = blueprint_source.name(); - - if active_gadgets.contains_key(&blueprint_id) { - return Ok(()); - } - - let mut binary_download_path = blueprint_source.get_binary().await?; - - // Ensure the binary is executable - if is_windows() { - if binary_download_path.extension().is_none() { - binary_download_path.set_extension("exe"); - } - } else if let Err(err) = chmod_x_file(&binary_download_path).await { - warn!("Failed to chmod +x the binary: {err}"); - } - - for service_id in &blueprint.services { - let sub_service_str = format!("{service_str}-{service_id}"); - let arguments = generate_process_arguments( - gadget_config, - blueprint_manager_opts, - blueprint_id, - *service_id, - blueprint.protocol, - )?; - - // Add required env vars for all child processes/gadgets - let mut env_vars = vec![ - ( - "HTTP_RPC_URL".to_string(), - gadget_config.http_rpc_endpoint.to_string(), - ), - ( - "WS_RPC_URL".to_string(), - gadget_config.ws_rpc_endpoint.to_string(), - ), - ( - "KEYSTORE_URI".to_string(), - blueprint_manager_opts.keystore_uri.clone(), - ), - ("BLUEPRINT_ID".to_string(), format!("{}", blueprint_id)), - ("SERVICE_ID".to_string(), format!("{}", service_id)), - ]; - - let base_data_dir = &blueprint_manager_opts.data_dir; - let data_dir = base_data_dir.join(format!("blueprint-{blueprint_id}-{sub_service_str}")); - env_vars.push(( - "DATA_DIR".to_string(), - data_dir.to_string_lossy().into_owned(), - )); - - // Ensure our child process inherits the current processes' environment vars - env_vars.extend(std::env::vars()); - - if blueprint.registration_mode { - env_vars.push(("REGISTRATION_MODE_ON".to_string(), "true".to_string())); - } - - info!("Starting protocol: {sub_service_str} with args: {arguments:?}"); - - // Now that the file is loaded, spawn the process - let process_handle = tokio::process::Command::new(&binary_download_path) - .kill_on_drop(true) - .stdout(std::process::Stdio::inherit()) // Inherit the stdout of this process - .stderr(std::process::Stdio::inherit()) // Inherit the stderr of this process - .stdin(std::process::Stdio::null()) - .current_dir(&std::env::current_dir()?) - .envs(env_vars) - .args(arguments) - .spawn()?; - - if blueprint.registration_mode { - // We must wait for the process to exit successfully - let status = process_handle.wait_with_output().await?; - if !status.status.success() { - error!( - "Protocol (registration mode) {sub_service_str} failed to execute: {status:?}" - ); - } else { - info!("***Protocol (registration mode) {sub_service_str} executed successfully***"); - } - } else { - // A normal running gadget binary. Store the process handle and let the event loop handle the rest - - let (status_handle, abort) = - generate_running_process_status_handle(process_handle, &sub_service_str); - - active_gadgets - .entry(blueprint_id) - .or_default() - .insert(*service_id, (status_handle, Some(abort))); - } - } - - Ok(()) -} - -pub fn generate_process_arguments( - gadget_config: &GadgetConfiguration, - opt: &BlueprintManagerConfig, + manager_opts: &BlueprintManagerConfig, blueprint_id: u64, service_id: u64, - protocol: Protocol, -) -> color_eyre::Result> { + blueprint: &FilteredBlueprint, + sub_service_str: &str, +) -> (Vec, Vec<(String, String)>) { let mut arguments = vec![]; arguments.push("run".to_string()); - if opt.test_mode { + if manager_opts.test_mode { arguments.push("--test-mode".to_string()); } - if opt.pretty { + if manager_opts.pretty { arguments.push("--pretty".to_string()); } @@ -152,10 +43,11 @@ pub fn generate_process_arguments( format!("--http-rpc-url={}", gadget_config.http_rpc_endpoint), format!("--ws-rpc-url={}", gadget_config.ws_rpc_endpoint), format!("--keystore-uri={}", gadget_config.keystore_uri), - format!("--protocol={}", protocol), + format!("--protocol={}", blueprint.protocol), format!( "--log-id=Blueprint-{blueprint_id}-Service-{service_id}{}", - opt.instance_id + manager_opts + .instance_id .clone() .map_or(String::new(), |id| format!("-{}", id)) ), @@ -167,9 +59,41 @@ pub fn generate_process_arguments( // } // Uses occurrences of clap short -v - if opt.verbose > 0 { - arguments.push(format!("-{}", "v".repeat(opt.verbose as usize))); + if manager_opts.verbose > 0 { + arguments.push(format!("-{}", "v".repeat(manager_opts.verbose as usize))); + } + + // Add required env vars for all child processes/gadgets + let mut env_vars = vec![ + ( + "HTTP_RPC_URL".to_string(), + gadget_config.http_rpc_endpoint.to_string(), + ), + ( + "WS_RPC_URL".to_string(), + gadget_config.ws_rpc_endpoint.to_string(), + ), + ( + "KEYSTORE_URI".to_string(), + manager_opts.keystore_uri.clone(), + ), + ("BLUEPRINT_ID".to_string(), format!("{}", blueprint_id)), + ("SERVICE_ID".to_string(), format!("{}", service_id)), + ]; + + let base_data_dir = &manager_opts.data_dir; + let data_dir = base_data_dir.join(format!("blueprint-{blueprint_id}-{sub_service_str}")); + env_vars.push(( + "DATA_DIR".to_string(), + data_dir.to_string_lossy().into_owned(), + )); + + // Ensure our child process inherits the current processes' environment vars + env_vars.extend(std::env::vars()); + + if blueprint.registration_mode { + env_vars.push(("REGISTRATION_MODE_ON".to_string(), "true".to_string())); } - Ok(arguments) + (arguments, env_vars) } diff --git a/crates/blueprint/manager/src/sources/testing.rs b/crates/blueprint/manager/src/sources/testing.rs index df26298..80bb3b8 100644 --- a/crates/blueprint/manager/src/sources/testing.rs +++ b/crates/blueprint/manager/src/sources/testing.rs @@ -1,6 +1,6 @@ +use crate::error::{Error, Result}; use crate::sources::BinarySourceFetcher; use async_trait::async_trait; -use color_eyre::Report; use gadget_logging::trace; use std::path::PathBuf; use tangle_subxt::tangle_testnet_runtime::api::runtime_types::tangle_primitives::services::TestFetcher; @@ -13,7 +13,7 @@ pub struct TestSourceFetcher { #[async_trait] impl BinarySourceFetcher for TestSourceFetcher { - async fn get_binary(&self) -> color_eyre::Result { + async fn get_binary(&self) -> Result { // Step 1: Build the binary. It will be stored in the root directory/bin/ let TestFetcher { cargo_package, @@ -21,9 +21,9 @@ impl BinarySourceFetcher for TestSourceFetcher { .. } = &self.fetcher; let cargo_bin = String::from_utf8(cargo_package.0 .0.clone()) - .map_err(|err| Report::msg(format!("Failed to parse `cargo_bin`: {:?}", err)))?; + .map_err(|err| Error::Other(format!("Failed to parse `cargo_bin`: {:?}", err)))?; let base_path_str = String::from_utf8(base_path.0 .0.clone()) - .map_err(|err| Report::msg(format!("Failed to parse `base_path`: {:?}", err)))?; + .map_err(|err| Error::Other(format!("Failed to parse `base_path`: {:?}", err)))?; let git_repo_root = get_git_repo_root_path().await?; let profile = if cfg!(debug_assertions) { @@ -57,9 +57,8 @@ impl BinarySourceFetcher for TestSourceFetcher { } let output = command.current_dir(&base_path).output().await?; - if !output.status.success() { - return Err(Report::msg(format!("Failed to build binary: {:?}", output))); + return Err(Error::BuildBinary(output)); } Ok(binary_path) @@ -73,7 +72,7 @@ impl BinarySourceFetcher for TestSourceFetcher { self.gadget_name.clone() } } -async fn get_git_repo_root_path() -> color_eyre::Result { +async fn get_git_repo_root_path() -> Result { // Run a process to determine the root directory for this repo let output = tokio::process::Command::new("git") .arg("rev-parse") @@ -82,10 +81,7 @@ async fn get_git_repo_root_path() -> color_eyre::Result { .await?; if !output.status.success() { - return Err(Report::msg(format!( - "Failed to get git root path: {:?}", - output - ))); + return Err(Error::FetchGitRoot(output)); } Ok(PathBuf::from(String::from_utf8(output.stdout)?.trim())) diff --git a/crates/clients/Cargo.toml b/crates/clients/Cargo.toml index bee79db..21127e5 100644 --- a/crates/clients/Cargo.toml +++ b/crates/clients/Cargo.toml @@ -10,6 +10,8 @@ gadget-client-tangle = { workspace = true, optional = true } gadget-client-networking = { workspace = true, optional = true } gadget-client-core = { workspace = true } +thiserror.workspace = true + [features] default = ["std"] std = [ @@ -17,6 +19,7 @@ std = [ "gadget-client-evm?/std", "gadget-client-networking?/std", "gadget-client-tangle?/std", + "thiserror/std" ] web = ["gadget-client-tangle?/web"] diff --git a/crates/clients/core/src/lib.rs b/crates/clients/core/src/lib.rs index eb9e23c..ba6470c 100644 --- a/crates/clients/core/src/lib.rs +++ b/crates/clients/core/src/lib.rs @@ -13,19 +13,24 @@ pub trait GadgetServicesClient: Send + Sync + 'static { type PublicAccountIdentity: Send + Sync + 'static; /// A generalized ID that distinguishes the current blueprint from others type Id: Send + Sync + 'static; + type Error: core::error::Error + From + Send + Sync + 'static; + /// Returns the set of operators for the current job async fn get_operators( &self, - ) -> Result, Error>; + ) -> Result< + OperatorSet, + Self::Error, + >; /// Returns the ID of the operator - async fn operator_id(&self) -> Result; + async fn operator_id(&self) -> Result; /// Returns the unique ID for this blueprint - async fn blueprint_id(&self) -> Result; + async fn blueprint_id(&self) -> Result; /// Returns an operator set with the index of the current operator within that set async fn get_operators_and_operator_id( &self, - ) -> Result<(OperatorSet, usize), Error> { + ) -> Result<(OperatorSet, usize), Self::Error> { let operators = self .get_operators() .await @@ -51,11 +56,12 @@ pub trait GadgetServicesClient: Send + Sync + 'static { } /// Returns the index of the current operator in the operator set - async fn get_operator_index(&self) -> Result { - self.get_operators_and_operator_id() + async fn get_operator_index(&self) -> Result { + let (_, index) = self + .get_operators_and_operator_id() .await - .map_err(|err| Error::GetOperatorIndex(err.to_string())) - .map(|(_, index)| index) + .map_err(|err| Error::GetOperatorIndex(err.to_string()))?; + Ok(index) } } diff --git a/crates/clients/eigenlayer/src/error.rs b/crates/clients/eigenlayer/src/error.rs index 6d64c15..04f8fe6 100644 --- a/crates/clients/eigenlayer/src/error.rs +++ b/crates/clients/eigenlayer/src/error.rs @@ -2,7 +2,7 @@ use gadget_std::string::ParseError; use thiserror::Error; #[derive(Debug, Error)] -pub enum EigenlayerClientError { +pub enum Error { #[error("IO error: {0}")] Io(#[from] gadget_std::io::Error), #[error("Parse error {0}")] @@ -25,10 +25,10 @@ pub enum EigenlayerClientError { OtherStatic(&'static str), } -impl From<&'static str> for EigenlayerClientError { +impl From<&'static str> for Error { fn from(e: &'static str) -> Self { - EigenlayerClientError::OtherStatic(e) + Error::OtherStatic(e) } } -pub type Result = gadget_std::result::Result; +pub type Result = gadget_std::result::Result; diff --git a/crates/clients/evm/src/error.rs b/crates/clients/evm/src/error.rs index 688dca5..df41f37 100644 --- a/crates/clients/evm/src/error.rs +++ b/crates/clients/evm/src/error.rs @@ -2,7 +2,7 @@ use gadget_std::string::String; use thiserror::Error; #[derive(Debug, Error)] -pub enum EvmError { +pub enum Error { #[error("Provider error: {0}")] Provider(String), #[error("Invalid address: {0}")] @@ -15,4 +15,4 @@ pub enum EvmError { Abi(String), } -pub type Result = gadget_std::result::Result; +pub type Result = gadget_std::result::Result; diff --git a/crates/clients/networking/src/error.rs b/crates/clients/networking/src/error.rs index 5f4c51d..710fb67 100644 --- a/crates/clients/networking/src/error.rs +++ b/crates/clients/networking/src/error.rs @@ -2,7 +2,7 @@ use gadget_std::string::String; use thiserror::Error; #[derive(Debug, Error)] -pub enum NetworkError { +pub enum Error { #[error("P2P error: {0}")] P2p(String), #[error("Transport error: {0}")] @@ -13,4 +13,4 @@ pub enum NetworkError { Configuration(String), } -pub type Result = gadget_std::result::Result; +pub type Result = gadget_std::result::Result; diff --git a/crates/clients/networking/src/p2p.rs b/crates/clients/networking/src/p2p.rs index 9ab9330..fc5c9bd 100644 --- a/crates/clients/networking/src/p2p.rs +++ b/crates/clients/networking/src/p2p.rs @@ -1,4 +1,4 @@ -use crate::error::{NetworkError, Result}; +use crate::error::{Error, Result}; use gadget_config::GadgetConfiguration; use gadget_crypto::k256_crypto::{K256SigningKey, K256VerifyingKey}; use gadget_networking::gossip::GossipHandle; @@ -35,7 +35,7 @@ impl P2PClient { pub fn libp2p_identity(&self, ed25519_seed: Vec) -> Result { let mut seed_bytes = ed25519_seed; let keypair = libp2p::identity::Keypair::ed25519_from_bytes(&mut seed_bytes) - .map_err(|err| NetworkError::Configuration(err.to_string()))?; + .map_err(|err| Error::Configuration(err.to_string()))?; Ok(keypair) } @@ -68,9 +68,7 @@ impl P2PClient { Ok(handle) => Ok(handle), Err(err) => { gadget_logging::error!("Failed to start network: {}", err.to_string()); - Err(NetworkError::Protocol(format!( - "Failed to start network: {err}" - ))) + Err(Error::Protocol(format!("Failed to start network: {err}"))) } } } diff --git a/crates/clients/src/error.rs b/crates/clients/src/error.rs new file mode 100644 index 0000000..bbe7735 --- /dev/null +++ b/crates/clients/src/error.rs @@ -0,0 +1,17 @@ +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error(transparent)] + Core(#[from] gadget_client_core::Error), + #[error(transparent)] + #[cfg(feature = "eigenlayer")] + Eigenlayer(#[from] gadget_client_eigenlayer::error::Error), + #[error(transparent)] + #[cfg(feature = "evm")] + Evm(#[from] gadget_client_evm::error::Error), + #[error(transparent)] + #[cfg(feature = "networking")] + Networking(#[from] gadget_client_networking::error::Error), + #[error(transparent)] + #[cfg(feature = "tangle")] + Tangle(#[from] gadget_client_tangle::error::Error), +} diff --git a/crates/clients/src/lib.rs b/crates/clients/src/lib.rs index d6e6c30..e82d0b2 100644 --- a/crates/clients/src/lib.rs +++ b/crates/clients/src/lib.rs @@ -1,5 +1,7 @@ #![cfg_attr(not(feature = "std"), no_std)] +pub mod error; + #[cfg(feature = "eigenlayer")] pub use gadget_client_eigenlayer as eigenlayer; diff --git a/crates/clients/tangle/src/client.rs b/crates/clients/tangle/src/client.rs index f7c8ac7..2c7a76c 100644 --- a/crates/clients/tangle/src/client.rs +++ b/crates/clients/tangle/src/client.rs @@ -1,5 +1,5 @@ use sp_core::ecdsa; -use crate::error::Result; +use crate::error::{Result, Error}; use crate::EventsClient; use gadget_std::sync::Arc; use gadget_std::time::Duration; @@ -10,7 +10,7 @@ use subxt::utils::AccountId32; use subxt::{self, PolkadotConfig}; use tangle_subxt::tangle_testnet_runtime::api; use tangle_subxt::tangle_testnet_runtime::api::runtime_types::pallet_multi_asset_delegation::types::operator::OperatorMetadata; -use gadget_client_core::{Error, GadgetServicesClient, OperatorSet}; +use gadget_client_core::{GadgetServicesClient, OperatorSet}; use gadget_config::GadgetConfiguration; use gadget_crypto_sp_core::{SpEcdsa, SpSr25519}; use gadget_keystore::{Keystore, KeystoreConfig}; @@ -56,18 +56,13 @@ impl TangleClient { keystore_config.fs_root(config.keystore_uri.replace("file://", "")) }; - let keystore = Arc::new(Keystore::new(keystore_config).map_err(|err| Error::msg(err))?); + let keystore = Arc::new(Keystore::new(keystore_config)?); let rpc_url = config.ws_rpc_endpoint.as_str(); - let client = TangleServicesClient::new( - subxt::OnlineClient::from_url(rpc_url) - .await - .map_err(|err| Error::msg(err))?, - ); + let client = TangleServicesClient::new(subxt::OnlineClient::from_url(rpc_url).await?); let account_id = keystore - .get_public_key_local::(KEY_ID) - .map_err(|err| Error::msg(err))? + .get_public_key_local::(KEY_ID)? .0 .0 .into(); @@ -137,13 +132,11 @@ impl TangleClient { .rpc_client .storage() .at_latest() - .await - .map_err(|err| Error::msg(err))?; + .await?; let metadata_storage_key = api::storage().multi_asset_delegation().operators(operator); - storage - .fetch(&metadata_storage_key) - .await - .map_err(|err| Error::msg(err)) + + let ret = storage.fetch(&metadata_storage_key).await?; + Ok(ret) } /// Retrieves the current party index and operator mapping @@ -162,10 +155,7 @@ impl TangleClient { Error, > { let parties = self.get_operators().await?; - let my_id = self - .keystore - .get_public_key_local::(KEY_ID) - .map_err(|err| Error::msg(err))?; + let my_id = self.keystore.get_public_key_local::(KEY_ID)?; gadget_logging::trace!( "Looking for {my_id:?} in parties: {:?}", @@ -175,7 +165,7 @@ impl TangleClient { let index_of_my_id = parties .iter() .position(|(_id, key)| key == &my_id.0) - .ok_or_else(|| Error::msg("Party not found in operator list"))?; + .ok_or(Error::PartyNotFound)?; Ok((index_of_my_id, parties)) } @@ -241,6 +231,7 @@ impl GadgetServicesClient for TangleClient { type PublicApplicationIdentity = ecdsa::Public; type PublicAccountIdentity = AccountId32; type Id = BlueprintId; + type Error = Error; /// Retrieves the ECDSA keys for all current service operators /// @@ -253,7 +244,7 @@ impl GadgetServicesClient for TangleClient { &self, ) -> std::result::Result< OperatorSet, - Error, + Self::Error, > { let client = &self.services_client; let current_blueprint = self.blueprint_id().await?; @@ -261,7 +252,7 @@ impl GadgetServicesClient for TangleClient { .config .protocol_settings .tangle() - .map_err(|err| Error::msg(err))? + .map_err(|_| Error::NotTangle)? .service_id .ok_or_else(|| Error::Other("No service ID injected into config".into()))?; let now = self @@ -271,14 +262,8 @@ impl GadgetServicesClient for TangleClient { let current_service_op = self .services_client .current_service_operators(now, service_id) - .await - .map_err(|err| Error::msg(err))?; - let storage = client - .rpc_client - .storage() - .at_latest() - .await - .map_err(|err| Error::msg(err))?; + .await?; + let storage = client.rpc_client.storage().at_latest().await?; let mut map = std::collections::BTreeMap::new(); for (operator, _) in current_service_op { @@ -287,7 +272,7 @@ impl GadgetServicesClient for TangleClient { .operators(current_blueprint, &operator); let maybe_pref = storage.fetch(&addr).await.map_err(|err| { - Error::msg(format!( + Error::Other(format!( "Failed to fetch operator storage for {operator}: {err}" )) })?; @@ -295,35 +280,29 @@ impl GadgetServicesClient for TangleClient { if let Some(pref) = maybe_pref { map.insert(operator, ecdsa::Public(pref.key)); } else { - return Err(Error::msg(format!( - "Missing ECDSA key for operator {operator}" - ))); + return Err(Error::MissingEcdsa(operator)); } } Ok(map) } - async fn operator_id(&self) -> std::result::Result { - Ok(self - .keystore - .get_public_key_local::(KEY_ID) - .map_err(|err| Error::msg(err))? - .0) + async fn operator_id( + &self, + ) -> std::result::Result { + Ok(self.keystore.get_public_key_local::(KEY_ID)?.0) } /// Retrieves the current blueprint ID from the configuration /// /// # Errors /// Returns an error if the blueprint ID is not found in the configuration - async fn blueprint_id(&self) -> std::result::Result { - let id = self + async fn blueprint_id(&self) -> std::result::Result { + let c = self .config .protocol_settings .tangle() - .map(|c| c.blueprint_id) - .map_err(|err| format!("Blueprint ID not found in configuration: {err}")) - .map_err(|err| Error::msg(err))?; - Ok(id) + .map_err(|_| Error::NotTangle)?; + Ok(c.blueprint_id) } } diff --git a/crates/clients/tangle/src/error.rs b/crates/clients/tangle/src/error.rs index 18fbe17..ba23f19 100644 --- a/crates/clients/tangle/src/error.rs +++ b/crates/clients/tangle/src/error.rs @@ -1,20 +1,34 @@ use gadget_std::io; use gadget_std::string::String; +use subxt_core::utils::AccountId32; use thiserror::Error; #[derive(Debug, Error)] -pub enum TangleClientError { - #[error("IO error: {0}")] - Io(#[from] io::Error), - #[error("Subxt error: {0}")] - Subxt(#[from] subxt::Error), +pub enum Error { #[error("Tangle error: {0}")] Tangle(TangleDispatchError), + #[error("Not a Tangle instance")] + NotTangle, + + #[error("Missing ECDSA key for operator: {0}")] + MissingEcdsa(AccountId32), + #[error("Party not found in operator list")] + PartyNotFound, + #[error("{0}")] Other(String), + + #[error(transparent)] + Keystore(#[from] gadget_keystore::Error), + #[error(transparent)] + Core(#[from] gadget_client_core::Error), + #[error("IO error: {0}")] + Io(#[from] io::Error), + #[error("Subxt error: {0}")] + Subxt(#[from] subxt::Error), } -pub type Result = gadget_std::result::Result; +pub type Result = gadget_std::result::Result; #[derive(Debug)] pub struct TangleDispatchError( @@ -31,9 +45,9 @@ impl From for TangleClientError { +impl From for Error { fn from(error: TangleDispatchError) -> Self { - TangleClientError::Tangle(error) + Error::Tangle(error) } } diff --git a/crates/clients/tangle/src/services.rs b/crates/clients/tangle/src/services.rs index 1878ab9..725acc2 100644 --- a/crates/clients/tangle/src/services.rs +++ b/crates/clients/tangle/src/services.rs @@ -1,4 +1,4 @@ -use crate::error::TangleClientError; +use crate::error::Error; use crate::error::{Result, TangleDispatchError}; use gadget_std::string::ToString; use gadget_std::vec::Vec; @@ -88,7 +88,7 @@ where let ret = self.rpc_client.storage().at(at).fetch(&call).await?; match ret { Some(blueprints) => Ok(blueprints.1), - None => Err(TangleClientError::Other("Blueprint not found".to_string())), + None => Err(Error::Other("Blueprint not found".to_string())), } } @@ -103,7 +103,7 @@ where let ret = self.rpc_client.storage().at(at).fetch(&call).await?; match ret { Some(blueprints) => Ok(blueprints.0), - None => Err(TangleClientError::Other("Blueprint not found".to_string())), + None => Err(Error::Other("Blueprint not found".to_string())), } }