bittensor_rs/extrinsics/
subnet.rs

1//! # Subnet Extrinsics
2//!
3//! Extrinsics for managing subnets on the Bittensor network:
4//! - `register_network`: Register a new subnet
5//! - `register_network_with_identity`: Register with identity info
6//! - `set_subnet_identity`: Update subnet identity
7
8use crate::api::api;
9use crate::error::BittensorError;
10use crate::extrinsics::ExtrinsicResponse;
11use subxt::OnlineClient;
12use subxt::PolkadotConfig;
13use tracing::{debug, warn};
14
15/// Subnet identity information
16#[derive(Debug, Clone, Default)]
17pub struct SubnetIdentity {
18    /// Subnet name
19    pub name: String,
20    /// GitHub repository URL
21    pub github_repo: String,
22    /// Contact email
23    pub contact: String,
24    /// Subnet description
25    pub description: String,
26    /// Subnet URL
27    pub url: String,
28    /// Discord invite
29    pub discord: String,
30    /// Logo URL
31    pub logo_url: String,
32    /// Additional info
33    pub additional: String,
34}
35
36impl SubnetIdentity {
37    /// Create a new subnet identity with just a name
38    ///
39    /// # Example
40    ///
41    /// ```
42    /// use bittensor_rs::extrinsics::SubnetIdentity;
43    ///
44    /// let identity = SubnetIdentity::new("My Subnet");
45    /// assert_eq!(identity.name, "My Subnet");
46    /// ```
47    pub fn new(name: impl Into<String>) -> Self {
48        Self {
49            name: name.into(),
50            ..Default::default()
51        }
52    }
53
54    /// Set the GitHub repository URL
55    pub fn with_github(mut self, repo: impl Into<String>) -> Self {
56        self.github_repo = repo.into();
57        self
58    }
59
60    /// Set the contact email
61    pub fn with_contact(mut self, contact: impl Into<String>) -> Self {
62        self.contact = contact.into();
63        self
64    }
65
66    /// Set the description
67    pub fn with_description(mut self, desc: impl Into<String>) -> Self {
68        self.description = desc.into();
69        self
70    }
71
72    /// Set the URL
73    pub fn with_url(mut self, url: impl Into<String>) -> Self {
74        self.url = url.into();
75        self
76    }
77
78    /// Set the Discord invite
79    pub fn with_discord(mut self, discord: impl Into<String>) -> Self {
80        self.discord = discord.into();
81        self
82    }
83
84    /// Set the logo URL
85    pub fn with_logo(mut self, logo: impl Into<String>) -> Self {
86        self.logo_url = logo.into();
87        self
88    }
89}
90
91/// Register a new subnet on the network
92///
93/// This creates a new subnet by paying the registration cost.
94/// The subnet netuid is returned on success by parsing the `NetworkAdded` event.
95///
96/// # Arguments
97///
98/// * `client` - The subxt client
99/// * `signer` - The signer (coldkey)
100///
101/// # Returns
102///
103/// The newly registered subnet netuid extracted from the NetworkAdded event
104///
105/// # Errors
106///
107/// Returns an error if:
108/// - Transaction submission fails
109/// - Transaction is not finalized successfully
110/// - NetworkAdded event is not found in the transaction events
111pub async fn register_network<S>(
112    client: &OnlineClient<PolkadotConfig>,
113    signer: &S,
114) -> Result<ExtrinsicResponse<u16>, BittensorError>
115where
116    S: subxt::tx::Signer<PolkadotConfig>,
117{
118    let call = api::tx()
119        .subtensor_module()
120        .register_network(signer.account_id());
121
122    debug!("Submitting register_network transaction");
123
124    // Submit and watch the transaction to get events
125    let tx_progress = client
126        .tx()
127        .sign_and_submit_then_watch_default(&call, signer)
128        .await
129        .map_err(|e| BittensorError::TxSubmissionError {
130            message: format!("Failed to submit register_network: {}", e),
131        })?;
132
133    let tx_hash = tx_progress.extrinsic_hash();
134    debug!("Transaction submitted with hash: {:?}", tx_hash);
135
136    // Wait for finalization and get events
137    let tx_events = tx_progress
138        .wait_for_finalized_success()
139        .await
140        .map_err(|e| {
141            warn!("Transaction finalization failed: {}", e);
142            BittensorError::TxFinalizationError {
143                reason: format!("register_network transaction failed: {}", e),
144            }
145        })?;
146
147    debug!("Transaction finalized successfully");
148
149    // Find the NetworkAdded event to extract the netuid
150    // NetworkAdded event has format: NetworkAdded(netuid: u16, modality: u16)
151    let network_added_event = tx_events
152        .find_first::<api::subtensor_module::events::NetworkAdded>()
153        .map_err(|e| {
154            warn!("Failed to decode NetworkAdded event: {}", e);
155            BittensorError::ChainError {
156                message: format!("Failed to decode NetworkAdded event: {}", e),
157            }
158        })?;
159
160    match network_added_event {
161        Some(event) => {
162            let netuid = event.0;
163            debug!(
164                "NetworkAdded event found: netuid={}, modality={}",
165                netuid, event.1
166            );
167            Ok(ExtrinsicResponse::success()
168                .with_message("Network registered successfully")
169                .with_extrinsic_hash(&format!("{:?}", tx_hash))
170                .with_data(netuid))
171        }
172        None => {
173            warn!("NetworkAdded event not found in transaction events");
174            // Log all events for debugging
175            for event in tx_events.iter().flatten() {
176                debug!(
177                    "Event found: {}::{}",
178                    event.pallet_name(),
179                    event.variant_name()
180                );
181            }
182            Err(BittensorError::ChainError {
183                message: "NetworkAdded event not found - network may not have been registered"
184                    .to_string(),
185            })
186        }
187    }
188}
189
190/// Register a new subnet with identity information
191///
192/// This creates a new subnet with associated metadata.
193/// The subnet netuid is returned on success by parsing the `NetworkAdded` event.
194///
195/// # Arguments
196///
197/// * `client` - The subxt client
198/// * `signer` - The signer (coldkey)
199/// * `identity` - Subnet identity information
200///
201/// # Returns
202///
203/// The newly registered subnet netuid extracted from the NetworkAdded event
204///
205/// # Errors
206///
207/// Returns an error if:
208/// - Transaction submission fails
209/// - Transaction is not finalized successfully
210/// - NetworkAdded event is not found in the transaction events
211pub async fn register_network_with_identity<S>(
212    client: &OnlineClient<PolkadotConfig>,
213    signer: &S,
214    identity: SubnetIdentity,
215) -> Result<ExtrinsicResponse<u16>, BittensorError>
216where
217    S: subxt::tx::Signer<PolkadotConfig>,
218{
219    // Save name for logging before consuming identity
220    let subnet_name = identity.name.clone();
221
222    // Convert to API type
223    let api_identity = api::runtime_types::pallet_subtensor::pallet::SubnetIdentityV3 {
224        subnet_name: identity.name.into_bytes(),
225        github_repo: identity.github_repo.into_bytes(),
226        subnet_contact: identity.contact.into_bytes(),
227        subnet_url: identity.url.into_bytes(),
228        discord: identity.discord.into_bytes(),
229        description: identity.description.into_bytes(),
230        logo_url: identity.logo_url.into_bytes(),
231        additional: identity.additional.into_bytes(),
232    };
233
234    let call = api::tx()
235        .subtensor_module()
236        .register_network_with_identity(signer.account_id(), Some(api_identity));
237
238    debug!(
239        "Submitting register_network_with_identity transaction for '{}'",
240        subnet_name
241    );
242
243    // Submit and watch the transaction to get events
244    let tx_progress = client
245        .tx()
246        .sign_and_submit_then_watch_default(&call, signer)
247        .await
248        .map_err(|e| BittensorError::TxSubmissionError {
249            message: format!("Failed to submit register_network_with_identity: {}", e),
250        })?;
251
252    let tx_hash = tx_progress.extrinsic_hash();
253    debug!("Transaction submitted with hash: {:?}", tx_hash);
254
255    // Wait for finalization and get events
256    let tx_events = tx_progress
257        .wait_for_finalized_success()
258        .await
259        .map_err(|e| {
260            warn!("Transaction finalization failed: {}", e);
261            BittensorError::TxFinalizationError {
262                reason: format!("register_network_with_identity transaction failed: {}", e),
263            }
264        })?;
265
266    debug!("Transaction finalized successfully");
267
268    // Find the NetworkAdded event to extract the netuid
269    let network_added_event = tx_events
270        .find_first::<api::subtensor_module::events::NetworkAdded>()
271        .map_err(|e| {
272            warn!("Failed to decode NetworkAdded event: {}", e);
273            BittensorError::ChainError {
274                message: format!("Failed to decode NetworkAdded event: {}", e),
275            }
276        })?;
277
278    match network_added_event {
279        Some(event) => {
280            let netuid = event.0;
281            debug!(
282                "NetworkAdded event found: netuid={}, modality={}",
283                netuid, event.1
284            );
285            Ok(ExtrinsicResponse::success()
286                .with_message("Network registered with identity successfully")
287                .with_extrinsic_hash(&format!("{:?}", tx_hash))
288                .with_data(netuid))
289        }
290        None => {
291            warn!("NetworkAdded event not found in transaction events");
292            // Log all events for debugging
293            for event in tx_events.iter().flatten() {
294                debug!(
295                    "Event found: {}::{}",
296                    event.pallet_name(),
297                    event.variant_name()
298                );
299            }
300            Err(BittensorError::ChainError {
301                message: "NetworkAdded event not found - network may not have been registered"
302                    .to_string(),
303            })
304        }
305    }
306}
307
308/// Set or update subnet identity information
309///
310/// # Arguments
311///
312/// * `client` - The subxt client
313/// * `signer` - The signer (subnet owner coldkey)
314/// * `netuid` - The subnet netuid
315/// * `identity` - New identity information
316pub async fn set_subnet_identity<S>(
317    client: &OnlineClient<PolkadotConfig>,
318    signer: &S,
319    netuid: u16,
320    identity: SubnetIdentity,
321) -> Result<ExtrinsicResponse<()>, BittensorError>
322where
323    S: subxt::tx::Signer<PolkadotConfig>,
324{
325    let call = api::tx().subtensor_module().set_subnet_identity(
326        netuid,
327        identity.name.into_bytes(),
328        identity.github_repo.into_bytes(),
329        identity.contact.into_bytes(),
330        identity.url.into_bytes(),
331        identity.discord.into_bytes(),
332        identity.description.into_bytes(),
333        identity.logo_url.into_bytes(),
334        identity.additional.into_bytes(),
335    );
336
337    let tx_hash = client
338        .tx()
339        .sign_and_submit_default(&call, signer)
340        .await
341        .map_err(|e| BittensorError::TxSubmissionError {
342            message: format!("Failed to set subnet identity: {}", e),
343        })?;
344
345    Ok(ExtrinsicResponse::success()
346        .with_message("Subnet identity updated")
347        .with_extrinsic_hash(&format!("{:?}", tx_hash))
348        .with_data(()))
349}
350
351/// Register in the root network (netuid 0)
352///
353/// This registers a hotkey in the root network for senate voting.
354///
355/// # Arguments
356///
357/// * `client` - The subxt client
358/// * `signer` - The signer (coldkey)
359/// * `hotkey` - The hotkey to register
360pub async fn root_register<S>(
361    client: &OnlineClient<PolkadotConfig>,
362    signer: &S,
363    hotkey: crate::AccountId,
364) -> Result<ExtrinsicResponse<()>, BittensorError>
365where
366    S: subxt::tx::Signer<PolkadotConfig>,
367{
368    let call = api::tx().subtensor_module().root_register(hotkey);
369
370    let tx_hash = client
371        .tx()
372        .sign_and_submit_default(&call, signer)
373        .await
374        .map_err(|e| BittensorError::TxSubmissionError {
375            message: format!("Failed to root register: {}", e),
376        })?;
377
378    Ok(ExtrinsicResponse::success()
379        .with_message("Root registration successful")
380        .with_extrinsic_hash(&format!("{:?}", tx_hash))
381        .with_data(()))
382}
383
384#[cfg(test)]
385mod tests {
386    use super::*;
387
388    #[test]
389    fn test_subnet_identity_new() {
390        let identity = SubnetIdentity::new("Test Subnet");
391        assert_eq!(identity.name, "Test Subnet");
392        assert!(identity.github_repo.is_empty());
393    }
394
395    #[test]
396    fn test_subnet_identity_builder() {
397        let identity = SubnetIdentity::new("My Subnet")
398            .with_github("https://github.com/example/subnet")
399            .with_contact("admin@example.com")
400            .with_description("A test subnet")
401            .with_url("https://example.com")
402            .with_discord("abc123")
403            .with_logo("https://example.com/logo.png");
404
405        assert_eq!(identity.name, "My Subnet");
406        assert_eq!(identity.github_repo, "https://github.com/example/subnet");
407        assert_eq!(identity.contact, "admin@example.com");
408        assert_eq!(identity.description, "A test subnet");
409        assert_eq!(identity.url, "https://example.com");
410        assert_eq!(identity.discord, "abc123");
411        assert_eq!(identity.logo_url, "https://example.com/logo.png");
412    }
413
414    #[test]
415    fn test_subnet_identity_default() {
416        let identity = SubnetIdentity::default();
417        assert!(identity.name.is_empty());
418        assert!(identity.github_repo.is_empty());
419    }
420
421    #[test]
422    fn test_subnet_identity_clone() {
423        let identity = SubnetIdentity::new("Test").with_github("https://github.com/test");
424        let cloned = identity.clone();
425        assert_eq!(identity.name, cloned.name);
426        assert_eq!(identity.github_repo, cloned.github_repo);
427    }
428
429    #[test]
430    fn test_subnet_identity_debug() {
431        let identity = SubnetIdentity::new("Test");
432        let debug = format!("{:?}", identity);
433        assert!(debug.contains("SubnetIdentity"));
434        assert!(debug.contains("Test"));
435    }
436}