diff --git a/pallets/parachain-staking/src/lib.rs b/pallets/parachain-staking/src/lib.rs index c3363bf4..ee245d83 100644 --- a/pallets/parachain-staking/src/lib.rs +++ b/pallets/parachain-staking/src/lib.rs @@ -565,7 +565,7 @@ pub mod pallet { /// It maps from an account to its delegation details. #[pallet::storage] #[pallet::getter(fn delegator_state)] - pub(crate) type DelegatorState = StorageMap< + pub type DelegatorState = StorageMap< _, Twox64Concat, T::AccountId, diff --git a/precompiles/parachain-staking/ParachainStaking.sol b/precompiles/parachain-staking/ParachainStaking.sol index 0ef1c503..42d98e93 100644 --- a/precompiles/parachain-staking/ParachainStaking.sol +++ b/precompiles/parachain-staking/ParachainStaking.sol @@ -19,6 +19,17 @@ interface ParachainStaking { uint256 commission; } + struct DelegationInfo { + bytes32 collator; + uint256 amount; + } + + struct CollatorDelegatorState { + bytes32 delegator; + DelegationInfo[] collators; + uint256 total; + } + /// Get all collator informations // selector: 0xaaacb283 function getCollatorList() external view returns (CollatorInfo[] memory); @@ -58,4 +69,36 @@ interface ParachainStaking { /// elapsed. /// selector: 0x0f615369 function unlockUnstaked(address target) external; + + + /// Get the delegations for a specific delegator or all delegators with paging support + /// If delegator is zero address (0x0), returns all delegators' states with paging + /// Otherwise returns the delegations for the specified delegator (paging applies to collators within delegator) + /// + /// IMPORTANT - Sorting behavior (same as above): + /// - When querying ALL delegators (0x0): The order of delegators is NOT sorted + /// - Each individual delegator's delegations: ARE sorted by stake amount in DESCENDING order + /// + /// INPUT/OUTPUT ADDRESS FORMAT: + /// - Input: Ethereum address (20 bytes) for the delegator parameter + /// - Output: All addresses in the returned structs are substrate account hashes (bytes32) + /// + /// @param delegator The delegator Ethereum address to query (use 0x0 for all delegators) + /// @param offset The starting index for pagination (0-based) + /// @param limit The maximum number of items to return (must be 1-512) + /// + /// selector: 0xbeae0df4 + function getDelegatorState(address delegator, uint256 offset, uint256 limit) external view returns (CollatorDelegatorState[] memory); + + /// Convert Ethereum address to substrate account hash (bytes32) + /// This shows how Ethereum addresses are mapped to substrate accounts internally + /// + /// Input: Standard Ethereum address (20 bytes) + /// Output: Substrate account hash (32 bytes) - the derived substrate account representation + /// + /// @param ethAddress The Ethereum address to convert + /// @return The substrate account hash (bytes32) derived from the Ethereum address + /// + /// selector: 0xb76f87bf + function convertEthToSubstrateAccount(address ethAddress) external view returns (bytes32); } diff --git a/precompiles/parachain-staking/src/lib.rs b/precompiles/parachain-staking/src/lib.rs index 83894f78..34cb7fb6 100644 --- a/precompiles/parachain-staking/src/lib.rs +++ b/precompiles/parachain-staking/src/lib.rs @@ -18,6 +18,9 @@ #![cfg_attr(not(feature = "std"), no_std)] +extern crate alloc; +use alloc::format; + #[cfg(test)] mod mock; @@ -31,15 +34,54 @@ use frame_support::{ }; use pallet_evm::AddressMapping; use precompile_utils::prelude::*; -use sp_core::{H256, U256}; +use sp_core::{H160, H256, U256}; use sp_runtime::traits::{Dispatchable, StaticLookup}; -use sp_std::{convert::TryInto, marker::PhantomData, vec::Vec}; +use sp_std::{convert::TryInto, marker::PhantomData, vec, vec::Vec}; type AccountIdOf = ::AccountId; type BalanceOf = <::Currency as Currency< ::AccountId, >>::Balance; +/// Helper struct for account conversions between H256 and AccountId +struct AccountConverter(PhantomData); + +impl AccountConverter +where + Runtime: frame_system::Config, + AccountIdOf: From<[u8; 32]>, + [u8; 32]: From>, +{ + /// Convert H256 to AccountId + pub fn h256_to_account_id(h256: H256) -> AccountIdOf { + AccountIdOf::::from(h256.to_fixed_bytes()) + } + + /// Convert AccountId to H256 + pub fn account_id_to_h256(account: AccountIdOf) -> H256 { + H256::from( as Into<[u8; 32]>>::into(account)) + } +} + +/// Gas cost constants and calculation utilities +struct GasCalculator; + +impl GasCalculator { + /// Gas cost for reading a single delegator state (max 75 delegations) + pub const SINGLE_DELEGATOR_READ: usize = 3789; + /// Gas cost per delegator in bulk operations (avg 3 delegations) + pub const BULK_DELEGATOR_READ_PER_ITEM: usize = 2580; + /// Gas cost for reading collator pool (realistic 64 collators) + pub const COLLATOR_POOL_READ: usize = 3072; + /// Maximum number of delegators to return in a single bulk query + pub const MAX_DELEGATORS_PER_QUERY: usize = 512; + + /// Calculate gas cost for bulk delegator operations + pub fn calculate_bulk_delegator_cost(count: usize) -> usize { + count.saturating_mul(Self::BULK_DELEGATOR_READ_PER_ITEM) + } +} + /// A precompile to wrap the functionality from parachain_staking. /// /// EXAMPLE USECASE: @@ -53,6 +95,19 @@ pub struct CollatorInfo { commission: U256, } +#[derive(Default, solidity::Codec)] +pub struct DelegationInfo { + collator: H256, + amount: U256, +} + +#[derive(Default, solidity::Codec)] +pub struct CollatorDelegatorState { + delegator: H256, + collators: Vec, + total: U256, +} + #[precompile_utils::precompile] impl ParachainStakingPrecompile where @@ -65,29 +120,172 @@ where [u8; 32]: From>, H256: From<[u8; 32]>, { + /// Helper method to get all collator info + fn get_all_collators_info() -> Vec { + parachain_staking::CandidatePool::::iter() + .map(|(_id, stake_info)| CollatorInfo { + owner: AccountConverter::::account_id_to_h256(stake_info.id), + amount: stake_info.total.into(), + commission: U256::from(stake_info.commission.deconstruct() as u128), + }) + .collect() + } + + /// Helper method to get top candidates as H256 addresses + fn get_top_candidates() -> Vec { + parachain_staking::Pallet::::top_candidates() + .into_iter() + .map(|stake_info| AccountConverter::::account_id_to_h256(stake_info.owner)) + .collect() + } + + /// Helper method to get current validators as H256 addresses + fn get_validators() -> Vec { + pallet_session::Pallet::::validators() + .into_iter() + .map(|validator| AccountConverter::::account_id_to_h256(validator)) + .collect() + } + + /// Get all delegators with paging support (optimized with lazy evaluation) + fn get_all_delegators_paged( + handle: &mut impl PrecompileHandle, + offset: U256, + limit: U256, + ) -> EvmResult> { + let offset_usize: usize = offset.try_into().unwrap_or(usize::MAX); + let limit_usize: usize = limit.try_into().unwrap_or(usize::MAX); + + // Validate input parameters + if offset != U256::zero() && offset_usize == usize::MAX { + return Err(RevertReason::custom("Invalid offset: value too large").into()); + } + + // Forbid limit = 0 to force explicit pagination + if limit == U256::zero() { + return Err(RevertReason::custom("Invalid limit: must be greater than 0").into()); + } + + // Forbid limit exceeding maximum to prevent resource exhaustion + if limit_usize > GasCalculator::MAX_DELEGATORS_PER_QUERY { + return Err(RevertReason::custom(format!( + "Invalid limit: maximum allowed is {}", + GasCalculator::MAX_DELEGATORS_PER_QUERY + )) + .into()); + } + + // Chain operations: skip -> take -> process (only processes what we need) + // Uses lazy evaluation with iterator chaining for optimal performance + let paged_delegators: Vec = parachain_staking::DelegatorState::< + Runtime, + >::iter() + .skip(offset_usize) + .take(limit_usize) + .map(|(delegator_account, state)| { + let delegator_h256 = AccountConverter::::account_id_to_h256(delegator_account); + let collators: Vec = state + .delegations + .into_iter() + .map(|stake| DelegationInfo { + collator: AccountConverter::::account_id_to_h256(stake.owner), + amount: stake.amount.into(), + }) + .collect(); + + CollatorDelegatorState { + delegator: delegator_h256, + collators, + total: state.total.into(), + } + }) + .collect(); + + // Account for reading only the processed delegator states + let processed_count = paged_delegators.len(); + handle.record_db_read::(GasCalculator::calculate_bulk_delegator_cost( + processed_count, + ))?; + + Ok(paged_delegators) + } + + /// Get single delegator state with paging support for their delegations + fn get_single_delegator_paged( + handle: &mut impl PrecompileHandle, + delegator: H256, + offset: U256, + limit: U256, + ) -> EvmResult> { + // Validate input parameters + let offset_usize: usize = offset.try_into().unwrap_or(usize::MAX); + let limit_usize: usize = limit.try_into().unwrap_or(usize::MAX); + + if offset != U256::zero() && offset_usize == usize::MAX { + return Err(RevertReason::custom("Invalid offset: value too large").into()); + } + + // Forbid limit = 0 for consistency (force explicit pagination) + if limit == U256::zero() { + return Err(RevertReason::custom("Invalid limit: must be greater than 0").into()); + } + + // Enforce consistent maximum limit for all query types + if limit_usize > GasCalculator::MAX_DELEGATORS_PER_QUERY { + return Err(RevertReason::custom(format!( + "Invalid limit: maximum allowed is {}", + GasCalculator::MAX_DELEGATORS_PER_QUERY + )) + .into()); + } + + // Gas accounting for single delegator state read + handle.record_db_read::(GasCalculator::SINGLE_DELEGATOR_READ)?; + + let delegator_account = AccountConverter::::h256_to_account_id(delegator); + + let delegator_state = + parachain_staking::Pallet::::delegator_state(&delegator_account); + + match delegator_state { + Some(state) => { + let mut collators: Vec = state + .delegations + .into_iter() + .map(|stake| DelegationInfo { + collator: AccountConverter::::account_id_to_h256(stake.owner), + amount: stake.amount.into(), + }) + .collect(); + + // Apply paging to collators (limit is always > 0 due to validation) + // If offset is beyond available collators, return empty + if offset_usize >= collators.len() { + return Ok(vec![]); + } + + // Skip offset items and take limit items + collators = collators.into_iter().skip(offset_usize).take(limit_usize).collect(); + + Ok(vec![CollatorDelegatorState { delegator, collators, total: state.total.into() }]) + }, + None => Ok(vec![]), + } + } + #[precompile::public("getCollatorList()")] #[precompile::public("get_collator_list()")] #[precompile::view] fn get_collator_list(handle: &mut impl PrecompileHandle) -> EvmResult> { // CandidatePool: UnBoundedVec(AccountId(32) + Balance(16)) - // we account for a theoretical 150 pool. + // we account for a realistic 64 collator pool. - handle.record_db_read::(7200)?; + handle.record_db_read::(GasCalculator::COLLATOR_POOL_READ)?; - let all_collators = parachain_staking::CandidatePool::::iter() - .map(|(_id, stake_info)| CollatorInfo { - owner: H256::from( as Into<[u8; 32]>>::into(stake_info.id)), - amount: stake_info.total.into(), - commission: U256::from(stake_info.commission.deconstruct() as u128), - }) - .collect::>(); - let top_candiate = parachain_staking::Pallet::::top_candidates() - .into_iter() - .map(|stake_info| { - H256::from( as Into<[u8; 32]>>::into(stake_info.owner)) - }) - .collect::>(); - let candidate_list = all_collators.into_iter().filter(|x| top_candiate.contains(&x.owner)); + let all_collators = Self::get_all_collators_info(); + let top_candidates = Self::get_top_candidates(); + let candidate_list = + all_collators.into_iter().filter(|x| top_candidates.contains(&x.owner)); Ok(candidate_list.collect::>()) } @@ -96,21 +294,12 @@ where #[precompile::view] fn get_wait_list(handle: &mut impl PrecompileHandle) -> EvmResult> { // CandidatePool: UnBoundedVec(AccountId(32) + Balance(16)) - // we account for a theoretical 150 pool. + // we account for a realistic 64 collator pool. - handle.record_db_read::(7200)?; + handle.record_db_read::(GasCalculator::COLLATOR_POOL_READ)?; - let all_collators = parachain_staking::CandidatePool::::iter() - .map(|(_id, stake_info)| CollatorInfo { - owner: H256::from( as Into<[u8; 32]>>::into(stake_info.id)), - amount: stake_info.total.into(), - commission: U256::from(stake_info.commission.deconstruct() as u128), - }) - .collect::>(); - let validators = pallet_session::Pallet::::validators() - .into_iter() - .map(|info| H256::from( as Into<[u8; 32]>>::into(info))) - .collect::>(); + let all_collators = Self::get_all_collators_info(); + let validators = Self::get_validators(); let candidate_list = all_collators.into_iter().filter(|x| !validators.contains(&x.owner)); Ok(candidate_list.collect::>()) } @@ -126,9 +315,9 @@ where // Build call with origin. let origin = Runtime::AddressMapping::into_account_id(handle.context().caller); - let collator: Runtime::AccountId = AccountIdOf::::from(collator.to_fixed_bytes()); + let collator_account = AccountConverter::::h256_to_account_id(collator); let collator: ::Source = - ::unlookup(collator.clone()); + ::unlookup(collator_account.clone()); let call = parachain_staking::Call::::join_delegators { collator, amount: stake }; // Dispatch call (if enough gas). @@ -148,9 +337,9 @@ where // Build call with origin. let origin = Runtime::AddressMapping::into_account_id(handle.context().caller); - let collator: Runtime::AccountId = AccountIdOf::::from(collator.to_fixed_bytes()); + let collator_account = AccountConverter::::h256_to_account_id(collator); let collator: ::Source = - ::unlookup(collator.clone()); + ::unlookup(collator_account.clone()); let call = parachain_staking::Call::::delegate_another_candidate { collator, amount: stake, @@ -180,9 +369,9 @@ where fn revoke_delegation(handle: &mut impl PrecompileHandle, collator: H256) -> EvmResult { // Build call with origin. let origin = Runtime::AddressMapping::into_account_id(handle.context().caller); - let collator: Runtime::AccountId = AccountIdOf::::from(collator.to_fixed_bytes()); + let collator_account = AccountConverter::::h256_to_account_id(collator); let collator: ::Source = - ::unlookup(collator.clone()); + ::unlookup(collator_account.clone()); let call = parachain_staking::Call::::revoke_delegation { collator }; // Dispatch call (if enough gas). @@ -202,9 +391,9 @@ where // Build call with origin. let origin = Runtime::AddressMapping::into_account_id(handle.context().caller); - let collator: Runtime::AccountId = AccountIdOf::::from(collator.to_fixed_bytes()); + let collator_account = AccountConverter::::h256_to_account_id(collator); let collator: ::Source = - ::unlookup(collator.clone()); + ::unlookup(collator_account.clone()); let call = parachain_staking::Call::::delegator_stake_more { candidate: collator, more: stake, @@ -227,9 +416,9 @@ where // Build call with origin. let origin = Runtime::AddressMapping::into_account_id(handle.context().caller); - let collator: Runtime::AccountId = AccountIdOf::::from(collator.to_fixed_bytes()); + let collator_account = AccountConverter::::h256_to_account_id(collator); let collator: ::Source = - ::unlookup(collator.clone()); + ::unlookup(collator_account.clone()); let call = parachain_staking::Call::::delegator_stake_less { candidate: collator, less: stake, @@ -257,6 +446,66 @@ where Ok(()) } + /// Get delegator state with pagination support + /// + /// Returns delegation information for a specific delegator or all delegators. + /// If delegator is zero address (0x0), returns all delegators' states with paging. + /// Otherwise returns the delegations for the specified delegator with paging. + /// + /// IMPORTANT - Sorting behavior: + /// - When querying ALL delegators (0x0): The order of delegators is NOT sorted, they are + /// returned in unpredictable storage iteration order + /// - Each individual delegator's delegations: ARE sorted by stake amount in descending order + /// (highest stake first), maintained by the parachain-staking pallet + /// + /// ADDRESS FORMAT: + /// - Input: Ethereum address (H160/20 bytes) for the delegator parameter + /// - Output: All addresses in returned structs are substrate account hashes (H256/32 bytes) + /// + /// Parameters: + /// - delegator: Ethereum address of delegator (or 0x0 for all delegators) + /// - offset: Starting index for pagination + /// - limit: Maximum number of results to return (1-512) + #[precompile::public("getDelegatorState(address,uint256,uint256)")] + #[precompile::public("get_delegator_state(address,uint256,uint256)")] + #[precompile::view] + fn get_delegator_state( + handle: &mut impl PrecompileHandle, + delegator: Address, + offset: U256, + limit: U256, + ) -> EvmResult> { + // Check if delegator is zero address (means get all delegators) + let delegator_h160: H160 = delegator.into(); + if delegator_h160 == H160::zero() { + Self::get_all_delegators_paged(handle, offset, limit) + } else { + // Convert Ethereum address to Substrate account via AddressMapping + let delegator_account = Runtime::AddressMapping::into_account_id(delegator_h160); + let delegator_h256 = AccountConverter::::account_id_to_h256(delegator_account); + Self::get_single_delegator_paged(handle, delegator_h256, offset, limit) + } + } + + /// Convert Ethereum address to substrate account hash + /// + /// Takes a standard Ethereum address (H160/20 bytes) as input and returns the corresponding + /// substrate account hash (H256/32 bytes) that represents the same identity in the substrate + /// runtime. This utility function shows how Ethereum addresses are mapped to substrate accounts + /// internally by the AddressMapping. Useful for debugging and understanding the mapping. + #[precompile::public("convertEthToSubstrateAccount(address)")] + #[precompile::public("convert_eth_to_substrate_account(address)")] + #[precompile::view] + fn convert_eth_to_substrate_account( + _handle: &mut impl PrecompileHandle, + eth_address: Address, + ) -> EvmResult { + let h160: H160 = eth_address.into(); + let substrate_account = Runtime::AddressMapping::into_account_id(h160); + let substrate_hash = AccountConverter::::account_id_to_h256(substrate_account); + Ok(substrate_hash) + } + fn u256_to_amount(value: U256) -> MayRevert> { value .try_into() diff --git a/precompiles/parachain-staking/src/tests.rs b/precompiles/parachain-staking/src/tests.rs index de1b2c92..f2882e15 100644 --- a/precompiles/parachain-staking/src/tests.rs +++ b/precompiles/parachain-staking/src/tests.rs @@ -21,7 +21,7 @@ use crate::{ roll_to, Balances, BlockNumber, ExtBuilder, PCall, Precompiles, PrecompilesValue, RuntimeOrigin, StakePallet, Test, }, - Address, BalanceOf, CollatorInfo, U256, + Address, BalanceOf, CollatorDelegatorState, CollatorInfo, DelegationInfo, U256, }; use frame_support::{ assert_ok, storage::bounded_btree_map::BoundedBTreeMap, traits::LockIdentifier, @@ -29,7 +29,7 @@ use frame_support::{ use pallet_balances::{BalanceLock, Reasons}; use parachain_staking::types::TotalStake; use precompile_utils::testing::{MockPeaqAccount, PrecompileTesterExt, PrecompilesModifierTester}; -use sp_core::H256; +use sp_core::{H160, H256}; const STAKING_ID: LockIdentifier = *b"peaqstak"; @@ -45,6 +45,12 @@ fn convert_mock_account_by_u8_list(account: MockPeaqAccount) -> H256 { H256::from(<[u8; 32]>::from(account)) } +fn convert_mock_account_to_address(account: MockPeaqAccount) -> Address { + // Convert MockPeaqAccount to an Ethereum address for input + let account_bytes: [u8; 32] = account.into(); + Address(H160::from_slice(&account_bytes[..20])) +} + #[test] fn test_selector_enum() { assert!(PCall::get_collator_list_selectors().contains(&0xaaacb283)); @@ -55,6 +61,10 @@ fn test_selector_enum() { assert!(PCall::delegator_stake_more_selectors().contains(&0x1b3d3cdf)); assert!(PCall::delegator_stake_less_selectors().contains(&0xb7e8947f)); assert!(PCall::unlock_unstaked_selectors().contains(&0x0f615369)); + // getDelegatorState now only supports the paged version with offset/limit parameters + assert!(PCall::get_delegator_state_selectors().contains(&0xbeae0df4)); + // convertEthToSubstrateAccount utility function + assert!(PCall::convert_eth_to_substrate_account_selectors().contains(&0xb76f87bf)); } #[test] @@ -71,6 +81,7 @@ fn modifiers() { ); tester.test_view_modifier(PCall::get_collator_list_selectors()); + tester.test_view_modifier(PCall::get_delegator_state_selectors()); }); } @@ -343,3 +354,798 @@ fn should_update_total_stake() { ); }) } + +#[test] +fn test_get_delegator_state() { + ExtBuilder::default() + .with_balances(vec![ + (MockPeaqAccount::Alice, 100), + (MockPeaqAccount::Bob, 200), + (MockPeaqAccount::Charlie, 300), + (MockPeaqAccount::David, 400), + ]) + .with_collators(vec![(MockPeaqAccount::Alice, 100), (MockPeaqAccount::Charlie, 200)]) + .with_delegators(vec![ + (MockPeaqAccount::Bob, MockPeaqAccount::Alice, 50), + (MockPeaqAccount::David, MockPeaqAccount::Alice, 60), + ]) + .build() + .execute_with(|| { + // Test Bob's delegator state + precompiles() + .prepare_test( + MockPeaqAccount::Bob, + MockPeaqAccount::EVMu1Account, + PCall::get_delegator_state { + delegator: convert_mock_account_to_address(MockPeaqAccount::Bob), + offset: U256::zero(), + limit: U256::from(10), + }, + ) + .expect_no_logs() + .execute_returns(vec![CollatorDelegatorState { + delegator: convert_mock_account_by_u8_list(MockPeaqAccount::Bob), + collators: vec![DelegationInfo { + collator: convert_mock_account_by_u8_list(MockPeaqAccount::Alice), + amount: U256::from(50), + }], + total: U256::from(50), + }]); + + // Test David's delegator state + precompiles() + .prepare_test( + MockPeaqAccount::Bob, + MockPeaqAccount::EVMu1Account, + PCall::get_delegator_state { + delegator: convert_mock_account_to_address(MockPeaqAccount::David), + offset: U256::zero(), + limit: U256::from(10), + }, + ) + .expect_no_logs() + .execute_returns(vec![CollatorDelegatorState { + delegator: convert_mock_account_by_u8_list(MockPeaqAccount::David), + collators: vec![DelegationInfo { + collator: convert_mock_account_by_u8_list(MockPeaqAccount::Alice), + amount: U256::from(60), + }], + total: U256::from(60), + }]); + + // Test non-existent delegator (Alice is a collator, not a delegator) + precompiles() + .prepare_test( + MockPeaqAccount::Bob, + MockPeaqAccount::EVMu1Account, + PCall::get_delegator_state { + delegator: convert_mock_account_to_address(MockPeaqAccount::Alice), + offset: U256::zero(), + limit: U256::from(10), + }, + ) + .expect_no_logs() + .execute_returns(Vec::::new()); + + // Now let Bob also delegate to Charlie + assert_ok!(StakePallet::delegate_another_candidate( + RuntimeOrigin::signed(MockPeaqAccount::Bob.into()), + MockPeaqAccount::Charlie.into(), + 30 + )); + + // Test Bob's updated delegator state (now delegating to both Alice and Charlie) + precompiles() + .prepare_test( + MockPeaqAccount::Bob, + MockPeaqAccount::EVMu1Account, + PCall::get_delegator_state { + delegator: convert_mock_account_to_address(MockPeaqAccount::Bob), + offset: U256::zero(), + limit: U256::from(10), + }, + ) + .expect_no_logs() + .execute_returns(vec![CollatorDelegatorState { + delegator: convert_mock_account_by_u8_list(MockPeaqAccount::Bob), + collators: vec![ + DelegationInfo { + collator: convert_mock_account_by_u8_list(MockPeaqAccount::Alice), + amount: U256::from(50), + }, + DelegationInfo { + collator: convert_mock_account_by_u8_list(MockPeaqAccount::Charlie), + amount: U256::from(30), + }, + ], + total: U256::from(80), + }]); + }) +} + +#[test] +fn test_get_all_delegators_state() { + ExtBuilder::default() + .with_balances(vec![ + (MockPeaqAccount::Alice, 100), + (MockPeaqAccount::Bob, 200), + (MockPeaqAccount::Charlie, 300), + (MockPeaqAccount::David, 400), + ]) + .with_collators(vec![(MockPeaqAccount::Alice, 100), (MockPeaqAccount::Charlie, 200)]) + .with_delegators(vec![ + (MockPeaqAccount::Bob, MockPeaqAccount::Alice, 50), + (MockPeaqAccount::David, MockPeaqAccount::Alice, 60), + ]) + .build() + .execute_with(|| { + // Add David's delegation to Charlie + assert_ok!(StakePallet::delegate_another_candidate( + RuntimeOrigin::signed(MockPeaqAccount::David.into()), + MockPeaqAccount::Charlie.into(), + 40 + )); + + // Test getting all delegators' states using zero address + // Since hash-based iteration order is unpredictable, we use a custom approach + // to verify the data content without relying on exact order + + // Create a custom test that can handle unordered results + let binding = precompiles(); + let tester = binding.prepare_test( + MockPeaqAccount::Bob, + MockPeaqAccount::EVMu1Account, + PCall::get_delegator_state { + delegator: Address(H160::zero()), + offset: U256::zero(), + limit: U256::from(10), + }, + ); + + let _result = tester.execute_some(); + + // The result should contain both Bob and David's delegator states + // We can't predict order, but we can verify both are present + // Note: This is a basic verification that the function executes and returns data + }) +} + +#[test] +fn test_delegator_collators_sorting_by_stake_amount() { + ExtBuilder::default() + .with_balances(vec![ + (MockPeaqAccount::Alice, 500), // Collator + (MockPeaqAccount::Bob, 500), // Collator + (MockPeaqAccount::Charlie, 500), // Collator + (MockPeaqAccount::David, 1000), // Delegator with delegations to multiple collators + ]) + .with_collators(vec![ + (MockPeaqAccount::Alice, 100), + (MockPeaqAccount::Bob, 200), + (MockPeaqAccount::Charlie, 300), + ]) + .with_delegators(vec![ + (MockPeaqAccount::David, MockPeaqAccount::Alice, 50), // Lowest stake to Alice + ]) + .build() + .execute_with(|| { + // Add delegation to Bob with middle stake + assert_ok!(StakePallet::delegate_another_candidate( + RuntimeOrigin::signed(MockPeaqAccount::David.into()), + MockPeaqAccount::Bob.into(), + 80 // Middle stake + )); + + // Add delegation to Charlie with highest stake + assert_ok!(StakePallet::delegate_another_candidate( + RuntimeOrigin::signed(MockPeaqAccount::David.into()), + MockPeaqAccount::Charlie.into(), + 100 // Highest stake + )); + + // Test David's delegations - collators are returned sorted by stake amount + // in descending order (as maintained by the parachain-staking pallet) + precompiles() + .prepare_test( + MockPeaqAccount::David, + MockPeaqAccount::EVMu1Account, + PCall::get_delegator_state { + delegator: convert_mock_account_to_address(MockPeaqAccount::David), + offset: U256::zero(), + limit: U256::from(10), + }, + ) + .expect_no_logs() + .execute_returns(vec![CollatorDelegatorState { + delegator: convert_mock_account_by_u8_list(MockPeaqAccount::David), + collators: vec![ + // Actual order returned by the pallet (appears to be sorted by stake + // descending): + DelegationInfo { + collator: convert_mock_account_by_u8_list(MockPeaqAccount::Charlie), + amount: U256::from(100), // Highest stake + }, + DelegationInfo { + collator: convert_mock_account_by_u8_list(MockPeaqAccount::Bob), + amount: U256::from(80), // Middle stake + }, + DelegationInfo { + collator: convert_mock_account_by_u8_list(MockPeaqAccount::Alice), + amount: U256::from(50), // Lowest stake + }, + ], + total: U256::from(230), // 100 + 80 + 50 + }]); + }) +} + +#[test] +fn test_limit_zero_validation() { + ExtBuilder::default() + .with_balances(vec![(MockPeaqAccount::Alice, 100), (MockPeaqAccount::Bob, 100)]) + .with_collators(vec![(MockPeaqAccount::Alice, 50)]) + .build() + .execute_with(|| { + // Test that limit = 0 is rejected for bulk queries (zero address) + precompiles() + .prepare_test( + MockPeaqAccount::Bob, + MockPeaqAccount::EVMu1Account, + PCall::get_delegator_state { + delegator: Address(H160::zero()), + offset: U256::zero(), + limit: U256::zero(), // Should be rejected + }, + ) + .expect_no_logs() + .execute_reverts(|output| output == b"Invalid limit: must be greater than 0"); + + // Test that limit > MAX_DELEGATORS_PER_QUERY is rejected + precompiles() + .prepare_test( + MockPeaqAccount::Bob, + MockPeaqAccount::EVMu1Account, + PCall::get_delegator_state { + delegator: Address(H160::zero()), + offset: U256::zero(), + limit: U256::from(1000), // 1000 > 512, should be rejected + }, + ) + .expect_no_logs() + .execute_reverts(|output| output == b"Invalid limit: maximum allowed is 512"); + + // Test that limit = 0 is also rejected for single delegator queries + precompiles() + .prepare_test( + MockPeaqAccount::Bob, + MockPeaqAccount::EVMu1Account, + PCall::get_delegator_state { + delegator: convert_mock_account_to_address(MockPeaqAccount::Alice), + offset: U256::zero(), + limit: U256::zero(), // Should be rejected even for single delegator + }, + ) + .expect_no_logs() + .execute_reverts(|output| output == b"Invalid limit: must be greater than 0"); + + // Test that limit > MAX_DELEGATORS_PER_QUERY is also rejected for single delegator + precompiles() + .prepare_test( + MockPeaqAccount::Bob, + MockPeaqAccount::EVMu1Account, + PCall::get_delegator_state { + delegator: convert_mock_account_to_address(MockPeaqAccount::Alice), + offset: U256::zero(), + limit: U256::from(1000), // 1000 > 512, should be rejected + }, + ) + .expect_no_logs() + .execute_reverts(|output| output == b"Invalid limit: maximum allowed is 512"); + }); +} + +#[test] +fn test_get_delegator_state_edge_cases() { + ExtBuilder::default() + .with_balances(vec![(MockPeaqAccount::Alice, 100), (MockPeaqAccount::Bob, 200)]) + .with_collators(vec![(MockPeaqAccount::Alice, 100)]) + .with_delegators(vec![(MockPeaqAccount::Bob, MockPeaqAccount::Alice, 50)]) + .build() + .execute_with(|| { + // Test zero address - should return all delegators (only Bob in this test) + // Since there's only 1 delegator, we can verify exact content + precompiles() + .prepare_test( + MockPeaqAccount::Bob, + MockPeaqAccount::EVMu1Account, + PCall::get_delegator_state { + delegator: Address(H160::zero()), + offset: U256::zero(), + limit: U256::from(10), + }, + ) + .expect_no_logs() + .execute_returns(vec![CollatorDelegatorState { + delegator: convert_mock_account_by_u8_list(MockPeaqAccount::Bob), + collators: vec![DelegationInfo { + collator: convert_mock_account_by_u8_list(MockPeaqAccount::Alice), + amount: U256::from(50), + }], + total: U256::from(50), + }]); + + // Test completely non-existent account (not a collator, not a delegator) + let non_existent_account = Address(H160::from([0x99; 20])); // Random account that doesn't exist + precompiles() + .prepare_test( + MockPeaqAccount::Bob, + MockPeaqAccount::EVMu1Account, + PCall::get_delegator_state { + delegator: non_existent_account, + offset: U256::zero(), + limit: U256::from(10), + }, + ) + .expect_no_logs() + .execute_returns(Vec::::new()); + + // Test collator account that exists but has no delegations + // (Alice is a collator but not a delegator) + precompiles() + .prepare_test( + MockPeaqAccount::Bob, + MockPeaqAccount::EVMu1Account, + PCall::get_delegator_state { + delegator: convert_mock_account_to_address(MockPeaqAccount::Alice), + offset: U256::zero(), + limit: U256::from(10), + }, + ) + .expect_no_logs() + .execute_returns(Vec::::new()); + }) +} + +#[test] +fn test_get_delegator_state_paging() { + ExtBuilder::default() + .with_balances(vec![ + (MockPeaqAccount::Alice, 500), // Collator + (MockPeaqAccount::Bob, 500), // Collator + (MockPeaqAccount::Charlie, 500), // Collator + (MockPeaqAccount::David, 1000), // Delegator with delegations to multiple collators + ]) + .with_collators(vec![ + (MockPeaqAccount::Alice, 100), + (MockPeaqAccount::Bob, 200), + (MockPeaqAccount::Charlie, 300), + ]) + .with_delegators(vec![ + (MockPeaqAccount::David, MockPeaqAccount::Alice, 50), // Lowest stake to Alice + ]) + .build() + .execute_with(|| { + // Add delegation to Bob with middle stake + assert_ok!(StakePallet::delegate_another_candidate( + RuntimeOrigin::signed(MockPeaqAccount::David.into()), + MockPeaqAccount::Bob.into(), + 80 // Middle stake + )); + + // Add delegation to Charlie with highest stake + assert_ok!(StakePallet::delegate_another_candidate( + RuntimeOrigin::signed(MockPeaqAccount::David.into()), + MockPeaqAccount::Charlie.into(), + 100 // Highest stake + )); + + // Test paging: get first 2 collators (offset=0, limit=2) + precompiles() + .prepare_test( + MockPeaqAccount::David, + MockPeaqAccount::EVMu1Account, + PCall::get_delegator_state { + delegator: convert_mock_account_to_address(MockPeaqAccount::David), + offset: U256::from(0), + limit: U256::from(2), + }, + ) + .expect_no_logs() + .execute_returns(vec![CollatorDelegatorState { + delegator: convert_mock_account_by_u8_list(MockPeaqAccount::David), + collators: vec![ + // Sorted by stake amount, limited to first 2: + DelegationInfo { + collator: convert_mock_account_by_u8_list(MockPeaqAccount::Charlie), + amount: U256::from(100), // Highest stake first + }, + DelegationInfo { + collator: convert_mock_account_by_u8_list(MockPeaqAccount::Bob), + amount: U256::from(80), // Middle stake second + }, + ], + total: U256::from(230), // Still shows total of all delegations + }]); + + // Test paging: get next 1 collator (offset=2, limit=1) + precompiles() + .prepare_test( + MockPeaqAccount::David, + MockPeaqAccount::EVMu1Account, + PCall::get_delegator_state { + delegator: convert_mock_account_to_address(MockPeaqAccount::David), + offset: U256::from(2), + limit: U256::from(1), + }, + ) + .expect_no_logs() + .execute_returns(vec![CollatorDelegatorState { + delegator: convert_mock_account_by_u8_list(MockPeaqAccount::David), + collators: vec![DelegationInfo { + collator: convert_mock_account_by_u8_list(MockPeaqAccount::Alice), + amount: U256::from(50), // Lowest stake last + }], + total: U256::from(230), // Still shows total of all delegations + }]); + + // Test paging: offset beyond available items (offset=10) - should return empty + precompiles() + .prepare_test( + MockPeaqAccount::David, + MockPeaqAccount::EVMu1Account, + PCall::get_delegator_state { + delegator: convert_mock_account_to_address(MockPeaqAccount::David), + offset: U256::from(10), + limit: U256::from(5), + }, + ) + .expect_no_logs() + .execute_returns(Vec::::new()); // Empty vector when offset exceeds + // available collators + }) +} + +#[test] +fn test_get_all_delegators_paging() { + ExtBuilder::default() + .with_balances(vec![ + (MockPeaqAccount::Alice, 500), // Collator + (MockPeaqAccount::Bob, 200), // Delegator 1 + (MockPeaqAccount::Charlie, 300), // Collator + (MockPeaqAccount::David, 400), // Delegator 2 + (MockPeaqAccount::ParentAccount, 500), // Delegator 3 + ]) + .with_collators(vec![(MockPeaqAccount::Alice, 100), (MockPeaqAccount::Charlie, 200)]) + .with_delegators(vec![ + (MockPeaqAccount::Bob, MockPeaqAccount::Alice, 50), + (MockPeaqAccount::David, MockPeaqAccount::Alice, 60), + (MockPeaqAccount::ParentAccount, MockPeaqAccount::Charlie, 40), + ]) + .build() + .execute_with(|| { + // First, get all delegators to understand the total count + // Test getting all delegators without paging (limit=0) + let _all_result = precompiles() + .prepare_test( + MockPeaqAccount::Bob, + MockPeaqAccount::EVMu1Account, + PCall::get_delegator_state { + delegator: Address(H160::zero()), + offset: U256::from(0), + limit: U256::from(10), // Get up to 10 results - get all + }, + ) + .execute_some(); + + // Test paging: get first 2 delegators (offset=0, limit=2) + let _paged_result = precompiles() + .prepare_test( + MockPeaqAccount::Bob, + MockPeaqAccount::EVMu1Account, + PCall::get_delegator_state { + delegator: Address(H160::zero()), + offset: U256::from(0), + limit: U256::from(2), + }, + ) + .execute_some(); + + // Test paging: get next delegator (offset=2, limit=1) + let _next_result = precompiles() + .prepare_test( + MockPeaqAccount::Bob, + MockPeaqAccount::EVMu1Account, + PCall::get_delegator_state { + delegator: Address(H160::zero()), + offset: U256::from(2), + limit: U256::from(1), + }, + ) + .execute_some(); + + // Test paging: offset beyond available items (offset=10) + // This should return empty but not error + precompiles() + .prepare_test( + MockPeaqAccount::Bob, + MockPeaqAccount::EVMu1Account, + PCall::get_delegator_state { + delegator: Address(H160::zero()), + offset: U256::from(10), + limit: U256::from(5), + }, + ) + .expect_no_logs() + .execute_returns(Vec::::new()); // Should return empty + + // Verify basic execution succeeded for the other tests + // (We can't verify exact content due to hash-based ordering, + // but we confirmed the functions execute without panicking) + }) +} + +#[test] +fn test_single_delegator_paging_verification() { + // This test verifies actual paging content with a single delegator + // so we can predict the exact results + ExtBuilder::default() + .with_balances(vec![ + (MockPeaqAccount::Alice, 500), // Collator + (MockPeaqAccount::Bob, 200), // Only delegator + ]) + .with_collators(vec![(MockPeaqAccount::Alice, 100)]) + .with_delegators(vec![(MockPeaqAccount::Bob, MockPeaqAccount::Alice, 75)]) + .build() + .execute_with(|| { + // Test 1: Get all delegators (should return exactly Bob) + precompiles() + .prepare_test( + MockPeaqAccount::Bob, + MockPeaqAccount::EVMu1Account, + PCall::get_delegator_state { + delegator: Address(H160::zero()), + offset: U256::from(0), + limit: U256::from(10), // Get up to 10 results + }, + ) + .expect_no_logs() + .execute_returns(vec![CollatorDelegatorState { + delegator: convert_mock_account_by_u8_list(MockPeaqAccount::Bob), + collators: vec![DelegationInfo { + collator: convert_mock_account_by_u8_list(MockPeaqAccount::Alice), + amount: U256::from(75), + }], + total: U256::from(75), + }]); + + // Test 2: Get first 1 delegator (should return Bob) + precompiles() + .prepare_test( + MockPeaqAccount::Bob, + MockPeaqAccount::EVMu1Account, + PCall::get_delegator_state { + delegator: Address(H160::zero()), + offset: U256::from(0), + limit: U256::from(1), + }, + ) + .expect_no_logs() + .execute_returns(vec![CollatorDelegatorState { + delegator: convert_mock_account_by_u8_list(MockPeaqAccount::Bob), + collators: vec![DelegationInfo { + collator: convert_mock_account_by_u8_list(MockPeaqAccount::Alice), + amount: U256::from(75), + }], + total: U256::from(75), + }]); + + // Test 3: Skip 1 delegator (offset=1) - should return empty + precompiles() + .prepare_test( + MockPeaqAccount::Bob, + MockPeaqAccount::EVMu1Account, + PCall::get_delegator_state { + delegator: Address(H160::zero()), + offset: U256::from(1), + limit: U256::from(5), + }, + ) + .expect_no_logs() + .execute_returns(Vec::::new()); + + // Test 4: Verify the original non-paged version still works + precompiles() + .prepare_test( + MockPeaqAccount::Bob, + MockPeaqAccount::EVMu1Account, + PCall::get_delegator_state { + delegator: Address(H160::zero()), + offset: U256::zero(), + limit: U256::from(10), + }, + ) + .expect_no_logs() + .execute_returns(vec![CollatorDelegatorState { + delegator: convert_mock_account_by_u8_list(MockPeaqAccount::Bob), + collators: vec![DelegationInfo { + collator: convert_mock_account_by_u8_list(MockPeaqAccount::Alice), + amount: U256::from(75), + }], + total: U256::from(75), + }]); + }) +} + +#[test] +fn test_paging_with_data_verification() { + // This test creates a single delegator scenario where we can predict exact results + ExtBuilder::default() + .with_balances(vec![ + (MockPeaqAccount::Alice, 500), + (MockPeaqAccount::Bob, 500), + (MockPeaqAccount::Charlie, 500), + (MockPeaqAccount::David, 300), + ]) + .with_collators(vec![ + (MockPeaqAccount::Alice, 100), + (MockPeaqAccount::Bob, 100), + (MockPeaqAccount::Charlie, 100), + ]) + .with_delegators(vec![ + // David's first delegation (genesis sets this up) + (MockPeaqAccount::David, MockPeaqAccount::Alice, 50), + ]) + .build() + .execute_with(|| { + // Add additional delegations for David + assert_ok!(StakePallet::delegate_another_candidate( + RuntimeOrigin::signed(MockPeaqAccount::David), + MockPeaqAccount::Bob, + 40 + )); + assert_ok!(StakePallet::delegate_another_candidate( + RuntimeOrigin::signed(MockPeaqAccount::David), + MockPeaqAccount::Charlie, + 30 + )); + + let david_addr = convert_mock_account_by_u8_list(MockPeaqAccount::David); + let david_address = convert_mock_account_to_address(MockPeaqAccount::David); + let alice_addr = convert_mock_account_by_u8_list(MockPeaqAccount::Alice); + let bob_addr = convert_mock_account_by_u8_list(MockPeaqAccount::Bob); + let charlie_addr = convert_mock_account_by_u8_list(MockPeaqAccount::Charlie); + + // Test 1: Get David's full state without paging + precompiles() + .prepare_test( + MockPeaqAccount::David, + MockPeaqAccount::EVMu1Account, + PCall::get_delegator_state { + delegator: david_address, + offset: U256::zero(), + limit: U256::from(10), + }, + ) + .expect_no_logs() + .execute_returns(vec![CollatorDelegatorState { + delegator: david_addr, + collators: vec![ + // Sorted by stake amount (descending order as maintained by pallet) + DelegationInfo { collator: alice_addr, amount: U256::from(50) }, + DelegationInfo { collator: bob_addr, amount: U256::from(40) }, + DelegationInfo { collator: charlie_addr, amount: U256::from(30) }, + ], + total: U256::from(120), + }]); + + // Test 2: Get first 2 collators with paging (offset=0, limit=2) + precompiles() + .prepare_test( + MockPeaqAccount::David, + MockPeaqAccount::EVMu1Account, + PCall::get_delegator_state { + delegator: david_address, + offset: U256::from(0), + limit: U256::from(2), + }, + ) + .expect_no_logs() + .execute_returns(vec![CollatorDelegatorState { + delegator: david_addr, + collators: vec![ + DelegationInfo { collator: alice_addr, amount: U256::from(50) }, + DelegationInfo { collator: bob_addr, amount: U256::from(40) }, + ], + total: U256::from(120), // Total remains the full amount + }]); + + // Test 3: Get last collator with paging (offset=2, limit=1) + precompiles() + .prepare_test( + MockPeaqAccount::David, + MockPeaqAccount::EVMu1Account, + PCall::get_delegator_state { + delegator: david_address, + offset: U256::from(2), + limit: U256::from(1), + }, + ) + .expect_no_logs() + .execute_returns(vec![CollatorDelegatorState { + delegator: david_addr, + collators: vec![DelegationInfo { + collator: charlie_addr, + amount: U256::from(30), + }], + total: U256::from(120), + }]); + + // Test 4: Offset beyond available collators returns empty + precompiles() + .prepare_test( + MockPeaqAccount::David, + MockPeaqAccount::EVMu1Account, + PCall::get_delegator_state { + delegator: david_address, + offset: U256::from(10), + limit: U256::from(5), + }, + ) + .expect_no_logs() + .execute_returns(Vec::::new()); + + // Test 5: Zero address with single delegator returns that delegator + precompiles() + .prepare_test( + MockPeaqAccount::David, + MockPeaqAccount::EVMu1Account, + PCall::get_delegator_state { + delegator: Address(H160::zero()), + offset: U256::from(0), + limit: U256::from(10), + }, + ) + .expect_no_logs() + .execute_returns(vec![CollatorDelegatorState { + delegator: david_addr, + collators: vec![ + DelegationInfo { collator: alice_addr, amount: U256::from(50) }, + DelegationInfo { collator: bob_addr, amount: U256::from(40) }, + DelegationInfo { collator: charlie_addr, amount: U256::from(30) }, + ], + total: U256::from(120), + }]); + }); +} + +#[test] +fn test_convert_eth_to_substrate_account() { + ExtBuilder::default() + .with_balances(vec![(MockPeaqAccount::Alice, 100)]) + .with_collators(vec![(MockPeaqAccount::Alice, 100)]) + .build() + .execute_with(|| { + let eth_address = Address(H160::from_slice(&[1u8; 20])); + + precompiles() + .prepare_test( + MockPeaqAccount::Alice, + MockPeaqAccount::EVMu1Account, + PCall::convert_eth_to_substrate_account { eth_address }, + ) + .expect_no_logs() + .execute_some(); + + // Test with zero address + let zero_address = Address(H160::zero()); + + precompiles() + .prepare_test( + MockPeaqAccount::Alice, + MockPeaqAccount::EVMu1Account, + PCall::convert_eth_to_substrate_account { eth_address: zero_address }, + ) + .expect_no_logs() + .execute_some(); + }); +}