From 5155b7592ddcd7cef7150d4ae6c5e582db2cd8ad Mon Sep 17 00:00:00 2001 From: Evgeny Svirsky Date: Wed, 29 Apr 2026 13:29:22 +0200 Subject: [PATCH 1/3] Added eco-tests for indexer + CI job to notify the indexer team --- .../workflows/eco-tests-indexer-notify.yml | 140 +++++++++++++ eco-tests/src/lib.rs | 3 + eco-tests/src/tests_taocom_indexer.rs | 189 ++++++++++++++++++ 3 files changed, 332 insertions(+) create mode 100644 .github/workflows/eco-tests-indexer-notify.yml create mode 100644 eco-tests/src/tests_taocom_indexer.rs diff --git a/.github/workflows/eco-tests-indexer-notify.yml b/.github/workflows/eco-tests-indexer-notify.yml new file mode 100644 index 0000000000..ea6b0ae46b --- /dev/null +++ b/.github/workflows/eco-tests-indexer-notify.yml @@ -0,0 +1,140 @@ +name: on eco-tests change notification + +on: + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + paths: + - 'eco-tests/**' + +permissions: + contents: read + pull-requests: write + issues: write + +concurrency: + group: eco-tests-indexer-notify-${{ github.ref }} + cancel-in-progress: true + +env: + ECO_TESTS_REVIEWERS: "evgeny-s" + +jobs: + notify: + name: Notify indexer reviewer + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: List changed files under eco-tests/ + id: changes + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + run: | + set -euo pipefail + changed=$(git diff --name-only "$BASE_SHA" "$HEAD_SHA" -- 'eco-tests/' || true) + { + echo "files<> "$GITHUB_OUTPUT" + + - name: Post or update sticky review-request comment + if: steps.changes.outputs.files != '' + uses: actions/github-script@v7 + env: + CHANGED_FILES: ${{ steps.changes.outputs.files }} + REVIEWERS: ${{ env.ECO_TESTS_REVIEWERS }} + with: + script: | + const marker = ''; + + const reviewers = (process.env.REVIEWERS || '') + .split(',') + .map(s => s.trim()) + .filter(Boolean); + const ccLine = reviewers.length + ? reviewers.map(u => `@${u}`).join(' ') + : '_(no reviewers configured — set ECO_TESTS_REVIEWERS in the workflow)_'; + + const changed = (process.env.CHANGED_FILES || '').trim(); + const fileList = changed + .split('\n') + .filter(Boolean) + .map(f => `- \`${f}\``) + .join('\n'); + + const body = [ + marker, + '### eco-tests changed — indexer review required', + '', + 'This PR modifies files under `eco-tests/`. and may affect downstream indexing.', + `**cc ${ccLine}** — please review manually', + '', + '
Changed files', + '', + fileList, + '', + '
', + ].join('\n'); + + const { owner, repo } = context.repo; + const issue_number = context.issue.number; + + const comments = await github.paginate( + github.rest.issues.listComments, + { owner, repo, issue_number, per_page: 100 } + ); + const existing = comments.find(c => c.body && c.body.includes(marker)); + + if (existing) { + if (existing.body !== body) { + await github.rest.issues.updateComment({ + owner, repo, comment_id: existing.id, body, + }); + } + } else { + await github.rest.issues.createComment({ + owner, repo, issue_number, body, + }); + } + + - name: Request reviews from configured reviewers + if: steps.changes.outputs.files != '' + uses: actions/github-script@v7 + env: + REVIEWERS: ${{ env.ECO_TESTS_REVIEWERS }} + with: + script: | + const reviewers = (process.env.REVIEWERS || '') + .split(',') + .map(s => s.trim()) + .filter(Boolean); + if (reviewers.length === 0) { + core.info('ECO_TESTS_REVIEWERS is empty — skipping review request.'); + return; + } + + const { owner, repo } = context.repo; + const pull_number = context.issue.number; + const pr = await github.rest.pulls.get({ owner, repo, pull_number }); + + // GitHub rejects requesting a review from the PR author. + const author = pr.data.user && pr.data.user.login; + const filtered = reviewers.filter(u => u !== author); + if (filtered.length === 0) { + core.info(`All configured reviewers are the PR author (${author}) — skipping.`); + return; + } + + try { + await github.rest.pulls.requestReviewers({ + owner, repo, pull_number, + reviewers: filtered, + }); + } catch (e) { + core.warning(`requestReviewers failed: ${e.message}`); + } diff --git a/eco-tests/src/lib.rs b/eco-tests/src/lib.rs index d7d60aca2b..98c003e742 100644 --- a/eco-tests/src/lib.rs +++ b/eco-tests/src/lib.rs @@ -4,3 +4,6 @@ mod helpers; mod mock; #[cfg(test)] mod tests; + +#[cfg(test)] +mod tests_taocom_indexer; \ No newline at end of file diff --git a/eco-tests/src/tests_taocom_indexer.rs b/eco-tests/src/tests_taocom_indexer.rs new file mode 100644 index 0000000000..9258f18102 --- /dev/null +++ b/eco-tests/src/tests_taocom_indexer.rs @@ -0,0 +1,189 @@ +//! Indexer-contract tests for the TAO.com / ecosystem indexer. +//! Any modification in these tests will notify the member responsible +//! for the communication between protocol and the indexer team. + +#![allow(clippy::unwrap_used)] +#![allow(clippy::arithmetic_side_effects)] + +use frame_support::traits::OnInitialize; +use pallet_subtensor::*; +use pallet_subtensor_swap as swap; +use sp_core::U256; +use subtensor_runtime_common::{MechId, NetUid, NetUidStorageIndex, TaoBalance}; + +use super::helpers::*; +use super::mock::*; + +#[test] +fn indexer_neuron_per_subnet_vectors() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1u16); + let netuid_idx = NetUidStorageIndex::from(netuid); + + let _: Vec = Active::::get(netuid); + let _: Vec = Consensus::::get(netuid); + let _: Vec = Dividends::::get(netuid); + let _: Vec = Incentive::::get(netuid_idx); + let _: Vec = LastUpdate::::get(netuid_idx); + let _: Vec = ValidatorPermit::::get(netuid); + let _: Vec = ValidatorTrust::::get(netuid); + let _ = Emission::::get(netuid); + }); +} + +#[test] +fn indexer_neuron_uid_maps() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1u16); + let netuid_idx = NetUidStorageIndex::from(netuid); + let hotkey = U256::from(1); + let uid: u16 = 0; + + let _: Option = Uids::::get(netuid, hotkey); + let _: U256 = Keys::::get(netuid, uid); + let _: Vec<(u16, u16)> = Weights::::get(netuid_idx, uid); + let _: Vec<(u16, u16)> = Bonds::::get(netuid_idx, uid); + let _: Option = Axons::::get(netuid, hotkey); + }); +} + +#[test] +fn indexer_ownership_and_childkey_graph() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1u16); + let key = U256::from(42); + + let _: U256 = Owner::::get(key); + let _: U256 = SubnetOwner::::get(netuid); + let _: U256 = SubnetOwnerHotkey::::get(netuid); + let _: Vec<(u64, U256)> = ChildKeys::::get(key, netuid); + let _: Vec<(u64, U256)> = ParentKeys::::get(key, netuid); + }); +} + +#[test] +fn indexer_stake_and_alpha_shares() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1u16); + let hotkey = U256::from(1); + let coldkey = U256::from(2); + + let _ = TotalHotkeyAlpha::::get(hotkey, netuid); + let _ = TotalHotkeyShares::::get(hotkey, netuid); + let _ = TotalHotkeySharesV2::::get(hotkey, netuid); + let _ = Alpha::::get((hotkey, coldkey, netuid)); + let _ = AlphaV2::::get((hotkey, coldkey, netuid)); + }); +} + +#[test] +fn indexer_subnet_metadata() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1u16); + let coldkey = U256::from(7); + + let _: u16 = TotalNetworks::::get(); + let _: Vec = TokenSymbol::::get(netuid); + let _ = IdentitiesV2::::get(coldkey); + let _ = SubnetIdentitiesV3::::get(netuid); + let _: MechId = MechanismCountCurrent::::get(netuid); + let _: Option = FirstEmissionBlockNumber::::get(netuid); + }); +} + +#[test] +fn indexer_subnet_pool_and_emissions() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1u16); + + let _ = SubnetMovingPrice::::get(netuid); + let _: u128 = SubnetVolume::::get(netuid); + let _ = SubnetTAO::::get(netuid); + let _ = SubnetAlphaIn::::get(netuid); + let _ = SubnetAlphaOut::::get(netuid); + let _ = SubnetTaoInEmission::::get(netuid); + let _ = SubnetAlphaInEmission::::get(netuid); + let _ = SubnetAlphaOutEmission::::get(netuid); + let _ = PendingValidatorEmission::::get(netuid); + let _ = PendingServerEmission::::get(netuid); + + let _ = swap::AlphaSqrtPrice::::get(netuid); + }); +} + +#[test] +fn indexer_subnet_hyperparams() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1u16); + + let _: u16 = Rho::::get(netuid); + let _: u16 = Kappa::::get(netuid); + let _: u16 = ImmunityPeriod::::get(netuid); + let _: u16 = MinAllowedWeights::::get(netuid); + let _: u16 = MaxWeightsLimit::::get(netuid); + let _: u16 = Tempo::::get(netuid); + let _: u64 = MinDifficulty::::get(netuid); + let _: u64 = MaxDifficulty::::get(netuid); + let _: u64 = WeightsVersionKey::::get(netuid); + let _: u64 = WeightsSetRateLimit::::get(netuid); + let _: u16 = AdjustmentInterval::::get(netuid); + let _: u16 = ActivityCutoff::::get(netuid); + let _: bool = NetworkRegistrationAllowed::::get(netuid); + let _: u16 = TargetRegistrationsPerInterval::::get(netuid); + let _ = MinBurn::::get(netuid); + let _ = MaxBurn::::get(netuid); + let _: u64 = BondsMovingAverage::::get(netuid); + let _: u16 = MaxRegistrationsPerBlock::::get(netuid); + let _: u64 = ServingRateLimit::::get(netuid); + let _: u16 = MaxAllowedValidators::::get(netuid); + let _: u64 = Difficulty::::get(netuid); + let _ = AdjustmentAlpha::::get(netuid); + let _: u64 = RevealPeriodEpochs::::get(netuid); + let _: bool = CommitRevealWeightsEnabled::::get(netuid); + let _: bool = LiquidAlphaOn::::get(netuid); + let _: i16 = AlphaSigmoidSteepness::::get(netuid); + let _: bool = Yuma3On::::get(netuid); + let _: bool = BondsResetOn::::get(netuid); + let _: (u16, u16) = AlphaValues::::get(netuid); + let _: RecycleOrBurnEnum = RecycleOrBurn::::get(netuid); + }); +} + +#[test] +fn indexer_step_and_toggles() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1u16); + + let _: u64 = BlocksSinceLastStep::::get(netuid); + let _: u64 = LastMechansimStepBlock::::get(netuid); + let _ = LastRateLimitedBlock::::iter().next(); + let _: bool = TransferToggle::::get(netuid); + let _: bool = swap::EnabledUserLiquidity::::get(netuid); + }); +} + +#[test] +fn indexer_network_economics() { + new_test_ext(1).execute_with(|| { + let _: TaoBalance = NetworkMinLockCost::::get(); + let _: TaoBalance = NetworkLastLockCost::::get(); + let _: u64 = NetworkLockReductionInterval::::get(); + let _: TaoBalance = TotalIssuance::::get(); + }); +} + +#[test] +fn indexer_runtime_api_signatures() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1u16); + let coldkey = U256::from(3); + let hotkey = U256::from(4); + + let _ = SubtensorModule::get_delegate(hotkey); + + let _ = SubtensorModule::get_stake_info_for_coldkeys(vec![coldkey]); + + use subtensor_swap_interface::SwapHandler; + let _ = ::SwapInterface::current_alpha_price(netuid); + }); +} From 9a17492e90aeb63cb73daf76d21d2512e1d6c19f Mon Sep 17 00:00:00 2001 From: Evgeny Svirsky Date: Wed, 29 Apr 2026 13:36:54 +0200 Subject: [PATCH 2/3] typo --- .github/workflows/eco-tests-indexer-notify.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/eco-tests-indexer-notify.yml b/.github/workflows/eco-tests-indexer-notify.yml index ea6b0ae46b..e55bbdf859 100644 --- a/.github/workflows/eco-tests-indexer-notify.yml +++ b/.github/workflows/eco-tests-indexer-notify.yml @@ -72,7 +72,7 @@ jobs: '### eco-tests changed — indexer review required', '', 'This PR modifies files under `eco-tests/`. and may affect downstream indexing.', - `**cc ${ccLine}** — please review manually', + `**cc ${ccLine}** — please review manually`, '', '
Changed files', '', From 2a2d66448fccdd9998e374e3d989f6f31cd6be1a Mon Sep 17 00:00:00 2001 From: Evgeny Svirsky Date: Thu, 30 Apr 2026 15:14:00 +0200 Subject: [PATCH 3/3] - address pr comments --- eco-tests/Cargo.toml | 4 ++ eco-tests/src/mock.rs | 81 ++++++++++++++++++++++++++- eco-tests/src/tests_taocom_indexer.rs | 39 +++++++------ 3 files changed, 107 insertions(+), 17 deletions(-) diff --git a/eco-tests/Cargo.toml b/eco-tests/Cargo.toml index 8884810fd6..bfe22d2072 100644 --- a/eco-tests/Cargo.toml +++ b/eco-tests/Cargo.toml @@ -32,6 +32,9 @@ pallet-scheduler = { git = "https://github.com/opentensor/polkadot-sdk.git", rev pallet-preimage = { git = "https://github.com/opentensor/polkadot-sdk.git", rev = "7cc54bf2d50ae3921d718736dfeb0de9468539c7", default-features = false, features = ["std"] } pallet-drand = { path = "../pallets/drand", default-features = false, features = ["std"] } pallet-subtensor-swap = { path = "../pallets/swap", default-features = false, features = ["std"] } +pallet-subtensor-swap-runtime-api = { path = "../pallets/swap/runtime-api", default-features = false, features = ["std"] } +subtensor-custom-rpc-runtime-api = { path = "../pallets/subtensor/runtime-api", default-features = false, features = ["std"] } +sp-api = { git = "https://github.com/opentensor/polkadot-sdk.git", rev = "7cc54bf2d50ae3921d718736dfeb0de9468539c7", default-features = false, features = ["std"] } pallet-crowdloan = { path = "../pallets/crowdloan", default-features = false, features = ["std"] } pallet-subtensor-proxy = { path = "../pallets/proxy", default-features = false, features = ["std"] } pallet-subtensor-utility = { path = "../pallets/utility", default-features = false, features = ["std"] } @@ -39,6 +42,7 @@ pallet-shield = { path = "../pallets/shield", default-features = false, features subtensor-runtime-common = { path = "../common", default-features = false, features = ["std"] } subtensor-swap-interface = { path = "../pallets/swap-interface", default-features = false, features = ["std"] } share-pool = { path = "../primitives/share-pool", default-features = false, features = ["std"] } +substrate-fixed = { git = "https://github.com/encointer/substrate-fixed.git", tag = "v0.6.0", default-features = false, features = ["std"] } safe-math = { path = "../primitives/safe-math", default-features = false, features = ["std"] } log = { version = "0.4.21", default-features = false, features = ["std"] } approx = "0.5" diff --git a/eco-tests/src/mock.rs b/eco-tests/src/mock.rs index 3188854dfa..60cb2df054 100644 --- a/eco-tests/src/mock.rs +++ b/eco-tests/src/mock.rs @@ -28,7 +28,8 @@ use sp_std::{cell::RefCell, cmp::Ordering, sync::OnceLock}; use sp_tracing::tracing_subscriber; use subtensor_runtime_common::{AuthorshipInfo, NetUid, TaoBalance}; use tracing_subscriber::{EnvFilter, layer::SubscriberExt, util::SubscriberInitExt}; -type Block = frame_system::mocking::MockBlock; +pub type Block = frame_system::mocking::MockBlock; +pub use api_mocks::MockApi; // Configure a mock runtime to test the pallet. frame_support::construct_runtime!( @@ -600,3 +601,81 @@ pub fn add_balance_to_coldkey_account(coldkey: &U256, tao: TaoBalance) { let credit = SubtensorModule::mint_tao(tao); let _ = SubtensorModule::spend_tao(coldkey, credit, tao).unwrap(); } + +mod api_mocks { + use codec::Compact; + use pallet_subtensor::rpc_info::delegate_info::DelegateInfo; + use pallet_subtensor::rpc_info::stake_info::StakeInfo; + use pallet_subtensor_swap_runtime_api::{SimSwapResult, SubnetPrice, SwapRuntimeApi}; + use sp_runtime::AccountId32; + use subtensor_custom_rpc_runtime_api::{DelegateInfoRuntimeApi, StakeInfoRuntimeApi}; + use subtensor_runtime_common::{AlphaBalance, NetUid, TaoBalance}; + + use super::Block; + + pub struct MockApi; + + sp_api::mock_impl_runtime_apis! { + impl DelegateInfoRuntimeApi for MockApi { + fn get_delegates() -> Vec> { Vec::new() } + fn get_delegate(_delegate_account: AccountId32) -> Option> { None } + fn get_delegated( + _delegatee_account: AccountId32, + ) -> Vec<(DelegateInfo, (Compact, Compact))> { + Vec::new() + } + } + + impl StakeInfoRuntimeApi for MockApi { + fn get_stake_info_for_coldkey(_coldkey_account: AccountId32) -> Vec> { + Vec::new() + } + fn get_stake_info_for_coldkeys( + _coldkey_accounts: Vec, + ) -> Vec<(AccountId32, Vec>)> { + Vec::new() + } + fn get_stake_info_for_hotkey_coldkey_netuid( + _hotkey_account: AccountId32, + _coldkey_account: AccountId32, + _netuid: NetUid, + ) -> Option> { + None + } + fn get_stake_fee( + _origin: Option<(AccountId32, NetUid)>, + _origin_coldkey_account: AccountId32, + _destination: Option<(AccountId32, NetUid)>, + _destination_coldkey_account: AccountId32, + _amount: u64, + ) -> u64 { + 0 + } + } + + impl SwapRuntimeApi for MockApi { + fn current_alpha_price(_netuid: NetUid) -> u64 { 0 } + fn current_alpha_price_all() -> Vec { Vec::new() } + fn sim_swap_tao_for_alpha(_netuid: NetUid, _tao: TaoBalance) -> SimSwapResult { + SimSwapResult { + tao_amount: 0u64.into(), + alpha_amount: 0u64.into(), + tao_fee: 0u64.into(), + alpha_fee: 0u64.into(), + tao_slippage: 0u64.into(), + alpha_slippage: 0u64.into(), + } + } + fn sim_swap_alpha_for_tao(_netuid: NetUid, _alpha: AlphaBalance) -> SimSwapResult { + SimSwapResult { + tao_amount: 0u64.into(), + alpha_amount: 0u64.into(), + tao_fee: 0u64.into(), + alpha_fee: 0u64.into(), + tao_slippage: 0u64.into(), + alpha_slippage: 0u64.into(), + } + } + } + } +} diff --git a/eco-tests/src/tests_taocom_indexer.rs b/eco-tests/src/tests_taocom_indexer.rs index 9258f18102..c0cf585920 100644 --- a/eco-tests/src/tests_taocom_indexer.rs +++ b/eco-tests/src/tests_taocom_indexer.rs @@ -5,11 +5,18 @@ #![allow(clippy::unwrap_used)] #![allow(clippy::arithmetic_side_effects)] -use frame_support::traits::OnInitialize; use pallet_subtensor::*; use pallet_subtensor_swap as swap; +use share_pool::SafeFloat; use sp_core::U256; -use subtensor_runtime_common::{MechId, NetUid, NetUidStorageIndex, TaoBalance}; +use substrate_fixed::types::U64F64; +use subtensor_runtime_common::{AlphaBalance, MechId, NetUid, NetUidStorageIndex, TaoBalance}; +use pallet_subtensor::rpc_info::delegate_info::DelegateInfo; +use pallet_subtensor::rpc_info::stake_info::StakeInfo; +use pallet_subtensor_swap_runtime_api::SwapRuntimeApi; +use sp_runtime::AccountId32; +use sp_runtime::traits::Block as BlockT; +use subtensor_custom_rpc_runtime_api::{DelegateInfoRuntimeApi, StakeInfoRuntimeApi}; use super::helpers::*; use super::mock::*; @@ -68,11 +75,11 @@ fn indexer_stake_and_alpha_shares() { let hotkey = U256::from(1); let coldkey = U256::from(2); - let _ = TotalHotkeyAlpha::::get(hotkey, netuid); - let _ = TotalHotkeyShares::::get(hotkey, netuid); - let _ = TotalHotkeySharesV2::::get(hotkey, netuid); - let _ = Alpha::::get((hotkey, coldkey, netuid)); - let _ = AlphaV2::::get((hotkey, coldkey, netuid)); + let _: AlphaBalance = TotalHotkeyAlpha::::get(hotkey, netuid); + let _: U64F64 = TotalHotkeyShares::::get(hotkey, netuid); + let _: SafeFloat = TotalHotkeySharesV2::::get(hotkey, netuid); + let _: U64F64 = Alpha::::get((hotkey, coldkey, netuid)); + let _: SafeFloat = AlphaV2::::get((hotkey, coldkey, netuid)); }); } @@ -174,16 +181,16 @@ fn indexer_network_economics() { #[test] fn indexer_runtime_api_signatures() { - new_test_ext(1).execute_with(|| { - let netuid = NetUid::from(1u16); - let coldkey = U256::from(3); - let hotkey = U256::from(4); + let at = ::Hash::default(); + let netuid = NetUid::from(1u16); + let acct = AccountId32::new([0u8; 32]); - let _ = SubtensorModule::get_delegate(hotkey); + let _: Option> = + DelegateInfoRuntimeApi::get_delegate(&MockApi, at, acct.clone()).unwrap(); - let _ = SubtensorModule::get_stake_info_for_coldkeys(vec![coldkey]); + let _: Vec<(AccountId32, Vec>)> = + StakeInfoRuntimeApi::get_stake_info_for_coldkeys(&MockApi, at, vec![acct.clone()]) + .unwrap(); - use subtensor_swap_interface::SwapHandler; - let _ = ::SwapInterface::current_alpha_price(netuid); - }); + let _: u64 = SwapRuntimeApi::current_alpha_price(&MockApi, at, netuid).unwrap(); }