Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
563 changes: 169 additions & 394 deletions Cargo.lock

Large diffs are not rendered by default.

7 changes: 5 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
[workspace]
members = ["bittensor-rs", "bittensor-wallet", "lightning-tensor"]
members = ["bittensor-rs", "lightning-tensor"]
# Note: bittensor-wallet excluded due to pyo3 linking issues
resolver = "2"

[workspace.dependencies]
clap = { version = "4.5.9", features = ["derive"] }
codec = { package = "parity-scale-codec", version = "3.6.1", default-features = false, features = [
"derive",
] }
parity-scale-codec = { version = "3.6.1", default-features = false, features = ["derive"] }
parity-scale-codec = { version = "3.6.1", default-features = false, features = [
"derive",
] }
sp-core = "34.0.0"
substrate-api-client = { version = "0.18.0", features = ["ws-client"] }
thiserror = "1.0.62"
Expand Down
5 changes: 1 addition & 4 deletions bittensor-rs/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,7 @@ fn main() {
if env::var("BITTENSOR_OFFLINE").is_ok() {
println!("cargo:warning=BITTENSOR_OFFLINE set, skipping metadata fetch");
if !code_path.exists() {
panic!(
"Offline mode but no cached code exists at {:?}",
code_path
);
panic!("Offline mode but no cached code exists at {:?}", code_path);
}
return;
}
Expand Down
5 changes: 2 additions & 3 deletions bittensor-rs/src/bin/generate-metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,15 +102,14 @@ async fn generate_metadata(network: &str, endpoint: &str) {
}

async fn fetch_metadata_bytes(url: &str) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
use subxt::backend::rpc::RpcClient;
use subxt::backend::legacy::LegacyRpcMethods;
use subxt::backend::rpc::RpcClient;
use subxt::PolkadotConfig;

// Connect via RPC and fetch raw metadata bytes
let rpc_client = RpcClient::from_insecure_url(url).await?;
let rpc = LegacyRpcMethods::<PolkadotConfig>::new(rpc_client);
let metadata_response = rpc.state_get_metadata(None).await?;
// Convert the response to bytes
Ok(metadata_response.into_raw())
}

9 changes: 5 additions & 4 deletions bittensor-rs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,10 +111,11 @@ pub use extrinsics::{

// Re-export queries
pub use queries::{
fields as metagraph_fields, get_balance, get_metagraph, get_neuron, get_neuron_lite, get_stake,
get_stake_info_for_coldkey, get_subnet_hyperparameters, get_subnet_info,
get_total_network_stake, get_total_subnets, get_uid_for_hotkey, subnet_exists, Metagraph,
NeuronInfo, NeuronInfoLite, SelectiveMetagraph, StakeInfo, SubnetHyperparameters, SubnetInfo,
fields as metagraph_fields, get_all_dynamic_info, get_balance, get_metagraph, get_neuron,
get_neuron_lite, get_stake, get_stake_info_for_coldkey, get_subnet_hyperparameters,
get_subnet_info, get_total_network_stake, get_total_subnets, get_uid_for_hotkey, subnet_exists,
DynamicSubnetInfo, Metagraph, NeuronInfo, NeuronInfoLite, SelectiveMetagraph, StakeInfo,
SubnetHyperparameters, SubnetInfo,
};

// Re-export key types from our generated API
Expand Down
4 changes: 2 additions & 2 deletions bittensor-rs/src/queries/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,6 @@ pub use account::{
pub use metagraph::{fields, get_metagraph, Metagraph, SelectiveMetagraph};
pub use neuron::{get_neuron, get_neuron_lite, get_uid_for_hotkey, NeuronInfo, NeuronInfoLite};
pub use subnet::{
get_subnet_hyperparameters, get_subnet_info, get_total_subnets, subnet_exists,
SubnetHyperparameters, SubnetInfo,
get_all_dynamic_info, get_subnet_hyperparameters, get_subnet_info, get_total_subnets,
subnet_exists, DynamicSubnetInfo, SubnetHyperparameters, SubnetInfo,
};
134 changes: 133 additions & 1 deletion bittensor-rs/src/queries/subnet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@

use crate::api::api;
use crate::error::BittensorError;
use subxt::ext::codec;
use subxt::OnlineClient;
use subxt::PolkadotConfig;

/// Subnet information
/// Subnet information (basic)
#[derive(Debug, Clone)]
pub struct SubnetInfo {
/// Subnet netuid
Expand All @@ -24,6 +25,38 @@ pub struct SubnetInfo {
pub registration_allowed: bool,
}

/// Dynamic subnet info with DTAO data (single RPC call for all subnets)
#[derive(Debug, Clone)]
pub struct DynamicSubnetInfo {
/// Subnet netuid
pub netuid: u16,
/// Subnet name (from identity or token symbol)
pub name: String,
/// Token symbol (e.g., "α", "τ")
pub symbol: String,
/// TAO emission per block (RAO)
pub tao_in_emission: u64,
/// Moving price (EMA) - used for emission weight calculation
/// Emission % = moving_price / Σ all_moving_prices × 100
pub moving_price: f64,
/// Price in TAO (tao_in / alpha_in)
pub price_tao: f64,
/// Alpha in pool (RAO)
pub alpha_in: u64,
/// Alpha out pool (RAO)
pub alpha_out: u64,
/// TAO in pool (RAO)
pub tao_in: u64,
/// Owner hotkey SS58
pub owner_hotkey: String,
/// Owner coldkey SS58
pub owner_coldkey: String,
/// Tempo (blocks per epoch)
pub tempo: u16,
/// Block number when subnet was registered
pub registered_at: u64,
}

/// Subnet hyperparameters
#[derive(Debug, Clone)]
pub struct SubnetHyperparameters {
Expand Down Expand Up @@ -311,6 +344,105 @@ pub async fn subnet_exists(
Ok(exists)
}

/// Get all subnet dynamic info in a single RPC call
///
/// This is MUCH faster than calling get_subnet_info for each subnet.
/// Returns DTAO pricing, emission, identity, and pool info for all subnets.
pub async fn get_all_dynamic_info(
client: &OnlineClient<PolkadotConfig>,
) -> Result<Vec<DynamicSubnetInfo>, BittensorError> {
let runtime_api =
client
.runtime_api()
.at_latest()
.await
.map_err(|e| BittensorError::RpcError {
message: format!("Failed to get runtime API: {}", e),
})?;

let payload = api::apis().subnet_info_runtime_api().get_all_dynamic_info();
let result = runtime_api
.call(payload)
.await
.map_err(|e| BittensorError::RpcError {
message: format!("Failed to call get_all_dynamic_info: {}", e),
})?;

let subnets: Vec<DynamicSubnetInfo> = result
.into_iter()
.flatten()
.map(|info| {
// Decode subnet name from compact bytes
let name = decode_compact_bytes(&info.subnet_name);
let symbol = decode_compact_bytes(&info.token_symbol);

// Extract identity name if available (identity uses plain Vec<u8>)
let display_name = info
.subnet_identity
.as_ref()
.and_then(|id| {
let n = String::from_utf8_lossy(&id.subnet_name).to_string();
if n.is_empty() {
None
} else {
Some(n)
}
})
.unwrap_or_else(|| name.clone());

// Calculate price from pool ratio: tao_in / alpha_in
// This is the actual exchange rate (how much TAO per 1 Alpha)
let alpha_in_f = info.alpha_in as f64 / 1_000_000_000.0; // Convert RAO to TAO
let tao_in_f = info.tao_in as f64 / 1_000_000_000.0;

let price_tao = if info.netuid == 0 {
1.0 // Root subnet always 1:1
} else if alpha_in_f > 0.0 {
tao_in_f / alpha_in_f
} else {
0.0
};

// Convert FixedI128<U32> to f64
// The type parameter from metadata is U32 (32 fractional bits)
// moving_price = bits / 2^32
let moving_price = (info.moving_price.bits as f64) / ((1u64 << 32) as f64);

DynamicSubnetInfo {
netuid: info.netuid,
name: if display_name.is_empty() {
format!("SN{}", info.netuid)
} else {
display_name
},
symbol: if symbol.is_empty() {
"α".to_string()
} else {
symbol
},
tao_in_emission: info.tao_in_emission,
moving_price,
price_tao,
alpha_in: info.alpha_in,
alpha_out: info.alpha_out,
tao_in: info.tao_in,
owner_hotkey: format!("{}", info.owner_hotkey),
owner_coldkey: format!("{}", info.owner_coldkey),
tempo: info.tempo,
registered_at: info.network_registered_at,
}
})
.collect();

Ok(subnets)
}

/// Decode compact bytes (Vec<Compact<u8>>) to String
fn decode_compact_bytes(bytes: &[codec::Compact<u8>]) -> String {
let raw: Vec<u8> = bytes.iter().map(|c| c.0).collect();
String::from_utf8_lossy(&raw).to_string()
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
36 changes: 35 additions & 1 deletion bittensor-rs/src/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ fn load_key_seed(path: &PathBuf) -> Result<String, Box<dyn std::error::Error>> {

fn signer_from_seed(seed: &str) -> Result<Keypair, Box<dyn std::error::Error + Send + Sync>> {
use subxt_signer::SecretUri;

// Parse the seed as a SecretUri (handles mnemonic, hex seeds, etc.)
let uri: SecretUri = seed.parse()?;
let keypair = Keypair::from_uri(&uri)?;
Expand Down Expand Up @@ -773,6 +773,40 @@ impl Service {
self.config.netuid
}

/// Get a healthy client from the connection pool for direct queries.
///
/// This provides access to the underlying chain client for making
/// custom queries not directly supported by the Service API.
///
/// # Returns
///
/// * `Result<Arc<OnlineClient<PolkadotConfig>>, BittensorError>` - The chain client
///
/// # Example
///
/// ```rust,no_run
/// # use bittensor::Service;
/// # use bittensor::config::BittensorConfig;
/// # #[tokio::main]
/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
/// # let config = BittensorConfig::default();
/// # let service = Service::new(config).await?;
/// let client = service.client().await?;
/// // Use client for custom queries
/// # Ok(())
/// # }
/// ```
pub async fn client(
&self,
) -> Result<std::sync::Arc<subxt::OnlineClient<subxt::PolkadotConfig>>, BittensorError> {
self.connection_pool
.get_healthy_client()
.await
.map_err(|e| BittensorError::NetworkError {
message: format!("Failed to get healthy client: {}", e),
})
}

/// Sign data with the service's signer (hotkey)
///
/// This method signs arbitrary data with the validator/miner's hotkey.
Expand Down
2 changes: 1 addition & 1 deletion bittensor-rs/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
use crate::error::BittensorError;
use crate::types::Hotkey;
use crate::AccountId;
use std::str::FromStr;
use sp_core::{sr25519, Pair};
use std::str::FromStr;

// Weight-related types

Expand Down
2 changes: 1 addition & 1 deletion bittensor-rs/src/wallet/keyfile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@

use crate::error::BittensorError;
use serde::{Deserialize, Serialize};
use std::path::Path;
use sp_core::{sr25519, Pair};
use std::path::Path;
use thiserror::Error;

/// Errors that can occur when loading keyfiles
Expand Down
2 changes: 1 addition & 1 deletion bittensor-rs/src/wallet/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ pub use signer::WalletSigner;
use crate::error::BittensorError;
use crate::types::Hotkey;
use crate::AccountId;
use std::path::{Path, PathBuf};
use sp_core::{sr25519, Pair};
use std::path::{Path, PathBuf};

/// Bittensor wallet for managing keys and signing transactions
///
Expand Down
7 changes: 3 additions & 4 deletions bittensor-rs/src/wallet/signer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,15 @@ impl WalletSigner {
// Get the seed bytes from the pair by converting to raw bytes
// The secret key is the first 64 bytes (32 bytes key + 32 bytes nonce)
let seed = pair.to_raw_vec();
let keypair = Keypair::from_secret_key(seed[..32].try_into().unwrap())
.expect("Valid 32-byte seed");
let keypair =
Keypair::from_secret_key(seed[..32].try_into().unwrap()).expect("Valid 32-byte seed");
Self { inner: keypair }
}

/// Create a signer from a seed phrase (mnemonic or hex seed)
pub fn from_seed(seed: &str) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
use subxt_signer::SecretUri;

let uri: SecretUri = seed.parse()?;
let keypair = Keypair::from_uri(&uri)?;
Ok(Self { inner: keypair })
Expand All @@ -69,7 +69,6 @@ impl std::fmt::Debug for WalletSigner {
}
}


#[cfg(test)]
mod tests {
use super::*;
Expand Down
Loading