From 6ab37d0a80110b952c47bb063aa852860b7e8d5c Mon Sep 17 00:00:00 2001 From: Donovan Tjemmes <37707055+Tjemmmic@users.noreply.github.com> Date: Fri, 31 Jan 2025 18:34:01 -0600 Subject: [PATCH] fix: update blueprint examples (#628) * fix(examples)!: updating and improving blueprint examples wip * fix!: blueprint examples update wip * fix: blueprint examples building * fix!: more example tests wip * fix: rustdoc-types version bump * fix: clippy * chore: toolchain bump * fix: example cleanup for docs --------- Co-authored-by: drewstone --- Cargo.lock | 19 ++- Cargo.toml | 4 +- blueprints/examples/Cargo.toml | 30 +---- blueprints/examples/build.rs | 4 +- blueprints/examples/src/eigen_context.rs | 83 ++++++++++---- blueprints/examples/src/main.rs | 15 +-- .../examples/src/periodic_web_poller.rs | 93 ++++++++++----- blueprints/examples/src/raw_tangle_events.rs | 34 +++--- blueprints/examples/src/services_context.rs | 62 +++++----- blueprints/examples/src/tests.rs | 108 ++++++++++++++---- .../incredible-squaring-symbiotic/Cargo.toml | 8 +- .../incredible-squaring-symbiotic/build.rs | 9 +- .../foundry.toml | 19 ++- .../incredible-squaring-symbiotic/src/lib.rs | 12 +- .../incredible-squaring-symbiotic/src/main.rs | 16 +-- crates/blueprint/serde/src/de.rs | 2 +- crates/clients/core/src/error.rs | 8 ++ crates/clients/eigenlayer/Cargo.toml | 2 + crates/clients/eigenlayer/src/error.rs | 6 + crates/clients/evm/Cargo.toml | 2 + crates/clients/evm/src/error.rs | 6 + crates/clients/networking/Cargo.toml | 2 + crates/clients/networking/src/error.rs | 6 + crates/clients/tangle/src/error.rs | 6 + crates/macros/blueprint-proc-macro/src/lib.rs | 4 +- crates/networking/src/networking.rs | 10 +- crates/sdk/Cargo.toml | 4 +- crates/sdk/src/error.rs | 16 ++- rust-toolchain.toml | 2 +- 29 files changed, 400 insertions(+), 192 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index be1c9fee8..abfe2b9e3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2547,6 +2547,18 @@ dependencies = [ "gadget-std", ] +[[package]] +name = "blueprint-examples" +version = "0.1.1" +dependencies = [ + "blueprint-sdk", + "color-eyre", + "serde", + "serde_json", + "tempfile", + "uuid 1.12.1", +] + [[package]] name = "blueprint-manager" version = "0.2.2" @@ -6542,6 +6554,7 @@ dependencies = [ "alloy-transport", "eigensdk", "gadget-anvil-testing-utils", + "gadget-client-core", "gadget-config", "gadget-std", "gadget-utils-evm", @@ -6571,6 +6584,7 @@ dependencies = [ "alloy-transport-http", "async-trait", "gadget-anvil-testing-utils", + "gadget-client-core", "gadget-logging", "gadget-rpc-calls", "gadget-std", @@ -6588,6 +6602,7 @@ dependencies = [ name = "gadget-client-networking" version = "0.1.0" dependencies = [ + "gadget-client-core", "gadget-config", "gadget-crypto", "gadget-logging", @@ -15220,9 +15235,9 @@ dependencies = [ [[package]] name = "rustdoc-types" -version = "0.33.0" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e33060dbec9e1d13d285c4cddc150a431569be97f33bf0b6c1ec6eea934c31ca" +checksum = "bf583db9958b3161d7980a56a8ee3c25e1a40708b81259be72584b7e0ea07c95" dependencies = [ "serde", ] diff --git a/Cargo.toml b/Cargo.toml index be8e02185..3a1902aaf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,6 @@ members = [ ] exclude = [ "blueprints/incredible-squaring-symbiotic", - "blueprints/examples" ] [workspace.package] @@ -38,6 +37,7 @@ broken_intra_doc_links = "deny" blueprint-sdk = { version = "0.1.0", path = "./crates/sdk", default-features = false } # Blueprint Examples +blueprint-examples = { version = "0.1.0", path = "./blueprints/examples", default-features = false } incredible-squaring-blueprint = { version = "0.1.1", path = "./blueprints/incredible-squaring", default-features = false } incredible-squaring-blueprint-eigenlayer = { version = "0.1.1", path = "./blueprints/incredible-squaring-eigenlayer", default-features = false } @@ -211,7 +211,7 @@ itertools = { version = "0.13.0", default-features = false } paste = { version = "1.0.15", default-features = false } proc-macro2 = { version = "1.0", default-features = false } quote = { version = "1.0", default-features = false } -rustdoc-types = { version = "0.33.0", default-features = false } +rustdoc-types = { version = "0.35.0", default-features = false } syn = { version = "2.0.75", default-features = false } trybuild = { version = "1.0", default-features = false } typed-builder = { version = "0.19", default-features = false } diff --git a/blueprints/examples/Cargo.toml b/blueprints/examples/Cargo.toml index f84bf9e85..47821b9c3 100644 --- a/blueprints/examples/Cargo.toml +++ b/blueprints/examples/Cargo.toml @@ -10,42 +10,20 @@ repository.workspace = true publish = false [dependencies] -async-trait = { workspace = true } -blueprint-sdk = { workspace = true, features = ["std"] } -eigensdk = { workspace = true } +blueprint-sdk = { workspace = true, features = ["std", "testing", "evm", "tangle", "eigenlayer", "macros", "cronjob"] } color-eyre = { workspace = true } -reqwest = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } tempfile = { workspace = true } -tokio = { workspace = true, features = ["full"] } -tokio-util = { workspace = true } -tracing = { workspace = true } uuid = { workspace = true } -alloy-primitives = { workspace = true } -alloy-json-abi = { workspace = true } -alloy-sol-types = { workspace = true } -alloy-rpc-client = { workspace = true } -alloy-rpc-types = { workspace = true } -alloy-rpc-types-eth = { workspace = true } -alloy-provider = { workspace = true } -alloy-pubsub = { workspace = true } -alloy-signer = { workspace = true } -alloy-signer-local = { workspace = true } -alloy-network = { workspace = true } -alloy-node-bindings = { workspace = true } -alloy-contract = { workspace = true } -alloy-consensus = { workspace = true } -alloy-transport = { workspace = true } -alloy-transport-http = { workspace = true } [dev-dependencies] -gadget-testing-utils = { workspace = true } +blueprint-sdk = { workspace = true, features = ["std", "testing", "evm", "tangle", "eigenlayer", "macros", "cronjob"] } [build-dependencies] -blueprint-metadata = { workspace = true } -blueprint-build-utils = { workspace = true } +blueprint-sdk = { workspace = true, features = ["build"] } + [features] default = ["std"] diff --git a/blueprints/examples/build.rs b/blueprints/examples/build.rs index 6d902527b..ac1740da6 100644 --- a/blueprints/examples/build.rs +++ b/blueprints/examples/build.rs @@ -1,7 +1,5 @@ fn main() { println!("cargo:rerun-if-changed=src/lib.rs"); println!("cargo:rerun-if-changed=src/main.rs"); - println!("cargo:rerun-if-changed=contracts/src/*"); - blueprint_build_utils::build_contracts(vec!["contracts"]); - blueprint_metadata::generate_json(); + blueprint_sdk::build::utils::build_contracts(vec!["contracts"]); } diff --git a/blueprints/examples/src/eigen_context.rs b/blueprints/examples/src/eigen_context.rs index aa1eb7c81..f94a1badc 100644 --- a/blueprints/examples/src/eigen_context.rs +++ b/blueprints/examples/src/eigen_context.rs @@ -1,17 +1,19 @@ -use alloy_primitives::U256; -use alloy_primitives::{address, Address, Bytes}; +use blueprint_sdk::alloy::primitives::{address, Bytes, U256}; +use blueprint_sdk::alloy::rpc::types::Log; +use blueprint_sdk::alloy::sol; +use blueprint_sdk::config::GadgetConfiguration; +use blueprint_sdk::contexts::eigenlayer::EigenlayerContext; +use blueprint_sdk::event_listeners::core::InitializableEventHandler; +use blueprint_sdk::event_listeners::evm::EvmContractEventListener; +use blueprint_sdk::macros::contexts::EigenlayerContext; +use blueprint_sdk::macros::load_abi; +use blueprint_sdk::std::{env, Zero}; +use blueprint_sdk::utils::evm::get_provider_http; +use blueprint_sdk::{job, Error}; use color_eyre::eyre::eyre; -use gadget_sdk::event_listener::evm::contracts::EvmContractEventListener; -use gadget_sdk::event_utils::InitializableEventHandler; -use gadget_sdk::subxt_core::ext::sp_runtime::traits::Zero; -use gadget_sdk::utils::evm::get_provider_http; -use gadget_sdk::{config::StdGadgetConfiguration, contexts::EigenlayerContext, job, load_abi}; use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use std::env; -use std::ops::Deref; -alloy_sol_types::sol!( +sol!( #[allow(missing_docs)] #[sol(rpc)] #[derive(Debug, Serialize, Deserialize)] @@ -24,14 +26,17 @@ load_abi!( "contracts/out/ExampleTaskManager.sol/ExampleTaskManager.json" ); +type ProcessorError = + blueprint_sdk::event_listeners::core::Error; + #[derive(Clone, EigenlayerContext)] pub struct ExampleEigenContext { #[config] - pub std_config: StdGadgetConfiguration, + pub std_config: GadgetConfiguration, } pub async fn constructor( - env: StdGadgetConfiguration, + env: GadgetConfiguration, ) -> color_eyre::Result { let example_address = env::var("EXAMPLE_TASK_MANAGER_ADDRESS") .map(|addr| addr.parse().expect("Invalid EXAMPLE_TASK_MANAGER_ADDRESS")) @@ -63,19 +68,25 @@ pub async fn constructor( pub async fn handle_job( ctx: ExampleEigenContext, event: ExampleTaskManager::NewTaskCreated, - log: alloy_rpc_types::Log, -) -> Result> { + log: Log, +) -> Result { // Example address, quorum number, and index let operator_addr = address!("70997970C51812dc3A010C7d01b50e0d17dc79C8"); let quorum_number: u8 = 0; let index: U256 = U256::from(0); // Get an Operator's ID as FixedBytes from its Address. - let operator_id = ctx.get_operator_id(operator_addr).await?; + let operator_id = ctx + .eigenlayer_client() + .await? + .get_operator_id(operator_addr) + .await?; println!("Operator ID from Address: {:?}", operator_id); // Get an Operator's latest stake update. let latest_stake_update = ctx + .eigenlayer_client() + .await? .get_latest_stake_update(operator_id, quorum_number) .await?; println!("Latest Stake Update: \n\tStake: {:?},\n\tUpdate Block Number: {:?},\n\tNext Update Block Number: {:?}", @@ -87,12 +98,16 @@ pub async fn handle_job( // Get Operator stake in Quorums at a given block. let stake_in_quorums_at_block = ctx + .eigenlayer_client() + .await? .get_operator_stake_in_quorums_at_block(block_number, Bytes::from(vec![0])) .await?; assert!(!stake_in_quorums_at_block.is_empty()); // Get an Operator's stake in Quorums at the current block. let stake_in_quorums_at_current_block = ctx + .eigenlayer_client() + .await? .get_operator_stake_in_quorums_at_current_block(operator_id) .await?; println!( @@ -102,12 +117,18 @@ pub async fn handle_job( assert!(!stake_in_quorums_at_current_block.is_empty()); // Get an Operator by ID. - let operator_by_id = ctx.get_operator_by_id(*operator_id).await?; + let operator_by_id = ctx + .eigenlayer_client() + .await? + .get_operator_by_id(*operator_id) + .await?; println!("Operator by ID: {:?}", operator_by_id); assert_eq!(operator_by_id, operator_addr); // Get an Operator stake history. let stake_history = ctx + .eigenlayer_client() + .await? .get_operator_stake_history(operator_id, quorum_number) .await?; println!("Stake History for {operator_id} in Quorum {quorum_number}:"); @@ -121,6 +142,8 @@ pub async fn handle_job( // Get an Operator stake update at a given index. let stake_update_at_index = ctx + .eigenlayer_client() + .await? .get_operator_stake_update_at_index(quorum_number, operator_id, index) .await?; println!("Stake Update at Index {index}: \n\tStake: {:?}\n\tUpdate Block Number: {:?}\n\tNext Update Block Number: {:?}", stake_update_at_index.stake, stake_update_at_index.updateBlockNumber, stake_update_at_index.nextUpdateBlockNumber); @@ -128,13 +151,19 @@ pub async fn handle_job( // Get an Operator's stake at a given block number. let stake_at_block_number = ctx + .eigenlayer_client() + .await? .get_operator_stake_at_block_number(operator_id, quorum_number, block_number) .await?; println!("Stake at Block Number: {:?}", stake_at_block_number); assert!(!stake_at_block_number.is_zero()); // Get an Operator's details. - let operator = ctx.get_operator_details(operator_addr).await?; + let operator = ctx + .eigenlayer_client() + .await? + .get_operator_details(operator_addr) + .await?; println!("Operator Details: \n\tAddress: {:?},\n\tEarnings receiver address: {:?},\n\tDelegation approver address: {:?},\n\tMetadata URL: {:?},\n\tStaker Opt Out Window Blocks: {:?}", operator.address, operator.earnings_receiver_address, @@ -145,11 +174,15 @@ pub async fn handle_job( // Get an Operator's latest stake update. let latest_stake_update = ctx + .eigenlayer_client() + .await? .get_latest_stake_update(operator_id, quorum_number) .await?; let block_number = latest_stake_update.updateBlockNumber - 1; // Get the total stake at a given block number from a given index. let total_stake_at_block_number_from_index = ctx + .eigenlayer_client() + .await? .get_total_stake_at_block_number_from_index(quorum_number, block_number, index) .await?; println!( @@ -159,7 +192,11 @@ pub async fn handle_job( assert!(total_stake_at_block_number_from_index.is_zero()); // Get the total stake history length of a given quorum. - let total_stake_history_length = ctx.get_total_stake_history_length(quorum_number).await?; + let total_stake_history_length = ctx + .eigenlayer_client() + .await? + .get_total_stake_history_length(quorum_number) + .await?; println!( "Total Stake History Length: {:?}", total_stake_history_length @@ -168,6 +205,8 @@ pub async fn handle_job( // Provides the public keys of existing registered operators within the provided block range. let existing_registered_operator_pub_keys = ctx + .eigenlayer_client() + .await? .query_existing_registered_operator_pub_keys(0, block_number as u64) .await?; println!( @@ -183,7 +222,7 @@ pub async fn handle_job( } pub async fn handle_events( - event: (ExampleTaskManager::NewTaskCreated, alloy_rpc_types::Log), -) -> Result<(ExampleTaskManager::NewTaskCreated, alloy_rpc_types::Log), gadget_sdk::Error> { - Ok(event) + event: (ExampleTaskManager::NewTaskCreated, Log), +) -> Result, ProcessorError> { + Ok(Some(event)) } diff --git a/blueprints/examples/src/main.rs b/blueprints/examples/src/main.rs index 892186173..793a20c55 100644 --- a/blueprints/examples/src/main.rs +++ b/blueprints/examples/src/main.rs @@ -1,11 +1,12 @@ -use alloy_primitives::Address; use blueprint_examples::{eigen_context, periodic_web_poller, raw_tangle_events, services_context}; -use gadget_sdk::info; -use gadget_sdk::runners::eigenlayer::EigenlayerBLSConfig; -use gadget_sdk::runners::{tangle::TangleConfig, BlueprintRunner}; -use std::env; +use blueprint_sdk::alloy::primitives::Address; +use blueprint_sdk::logging::info; +use blueprint_sdk::runners::core::runner::BlueprintRunner; +use blueprint_sdk::runners::eigenlayer::bls::EigenlayerBLSConfig; +use blueprint_sdk::runners::tangle::tangle::TangleConfig; +use blueprint_sdk::std::env; -#[gadget_sdk::main(env)] +#[blueprint_sdk::main(env)] async fn main() -> Result<(), Box> { info!("~~~ Executing Blueprint Examples ~~~"); @@ -19,7 +20,7 @@ async fn main() -> Result<(), Box> { info!("Running Tangle examples"); BlueprintRunner::new(TangleConfig::default(), env.clone()) .job(raw_tangle_events::constructor(env.clone()).await?) - .job(periodic_web_poller::constructor()) + .job(periodic_web_poller::constructor("1/2 * * * * *")) .job(services_context::constructor(env.clone()).await?) .run() .await?; diff --git a/blueprints/examples/src/periodic_web_poller.rs b/blueprints/examples/src/periodic_web_poller.rs index c3fa887fa..aa61c8af1 100644 --- a/blueprints/examples/src/periodic_web_poller.rs +++ b/blueprints/examples/src/periodic_web_poller.rs @@ -1,65 +1,95 @@ -use gadget_sdk::async_trait::async_trait; -use gadget_sdk::event_listener::periodic::PeriodicEventListener; -use gadget_sdk::event_listener::EventListener; -use gadget_sdk::event_utils::InitializableEventHandler; -use gadget_sdk::job; -use std::convert::Infallible; +use blueprint_sdk::alloy::transports::http::reqwest; +use blueprint_sdk::event_listeners::core::{EventListener, InitializableEventHandler}; +use blueprint_sdk::event_listeners::cronjob::{ + error::Error as CronJobError, CronJob, CronJobDefinition, +}; +use blueprint_sdk::logging::info; +use blueprint_sdk::macros::ext::async_trait::async_trait; +use blueprint_sdk::{job, Error}; -pub fn constructor() -> impl InitializableEventHandler { +type ProcessorError = blueprint_sdk::event_listeners::core::Error; + +pub fn constructor(cron: &'static str) -> impl InitializableEventHandler { WebPollerEventHandler { - client: reqwest::Client::new(), + context: WebPollerContext::new(cron, reqwest::Client::new()), + } +} + +#[derive(Clone)] +pub struct WebPollerContext { + cron: &'static str, + client: reqwest::Client, +} + +impl WebPollerContext { + pub fn new(cron: &'static str, client: reqwest::Client) -> Self { + Self { cron, client } + } +} + +impl CronJobDefinition for WebPollerContext { + fn cron(&self) -> impl Into { + self.cron } } #[job( id = 0, - params(value), event_listener( - listener = PeriodicEventListener< - 2000, WebPoller, serde_json::Value, reqwest::Client - >, - pre_processor = pre_process, + listener = CronJob, post_processor = post_process, ), )] -// Maps a boolean value obtained from pre-processing to a u8 value -pub async fn web_poller(value: bool, client: reqwest::Client) -> Result { - gadget_sdk::info!("Running web_poller on value: {value}"); - Ok(value as u8) -} +pub async fn web_poller(context: WebPollerContext) -> Result { + // Send a GET request to the JSONPlaceholder API + let response = context + .client + .get("https://jsonplaceholder.typicode.com/todos/10") + .send() + .await + .map_err(|err| Error::Other(err.to_string()))?; -// Maps a JSON response to a boolean value -pub async fn pre_process(event: serde_json::Value) -> Result { - gadget_sdk::info!("Running web_poller pre-processor on value: {event}"); - let completed = event["completed"].as_bool().unwrap_or(false); - Ok(completed) + // Check if the request was successful + if !response.status().is_success() { + return Err(Error::Other("Request failed".to_string())); + } + + let value: serde_json::Value = response + .json() + .await + .map_err(|err| Error::Other(err.to_string()))?; + let completed = value["completed"].as_bool().unwrap_or(false); + + info!("Running web_poller on value: {completed}"); + Ok(completed as u8) } // Received the u8 value output from the job and performs any last post-processing -pub async fn post_process(job_output: u8) -> Result<(), gadget_sdk::Error> { - gadget_sdk::info!("Running web_poller post-processor on value: {job_output}"); +pub async fn post_process(job_output: u8) -> Result<(), ProcessorError> { + info!("Running web_poller post-processor on value: {job_output}"); if job_output == 1 { Ok(()) } else { - Err(gadget_sdk::Error::Other( + Err(ProcessorError::EventHandler( "Job failed since query returned with a false status".to_string(), )) } } -/// Define an event listener that polls a webserver pub struct WebPoller { - pub client: reqwest::Client, + pub context: WebPollerContext, } #[async_trait] -impl EventListener for WebPoller { - async fn new(context: &reqwest::Client) -> Result +impl EventListener for WebPoller { + type ProcessorError = CronJobError; + + async fn new(context: &WebPollerContext) -> Result where Self: Sized, { Ok(Self { - client: context.clone(), + context: context.clone(), }) } @@ -67,6 +97,7 @@ impl EventListener for WebPoller { async fn next_event(&mut self) -> Option { // Send a GET request to the JSONPlaceholder API let response = self + .context .client .get("https://jsonplaceholder.typicode.com/todos/10") .send() diff --git a/blueprints/examples/src/raw_tangle_events.rs b/blueprints/examples/src/raw_tangle_events.rs index ec16a11a9..63ad64a11 100644 --- a/blueprints/examples/src/raw_tangle_events.rs +++ b/blueprints/examples/src/raw_tangle_events.rs @@ -1,28 +1,32 @@ -use gadget_sdk::config::StdGadgetConfiguration; -use gadget_sdk::contexts::TangleClientContext; -use gadget_sdk::event_listener::tangle::{TangleEvent, TangleEventListener}; -use gadget_sdk::event_utils::InitializableEventHandler; -use gadget_sdk::job; -use gadget_sdk::tangle_subxt::tangle_testnet_runtime::api; +use blueprint_sdk::config::GadgetConfiguration; +use blueprint_sdk::contexts::keystore::KeystoreContext; +use blueprint_sdk::crypto::sp_core::SpSr25519; +use blueprint_sdk::event_listeners::core::InitializableEventHandler; +use blueprint_sdk::event_listeners::tangle::events::{TangleEvent, TangleEventListener}; +use blueprint_sdk::job; +use blueprint_sdk::keystore::backends::Backend; +use blueprint_sdk::logging::info; +use blueprint_sdk::macros::contexts::{ServicesContext, TangleClientContext}; +use blueprint_sdk::tangle_subxt::tangle_testnet_runtime::api; -#[derive(Clone, TangleClientContext)] +#[derive(Clone, TangleClientContext, ServicesContext)] pub struct MyContext { #[config] - sdk_config: StdGadgetConfiguration, + sdk_config: GadgetConfiguration, #[call_id] call_id: Option, } pub async fn constructor( - env: StdGadgetConfiguration, + env: GadgetConfiguration, ) -> color_eyre::Result { - use gadget_sdk::subxt_core::tx::signer::Signer; - let signer = env - .first_sr25519_signer() + .clone() + .keystore() + .first_local::() .map_err(|e| color_eyre::eyre::eyre!(e))?; - gadget_sdk::info!("Starting the event watcher for {} ...", signer.account_id()); + info!("Starting the event watcher for {:?} ...", signer.0); RawEventHandler::new( &env, MyContext { @@ -40,14 +44,14 @@ pub async fn constructor( listener = TangleEventListener, ), )] -pub fn raw(event: TangleEvent, context: MyContext) -> Result { +pub fn raw(event: TangleEvent, context: MyContext) -> Result { if let Some(balance_transfer) = event .evt .as_event::() .ok() .flatten() { - gadget_sdk::info!("Found a balance transfer: {balance_transfer:?}"); + info!("Found a balance transfer: {balance_transfer:?}"); } Ok(0) } diff --git a/blueprints/examples/src/services_context.rs b/blueprints/examples/src/services_context.rs index 383830278..f4815f1d9 100644 --- a/blueprints/examples/src/services_context.rs +++ b/blueprints/examples/src/services_context.rs @@ -1,31 +1,38 @@ -use gadget_sdk::config::StdGadgetConfiguration; -use gadget_sdk::contexts::{ServicesContext, TangleClientContext}; -use gadget_sdk::event_listener::tangle::jobs::{services_post_processor, services_pre_processor}; -use gadget_sdk::event_listener::tangle::TangleEventListener; -use gadget_sdk::event_utils::InitializableEventHandler; -use gadget_sdk::job; -use gadget_sdk::subxt_core::utils::AccountId32; -use gadget_sdk::tangle_subxt::tangle_testnet_runtime::api::services::events::JobCalled; +use blueprint_sdk::config::GadgetConfiguration; +use blueprint_sdk::contexts::keystore::KeystoreContext; +use blueprint_sdk::contexts::tangle::TangleClientContext; +use blueprint_sdk::crypto::sp_core::SpSr25519; +use blueprint_sdk::event_listeners::core::InitializableEventHandler; +use blueprint_sdk::event_listeners::tangle::events::TangleEventListener; +use blueprint_sdk::event_listeners::tangle::services::{ + services_post_processor, services_pre_processor, +}; +use blueprint_sdk::job; +use blueprint_sdk::keystore::backends::Backend; +use blueprint_sdk::logging::info; +use blueprint_sdk::macros::contexts::{ServicesContext, TangleClientContext}; +use blueprint_sdk::macros::ext::clients::GadgetServicesClient; +use blueprint_sdk::tangle_subxt::subxt::utils::AccountId32; +use blueprint_sdk::tangle_subxt::tangle_testnet_runtime::api::services::events::JobCalled; #[derive(Clone, ServicesContext, TangleClientContext)] pub struct ExampleServiceContext { #[config] - sdk_config: StdGadgetConfiguration, + sdk_config: GadgetConfiguration, #[call_id] call_id: Option, } pub async fn constructor( - env: StdGadgetConfiguration, + env: GadgetConfiguration, ) -> color_eyre::Result { - use gadget_sdk::subxt_core::tx::signer::Signer; - let signer = env .clone() - .first_sr25519_signer() + .keystore() + .first_local::() .map_err(|e| color_eyre::eyre::eyre!(e))?; - gadget_sdk::info!("Starting the event watcher for {} ...", signer.account_id()); + info!("Starting the event watcher for {:?} ...", signer.0); HandleJobEventHandler::new( &env.clone(), ExampleServiceContext { @@ -49,23 +56,26 @@ pub async fn constructor( pub async fn handle_job( context: ExampleServiceContext, job_details: Vec, -) -> Result { +) -> Result { let client = context.tangle_client().await.unwrap(); - let blueprint_owner = context.current_blueprint_owner(&client).await.unwrap(); - let blueprint = context.current_blueprint(&client).await.unwrap(); - let operators_and_percents = context.current_service_operators(&client).await.unwrap(); + let blueprint_id = client.blueprint_id().await.unwrap(); + let block = client.now().await.unwrap(); + let blueprint_owner = client + .current_blueprint_owner(block, blueprint_id) + .await + .unwrap(); + let blueprint = client + .current_blueprint_owner(block, blueprint_id) + .await + .unwrap(); + let operators_and_percents = client + .current_service_operators(block, blueprint_id) + .await + .unwrap(); let operators = operators_and_percents .iter() .map(|(op, per)| op) .cloned() .collect::>(); - let restaking_delegations = context - .operator_delegations(&client, operators.clone()) - .await - .unwrap(); - let operators_metadata = context - .operators_metadata(&client, operators) - .await - .unwrap(); Ok(0) } diff --git a/blueprints/examples/src/tests.rs b/blueprints/examples/src/tests.rs index 75e934d42..b926a623f 100644 --- a/blueprints/examples/src/tests.rs +++ b/blueprints/examples/src/tests.rs @@ -1,22 +1,26 @@ use crate::eigen_context; use crate::eigen_context::ExampleTaskManager; -use alloy_primitives::Address; -use alloy_provider::Provider; -use blueprint_test_utils::eigenlayer_test_env::start_default_anvil_testnet; -use blueprint_test_utils::helpers::get_receipt; -use blueprint_test_utils::{inject_test_keys, KeyGenType}; -use gadget_io::SupportedChains; -use gadget_sdk::config::protocol::EigenlayerContractAddresses; -use gadget_sdk::config::ContextConfig; -use gadget_sdk::info; -use gadget_sdk::logging::setup_log; -use gadget_sdk::runners::eigenlayer::EigenlayerBLSConfig; -use gadget_sdk::runners::BlueprintRunner; -use gadget_sdk::utils::evm::get_provider_http; -use reqwest::Url; -use std::path::Path; -use std::time::Duration; -use tokio::time::timeout; +use blueprint_sdk::alloy::primitives::Address; +use blueprint_sdk::alloy::providers::Provider; +use blueprint_sdk::alloy::transports::http::reqwest::Url; +use blueprint_sdk::config::protocol::EigenlayerContractAddresses; +use blueprint_sdk::config::supported_chains::SupportedChains; +use blueprint_sdk::config::ContextConfig; +use blueprint_sdk::logging::{info, setup_log}; +use blueprint_sdk::runners::core::runner::BlueprintRunner; +use blueprint_sdk::runners::eigenlayer::bls::EigenlayerBLSConfig; +use blueprint_sdk::std::path::Path; +use blueprint_sdk::std::time::Duration; +use blueprint_sdk::testing::tempfile; +use blueprint_sdk::testing::utils::anvil::keys::{inject_anvil_key, ANVIL_PRIVATE_KEYS}; +use blueprint_sdk::testing::utils::anvil::{get_receipt, start_default_anvil_testnet}; +use blueprint_sdk::testing::utils::harness::TestHarness; +use blueprint_sdk::testing::utils::runner::TestEnv; +use blueprint_sdk::testing::utils::tangle::{OutputValue, TangleTestHarness}; +use blueprint_sdk::tokio; +use blueprint_sdk::tokio::time::timeout; +use blueprint_sdk::utils::evm::get_provider_http; +use color_eyre::Result; #[tokio::test] async fn test_eigenlayer_context() { @@ -74,9 +78,7 @@ async fn test_eigenlayer_context() { let keystore_path = &format!("{}", tmp_dir.path().display()); let keystore_path = Path::new(keystore_path); let keystore_uri = keystore_path.join(format!("keystores/{}", uuid::Uuid::new_v4())); - inject_test_keys(&keystore_uri, KeyGenType::Anvil(1)) - .await - .expect("Failed to inject testing keys for Blueprint Examples Test"); + inject_anvil_key(&keystore_uri, ANVIL_PRIVATE_KEYS[1]).unwrap(); let keystore_uri_normalized = std::path::absolute(&keystore_uri).expect("Failed to resolve keystore URI"); let keystore_uri_str = format!("file:{}", keystore_uri_normalized.display()); @@ -85,10 +87,11 @@ async fn test_eigenlayer_context() { url, Url::parse(&ws_endpoint).unwrap(), keystore_uri_str, + None, SupportedChains::LocalTestnet, EigenlayerContractAddresses::default(), ); - let env = gadget_sdk::config::load(config).expect("Failed to load environment"); + let env = blueprint_sdk::config::load(config).expect("Failed to load environment"); let mut blueprint = BlueprintRunner::new( EigenlayerBLSConfig::new(Address::default(), Address::default()), @@ -127,3 +130,66 @@ async fn test_eigenlayer_context() { Err(_) => panic!("Test timed out"), } } + +#[tokio::test] +async fn test_periodic_web_poller() -> Result<()> { + setup_log(); + + // Initialize test harness + let temp_dir = tempfile::TempDir::new()?; + let harness = TangleTestHarness::setup(temp_dir).await?; + + // Setup service + let (mut test_env, service_id) = harness.setup_services().await?; + + // Add the web poller job + test_env.add_job(crate::periodic_web_poller::constructor("*/5 * * * * *")); + + // Run the test environment + let _test_handle = tokio::spawn(async move { + test_env.run_runner().await.unwrap(); + }); + + // Wait for a few seconds to allow the job to execute + tokio::time::sleep(Duration::from_secs(10)).await; + + // Execute job and verify result + let results = harness + .execute_job(service_id, 0, vec![], vec![OutputValue::Uint64(1)]) + .await?; + + assert_eq!(results.service_id, service_id); + Ok(()) +} + +#[tokio::test] +async fn test_raw_tangle_events() -> Result<()> { + setup_log(); + + // Initialize test harness + let temp_dir = tempfile::TempDir::new()?; + let harness = TangleTestHarness::setup(temp_dir).await?; + let env = harness.env().clone(); + + // Setup service + let (mut test_env, service_id) = harness.setup_services().await?; + + // Add the raw tangle events job + test_env.add_job(crate::raw_tangle_events::constructor(env.clone()).await?); + + // Run the test environment + let _test_handle = tokio::spawn(async move { + test_env.run_runner().await.unwrap(); + }); + + // Wait for a few seconds to allow the job to execute + tokio::time::sleep(Duration::from_secs(10)).await; + + // Execute job and verify result + let results = harness + .execute_job(service_id, 0, vec![], vec![OutputValue::Uint64(0)]) + .await?; + + assert_eq!(results.service_id, service_id); + Ok(()) +} diff --git a/blueprints/incredible-squaring-symbiotic/Cargo.toml b/blueprints/incredible-squaring-symbiotic/Cargo.toml index 249f4f8a7..cd91a8ef2 100644 --- a/blueprints/incredible-squaring-symbiotic/Cargo.toml +++ b/blueprints/incredible-squaring-symbiotic/Cargo.toml @@ -10,11 +10,13 @@ repository.workspace = true publish = false [dependencies] -blueprint-sdk = { workspace = true, features = ["std"] } +blueprint-sdk = { workspace = true, features = ["std", "evm", "macros"] } +alloy-rpc-types = { workspace = true } +alloy-sol-types = { workspace = true } +lazy_static = { workspace = true } [build-dependencies] -blueprint-metadata = { workspace = true } -blueprint-build-utils = { workspace = true } +blueprint-sdk = { workspace = true, features = ["std", "build"] } [features] default = ["std"] diff --git a/blueprints/incredible-squaring-symbiotic/build.rs b/blueprints/incredible-squaring-symbiotic/build.rs index 83c51e77f..07821e6f5 100644 --- a/blueprints/incredible-squaring-symbiotic/build.rs +++ b/blueprints/incredible-squaring-symbiotic/build.rs @@ -1,9 +1,4 @@ fn main() { - // TODO - // let contract_dirs: Vec<&str> = vec![ - // "./contracts/dependencies/core", - // "./contracts/dependencies/forge-std", - // "./contracts", - // ]; - // blueprint_build_utils::build_contracts(contract_dirs); + let contract_dirs: Vec<&str> = vec!["./contracts"]; + blueprint_sdk::build::utils::build_contracts(contract_dirs); } diff --git a/blueprints/incredible-squaring-symbiotic/foundry.toml b/blueprints/incredible-squaring-symbiotic/foundry.toml index 5f051bc5b..76e6a297b 100644 --- a/blueprints/incredible-squaring-symbiotic/foundry.toml +++ b/blueprints/incredible-squaring-symbiotic/foundry.toml @@ -1,6 +1,23 @@ [profile.default] +evm_version = "shanghai" src = "contracts/src" +test = "contracts/test" out = "contracts/out" +script = "contracts/script" +cache_path = "contracts/cache" +broadcast = "contracts/broadcast" libs = ["dependencies"] solc_version = "0.8.20" -viaIR = true \ No newline at end of file +viaIR = true + +[soldeer] +recursive_deps = true +remappings_location = "txt" +remappings_version = false + +[dependencies] +forge-std = "1.9.5" +openzeppelin-upgrades = "4.8.0" +openzeppelin = "4.8.0" + +# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options \ No newline at end of file diff --git a/blueprints/incredible-squaring-symbiotic/src/lib.rs b/blueprints/incredible-squaring-symbiotic/src/lib.rs index 3886526c1..a08f6a8e2 100644 --- a/blueprints/incredible-squaring-symbiotic/src/lib.rs +++ b/blueprints/incredible-squaring-symbiotic/src/lib.rs @@ -1,8 +1,8 @@ -use alloy_primitives::U256; use alloy_sol_types::sol; -use gadget_sdk::event_listener::evm::contracts::EvmContractEventListener; -use gadget_sdk::{job, load_abi}; -use serde::{Deserialize, Serialize}; +use blueprint_sdk::alloy::primitives::U256; +use blueprint_sdk::event_listeners::evm::EvmContractEventListener; +use blueprint_sdk::macros::load_abi; +use blueprint_sdk::{job, Error}; use std::ops::Deref; sol!( @@ -32,7 +32,7 @@ pub struct MyContext; pre_processor = convert_event_to_inputs, ), )] -pub fn xsquare(x: U256, context: MyContext) -> Result { +pub fn xsquare(x: U256, context: MyContext) -> Result { Ok(x.saturating_pow(U256::from(2))) } @@ -42,6 +42,6 @@ pub async fn convert_event_to_inputs( IncredibleSquaringTaskManager::NewTaskCreated, alloy_rpc_types::Log, ), -) -> Result<(U256,), gadget_sdk::Error> { +) -> Result<(U256,), Error> { Ok((event.task.numberToBeSquared,)) } diff --git a/blueprints/incredible-squaring-symbiotic/src/main.rs b/blueprints/incredible-squaring-symbiotic/src/main.rs index 4eaf10e72..0b228499b 100644 --- a/blueprints/incredible-squaring-symbiotic/src/main.rs +++ b/blueprints/incredible-squaring-symbiotic/src/main.rs @@ -1,13 +1,9 @@ -use alloy_network::EthereumWallet; -use color_eyre::Result; -use gadget_sdk::runners::symbiotic::SymbioticConfig; -use gadget_sdk::runners::BlueprintRunner; -use gadget_sdk::{info, keystore::BackendExt}; +use blueprint_sdk::alloy::network::EthereumWallet; +use blueprint_sdk::logging::info; +use blueprint_sdk::main; +use blueprint_sdk::runners::core::runner::BlueprintRunner; +use blueprint_sdk::utils::evm::get_wallet_provider_http; use incredible_squaring_blueprint_symbiotic::{self as blueprint, IncredibleSquaringTaskManager}; - -use alloy_primitives::{address, Address}; -use gadget_sdk::utils::evm::get_wallet_provider_http; -use lazy_static::lazy_static; use std::env; // Environment variables with default values @@ -17,7 +13,7 @@ lazy_static! { .unwrap_or_else(|_| address!("0000000000000000000000000000000000000000")); } -#[gadget_sdk::main(env)] +#[main(env)] async fn main() { let operator_signer = env.keystore()?.ecdsa_key()?.alloy_key()?; let wallet = EthereumWallet::new(operator_signer); diff --git a/crates/blueprint/serde/src/de.rs b/crates/blueprint/serde/src/de.rs index ffbd6cd89..13e4e2d11 100644 --- a/crates/blueprint/serde/src/de.rs +++ b/crates/blueprint/serde/src/de.rs @@ -70,7 +70,7 @@ impl<'de> de::Deserializer<'de> for Deserializer { match self.0 { Field::String(bound_string) => string = String::from_utf8(bound_string.0 .0)?, _ => return Err(self.invalid_type(&visitor)), - }; + } let mut chars = string.chars(); let Some(ch) = chars.next() else { diff --git a/crates/clients/core/src/error.rs b/crates/clients/core/src/error.rs index 110f5a47f..0639fe2e7 100644 --- a/crates/clients/core/src/error.rs +++ b/crates/clients/core/src/error.rs @@ -1,5 +1,13 @@ #[derive(thiserror::Error, Debug)] pub enum Error { + #[error("Eigenlayer error: `{0}`")] + Eigenlayer(String), + #[error("EVM error: `{0}`")] + Evm(String), + #[error("Tangle error: `{0}`")] + Tangle(String), + #[error("Network error: `{0}`")] + Network(String), #[error("Unable to fetch operators: `{0}`")] GetOperators(String), #[error("Unable to fetch operator id: `{0}`")] diff --git a/crates/clients/eigenlayer/Cargo.toml b/crates/clients/eigenlayer/Cargo.toml index 94dd5d853..c67956278 100644 --- a/crates/clients/eigenlayer/Cargo.toml +++ b/crates/clients/eigenlayer/Cargo.toml @@ -10,6 +10,7 @@ repository.workspace = true [dependencies] gadget-config = { workspace = true, features = ["eigenlayer"] } +gadget-client-core = { workspace = true } gadget-std = { workspace = true } gadget-utils-evm = { workspace = true } alloy-contract = { workspace = true } @@ -48,5 +49,6 @@ std = [ "gadget-std/std", "gadget-utils-evm/std", "tokio/full", + "gadget-client-core/std", # TODO: "url/std", ] \ No newline at end of file diff --git a/crates/clients/eigenlayer/src/error.rs b/crates/clients/eigenlayer/src/error.rs index 243b977cc..10ca7198e 100644 --- a/crates/clients/eigenlayer/src/error.rs +++ b/crates/clients/eigenlayer/src/error.rs @@ -33,4 +33,10 @@ impl From<&'static str> for Error { } } +impl From for gadget_client_core::error::Error { + fn from(value: Error) -> Self { + gadget_client_core::error::Error::Eigenlayer(value.to_string()) + } +} + pub type Result = gadget_std::result::Result; diff --git a/crates/clients/evm/Cargo.toml b/crates/clients/evm/Cargo.toml index 2c02621a2..7d36c990b 100644 --- a/crates/clients/evm/Cargo.toml +++ b/crates/clients/evm/Cargo.toml @@ -10,6 +10,7 @@ repository.workspace = true [dependencies] gadget-std = { workspace = true } +gadget-client-core = { workspace = true } hex = { workspace = true } serde = { workspace = true } serde_json = { workspace = true, features = ["alloc"] } @@ -47,6 +48,7 @@ default = ["std"] std = [ "gadget-logging/std", "gadget-rpc-calls/std", + "gadget-client-core/std", "gadget-std/std", "hex/std", "serde/std", diff --git a/crates/clients/evm/src/error.rs b/crates/clients/evm/src/error.rs index df41f378a..efb2e3ca3 100644 --- a/crates/clients/evm/src/error.rs +++ b/crates/clients/evm/src/error.rs @@ -15,4 +15,10 @@ pub enum Error { Abi(String), } +impl From for gadget_client_core::error::Error { + fn from(value: Error) -> Self { + gadget_client_core::error::Error::Evm(value.to_string()) + } +} + pub type Result = gadget_std::result::Result; diff --git a/crates/clients/networking/Cargo.toml b/crates/clients/networking/Cargo.toml index fe91398e4..0cdaf48d2 100644 --- a/crates/clients/networking/Cargo.toml +++ b/crates/clients/networking/Cargo.toml @@ -13,6 +13,7 @@ gadget-config = { workspace = true, features = ["networking"] } gadget-crypto = { workspace = true, features = ["k256"] } gadget-logging = { workspace = true } gadget-networking = { workspace = true, features = ["round-based-compat"] } +gadget-client-core = { workspace = true } gadget-std = { workspace = true } libp2p = { workspace = true } proc-macro2 = { workspace = true } @@ -26,6 +27,7 @@ std = [ "gadget-config/std", "gadget-crypto/std", "gadget-logging/std", + "gadget-client-core/std", "gadget-networking/std", "gadget-std/std", "serde/std", diff --git a/crates/clients/networking/src/error.rs b/crates/clients/networking/src/error.rs index 710fb67d3..8d1e18031 100644 --- a/crates/clients/networking/src/error.rs +++ b/crates/clients/networking/src/error.rs @@ -13,4 +13,10 @@ pub enum Error { Configuration(String), } +impl From for gadget_client_core::error::Error { + fn from(value: Error) -> Self { + gadget_client_core::error::Error::Network(value.to_string()) + } +} + pub type Result = gadget_std::result::Result; diff --git a/crates/clients/tangle/src/error.rs b/crates/clients/tangle/src/error.rs index d0a81c8ce..4541fcdf6 100644 --- a/crates/clients/tangle/src/error.rs +++ b/crates/clients/tangle/src/error.rs @@ -28,6 +28,12 @@ pub enum Error { Subxt(#[from] subxt::Error), } +impl From for gadget_client_core::error::Error { + fn from(value: Error) -> Self { + gadget_client_core::error::Error::Tangle(value.to_string()) + } +} + pub type Result = gadget_std::result::Result; #[derive(Debug)] diff --git a/crates/macros/blueprint-proc-macro/src/lib.rs b/crates/macros/blueprint-proc-macro/src/lib.rs index 59abcfdd3..879cac8ce 100644 --- a/crates/macros/blueprint-proc-macro/src/lib.rs +++ b/crates/macros/blueprint-proc-macro/src/lib.rs @@ -38,7 +38,7 @@ mod sdk_main; /// - `id`: The unique identifier for the job (must be in the range of 0..[`u8::MAX`]) /// - `params`: The parameters of the job function, must be a tuple of identifiers in the function signature. /// - `result`: The result of the job function, must be a type that this job returns. -/// also, it can be omitted if the return type is simple to infer, like `u32` or `Vec` just use `_`. +/// also, it can be omitted if the return type is simple to infer, like `u32` or `Vec` just use `_`. /// - `skip_codegen`: A flag to skip the code generation for the job, useful for manual event handling. #[proc_macro_attribute] pub fn job(args: TokenStream, input: TokenStream) -> TokenStream { @@ -64,7 +64,7 @@ pub fn job(args: TokenStream, input: TokenStream) -> TokenStream { /// - `id`: The unique identifier for the report (must be in the range of 0..[`u8::MAX`]) /// - `params`: The parameters of the report function, must be a tuple of identifiers in the function signature. /// - `result`: The result of the report function, must be a type that this report returns. -/// It can be omitted if the return type is simple to infer, like `u32` or `Vec` by using `_`. +/// It can be omitted if the return type is simple to infer, like `u32` or `Vec` by using `_`. /// - `skip_codegen`: A flag to skip the code generation for the report, useful for manual event handling. #[proc_macro_attribute] pub fn report(args: TokenStream, input: TokenStream) -> TokenStream { diff --git a/crates/networking/src/networking.rs b/crates/networking/src/networking.rs index 18eaa8409..e14f28678 100644 --- a/crates/networking/src/networking.rs +++ b/crates/networking/src/networking.rs @@ -593,6 +593,7 @@ mod tests { use gadget_crypto::KeyType; use gadget_logging::setup_log; use gadget_std::collections::BTreeMap; + use gadget_std::sync::LazyLock; use serde::{Deserialize, Serialize}; use tokio::time::sleep; @@ -910,10 +911,11 @@ mod tests { node_with_id().0 } - lazy_static::lazy_static! { - static ref NODE_COUNT: usize = std::env::var("IN_CI").map_or_else(|_| 10, |_| 2); - static ref MESSAGE_COUNT: usize = std::env::var("IN_CI").map_or_else(|_| 10, |_| 100); - } + static NODE_COUNT: LazyLock = + LazyLock::new(|| std::env::var("IN_CI").map_or_else(|_| 10, |_| 2)); + #[allow(dead_code)] + static MESSAGE_COUNT: LazyLock = + LazyLock::new(|| std::env::var("IN_CI").map_or_else(|_| 10, |_| 100)); #[tokio::test(flavor = "multi_thread")] #[allow(clippy::cast_possible_truncation)] diff --git a/crates/sdk/Cargo.toml b/crates/sdk/Cargo.toml index 8eb735bcf..b440919d2 100644 --- a/crates/sdk/Cargo.toml +++ b/crates/sdk/Cargo.toml @@ -154,4 +154,6 @@ networking-ed25519 = [ local-store = ["gadget-stores/local"] -round-based-compat = ["gadget-networking/round-based-compat"] \ No newline at end of file +round-based-compat = ["gadget-networking/round-based-compat"] + +cronjob = ["gadget-event-listeners/cronjob"] \ No newline at end of file diff --git a/crates/sdk/src/error.rs b/crates/sdk/src/error.rs index 33eb49193..d470ea594 100644 --- a/crates/sdk/src/error.rs +++ b/crates/sdk/src/error.rs @@ -56,6 +56,21 @@ pub enum AlloyError { LocalSigner(#[from] alloy::signers::local::LocalSignerError), } +// Two-layer Client conversions +macro_rules! implement_client_error { + ($feature:literal, $client_type:path) => { + #[cfg(feature = $feature)] + impl From<$client_type> for Error { + fn from(value: $client_type) -> Self { + Error::Client(value.into()) + } + } + }; +} +implement_client_error!("eigenlayer", gadget_clients::eigenlayer::error::Error); +implement_client_error!("evm", gadget_clients::evm::error::Error); +implement_client_error!("tangle", gadget_clients::tangle::error::Error); + #[cfg(any(feature = "evm", feature = "eigenlayer"))] macro_rules! implement_from_alloy_error { ($($path:ident)::+, $variant:ident) => { @@ -66,7 +81,6 @@ macro_rules! implement_from_alloy_error { } }; } - #[cfg(any(feature = "evm", feature = "eigenlayer"))] implement_from_alloy_error!(signers::Error, Signer); #[cfg(any(feature = "evm", feature = "eigenlayer"))] diff --git a/rust-toolchain.toml b/rust-toolchain.toml index bf56dc4aa..0c7e40fbe 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -2,6 +2,6 @@ # Currently we are using this specific nightly version since we rely on the # rustdoc API which is not yet stabilized. We will keep updating this version # as we go along. -channel = "nightly-2025-01-22" +channel = "nightly-2025-01-30" components = ["rustfmt", "clippy", "rust-src"] profile = "minimal"