diff --git a/Cargo.lock b/Cargo.lock index f936e40cf6..71fe31e5f4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -71,6 +71,17 @@ dependencies = [ "subtle 2.6.1", ] +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom 0.2.16", + "once_cell", + "version_check", +] + [[package]] name = "ahash" version = "0.8.12" @@ -497,7 +508,7 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43d68f2d516162846c1238e755a7c4d131b892b70cc70c471a8e3ca3ed818fce" dependencies = [ - "ahash", + "ahash 0.8.12", "ark-ff 0.5.0", "ark-poly 0.5.0", "ark-serialize 0.5.0", @@ -732,7 +743,7 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "579305839da207f02b89cd1679e50e67b4331e2f9294a57693e5051b7703fe27" dependencies = [ - "ahash", + "ahash 0.8.12", "ark-ff 0.5.0", "ark-serialize 0.5.0", "ark-std 0.5.0", @@ -1669,6 +1680,29 @@ dependencies = [ "piper", ] +[[package]] +name = "borsh" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1da5ab77c1437701eeff7c88d968729e7766172279eab0676857b3d63af7a6f" +dependencies = [ + "borsh-derive", + "cfg_aliases 0.2.1", +] + +[[package]] +name = "borsh-derive" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0686c856aa6aac0c4498f936d7d6a02df690f614c03e4d906d1018062b5c5e2c" +dependencies = [ + "once_cell", + "proc-macro-crate 3.4.0", + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "bounded-collections" version = "0.1.9" @@ -1917,6 +1951,17 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "bstr" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +dependencies = [ + "memchr", + "regex-automata 0.4.11", + "serde", +] + [[package]] name = "build-helper" version = "0.1.1" @@ -1944,6 +1989,28 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" +[[package]] +name = "bytecheck" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "bytemuck" version = "1.24.0" @@ -3760,6 +3827,7 @@ dependencies = [ "proc-macro2", "quote", "syn 2.0.106", + "unicode-xid", ] [[package]] @@ -5691,6 +5759,9 @@ name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash 0.7.8", +] [[package]] name = "hashbrown" @@ -5698,7 +5769,7 @@ version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" dependencies = [ - "ahash", + "ahash 0.8.12", ] [[package]] @@ -5707,7 +5778,7 @@ version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ - "ahash", + "ahash 0.8.12", "allocator-api2", "serde", ] @@ -8364,6 +8435,7 @@ dependencies = [ "polkadot-runtime-common", "precompile-utils", "rand_chacha 0.3.1", + "safe-math", "scale-info", "serde_json", "sha2 0.10.9", @@ -8582,7 +8654,7 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d" dependencies = [ - "proc-macro-crate 3.4.0", + "proc-macro-crate 1.1.3", "proc-macro2", "quote", "syn 2.0.106", @@ -10775,15 +10847,18 @@ dependencies = [ "log", "ndarray", "num-traits", + "pallet-aura", "pallet-balances", "pallet-commitments", "pallet-crowdloan", "pallet-drand", "pallet-preimage", "pallet-scheduler", + "pallet-shield", "pallet-subtensor-proxy", "pallet-subtensor-swap", "pallet-subtensor-utility", + "pallet-timestamp", "pallet-transaction-payment", "parity-scale-codec", "polkadot-runtime-common", @@ -10795,6 +10870,7 @@ dependencies = [ "serde_json", "sha2 0.10.9", "share-pool", + "sp-consensus-aura", "sp-core", "sp-io", "sp-keyring", @@ -10837,6 +10913,9 @@ dependencies = [ "log", "pallet-subtensor-swap-runtime-api", "parity-scale-codec", + "rand 0.8.5", + "rayon", + "safe-bigmath", "safe-math", "scale-info", "serde", @@ -10873,6 +10952,7 @@ dependencies = [ "frame-support", "parity-scale-codec", "scale-info", + "serde", "sp-api", "sp-std", "subtensor-macros", @@ -13116,18 +13196,23 @@ name = "precompile-utils" version = "0.1.0" source = "git+https://github.com/opentensor/frontier?rev=f12a1274f91442a564bb722a2b9547caba487fa0#f12a1274f91442a564bb722a2b9547caba487fa0" dependencies = [ + "derive_more 1.0.0", "environmental", "evm", "fp-evm", "frame-support", "frame-system", "hex", + "hex-literal", "impl-trait-for-tuples", "log", "num_enum", "pallet-evm", "parity-scale-codec", "precompile-utils-macro", + "scale-info", + "serde", + "similar-asserts", "sp-core", "sp-io", "sp-runtime", @@ -13472,6 +13557,26 @@ dependencies = [ "cc", ] +[[package]] +name = "ptr_meta" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "quanta" version = "0.12.6" @@ -13580,6 +13685,29 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "quoth" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76d9da82a5dc3ff2fb2eee43d2b434fb197a9bf6a2a243850505b61584f888d2" +dependencies = [ + "quoth-macros", + "regex", + "rust_decimal", + "safe-string", +] + +[[package]] +name = "quoth-macros" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58547202bec9896e773db7ef04b4d47c444f9c97bc4386f36e55718c347db440" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "r-efi" version = "5.3.0" @@ -13858,6 +13986,15 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" +[[package]] +name = "rend" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" +dependencies = [ + "bytecheck", +] + [[package]] name = "resolv-conf" version = "0.7.5" @@ -13912,6 +14049,35 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "rkyv" +version = "0.7.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9008cd6385b9e161d8229e1f6549dd23c3d022f132a2ea37ac3a10ac4935779b" +dependencies = [ + "bitvec", + "bytecheck", + "bytes", + "hashbrown 0.12.3", + "ptr_meta", + "rend", + "rkyv_derive", + "seahash", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.7.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "503d1d27590a2b0a3a4ca4c94755aa2875657196ecbf401a42eff41d7de532c0" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "rlp" version = "0.5.2" @@ -14147,6 +14313,22 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48fd7bd8a6377e15ad9d42a8ec25371b94ddc67abe7c8b9127bec79bebaaae18" +[[package]] +name = "rust_decimal" +version = "1.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35affe401787a9bd846712274d97654355d21b2a2c092a3139aabe31e9022282" +dependencies = [ + "arrayvec 0.7.6", + "borsh", + "bytes", + "num-traits", + "rand 0.8.5", + "rkyv", + "serde", + "serde_json", +] + [[package]] name = "rustc-demangle" version = "0.1.26" @@ -14402,6 +14584,17 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +[[package]] +name = "safe-bigmath" +version = "0.3.0" +source = "git+https://github.com/sam0x17/safe-bigmath#1da0a09c5bcf143fa7c464b431bfaeff5476b080" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", + "quoth", +] + [[package]] name = "safe-math" version = "0.1.0" @@ -14421,6 +14614,12 @@ dependencies = [ "rustc_version 0.2.3", ] +[[package]] +name = "safe-string" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2fc51f1e562058dee569383bfdb5a58752bfeb7fa7f0823f5c07c4c45381b5a" + [[package]] name = "safe_arch" version = "0.7.4" @@ -14842,7 +15041,7 @@ name = "sc-consensus-grandpa" version = "0.36.0" source = "git+https://github.com/opentensor/polkadot-sdk.git?rev=a584a577eeaf31e3f1a65e91b0e0b41f0356f7c8#a584a577eeaf31e3f1a65e91b0e0b41f0356f7c8" dependencies = [ - "ahash", + "ahash 0.8.12", "array-bytes 6.2.3", "async-trait", "dyn-clone", @@ -15145,7 +15344,7 @@ name = "sc-network-gossip" version = "0.51.0" source = "git+https://github.com/opentensor/polkadot-sdk.git?rev=a584a577eeaf31e3f1a65e91b0e0b41f0356f7c8#a584a577eeaf31e3f1a65e91b0e0b41f0356f7c8" dependencies = [ - "ahash", + "ahash 0.8.12", "futures", "futures-timer", "log", @@ -15858,7 +16057,7 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "356285bbf17bea63d9e52e96bd18f039672ac92b55b8cb997d6162a2a37d1649" dependencies = [ - "ahash", + "ahash 0.8.12", "cfg-if", "hashbrown 0.13.2", ] @@ -15922,6 +16121,12 @@ dependencies = [ "sha2 0.10.9", ] +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + [[package]] name = "sec1" version = "0.7.3" @@ -16353,6 +16558,32 @@ dependencies = [ "wide", ] +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "similar" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" +dependencies = [ + "bstr", + "unicode-segmentation", +] + +[[package]] +name = "similar-asserts" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b441962c817e33508847a22bd82f03a30cff43642dc2fae8b050566121eb9a" +dependencies = [ + "console", + "similar", +] + [[package]] name = "simple-dns" version = "0.9.3" @@ -17433,7 +17664,7 @@ name = "sp-trie" version = "40.0.0" source = "git+https://github.com/opentensor/polkadot-sdk.git?rev=a584a577eeaf31e3f1a65e91b0e0b41f0356f7c8#a584a577eeaf31e3f1a65e91b0e0b41f0356f7c8" dependencies = [ - "ahash", + "ahash 0.8.12", "foldhash 0.1.5", "hash-db", "hashbrown 0.15.5", @@ -18081,7 +18312,7 @@ dependencies = [ name = "subtensor-macros" version = "0.1.0" dependencies = [ - "ahash", + "ahash 0.8.12", "proc-macro2", "quote", "syn 2.0.106", @@ -18105,6 +18336,7 @@ dependencies = [ "pallet-evm-precompile-modexp", "pallet-evm-precompile-sha3fips", "pallet-evm-precompile-simple", + "pallet-shield", "pallet-subtensor", "pallet-subtensor-proxy", "pallet-subtensor-swap", @@ -19675,7 +19907,7 @@ version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c128c039340ffd50d4195c3f8ce31aac357f06804cfc494c8b9508d4b30dca4" dependencies = [ - "ahash", + "ahash 0.8.12", "hashbrown 0.14.5", "string-interner", ] diff --git a/Cargo.toml b/Cargo.toml index efc22431fa..d1784b025b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,6 +61,7 @@ pallet-subtensor-swap-runtime-api = { path = "pallets/swap/runtime-api", default pallet-subtensor-swap-rpc = { path = "pallets/swap/rpc", default-features = false } procedural-fork = { path = "support/procedural-fork", default-features = false } safe-math = { path = "primitives/safe-math", default-features = false } +safe-bigmath = { package = "safe-bigmath", default-features = false, git = "https://github.com/sam0x17/safe-bigmath" } share-pool = { path = "primitives/share-pool", default-features = false } subtensor-macros = { path = "support/macros", default-features = false } subtensor-custom-rpc = { default-features = false, path = "pallets/subtensor/rpc" } diff --git a/chain-extensions/src/lib.rs b/chain-extensions/src/lib.rs index eaaee70d27..2e2eb807fa 100644 --- a/chain-extensions/src/lib.rs +++ b/chain-extensions/src/lib.rs @@ -18,7 +18,7 @@ use pallet_subtensor_proxy as pallet_proxy; use pallet_subtensor_proxy::WeightInfo; use sp_runtime::{DispatchError, Weight, traits::StaticLookup}; use sp_std::marker::PhantomData; -use substrate_fixed::types::U96F32; +use substrate_fixed::types::U64F64; use subtensor_runtime_common::{AlphaCurrency, NetUid, ProxyType, TaoCurrency}; use subtensor_swap_interface::SwapHandler; @@ -520,7 +520,7 @@ where netuid.into(), ); - let price = current_alpha_price.saturating_mul(U96F32::from_num(1_000_000_000)); + let price = current_alpha_price.saturating_mul(U64F64::from_num(1_000_000_000)); let price: u64 = price.saturating_to_num(); let encoded_result = price.encode(); diff --git a/chain-extensions/src/mock.rs b/chain-extensions/src/mock.rs index 660aa0c77c..a1e6334b1b 100644 --- a/chain-extensions/src/mock.rs +++ b/chain-extensions/src/mock.rs @@ -25,7 +25,7 @@ use sp_runtime::{ traits::{BlakeTwo256, Convert, IdentityLookup}, }; use sp_std::{cell::RefCell, cmp::Ordering, sync::OnceLock}; -use subtensor_runtime_common::{AlphaCurrency, NetUid, TaoCurrency}; +use subtensor_runtime_common::{AlphaCurrency, AuthorshipInfo, NetUid, TaoCurrency}; type Block = frame_system::mocking::MockBlock; @@ -262,6 +262,14 @@ parameter_types! { pub const AnnouncementDepositFactor: Balance = 1; } +pub struct MockAuthorshipProvider; + +impl AuthorshipInfo for MockAuthorshipProvider { + fn author() -> Option { + Some(U256::from(12345u64)) + } +} + parameter_types! { pub const InitialMinAllowedWeights: u16 = 0; pub const InitialEmissionValue: u16 = 0; @@ -325,9 +333,8 @@ parameter_types! { pub const InitialAlphaLow: u16 = 45875; // Represents 0.7 as per the production default pub const InitialLiquidAlphaOn: bool = false; // Default value for LiquidAlphaOn pub const InitialYuma3On: bool = false; // Default value for Yuma3On - // pub const InitialNetworkMaxStake: u64 = u64::MAX; // (DEPRECATED) - pub const InitialColdkeySwapScheduleDuration: u64 = 5 * 24 * 60 * 60 / 12; // Default as 5 days - pub const InitialColdkeySwapRescheduleDuration: u64 = 24 * 60 * 60 / 12; // Default as 1 day + pub const InitialColdkeySwapAnnouncementDelay: u64 = 50; + pub const InitialColdkeySwapReannouncementDelay: u64 = 10; pub const InitialDissolveNetworkScheduleDuration: u64 = 5 * 24 * 60 * 60 / 12; // Default as 5 days pub const InitialTaoWeight: u64 = 0; // 100% global weight. pub const InitialEmaPriceHalvingPeriod: u64 = 201_600_u64; // 4 weeks @@ -397,8 +404,8 @@ impl pallet_subtensor::Config for Test { type LiquidAlphaOn = InitialLiquidAlphaOn; type Yuma3On = InitialYuma3On; type Preimages = Preimage; - type InitialColdkeySwapScheduleDuration = InitialColdkeySwapScheduleDuration; - type InitialColdkeySwapRescheduleDuration = InitialColdkeySwapRescheduleDuration; + type InitialColdkeySwapAnnouncementDelay = InitialColdkeySwapAnnouncementDelay; + type InitialColdkeySwapReannouncementDelay = InitialColdkeySwapReannouncementDelay; type InitialDissolveNetworkScheduleDuration = InitialDissolveNetworkScheduleDuration; type InitialTaoWeight = InitialTaoWeight; type InitialEmaPriceHalvingPeriod = InitialEmaPriceHalvingPeriod; @@ -412,13 +419,13 @@ impl pallet_subtensor::Config for Test { type MaxImmuneUidsPercentage = MaxImmuneUidsPercentage; type CommitmentsInterface = CommitmentsI; type EvmKeyAssociateRateLimit = EvmKeyAssociateRateLimit; + type AuthorshipProvider = MockAuthorshipProvider; } // Swap-related parameter types parameter_types! { pub const SwapProtocolId: PalletId = PalletId(*b"ten/swap"); pub const SwapMaxFeeRate: u16 = 10000; // 15.26% - pub const SwapMaxPositions: u32 = 100; pub const SwapMinimumLiquidity: u64 = 1_000; pub const SwapMinimumReserve: NonZeroU64 = NonZeroU64::new(100).unwrap(); } @@ -430,7 +437,6 @@ impl pallet_subtensor_swap::Config for Test { type TaoReserve = TaoCurrencyReserve; type AlphaReserve = AlphaCurrencyReserve; type MaxFeeRate = SwapMaxFeeRate; - type MaxPositions = SwapMaxPositions; type MinimumLiquidity = SwapMinimumLiquidity; type MinimumReserve = SwapMinimumReserve; type WeightInfo = (); diff --git a/chain-extensions/src/tests.rs b/chain-extensions/src/tests.rs index bd6f46c8ab..f3abfa2c91 100644 --- a/chain-extensions/src/tests.rs +++ b/chain-extensions/src/tests.rs @@ -10,7 +10,7 @@ use pallet_subtensor::DefaultMinStake; use sp_core::Get; use sp_core::U256; use sp_runtime::DispatchError; -use substrate_fixed::types::U96F32; +use substrate_fixed::types::U64F64; use subtensor_runtime_common::{AlphaCurrency, Currency as CurrencyTrait, NetUid, TaoCurrency}; use subtensor_swap_interface::SwapHandler; @@ -985,7 +985,7 @@ fn get_alpha_price_returns_encoded_price() { as SwapHandler>::current_alpha_price( netuid.into(), ); - let expected_price_scaled = expected_price.saturating_mul(U96F32::from_num(1_000_000_000)); + let expected_price_scaled = expected_price.saturating_mul(U64F64::from_num(1_000_000_000)); let expected_price_u64: u64 = expected_price_scaled.saturating_to_num(); let mut env = MockEnv::new(FunctionId::GetAlphaPriceV1, caller, netuid.encode()); diff --git a/common/src/lib.rs b/common/src/lib.rs index 658f8b2e01..f28ec6d878 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -260,6 +260,12 @@ pub trait BalanceOps { ) -> Result; } +/// Allows to query the current block author +pub trait AuthorshipInfo { + /// Return the current block author + fn author() -> Option; +} + pub mod time { use super::*; diff --git a/contract-tests/package-lock.json b/contract-tests/package-lock.json new file mode 100644 index 0000000000..06c722a6de --- /dev/null +++ b/contract-tests/package-lock.json @@ -0,0 +1,6222 @@ +{ + "name": "contract-tests", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "license": "ISC", + "dependencies": { + "@polkadot-api/descriptors": "file:.papi/descriptors", + "@polkadot-api/ink-contracts": "^0.4.1", + "@polkadot-api/sdk-ink": "^0.5.1", + "@polkadot-labs/hdkd": "^0.0.25", + "@polkadot-labs/hdkd-helpers": "^0.0.25", + "@polkadot/api": "^16.4.6", + "@polkadot/util-crypto": "^14.0.1", + "@types/mocha": "^10.0.10", + "dotenv": "17.2.1", + "ethers": "^6.13.5", + "mocha": "^11.1.0", + "polkadot-api": "^1.22.0", + "rxjs": "^7.8.2", + "scale-ts": "^1.6.1", + "viem": "2.23.4", + "ws": "^8.18.2" + }, + "devDependencies": { + "@types/chai": "^5.0.1", + "@types/node": "^22.18.0", + "assert": "^2.1.0", + "chai": "^6.0.1", + "prettier": "^3.3.3", + "ts-node": "^10.9.2", + "typescript": "^5.7.2" + } + }, + ".papi/descriptors": { + "name": "@polkadot-api/descriptors", + "version": "0.1.0-autogenerated.5063582544821983772", + "peerDependencies": { + "polkadot-api": ">=1.21.0" + } + }, + "node_modules/@adraffy/ens-normalize": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.11.1.tgz", + "integrity": "sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==", + "license": "MIT" + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@commander-js/extra-typings": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/@commander-js/extra-typings/-/extra-typings-14.0.0.tgz", + "integrity": "sha512-hIn0ncNaJRLkZrxBIp5AsW/eXEHNKYQBh0aPdoUqNgD+Io3NIykQqpKFyKcuasZhicGaEZJX/JBSIkZ4e5x8Dg==", + "license": "MIT", + "peerDependencies": { + "commander": "~14.0.0" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@ethereumjs/rlp": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/@ethereumjs/rlp/-/rlp-10.1.0.tgz", + "integrity": "sha512-r67BJbwilammAqYI4B5okA66cNdTlFzeWxPNJOolKV52ZS/flo0tUBf4x4gxWXBgh48OgsdFV1Qp5pRoSe8IhQ==", + "license": "MPL-2.0", + "bin": { + "rlp": "bin/rlp.cjs" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/gen-mapping/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@noble/ciphers": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", + "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/curves": { + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", + "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/curves/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz", + "integrity": "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@polkadot-api/cli": { + "version": "0.16.3", + "resolved": "https://registry.npmjs.org/@polkadot-api/cli/-/cli-0.16.3.tgz", + "integrity": "sha512-s+p3dFw1vOeyMMqhUbt1RFyqPZdR7vg6joS0v9wBvK3qX5xU+QfOOaMxXJ8fl0mJEbwoJnJsvVl4MzjsABaKCg==", + "license": "MIT", + "dependencies": { + "@commander-js/extra-typings": "^14.0.0", + "@polkadot-api/codegen": "0.20.0", + "@polkadot-api/ink-contracts": "0.4.3", + "@polkadot-api/json-rpc-provider": "0.0.4", + "@polkadot-api/known-chains": "0.9.15", + "@polkadot-api/legacy-provider": "0.3.6", + "@polkadot-api/metadata-compatibility": "0.4.1", + "@polkadot-api/observable-client": "0.17.0", + "@polkadot-api/polkadot-sdk-compat": "2.3.3", + "@polkadot-api/sm-provider": "0.1.14", + "@polkadot-api/smoldot": "0.3.14", + "@polkadot-api/substrate-bindings": "0.16.5", + "@polkadot-api/substrate-client": "0.4.7", + "@polkadot-api/utils": "0.2.0", + "@polkadot-api/wasm-executor": "^0.2.2", + "@polkadot-api/ws-provider": "0.7.4", + "@types/node": "^24.10.1", + "commander": "^14.0.2", + "execa": "^9.6.0", + "fs.promises.exists": "^1.1.4", + "ora": "^9.0.0", + "read-pkg": "^10.0.0", + "rxjs": "^7.8.2", + "tsc-prog": "^2.3.0", + "tsup": "8.5.0", + "typescript": "^5.9.3", + "write-package": "^7.2.0" + }, + "bin": { + "papi": "dist/main.js", + "polkadot-api": "dist/main.js" + } + }, + "node_modules/@polkadot-api/cli/node_modules/@types/node": { + "version": "24.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", + "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@polkadot-api/cli/node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "license": "MIT" + }, + "node_modules/@polkadot-api/codegen": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@polkadot-api/codegen/-/codegen-0.20.0.tgz", + "integrity": "sha512-akwPArm35UZcebUFtTKcEkdBLCjYyKweGw3/tT04p/EtM4OsQ1FxhRdXZ51ScBC3JVGCFQTUO2hNsd1E6YXvlw==", + "license": "MIT", + "dependencies": { + "@polkadot-api/ink-contracts": "0.4.3", + "@polkadot-api/metadata-builders": "0.13.7", + "@polkadot-api/metadata-compatibility": "0.4.1", + "@polkadot-api/substrate-bindings": "0.16.5", + "@polkadot-api/utils": "0.2.0" + } + }, + "node_modules/@polkadot-api/common-sdk-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@polkadot-api/common-sdk-utils/-/common-sdk-utils-0.1.0.tgz", + "integrity": "sha512-cgA9fh8dfBai9b46XaaQmj9vwzyHStQjc/xrAvQksgF6SqvZ0yAfxVqLvGrsz/Xi3dsAdKLg09PybC7MUAMv9w==", + "license": "MIT", + "peerDependencies": { + "polkadot-api": "^1.8.1", + "rxjs": ">=7.8.1" + } + }, + "node_modules/@polkadot-api/descriptors": { + "resolved": ".papi/descriptors", + "link": true + }, + "node_modules/@polkadot-api/ink-contracts": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@polkadot-api/ink-contracts/-/ink-contracts-0.4.3.tgz", + "integrity": "sha512-Wl+4Dxjt0GAl+rADZEgrrqEesqX/xygTpX18TmzmspcKhb9QIZf9FJI8A5Sgtq0TKAOwsd1d/hbHVX3LgbXFXg==", + "license": "MIT", + "dependencies": { + "@polkadot-api/metadata-builders": "0.13.7", + "@polkadot-api/substrate-bindings": "0.16.5", + "@polkadot-api/utils": "0.2.0" + } + }, + "node_modules/@polkadot-api/json-rpc-provider": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/@polkadot-api/json-rpc-provider/-/json-rpc-provider-0.0.4.tgz", + "integrity": "sha512-9cDijLIxzHOBuq6yHqpqjJ9jBmXrctjc1OFqU+tQrS96adQze3mTIH6DTgfb/0LMrqxzxffz1HQGrIlEH00WrA==", + "license": "MIT" + }, + "node_modules/@polkadot-api/json-rpc-provider-proxy": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@polkadot-api/json-rpc-provider-proxy/-/json-rpc-provider-proxy-0.2.7.tgz", + "integrity": "sha512-+HM4JQXzO2GPUD2++4GOLsmFL6LO8RoLvig0HgCLuypDgfdZMlwd8KnyGHjRnVEHA5X+kvXbk84TDcAXVxTazQ==", + "license": "MIT" + }, + "node_modules/@polkadot-api/known-chains": { + "version": "0.9.15", + "resolved": "https://registry.npmjs.org/@polkadot-api/known-chains/-/known-chains-0.9.15.tgz", + "integrity": "sha512-VQGu2Anvnx0y0Ltd6sQB3aYzQFGsaQwf2znh+w4Oflaxln5lsjO/+trpXz/rdrdgyi0iafkhpeho/p/EGBwJ+A==", + "license": "MIT" + }, + "node_modules/@polkadot-api/legacy-provider": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@polkadot-api/legacy-provider/-/legacy-provider-0.3.6.tgz", + "integrity": "sha512-JZQg0HVtBowFKxNrZdnMBKXmeSBD4yFlz6egEpvE97RXRvjaBzTaVuFFhBchngq9YmgFQewuWSoX5XSUW6hcEg==", + "license": "MIT", + "dependencies": { + "@polkadot-api/json-rpc-provider": "0.0.4", + "@polkadot-api/raw-client": "0.1.1", + "@polkadot-api/substrate-bindings": "0.16.5", + "@polkadot-api/utils": "0.2.0" + }, + "peerDependencies": { + "rxjs": ">=7.8.0" + } + }, + "node_modules/@polkadot-api/logs-provider": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@polkadot-api/logs-provider/-/logs-provider-0.0.6.tgz", + "integrity": "sha512-4WgHlvy+xee1ADaaVf6+MlK/+jGMtsMgAzvbQOJZnP4PfQuagoTqaeayk8HYKxXGphogLlPbD06tANxcb+nvAg==", + "license": "MIT", + "dependencies": { + "@polkadot-api/json-rpc-provider": "0.0.4" + } + }, + "node_modules/@polkadot-api/merkleize-metadata": { + "version": "1.1.27", + "resolved": "https://registry.npmjs.org/@polkadot-api/merkleize-metadata/-/merkleize-metadata-1.1.27.tgz", + "integrity": "sha512-OdKwOzzrLL0Ju3pQA9LjeQEquMcD+KtLybUAO3fVxwjxD5cyI0RwillGoAIBJvfMaZpNxnxJnD+WzNjRcr7FiQ==", + "license": "MIT", + "dependencies": { + "@polkadot-api/metadata-builders": "0.13.7", + "@polkadot-api/substrate-bindings": "0.16.5", + "@polkadot-api/utils": "0.2.0" + } + }, + "node_modules/@polkadot-api/metadata-builders": { + "version": "0.13.7", + "resolved": "https://registry.npmjs.org/@polkadot-api/metadata-builders/-/metadata-builders-0.13.7.tgz", + "integrity": "sha512-xwggY8F/gtX7qGzz+jzP3DZvWgBWIIFQhk+r2MJ431CR+tNKeTtzGdwNocVrb9NYTK2naC9ckJS14nrNM6LWLw==", + "license": "MIT", + "dependencies": { + "@polkadot-api/substrate-bindings": "0.16.5", + "@polkadot-api/utils": "0.2.0" + } + }, + "node_modules/@polkadot-api/metadata-compatibility": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@polkadot-api/metadata-compatibility/-/metadata-compatibility-0.4.1.tgz", + "integrity": "sha512-mZt4Af6oPXEHAprrckJiSZkWRVf0mqwF+Bm+703rPsezLptQid9AjSzh1hkgIkOrPbg6IhWbmMhbuJVjx9VeQA==", + "license": "MIT", + "dependencies": { + "@polkadot-api/metadata-builders": "0.13.7", + "@polkadot-api/substrate-bindings": "0.16.5" + } + }, + "node_modules/@polkadot-api/observable-client": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@polkadot-api/observable-client/-/observable-client-0.17.0.tgz", + "integrity": "sha512-hilb12Fg1JrlM/0nucMT85//EQltB53fmoh7YNBsZMiNpavn/3qGTO4s0JMlC/LBbddYg0nxA+DMkSVlapo7cQ==", + "license": "MIT", + "dependencies": { + "@polkadot-api/metadata-builders": "0.13.7", + "@polkadot-api/substrate-bindings": "0.16.5", + "@polkadot-api/substrate-client": "0.4.7", + "@polkadot-api/utils": "0.2.0" + }, + "peerDependencies": { + "rxjs": ">=7.8.0" + } + }, + "node_modules/@polkadot-api/pjs-signer": { + "version": "0.6.17", + "resolved": "https://registry.npmjs.org/@polkadot-api/pjs-signer/-/pjs-signer-0.6.17.tgz", + "integrity": "sha512-bxFtyiNOchV0osh6m+1CaN4tkWF7Mo4IT9XPLZBwSybpHZgwmu2wbhgqBkVL98QMyGzud7NHfrJsTCgFU6jHGg==", + "license": "MIT", + "dependencies": { + "@polkadot-api/metadata-builders": "0.13.7", + "@polkadot-api/polkadot-signer": "0.1.6", + "@polkadot-api/signers-common": "0.1.18", + "@polkadot-api/substrate-bindings": "0.16.5", + "@polkadot-api/utils": "0.2.0" + } + }, + "node_modules/@polkadot-api/polkadot-sdk-compat": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/@polkadot-api/polkadot-sdk-compat/-/polkadot-sdk-compat-2.3.3.tgz", + "integrity": "sha512-p30po+iv4trniSJ7UZiIt/rFInvtA9Tzg65EzuRkCaQAnh54a3MPp9w/q+x+SNLEcfzVLvf8LyPnMPOIpKuj5w==", + "license": "MIT", + "dependencies": { + "@polkadot-api/json-rpc-provider": "0.0.4" + } + }, + "node_modules/@polkadot-api/polkadot-signer": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@polkadot-api/polkadot-signer/-/polkadot-signer-0.1.6.tgz", + "integrity": "sha512-X7ghAa4r7doETtjAPTb50IpfGtrBmy3BJM5WCfNKa1saK04VFY9w+vDn+hwEcM4p0PcDHt66Ts74hzvHq54d9A==", + "license": "MIT" + }, + "node_modules/@polkadot-api/raw-client": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@polkadot-api/raw-client/-/raw-client-0.1.1.tgz", + "integrity": "sha512-HxalpNEo8JCYXfxKM5p3TrK8sEasTGMkGjBNLzD4TLye9IK2smdb5oTvp2yfkU1iuVBdmjr69uif4NaukOYo2g==", + "license": "MIT", + "dependencies": { + "@polkadot-api/json-rpc-provider": "0.0.4" + } + }, + "node_modules/@polkadot-api/sdk-ink": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@polkadot-api/sdk-ink/-/sdk-ink-0.5.1.tgz", + "integrity": "sha512-9pRnghjigivvgq7375hzkoazstvPDbc0YB01Jzw1/MYKcX+YJn1p/H8SAQTWbKlz2ohFgi1nwU52a0bsmKqb/Q==", + "license": "MIT", + "dependencies": { + "@ethereumjs/rlp": "^10.0.0", + "@polkadot-api/common-sdk-utils": "0.1.0", + "@polkadot-api/substrate-bindings": "^0.16.3", + "abitype": "^1.1.1", + "viem": "^2.37.9" + }, + "peerDependencies": { + "@polkadot-api/ink-contracts": ">=0.4.0", + "polkadot-api": ">=1.19.0", + "rxjs": ">=7.8.0" + } + }, + "node_modules/@polkadot-api/sdk-ink/node_modules/@noble/curves": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.1.tgz", + "integrity": "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@polkadot-api/sdk-ink/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@polkadot-api/sdk-ink/node_modules/isows": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/isows/-/isows-1.0.7.tgz", + "integrity": "sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "peerDependencies": { + "ws": "*" + } + }, + "node_modules/@polkadot-api/sdk-ink/node_modules/ox": { + "version": "0.9.6", + "resolved": "https://registry.npmjs.org/ox/-/ox-0.9.6.tgz", + "integrity": "sha512-8SuCbHPvv2eZLYXrNmC0EC12rdzXQLdhnOMlHDW2wiCPLxBrOOJwX5L5E61by+UjTPOryqQiRSnjIKCI+GykKg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "dependencies": { + "@adraffy/ens-normalize": "^1.11.0", + "@noble/ciphers": "^1.3.0", + "@noble/curves": "1.9.1", + "@noble/hashes": "^1.8.0", + "@scure/bip32": "^1.7.0", + "@scure/bip39": "^1.6.0", + "abitype": "^1.0.9", + "eventemitter3": "5.0.1" + }, + "peerDependencies": { + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@polkadot-api/sdk-ink/node_modules/viem": { + "version": "2.41.2", + "resolved": "https://registry.npmjs.org/viem/-/viem-2.41.2.tgz", + "integrity": "sha512-LYliajglBe1FU6+EH9mSWozp+gRA/QcHfxeD9Odf83AdH5fwUS7DroH4gHvlv6Sshqi1uXrYFA2B/EOczxd15g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "dependencies": { + "@noble/curves": "1.9.1", + "@noble/hashes": "1.8.0", + "@scure/bip32": "1.7.0", + "@scure/bip39": "1.6.0", + "abitype": "1.1.0", + "isows": "1.0.7", + "ox": "0.9.6", + "ws": "8.18.3" + }, + "peerDependencies": { + "typescript": ">=5.0.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@polkadot-api/sdk-ink/node_modules/viem/node_modules/abitype": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.1.0.tgz", + "integrity": "sha512-6Vh4HcRxNMLA0puzPjM5GBgT4aAcFGKZzSgAXvuZ27shJP6NEpielTuqbBmZILR5/xd0PizkBGy5hReKz9jl5A==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/wevm" + }, + "peerDependencies": { + "typescript": ">=5.0.4", + "zod": "^3.22.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/@polkadot-api/signer": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@polkadot-api/signer/-/signer-0.2.11.tgz", + "integrity": "sha512-32tqbJo6JDfc/lHg+nTveeunFRULonWoTQX9xbs70arr/tAyyZfljupdECRK8CVRx1777es/CQO3QVj8EpWtYg==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "^2.0.1", + "@polkadot-api/merkleize-metadata": "1.1.27", + "@polkadot-api/polkadot-signer": "0.1.6", + "@polkadot-api/signers-common": "0.1.18", + "@polkadot-api/substrate-bindings": "0.16.5", + "@polkadot-api/utils": "0.2.0" + } + }, + "node_modules/@polkadot-api/signers-common": { + "version": "0.1.18", + "resolved": "https://registry.npmjs.org/@polkadot-api/signers-common/-/signers-common-0.1.18.tgz", + "integrity": "sha512-UQXuRZoQ+jMolEpIPF0mVXcoqQ/382fHrSOgfK5sIvjeH0HPf4P+s3IwcnwyAdpHY2gdHXYlHd/SAw7Q1gJ4EA==", + "license": "MIT", + "dependencies": { + "@polkadot-api/metadata-builders": "0.13.7", + "@polkadot-api/polkadot-signer": "0.1.6", + "@polkadot-api/substrate-bindings": "0.16.5", + "@polkadot-api/utils": "0.2.0" + } + }, + "node_modules/@polkadot-api/sm-provider": { + "version": "0.1.14", + "resolved": "https://registry.npmjs.org/@polkadot-api/sm-provider/-/sm-provider-0.1.14.tgz", + "integrity": "sha512-QQvoeBSIwnEm8IUhGA6sBU6LNh2v7SOuVOnF77ZD7P5ELTrdmQH2Tcn0W15qGTmTG45b3Z52XsKpuQbIJ7c7XA==", + "license": "MIT", + "dependencies": { + "@polkadot-api/json-rpc-provider": "0.0.4", + "@polkadot-api/json-rpc-provider-proxy": "0.2.7" + }, + "peerDependencies": { + "@polkadot-api/smoldot": ">=0.3" + } + }, + "node_modules/@polkadot-api/smoldot": { + "version": "0.3.14", + "resolved": "https://registry.npmjs.org/@polkadot-api/smoldot/-/smoldot-0.3.14.tgz", + "integrity": "sha512-eWqO0xFQaKzqY5mRYxYuZcj1IiaLcQP+J38UQyuJgEorm+9yHVEQ/XBWoM83P+Y8TwE5IWTICp1LCVeiFQTGPQ==", + "license": "MIT", + "dependencies": { + "@types/node": "^24.5.2", + "smoldot": "2.0.39" + } + }, + "node_modules/@polkadot-api/smoldot/node_modules/@types/node": { + "version": "24.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", + "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@polkadot-api/smoldot/node_modules/smoldot": { + "version": "2.0.39", + "resolved": "https://registry.npmjs.org/smoldot/-/smoldot-2.0.39.tgz", + "integrity": "sha512-yFMSzI6nkqWFTNao99lBA/TguUFU+bR3A5UGTDd/QqqB12jqzvZnmW/No6l2rKmagt8Qx/KybMNowV/E28znhA==", + "license": "GPL-3.0-or-later WITH Classpath-exception-2.0", + "dependencies": { + "ws": "^8.8.1" + } + }, + "node_modules/@polkadot-api/smoldot/node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "license": "MIT" + }, + "node_modules/@polkadot-api/substrate-bindings": { + "version": "0.16.5", + "resolved": "https://registry.npmjs.org/@polkadot-api/substrate-bindings/-/substrate-bindings-0.16.5.tgz", + "integrity": "sha512-QFgNlBmtLtiUGTCTurxcE6UZrbI2DaQ5/gyIiC2FYfEhStL8tl20b09FRYHcSjY+lxN42Rcf9HVX+MCFWLYlpQ==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "^2.0.1", + "@polkadot-api/utils": "0.2.0", + "@scure/base": "^2.0.0", + "scale-ts": "^1.6.1" + } + }, + "node_modules/@polkadot-api/substrate-bindings/node_modules/@scure/base": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-2.0.0.tgz", + "integrity": "sha512-3E1kpuZginKkek01ovG8krQ0Z44E3DHPjc5S2rjJw9lZn3KSQOs8S7wqikF/AH7iRanHypj85uGyxk0XAyC37w==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@polkadot-api/substrate-client": { + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/@polkadot-api/substrate-client/-/substrate-client-0.4.7.tgz", + "integrity": "sha512-Mmx9VKincVqfVQmq89gzDk4DN3uKwf8CxoqYvq+EiPUZ1QmMUc7X4QMwG1MXIlYdnm5LSXzn+2Jn8ik8xMgL+w==", + "license": "MIT", + "dependencies": { + "@polkadot-api/json-rpc-provider": "0.0.4", + "@polkadot-api/raw-client": "0.1.1", + "@polkadot-api/utils": "0.2.0" + } + }, + "node_modules/@polkadot-api/utils": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@polkadot-api/utils/-/utils-0.2.0.tgz", + "integrity": "sha512-nY3i5fQJoAxU4n3bD7Fs208/KR2J95SGfVc58kDjbRYN5a84kWaGEqzjBNtP9oqht49POM8Bm9mbIrkvC1Bzuw==", + "license": "MIT" + }, + "node_modules/@polkadot-api/wasm-executor": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@polkadot-api/wasm-executor/-/wasm-executor-0.2.3.tgz", + "integrity": "sha512-B2h1o+Qlo9idpASaHvMSoViB2I5ko5OAfwfhYF8LQDkTADK0B+SeStzNj1Qn+FG34wqTuv7HzBCdjaUgzYINJQ==", + "license": "MIT" + }, + "node_modules/@polkadot-api/ws-provider": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@polkadot-api/ws-provider/-/ws-provider-0.7.4.tgz", + "integrity": "sha512-mkk2p8wPht+ljU1xULCPMsLpNF7NHuGaufuDCIZZgopALaZpfVFJxc3qa9s6Xv8X3hM+TRoC5WknuD1ykRY99A==", + "license": "MIT", + "dependencies": { + "@polkadot-api/json-rpc-provider": "0.0.4", + "@polkadot-api/json-rpc-provider-proxy": "0.2.7", + "@types/ws": "^8.18.1", + "ws": "^8.18.3" + } + }, + "node_modules/@polkadot-labs/hdkd": { + "version": "0.0.25", + "resolved": "https://registry.npmjs.org/@polkadot-labs/hdkd/-/hdkd-0.0.25.tgz", + "integrity": "sha512-+yZJC1TE4ZKdfoILw8nGxu3H/klrYXm9GdVB0kcyQDecq320ThUmM1M4l8d1F/3QD0Nez9NwHi9t5B++OgJU5A==", + "license": "MIT", + "dependencies": { + "@polkadot-labs/hdkd-helpers": "~0.0.26" + } + }, + "node_modules/@polkadot-labs/hdkd-helpers": { + "version": "0.0.25", + "resolved": "https://registry.npmjs.org/@polkadot-labs/hdkd-helpers/-/hdkd-helpers-0.0.25.tgz", + "integrity": "sha512-GwHayBuyHKfzvGD0vG47NbjFeiK6rRQHQAn1syut9nt0mhXMg4yb3tJ//IyM317qWuDU3HbD2OIp5jKDEQz2/A==", + "license": "MIT", + "dependencies": { + "@noble/curves": "^2.0.0", + "@noble/hashes": "^2.0.0", + "@scure/base": "^2.0.0", + "@scure/sr25519": "^0.3.0", + "scale-ts": "^1.6.1" + } + }, + "node_modules/@polkadot-labs/hdkd-helpers/node_modules/@noble/curves": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-2.0.1.tgz", + "integrity": "sha512-vs1Az2OOTBiP4q0pwjW5aF0xp9n4MxVrmkFBxc6EKZc6ddYx5gaZiAsZoq0uRRXWbi3AT/sBqn05eRPtn1JCPw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "2.0.1" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@polkadot-labs/hdkd-helpers/node_modules/@scure/base": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-2.0.0.tgz", + "integrity": "sha512-3E1kpuZginKkek01ovG8krQ0Z44E3DHPjc5S2rjJw9lZn3KSQOs8S7wqikF/AH7iRanHypj85uGyxk0XAyC37w==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@polkadot-labs/hdkd/node_modules/@noble/curves": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-2.0.1.tgz", + "integrity": "sha512-vs1Az2OOTBiP4q0pwjW5aF0xp9n4MxVrmkFBxc6EKZc6ddYx5gaZiAsZoq0uRRXWbi3AT/sBqn05eRPtn1JCPw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "2.0.1" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@polkadot-labs/hdkd/node_modules/@polkadot-labs/hdkd-helpers": { + "version": "0.0.26", + "resolved": "https://registry.npmjs.org/@polkadot-labs/hdkd-helpers/-/hdkd-helpers-0.0.26.tgz", + "integrity": "sha512-mp3GCSiOQeh4aPt+DYBQq6UnX/tKgYUH5F75knjW3ATSA90ifEEWWjRan0Bddt4QKYKamaDGadK9GbVREgzQFw==", + "license": "MIT", + "dependencies": { + "@noble/curves": "^2.0.1", + "@noble/hashes": "^2.0.1", + "@scure/base": "^2.0.0", + "@scure/sr25519": "^0.3.0", + "scale-ts": "^1.6.1" + } + }, + "node_modules/@polkadot-labs/hdkd/node_modules/@scure/base": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-2.0.0.tgz", + "integrity": "sha512-3E1kpuZginKkek01ovG8krQ0Z44E3DHPjc5S2rjJw9lZn3KSQOs8S7wqikF/AH7iRanHypj85uGyxk0XAyC37w==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@polkadot/api": { + "version": "16.5.3", + "resolved": "https://registry.npmjs.org/@polkadot/api/-/api-16.5.3.tgz", + "integrity": "sha512-Ptwo0f5Qonmus7KIklsbFcGTdHtNjbTAwl5GGI8Mp0dmBc7Y/ISJpIJX49UrG6FhW6COMa0ItsU87XIWMRwI/Q==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/api-augment": "16.5.3", + "@polkadot/api-base": "16.5.3", + "@polkadot/api-derive": "16.5.3", + "@polkadot/keyring": "^13.5.9", + "@polkadot/rpc-augment": "16.5.3", + "@polkadot/rpc-core": "16.5.3", + "@polkadot/rpc-provider": "16.5.3", + "@polkadot/types": "16.5.3", + "@polkadot/types-augment": "16.5.3", + "@polkadot/types-codec": "16.5.3", + "@polkadot/types-create": "16.5.3", + "@polkadot/types-known": "16.5.3", + "@polkadot/util": "^13.5.9", + "@polkadot/util-crypto": "^13.5.9", + "eventemitter3": "^5.0.1", + "rxjs": "^7.8.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/api-augment": { + "version": "16.5.3", + "resolved": "https://registry.npmjs.org/@polkadot/api-augment/-/api-augment-16.5.3.tgz", + "integrity": "sha512-9+8YKSS66x9qpWS+ZQ/FSm9P4mgE+icD53oAmeIykriPW2gcSTAiNufLwAjmAJAkOLcqbTD7LPjFW6xFlmtYsA==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/api-base": "16.5.3", + "@polkadot/rpc-augment": "16.5.3", + "@polkadot/types": "16.5.3", + "@polkadot/types-augment": "16.5.3", + "@polkadot/types-codec": "16.5.3", + "@polkadot/util": "^13.5.9", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/api-base": { + "version": "16.5.3", + "resolved": "https://registry.npmjs.org/@polkadot/api-base/-/api-base-16.5.3.tgz", + "integrity": "sha512-M1+pY6OFQ1uOB73VQMt2JAGq/UVISVQJISqyfjiUllUc0qIzaDMkcZxRqE34Lwaib3fD3RuIpG6dXqCL9rdzJQ==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/rpc-core": "16.5.3", + "@polkadot/types": "16.5.3", + "@polkadot/util": "^13.5.9", + "rxjs": "^7.8.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/api-derive": { + "version": "16.5.3", + "resolved": "https://registry.npmjs.org/@polkadot/api-derive/-/api-derive-16.5.3.tgz", + "integrity": "sha512-nMsnSC/N1SK1kNhgh2FhrrR1S8bTVH+3WsuBHFRzl+txKHq232IeIn9LpebSvgZdd77PaKaYBxbhYcNaA8Ypew==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/api": "16.5.3", + "@polkadot/api-augment": "16.5.3", + "@polkadot/api-base": "16.5.3", + "@polkadot/rpc-core": "16.5.3", + "@polkadot/types": "16.5.3", + "@polkadot/types-codec": "16.5.3", + "@polkadot/util": "^13.5.9", + "@polkadot/util-crypto": "^13.5.9", + "rxjs": "^7.8.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/api-derive/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@polkadot/api-derive/node_modules/@polkadot/util-crypto": { + "version": "13.5.9", + "resolved": "https://registry.npmjs.org/@polkadot/util-crypto/-/util-crypto-13.5.9.tgz", + "integrity": "sha512-foUesMhxkTk8CZ0/XEcfvHk6I0O+aICqqVJllhOpyp/ZVnrTBKBf59T6RpsXx2pCtBlMsLRvg/6Mw7RND1HqDg==", + "license": "Apache-2.0", + "dependencies": { + "@noble/curves": "^1.3.0", + "@noble/hashes": "^1.3.3", + "@polkadot/networks": "13.5.9", + "@polkadot/util": "13.5.9", + "@polkadot/wasm-crypto": "^7.5.3", + "@polkadot/wasm-util": "^7.5.3", + "@polkadot/x-bigint": "13.5.9", + "@polkadot/x-randomvalues": "13.5.9", + "@scure/base": "^1.1.7", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@polkadot/util": "13.5.9" + } + }, + "node_modules/@polkadot/api/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@polkadot/api/node_modules/@polkadot/util-crypto": { + "version": "13.5.9", + "resolved": "https://registry.npmjs.org/@polkadot/util-crypto/-/util-crypto-13.5.9.tgz", + "integrity": "sha512-foUesMhxkTk8CZ0/XEcfvHk6I0O+aICqqVJllhOpyp/ZVnrTBKBf59T6RpsXx2pCtBlMsLRvg/6Mw7RND1HqDg==", + "license": "Apache-2.0", + "dependencies": { + "@noble/curves": "^1.3.0", + "@noble/hashes": "^1.3.3", + "@polkadot/networks": "13.5.9", + "@polkadot/util": "13.5.9", + "@polkadot/wasm-crypto": "^7.5.3", + "@polkadot/wasm-util": "^7.5.3", + "@polkadot/x-bigint": "13.5.9", + "@polkadot/x-randomvalues": "13.5.9", + "@scure/base": "^1.1.7", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@polkadot/util": "13.5.9" + } + }, + "node_modules/@polkadot/keyring": { + "version": "13.5.9", + "resolved": "https://registry.npmjs.org/@polkadot/keyring/-/keyring-13.5.9.tgz", + "integrity": "sha512-bMCpHDN7U8ytxawjBZ89/he5s3AmEZuOdkM/ABcorh/flXNPfyghjFK27Gy4OKoFxX52yJ2sTHR4NxM87GuFXQ==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/util": "13.5.9", + "@polkadot/util-crypto": "13.5.9", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@polkadot/util": "13.5.9", + "@polkadot/util-crypto": "13.5.9" + } + }, + "node_modules/@polkadot/keyring/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@polkadot/keyring/node_modules/@polkadot/util-crypto": { + "version": "13.5.9", + "resolved": "https://registry.npmjs.org/@polkadot/util-crypto/-/util-crypto-13.5.9.tgz", + "integrity": "sha512-foUesMhxkTk8CZ0/XEcfvHk6I0O+aICqqVJllhOpyp/ZVnrTBKBf59T6RpsXx2pCtBlMsLRvg/6Mw7RND1HqDg==", + "license": "Apache-2.0", + "dependencies": { + "@noble/curves": "^1.3.0", + "@noble/hashes": "^1.3.3", + "@polkadot/networks": "13.5.9", + "@polkadot/util": "13.5.9", + "@polkadot/wasm-crypto": "^7.5.3", + "@polkadot/wasm-util": "^7.5.3", + "@polkadot/x-bigint": "13.5.9", + "@polkadot/x-randomvalues": "13.5.9", + "@scure/base": "^1.1.7", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@polkadot/util": "13.5.9" + } + }, + "node_modules/@polkadot/networks": { + "version": "13.5.9", + "resolved": "https://registry.npmjs.org/@polkadot/networks/-/networks-13.5.9.tgz", + "integrity": "sha512-nmKUKJjiLgcih0MkdlJNMnhEYdwEml2rv/h59ll2+rAvpsVWMTLCb6Cq6q7UC44+8kiWK2UUJMkFU+3PFFxndA==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/util": "13.5.9", + "@substrate/ss58-registry": "^1.51.0", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/rpc-augment": { + "version": "16.5.3", + "resolved": "https://registry.npmjs.org/@polkadot/rpc-augment/-/rpc-augment-16.5.3.tgz", + "integrity": "sha512-q3Y+b0FSwbYe8Qopd4In+9KCL3eH5QmGVvimX7Z8+cvQ9+h+JUA6TP1bfpWBmYJRKlolaljsBQPBWoubchmxSw==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/rpc-core": "16.5.3", + "@polkadot/types": "16.5.3", + "@polkadot/types-codec": "16.5.3", + "@polkadot/util": "^13.5.9", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/rpc-core": { + "version": "16.5.3", + "resolved": "https://registry.npmjs.org/@polkadot/rpc-core/-/rpc-core-16.5.3.tgz", + "integrity": "sha512-UYEIRhO/1uTz/rpWLwUN9Re3c4fuTs0I9RR8dHKpKsH3jZTs1M3CtqME3NNzpGqApY1xb9tZemU/0GfHjCpeBQ==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/rpc-augment": "16.5.3", + "@polkadot/rpc-provider": "16.5.3", + "@polkadot/types": "16.5.3", + "@polkadot/util": "^13.5.9", + "rxjs": "^7.8.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/rpc-provider": { + "version": "16.5.3", + "resolved": "https://registry.npmjs.org/@polkadot/rpc-provider/-/rpc-provider-16.5.3.tgz", + "integrity": "sha512-O7hD82HwjT4XJ4i/G58B52RSDM7arHXSpzahZKz4/wtb4x6d6b4JVdfZoskInadARFi5RwIWCrftwPtpRH81Fw==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/keyring": "^13.5.9", + "@polkadot/types": "16.5.3", + "@polkadot/types-support": "16.5.3", + "@polkadot/util": "^13.5.9", + "@polkadot/util-crypto": "^13.5.9", + "@polkadot/x-fetch": "^13.5.9", + "@polkadot/x-global": "^13.5.9", + "@polkadot/x-ws": "^13.5.9", + "eventemitter3": "^5.0.1", + "mock-socket": "^9.3.1", + "nock": "^13.5.5", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@substrate/connect": "0.8.11" + } + }, + "node_modules/@polkadot/rpc-provider/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@polkadot/rpc-provider/node_modules/@polkadot/util-crypto": { + "version": "13.5.9", + "resolved": "https://registry.npmjs.org/@polkadot/util-crypto/-/util-crypto-13.5.9.tgz", + "integrity": "sha512-foUesMhxkTk8CZ0/XEcfvHk6I0O+aICqqVJllhOpyp/ZVnrTBKBf59T6RpsXx2pCtBlMsLRvg/6Mw7RND1HqDg==", + "license": "Apache-2.0", + "dependencies": { + "@noble/curves": "^1.3.0", + "@noble/hashes": "^1.3.3", + "@polkadot/networks": "13.5.9", + "@polkadot/util": "13.5.9", + "@polkadot/wasm-crypto": "^7.5.3", + "@polkadot/wasm-util": "^7.5.3", + "@polkadot/x-bigint": "13.5.9", + "@polkadot/x-randomvalues": "13.5.9", + "@scure/base": "^1.1.7", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@polkadot/util": "13.5.9" + } + }, + "node_modules/@polkadot/types": { + "version": "16.5.3", + "resolved": "https://registry.npmjs.org/@polkadot/types/-/types-16.5.3.tgz", + "integrity": "sha512-xy9uv/X4iT7uJ7TNCoqbcMkR8ePHwNW6DgpOU+1y1zc/KSu9ZC5i+haFOL68BpmR/QXk99YfuHoKwXvteDmykw==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/keyring": "^13.5.9", + "@polkadot/types-augment": "16.5.3", + "@polkadot/types-codec": "16.5.3", + "@polkadot/types-create": "16.5.3", + "@polkadot/util": "^13.5.9", + "@polkadot/util-crypto": "^13.5.9", + "rxjs": "^7.8.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/types-augment": { + "version": "16.5.3", + "resolved": "https://registry.npmjs.org/@polkadot/types-augment/-/types-augment-16.5.3.tgz", + "integrity": "sha512-SfS4arJUxW6BeCEhLMVPrZwWOLte69k5+/lvEKOKHQA8Mz0MEkD4uqGZGibDjgBgdnu8N+3b+rs+Fn3YfZu4yA==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/types": "16.5.3", + "@polkadot/types-codec": "16.5.3", + "@polkadot/util": "^13.5.9", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/types-codec": { + "version": "16.5.3", + "resolved": "https://registry.npmjs.org/@polkadot/types-codec/-/types-codec-16.5.3.tgz", + "integrity": "sha512-b+oKMrIZrsFH4pPwvGQ6lMS8oFrYAGMy9QSbytA+KDmXAgTCtShz5XGvdQabvsGCjJ45EKgkKpKynVcYh3gk8g==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/util": "^13.5.9", + "@polkadot/x-bigint": "^13.5.9", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/types-create": { + "version": "16.5.3", + "resolved": "https://registry.npmjs.org/@polkadot/types-create/-/types-create-16.5.3.tgz", + "integrity": "sha512-XGnBLNamPh7eQGcHNGFghA/prH7z2BsQ+9EVSbHCvw9ENr/Ow24mmmkZyMG5WM/5I6/4HRdfwFJucYt1GL/p9g==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/types-codec": "16.5.3", + "@polkadot/util": "^13.5.9", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/types-known": { + "version": "16.5.3", + "resolved": "https://registry.npmjs.org/@polkadot/types-known/-/types-known-16.5.3.tgz", + "integrity": "sha512-ZLAZI24bQD0C9CJWYHxrLG8QSmzRzfWa51rlSNwZ9Atsc3R+GeX1YZGc9IljpQxYJCHrCqd6X8TXpAmEJdnbKw==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/networks": "^13.5.9", + "@polkadot/types": "16.5.3", + "@polkadot/types-codec": "16.5.3", + "@polkadot/types-create": "16.5.3", + "@polkadot/util": "^13.5.9", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/types-support": { + "version": "16.5.3", + "resolved": "https://registry.npmjs.org/@polkadot/types-support/-/types-support-16.5.3.tgz", + "integrity": "sha512-ggyIRV+4Kn+aG1PiVT0PE00pAqMveyS3CuFsW9gJnKxeev4VrGfr08R4vw/61D7uIfpilkQdkXNgXAbeN09Mxg==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/util": "^13.5.9", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/types/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@polkadot/types/node_modules/@polkadot/util-crypto": { + "version": "13.5.9", + "resolved": "https://registry.npmjs.org/@polkadot/util-crypto/-/util-crypto-13.5.9.tgz", + "integrity": "sha512-foUesMhxkTk8CZ0/XEcfvHk6I0O+aICqqVJllhOpyp/ZVnrTBKBf59T6RpsXx2pCtBlMsLRvg/6Mw7RND1HqDg==", + "license": "Apache-2.0", + "dependencies": { + "@noble/curves": "^1.3.0", + "@noble/hashes": "^1.3.3", + "@polkadot/networks": "13.5.9", + "@polkadot/util": "13.5.9", + "@polkadot/wasm-crypto": "^7.5.3", + "@polkadot/wasm-util": "^7.5.3", + "@polkadot/x-bigint": "13.5.9", + "@polkadot/x-randomvalues": "13.5.9", + "@scure/base": "^1.1.7", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@polkadot/util": "13.5.9" + } + }, + "node_modules/@polkadot/util": { + "version": "13.5.9", + "resolved": "https://registry.npmjs.org/@polkadot/util/-/util-13.5.9.tgz", + "integrity": "sha512-pIK3XYXo7DKeFRkEBNYhf3GbCHg6dKQisSvdzZwuyzA6m7YxQq4DFw4IE464ve4Z7WsJFt3a6C9uII36hl9EWw==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/x-bigint": "13.5.9", + "@polkadot/x-global": "13.5.9", + "@polkadot/x-textdecoder": "13.5.9", + "@polkadot/x-textencoder": "13.5.9", + "@types/bn.js": "^5.1.6", + "bn.js": "^5.2.1", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/util-crypto": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/@polkadot/util-crypto/-/util-crypto-14.0.1.tgz", + "integrity": "sha512-Cu7AKUzBTsUkbOtyuNzXcTpDjR9QW0fVR56o3gBmzfUCmvO1vlsuGzmmPzqpHymQQ3rrfqV78CPs62EGhw0R+A==", + "license": "Apache-2.0", + "dependencies": { + "@noble/curves": "^1.3.0", + "@noble/hashes": "^1.3.3", + "@polkadot/networks": "14.0.1", + "@polkadot/util": "14.0.1", + "@polkadot/wasm-crypto": "^7.5.3", + "@polkadot/wasm-util": "^7.5.3", + "@polkadot/x-bigint": "14.0.1", + "@polkadot/x-randomvalues": "14.0.1", + "@scure/base": "^1.1.7", + "@scure/sr25519": "^0.2.0", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@polkadot/util": "14.0.1" + } + }, + "node_modules/@polkadot/util-crypto/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@polkadot/util-crypto/node_modules/@polkadot/networks": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/@polkadot/networks/-/networks-14.0.1.tgz", + "integrity": "sha512-wGlBtXDkusRAj4P7uxfPz80gLO1+j99MLBaQi3bEym2xrFrFhgIWVHOZlBit/1PfaBjhX2Z8XjRxaM2w1p7w2w==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/util": "14.0.1", + "@substrate/ss58-registry": "^1.51.0", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/util-crypto/node_modules/@polkadot/util": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/@polkadot/util/-/util-14.0.1.tgz", + "integrity": "sha512-764HhxkPV3x5rM0/p6QdynC2dw26n+SaE+jisjx556ViCd4E28Ke4xSPef6C0Spy4aoXf2gt0PuLEcBvd6fVZg==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/x-bigint": "14.0.1", + "@polkadot/x-global": "14.0.1", + "@polkadot/x-textdecoder": "14.0.1", + "@polkadot/x-textencoder": "14.0.1", + "@types/bn.js": "^5.1.6", + "bn.js": "^5.2.1", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/util-crypto/node_modules/@polkadot/x-bigint": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/@polkadot/x-bigint/-/x-bigint-14.0.1.tgz", + "integrity": "sha512-gfozjGnebr2rqURs31KtaWumbW4rRZpbiluhlmai6luCNrf5u8pB+oLA35kPEntrsLk9PnIG9OsC/n4hEtx4OQ==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/x-global": "14.0.1", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/util-crypto/node_modules/@polkadot/x-global": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/@polkadot/x-global/-/x-global-14.0.1.tgz", + "integrity": "sha512-aCI44DJU4fU0XXqrrSGIpi7JrZXK2kpe0jaQ2p6oDVXOOYEnZYXnMhTTmBE1lF/xtxzX50MnZrrU87jziU0qbA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/util-crypto/node_modules/@polkadot/x-randomvalues": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/@polkadot/x-randomvalues/-/x-randomvalues-14.0.1.tgz", + "integrity": "sha512-/XkQcvshzJLHITuPrN3zmQKuFIPdKWoaiHhhVLD6rQWV60lTXA3ajw3ocju8ZN7xRxnweMS9Ce0kMPYa0NhRMg==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/x-global": "14.0.1", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@polkadot/util": "14.0.1", + "@polkadot/wasm-util": "*" + } + }, + "node_modules/@polkadot/util-crypto/node_modules/@polkadot/x-textdecoder": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/@polkadot/x-textdecoder/-/x-textdecoder-14.0.1.tgz", + "integrity": "sha512-CcWiPCuPVJsNk4Vq43lgFHqLRBQHb4r9RD7ZIYgmwoebES8TNm4g2ew9ToCzakFKSpzKu6I07Ne9wv/dt5zLuw==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/x-global": "14.0.1", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/util-crypto/node_modules/@polkadot/x-textencoder": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/@polkadot/x-textencoder/-/x-textencoder-14.0.1.tgz", + "integrity": "sha512-VY51SpQmF1ccmAGLfxhYnAe95Spfz049WZ/+kK4NfsGF9WejxVdU53Im5C80l45r8qHuYQsCWU3+t0FNunh2Kg==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/x-global": "14.0.1", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/util-crypto/node_modules/@scure/sr25519": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@scure/sr25519/-/sr25519-0.2.0.tgz", + "integrity": "sha512-uUuLP7Z126XdSizKtrCGqYyR3b3hYtJ6Fg/XFUXmc2//k2aXHDLqZwFeXxL97gg4XydPROPVnuaHGF2+xriSKg==", + "license": "MIT", + "dependencies": { + "@noble/curves": "~1.9.2", + "@noble/hashes": "~1.8.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@polkadot/wasm-bridge": { + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/@polkadot/wasm-bridge/-/wasm-bridge-7.5.3.tgz", + "integrity": "sha512-mUvwwNH+uP1wqpMuHjmEwHxRIaVc5csmb+ukycWQGhzwhpXe/0fvBEU2TQ8kwgqO2MU0FS3hN/QcIWKfPRJgxQ==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/wasm-util": "7.5.3", + "tslib": "^2.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@polkadot/util": "*", + "@polkadot/x-randomvalues": "*" + } + }, + "node_modules/@polkadot/wasm-crypto": { + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/@polkadot/wasm-crypto/-/wasm-crypto-7.5.3.tgz", + "integrity": "sha512-dmKUM9vw1wrnCHGuIeOtQo1pwuSF7fkyF4TYimTn3tAa0+3cDctYBErtGxgUeqP0Bo4Q0Of4/vnHlSk5Rbt9Uw==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/wasm-bridge": "7.5.3", + "@polkadot/wasm-crypto-asmjs": "7.5.3", + "@polkadot/wasm-crypto-init": "7.5.3", + "@polkadot/wasm-crypto-wasm": "7.5.3", + "@polkadot/wasm-util": "7.5.3", + "tslib": "^2.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@polkadot/util": "*", + "@polkadot/x-randomvalues": "*" + } + }, + "node_modules/@polkadot/wasm-crypto-asmjs": { + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/@polkadot/wasm-crypto-asmjs/-/wasm-crypto-asmjs-7.5.3.tgz", + "integrity": "sha512-fSbbjI+4p0U3PQ8nOz/3p7euHriSdh+2CSywNuXHa8fMaYlMqCKt9K7+HI8CQ4RZNvZWDq+Py1nEDEkM4rZrvw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@polkadot/util": "*" + } + }, + "node_modules/@polkadot/wasm-crypto-init": { + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/@polkadot/wasm-crypto-init/-/wasm-crypto-init-7.5.3.tgz", + "integrity": "sha512-KvUpxqvW70XhuDiw/N6rM8fQ7zRjIFblw+vdJ0/wwyagwg9jrYNA9TMei5ksQd9sxGCGXN/xJmwHJXuUjkocmg==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/wasm-bridge": "7.5.3", + "@polkadot/wasm-crypto-asmjs": "7.5.3", + "@polkadot/wasm-crypto-wasm": "7.5.3", + "@polkadot/wasm-util": "7.5.3", + "tslib": "^2.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@polkadot/util": "*", + "@polkadot/x-randomvalues": "*" + } + }, + "node_modules/@polkadot/wasm-crypto-wasm": { + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/@polkadot/wasm-crypto-wasm/-/wasm-crypto-wasm-7.5.3.tgz", + "integrity": "sha512-fc88+HyVxebB/40GVgGUOLBqyO3C571DXWPTFmtt5EX9H8gw7Jg0Bkitz7hgSVP2x4FjXpqS9UNTJ8trVH0x1A==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/wasm-util": "7.5.3", + "tslib": "^2.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@polkadot/util": "*" + } + }, + "node_modules/@polkadot/wasm-util": { + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/@polkadot/wasm-util/-/wasm-util-7.5.3.tgz", + "integrity": "sha512-hBr9bbjS+Yr7DrDUSkIIuvlTSoAlI8WXuo9YEB4C76j130u/cl+zyq6Iy/WnaTE6QH+8i9DhM8QTety6TqYnUQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@polkadot/util": "*" + } + }, + "node_modules/@polkadot/x-bigint": { + "version": "13.5.9", + "resolved": "https://registry.npmjs.org/@polkadot/x-bigint/-/x-bigint-13.5.9.tgz", + "integrity": "sha512-JVW6vw3e8fkcRyN9eoc6JIl63MRxNQCP/tuLdHWZts1tcAYao0hpWUzteqJY93AgvmQ91KPsC1Kf3iuuZCi74g==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/x-global": "13.5.9", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/x-fetch": { + "version": "13.5.9", + "resolved": "https://registry.npmjs.org/@polkadot/x-fetch/-/x-fetch-13.5.9.tgz", + "integrity": "sha512-urwXQZtT4yYROiRdJS6zHu18J/jCoAGpbgPIAjwdqjT11t9XIq4SjuPMxD19xBRhbYe9ocWV8i1KHuoMbZgKbA==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/x-global": "13.5.9", + "node-fetch": "^3.3.2", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/x-global": { + "version": "13.5.9", + "resolved": "https://registry.npmjs.org/@polkadot/x-global/-/x-global-13.5.9.tgz", + "integrity": "sha512-zSRWvELHd3Q+bFkkI1h2cWIqLo1ETm+MxkNXLec3lB56iyq/MjWBxfXnAFFYFayvlEVneo7CLHcp+YTFd9aVSA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/x-randomvalues": { + "version": "13.5.9", + "resolved": "https://registry.npmjs.org/@polkadot/x-randomvalues/-/x-randomvalues-13.5.9.tgz", + "integrity": "sha512-Uuuz3oubf1JCCK97fsnVUnHvk4BGp/W91mQWJlgl5TIOUSSTIRr+lb5GurCfl4kgnQq53Zi5fJV+qR9YumbnZw==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/x-global": "13.5.9", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@polkadot/util": "13.5.9", + "@polkadot/wasm-util": "*" + } + }, + "node_modules/@polkadot/x-textdecoder": { + "version": "13.5.9", + "resolved": "https://registry.npmjs.org/@polkadot/x-textdecoder/-/x-textdecoder-13.5.9.tgz", + "integrity": "sha512-W2HhVNUbC/tuFdzNMbnXAWsIHSg9SC9QWDNmFD3nXdSzlXNgL8NmuiwN2fkYvCQBtp/XSoy0gDLx0C+Fo19cfw==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/x-global": "13.5.9", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/x-textencoder": { + "version": "13.5.9", + "resolved": "https://registry.npmjs.org/@polkadot/x-textencoder/-/x-textencoder-13.5.9.tgz", + "integrity": "sha512-SG0MHnLUgn1ZxFdm0KzMdTHJ47SfqFhdIPMcGA0Mg/jt2rwrfrP3jtEIJMsHfQpHvfsNPfv55XOMmoPWuQnP/Q==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/x-global": "13.5.9", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/x-ws": { + "version": "13.5.9", + "resolved": "https://registry.npmjs.org/@polkadot/x-ws/-/x-ws-13.5.9.tgz", + "integrity": "sha512-NKVgvACTIvKT8CjaQu9d0dERkZsWIZngX/4NVSjc01WHmln4F4y/zyBdYn/Z2V0Zw28cISx+lB4qxRmqTe7gbg==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/x-global": "13.5.9", + "tslib": "^2.8.0", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz", + "integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz", + "integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz", + "integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz", + "integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz", + "integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz", + "integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz", + "integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz", + "integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz", + "integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz", + "integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz", + "integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz", + "integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz", + "integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz", + "integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz", + "integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz", + "integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz", + "integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz", + "integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz", + "integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz", + "integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz", + "integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz", + "integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rx-state/core": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@rx-state/core/-/core-0.1.4.tgz", + "integrity": "sha512-Z+3hjU2xh1HisLxt+W5hlYX/eGSDaXXP+ns82gq/PLZpkXLu0uwcNUh9RLY3Clq4zT+hSsA3vcpIGt6+UAb8rQ==", + "license": "MIT", + "peerDependencies": { + "rxjs": ">=7" + } + }, + "node_modules/@scure/base": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz", + "integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.7.0.tgz", + "integrity": "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==", + "license": "MIT", + "dependencies": { + "@noble/curves": "~1.9.0", + "@noble/hashes": "~1.8.0", + "@scure/base": "~1.2.5" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.6.0.tgz", + "integrity": "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "~1.8.0", + "@scure/base": "~1.2.5" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/sr25519": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@scure/sr25519/-/sr25519-0.3.0.tgz", + "integrity": "sha512-SKsinX2sImunfcsH3seGrwH/OayBwwaJqVN8J1cJBNRCfbBq5q0jyTKGa9PcW1HWv9vXT6Yuq41JsxFLvF59ew==", + "license": "MIT", + "dependencies": { + "@noble/curves": "~2.0.0", + "@noble/hashes": "~2.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/sr25519/node_modules/@noble/curves": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-2.0.1.tgz", + "integrity": "sha512-vs1Az2OOTBiP4q0pwjW5aF0xp9n4MxVrmkFBxc6EKZc6ddYx5gaZiAsZoq0uRRXWbi3AT/sBqn05eRPtn1JCPw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "2.0.1" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@sec-ant/readable-stream": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", + "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", + "license": "MIT" + }, + "node_modules/@sindresorhus/merge-streams": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", + "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@substrate/connect": { + "version": "0.8.11", + "resolved": "https://registry.npmjs.org/@substrate/connect/-/connect-0.8.11.tgz", + "integrity": "sha512-ofLs1PAO9AtDdPbdyTYj217Pe+lBfTLltdHDs3ds8no0BseoLeAGxpz1mHfi7zB4IxI3YyAiLjH6U8cw4pj4Nw==", + "license": "GPL-3.0-only", + "optional": true, + "dependencies": { + "@substrate/connect-extension-protocol": "^2.0.0", + "@substrate/connect-known-chains": "^1.1.5", + "@substrate/light-client-extension-helpers": "^1.0.0", + "smoldot": "2.0.26" + } + }, + "node_modules/@substrate/connect-extension-protocol": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@substrate/connect-extension-protocol/-/connect-extension-protocol-2.2.2.tgz", + "integrity": "sha512-t66jwrXA0s5Goq82ZtjagLNd7DPGCNjHeehRlE/gcJmJ+G56C0W+2plqOMRicJ8XGR1/YFnUSEqUFiSNbjGrAA==", + "license": "GPL-3.0-only", + "optional": true + }, + "node_modules/@substrate/connect-known-chains": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/@substrate/connect-known-chains/-/connect-known-chains-1.10.3.tgz", + "integrity": "sha512-OJEZO1Pagtb6bNE3wCikc2wrmvEU5x7GxFFLqqbz1AJYYxSlrPCGu4N2og5YTExo4IcloNMQYFRkBGue0BKZ4w==", + "license": "GPL-3.0-only", + "optional": true + }, + "node_modules/@substrate/light-client-extension-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@substrate/light-client-extension-helpers/-/light-client-extension-helpers-1.0.0.tgz", + "integrity": "sha512-TdKlni1mBBZptOaeVrKnusMg/UBpWUORNDv5fdCaJklP4RJiFOzBCrzC+CyVI5kQzsXBisZ+2pXm+rIjS38kHg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@polkadot-api/json-rpc-provider": "^0.0.1", + "@polkadot-api/json-rpc-provider-proxy": "^0.1.0", + "@polkadot-api/observable-client": "^0.3.0", + "@polkadot-api/substrate-client": "^0.1.2", + "@substrate/connect-extension-protocol": "^2.0.0", + "@substrate/connect-known-chains": "^1.1.5", + "rxjs": "^7.8.1" + }, + "peerDependencies": { + "smoldot": "2.x" + } + }, + "node_modules/@substrate/light-client-extension-helpers/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "optional": true, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@substrate/light-client-extension-helpers/node_modules/@polkadot-api/json-rpc-provider": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@polkadot-api/json-rpc-provider/-/json-rpc-provider-0.0.1.tgz", + "integrity": "sha512-/SMC/l7foRjpykLTUTacIH05H3mr9ip8b5xxfwXlVezXrNVLp3Cv0GX6uItkKd+ZjzVPf3PFrDF2B2/HLSNESA==", + "license": "MIT", + "optional": true + }, + "node_modules/@substrate/light-client-extension-helpers/node_modules/@polkadot-api/json-rpc-provider-proxy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@polkadot-api/json-rpc-provider-proxy/-/json-rpc-provider-proxy-0.1.0.tgz", + "integrity": "sha512-8GSFE5+EF73MCuLQm8tjrbCqlgclcHBSRaswvXziJ0ZW7iw3UEMsKkkKvELayWyBuOPa2T5i1nj6gFOeIsqvrg==", + "license": "MIT", + "optional": true + }, + "node_modules/@substrate/light-client-extension-helpers/node_modules/@polkadot-api/metadata-builders": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@polkadot-api/metadata-builders/-/metadata-builders-0.3.2.tgz", + "integrity": "sha512-TKpfoT6vTb+513KDzMBTfCb/ORdgRnsS3TDFpOhAhZ08ikvK+hjHMt5plPiAX/OWkm1Wc9I3+K6W0hX5Ab7MVg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@polkadot-api/substrate-bindings": "0.6.0", + "@polkadot-api/utils": "0.1.0" + } + }, + "node_modules/@substrate/light-client-extension-helpers/node_modules/@polkadot-api/observable-client": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@polkadot-api/observable-client/-/observable-client-0.3.2.tgz", + "integrity": "sha512-HGgqWgEutVyOBXoGOPp4+IAq6CNdK/3MfQJmhCJb8YaJiaK4W6aRGrdQuQSTPHfERHCARt9BrOmEvTXAT257Ug==", + "license": "MIT", + "optional": true, + "dependencies": { + "@polkadot-api/metadata-builders": "0.3.2", + "@polkadot-api/substrate-bindings": "0.6.0", + "@polkadot-api/utils": "0.1.0" + }, + "peerDependencies": { + "@polkadot-api/substrate-client": "0.1.4", + "rxjs": ">=7.8.0" + } + }, + "node_modules/@substrate/light-client-extension-helpers/node_modules/@polkadot-api/substrate-bindings": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@polkadot-api/substrate-bindings/-/substrate-bindings-0.6.0.tgz", + "integrity": "sha512-lGuhE74NA1/PqdN7fKFdE5C1gNYX357j1tWzdlPXI0kQ7h3kN0zfxNOpPUN7dIrPcOFZ6C0tRRVrBylXkI6xPw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@noble/hashes": "^1.3.1", + "@polkadot-api/utils": "0.1.0", + "@scure/base": "^1.1.1", + "scale-ts": "^1.6.0" + } + }, + "node_modules/@substrate/light-client-extension-helpers/node_modules/@polkadot-api/substrate-client": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@polkadot-api/substrate-client/-/substrate-client-0.1.4.tgz", + "integrity": "sha512-MljrPobN0ZWTpn++da9vOvt+Ex+NlqTlr/XT7zi9sqPtDJiQcYl+d29hFAgpaeTqbeQKZwz3WDE9xcEfLE8c5A==", + "license": "MIT", + "optional": true, + "dependencies": { + "@polkadot-api/json-rpc-provider": "0.0.1", + "@polkadot-api/utils": "0.1.0" + } + }, + "node_modules/@substrate/light-client-extension-helpers/node_modules/@polkadot-api/utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@polkadot-api/utils/-/utils-0.1.0.tgz", + "integrity": "sha512-MXzWZeuGxKizPx2Xf/47wx9sr/uxKw39bVJUptTJdsaQn/TGq+z310mHzf1RCGvC1diHM8f593KrnDgc9oNbJA==", + "license": "MIT", + "optional": true + }, + "node_modules/@substrate/ss58-registry": { + "version": "1.51.0", + "resolved": "https://registry.npmjs.org/@substrate/ss58-registry/-/ss58-registry-1.51.0.tgz", + "integrity": "sha512-TWDurLiPxndFgKjVavCniytBIw+t4ViOi7TYp9h/D0NMmkEc9klFTo+827eyEJ0lELpqO207Ey7uGxUa+BS1jQ==", + "license": "Apache-2.0" + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/bn.js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-5.2.0.tgz", + "integrity": "sha512-DLbJ1BPqxvQhIGbeu8VbUC1DiAiahHtAYvA0ZEAa4P31F7IaArc8z3C3BRQdWX4mtLQuABG4yzp76ZrS02Ui1Q==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/bn.js/node_modules/@types/node": { + "version": "24.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", + "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/bn.js/node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "license": "MIT" + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/@types/mocha": { + "version": "10.0.10", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz", + "integrity": "sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.1.tgz", + "integrity": "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/normalize-package-data": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", + "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", + "license": "MIT" + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/ws/node_modules/@types/node": { + "version": "24.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", + "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/ws/node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "license": "MIT" + }, + "node_modules/abitype": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.2.0.tgz", + "integrity": "sha512-fD3ROjckUrWsybaSor2AdWxzA0e/DSyV2dA4aYd7bd8orHsoJjl09fOgKfUkTDfk0BsDGBf4NBgu/c7JoS2Npw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/wevm" + }, + "peerDependencies": { + "typescript": ">=5.0.4", + "zod": "^3.22.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/aes-js": { + "version": "4.0.0-beta.5", + "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-4.0.0-beta.5.tgz", + "integrity": "sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==", + "license": "MIT" + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "license": "MIT" + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/assert": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz", + "integrity": "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "is-nan": "^1.3.2", + "object-is": "^1.1.5", + "object.assign": "^4.1.4", + "util": "^0.12.5" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/bn.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.2.tgz", + "integrity": "sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw==", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "license": "ISC" + }, + "node_modules/bundle-require": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-5.1.0.tgz", + "integrity": "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==", + "license": "MIT", + "dependencies": { + "load-tsconfig": "^0.2.3" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "peerDependencies": { + "esbuild": ">=0.18" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/chai": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.1.tgz", + "integrity": "sha512-p4Z49OGG5W/WBCPSS/dH3jQ73kD6tiMmUM+bckNK6Jr5JHMG3k9bg/BvKR8lKmtVBKmOiuVaV2ws8s9oSbwysg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-spinners": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-3.3.0.tgz", + "integrity": "sha512-/+40ljC3ONVnYIttjMWrlL51nItDAbBrq2upN8BPyvGU/2n5Oxw3tbNwORCaNuNqLJnxGqOfjUuhsv7l5Q4IsQ==", + "license": "MIT", + "engines": { + "node": ">=18.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/commander": { + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.2.tgz", + "integrity": "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deepmerge-ts": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", + "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/detect-indent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-7.0.2.tgz", + "integrity": "sha512-y+8xyqdGLL+6sh0tVeHcfP/QDd8gUgbasolJJpY7NgeQGSZ739bDtSiaiDgtoicy+mtYB81dKLxO9xRhCyIB3A==", + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dotenv": { + "version": "17.2.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.1.tgz", + "integrity": "sha512-kQhDYKZecqnM0fCnzI5eIv5L4cAe/iRI+HqMbO/hbRdTAeXDG+M9FjipUxNfbARuEg4iHIbhnhs78BCHNbSxEQ==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ethers": { + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.16.0.tgz", + "integrity": "sha512-U1wulmetNymijEhpSEQ7Ct/P/Jw9/e7R1j5XIbPRydgV2DjLVMsULDlNksq3RQnFgKoLlZf88ijYtWEXcPa07A==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/ethers-io/" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@adraffy/ens-normalize": "1.10.1", + "@noble/curves": "1.2.0", + "@noble/hashes": "1.3.2", + "@types/node": "22.7.5", + "aes-js": "4.0.0-beta.5", + "tslib": "2.7.0", + "ws": "8.17.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/ethers/node_modules/@adraffy/ens-normalize": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz", + "integrity": "sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==", + "license": "MIT" + }, + "node_modules/ethers/node_modules/@noble/curves": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", + "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.3.2" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/ethers/node_modules/@noble/hashes": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/ethers/node_modules/@types/node": { + "version": "22.7.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz", + "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/ethers/node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", + "license": "0BSD" + }, + "node_modules/ethers/node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "license": "MIT" + }, + "node_modules/ethers/node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "license": "MIT" + }, + "node_modules/execa": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-9.6.1.tgz", + "integrity": "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==", + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^4.0.0", + "cross-spawn": "^7.0.6", + "figures": "^6.1.0", + "get-stream": "^9.0.0", + "human-signals": "^8.0.1", + "is-plain-obj": "^4.1.0", + "is-stream": "^4.0.1", + "npm-run-path": "^6.0.0", + "pretty-ms": "^9.2.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^4.0.0", + "yoctocolors": "^2.1.1" + }, + "engines": { + "node": "^18.19.0 || >=20.5.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/figures": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", + "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==", + "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fix-dts-default-cjs-exports": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fix-dts-default-cjs-exports/-/fix-dts-default-cjs-exports-1.0.1.tgz", + "integrity": "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==", + "license": "MIT", + "dependencies": { + "magic-string": "^0.30.17", + "mlly": "^1.7.4", + "rollup": "^4.34.8" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + } + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/fs.promises.exists": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/fs.promises.exists/-/fs.promises.exists-1.1.4.tgz", + "integrity": "sha512-lJzUGWbZn8vhGWBedA+RYjB/BeJ+3458ljUfmplqhIeb6ewzTFWNPCR1HCiYCkXV9zxcHz9zXkJzMsEgDLzh3Q==", + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/fs.promises.exists?sponsor=1" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", + "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "license": "MIT", + "dependencies": { + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/hosted-git-info": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.2.tgz", + "integrity": "sha512-M422h7o/BR3rmCQ8UHi7cyyMqKltdP9Uo+J2fXK+RSAY+wTcKOIRyhTuKv4qn+DJf3g+PL890AzId5KZpX+CBg==", + "license": "ISC", + "dependencies": { + "lru-cache": "^11.1.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "11.2.4", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", + "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/human-signals": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.1.tgz", + "integrity": "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/index-to-position": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.2.0.tgz", + "integrity": "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-arguments": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", + "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-nan": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz", + "integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/isows": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/isows/-/isows-1.0.6.tgz", + "integrity": "sha512-lPHCayd40oW98/I0uvgaHKWCSvkzY27LjWLbtzOm64yQ+G3Q5npjjbdppU65iZXkK1Zt+kH9pfegli0AYfwYYw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "peerDependencies": { + "ws": "*" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "license": "ISC" + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, + "node_modules/load-tsconfig": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz", + "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols/node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mlly": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.0.tgz", + "integrity": "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==", + "license": "MIT", + "dependencies": { + "acorn": "^8.15.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.1" + } + }, + "node_modules/mocha": { + "version": "11.7.5", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.5.tgz", + "integrity": "sha512-mTT6RgopEYABzXWFx+GcJ+ZQ32kp4fMf0xvpZIIfSq9Z8lC/++MtcCnQ9t5FP2veYEP95FIYSvW+U9fV4xrlig==", + "license": "MIT", + "dependencies": { + "browser-stdout": "^1.3.1", + "chokidar": "^4.0.1", + "debug": "^4.3.5", + "diff": "^7.0.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^10.4.5", + "he": "^1.2.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^9.0.5", + "ms": "^2.1.3", + "picocolors": "^1.1.1", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^9.2.0", + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1", + "yargs-unparser": "^2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/mocha/node_modules/diff": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", + "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/mock-socket": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/mock-socket/-/mock-socket-9.3.1.tgz", + "integrity": "sha512-qxBgB7Qa2sEQgHFjj0dSigq7fX4k6Saisd5Nelwp2q8mlbAFh5dHV9JTTlF8viYJLSSWgMCZFUom8PJcMNBoJw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nock": { + "version": "13.5.6", + "resolved": "https://registry.npmjs.org/nock/-/nock-13.5.6.tgz", + "integrity": "sha512-o2zOYiCpzRqSzPj0Zt/dQ/DqZeYoaQ7TUonc/xUPjCGl9WeHpNbxgVvOquXYAaJzI0M9BXV3HTzG0p8IUAbBTQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.1.0", + "json-stringify-safe": "^5.0.1", + "propagate": "^2.0.0" + }, + "engines": { + "node": ">= 10.13" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/normalize-package-data": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-8.0.0.tgz", + "integrity": "sha512-RWk+PI433eESQ7ounYxIp67CYuVsS1uYSonX3kA6ps/3LWfjVQa/ptEg6Y3T6uAMq1mWpX9PQ+qx+QaHpsc7gQ==", + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^9.0.0", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-run-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz", + "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==", + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-is": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", + "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-9.0.0.tgz", + "integrity": "sha512-m0pg2zscbYgWbqRR6ABga5c3sZdEon7bSgjnlXC64kxtxLOyjRcbbUkLj7HFyy/FTD+P2xdBWu8snGhYI0jc4A==", + "license": "MIT", + "dependencies": { + "chalk": "^5.6.2", + "cli-cursor": "^5.0.0", + "cli-spinners": "^3.2.0", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^2.1.0", + "log-symbols": "^7.0.1", + "stdin-discarder": "^0.2.2", + "string-width": "^8.1.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ora/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ora/node_modules/log-symbols": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz", + "integrity": "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==", + "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0", + "yoctocolors": "^2.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/string-width": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.0.tgz", + "integrity": "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==", + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/ox": { + "version": "0.6.7", + "resolved": "https://registry.npmjs.org/ox/-/ox-0.6.7.tgz", + "integrity": "sha512-17Gk/eFsFRAZ80p5eKqv89a57uXjd3NgIf1CaXojATPBuujVc/fQSVhBeAU9JCRB+k7J50WQAyWTxK19T9GgbA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "dependencies": { + "@adraffy/ens-normalize": "^1.10.1", + "@noble/curves": "^1.6.0", + "@noble/hashes": "^1.5.0", + "@scure/bip32": "^1.5.0", + "@scure/bip39": "^1.4.0", + "abitype": "^1.0.6", + "eventemitter3": "5.0.1" + }, + "peerDependencies": { + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/ox/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, + "node_modules/parse-json": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz", + "integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "index-to-position": "^1.1.0", + "type-fest": "^4.39.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-ms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", + "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/polkadot-api": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/polkadot-api/-/polkadot-api-1.22.0.tgz", + "integrity": "sha512-uREBLroPbnJxBBQ+qSkKLF493qukX4PAg32iThlELrZdxfNNgro6nvWRdVmBv73tFHvf+nyWWHKTx1c57nbixg==", + "license": "MIT", + "dependencies": { + "@polkadot-api/cli": "0.16.3", + "@polkadot-api/ink-contracts": "0.4.3", + "@polkadot-api/json-rpc-provider": "0.0.4", + "@polkadot-api/known-chains": "0.9.15", + "@polkadot-api/logs-provider": "0.0.6", + "@polkadot-api/metadata-builders": "0.13.7", + "@polkadot-api/metadata-compatibility": "0.4.1", + "@polkadot-api/observable-client": "0.17.0", + "@polkadot-api/pjs-signer": "0.6.17", + "@polkadot-api/polkadot-sdk-compat": "2.3.3", + "@polkadot-api/polkadot-signer": "0.1.6", + "@polkadot-api/signer": "0.2.11", + "@polkadot-api/sm-provider": "0.1.14", + "@polkadot-api/smoldot": "0.3.14", + "@polkadot-api/substrate-bindings": "0.16.5", + "@polkadot-api/substrate-client": "0.4.7", + "@polkadot-api/utils": "0.2.0", + "@polkadot-api/ws-provider": "0.7.4", + "@rx-state/core": "^0.1.4" + }, + "bin": { + "papi": "bin/cli.mjs", + "polkadot-api": "bin/cli.mjs" + }, + "peerDependencies": { + "rxjs": ">=7.8.0" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/prettier": { + "version": "3.7.4", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.4.tgz", + "integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/pretty-ms": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.0.tgz", + "integrity": "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==", + "license": "MIT", + "dependencies": { + "parse-ms": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/propagate": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", + "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/read-pkg": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-10.0.0.tgz", + "integrity": "sha512-A70UlgfNdKI5NSvTTfHzLQj7NJRpJ4mT5tGafkllJ4wh71oYuGm/pzphHcmW4s35iox56KSK721AihodoXSc/A==", + "license": "MIT", + "dependencies": { + "@types/normalize-package-data": "^2.4.4", + "normalize-package-data": "^8.0.0", + "parse-json": "^8.3.0", + "type-fest": "^5.2.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg/node_modules/type-fest": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.3.0.tgz", + "integrity": "sha512-d9CwU93nN0IA1QL+GSNDdwLAu1Ew5ZjTwupvedwg3WdfoH6pIDvYQ2hV0Uc2nKBLPq7NB5apCx57MLS5qlmO5g==", + "license": "(MIT OR CC0-1.0)", + "dependencies": { + "tagged-tag": "^1.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/rollup": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz", + "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==", + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.53.3", + "@rollup/rollup-android-arm64": "4.53.3", + "@rollup/rollup-darwin-arm64": "4.53.3", + "@rollup/rollup-darwin-x64": "4.53.3", + "@rollup/rollup-freebsd-arm64": "4.53.3", + "@rollup/rollup-freebsd-x64": "4.53.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.3", + "@rollup/rollup-linux-arm-musleabihf": "4.53.3", + "@rollup/rollup-linux-arm64-gnu": "4.53.3", + "@rollup/rollup-linux-arm64-musl": "4.53.3", + "@rollup/rollup-linux-loong64-gnu": "4.53.3", + "@rollup/rollup-linux-ppc64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-musl": "4.53.3", + "@rollup/rollup-linux-s390x-gnu": "4.53.3", + "@rollup/rollup-linux-x64-gnu": "4.53.3", + "@rollup/rollup-linux-x64-musl": "4.53.3", + "@rollup/rollup-openharmony-arm64": "4.53.3", + "@rollup/rollup-win32-arm64-msvc": "4.53.3", + "@rollup/rollup-win32-ia32-msvc": "4.53.3", + "@rollup/rollup-win32-x64-gnu": "4.53.3", + "@rollup/rollup-win32-x64-msvc": "4.53.3", + "fsevents": "~2.3.2" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/scale-ts": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/scale-ts/-/scale-ts-1.6.1.tgz", + "integrity": "sha512-PBMc2AWc6wSEqJYBDPcyCLUj9/tMKnLX70jLOSndMtcUoLQucP/DM0vnQo1wJAYjTrQiq8iG9rD0q6wFzgjH7g==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/smoldot": { + "version": "2.0.26", + "resolved": "https://registry.npmjs.org/smoldot/-/smoldot-2.0.26.tgz", + "integrity": "sha512-F+qYmH4z2s2FK+CxGj8moYcd1ekSIKH8ywkdqlOz88Dat35iB1DIYL11aILN46YSGMzQW/lbJNS307zBSDN5Ig==", + "license": "GPL-3.0-or-later WITH Classpath-exception-2.0", + "optional": true, + "dependencies": { + "ws": "^8.8.1" + } + }, + "node_modules/sort-keys": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-5.1.0.tgz", + "integrity": "sha512-aSbHV0DaBcr7u0PVHXzM6NbZNAtrr9sF6+Qfs9UUVG7Ll3jQ6hHi8F/xqIIcn2rvIVbr0v/2zyjSdwSV47AgLQ==", + "license": "MIT", + "dependencies": { + "is-plain-obj": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/source-map": { + "version": "0.8.0-beta.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", + "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", + "license": "BSD-3-Clause", + "dependencies": { + "whatwg-url": "^7.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.22", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.22.tgz", + "integrity": "sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ==", + "license": "CC0-1.0" + }, + "node_modules/stdin-discarder": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", + "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz", + "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/sucrase/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/tagged-tag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz", + "integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tr46": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", + "license": "MIT", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "license": "Apache-2.0" + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/tsc-prog": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tsc-prog/-/tsc-prog-2.3.0.tgz", + "integrity": "sha512-ycET2d75EgcX7y8EmG4KiZkLAwUzbY4xRhA6NU0uVbHkY4ZjrAAuzTMxXI85kOwATqPnBI5C/7y7rlpY0xdqHA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "typescript": ">=4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tsup": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/tsup/-/tsup-8.5.0.tgz", + "integrity": "sha512-VmBp77lWNQq6PfuMqCHD3xWl22vEoWsKajkF8t+yMBawlUS8JzEI+vOVMeuNZIuMML8qXRizFKi9oD5glKQVcQ==", + "license": "MIT", + "dependencies": { + "bundle-require": "^5.1.0", + "cac": "^6.7.14", + "chokidar": "^4.0.3", + "consola": "^3.4.0", + "debug": "^4.4.0", + "esbuild": "^0.25.0", + "fix-dts-default-cjs-exports": "^1.0.0", + "joycon": "^3.1.1", + "picocolors": "^1.1.1", + "postcss-load-config": "^6.0.1", + "resolve-from": "^5.0.0", + "rollup": "^4.34.8", + "source-map": "0.8.0-beta.0", + "sucrase": "^3.35.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.11", + "tree-kill": "^1.2.2" + }, + "bin": { + "tsup": "dist/cli-default.js", + "tsup-node": "dist/cli-node.js" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@microsoft/api-extractor": "^7.36.0", + "@swc/core": "^1", + "postcss": "^8.4.12", + "typescript": ">=4.5.0" + }, + "peerDependenciesMeta": { + "@microsoft/api-extractor": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "postcss": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ufo": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz", + "integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==", + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unicorn-magic": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/viem": { + "version": "2.23.4", + "resolved": "https://registry.npmjs.org/viem/-/viem-2.23.4.tgz", + "integrity": "sha512-UQquuolKlS1w5H5e0Fd1KKoUlIPJryIEBzY5AUhGyV1ka+9O6+3uYVhUzj6RbvGK0PtsMKn2ddwPZFwjNDVU/A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "dependencies": { + "@noble/curves": "1.8.1", + "@noble/hashes": "1.7.1", + "@scure/bip32": "1.6.2", + "@scure/bip39": "1.5.4", + "abitype": "1.0.8", + "isows": "1.0.6", + "ox": "0.6.7", + "ws": "8.18.0" + }, + "peerDependencies": { + "typescript": ">=5.0.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/viem/node_modules/@noble/curves": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.8.1.tgz", + "integrity": "sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.7.1" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/viem/node_modules/@noble/hashes": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.1.tgz", + "integrity": "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/viem/node_modules/@scure/bip32": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.6.2.tgz", + "integrity": "sha512-t96EPDMbtGgtb7onKKqxRLfE5g05k7uHnHRM2xdE6BP/ZmxaLtPek4J4KfVn/90IQNrU1IOAqMgiDtUdtbe3nw==", + "license": "MIT", + "dependencies": { + "@noble/curves": "~1.8.1", + "@noble/hashes": "~1.7.1", + "@scure/base": "~1.2.2" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/viem/node_modules/@scure/bip32/node_modules/@noble/curves": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.8.2.tgz", + "integrity": "sha512-vnI7V6lFNe0tLAuJMu+2sX+FcL14TaCWy1qiczg1VwRmPrpQCdq5ESXQMqUc2tluRNf6irBXrWbl1mGN8uaU/g==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.7.2" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/viem/node_modules/@scure/bip32/node_modules/@noble/hashes": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.2.tgz", + "integrity": "sha512-biZ0NUSxyjLLqo6KxEJ1b+C2NAx0wtDoFvCaXHGgUkeHzf3Xc1xKumFKREuT7f7DARNZ/slvYUwFG6B0f2b6hQ==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/viem/node_modules/@scure/bip39": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.5.4.tgz", + "integrity": "sha512-TFM4ni0vKvCfBpohoh+/lY05i9gRbSwXWngAsF4CABQxoaOHijxuaZ2R6cStDQ5CHtHO9aGJTr4ksVJASRRyMA==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "~1.7.1", + "@scure/base": "~1.2.4" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/viem/node_modules/abitype": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.0.8.tgz", + "integrity": "sha512-ZeiI6h3GnW06uYDLx0etQtX/p8E24UaHHBj57RSjK7YBFe7iuVn07EDpOeP451D06sF27VOz9JJPlIKJmXgkEg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/wevm" + }, + "peerDependencies": { + "typescript": ">=5.0.4", + "zod": "^3 >=3.22.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/viem/node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", + "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", + "license": "MIT", + "dependencies": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/workerpool": { + "version": "9.3.4", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-9.3.4.tgz", + "integrity": "sha512-TmPRQYYSAnnDiEB0P/Ytip7bFGvqnSU6I2BcuSw7Hx+JSg/DsUi5ebYfc8GYaSdpuvOcEs6dXxPurOYpe9QFwg==", + "license": "Apache-2.0" + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/write-file-atomic": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", + "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/write-json-file": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/write-json-file/-/write-json-file-6.0.0.tgz", + "integrity": "sha512-MNHcU3f9WxnNyR6MxsYSj64Jz0+dwIpisWKWq9gqLj/GwmA9INg3BZ3vt70/HB3GEwrnDQWr4RPrywnhNzmUFA==", + "license": "MIT", + "dependencies": { + "detect-indent": "^7.0.1", + "is-plain-obj": "^4.1.0", + "sort-keys": "^5.0.0", + "write-file-atomic": "^5.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/write-package": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/write-package/-/write-package-7.2.0.tgz", + "integrity": "sha512-uMQTubF/vcu+Wd0b5BGtDmiXePd/+44hUWQz2nZPbs92/BnxRo74tqs+hqDo12RLiEd+CXFKUwxvvIZvtt34Jw==", + "license": "MIT", + "dependencies": { + "deepmerge-ts": "^7.1.0", + "read-pkg": "^9.0.1", + "sort-keys": "^5.0.0", + "type-fest": "^4.23.0", + "write-json-file": "^6.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/write-package/node_modules/hosted-git-info": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", + "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==", + "license": "ISC", + "dependencies": { + "lru-cache": "^10.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/write-package/node_modules/normalize-package-data": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.2.tgz", + "integrity": "sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==", + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^7.0.0", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/write-package/node_modules/read-pkg": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-9.0.1.tgz", + "integrity": "sha512-9viLL4/n1BJUCT1NXVTdS1jtm80yDEgR5T4yCelII49Mbj0v1rZdKqj7zCiYdbB0CuCgdrvHcNogAKTFPBocFA==", + "license": "MIT", + "dependencies": { + "@types/normalize-package-data": "^2.4.3", + "normalize-package-data": "^6.0.0", + "parse-json": "^8.0.0", + "type-fest": "^4.6.0", + "unicorn-magic": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/write-package/node_modules/unicorn-magic": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", + "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "license": "MIT", + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-unparser/node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoctocolors": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz", + "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/contract-tests/src/contracts/precompileWrapper.sol b/contract-tests/src/contracts/precompileWrapper.sol new file mode 100644 index 0000000000..9f5fe242c1 --- /dev/null +++ b/contract-tests/src/contracts/precompileWrapper.sol @@ -0,0 +1,362 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +// Precompile addresses +address constant ISUBTENSOR_BALANCE_TRANSFER_ADDRESS = 0x0000000000000000000000000000000000000800; +address constant IMETAGRAPH_ADDRESS = 0x0000000000000000000000000000000000000802; +address constant ISUBNET_ADDRESS = 0x0000000000000000000000000000000000000803; +address constant INEURON_ADDRESS = 0x0000000000000000000000000000000000000804; +address constant ISTAKING_V2_ADDRESS = 0x0000000000000000000000000000000000000805; +address constant IUID_LOOKUP_ADDRESS = 0x0000000000000000000000000000000000000806; +address constant IALPHA_ADDRESS = 0x0000000000000000000000000000000000000808; +address constant ICROWDLOAN_ADDRESS = 0x0000000000000000000000000000000000000809; +address constant ILEASING_ADDRESS = 0x000000000000000000000000000000000000080a; +address constant IPROXY_ADDRESS = 0x000000000000000000000000000000000000080b; +address constant IADDRESS_MAPPING_ADDRESS = 0x000000000000000000000000000000000000080C; + +// Interface definitions +interface ISubtensorBalanceTransfer { + function transfer(bytes32 data) external payable; +} + +interface IMetagraph { + function getUidCount(uint16 netuid) external view returns (uint16); +} + +interface ISubnet { + function registerNetwork( + bytes32 hotkey, + string memory subnetName, + string memory githubRepo, + string memory subnetContact, + string memory subnetUrl, + string memory discord, + string memory description, + string memory additional + ) external payable; + function getServingRateLimit(uint16 netuid) external view returns (uint64); +} + +interface INeuron { + function burnedRegister(uint16 netuid, bytes32 hotkey) external payable; +} + +interface IStaking { + function addStake( + bytes32 hotkey, + uint256 amount, + uint256 netuid + ) external payable; + function removeStake( + bytes32 hotkey, + uint256 amount, + uint256 netuid + ) external payable; + function getTotalColdkeyStake( + bytes32 coldkey + ) external view returns (uint256); + function getTotalHotkeyStake( + bytes32 hotkey + ) external view returns (uint256); +} + +struct LookupItem { + uint16 uid; + uint64 block_associated; +} + +interface IUidLookup { + function uidLookup( + uint16 netuid, + address evm_address, + uint16 limit + ) external view returns (LookupItem[] memory); +} + +interface IAlpha { + function getAlphaPrice(uint16 netuid) external view returns (uint256); +} + +struct CrowdloanInfo { + bytes32 creator; + uint64 deposit; + uint64 min_contribution; + uint32 end; + uint64 cap; + bytes32 funds_account; + uint64 raised; + bool has_target_address; + bytes32 target_address; + bool finalized; + uint32 contributors_count; +} + +interface ICrowdloan { + function getCrowdloan( + uint32 crowdloanId + ) external view returns (CrowdloanInfo memory); + function getContribution( + uint32 crowdloanId, + bytes32 coldkey + ) external view returns (uint64); + function create( + uint64 deposit, + uint64 minContribution, + uint64 cap, + uint32 end, + address targetAddress + ) external payable; +} + +struct LeaseInfo { + bytes32 beneficiary; + bytes32 coldkey; + bytes32 hotkey; + uint8 emissions_share; + bool has_end_block; + uint32 end_block; + uint16 netuid; + uint64 cost; +} + +interface ILeasing { + function getContributorShare( + uint32 leaseId, + bytes32 contributor + ) external view returns (uint128, uint128); + function createLeaseCrowdloan( + uint64 crowdloanDeposit, + uint64 crowdloanMinContribution, + uint64 crowdloanCap, + uint32 crowdloanEnd, + uint8 leasingEmissionsShare, + bool hasLeasingEndBlock, + uint32 leasingEndBlock + ) external payable; +} + +interface IProxy { + struct ProxyInfo { + bytes32 delegate; + uint256 proxy_type; + uint256 delay; + } + + function addProxy( + bytes32 delegate, + uint8 proxy_type, + uint32 delay + ) external; + function proxyCall( + bytes32 real, + uint8[] memory force_proxy_type, + uint8[] memory call + ) external; + function getProxies( + bytes32 account + ) external view returns (ProxyInfo[] memory); +} + +interface IAddressMapping { + function addressMapping( + address target_address + ) external view returns (bytes32); +} + +/** + * @title PrecompileWrapper + * @dev A wrapper contract that calls all precompile functions directly + * instead of using low-level calls like address.call() + */ +contract PrecompileWrapper { + ISubtensorBalanceTransfer public constant balanceTransfer = + ISubtensorBalanceTransfer(ISUBTENSOR_BALANCE_TRANSFER_ADDRESS); + IMetagraph public constant metagraph = IMetagraph(IMETAGRAPH_ADDRESS); + ISubnet public constant subnet = ISubnet(ISUBNET_ADDRESS); + INeuron public constant neuron = INeuron(INEURON_ADDRESS); + IStaking public constant staking = IStaking(ISTAKING_V2_ADDRESS); + IUidLookup public constant uidLookupPrecompile = + IUidLookup(IUID_LOOKUP_ADDRESS); + IAlpha public constant alpha = IAlpha(IALPHA_ADDRESS); + ICrowdloan public constant crowdloan = ICrowdloan(ICROWDLOAN_ADDRESS); + ILeasing public constant leasing = ILeasing(ILEASING_ADDRESS); + IProxy public constant proxy = IProxy(IPROXY_ADDRESS); + IAddressMapping public constant addressMappingPrecompile = + IAddressMapping(IADDRESS_MAPPING_ADDRESS); + + // ============ SubtensorBalanceTransfer Functions ============ + function transfer(bytes32 data) external payable { + balanceTransfer.transfer{value: msg.value}(data); + } + + // ============ Metagraph Functions ============ + + function getUidCount(uint16 netuid) external view returns (uint16) { + return metagraph.getUidCount(netuid); + } + + // ============ Subnet Functions ============ + + function registerNetworkWithDetails( + bytes32 hotkey, + string memory subnetName, + string memory githubRepo, + string memory subnetContact, + string memory subnetUrl, + string memory discord, + string memory description, + string memory additional + ) external payable { + subnet.registerNetwork( + hotkey, + subnetName, + githubRepo, + subnetContact, + subnetUrl, + discord, + description, + additional + ); + } + + function getServingRateLimit(uint16 netuid) external view returns (uint64) { + return subnet.getServingRateLimit(netuid); + } + + // ============ Neuron Functions ============ + + function burnedRegister(uint16 netuid, bytes32 hotkey) external payable { + neuron.burnedRegister{value: msg.value}(netuid, hotkey); + } + + // ============ Staking Functions ============ + function addStake( + bytes32 hotkey, + uint256 amount, + uint256 netuid + ) external payable { + staking.addStake(hotkey, amount, netuid); + } + + function removeStake( + bytes32 hotkey, + uint256 amount, + uint256 netuid + ) external payable { + staking.removeStake(hotkey, amount, netuid); + } + + function getTotalColdkeyStake( + bytes32 coldkey + ) external view returns (uint256) { + return staking.getTotalColdkeyStake(coldkey); + } + + function getTotalHotkeyStake( + bytes32 hotkey + ) external view returns (uint256) { + return staking.getTotalHotkeyStake(hotkey); + } + + // ============ Alpha Functions ============ + + function getAlphaPrice(uint16 netuid) external view returns (uint256) { + return alpha.getAlphaPrice(netuid); + } + + // ============ Address Mapping Functions ============ + + function addressMapping( + address target_address + ) external view returns (bytes32) { + return addressMappingPrecompile.addressMapping(target_address); + } + + // ============ Proxy Functions ============ + + function proxyCall( + bytes32 real, + uint8[] memory force_proxy_type, + uint8[] memory call + ) external { + proxy.proxyCall(real, force_proxy_type, call); + } + + function addProxy( + bytes32 delegate, + uint8 proxy_type, + uint32 delay + ) external { + proxy.addProxy(delegate, proxy_type, delay); + } + + function getProxies( + bytes32 account + ) external view returns (IProxy.ProxyInfo[] memory) { + return proxy.getProxies(account); + } + + // ============ UID Lookup Functions ============ + + function uidLookup( + uint16 netuid, + address evm_address, + uint16 limit + ) external view returns (LookupItem[] memory) { + return uidLookupPrecompile.uidLookup(netuid, evm_address, limit); + } + + // ============ Crowdloan Functions ============ + + function getCrowdloan( + uint32 crowdloanId + ) external view returns (CrowdloanInfo memory) { + return crowdloan.getCrowdloan(crowdloanId); + } + + function getContribution( + uint32 crowdloanId, + bytes32 coldkey + ) external view returns (uint64) { + return crowdloan.getContribution(crowdloanId, coldkey); + } + + function createCrowdloan( + uint64 deposit, + uint64 minContribution, + uint64 cap, + uint32 end, + address targetAddress + ) external payable { + crowdloan.create(deposit, minContribution, cap, end, targetAddress); + } + + // ============ Leasing Functions ============ + + function getContributorShare( + uint32 leaseId, + bytes32 contributor + ) external view returns (uint128, uint128) { + return leasing.getContributorShare(leaseId, contributor); + } + + function createLeaseCrowdloan( + uint64 crowdloanDeposit, + uint64 crowdloanMinContribution, + uint64 crowdloanCap, + uint32 crowdloanEnd, + uint8 leasingEmissionsShare, + bool hasLeasingEndBlock, + uint32 leasingEndBlock + ) external payable { + leasing.createLeaseCrowdloan( + crowdloanDeposit, + crowdloanMinContribution, + crowdloanCap, + crowdloanEnd, + leasingEmissionsShare, + hasLeasingEndBlock, + leasingEndBlock + ); + } +} diff --git a/contract-tests/src/contracts/precompileWrapper.ts b/contract-tests/src/contracts/precompileWrapper.ts new file mode 100644 index 0000000000..9916b735e9 --- /dev/null +++ b/contract-tests/src/contracts/precompileWrapper.ts @@ -0,0 +1,714 @@ +export const PRECOMPILE_WRAPPER_ABI = [ + { + "inputs": [ + { + "internalType": "bytes32", + "name": "delegate", + "type": "bytes32" + }, + { + "internalType": "uint8", + "name": "proxy_type", + "type": "uint8" + }, + { + "internalType": "uint32", + "name": "delay", + "type": "uint32" + } + ], + "name": "addProxy", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "hotkey", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "netuid", + "type": "uint256" + } + ], + "name": "addStake", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "target_address", + "type": "address" + } + ], + "name": "addressMapping", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "addressMappingPrecompile", + "outputs": [ + { + "internalType": "contract IAddressMapping", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "alpha", + "outputs": [ + { + "internalType": "contract IAlpha", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "balanceTransfer", + "outputs": [ + { + "internalType": "contract ISubtensorBalanceTransfer", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint16", + "name": "netuid", + "type": "uint16" + }, + { + "internalType": "bytes32", + "name": "hotkey", + "type": "bytes32" + } + ], + "name": "burnedRegister", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "deposit", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "minContribution", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "cap", + "type": "uint64" + }, + { + "internalType": "uint32", + "name": "end", + "type": "uint32" + }, + { + "internalType": "address", + "name": "targetAddress", + "type": "address" + } + ], + "name": "createCrowdloan", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "crowdloanDeposit", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "crowdloanMinContribution", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "crowdloanCap", + "type": "uint64" + }, + { + "internalType": "uint32", + "name": "crowdloanEnd", + "type": "uint32" + }, + { + "internalType": "uint8", + "name": "leasingEmissionsShare", + "type": "uint8" + }, + { + "internalType": "bool", + "name": "hasLeasingEndBlock", + "type": "bool" + }, + { + "internalType": "uint32", + "name": "leasingEndBlock", + "type": "uint32" + } + ], + "name": "createLeaseCrowdloan", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "crowdloan", + "outputs": [ + { + "internalType": "contract ICrowdloan", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint16", + "name": "netuid", + "type": "uint16" + } + ], + "name": "getAlphaPrice", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint32", + "name": "crowdloanId", + "type": "uint32" + }, + { + "internalType": "bytes32", + "name": "coldkey", + "type": "bytes32" + } + ], + "name": "getContribution", + "outputs": [ + { + "internalType": "uint64", + "name": "", + "type": "uint64" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint32", + "name": "leaseId", + "type": "uint32" + }, + { + "internalType": "bytes32", + "name": "contributor", + "type": "bytes32" + } + ], + "name": "getContributorShare", + "outputs": [ + { + "internalType": "uint128", + "name": "", + "type": "uint128" + }, + { + "internalType": "uint128", + "name": "", + "type": "uint128" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint32", + "name": "crowdloanId", + "type": "uint32" + } + ], + "name": "getCrowdloan", + "outputs": [ + { + "components": [ + { + "internalType": "bytes32", + "name": "creator", + "type": "bytes32" + }, + { + "internalType": "uint64", + "name": "deposit", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "min_contribution", + "type": "uint64" + }, + { + "internalType": "uint32", + "name": "end", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "cap", + "type": "uint64" + }, + { + "internalType": "bytes32", + "name": "funds_account", + "type": "bytes32" + }, + { + "internalType": "uint64", + "name": "raised", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "has_target_address", + "type": "bool" + }, + { + "internalType": "bytes32", + "name": "target_address", + "type": "bytes32" + }, + { + "internalType": "bool", + "name": "finalized", + "type": "bool" + }, + { + "internalType": "uint32", + "name": "contributors_count", + "type": "uint32" + } + ], + "internalType": "struct CrowdloanInfo", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "account", + "type": "bytes32" + } + ], + "name": "getProxies", + "outputs": [ + { + "components": [ + { + "internalType": "bytes32", + "name": "delegate", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "proxy_type", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "delay", + "type": "uint256" + } + ], + "internalType": "struct IProxy.ProxyInfo[]", + "name": "", + "type": "tuple[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint16", + "name": "netuid", + "type": "uint16" + } + ], + "name": "getServingRateLimit", + "outputs": [ + { + "internalType": "uint64", + "name": "", + "type": "uint64" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "coldkey", + "type": "bytes32" + } + ], + "name": "getTotalColdkeyStake", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "hotkey", + "type": "bytes32" + } + ], + "name": "getTotalHotkeyStake", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint16", + "name": "netuid", + "type": "uint16" + } + ], + "name": "getUidCount", + "outputs": [ + { + "internalType": "uint16", + "name": "", + "type": "uint16" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "leasing", + "outputs": [ + { + "internalType": "contract ILeasing", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "metagraph", + "outputs": [ + { + "internalType": "contract IMetagraph", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "neuron", + "outputs": [ + { + "internalType": "contract INeuron", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "proxy", + "outputs": [ + { + "internalType": "contract IProxy", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "real", + "type": "bytes32" + }, + { + "internalType": "uint8[]", + "name": "force_proxy_type", + "type": "uint8[]" + }, + { + "internalType": "uint8[]", + "name": "call", + "type": "uint8[]" + } + ], + "name": "proxyCall", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "hotkey", + "type": "bytes32" + }, + { + "internalType": "string", + "name": "subnetName", + "type": "string" + }, + { + "internalType": "string", + "name": "githubRepo", + "type": "string" + }, + { + "internalType": "string", + "name": "subnetContact", + "type": "string" + }, + { + "internalType": "string", + "name": "subnetUrl", + "type": "string" + }, + { + "internalType": "string", + "name": "discord", + "type": "string" + }, + { + "internalType": "string", + "name": "description", + "type": "string" + }, + { + "internalType": "string", + "name": "additional", + "type": "string" + } + ], + "name": "registerNetworkWithDetails", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "hotkey", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "netuid", + "type": "uint256" + } + ], + "name": "removeStake", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "staking", + "outputs": [ + { + "internalType": "contract IStaking", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "subnet", + "outputs": [ + { + "internalType": "contract ISubnet", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "data", + "type": "bytes32" + } + ], + "name": "transfer", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint16", + "name": "netuid", + "type": "uint16" + }, + { + "internalType": "address", + "name": "evm_address", + "type": "address" + }, + { + "internalType": "uint16", + "name": "limit", + "type": "uint16" + } + ], + "name": "uidLookup", + "outputs": [ + { + "components": [ + { + "internalType": "uint16", + "name": "uid", + "type": "uint16" + }, + { + "internalType": "uint64", + "name": "block_associated", + "type": "uint64" + } + ], + "internalType": "struct LookupItem[]", + "name": "", + "type": "tuple[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "uidLookupPrecompile", + "outputs": [ + { + "internalType": "contract IUidLookup", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + } +]; + +export const PRECOMPILE_WRAPPER_BYTECODE = "6080604052348015600e575f5ffd5b50612bb98061001c5f395ff3fe6080604052600436106101d6575f3560e01c80637d691e3011610101578063b1f789ef11610094578063d75e3e0d11610063578063d75e3e0d146106a9578063db1d0fd5146106d3578063ec556889146106fd578063fc6679fb14610727576101d6565b8063b1f789ef146105fd578063bfe252a214610639578063caf2ebf214610663578063cd6f4eb11461068d576101d6565b80639f246f6f116100d05780639f246f6f14610551578063a21762761461058d578063ac3166bf146105b7578063afed65f9146105e1576101d6565b80637d691e30146104815780638bba466c1461049d57806394e3ac6f146104d9578063998538c414610515576101d6565b80634c378a96116101795780635e25f3f8116101485780635e25f3f8146103d157806369e38bc3146103ed57806371214e27146104295780637444dadc14610445576101d6565b80634c378a96146103175780634cf088d9146103415780635b53ddde1461036b5780635b7210c514610395576101d6565b80631f193572116101b55780631f193572146102665780631fc9b141146102a25780633175bd98146102be5780634054ecca146102fb576101d6565b80620ae759146101da5780630494cd9a146102025780630cadeda51461023e575b5f5ffd5b3480156101e5575f5ffd5b5061020060048036038101906101fb91906113ab565b610751565b005b34801561020d575f5ffd5b506102286004803603810190610223919061148d565b6107c1565b60405161023591906114c7565b60405180910390f35b348015610249575f5ffd5b50610264600480360381019061025f9190611519565b610843565b005b348015610271575f5ffd5b5061028c600480360381019061028791906115a0565b6108b4565b60405161029991906115da565b60405180910390f35b6102bc60048036038101906102b79190611626565b610936565b005b3480156102c9575f5ffd5b506102e460048036038101906102df9190611676565b6109a7565b6040516102f29291906116de565b60405180910390f35b61031560048036038101906103109190611705565b610a2f565b005b348015610322575f5ffd5b5061032b610a9d565b604051610338919061179e565b60405180910390f35b34801561034c575f5ffd5b50610355610aa3565b60405161036291906117d7565b60405180910390f35b348015610376575f5ffd5b5061037f610aa9565b60405161038c9190611810565b60405180910390f35b3480156103a0575f5ffd5b506103bb60048036038101906103b69190611676565b610aaf565b6040516103c8919061184b565b60405180910390f35b6103eb60048036038101906103e69190611914565b610b34565b005b3480156103f8575f5ffd5b50610413600480360381019061040e91906115a0565b610bb4565b6040516104209190611a98565b60405180910390f35b610443600480360381019061043e9190611adb565b610c36565b005b348015610450575f5ffd5b5061046b600480360381019061046691906115a0565b610cad565b604051610478919061184b565b60405180910390f35b61049b60048036038101906104969190611626565b610d2f565b005b3480156104a8575f5ffd5b506104c360048036038101906104be9190611b52565b610da0565b6040516104d09190611ca3565b60405180910390f35b3480156104e4575f5ffd5b506104ff60048036038101906104fa9190611cbd565b610e2a565b60405161050c9190611ddf565b60405180910390f35b348015610520575f5ffd5b5061053b60048036038101906105369190611cbd565b610eb0565b6040516105489190611a98565b60405180910390f35b34801561055c575f5ffd5b5061057760048036038101906105729190611cbd565b610f32565b6040516105849190611a98565b60405180910390f35b348015610598575f5ffd5b506105a1610fb4565b6040516105ae9190611e1f565b60405180910390f35b3480156105c2575f5ffd5b506105cb610fba565b6040516105d89190611e58565b60405180910390f35b6105fb60048036038101906105f69190611e9b565b610fc0565b005b348015610608575f5ffd5b50610623600480360381019061061e9190611f38565b61103d565b604051610630919061206c565b60405180910390f35b348015610644575f5ffd5b5061064d6110c9565b60405161065a91906120ac565b60405180910390f35b34801561066e575f5ffd5b506106776110cf565b60405161068491906120e5565b60405180910390f35b6106a760048036038101906106a29190611cbd565b6110d5565b005b3480156106b4575f5ffd5b506106bd611142565b6040516106ca919061211e565b60405180910390f35b3480156106de575f5ffd5b506106e7611148565b6040516106f49190612157565b60405180910390f35b348015610708575f5ffd5b5061071161114e565b60405161071e9190612190565b60405180910390f35b348015610732575f5ffd5b5061073b611154565b60405161074891906121c9565b60405180910390f35b61080b73ffffffffffffffffffffffffffffffffffffffff16620ae7598484846040518463ffffffff1660e01b815260040161078f93929190612299565b5f604051808303815f87803b1580156107a6575f5ffd5b505af11580156107b8573d5f5f3e3d5ffd5b50505050505050565b5f61080c73ffffffffffffffffffffffffffffffffffffffff16630494cd9a836040518263ffffffff1660e01b81526004016107fd91906122eb565b602060405180830381865afa158015610818573d5f5f3e3d5ffd5b505050506040513d601f19601f8201168201806040525081019061083c9190612318565b9050919050565b61080b73ffffffffffffffffffffffffffffffffffffffff16630cadeda58484846040518463ffffffff1660e01b815260040161088293929190612361565b5f604051808303815f87803b158015610899575f5ffd5b505af11580156108ab573d5f5f3e3d5ffd5b50505050505050565b5f61080273ffffffffffffffffffffffffffffffffffffffff16631f193572836040518263ffffffff1660e01b81526004016108f091906115da565b602060405180830381865afa15801561090b573d5f5f3e3d5ffd5b505050506040513d601f19601f8201168201806040525081019061092f91906123aa565b9050919050565b61080573ffffffffffffffffffffffffffffffffffffffff16631fc9b1418484846040518463ffffffff1660e01b8152600401610975939291906123d5565b5f604051808303815f87803b15801561098c575f5ffd5b505af115801561099e573d5f5f3e3d5ffd5b50505050505050565b5f5f61080a73ffffffffffffffffffffffffffffffffffffffff16633175bd9885856040518363ffffffff1660e01b81526004016109e692919061240a565b6040805180830381865afa158015610a00573d5f5f3e3d5ffd5b505050506040513d601f19601f82011682018060405250810190610a24919061245b565b915091509250929050565b61080473ffffffffffffffffffffffffffffffffffffffff16634054ecca83836040518363ffffffff1660e01b8152600401610a6c929190612499565b5f604051808303815f87803b158015610a83575f5ffd5b505af1158015610a95573d5f5f3e3d5ffd5b505050505050565b61080481565b61080581565b61080a81565b5f61080973ffffffffffffffffffffffffffffffffffffffff16635b7210c584846040518363ffffffff1660e01b8152600401610aed92919061240a565b602060405180830381865afa158015610b08573d5f5f3e3d5ffd5b505050506040513d601f19601f82011682018060405250810190610b2c91906124d4565b905092915050565b61080373ffffffffffffffffffffffffffffffffffffffff16631cf98c6b89898989898989896040518963ffffffff1660e01b8152600401610b7d98979695949392919061255f565b5f604051808303815f87803b158015610b94575f5ffd5b505af1158015610ba6573d5f5f3e3d5ffd5b505050505050505050505050565b5f61080873ffffffffffffffffffffffffffffffffffffffff166369e38bc3836040518263ffffffff1660e01b8152600401610bf091906115da565b602060405180830381865afa158015610c0b573d5f5f3e3d5ffd5b505050506040513d601f19601f82011682018060405250810190610c2f9190612620565b9050919050565b61080973ffffffffffffffffffffffffffffffffffffffff1663127e1adb86868686866040518663ffffffff1660e01b8152600401610c7995949392919061264b565b5f604051808303815f87803b158015610c90575f5ffd5b505af1158015610ca2573d5f5f3e3d5ffd5b505050505050505050565b5f61080373ffffffffffffffffffffffffffffffffffffffff16637444dadc836040518263ffffffff1660e01b8152600401610ce991906115da565b602060405180830381865afa158015610d04573d5f5f3e3d5ffd5b505050506040513d601f19601f82011682018060405250810190610d2891906124d4565b9050919050565b61080573ffffffffffffffffffffffffffffffffffffffff16637d691e308484846040518463ffffffff1660e01b8152600401610d6e939291906123d5565b5f604051808303815f87803b158015610d85575f5ffd5b505af1158015610d97573d5f5f3e3d5ffd5b50505050505050565b610da861115a565b61080973ffffffffffffffffffffffffffffffffffffffff16638bba466c836040518263ffffffff1660e01b8152600401610de3919061269c565b61016060405180830381865afa158015610dff573d5f5f3e3d5ffd5b505050506040513d601f19601f82011682018060405250810190610e2391906127ea565b9050919050565b606061080b73ffffffffffffffffffffffffffffffffffffffff166394e3ac6f836040518263ffffffff1660e01b8152600401610e6791906114c7565b5f60405180830381865afa158015610e81573d5f5f3e3d5ffd5b505050506040513d5f823e3d601f19601f82011682018060405250810190610ea99190612937565b9050919050565b5f61080573ffffffffffffffffffffffffffffffffffffffff1663998538c4836040518263ffffffff1660e01b8152600401610eec91906114c7565b602060405180830381865afa158015610f07573d5f5f3e3d5ffd5b505050506040513d601f19601f82011682018060405250810190610f2b9190612620565b9050919050565b5f61080573ffffffffffffffffffffffffffffffffffffffff16639f246f6f836040518263ffffffff1660e01b8152600401610f6e91906114c7565b602060405180830381865afa158015610f89573d5f5f3e3d5ffd5b505050506040513d601f19601f82011682018060405250810190610fad9190612620565b9050919050565b61080681565b61080c81565b61080a73ffffffffffffffffffffffffffffffffffffffff1663afed65f9888888888888886040518863ffffffff1660e01b8152600401611007979695949392919061298d565b5f604051808303815f87803b15801561101e575f5ffd5b505af1158015611030573d5f5f3e3d5ffd5b5050505050505050505050565b606061080673ffffffffffffffffffffffffffffffffffffffff1663b1f789ef8585856040518463ffffffff1660e01b815260040161107e939291906129fa565b5f60405180830381865afa158015611098573d5f5f3e3d5ffd5b505050506040513d5f823e3d601f19601f820116820180604052508101906110c09190612b3c565b90509392505050565b61080981565b61080381565b61080073ffffffffffffffffffffffffffffffffffffffff1663cd6f4eb134836040518363ffffffff1660e01b815260040161111191906114c7565b5f604051808303818588803b158015611128575f5ffd5b505af115801561113a573d5f5f3e3d5ffd5b505050505050565b61080081565b61080881565b61080b81565b61080281565b6040518061016001604052805f81526020015f67ffffffffffffffff1681526020015f67ffffffffffffffff1681526020015f63ffffffff1681526020015f67ffffffffffffffff1681526020015f81526020015f67ffffffffffffffff1681526020015f151581526020015f81526020015f151581526020015f63ffffffff1681525090565b5f604051905090565b5f5ffd5b5f5ffd5b5f819050919050565b611204816111f2565b811461120e575f5ffd5b50565b5f8135905061121f816111fb565b92915050565b5f5ffd5b5f601f19601f8301169050919050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b61126f82611229565b810181811067ffffffffffffffff8211171561128e5761128d611239565b5b80604052505050565b5f6112a06111e1565b90506112ac8282611266565b919050565b5f67ffffffffffffffff8211156112cb576112ca611239565b5b602082029050602081019050919050565b5f5ffd5b5f60ff82169050919050565b6112f5816112e0565b81146112ff575f5ffd5b50565b5f81359050611310816112ec565b92915050565b5f611328611323846112b1565b611297565b9050808382526020820190506020840283018581111561134b5761134a6112dc565b5b835b8181101561137457806113608882611302565b84526020840193505060208101905061134d565b5050509392505050565b5f82601f83011261139257611391611225565b5b81356113a2848260208601611316565b91505092915050565b5f5f5f606084860312156113c2576113c16111ea565b5b5f6113cf86828701611211565b935050602084013567ffffffffffffffff8111156113f0576113ef6111ee565b5b6113fc8682870161137e565b925050604084013567ffffffffffffffff81111561141d5761141c6111ee565b5b6114298682870161137e565b9150509250925092565b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f61145c82611433565b9050919050565b61146c81611452565b8114611476575f5ffd5b50565b5f8135905061148781611463565b92915050565b5f602082840312156114a2576114a16111ea565b5b5f6114af84828501611479565b91505092915050565b6114c1816111f2565b82525050565b5f6020820190506114da5f8301846114b8565b92915050565b5f63ffffffff82169050919050565b6114f8816114e0565b8114611502575f5ffd5b50565b5f81359050611513816114ef565b92915050565b5f5f5f606084860312156115305761152f6111ea565b5b5f61153d86828701611211565b935050602061154e86828701611302565b925050604061155f86828701611505565b9150509250925092565b5f61ffff82169050919050565b61157f81611569565b8114611589575f5ffd5b50565b5f8135905061159a81611576565b92915050565b5f602082840312156115b5576115b46111ea565b5b5f6115c28482850161158c565b91505092915050565b6115d481611569565b82525050565b5f6020820190506115ed5f8301846115cb565b92915050565b5f819050919050565b611605816115f3565b811461160f575f5ffd5b50565b5f81359050611620816115fc565b92915050565b5f5f5f6060848603121561163d5761163c6111ea565b5b5f61164a86828701611211565b935050602061165b86828701611612565b925050604061166c86828701611612565b9150509250925092565b5f5f6040838503121561168c5761168b6111ea565b5b5f61169985828601611505565b92505060206116aa85828601611211565b9150509250929050565b5f6fffffffffffffffffffffffffffffffff82169050919050565b6116d8816116b4565b82525050565b5f6040820190506116f15f8301856116cf565b6116fe60208301846116cf565b9392505050565b5f5f6040838503121561171b5761171a6111ea565b5b5f6117288582860161158c565b925050602061173985828601611211565b9150509250929050565b5f819050919050565b5f61176661176161175c84611433565b611743565b611433565b9050919050565b5f6117778261174c565b9050919050565b5f6117888261176d565b9050919050565b6117988161177e565b82525050565b5f6020820190506117b15f83018461178f565b92915050565b5f6117c18261176d565b9050919050565b6117d1816117b7565b82525050565b5f6020820190506117ea5f8301846117c8565b92915050565b5f6117fa8261176d565b9050919050565b61180a816117f0565b82525050565b5f6020820190506118235f830184611801565b92915050565b5f67ffffffffffffffff82169050919050565b61184581611829565b82525050565b5f60208201905061185e5f83018461183c565b92915050565b5f5ffd5b5f67ffffffffffffffff82111561188257611881611239565b5b61188b82611229565b9050602081019050919050565b828183375f83830152505050565b5f6118b86118b384611868565b611297565b9050828152602081018484840111156118d4576118d3611864565b5b6118df848285611898565b509392505050565b5f82601f8301126118fb576118fa611225565b5b813561190b8482602086016118a6565b91505092915050565b5f5f5f5f5f5f5f5f610100898b031215611931576119306111ea565b5b5f61193e8b828c01611211565b985050602089013567ffffffffffffffff81111561195f5761195e6111ee565b5b61196b8b828c016118e7565b975050604089013567ffffffffffffffff81111561198c5761198b6111ee565b5b6119988b828c016118e7565b965050606089013567ffffffffffffffff8111156119b9576119b86111ee565b5b6119c58b828c016118e7565b955050608089013567ffffffffffffffff8111156119e6576119e56111ee565b5b6119f28b828c016118e7565b94505060a089013567ffffffffffffffff811115611a1357611a126111ee565b5b611a1f8b828c016118e7565b93505060c089013567ffffffffffffffff811115611a4057611a3f6111ee565b5b611a4c8b828c016118e7565b92505060e089013567ffffffffffffffff811115611a6d57611a6c6111ee565b5b611a798b828c016118e7565b9150509295985092959890939650565b611a92816115f3565b82525050565b5f602082019050611aab5f830184611a89565b92915050565b611aba81611829565b8114611ac4575f5ffd5b50565b5f81359050611ad581611ab1565b92915050565b5f5f5f5f5f60a08688031215611af457611af36111ea565b5b5f611b0188828901611ac7565b9550506020611b1288828901611ac7565b9450506040611b2388828901611ac7565b9350506060611b3488828901611505565b9250506080611b4588828901611479565b9150509295509295909350565b5f60208284031215611b6757611b666111ea565b5b5f611b7484828501611505565b91505092915050565b611b86816111f2565b82525050565b611b9581611829565b82525050565b611ba4816114e0565b82525050565b5f8115159050919050565b611bbe81611baa565b82525050565b61016082015f820151611bd95f850182611b7d565b506020820151611bec6020850182611b8c565b506040820151611bff6040850182611b8c565b506060820151611c126060850182611b9b565b506080820151611c256080850182611b8c565b5060a0820151611c3860a0850182611b7d565b5060c0820151611c4b60c0850182611b8c565b5060e0820151611c5e60e0850182611bb5565b50610100820151611c73610100850182611b7d565b50610120820151611c88610120850182611bb5565b50610140820151611c9d610140850182611b9b565b50505050565b5f61016082019050611cb75f830184611bc4565b92915050565b5f60208284031215611cd257611cd16111ea565b5b5f611cdf84828501611211565b91505092915050565b5f81519050919050565b5f82825260208201905092915050565b5f819050602082019050919050565b611d1a816115f3565b82525050565b606082015f820151611d345f850182611b7d565b506020820151611d476020850182611d11565b506040820151611d5a6040850182611d11565b50505050565b5f611d6b8383611d20565b60608301905092915050565b5f602082019050919050565b5f611d8d82611ce8565b611d978185611cf2565b9350611da283611d02565b805f5b83811015611dd2578151611db98882611d60565b9750611dc483611d77565b925050600181019050611da5565b5085935050505092915050565b5f6020820190508181035f830152611df78184611d83565b905092915050565b5f611e098261176d565b9050919050565b611e1981611dff565b82525050565b5f602082019050611e325f830184611e10565b92915050565b5f611e428261176d565b9050919050565b611e5281611e38565b82525050565b5f602082019050611e6b5f830184611e49565b92915050565b611e7a81611baa565b8114611e84575f5ffd5b50565b5f81359050611e9581611e71565b92915050565b5f5f5f5f5f5f5f60e0888a031215611eb657611eb56111ea565b5b5f611ec38a828b01611ac7565b9750506020611ed48a828b01611ac7565b9650506040611ee58a828b01611ac7565b9550506060611ef68a828b01611505565b9450506080611f078a828b01611302565b93505060a0611f188a828b01611e87565b92505060c0611f298a828b01611505565b91505092959891949750929550565b5f5f5f60608486031215611f4f57611f4e6111ea565b5b5f611f5c8682870161158c565b9350506020611f6d86828701611479565b9250506040611f7e8682870161158c565b9150509250925092565b5f81519050919050565b5f82825260208201905092915050565b5f819050602082019050919050565b611fba81611569565b82525050565b604082015f820151611fd45f850182611fb1565b506020820151611fe76020850182611b8c565b50505050565b5f611ff88383611fc0565b60408301905092915050565b5f602082019050919050565b5f61201a82611f88565b6120248185611f92565b935061202f83611fa2565b805f5b8381101561205f5781516120468882611fed565b975061205183612004565b925050600181019050612032565b5085935050505092915050565b5f6020820190508181035f8301526120848184612010565b905092915050565b5f6120968261176d565b9050919050565b6120a68161208c565b82525050565b5f6020820190506120bf5f83018461209d565b92915050565b5f6120cf8261176d565b9050919050565b6120df816120c5565b82525050565b5f6020820190506120f85f8301846120d6565b92915050565b5f6121088261176d565b9050919050565b612118816120fe565b82525050565b5f6020820190506121315f83018461210f565b92915050565b5f6121418261176d565b9050919050565b61215181612137565b82525050565b5f60208201905061216a5f830184612148565b92915050565b5f61217a8261176d565b9050919050565b61218a81612170565b82525050565b5f6020820190506121a35f830184612181565b92915050565b5f6121b38261176d565b9050919050565b6121c3816121a9565b82525050565b5f6020820190506121dc5f8301846121ba565b92915050565b5f81519050919050565b5f82825260208201905092915050565b5f819050602082019050919050565b612214816112e0565b82525050565b5f612225838361220b565b60208301905092915050565b5f602082019050919050565b5f612247826121e2565b61225181856121ec565b935061225c836121fc565b805f5b8381101561228c578151612273888261221a565b975061227e83612231565b92505060018101905061225f565b5085935050505092915050565b5f6060820190506122ac5f8301866114b8565b81810360208301526122be818561223d565b905081810360408301526122d2818461223d565b9050949350505050565b6122e581611452565b82525050565b5f6020820190506122fe5f8301846122dc565b92915050565b5f81519050612312816111fb565b92915050565b5f6020828403121561232d5761232c6111ea565b5b5f61233a84828501612304565b91505092915050565b61234c816112e0565b82525050565b61235b816114e0565b82525050565b5f6060820190506123745f8301866114b8565b6123816020830185612343565b61238e6040830184612352565b949350505050565b5f815190506123a481611576565b92915050565b5f602082840312156123bf576123be6111ea565b5b5f6123cc84828501612396565b91505092915050565b5f6060820190506123e85f8301866114b8565b6123f56020830185611a89565b6124026040830184611a89565b949350505050565b5f60408201905061241d5f830185612352565b61242a60208301846114b8565b9392505050565b61243a816116b4565b8114612444575f5ffd5b50565b5f8151905061245581612431565b92915050565b5f5f60408385031215612471576124706111ea565b5b5f61247e85828601612447565b925050602061248f85828601612447565b9150509250929050565b5f6040820190506124ac5f8301856115cb565b6124b960208301846114b8565b9392505050565b5f815190506124ce81611ab1565b92915050565b5f602082840312156124e9576124e86111ea565b5b5f6124f6848285016124c0565b91505092915050565b5f81519050919050565b5f82825260208201905092915050565b8281835e5f83830152505050565b5f612531826124ff565b61253b8185612509565b935061254b818560208601612519565b61255481611229565b840191505092915050565b5f610100820190506125735f83018b6114b8565b8181036020830152612585818a612527565b905081810360408301526125998189612527565b905081810360608301526125ad8188612527565b905081810360808301526125c18187612527565b905081810360a08301526125d58186612527565b905081810360c08301526125e98185612527565b905081810360e08301526125fd8184612527565b90509998505050505050505050565b5f8151905061261a816115fc565b92915050565b5f60208284031215612635576126346111ea565b5b5f6126428482850161260c565b91505092915050565b5f60a08201905061265e5f83018861183c565b61266b602083018761183c565b612678604083018661183c565b6126856060830185612352565b61269260808301846122dc565b9695505050505050565b5f6020820190506126af5f830184612352565b92915050565b5f5ffd5b5f815190506126c7816114ef565b92915050565b5f815190506126db81611e71565b92915050565b5f61016082840312156126f7576126f66126b5565b5b612702610160611297565b90505f61271184828501612304565b5f830152506020612724848285016124c0565b6020830152506040612738848285016124c0565b604083015250606061274c848285016126b9565b6060830152506080612760848285016124c0565b60808301525060a061277484828501612304565b60a08301525060c0612788848285016124c0565b60c08301525060e061279c848285016126cd565b60e0830152506101006127b184828501612304565b610100830152506101206127c7848285016126cd565b610120830152506101406127dd848285016126b9565b6101408301525092915050565b5f6101608284031215612800576127ff6111ea565b5b5f61280d848285016126e1565b91505092915050565b5f67ffffffffffffffff8211156128305761282f611239565b5b602082029050602081019050919050565b5f60608284031215612856576128556126b5565b5b6128606060611297565b90505f61286f84828501612304565b5f8301525060206128828482850161260c565b60208301525060406128968482850161260c565b60408301525092915050565b5f6128b46128af84612816565b611297565b905080838252602082019050606084028301858111156128d7576128d66112dc565b5b835b8181101561290057806128ec8882612841565b8452602084019350506060810190506128d9565b5050509392505050565b5f82601f83011261291e5761291d611225565b5b815161292e8482602086016128a2565b91505092915050565b5f6020828403121561294c5761294b6111ea565b5b5f82015167ffffffffffffffff811115612969576129686111ee565b5b6129758482850161290a565b91505092915050565b61298781611baa565b82525050565b5f60e0820190506129a05f83018a61183c565b6129ad602083018961183c565b6129ba604083018861183c565b6129c76060830187612352565b6129d46080830186612343565b6129e160a083018561297e565b6129ee60c0830184612352565b98975050505050505050565b5f606082019050612a0d5f8301866115cb565b612a1a60208301856122dc565b612a2760408301846115cb565b949350505050565b5f67ffffffffffffffff821115612a4957612a48611239565b5b602082029050602081019050919050565b5f60408284031215612a6f57612a6e6126b5565b5b612a796040611297565b90505f612a8884828501612396565b5f830152506020612a9b848285016124c0565b60208301525092915050565b5f612ab9612ab484612a2f565b611297565b90508083825260208201905060408402830185811115612adc57612adb6112dc565b5b835b81811015612b055780612af18882612a5a565b845260208401935050604081019050612ade565b5050509392505050565b5f82601f830112612b2357612b22611225565b5b8151612b33848260208601612aa7565b91505092915050565b5f60208284031215612b5157612b506111ea565b5b5f82015167ffffffffffffffff811115612b6e57612b6d6111ee565b5b612b7a84828501612b0f565b9150509291505056fea2646970667358221220768c64014d2253c661e44d07f480f7a203eb9e422f680d00272498325a4f6ad964736f6c634300081e0033"; diff --git a/contract-tests/src/contracts/votingPower.ts b/contract-tests/src/contracts/votingPower.ts new file mode 100644 index 0000000000..bbcc3ca6e6 --- /dev/null +++ b/contract-tests/src/contracts/votingPower.ts @@ -0,0 +1,104 @@ +export const IVOTING_POWER_ADDRESS = "0x000000000000000000000000000000000000080d"; + +export const IVotingPowerABI = [ + { + "inputs": [ + { + "internalType": "uint16", + "name": "netuid", + "type": "uint16" + }, + { + "internalType": "bytes32", + "name": "hotkey", + "type": "bytes32" + } + ], + "name": "getVotingPower", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint16", + "name": "netuid", + "type": "uint16" + } + ], + "name": "isVotingPowerTrackingEnabled", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint16", + "name": "netuid", + "type": "uint16" + } + ], + "name": "getVotingPowerDisableAtBlock", + "outputs": [ + { + "internalType": "uint64", + "name": "", + "type": "uint64" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint16", + "name": "netuid", + "type": "uint16" + } + ], + "name": "getVotingPowerEmaAlpha", + "outputs": [ + { + "internalType": "uint64", + "name": "", + "type": "uint64" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint16", + "name": "netuid", + "type": "uint16" + } + ], + "name": "getTotalVotingPower", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + } +] diff --git a/contract-tests/src/subtensor.ts b/contract-tests/src/subtensor.ts index fab9e8cc10..f5829c76aa 100644 --- a/contract-tests/src/subtensor.ts +++ b/contract-tests/src/subtensor.ts @@ -1,17 +1,20 @@ import * as assert from "assert"; import { devnet, MultiAddress } from '@polkadot-api/descriptors'; -import { TypedApi, TxCallData, Binary, Enum } from 'polkadot-api'; +import { TypedApi, TxCallData, Binary, Enum, getTypedCodecs } from 'polkadot-api'; import { KeyPair } from "@polkadot-labs/hdkd-helpers" import { getAliceSigner, waitForTransactionCompletion, getSignerFromKeypair, waitForTransactionWithRetry } from './substrate' import { convertH160ToSS58, convertPublicKeyToSs58, ethAddressToH160 } from './address-utils' import { tao } from './balance-math' import internal from "stream"; +import { createCodec } from "scale-ts"; // create a new subnet and return netuid export async function addNewSubnetwork(api: TypedApi, hotkey: KeyPair, coldkey: KeyPair) { const alice = getAliceSigner() const totalNetworks = await api.query.SubtensorModule.TotalNetworks.getValue() + const defaultNetworkLastLockCost = await api.query.SubtensorModule.NetworkLastLockCost.getValue() + const rateLimit = await api.query.SubtensorModule.NetworkRateLimit.getValue() if (rateLimit !== BigInt(0)) { const internalCall = api.tx.AdminUtils.sudo_set_network_rate_limit({ rate_limit: BigInt(0) }) @@ -26,6 +29,9 @@ export async function addNewSubnetwork(api: TypedApi, hotkey: Key const newTotalNetworks = await api.query.SubtensorModule.TotalNetworks.getValue() // could create multiple subnetworks during retry, just return the first created one assert.ok(newTotalNetworks > totalNetworks) + + // reset network last lock cost to 0, to avoid the lock cost calculation error + await setNetworkLastLockCost(api, defaultNetworkLastLockCost) return totalNetworks } @@ -398,4 +404,19 @@ export async function sendWasmContractExtrinsic(api: TypedApi, co storage_deposit_limit: BigInt(1000000000) }) await waitForTransactionWithRetry(api, tx, signer) -} \ No newline at end of file +} + +export async function setNetworkLastLockCost(api: TypedApi, defaultNetworkLastLockCost: bigint) { + const alice = getAliceSigner() + const key = await api.query.SubtensorModule.NetworkLastLockCost.getKey() + const codec = await getTypedCodecs(devnet); + const value = codec.query.SubtensorModule.NetworkLastLockCost.value.enc(defaultNetworkLastLockCost) + const internalCall = api.tx.System.set_storage({ + items: [[Binary.fromHex(key), Binary.fromBytes(value)]] + }) + const tx = api.tx.Sudo.sudo({ call: internalCall.decodedCall }) + await waitForTransactionWithRetry(api, tx, alice) + + const valueOnChain = await api.query.SubtensorModule.NetworkLastLockCost.getValue() + assert.equal(defaultNetworkLastLockCost, valueOnChain) +} \ No newline at end of file diff --git a/contract-tests/test/precompileWrapper.direct-call.test.ts b/contract-tests/test/precompileWrapper.direct-call.test.ts new file mode 100644 index 0000000000..5d63dfbb44 --- /dev/null +++ b/contract-tests/test/precompileWrapper.direct-call.test.ts @@ -0,0 +1,403 @@ +import * as assert from "assert"; +import { getDevnetApi, getRandomSubstrateKeypair, getBalance, getSignerFromKeypair } from "../src/substrate"; +import { devnet } from "@polkadot-api/descriptors"; +import { TypedApi, Binary } from "polkadot-api"; +import { convertH160ToSS58, convertPublicKeyToSs58, convertH160ToPublicKey } from "../src/address-utils"; +import { tao, raoToEth } from "../src/balance-math"; +import { + forceSetBalanceToSs58Address, + addNewSubnetwork, + startCall, + disableWhiteListCheck, + forceSetBalanceToEthAddress, + +} from "../src/subtensor"; +import { ethers } from "ethers"; +import { generateRandomEthersWallet, getPublicClient } from "../src/utils"; +import { PRECOMPILE_WRAPPER_ABI, PRECOMPILE_WRAPPER_BYTECODE } from "../src/contracts/precompileWrapper"; +import { ETH_LOCAL_URL } from "../src/config"; +import { PublicClient } from "viem"; +import { IProxyABI, IPROXY_ADDRESS } from "../src/contracts/proxy" + +describe("PrecompileWrapper - Direct Call Tests", () => { + const hotkey = getRandomSubstrateKeypair(); + const coldkey = getRandomSubstrateKeypair(); + const wallet1 = generateRandomEthersWallet(); + const wallet2 = generateRandomEthersWallet(); + + let api: TypedApi; + let publicClient: PublicClient; + let wrapperContract: ethers.Contract; + let wrapperAddress: string; + let netuid: number; + + before(async () => { + api = await getDevnetApi(); + publicClient = await getPublicClient(ETH_LOCAL_URL); + await disableWhiteListCheck(api, true); + await forceSetBalanceToSs58Address(api, convertPublicKeyToSs58(hotkey.publicKey)); + await forceSetBalanceToSs58Address(api, convertPublicKeyToSs58(coldkey.publicKey)); + await forceSetBalanceToEthAddress(api, wallet1.address); + await forceSetBalanceToEthAddress(api, wallet2.address); + await addNewSubnetwork(api, hotkey, coldkey); + netuid = (await api.query.SubtensorModule.TotalNetworks.getValue()) - 1; + await startCall(api, netuid, coldkey); + + const factory = new ethers.ContractFactory( + PRECOMPILE_WRAPPER_ABI, + PRECOMPILE_WRAPPER_BYTECODE, + wallet1 + ); + const deployContract = await factory.deploy(); + await deployContract.waitForDeployment(); + wrapperAddress = await deployContract.getAddress(); + await forceSetBalanceToEthAddress(api, wrapperAddress); + + console.log("Wrapper contract deployed at:", wrapperAddress); + console.log("Testing in subnet:", netuid); + + wrapperContract = new ethers.Contract(wrapperAddress, PRECOMPILE_WRAPPER_ABI, wallet1); + }); + + describe("Balance Transfer Precompile Direct Calls", () => { + it("Should transfer balance via wrapper", async () => { + const keypair = getRandomSubstrateKeypair(); + const transferAmount = raoToEth(tao(1)); + + // Transfer via wrapper + const transferTx = await wrapperContract.transfer(keypair.publicKey, { value: transferAmount.toString() }); + await transferTx.wait(); + + const balance = await getBalance(api, convertPublicKeyToSs58(keypair.publicKey)); + assert.ok(balance >= tao(1), "Balance should be transferred"); + }); + }); + + describe("Metagraph Precompile Direct Calls", () => { + it("Should get UID count via wrapper", async () => { + const uidCountViaWrapper = await wrapperContract.getUidCount(netuid); + assert.ok(uidCountViaWrapper !== undefined, "UID count should be not undefined"); + }); + }); + + describe("Subnet Precompile Direct Calls", () => { + it("Should get serving rate limit via wrapper", async () => { + const rateLimitViaWrapper = await wrapperContract.getServingRateLimit(netuid); + + assert.ok(rateLimitViaWrapper !== undefined, "Rate limit should be not undefined"); + }); + + it("Should register network with details via wrapper", async () => { + const newHotkey = getRandomSubstrateKeypair(); + await forceSetBalanceToSs58Address(api, convertPublicKeyToSs58(newHotkey.publicKey)); + + const totalNetworksBefore = await api.query.SubtensorModule.TotalNetworks.getValue(); + + const registerTx = await wrapperContract.registerNetworkWithDetails( + newHotkey.publicKey, + "Test Subnet", + "https://github.com/test/repo", + "test@example.com", + "https://test.example.com", + "test#1234", + "Test description", + "Additional info", + { value: raoToEth(tao(100)).toString() } + ); + await registerTx.wait(); + + const totalNetworksAfter = await api.query.SubtensorModule.TotalNetworks.getValue(); + const beforeValue = typeof totalNetworksBefore === 'bigint' ? totalNetworksBefore : BigInt(totalNetworksBefore); + assert.equal(totalNetworksAfter, beforeValue + BigInt(1), "Network should be registered"); + }); + }); + + describe("Neuron Precompile Direct Calls", () => { + it("Should register neuron via wrapper", async () => { + const newHotkey = getRandomSubstrateKeypair(); + const newColdkey = getRandomSubstrateKeypair(); + await forceSetBalanceToSs58Address(api, convertPublicKeyToSs58(newHotkey.publicKey)); + await forceSetBalanceToSs58Address(api, convertPublicKeyToSs58(newColdkey.publicKey)); + + // Use a reasonable burn amount (100 TAO) + const burnAmount = tao(100); + + const registerTx = await wrapperContract.burnedRegister( + netuid, + newHotkey.publicKey, + { value: raoToEth(burnAmount).toString() } + ); + await registerTx.wait(); + + const uid = await api.query.SubtensorModule.Uids.getValue(netuid, convertPublicKeyToSs58(newHotkey.publicKey)); + assert.ok(uid !== undefined, "Neuron should be registered"); + }); + }); + + describe("Staking Precompile Direct Calls", () => { + it("Should get total coldkey stake via wrapper", async () => { + const stakeViaWrapper = await wrapperContract.getTotalColdkeyStake(coldkey.publicKey); + assert.ok(stakeViaWrapper !== undefined, "Total coldkey stake should be not undefined"); + }); + + it("Should get total hotkey stake via wrapper", async () => { + const stakeViaWrapper = await wrapperContract.getTotalHotkeyStake(hotkey.publicKey); + assert.ok(stakeViaWrapper !== undefined, "Total hotkey stake should be not undefined"); + }); + + it("Should add stake via wrapper", async () => { + const stakeAmount = tao(2); + const stakeBefore = await api.query.SubtensorModule.Alpha.getValue( + convertPublicKeyToSs58(hotkey.publicKey), + convertH160ToSS58(wrapperAddress), + netuid + ); + + const addStakeTx = await wrapperContract.addStake( + hotkey.publicKey, + stakeAmount.toString(), + netuid, + { value: raoToEth(stakeAmount).toString() } + ); + await addStakeTx.wait(); + + const stakeAfter = await api.query.SubtensorModule.Alpha.getValue( + convertPublicKeyToSs58(hotkey.publicKey), + convertH160ToSS58(wrapperAddress), + netuid + ); + assert.ok(stakeAfter > stakeBefore, "Stake should be increased"); + }); + + it("Should remove stake via wrapper", async () => { + const removeAmount = tao(1); + const stakeBefore = await api.query.SubtensorModule.Alpha.getValue( + convertPublicKeyToSs58(hotkey.publicKey), + convertH160ToSS58(wrapperAddress), + netuid + ); + + const removeStakeTx = await wrapperContract.removeStake( + hotkey.publicKey, + removeAmount.toString(), + netuid + ); + await removeStakeTx.wait(); + + const stakeAfter = await api.query.SubtensorModule.Alpha.getValue( + convertPublicKeyToSs58(hotkey.publicKey), + convertH160ToSS58(wrapperAddress), + netuid + ); + assert.ok(stakeAfter < stakeBefore, "Stake should be decreased"); + }); + }); + + describe("UID Lookup Precompile Direct Calls", () => { + it("Should lookup UID via wrapper", async () => { + const evmAddress = wallet1.address; + const limit = 10; + const lookupViaWrapper = await wrapperContract.uidLookup(netuid, evmAddress, limit); + + assert.ok(Array.isArray(lookupViaWrapper), "Lookup should return an array"); + }); + }); + + describe("Alpha Precompile Direct Calls", () => { + it("Should get alpha price via wrapper", async () => { + const priceViaWrapper = await wrapperContract.getAlphaPrice(netuid); + assert.ok(priceViaWrapper !== undefined, "Alpha price should be not undefined"); + }); + }); + + describe("Crowdloan Precompile Direct Calls", () => { + it("Should get crowdloan via wrapper", async () => { + // First create a crowdloan via substrate + const nextId = await api.query.Crowdloan.NextCrowdloanId.getValue(); + const end = await api.query.System.Number.getValue() + 100; + const deposit = BigInt(15_000_000_000); // 15 TAO + const minContribution = BigInt(1_000_000_000); // 1 TAO + const cap = BigInt(100_000_000_000); // 100 TAO + + const signer = getSignerFromKeypair(coldkey); + await api.tx.Crowdloan.create({ + deposit, + min_contribution: minContribution, + cap, + end, + target_address: undefined, + call: api.tx.System.remark({ remark: Binary.fromText("test") }).decodedCall + }).signAndSubmit(signer); + + // Wait a bit for the transaction to be included + await new Promise(resolve => setTimeout(resolve, 2000)); + + const crowdloanViaWrapper = await wrapperContract.getCrowdloan(nextId); + + assert.ok(crowdloanViaWrapper !== undefined, "Crowdloan should be not undefined"); + }); + + it("Should get contribution via wrapper", async () => { + const nextId = await api.query.Crowdloan.NextCrowdloanId.getValue(); + const contributionViaWrapper = await wrapperContract.getContribution(nextId - 1, coldkey.publicKey); + + assert.ok(contributionViaWrapper !== undefined, "Contribution should be not undefined"); + }); + + it("Should create crowdloan via wrapper", async () => { + const deposit = BigInt(20_000_000_000); // 20 TAO + const minContribution = BigInt(2_000_000_000); // 2 TAO + const cap = BigInt(200_000_000_000); // 200 TAO + const end = Number(await api.query.System.Number.getValue()) + 100; + const targetAddress = wallet2.address; + + const nextIdBefore = await api.query.Crowdloan.NextCrowdloanId.getValue(); + + const createTx = await wrapperContract.createCrowdloan( + deposit.toString(), + minContribution.toString(), + cap.toString(), + end, + targetAddress, + { value: raoToEth(deposit).toString() } + ); + await createTx.wait(); + + const nextIdAfter = await api.query.Crowdloan.NextCrowdloanId.getValue(); + const beforeId = typeof nextIdBefore === 'bigint' ? nextIdBefore : BigInt(nextIdBefore); + assert.equal(nextIdAfter, beforeId + BigInt(1), "Crowdloan should be created"); + }); + }); + + + describe("Leasing Precompile Direct Calls", () => { + it("Should get contributor share via wrapper", async () => { + // First create a lease crowdloan + const nextCrowdloanId = await api.query.Crowdloan.NextCrowdloanId.getValue(); + const crowdloanDeposit = BigInt(100_000_000_000); // 100 TAO + const networkLastLockCost = await api.query.SubtensorModule.NetworkLastLockCost.getValue(); + const lockCostValue = typeof networkLastLockCost === 'bigint' ? networkLastLockCost : BigInt(networkLastLockCost); + const crowdloanCap = lockCostValue * BigInt(2); + const currentBlock = await api.query.System.Number.getValue(); + const crowdloanEnd = currentBlock + 100; + const leasingEmissionsShare = 15; + const leasingEndBlock = currentBlock + 300; + + const signer = getSignerFromKeypair(coldkey); + await api.tx.Crowdloan.create({ + deposit: crowdloanDeposit, + min_contribution: BigInt(1_000_000_000), + cap: crowdloanCap, + end: crowdloanEnd, + target_address: undefined, + call: api.tx.SubtensorModule.register_leased_network({ + emissions_share: leasingEmissionsShare, + end_block: leasingEndBlock, + }).decodedCall + }).signAndSubmit(signer); + + await new Promise(resolve => setTimeout(resolve, 2000)); + + const nextLeaseId = await api.query.SubtensorModule.NextSubnetLeaseId.getValue(); + + // Get contributor share + const shareViaWrapper = await wrapperContract.getContributorShare(nextLeaseId, coldkey.publicKey); + + assert.ok(shareViaWrapper !== undefined, "Share should be not undefined"); + + }); + + it("Should create lease crowdloan via wrapper", async () => { + const crowdloanDeposit = BigInt(100_000_000_000); // 100 TAO + const crowdloanMinContribution = BigInt(1_000_000_000); // 1 TAO + const networkLastLockCost = await api.query.SubtensorModule.NetworkLastLockCost.getValue(); + const lockCostValue = typeof networkLastLockCost === 'bigint' ? networkLastLockCost : BigInt(networkLastLockCost); + const crowdloanCap = lockCostValue * BigInt(2); + const currentBlock = await api.query.System.Number.getValue(); + const currentBlockValue = typeof currentBlock === 'bigint' ? Number(currentBlock) : currentBlock; + const crowdloanEnd = currentBlockValue + 100; + const leasingEmissionsShare = 15; + const hasLeasingEndBlock = true; + const leasingEndBlock = currentBlockValue + 300; + + const nextCrowdloanIdBefore = await api.query.Crowdloan.NextCrowdloanId.getValue(); + + const createTx = await wrapperContract.createLeaseCrowdloan( + crowdloanDeposit.toString(), + crowdloanMinContribution.toString(), + crowdloanCap.toString(), + crowdloanEnd, + leasingEmissionsShare, + hasLeasingEndBlock, + leasingEndBlock, + { value: raoToEth(crowdloanDeposit).toString() } + ); + await createTx.wait(); + + const nextCrowdloanIdAfter = await api.query.Crowdloan.NextCrowdloanId.getValue(); + const beforeId = typeof nextCrowdloanIdBefore === 'bigint' ? nextCrowdloanIdBefore : BigInt(nextCrowdloanIdBefore); + assert.equal(nextCrowdloanIdAfter, beforeId + BigInt(1), "Lease crowdloan should be created"); + }); + }); + + + describe("Proxy Precompile Direct Calls", () => { + it("Should get proxies via wrapper", async () => { + const accountKey = convertH160ToPublicKey(wallet1.address); + const proxiesViaWrapper = await wrapperContract.getProxies(accountKey); + + assert.ok(proxiesViaWrapper !== undefined, "Proxies should be not undefined"); + assert.ok(Array.isArray(proxiesViaWrapper), "Proxies should be an array"); + }); + it("Should add proxy via wrapper", async () => { + const delegate = getRandomSubstrateKeypair(); + await forceSetBalanceToSs58Address(api, convertPublicKeyToSs58(delegate.publicKey)); + const delegateKey = delegate.publicKey; + const proxyType = 0; + const delay = 0; + + const proxiesBefore = await api.query.Proxy.Proxies.getValue(convertH160ToSS58(wrapperAddress)); + + const addProxyTx = await wrapperContract.addProxy(delegateKey, proxyType, delay); + await addProxyTx.wait(); + + const proxiesAfter = await api.query.Proxy.Proxies.getValue(convertH160ToSS58(wrapperAddress)); + assert.ok(proxiesAfter[0].length > proxiesBefore[0].length, "Proxy should be added"); + }); + + it("Should proxy call via wrapper", async () => { + const proxyType = 0; + const delay = 0; + + const proxyContract = new ethers.Contract(IPROXY_ADDRESS, IProxyABI, wallet1); + const addProxyTx = await proxyContract.addProxy(convertH160ToPublicKey(wrapperAddress), proxyType, delay); + await addProxyTx.wait(); + + // Create a simple call (remark) + const remarkCall = api.tx.System.remark({ remark: Binary.fromText("") }); + + const callData = await remarkCall.getEncodedData(); + const data = callData.asBytes(); + + const proxyCallTx = await wrapperContract.proxyCall( + convertH160ToPublicKey(wallet1.address), + [proxyType], + [...data] + ); + await proxyCallTx.wait(); + + // Verify the call was executed (no error means success) + assert.ok(proxyCallTx, "Proxy call should succeed"); + }); + }); + + describe("Address Mapping Precompile Direct Calls", () => { + it("Should map address via wrapper", async () => { + const testAddress = wallet1.address; + const mappedViaWrapper = await wrapperContract.addressMapping(testAddress); + + assert.ok(mappedViaWrapper !== undefined, "Mapped address should be not undefined"); + assert.ok(mappedViaWrapper !== "0x0000000000000000000000000000000000000000000000000000000000000000", "Mapped address should not be zero"); + }); + }); +}); diff --git a/contract-tests/test/runtime.call.precompile.test.ts b/contract-tests/test/runtime.call.precompile.test.ts index c409b25c8b..7bacc947fd 100644 --- a/contract-tests/test/runtime.call.precompile.test.ts +++ b/contract-tests/test/runtime.call.precompile.test.ts @@ -66,7 +66,7 @@ describe("Test the dispatch precompile", () => { it("Storage query only allow some pallets prefixed storage", async () => { const authorizedKeys = [ await api.query.SubtensorModule.TotalNetworks.getKey(), - await api.query.Swap.AlphaSqrtPrice.getKey(), + await api.query.Swap.FeeRate.getKey(), await api.query.Balances.TotalIssuance.getKey(), await api.query.Proxy.Announcements.getKey(), await api.query.Scheduler.Agenda.getKey(), diff --git a/contract-tests/test/subnet.precompile.hyperparameter.test.ts b/contract-tests/test/subnet.precompile.hyperparameter.test.ts index 8598b45a81..75d361a77f 100644 --- a/contract-tests/test/subnet.precompile.hyperparameter.test.ts +++ b/contract-tests/test/subnet.precompile.hyperparameter.test.ts @@ -2,12 +2,13 @@ import * as assert from "assert"; import { getAliceSigner, getDevnetApi, getRandomSubstrateKeypair, waitForTransactionWithRetry } from "../src/substrate" import { devnet } from "@polkadot-api/descriptors" -import { Binary, TypedApi, getTypedCodecs } from "polkadot-api"; +import { Binary, FixedSizeBinary, TypedApi, getTypedCodecs } from "polkadot-api"; import { convertH160ToSS58, convertPublicKeyToSs58 } from "../src/address-utils" import { generateRandomEthersWallet } from "../src/utils"; import { ISubnetABI, ISUBNET_ADDRESS } from "../src/contracts/subnet" import { ethers } from "ethers" import { disableAdminFreezeWindowAndOwnerHyperparamRateLimit, forceSetBalanceToEthAddress, forceSetBalanceToSs58Address } from "../src/subtensor" +import { blake2AsU8a } from "@polkadot/util-crypto" describe("Test the Subnet precompile contract", () => { // init eth part @@ -552,16 +553,16 @@ describe("Test the Subnet precompile contract", () => { const netuid = totalNetwork - 1; const coldkeySs58 = convertH160ToSS58(wallet.address) - const newColdkeySs58 = convertPublicKeyToSs58(hotkey1.publicKey) + const newColdkeyHash = FixedSizeBinary.fromBytes(blake2AsU8a(hotkey1.publicKey)) const currentBlock = await api.query.System.Number.getValue() const executionBlock = currentBlock + 10 const codec = await getTypedCodecs(devnet); - const valueBytes = codec.query.SubtensorModule.ColdkeySwapScheduled.value.enc([ + const valueBytes = codec.query.SubtensorModule.ColdkeySwapAnnouncements.value.enc([ executionBlock, - newColdkeySs58, + newColdkeyHash ]) - const key = await api.query.SubtensorModule.ColdkeySwapScheduled.getKey(coldkeySs58); + const key = await api.query.SubtensorModule.ColdkeySwapAnnouncements.getKey(coldkeySs58); // Use sudo + set_storage since the swap-scheduled check only exists in the tx extension. const setStorageCall = api.tx.System.set_storage({ @@ -570,8 +571,9 @@ describe("Test the Subnet precompile contract", () => { const sudoTx = api.tx.Sudo.sudo({ call: setStorageCall.decodedCall }) await waitForTransactionWithRetry(api, sudoTx, getAliceSigner()) - const storedValue = await api.query.SubtensorModule.ColdkeySwapScheduled.getValue(coldkeySs58) - assert.deepStrictEqual(storedValue, [executionBlock, newColdkeySs58]) + const storedValue = await api.query.SubtensorModule.ColdkeySwapAnnouncements.getValue(coldkeySs58) + assert.equal(storedValue?.[0], executionBlock) + assert.equal(storedValue?.[1].asHex(), newColdkeyHash.asHex()) await assert.rejects(async () => { const tx = await contract.setServingRateLimit(netuid, 100); diff --git a/contract-tests/test/transaction.replace.test.ts b/contract-tests/test/transaction.replace.test.ts new file mode 100644 index 0000000000..afb95ed9d5 --- /dev/null +++ b/contract-tests/test/transaction.replace.test.ts @@ -0,0 +1,83 @@ +import * as assert from "assert"; + +import { getDevnetApi, getRandomSubstrateSigner, } from "../src/substrate" +import { getPublicClient } from "../src/utils"; +import { ETH_LOCAL_URL, IBALANCETRANSFER_ADDRESS, IBalanceTransferABI } from "../src/config"; +import { devnet } from "@polkadot-api/descriptors" +import { PublicClient } from "viem"; +import { TypedApi } from "polkadot-api"; +import { generateRandomEthersWallet } from "../src/utils"; +import { tao, raoToEth } from "../src/balance-math"; +import { toViemAddress, } from "../src/address-utils" +import { getContract } from "../src/eth" +import { forceSetBalanceToEthAddress, } from "../src/subtensor"; + +describe("Transaction replace tests", () => { + // init eth part + const wallet = generateRandomEthersWallet(); + const wallet2 = generateRandomEthersWallet(); + const signer = getRandomSubstrateSigner(); + let publicClient: PublicClient; + let api: TypedApi + + before(async () => { + + publicClient = await getPublicClient(ETH_LOCAL_URL) + api = await getDevnetApi() + await forceSetBalanceToEthAddress(api, wallet.address) + }); + + it("Can replace simple transfer transaction", async () => { + const transferBalance = raoToEth(tao(1)) + + const gasPrice = BigInt(10e9) + const gasLimit = BigInt(1000000) + const nonce = await publicClient.getTransactionCount({ address: toViemAddress(wallet.address) }) + + for (let i = 1; i < 10; i++) { + const transfer = { + to: wallet2.address, + value: transferBalance.toString(), + nonce: nonce, + gasPrice: gasPrice * BigInt(i), + gasLimit: gasLimit * BigInt(i) + } + + try { + await wallet.sendTransaction(transfer) + } catch (error) { + // ignore error, previous transaction could be mined. the nonce is wrong. + } + await new Promise(resolve => setTimeout(resolve, 10)) + } + + // check the node not crashed + await forceSetBalanceToEthAddress(api, wallet.address) + }) + + it("Can replace precompile call transaction", async () => { + const contract = getContract(IBALANCETRANSFER_ADDRESS, IBalanceTransferABI, wallet) + const transferBalance = raoToEth(tao(1)) + + const gasPrice = BigInt(10e9) + const gasLimit = BigInt(1000000) + const nonce = await publicClient.getTransactionCount({ address: toViemAddress(wallet.address) }) + + for (let i = 1; i < 10; i++) { + try { + await contract.transfer(signer.publicKey, { + value: transferBalance.toString(), + nonce: nonce, + gasPrice: gasPrice * BigInt(i), + gasLimit: gasLimit * BigInt(i) + }) + } catch (error) { + // ignore error, previous transaction could be mined. the nonce is wrong. + } + + await new Promise(resolve => setTimeout(resolve, 10)) + } + // check the node not crashed + await forceSetBalanceToEthAddress(api, wallet.address) + }) +}) \ No newline at end of file diff --git a/contract-tests/test/votingPower.precompile.test.ts b/contract-tests/test/votingPower.precompile.test.ts new file mode 100644 index 0000000000..f98edd5fc1 --- /dev/null +++ b/contract-tests/test/votingPower.precompile.test.ts @@ -0,0 +1,226 @@ +import * as assert from "assert"; + +import { getDevnetApi, getRandomSubstrateKeypair, getAliceSigner, getSignerFromKeypair, waitForTransactionWithRetry } from "../src/substrate" +import { getPublicClient } from "../src/utils"; +import { ETH_LOCAL_URL } from "../src/config"; +import { devnet } from "@polkadot-api/descriptors" +import { PublicClient } from "viem"; +import { PolkadotSigner, TypedApi } from "polkadot-api"; +import { toViemAddress, convertPublicKeyToSs58 } from "../src/address-utils" +import { IVotingPowerABI, IVOTING_POWER_ADDRESS } from "../src/contracts/votingPower" +import { forceSetBalanceToSs58Address, addNewSubnetwork, startCall } from "../src/subtensor"; + +describe("Test VotingPower Precompile", () => { + // init substrate part + const hotkey = getRandomSubstrateKeypair(); + const coldkey = getRandomSubstrateKeypair(); + let publicClient: PublicClient; + + let api: TypedApi; + + // sudo account alice as signer + let alice: PolkadotSigner; + + // init other variable + let subnetId = 0; + + before(async () => { + // init variables got from await and async + publicClient = await getPublicClient(ETH_LOCAL_URL) + api = await getDevnetApi() + alice = await getAliceSigner(); + + await forceSetBalanceToSs58Address(api, convertPublicKeyToSs58(hotkey.publicKey)) + await forceSetBalanceToSs58Address(api, convertPublicKeyToSs58(coldkey.publicKey)) + + let netuid = await addNewSubnetwork(api, hotkey, coldkey) + await startCall(api, netuid, coldkey) + subnetId = netuid + }) + + describe("VotingPower Tracking Status Functions", () => { + it("isVotingPowerTrackingEnabled returns false by default", async () => { + const isEnabled = await publicClient.readContract({ + abi: IVotingPowerABI, + address: toViemAddress(IVOTING_POWER_ADDRESS), + functionName: "isVotingPowerTrackingEnabled", + args: [subnetId] + }) + + assert.ok(isEnabled !== undefined, "isVotingPowerTrackingEnabled should return a value"); + assert.strictEqual(typeof isEnabled, 'boolean', "isVotingPowerTrackingEnabled should return a boolean"); + // By default, voting power tracking is disabled + assert.strictEqual(isEnabled, false, "Voting power tracking should be disabled by default"); + }); + + it("getVotingPowerDisableAtBlock returns 0 when not scheduled", async () => { + const disableAtBlock = await publicClient.readContract({ + abi: IVotingPowerABI, + address: toViemAddress(IVOTING_POWER_ADDRESS), + functionName: "getVotingPowerDisableAtBlock", + args: [subnetId] + }) + + assert.ok(disableAtBlock !== undefined, "getVotingPowerDisableAtBlock should return a value"); + assert.strictEqual(typeof disableAtBlock, 'bigint', "getVotingPowerDisableAtBlock should return a bigint"); + assert.strictEqual(disableAtBlock, BigInt(0), "Disable at block should be 0 when not scheduled"); + }); + + it("getVotingPowerEmaAlpha returns default alpha value", async () => { + const alpha = await publicClient.readContract({ + abi: IVotingPowerABI, + address: toViemAddress(IVOTING_POWER_ADDRESS), + functionName: "getVotingPowerEmaAlpha", + args: [subnetId] + }) + + assert.ok(alpha !== undefined, "getVotingPowerEmaAlpha should return a value"); + assert.strictEqual(typeof alpha, 'bigint', "getVotingPowerEmaAlpha should return a bigint"); + // Default alpha is 0_003_570_000_000_000_000 // 0.00357 * 10^18 = 2 weeks e-folding (time-constant) @ 361 + assert.strictEqual(alpha, BigInt("3570000000000000"), "Default alpha should be 0.00357 * 10^18 (3570000000000000)"); + }); + }); + + describe("VotingPower Query Functions", () => { + it("getVotingPower returns 0 for hotkey without voting power", async () => { + // Convert hotkey public key to bytes32 format (0x prefixed hex string) + const hotkeyBytes32 = '0x' + Buffer.from(hotkey.publicKey).toString('hex'); + + const votingPower = await publicClient.readContract({ + abi: IVotingPowerABI, + address: toViemAddress(IVOTING_POWER_ADDRESS), + functionName: "getVotingPower", + args: [subnetId, hotkeyBytes32 as `0x${string}`] + }) + + assert.ok(votingPower !== undefined, "getVotingPower should return a value"); + assert.strictEqual(typeof votingPower, 'bigint', "getVotingPower should return a bigint"); + // Without voting power tracking enabled, voting power should be 0 + assert.strictEqual(votingPower, BigInt(0), "Voting power should be 0 when tracking is disabled"); + }); + + it("getVotingPower returns 0 for unknown hotkey", async () => { + // Generate a random hotkey that doesn't exist + const randomHotkey = getRandomSubstrateKeypair(); + const randomHotkeyBytes32 = '0x' + Buffer.from(randomHotkey.publicKey).toString('hex'); + + const votingPower = await publicClient.readContract({ + abi: IVotingPowerABI, + address: toViemAddress(IVOTING_POWER_ADDRESS), + functionName: "getVotingPower", + args: [subnetId, randomHotkeyBytes32 as `0x${string}`] + }) + + assert.ok(votingPower !== undefined, "getVotingPower should return a value"); + assert.strictEqual(votingPower, BigInt(0), "Voting power should be 0 for unknown hotkey"); + }); + + it("getTotalVotingPower returns 0 when no voting power exists", async () => { + const totalVotingPower = await publicClient.readContract({ + abi: IVotingPowerABI, + address: toViemAddress(IVOTING_POWER_ADDRESS), + functionName: "getTotalVotingPower", + args: [subnetId] + }) + + assert.ok(totalVotingPower !== undefined, "getTotalVotingPower should return a value"); + assert.strictEqual(typeof totalVotingPower, 'bigint', "getTotalVotingPower should return a bigint"); + assert.strictEqual(totalVotingPower, BigInt(0), "Total voting power should be 0 when tracking is disabled"); + }); + }); + + describe("VotingPower with Tracking Enabled", () => { + let enabledSubnetId: number; + + before(async () => { + // Create a new subnet for this test + const hotkey2 = getRandomSubstrateKeypair(); + const coldkey2 = getRandomSubstrateKeypair(); + + await forceSetBalanceToSs58Address(api, convertPublicKeyToSs58(hotkey2.publicKey)) + await forceSetBalanceToSs58Address(api, convertPublicKeyToSs58(coldkey2.publicKey)) + + enabledSubnetId = await addNewSubnetwork(api, hotkey2, coldkey2) + await startCall(api, enabledSubnetId, coldkey2) + + // Enable voting power tracking via sudo + const internalCall = api.tx.SubtensorModule.enable_voting_power_tracking({ netuid: enabledSubnetId }) + const tx = api.tx.Sudo.sudo({ call: internalCall.decodedCall }) + await waitForTransactionWithRetry(api, tx, alice) + }); + + it("isVotingPowerTrackingEnabled returns true after enabling", async () => { + const isEnabled = await publicClient.readContract({ + abi: IVotingPowerABI, + address: toViemAddress(IVOTING_POWER_ADDRESS), + functionName: "isVotingPowerTrackingEnabled", + args: [enabledSubnetId] + }) + + assert.strictEqual(isEnabled, true, "Voting power tracking should be enabled"); + }); + + it("getVotingPowerDisableAtBlock still returns 0 when enabled but not scheduled for disable", async () => { + const disableAtBlock = await publicClient.readContract({ + abi: IVotingPowerABI, + address: toViemAddress(IVOTING_POWER_ADDRESS), + functionName: "getVotingPowerDisableAtBlock", + args: [enabledSubnetId] + }) + + assert.strictEqual(disableAtBlock, BigInt(0), "Disable at block should still be 0"); + }); + }); + + describe("All precompile functions are accessible", () => { + it("All VotingPower precompile functions can be called", async () => { + const hotkeyBytes32 = '0x' + Buffer.from(hotkey.publicKey).toString('hex'); + + // Test all five functions + const results = await Promise.all([ + publicClient.readContract({ + abi: IVotingPowerABI, + address: toViemAddress(IVOTING_POWER_ADDRESS), + functionName: "getVotingPower", + args: [subnetId, hotkeyBytes32 as `0x${string}`] + }), + publicClient.readContract({ + abi: IVotingPowerABI, + address: toViemAddress(IVOTING_POWER_ADDRESS), + functionName: "isVotingPowerTrackingEnabled", + args: [subnetId] + }), + publicClient.readContract({ + abi: IVotingPowerABI, + address: toViemAddress(IVOTING_POWER_ADDRESS), + functionName: "getVotingPowerDisableAtBlock", + args: [subnetId] + }), + publicClient.readContract({ + abi: IVotingPowerABI, + address: toViemAddress(IVOTING_POWER_ADDRESS), + functionName: "getVotingPowerEmaAlpha", + args: [subnetId] + }), + publicClient.readContract({ + abi: IVotingPowerABI, + address: toViemAddress(IVOTING_POWER_ADDRESS), + functionName: "getTotalVotingPower", + args: [subnetId] + }) + ]); + + // All functions should return defined values + results.forEach((result: unknown, index: number) => { + assert.ok(result !== undefined, `Function ${index} should return a value`); + }); + + // Verify types + assert.strictEqual(typeof results[0], 'bigint', "getVotingPower should return bigint"); + assert.strictEqual(typeof results[1], 'boolean', "isVotingPowerTrackingEnabled should return boolean"); + assert.strictEqual(typeof results[2], 'bigint', "getVotingPowerDisableAtBlock should return bigint"); + assert.strictEqual(typeof results[3], 'bigint', "getVotingPowerEmaAlpha should return bigint"); + assert.strictEqual(typeof results[4], 'bigint', "getTotalVotingPower should return bigint"); + }); + }); +}); diff --git a/contract-tests/yarn.lock b/contract-tests/yarn.lock index 25300ca989..080ecb1325 100644 --- a/contract-tests/yarn.lock +++ b/contract-tests/yarn.lock @@ -155,35 +155,50 @@ dependencies: "@noble/hashes" "1.8.0" -"@noble/hashes@^1.3.1", "@noble/hashes@^1.3.3", "@noble/hashes@^1.5.0", "@noble/hashes@^1.8.0", "@noble/hashes@~1.8.0", "@noble/hashes@1.8.0": +"@noble/hashes@^1.3.1": version "1.8.0" resolved "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz" integrity sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A== -"@noble/hashes@^2.0.0", "@noble/hashes@~2.0.0", "@noble/hashes@2.0.1": - version "2.0.1" - resolved "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz" - integrity sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw== +"@noble/hashes@^1.3.3", "@noble/hashes@~1.8.0": + version "1.8.0" + resolved "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz" + integrity sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A== + +"@noble/hashes@^1.5.0": + version "1.8.0" + resolved "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz" + integrity sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A== -"@noble/hashes@^2.0.1", "@noble/hashes@2.0.1": +"@noble/hashes@^1.8.0", "@noble/hashes@1.8.0": + version "1.8.0" + resolved "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz" + integrity sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A== + +"@noble/hashes@^2.0.0", "@noble/hashes@^2.0.1", "@noble/hashes@~2.0.0", "@noble/hashes@2.0.1": version "2.0.1" resolved "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz" integrity sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw== -"@noble/hashes@~1.7.1", "@noble/hashes@1.7.2": - version "1.7.2" - resolved "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.2.tgz" - integrity sha512-biZ0NUSxyjLLqo6KxEJ1b+C2NAx0wtDoFvCaXHGgUkeHzf3Xc1xKumFKREuT7f7DARNZ/slvYUwFG6B0f2b6hQ== +"@noble/hashes@~1.7.1", "@noble/hashes@1.7.1": + version "1.7.1" + resolved "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.1.tgz" + integrity sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ== + +"@noble/hashes@~1.8.0": + version "1.8.0" + resolved "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz" + integrity sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A== "@noble/hashes@1.3.2": version "1.3.2" resolved "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz" integrity sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ== -"@noble/hashes@1.7.1": - version "1.7.1" - resolved "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.1.tgz" - integrity sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ== +"@noble/hashes@1.7.2": + version "1.7.2" + resolved "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.2.tgz" + integrity sha512-biZ0NUSxyjLLqo6KxEJ1b+C2NAx0wtDoFvCaXHGgUkeHzf3Xc1xKumFKREuT7f7DARNZ/slvYUwFG6B0f2b6hQ== "@pkgjs/parseargs@^0.11.0": version "0.11.0" @@ -240,7 +255,7 @@ integrity sha512-cgA9fh8dfBai9b46XaaQmj9vwzyHStQjc/xrAvQksgF6SqvZ0yAfxVqLvGrsz/Xi3dsAdKLg09PybC7MUAMv9w== "@polkadot-api/descriptors@file:.papi/descriptors": - version "0.1.0-autogenerated.14746733976505338329" + version "0.1.0-autogenerated.5063582544821983772" resolved "file:.papi/descriptors" "@polkadot-api/ink-contracts@^0.4.1", "@polkadot-api/ink-contracts@>=0.4.0", "@polkadot-api/ink-contracts@0.4.3": @@ -262,7 +277,7 @@ resolved "https://registry.npmjs.org/@polkadot-api/json-rpc-provider-proxy/-/json-rpc-provider-proxy-0.2.7.tgz" integrity sha512-+HM4JQXzO2GPUD2++4GOLsmFL6LO8RoLvig0HgCLuypDgfdZMlwd8KnyGHjRnVEHA5X+kvXbk84TDcAXVxTazQ== -"@polkadot-api/json-rpc-provider@^0.0.1": +"@polkadot-api/json-rpc-provider@^0.0.1", "@polkadot-api/json-rpc-provider@0.0.1": version "0.0.1" resolved "https://registry.npmjs.org/@polkadot-api/json-rpc-provider/-/json-rpc-provider-0.0.1.tgz" integrity sha512-/SMC/l7foRjpykLTUTacIH05H3mr9ip8b5xxfwXlVezXrNVLp3Cv0GX6uItkKd+ZjzVPf3PFrDF2B2/HLSNESA== @@ -445,7 +460,15 @@ "@scure/base" "^1.1.1" scale-ts "^1.6.0" -"@polkadot-api/substrate-client@^0.1.2", "@polkadot-api/substrate-client@0.1.4", "@polkadot-api/substrate-client@0.4.7": +"@polkadot-api/substrate-client@^0.1.2", "@polkadot-api/substrate-client@0.1.4": + version "0.1.4" + resolved "https://registry.npmjs.org/@polkadot-api/substrate-client/-/substrate-client-0.1.4.tgz" + integrity sha512-MljrPobN0ZWTpn++da9vOvt+Ex+NlqTlr/XT7zi9sqPtDJiQcYl+d29hFAgpaeTqbeQKZwz3WDE9xcEfLE8c5A== + dependencies: + "@polkadot-api/json-rpc-provider" "0.0.1" + "@polkadot-api/utils" "0.1.0" + +"@polkadot-api/substrate-client@0.4.7": version "0.4.7" resolved "https://registry.npmjs.org/@polkadot-api/substrate-client/-/substrate-client-0.4.7.tgz" integrity sha512-Mmx9VKincVqfVQmq89gzDk4DN3uKwf8CxoqYvq+EiPUZ1QmMUc7X4QMwG1MXIlYdnm5LSXzn+2Jn8ik8xMgL+w== @@ -2476,6 +2499,13 @@ signal-exit@^4.0.1, signal-exit@^4.1.0: resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz" integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== +smoldot@2.0.26, smoldot@2.x: + version "2.0.26" + resolved "https://registry.npmjs.org/smoldot/-/smoldot-2.0.26.tgz" + integrity sha512-F+qYmH4z2s2FK+CxGj8moYcd1ekSIKH8ywkdqlOz88Dat35iB1DIYL11aILN46YSGMzQW/lbJNS307zBSDN5Ig== + dependencies: + ws "^8.8.1" + smoldot@2.0.39: version "2.0.39" resolved "https://registry.npmjs.org/smoldot/-/smoldot-2.0.39.tgz" diff --git a/node/src/benchmarking.rs b/node/src/benchmarking.rs index 5430a75d9e..02e31e750e 100644 --- a/node/src/benchmarking.rs +++ b/node/src/benchmarking.rs @@ -124,28 +124,25 @@ pub fn create_benchmark_extrinsic( .checked_next_power_of_two() .map(|c| c / 2) .unwrap_or(2) as u64; - let extra: runtime::TransactionExtensions = - ( - frame_system::CheckNonZeroSender::::new(), - frame_system::CheckSpecVersion::::new(), - frame_system::CheckTxVersion::::new(), - frame_system::CheckGenesis::::new(), - frame_system::CheckEra::::from(sp_runtime::generic::Era::mortal( - period, - best_block.saturated_into(), - )), - check_nonce::CheckNonce::::from(nonce), - frame_system::CheckWeight::::new(), - transaction_payment_wrapper::ChargeTransactionPaymentWrapper::new( - pallet_transaction_payment::ChargeTransactionPayment::::from(0), - ), - sudo_wrapper::SudoTransactionExtension::::new(), - pallet_subtensor::transaction_extension::SubtensorTransactionExtension::< - runtime::Runtime, - >::new(), - pallet_drand::drand_priority::DrandPriority::::new(), - frame_metadata_hash_extension::CheckMetadataHash::::new(true), - ); + let extra: runtime::TransactionExtensions = ( + frame_system::CheckNonZeroSender::::new(), + frame_system::CheckSpecVersion::::new(), + frame_system::CheckTxVersion::::new(), + frame_system::CheckGenesis::::new(), + frame_system::CheckEra::::from(sp_runtime::generic::Era::mortal( + period, + best_block.saturated_into(), + )), + check_nonce::CheckNonce::::from(nonce), + frame_system::CheckWeight::::new(), + transaction_payment_wrapper::ChargeTransactionPaymentWrapper::new( + pallet_transaction_payment::ChargeTransactionPayment::::from(0), + ), + sudo_wrapper::SudoTransactionExtension::::new(), + pallet_subtensor::SubtensorTransactionExtension::::new(), + pallet_drand::drand_priority::DrandPriority::::new(), + frame_metadata_hash_extension::CheckMetadataHash::::new(true), + ); let raw_payload = runtime::SignedPayload::from_raw( call.clone(), diff --git a/node/src/mev_shield/author.rs b/node/src/mev_shield/author.rs index 8ac57b1d52..35f362df3d 100644 --- a/node/src/mev_shield/author.rs +++ b/node/src/mev_shield/author.rs @@ -375,28 +375,24 @@ where let current_block: u64 = info.best_number.saturated_into(); let era = Era::mortal(ERA_PERIOD, current_block); - let extra: Extra = - ( - frame_system::CheckNonZeroSender::::new(), - frame_system::CheckSpecVersion::::new(), - frame_system::CheckTxVersion::::new(), - frame_system::CheckGenesis::::new(), - frame_system::CheckEra::::from(era), - node_subtensor_runtime::check_nonce::CheckNonce::::from(nonce).into(), - frame_system::CheckWeight::::new(), - node_subtensor_runtime::transaction_payment_wrapper::ChargeTransactionPaymentWrapper::< - runtime::Runtime, - >::new(pallet_transaction_payment::ChargeTransactionPayment::< - runtime::Runtime, - >::from(0u64)), - node_subtensor_runtime::sudo_wrapper::SudoTransactionExtension::::new( - ), - pallet_subtensor::transaction_extension::SubtensorTransactionExtension::< - runtime::Runtime, - >::new(), - pallet_drand::drand_priority::DrandPriority::::new(), - frame_metadata_hash_extension::CheckMetadataHash::::new(false), - ); + let extra: Extra = ( + frame_system::CheckNonZeroSender::::new(), + frame_system::CheckSpecVersion::::new(), + frame_system::CheckTxVersion::::new(), + frame_system::CheckGenesis::::new(), + frame_system::CheckEra::::from(era), + node_subtensor_runtime::check_nonce::CheckNonce::::from(nonce).into(), + frame_system::CheckWeight::::new(), + node_subtensor_runtime::transaction_payment_wrapper::ChargeTransactionPaymentWrapper::< + runtime::Runtime, + >::new(pallet_transaction_payment::ChargeTransactionPayment::< + runtime::Runtime, + >::from(0u64)), + node_subtensor_runtime::sudo_wrapper::SudoTransactionExtension::::new(), + pallet_subtensor::SubtensorTransactionExtension::::new(), + pallet_drand::drand_priority::DrandPriority::::new(), + frame_metadata_hash_extension::CheckMetadataHash::::new(false), + ); // 3) Manually construct the `Implicit` tuple that the runtime will also derive. type Implicit = >::Implicit; diff --git a/pallets/admin-utils/src/benchmarking.rs b/pallets/admin-utils/src/benchmarking.rs index 7b8124144d..9cab4894e9 100644 --- a/pallets/admin-utils/src/benchmarking.rs +++ b/pallets/admin-utils/src/benchmarking.rs @@ -263,7 +263,7 @@ mod benchmarks { ); #[extrinsic_call] - _(RawOrigin::Root, 1u16.into()/*netuid*/, 2048u16/*max_allowed_uids*/)/*sudo_set_max_allowed_uids*/; + _(RawOrigin::Root, 1u16.into()/*netuid*/, 256u16/*max_allowed_uids*/)/*sudo_set_max_allowed_uids*/; } #[benchmark] @@ -480,7 +480,13 @@ mod benchmarks { } #[benchmark] - fn sudo_set_coldkey_swap_schedule_duration() { + fn sudo_set_coldkey_swap_announcement_delay() { + #[extrinsic_call] + _(RawOrigin::Root, 100u32.into()); + } + + #[benchmark] + fn sudo_set_coldkey_swap_reannouncement_delay() { #[extrinsic_call] _(RawOrigin::Root, 100u32.into()); } diff --git a/pallets/admin-utils/src/lib.rs b/pallets/admin-utils/src/lib.rs index 4143e3fb3a..654f7207b8 100644 --- a/pallets/admin-utils/src/lib.rs +++ b/pallets/admin-utils/src/lib.rs @@ -145,6 +145,8 @@ pub mod pallet { Leasing, /// Address mapping precompile AddressMapping, + /// Voting power precompile + VotingPower, } #[pallet::type_value] @@ -505,7 +507,7 @@ pub mod pallet { /// The extrinsic will call the Subtensor pallet to set the maximum allowed UIDs for a subnet. #[pallet::call_index(15)] #[pallet::weight(Weight::from_parts(32_140_000, 0) - .saturating_add(::DbWeight::get().reads(5_u64)) + .saturating_add(::DbWeight::get().reads(6_u64)) .saturating_add(::DbWeight::get().writes(1_u64)))] pub fn sudo_set_max_allowed_uids( origin: OriginFor, @@ -534,6 +536,12 @@ pub mod pallet { max_allowed_uids <= DefaultMaxAllowedUids::::get(), Error::::MaxAllowedUidsGreaterThanDefaultMaxAllowedUids ); + // Prevent chain bloat: Require max UIDs to be limited + let mechanism_count = pallet_subtensor::MechanismCountCurrent::::get(netuid); + pallet_subtensor::Pallet::::ensure_max_uids_over_all_mechanisms( + max_allowed_uids, + mechanism_count.into(), + )?; pallet_subtensor::Pallet::::set_max_allowed_uids(netuid, max_allowed_uids); pallet_subtensor::Pallet::::record_owner_rl( maybe_owner, @@ -1291,40 +1299,6 @@ pub mod pallet { res } - /// Sets the duration of the coldkey swap schedule. - /// - /// This extrinsic allows the root account to set the duration for the coldkey swap schedule. - /// The coldkey swap schedule determines how long it takes for a coldkey swap operation to complete. - /// - /// # Arguments - /// * `origin` - The origin of the call, which must be the root account. - /// * `duration` - The new duration for the coldkey swap schedule, in number of blocks. - /// - /// # Errors - /// * `BadOrigin` - If the caller is not the root account. - /// - /// # Weight - /// Weight is handled by the `#[pallet::weight]` attribute. - #[pallet::call_index(54)] - #[pallet::weight(Weight::from_parts(5_000_000, 0) - .saturating_add(T::DbWeight::get().reads(0_u64)) - .saturating_add(T::DbWeight::get().writes(1_u64)))] - pub fn sudo_set_coldkey_swap_schedule_duration( - origin: OriginFor, - duration: BlockNumberFor, - ) -> DispatchResult { - // Ensure the call is made by the root account - ensure_root(origin)?; - - // Set the new duration of schedule coldkey swap - pallet_subtensor::Pallet::::set_coldkey_swap_schedule_duration(duration); - - // Log the change - log::trace!("ColdkeySwapScheduleDurationSet( duration: {duration:?} )"); - - Ok(()) - } - /// Sets the duration of the dissolve network schedule. /// /// This extrinsic allows the root account to set the duration for the dissolve network schedule. @@ -2091,6 +2065,20 @@ pub mod pallet { Ok(()) } + /// Sets the global maximum number of mechanisms in a subnet + #[pallet::call_index(88)] + #[pallet::weight(Weight::from_parts(15_000_000, 0) + .saturating_add(::DbWeight::get().reads(1_u64)) + .saturating_add(::DbWeight::get().writes(1_u64)))] + pub fn sudo_set_max_mechanism_count( + origin: OriginFor, + max_mechanism_count: MechId, + ) -> DispatchResult { + ensure_root(origin)?; + pallet_subtensor::Pallet::::do_set_max_mechanism_count(max_mechanism_count)?; + Ok(()) + } + /// Sets the minimum number of non-immortal & non-immune UIDs that must remain in a subnet #[pallet::call_index(84)] #[pallet::weight(Weight::from_parts(7_114_000, 0) @@ -2116,6 +2104,36 @@ pub mod pallet { log::debug!("StartCallDelay( delay: {delay:?} ) "); Ok(()) } + + /// Sets the announcement delay for coldkey swap. + #[pallet::call_index(86)] + #[pallet::weight(Weight::from_parts(5_000_000, 0) + .saturating_add(::DbWeight::get().reads(0_u64)) + .saturating_add(::DbWeight::get().writes(1_u64)))] + pub fn sudo_set_coldkey_swap_announcement_delay( + origin: OriginFor, + duration: BlockNumberFor, + ) -> DispatchResult { + ensure_root(origin)?; + pallet_subtensor::Pallet::::set_coldkey_swap_announcement_delay(duration); + log::trace!("ColdkeySwapAnnouncementDelaySet( duration: {duration:?} )"); + Ok(()) + } + + /// Sets the coldkey swap reannouncement delay. + #[pallet::call_index(87)] + #[pallet::weight(Weight::from_parts(5_000_000, 0) + .saturating_add(::DbWeight::get().reads(0_u64)) + .saturating_add(::DbWeight::get().writes(1_u64)))] + pub fn sudo_set_coldkey_swap_reannouncement_delay( + origin: OriginFor, + duration: BlockNumberFor, + ) -> DispatchResult { + ensure_root(origin)?; + pallet_subtensor::Pallet::::set_coldkey_swap_reannouncement_delay(duration); + log::trace!("ColdkeySwapReannouncementDelaySet( duration: {duration:?} )"); + Ok(()) + } } } diff --git a/pallets/admin-utils/src/tests/mock.rs b/pallets/admin-utils/src/tests/mock.rs index 0117dff889..19d2e891cd 100644 --- a/pallets/admin-utils/src/tests/mock.rs +++ b/pallets/admin-utils/src/tests/mock.rs @@ -19,7 +19,7 @@ use sp_runtime::{ }; use sp_std::cmp::Ordering; use sp_weights::Weight; -use subtensor_runtime_common::{NetUid, TaoCurrency}; +use subtensor_runtime_common::{AuthorshipInfo, NetUid, TaoCurrency}; type Block = frame_system::mocking::MockBlock; // Configure a mock runtime to test the pallet. @@ -74,6 +74,14 @@ pub type BlockNumber = u64; pub type TestAuthId = test_crypto::TestAuthId; pub type UncheckedExtrinsic = TestXt; +pub struct MockAuthorshipProvider; + +impl AuthorshipInfo for MockAuthorshipProvider { + fn author() -> Option { + Some(U256::from(12345u64)) + } +} + parameter_types! { pub const InitialMinAllowedWeights: u16 = 0; pub const InitialEmissionValue: u16 = 0; @@ -91,7 +99,7 @@ parameter_types! { pub const SelfOwnership: u64 = 2; pub const InitialImmunityPeriod: u16 = 2; pub const InitialMinAllowedUids: u16 = 2; - pub const InitialMaxAllowedUids: u16 = 16; + pub const InitialMaxAllowedUids: u16 = 256; pub const InitialBondsMovingAverage: u64 = 900_000; pub const InitialBondsPenalty: u16 = u16::MAX; pub const InitialBondsResetOn: bool = false; @@ -131,17 +139,14 @@ parameter_types! { pub const InitialNetworkMinLockCost: u64 = 100_000_000_000; pub const InitialSubnetOwnerCut: u16 = 0; // 0%. 100% of rewards go to validators + miners. pub const InitialNetworkLockReductionInterval: u64 = 2; // 2 blocks. - // pub const InitialSubnetLimit: u16 = 10; // (DEPRECATED) pub const InitialNetworkRateLimit: u64 = 0; pub const InitialKeySwapCost: u64 = 1_000_000_000; pub const InitialAlphaHigh: u16 = 58982; // Represents 0.9 as per the production default pub const InitialAlphaLow: u16 = 45875; // Represents 0.7 as per the production default pub const InitialLiquidAlphaOn: bool = false; // Default value for LiquidAlphaOn pub const InitialYuma3On: bool = false; // Default value for Yuma3On - // pub const InitialHotkeyEmissionTempo: u64 = 1; // (DEPRECATED) - // pub const InitialNetworkMaxStake: u64 = u64::MAX; // (DEPRECATED) - pub const InitialColdkeySwapScheduleDuration: u64 = 5 * 24 * 60 * 60 / 12; // 5 days - pub const InitialColdkeySwapRescheduleDuration: u64 = 24 * 60 * 60 / 12; // 1 day + pub const InitialColdkeySwapAnnouncementDelay: u64 = 50; + pub const InitialColdkeySwapReannouncementDelay: u64 = 10; pub const InitialDissolveNetworkScheduleDuration: u64 = 5 * 24 * 60 * 60 / 12; // 5 days pub const InitialTaoWeight: u64 = u64::MAX/10; // 10% global weight. pub const InitialEmaPriceHalvingPeriod: u64 = 201_600_u64; // 4 weeks @@ -210,8 +215,8 @@ impl pallet_subtensor::Config for Test { type LiquidAlphaOn = InitialLiquidAlphaOn; type Yuma3On = InitialYuma3On; type Preimages = (); - type InitialColdkeySwapScheduleDuration = InitialColdkeySwapScheduleDuration; - type InitialColdkeySwapRescheduleDuration = InitialColdkeySwapRescheduleDuration; + type InitialColdkeySwapAnnouncementDelay = InitialColdkeySwapAnnouncementDelay; + type InitialColdkeySwapReannouncementDelay = InitialColdkeySwapReannouncementDelay; type InitialDissolveNetworkScheduleDuration = InitialDissolveNetworkScheduleDuration; type InitialTaoWeight = InitialTaoWeight; type InitialEmaPriceHalvingPeriod = InitialEmaPriceHalvingPeriod; @@ -225,6 +230,7 @@ impl pallet_subtensor::Config for Test { type MaxImmuneUidsPercentage = MaxImmuneUidsPercentage; type CommitmentsInterface = CommitmentsI; type EvmKeyAssociateRateLimit = EvmKeyAssociateRateLimit; + type AuthorshipProvider = MockAuthorshipProvider; } parameter_types! { @@ -325,7 +331,6 @@ impl pallet_balances::Config for Test { parameter_types! { pub const SwapProtocolId: PalletId = PalletId(*b"ten/swap"); pub const SwapMaxFeeRate: u16 = 10000; // 15.26% - pub const SwapMaxPositions: u32 = 100; pub const SwapMinimumLiquidity: u64 = 1_000; pub const SwapMinimumReserve: NonZeroU64 = NonZeroU64::new(1_000_000).unwrap(); } @@ -337,7 +342,6 @@ impl pallet_subtensor_swap::Config for Test { type TaoReserve = pallet_subtensor::TaoCurrencyReserve; type AlphaReserve = pallet_subtensor::AlphaCurrencyReserve; type MaxFeeRate = SwapMaxFeeRate; - type MaxPositions = SwapMaxPositions; type MinimumLiquidity = SwapMinimumLiquidity; type MinimumReserve = SwapMinimumReserve; type WeightInfo = (); diff --git a/pallets/admin-utils/src/tests/mod.rs b/pallets/admin-utils/src/tests/mod.rs index 93d56ec343..f87ba31bba 100644 --- a/pallets/admin-utils/src/tests/mod.rs +++ b/pallets/admin-utils/src/tests/mod.rs @@ -10,7 +10,10 @@ use pallet_subtensor::{ TargetRegistrationsPerInterval, Tempo, WeightsVersionKeyRateLimit, *, }; // use pallet_subtensor::{migrations, Event}; -use pallet_subtensor::{Event, utils::rate_limiting::TransactionType}; +use pallet_subtensor::{ + Event, subnets::mechanism::MAX_MECHANISM_COUNT_PER_SUBNET, + utils::rate_limiting::TransactionType, +}; use sp_consensus_grandpa::AuthorityId as GrandpaId; use sp_core::{Get, Pair, U256, ed25519}; use substrate_fixed::types::I96F32; @@ -540,6 +543,20 @@ fn test_sudo_set_max_allowed_uids() { Error::::MaxAllowedUidsGreaterThanDefaultMaxAllowedUids ); + // Trying to set max allowed uids that would cause max_allowed_uids * mechanism_count > 256 + MaxAllowedUids::::insert(netuid, 8); + MechanismCountCurrent::::insert(netuid, MechId::from(32)); + let large_max_uids = 16; + assert_noop!( + AdminUtils::sudo_set_max_allowed_uids( + <::RuntimeOrigin>::root(), + netuid, + large_max_uids + ), + SubtensorError::::TooManyUIDsPerMechanism + ); + MechanismCountCurrent::::insert(netuid, MechId::from(1)); + // Normal case assert_ok!(AdminUtils::sudo_set_max_allowed_uids( <::RuntimeOrigin>::root(), @@ -1382,39 +1399,74 @@ fn test_sudo_get_set_alpha() { } #[test] -fn test_sudo_set_coldkey_swap_schedule_duration() { +fn test_sudo_set_coldkey_swap_announcement_delay() { new_test_ext().execute_with(|| { // Arrange let root = RuntimeOrigin::root(); let non_root = RuntimeOrigin::signed(U256::from(1)); - let new_duration = 100u32.into(); + let new_delay = 100u32.into(); // Act & Assert: Non-root account should fail assert_noop!( - AdminUtils::sudo_set_coldkey_swap_schedule_duration(non_root, new_duration), + AdminUtils::sudo_set_coldkey_swap_announcement_delay(non_root, new_delay), DispatchError::BadOrigin ); // Act: Root account should succeed - assert_ok!(AdminUtils::sudo_set_coldkey_swap_schedule_duration( + assert_ok!(AdminUtils::sudo_set_coldkey_swap_announcement_delay( root.clone(), - new_duration + new_delay )); - // Assert: Check if the duration was actually set + // Assert: Check if the delay was actually set assert_eq!( - pallet_subtensor::ColdkeySwapScheduleDuration::::get(), - new_duration + pallet_subtensor::ColdkeySwapAnnouncementDelay::::get(), + new_delay ); // Act & Assert: Setting the same value again should succeed (idempotent operation) - assert_ok!(AdminUtils::sudo_set_coldkey_swap_schedule_duration( - root, - new_duration + assert_ok!(AdminUtils::sudo_set_coldkey_swap_announcement_delay( + root, new_delay )); // You might want to check for events here if your pallet emits them - System::assert_last_event(Event::ColdkeySwapScheduleDurationSet(new_duration).into()); + System::assert_last_event(Event::ColdkeySwapAnnouncementDelaySet(new_delay).into()); + }); +} + +#[test] +fn test_sudo_set_coldkey_swap_reannouncement_delay() { + new_test_ext().execute_with(|| { + // Arrange + let root = RuntimeOrigin::root(); + let non_root = RuntimeOrigin::signed(U256::from(1)); + let new_delay = 100u32.into(); + + // Act & Assert: Non-root account should fail + assert_noop!( + AdminUtils::sudo_set_coldkey_swap_reannouncement_delay(non_root, new_delay), + DispatchError::BadOrigin + ); + + // Act: Root account should succeed + assert_ok!(AdminUtils::sudo_set_coldkey_swap_reannouncement_delay( + root.clone(), + new_delay + )); + + // Assert: Check if the delay was actually set + assert_eq!( + pallet_subtensor::ColdkeySwapReannouncementDelay::::get(), + new_delay + ); + + // Act & Assert: Setting the same value again should succeed (idempotent operation) + assert_ok!(AdminUtils::sudo_set_coldkey_swap_reannouncement_delay( + root, new_delay + )); + + // You might want to check for events here if your pallet emits them + System::assert_last_event(Event::ColdkeySwapReannouncementDelaySet(new_delay).into()); }); } @@ -2341,6 +2393,7 @@ fn test_sudo_set_mechanism_count() { add_network(netuid, 10); // Set the Subnet Owner SubnetOwner::::insert(netuid, sn_owner); + MaxAllowedUids::::insert(netuid, 256_u16); assert_eq!( AdminUtils::sudo_set_mechanism_count( @@ -2354,7 +2407,13 @@ fn test_sudo_set_mechanism_count() { AdminUtils::sudo_set_mechanism_count(RuntimeOrigin::root(), netuid, ss_count_bad), pallet_subtensor::Error::::InvalidValue ); + assert_noop!( + AdminUtils::sudo_set_mechanism_count(RuntimeOrigin::root(), netuid, ss_count_ok), + pallet_subtensor::Error::::TooManyUIDsPerMechanism + ); + // Reduce max UIDs to 128 + MaxAllowedUids::::insert(netuid, 128_u16); assert_ok!(AdminUtils::sudo_set_mechanism_count( <::RuntimeOrigin>::root(), netuid, @@ -2380,6 +2439,8 @@ fn test_sudo_set_mechanism_count_and_emissions() { add_network(netuid, 10); // Set the Subnet Owner SubnetOwner::::insert(netuid, sn_owner); + MaxMechanismCount::::set(MechId::from(2)); + MaxAllowedUids::::set(netuid, 128_u16); assert_ok!(AdminUtils::sudo_set_mechanism_count( <::RuntimeOrigin>::signed(sn_owner), @@ -2868,6 +2929,35 @@ fn test_sudo_set_min_allowed_uids() { }); } +#[test] +fn test_sudo_set_max_mechanism_count() { + new_test_ext().execute_with(|| { + // Normal case + assert_ok!(AdminUtils::sudo_set_max_mechanism_count( + <::RuntimeOrigin>::root(), + MechId::from(10) + )); + + // Zero fails + assert_noop!( + AdminUtils::sudo_set_max_mechanism_count( + <::RuntimeOrigin>::root(), + MechId::from(0) + ), + pallet_subtensor::Error::::InvalidValue + ); + + // Over max bound fails + assert_noop!( + AdminUtils::sudo_set_max_mechanism_count( + <::RuntimeOrigin>::root(), + MechId::from(MAX_MECHANISM_COUNT_PER_SUBNET + 1) + ), + pallet_subtensor::Error::::InvalidValue + ); + }); +} + #[test] fn test_sudo_set_min_non_immune_uids() { new_test_ext().execute_with(|| { diff --git a/pallets/shield/src/benchmarking.rs b/pallets/shield/src/benchmarking.rs index 1414779314..e3890bae76 100644 --- a/pallets/shield/src/benchmarking.rs +++ b/pallets/shield/src/benchmarking.rs @@ -4,7 +4,7 @@ use frame_benchmarking::v2::*; use frame_support::{BoundedVec, pallet_prelude::ConstU32}; use frame_system::{RawOrigin, pallet_prelude::BlockNumberFor}; use sp_core::sr25519; -use sp_runtime::traits::Hash as HashT; +use sp_runtime::{AccountId32, traits::Hash as HashT}; use sp_std::vec; // /// Helper to build bounded bytes (public key) of a given length. @@ -40,6 +40,8 @@ fn bounded_ct(len: usize) -> BoundedVec> { ::RuntimeCall: From>, // Needed so we can seed Authorities from a dev sr25519 pubkey. ::AuthorityId: From, + ::AccountId: From + Into, + ::RuntimeOrigin: From> )] mod benches { use super::*; diff --git a/pallets/shield/src/lib.rs b/pallets/shield/src/lib.rs index eed0161f20..e0fc250058 100644 --- a/pallets/shield/src/lib.rs +++ b/pallets/shield/src/lib.rs @@ -86,9 +86,7 @@ pub mod pallet { #[pallet::config] pub trait Config: - frame_system::Config>> - + pallet_timestamp::Config - + pallet_aura::Config + frame_system::Config>> + pallet_aura::Config { type RuntimeCall: Parameter + sp_runtime::traits::Dispatchable< @@ -96,7 +94,7 @@ pub mod pallet { PostInfo = PostDispatchInfo, > + GetDispatchInfo; - type AuthorityOrigin: AuthorityOriginExt; + type AuthorityOrigin: AuthorityOriginExt; } #[pallet::pallet] @@ -246,13 +244,9 @@ pub mod pallet { /// Announce the ML‑KEM public key that will become `CurrentKey` in /// the following block. #[pallet::call_index(0)] - #[pallet::weight(( - Weight::from_parts(20_999_999_999, 0) - .saturating_add(T::DbWeight::get().reads(1_u64)) - .saturating_add(T::DbWeight::get().writes(1_u64)), - DispatchClass::Operational, - Pays::Yes - ))] + #[pallet::weight(Weight::from_parts(20_999_999_999, 0) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)))] #[allow(clippy::useless_conversion)] pub fn announce_next_key( origin: OriginFor, diff --git a/pallets/subtensor/Cargo.toml b/pallets/subtensor/Cargo.toml index 2e35a89d19..a6fadec1f4 100644 --- a/pallets/subtensor/Cargo.toml +++ b/pallets/subtensor/Cargo.toml @@ -55,11 +55,15 @@ sha2.workspace = true rand_chacha.workspace = true pallet-crowdloan.workspace = true pallet-subtensor-proxy.workspace = true +pallet-shield.workspace = true [dev-dependencies] pallet-balances = { workspace = true, features = ["std"] } pallet-scheduler.workspace = true pallet-subtensor-proxy.workspace = true +pallet-aura.workspace = true +pallet-timestamp.workspace = true +sp-consensus-aura.workspace = true subtensor-runtime-common.workspace = true pallet-subtensor-swap.workspace = true sp-version.workspace = true @@ -87,7 +91,10 @@ try-runtime = [ "pallet-crowdloan/try-runtime", "pallet-drand/try-runtime", "pallet-subtensor-proxy/try-runtime", - "pallet-subtensor-utility/try-runtime" + "pallet-subtensor-utility/try-runtime", + "pallet-shield/try-runtime", + "pallet-timestamp/try-runtime", + "pallet-aura/try-runtime", ] default = ["std"] std = [ @@ -115,6 +122,10 @@ std = [ "pallet-drand/std", "pallet-subtensor-proxy/std", "pallet-subtensor-swap/std", + "pallet-shield/std", + "pallet-timestamp/std", + "pallet-aura/std", + "sp-consensus-aura/std", "subtensor-swap-interface/std", "pallet-subtensor-utility/std", "safe-math/std", @@ -147,7 +158,9 @@ runtime-benchmarks = [ "pallet-drand/runtime-benchmarks", "pallet-subtensor-proxy/runtime-benchmarks", "pallet-subtensor-swap/runtime-benchmarks", - "pallet-subtensor-utility/runtime-benchmarks" + "pallet-subtensor-utility/runtime-benchmarks", + "pallet-shield/runtime-benchmarks", + "pallet-timestamp/runtime-benchmarks" ] pow-faucet = [] fast-runtime = ["subtensor-runtime-common/fast-runtime"] diff --git a/pallets/subtensor/src/benchmarks.rs b/pallets/subtensor/src/benchmarks.rs index f61c35aede..a43a468bef 100644 --- a/pallets/subtensor/src/benchmarks.rs +++ b/pallets/subtensor/src/benchmarks.rs @@ -16,9 +16,15 @@ use sp_runtime::{ }; use sp_std::collections::btree_set::BTreeSet; use sp_std::vec; +use substrate_fixed::types::U64F64; use subtensor_runtime_common::{AlphaCurrency, NetUid, TaoCurrency}; +use subtensor_swap_interface::SwapHandler; -#[frame_benchmarking::v2::benchmarks] +#[benchmarks( + where + T: pallet_balances::Config, + ::ExistentialDeposit: Get, +)] mod pallet_benchmarks { use super::*; @@ -62,6 +68,8 @@ mod pallet_benchmarks { Subtensor::::set_max_registrations_per_block(netuid, 4096); Subtensor::::set_target_registrations_per_interval(netuid, 4096); Subtensor::::set_commit_reveal_weights_enabled(netuid, false); + SubnetTAO::::insert(netuid, TaoCurrency::from(1_000_000_000_000_u64)); + SubnetAlphaIn::::insert(netuid, AlphaCurrency::from(1_000_000_000_000_000_u64)); let mut seed: u32 = 1; let mut dests = Vec::new(); @@ -369,17 +377,6 @@ mod pallet_benchmarks { ); } - #[benchmark] - fn schedule_swap_coldkey() { - let old_coldkey: T::AccountId = account("old_cold", 0, 1); - let new_coldkey: T::AccountId = account("new_cold", 1, 2); - let amount: u64 = 100_000_000_000_000; - Subtensor::::add_balance_to_coldkey_account(&old_coldkey, amount); - - #[extrinsic_call] - _(RawOrigin::Signed(old_coldkey.clone()), new_coldkey.clone()); - } - #[benchmark] fn sudo_set_tx_childkey_take_rate_limit() { let new_rate_limit: u64 = 100; @@ -419,14 +416,64 @@ mod pallet_benchmarks { } #[benchmark] - fn swap_coldkey() { + fn announce_coldkey_swap() { + let coldkey: T::AccountId = account("old_coldkey", 0, 0); + let new_coldkey: T::AccountId = account("new_coldkey", 0, 0); + let new_coldkey_hash: T::Hash = ::Hashing::hash_of(&new_coldkey); + + let ed = ::ExistentialDeposit::get(); + let swap_cost = Subtensor::::get_key_swap_cost(); + Subtensor::::add_balance_to_coldkey_account(&coldkey, swap_cost.to_u64() + ed); + + #[extrinsic_call] + _(RawOrigin::Signed(coldkey), new_coldkey_hash); + } + + #[benchmark] + fn swap_coldkey_announced() { let old_coldkey: T::AccountId = account("old_coldkey", 0, 0); let new_coldkey: T::AccountId = account("new_coldkey", 0, 0); + let new_coldkey_hash: T::Hash = ::Hashing::hash_of(&new_coldkey); let hotkey1: T::AccountId = account("hotkey1", 0, 0); + + let now = frame_system::Pallet::::block_number(); + let delay = ColdkeySwapAnnouncementDelay::::get(); + ColdkeySwapAnnouncements::::insert(&old_coldkey, (now, new_coldkey_hash)); + frame_system::Pallet::::set_block_number(now + delay + 1u32.into()); + let netuid = NetUid::from(1); + Subtensor::::init_new_network(netuid, 1); + Subtensor::::set_network_registration_allowed(netuid, true); + Subtensor::::set_network_pow_registration_allowed(netuid, true); + + let block_number = Subtensor::::get_current_block_as_u64(); + let (nonce, work) = + Subtensor::::create_work_for_block_number(netuid, block_number, 3, &hotkey1); + let _ = Subtensor::::register( + RawOrigin::Signed(old_coldkey.clone()).into(), + netuid, + block_number, + nonce, + work.clone(), + hotkey1.clone(), + old_coldkey.clone(), + ); + + #[extrinsic_call] + _(RawOrigin::Signed(old_coldkey), new_coldkey); + } + + #[benchmark] + fn swap_coldkey() { + let old_coldkey: T::AccountId = account("old_coldkey", 0, 0); + let new_coldkey: T::AccountId = account("new_coldkey", 0, 0); + let hotkey1: T::AccountId = account("hotkey1", 0, 0); + + let ed = ::ExistentialDeposit::get(); let swap_cost = Subtensor::::get_key_swap_cost(); - let free_balance_old = swap_cost + 12345.into(); + Subtensor::::add_balance_to_coldkey_account(&old_coldkey, swap_cost.to_u64() + ed); + let netuid = NetUid::from(1); Subtensor::::init_new_network(netuid, 1); Subtensor::::set_network_registration_allowed(netuid, true); Subtensor::::set_network_pow_registration_allowed(netuid, true); @@ -444,19 +491,6 @@ mod pallet_benchmarks { old_coldkey.clone(), ); - Subtensor::::add_balance_to_coldkey_account(&old_coldkey, free_balance_old.into()); - let name: Vec = b"The fourth Coolest Identity".to_vec(); - let identity = ChainIdentityV2 { - name, - url: vec![], - github_repo: vec![], - image: vec![], - discord: vec![], - description: vec![], - additional: vec![], - }; - IdentitiesV2::::insert(&old_coldkey, identity); - #[extrinsic_call] _( RawOrigin::Root, @@ -466,6 +500,31 @@ mod pallet_benchmarks { ); } + #[benchmark] + fn dispute_coldkey_swap() { + let coldkey: T::AccountId = account("old_coldkey", 0, 0); + let coldkey_hash: T::Hash = ::Hashing::hash_of(&coldkey); + let now = frame_system::Pallet::::block_number(); + + ColdkeySwapAnnouncements::::insert(&coldkey, (now, coldkey_hash)); + + #[extrinsic_call] + _(RawOrigin::Signed(coldkey)); + } + + #[benchmark] + fn reset_coldkey_swap() { + let coldkey: T::AccountId = account("old_coldkey", 0, 0); + let coldkey_hash: T::Hash = ::Hashing::hash_of(&coldkey); + let now = frame_system::Pallet::::block_number(); + + ColdkeySwapAnnouncements::::insert(&coldkey, (now, coldkey_hash)); + ColdkeySwapDisputes::::insert(&coldkey, now); + + #[extrinsic_call] + _(RawOrigin::Root, coldkey); + } + #[benchmark] fn batch_reveal_weights() { let tempo: u16 = 0; @@ -674,13 +733,12 @@ mod pallet_benchmarks { let coldkey: T::AccountId = account("Test", 0, seed); let hotkey: T::AccountId = account("Alice", 0, seed); - let amount = 900_000_000_000; - let limit = TaoCurrency::from(6_000_000_000); - let amount_to_be_staked = TaoCurrency::from(44_000_000_000); - Subtensor::::add_balance_to_coldkey_account(&coldkey.clone(), amount); + let initial_balance = 900_000_000_000; + Subtensor::::add_balance_to_coldkey_account(&coldkey.clone(), initial_balance); - let tao_reserve = TaoCurrency::from(150_000_000_000); - let alpha_in = AlphaCurrency::from(100_000_000_000); + // Price = 0.01 + let tao_reserve = TaoCurrency::from(1_000_000_000_000); + let alpha_in = AlphaCurrency::from(100_000_000_000_000); SubnetTAO::::insert(netuid, tao_reserve); SubnetAlphaIn::::insert(netuid, alpha_in); @@ -690,14 +748,23 @@ mod pallet_benchmarks { hotkey.clone() )); + // Read current price and set limit price 0.1% higher, which is certainly getting hit + // by swapping 100 TAO + let current_price = T::SwapInterface::current_alpha_price(netuid); + let limit = current_price + .saturating_mul(U64F64::saturating_from_num(1_001_000_000)) + .saturating_to_num::(); + let amount_to_be_staked = TaoCurrency::from(100_000_000_000); + + // Allow partial (worst case) #[extrinsic_call] _( RawOrigin::Signed(coldkey.clone()), hotkey, netuid, amount_to_be_staked, - limit, - false, + limit.into(), + true, ); } @@ -774,9 +841,9 @@ mod pallet_benchmarks { let hotkey: T::AccountId = account("Alice", 0, seed); Subtensor::::set_burn(netuid, 1.into()); - let limit = TaoCurrency::from(1_000_000_000); - let tao_reserve = TaoCurrency::from(150_000_000_000); - let alpha_in = AlphaCurrency::from(100_000_000_000); + // Price = 0.01 + let tao_reserve = TaoCurrency::from(1_000_000_000_000); + let alpha_in = AlphaCurrency::from(100_000_000_000_000); SubnetTAO::::insert(netuid, tao_reserve); SubnetAlphaIn::::insert(netuid, alpha_in); @@ -799,7 +866,13 @@ mod pallet_benchmarks { u64_staked_amt.into() )); - let amount_unstaked = AlphaCurrency::from(30_000_000_000); + // Read current price and set limit price 0.01% lower, which is certainly getting hit + // by swapping 100 Alpha + let current_price = T::SwapInterface::current_alpha_price(netuid); + let limit = current_price + .saturating_mul(U64F64::saturating_from_num(999_900_000)) + .saturating_to_num::(); + let amount_unstaked = AlphaCurrency::from(100_000_000_000); // Remove stake limit for benchmark StakingOperationRateLimiter::::remove((hotkey.clone(), coldkey.clone(), netuid)); @@ -810,8 +883,8 @@ mod pallet_benchmarks { hotkey.clone(), netuid, amount_unstaked, - limit, - false, + limit.into(), + true, ); } @@ -1265,8 +1338,9 @@ mod pallet_benchmarks { let hotkey: T::AccountId = account("Alice", 0, seed); Subtensor::::set_burn(netuid, 1.into()); - SubnetTAO::::insert(netuid, TaoCurrency::from(150_000_000_000)); - SubnetAlphaIn::::insert(netuid, AlphaCurrency::from(100_000_000_000)); + // Price = 0.01 + SubnetTAO::::insert(netuid, TaoCurrency::from(1_000_000_000_000)); + SubnetAlphaIn::::insert(netuid, AlphaCurrency::from(100_000_000_000_000)); Subtensor::::add_balance_to_coldkey_account(&coldkey.clone(), 1000000u32.into()); @@ -1313,9 +1387,8 @@ mod pallet_benchmarks { let hotkey: T::AccountId = account("Alice", 0, seed); Subtensor::::set_burn(netuid, 1.into()); - let limit = TaoCurrency::from(1_000_000_000); - let tao_reserve = TaoCurrency::from(150_000_000_000); - let alpha_in = AlphaCurrency::from(100_000_000_000); + let tao_reserve = TaoCurrency::from(1_000_000_000_000); + let alpha_in = AlphaCurrency::from(100_000_000_000_000); SubnetTAO::::insert(netuid, tao_reserve); SubnetAlphaIn::::insert(netuid, alpha_in); @@ -1328,7 +1401,13 @@ mod pallet_benchmarks { hotkey.clone() )); - let u64_staked_amt = 100_000_000_000; + // Read current price and set limit price 50% lower, which is not getting hit + // by swapping 1 TAO + let current_price = T::SwapInterface::current_alpha_price(netuid); + let limit = current_price + .saturating_mul(U64F64::saturating_from_num(500_000_000)) + .saturating_to_num::(); + let u64_staked_amt = 1_000_000_000; Subtensor::::add_balance_to_coldkey_account(&coldkey.clone(), u64_staked_amt); assert_ok!(Subtensor::::add_stake( @@ -1345,7 +1424,7 @@ mod pallet_benchmarks { RawOrigin::Signed(coldkey.clone()), hotkey.clone(), netuid, - Some(limit), + Some(limit.into()), ); } @@ -1666,4 +1745,47 @@ mod pallet_benchmarks { #[extrinsic_call] _(RawOrigin::Root, netuid, 100); } + + #[benchmark] + fn add_stake_burn() { + let netuid = NetUid::from(1); + let tempo: u16 = 1; + let seed: u32 = 1; + + Subtensor::::init_new_network(netuid, tempo); + SubtokenEnabled::::insert(netuid, true); + Subtensor::::set_burn(netuid, 1000.into()); + Subtensor::::set_network_registration_allowed(netuid, true); + Subtensor::::set_max_allowed_uids(netuid, 4096); + + let coldkey: T::AccountId = account("Test", 0, seed); + let hotkey: T::AccountId = account("Alice", 0, seed); + + SubnetOwner::::set(netuid, coldkey.clone()); + + let balance_update = 900_000_000_000; + let limit = TaoCurrency::from(6_000_000_000); + let amount = TaoCurrency::from(44_000_000_000); + Subtensor::::add_balance_to_coldkey_account(&coldkey.clone(), balance_update); + + let tao_reserve = TaoCurrency::from(150_000_000_000); + let alpha_in = AlphaCurrency::from(100_000_000_000); + SubnetTAO::::insert(netuid, tao_reserve); + SubnetAlphaIn::::insert(netuid, alpha_in); + + assert_ok!(Subtensor::::do_burned_registration( + RawOrigin::Signed(coldkey.clone()).into(), + netuid, + hotkey.clone() + )); + + #[extrinsic_call] + _( + RawOrigin::Signed(coldkey.clone()), + hotkey, + netuid, + amount, + Some(limit), + ); + } } diff --git a/pallets/subtensor/src/coinbase/block_emission.rs b/pallets/subtensor/src/coinbase/block_emission.rs index 064bab4d2a..c499fe3117 100644 --- a/pallets/subtensor/src/coinbase/block_emission.rs +++ b/pallets/subtensor/src/coinbase/block_emission.rs @@ -3,7 +3,7 @@ use frame_support::traits::Get; use safe_math::*; use substrate_fixed::{ transcendental::log2, - types::{I96F32, U96F32}, + types::{I96F32, U64F64}, }; use subtensor_runtime_common::{NetUid, TaoCurrency}; use subtensor_swap_interface::SwapHandler; @@ -36,15 +36,15 @@ impl Pallet { alpha_block_emission: u64, ) -> (u64, u64, u64) { // Init terms. - let mut tao_in_emission: U96F32 = U96F32::saturating_from_num(tao_emission); - let float_alpha_block_emission: U96F32 = U96F32::saturating_from_num(alpha_block_emission); + let mut tao_in_emission: U64F64 = U64F64::saturating_from_num(tao_emission); + let float_alpha_block_emission: U64F64 = U64F64::saturating_from_num(alpha_block_emission); // Get alpha price for subnet. let alpha_price = T::SwapInterface::current_alpha_price(netuid.into()); log::debug!("{netuid:?} - alpha_price: {alpha_price:?}"); // Get initial alpha_in - let mut alpha_in_emission: U96F32 = U96F32::saturating_from_num(tao_emission) + let mut alpha_in_emission: U64F64 = U64F64::saturating_from_num(tao_emission) .checked_div(alpha_price) .unwrap_or(float_alpha_block_emission); @@ -62,11 +62,11 @@ impl Pallet { } // Avoid rounding errors. - if tao_in_emission < U96F32::saturating_from_num(1) - || alpha_in_emission < U96F32::saturating_from_num(1) - { - alpha_in_emission = U96F32::saturating_from_num(0); - tao_in_emission = U96F32::saturating_from_num(0); + let zero = U64F64::saturating_from_num(0); + let one = U64F64::saturating_from_num(1); + if tao_in_emission < one || alpha_in_emission < one { + alpha_in_emission = zero; + tao_in_emission = zero; } // Set Alpha in emission. diff --git a/pallets/subtensor/src/coinbase/root.rs b/pallets/subtensor/src/coinbase/root.rs index 83567b6f57..ba7d54ed3c 100644 --- a/pallets/subtensor/src/coinbase/root.rs +++ b/pallets/subtensor/src/coinbase/root.rs @@ -213,7 +213,6 @@ impl Pallet { Self::finalize_all_subnet_root_dividends(netuid); // --- Perform the cleanup before removing the network. - T::SwapInterface::dissolve_all_liquidity_providers(netuid)?; Self::destroy_alpha_in_out_stakes(netuid)?; T::SwapInterface::clear_protocol_liquidity(netuid)?; T::CommitmentsInterface::purge_netuid(netuid); @@ -300,7 +299,6 @@ impl Pallet { SubnetMovingPrice::::remove(netuid); SubnetTaoFlow::::remove(netuid); SubnetEmaTaoFlow::::remove(netuid); - SubnetTaoProvided::::remove(netuid); // --- 13. Token / mechanism / registration toggles. TokenSymbol::::remove(netuid); diff --git a/pallets/subtensor/src/coinbase/run_coinbase.rs b/pallets/subtensor/src/coinbase/run_coinbase.rs index 2091946598..f40d4e704b 100644 --- a/pallets/subtensor/src/coinbase/run_coinbase.rs +++ b/pallets/subtensor/src/coinbase/run_coinbase.rs @@ -67,7 +67,8 @@ impl Pallet { let tao_to_swap_with: TaoCurrency = tou64!(excess_tao.get(netuid_i).unwrap_or(&asfloat!(0))).into(); - T::SwapInterface::adjust_protocol_liquidity(*netuid_i, tao_in_i, alpha_in_i); + let (actual_injected_tao, actual_injected_alpha) = + T::SwapInterface::adjust_protocol_liquidity(*netuid_i, tao_in_i, alpha_in_i); if tao_to_swap_with > TaoCurrency::ZERO { let buy_swap_result = Self::swap_tao_for_alpha( @@ -87,7 +88,8 @@ impl Pallet { AlphaCurrency::from(tou64!(*alpha_in.get(netuid_i).unwrap_or(&asfloat!(0)))); SubnetAlphaInEmission::::insert(*netuid_i, alpha_in_i); SubnetAlphaIn::::mutate(*netuid_i, |total| { - *total = total.saturating_add(alpha_in_i); + // Reserves also received fees in addition to alpha_in_i + *total = total.saturating_add(actual_injected_alpha); }); // Inject TAO in. @@ -95,7 +97,8 @@ impl Pallet { tou64!(*tao_in.get(netuid_i).unwrap_or(&asfloat!(0))).into(); SubnetTaoInEmission::::insert(*netuid_i, injected_tao); SubnetTAO::::mutate(*netuid_i, |total| { - *total = total.saturating_add(injected_tao); + // Reserves also received fees in addition to injected_tao + *total = total.saturating_add(actual_injected_tao); }); TotalStake::::mutate(|total| { *total = total.saturating_add(injected_tao); @@ -140,7 +143,8 @@ impl Pallet { log::debug!("alpha_emission_i: {alpha_emission_i:?}"); // Get subnet price. - let price_i: U96F32 = T::SwapInterface::current_alpha_price(netuid_i.into()); + let price_i: U96F32 = + U96F32::saturating_from_num(T::SwapInterface::current_alpha_price(netuid_i.into())); log::debug!("price_i: {price_i:?}"); let mut tao_in_i: U96F32 = tao_emission_i; diff --git a/pallets/subtensor/src/epoch/run_epoch.rs b/pallets/subtensor/src/epoch/run_epoch.rs index f56b8a89a4..2290b49b8d 100644 --- a/pallets/subtensor/src/epoch/run_epoch.rs +++ b/pallets/subtensor/src/epoch/run_epoch.rs @@ -22,6 +22,7 @@ pub struct EpochTerms { pub validator_trust: u16, pub new_validator_permit: bool, pub bond: Vec<(u16, u16)>, + pub stake: AlphaCurrency, } pub struct EpochOutput(pub BTreeMap); @@ -988,6 +989,10 @@ impl Pallet { .iter() .map(|xi| fixed_proportion_to_u16(*xi)) .collect::>(); + let raw_stake: Vec = total_stake + .iter() + .map(|s| s.saturating_to_num::()) + .collect::>(); for (_hotkey, terms) in terms_map.iter_mut() { terms.dividend = cloned_dividends.get(terms.uid).copied().unwrap_or_default(); @@ -1012,6 +1017,7 @@ impl Pallet { .get(terms.uid) .copied() .unwrap_or_default(); + terms.stake = raw_stake.get(terms.uid).copied().unwrap_or_default().into(); let old_validator_permit = validator_permits .get(terms.uid) .copied() diff --git a/pallets/subtensor/src/extensions/check_coldkey_swap.rs b/pallets/subtensor/src/extensions/check_coldkey_swap.rs new file mode 100644 index 0000000000..db603f9614 --- /dev/null +++ b/pallets/subtensor/src/extensions/check_coldkey_swap.rs @@ -0,0 +1,384 @@ +use crate::{Call, ColdkeySwapAnnouncements, ColdkeySwapDisputes, Config, CustomTransactionError}; +use codec::{Decode, DecodeWithMemTracking, Encode}; +use frame_support::dispatch::{DispatchInfo, PostDispatchInfo}; +use frame_support::traits::IsSubType; +use pallet_subtensor_proxy::Call as ProxyCall; +use scale_info::TypeInfo; +use sp_runtime::{ + impl_tx_ext_default, + traits::{ + AsSystemOriginSigner, DispatchInfoOf, Dispatchable, Implication, StaticLookup, + TransactionExtension, ValidateResult, + }, + transaction_validity::TransactionSource, +}; +use sp_std::marker::PhantomData; +use subtensor_macros::freeze_struct; + +type CallOf = ::RuntimeCall; +type OriginOf = ::RuntimeOrigin; +type LookupOf = ::Lookup; + +#[freeze_struct("483277dc74a5aa56")] +#[derive(Default, Encode, Decode, DecodeWithMemTracking, Clone, Eq, PartialEq, TypeInfo)] +pub struct CheckColdkeySwap(pub PhantomData); + +impl CheckColdkeySwap { + pub fn new() -> Self { + Self(Default::default()) + } +} + +impl sp_std::fmt::Debug for CheckColdkeySwap { + fn fmt(&self, f: &mut sp_std::fmt::Formatter) -> sp_std::fmt::Result { + write!(f, "CheckColdkeySwap") + } +} + +impl + TransactionExtension> for CheckColdkeySwap +where + CallOf: Dispatchable + + IsSubType> + + IsSubType> + + IsSubType>, + OriginOf: AsSystemOriginSigner + Clone, +{ + const IDENTIFIER: &'static str = "CheckColdkeySwap"; + + type Implicit = (); + type Val = (); + type Pre = (); + + fn validate( + &self, + origin: OriginOf, + call: &CallOf, + _info: &DispatchInfoOf>, + _len: usize, + _self_implicit: Self::Implicit, + _inherited_implication: &impl Implication, + _source: TransactionSource, + ) -> ValidateResult> { + // Ensure the transaction is signed, else we just skip the extension. + let Some(who) = origin.as_system_origin_signer() else { + return Ok((Default::default(), (), origin)); + }; + + // Get the real account and origin if we are behind a proxy. + let (who, call) = if let Some( + ProxyCall::proxy { real, call, .. } | ProxyCall::proxy_announced { real, call, .. }, + ) = call.is_sub_type() + { + let real = LookupOf::::lookup(real.clone()) + .map_err(|_| CustomTransactionError::InvalidRealAccount)?; + (real, (*call.clone()).into()) + } else { + (who.clone(), call.clone()) + }; + + if ColdkeySwapAnnouncements::::contains_key(&who) { + if ColdkeySwapDisputes::::contains_key(&who) { + return Err(CustomTransactionError::ColdkeySwapDisputed.into()); + } + + let is_allowed_direct = matches!( + call.is_sub_type(), + Some( + Call::announce_coldkey_swap { .. } + | Call::swap_coldkey_announced { .. } + | Call::dispute_coldkey_swap { .. } + ) + ); + + let is_mev_protected = matches!( + IsSubType::>::is_sub_type(&call), + Some(pallet_shield::Call::submit_encrypted { .. }) + ); + + if !is_allowed_direct && !is_mev_protected { + return Err(CustomTransactionError::ColdkeySwapAnnounced.into()); + } + } + + Ok((Default::default(), (), origin)) + } + + impl_tx_ext_default!(CallOf; weight prepare); +} + +#[cfg(test)] +#[allow(clippy::expect_used, clippy::unwrap_used)] +mod tests { + use super::*; + use crate::{BalancesCall, DefaultMinStake, tests::mock::*}; + use frame_support::testing_prelude::*; + use frame_support::{dispatch::GetDispatchInfo, traits::OriginTrait}; + use frame_system::Call as SystemCall; + use sp_core::U256; + use sp_runtime::{ + BoundedVec, + traits::{AsTransactionAuthorizedOrigin, Hash, TxBaseImplication}, + }; + use subtensor_runtime_common::{Currency, NetUid}; + + type HashingOf = ::Hashing; + + const CALL: RuntimeCall = RuntimeCall::System(SystemCall::remark { remark: vec![] }); + + #[test] + fn skipped_for_non_signed_origins() { + new_test_ext(1).execute_with(|| { + let info = CALL.get_dispatch_info(); + let len = 0_usize; + + let (_, _, origin) = CheckColdkeySwap::::new() + .validate( + None.into(), + &CALL, + &info, + len, + (), + &TxBaseImplication(CALL), + TransactionSource::External, + ) + .unwrap(); + assert!(!origin.is_transaction_authorized()); + + let (_, _, origin) = CheckColdkeySwap::::new() + .validate( + RuntimeOrigin::root().into(), + &CALL, + &info, + len, + (), + &TxBaseImplication(CALL), + TransactionSource::External, + ) + .unwrap(); + assert!(origin.as_system_ref().unwrap().is_root()); + }) + } + + #[test] + fn skipped_if_no_active_swap() { + new_test_ext(1).execute_with(|| { + let who = U256::from(1); + let info = CALL.get_dispatch_info(); + let len = 0_usize; + + let (_, _, origin) = CheckColdkeySwap::::new() + .validate( + RuntimeOrigin::signed(who).into(), + &CALL, + &info, + len, + (), + &TxBaseImplication(CALL), + TransactionSource::External, + ) + .unwrap(); + assert_eq!(origin.as_signer(), Some(&who)); + }) + } + + #[test] + fn validate_calls_correctly() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + let stake = DefaultMinStake::::get().to_u64(); + let who = U256::from(1); + let now = System::block_number(); + let another_coldkey = U256::from(3); + let another_coldkey_hash = HashingOf::::hash_of(&another_coldkey); + let new_coldkey = U256::from(42); + let new_coldkey_hash = HashingOf::::hash_of(&new_coldkey); + ColdkeySwapAnnouncements::::insert(who, (now, new_coldkey_hash)); + + let reserve = stake * 10; + setup_reserves(netuid, reserve.into(), reserve.into()); + + // Setup network and neuron + let hotkey = U256::from(2); + add_network(netuid, 1, 0); + register_ok_neuron(netuid, hotkey, who, 0); + + SubtensorModule::add_balance_to_coldkey_account(&who, u64::MAX); + + let forbidden_calls: Vec = vec![ + RuntimeCall::SubtensorModule(SubtensorCall::dissolve_network { + netuid, + coldkey: who, + }), + RuntimeCall::SubtensorModule(SubtensorCall::add_stake { + hotkey, + netuid, + amount_staked: stake.into(), + }), + RuntimeCall::SubtensorModule(SubtensorCall::add_stake_limit { + hotkey, + netuid, + amount_staked: stake.into(), + limit_price: stake.into(), + allow_partial: false, + }), + RuntimeCall::SubtensorModule(SubtensorCall::swap_stake { + hotkey, + origin_netuid: netuid, + destination_netuid: netuid, + alpha_amount: stake.into(), + }), + RuntimeCall::SubtensorModule(SubtensorCall::swap_stake_limit { + hotkey, + origin_netuid: netuid, + destination_netuid: netuid, + alpha_amount: stake.into(), + limit_price: stake.into(), + allow_partial: false, + }), + RuntimeCall::SubtensorModule(SubtensorCall::move_stake { + origin_hotkey: hotkey, + destination_hotkey: hotkey, + origin_netuid: netuid, + destination_netuid: netuid, + alpha_amount: stake.into(), + }), + RuntimeCall::SubtensorModule(SubtensorCall::transfer_stake { + destination_coldkey: new_coldkey, + hotkey, + origin_netuid: netuid, + destination_netuid: netuid, + alpha_amount: stake.into(), + }), + RuntimeCall::SubtensorModule(SubtensorCall::remove_stake { + hotkey, + netuid, + amount_unstaked: (DefaultMinStake::::get().to_u64() * 2).into(), + }), + RuntimeCall::SubtensorModule(SubtensorCall::remove_stake_limit { + hotkey, + netuid, + amount_unstaked: (stake * 2).into(), + limit_price: 123456789.into(), + allow_partial: true, + }), + RuntimeCall::SubtensorModule(SubtensorCall::burned_register { netuid, hotkey }), + RuntimeCall::Balances(BalancesCall::transfer_all { + dest: new_coldkey, + keep_alive: false, + }), + RuntimeCall::Balances(BalancesCall::transfer_keep_alive { + dest: new_coldkey, + value: 100_000_000_000, + }), + RuntimeCall::Balances(BalancesCall::transfer_allow_death { + dest: new_coldkey, + value: 100_000_000_000, + }), + ]; + + // Forbidden calls through direct origin + for call in &forbidden_calls { + assert_eq!( + ext_validate(who, call.clone()).unwrap_err(), + CustomTransactionError::ColdkeySwapAnnounced.into() + ); + } + + let delegate = U256::from(2); + + // Forbidden calls through proxy + for call in &forbidden_calls { + let proxy_calls = build_proxy_calls(who, delegate, call.clone()); + for proxy_call in proxy_calls { + assert_eq!( + ext_validate(delegate, proxy_call.clone()).unwrap_err(), + CustomTransactionError::ColdkeySwapAnnounced.into() + ); + } + } + + let authorized_calls: Vec = vec![ + RuntimeCall::SubtensorModule(SubtensorCall::announce_coldkey_swap { + new_coldkey_hash: another_coldkey_hash, + }), + RuntimeCall::SubtensorModule(SubtensorCall::swap_coldkey_announced { new_coldkey }), + RuntimeCall::SubtensorModule(SubtensorCall::dispute_coldkey_swap {}), + RuntimeCall::Shield(pallet_shield::Call::submit_encrypted { + commitment: ::Hashing::hash_of(&new_coldkey), + ciphertext: BoundedVec::truncate_from(vec![1, 2, 3, 4]), + }), + ]; + + // Authorized calls through direct origin + for call in &authorized_calls { + let (_, _, origin) = ext_validate(who, call.clone()).unwrap(); + assert_eq!(origin.as_signer(), Some(&who)); + } + + // Authorized calls through proxy + for call in &authorized_calls { + let proxy_calls = build_proxy_calls(who, delegate, call.clone()); + for proxy_call in proxy_calls { + let (_, _, origin) = ext_validate(delegate, proxy_call.clone()).unwrap(); + assert_eq!(origin.as_signer(), Some(&delegate)); + } + } + + ColdkeySwapDisputes::::insert(who, now); + + // All calls should fail when the coldkey swap is disputed + let all_calls = forbidden_calls.iter().chain(authorized_calls.iter()); + + // All calls through direct origin during dispute + for call in all_calls.clone() { + assert_eq!( + ext_validate(who, call.clone()).unwrap_err(), + CustomTransactionError::ColdkeySwapDisputed.into() + ); + } + + // All calls through proxy during dispute + for call in all_calls { + let proxy_calls = build_proxy_calls(who, delegate, call.clone()); + for proxy_call in proxy_calls { + assert_eq!( + ext_validate(delegate, proxy_call.clone()).unwrap_err(), + CustomTransactionError::ColdkeySwapDisputed.into() + ); + } + } + }) + } + + fn build_proxy_calls(who: U256, delegate: U256, call: RuntimeCall) -> Vec { + vec![ + RuntimeCall::Proxy(ProxyCall::proxy { + real: who, + force_proxy_type: None, + call: Box::new(call.clone()), + }), + RuntimeCall::Proxy(ProxyCall::proxy_announced { + delegate, + real: who, + force_proxy_type: None, + call: Box::new(call.clone()), + }), + ] + } + + fn ext_validate(who: U256, call: RuntimeCall) -> ValidateResult<(), RuntimeCall> { + let info = call.get_dispatch_info(); + let len = 0_usize; + + CheckColdkeySwap::::new().validate( + RuntimeOrigin::signed(who).into(), + &call.clone(), + &info, + len, + (), + &TxBaseImplication(call), + TransactionSource::External, + ) + } +} diff --git a/pallets/subtensor/src/extensions/mod.rs b/pallets/subtensor/src/extensions/mod.rs new file mode 100644 index 0000000000..d77987c583 --- /dev/null +++ b/pallets/subtensor/src/extensions/mod.rs @@ -0,0 +1,5 @@ +mod check_coldkey_swap; +mod subtensor; + +pub use check_coldkey_swap::*; +pub use subtensor::*; diff --git a/pallets/subtensor/src/transaction_extension.rs b/pallets/subtensor/src/extensions/subtensor.rs similarity index 70% rename from pallets/subtensor/src/transaction_extension.rs rename to pallets/subtensor/src/extensions/subtensor.rs index cf1d410ea9..a24a54f3fd 100644 --- a/pallets/subtensor/src/transaction_extension.rs +++ b/pallets/subtensor/src/extensions/subtensor.rs @@ -1,24 +1,30 @@ use crate::{ - BalancesCall, Call, ColdkeySwapScheduled, Config, CustomTransactionError, Error, Pallet, + BalancesCall, Call, CheckColdkeySwap, Config, CustomTransactionError, Error, Pallet, TransactionType, }; use codec::{Decode, DecodeWithMemTracking, Encode}; use frame_support::dispatch::{DispatchInfo, PostDispatchInfo}; -use frame_support::pallet_prelude::Weight; use frame_support::traits::IsSubType; use scale_info::TypeInfo; use sp_runtime::traits::{ AsSystemOriginSigner, DispatchInfoOf, Dispatchable, Implication, TransactionExtension, ValidateResult, }; -use sp_runtime::transaction_validity::{ - TransactionSource, TransactionValidity, TransactionValidityError, ValidTransaction, +use sp_runtime::transaction_validity::{InvalidTransaction, TransactionValidityError}; +use sp_runtime::{ + impl_tx_ext_default, + transaction_validity::{TransactionSource, TransactionValidity, ValidTransaction}, }; use sp_std::marker::PhantomData; use sp_std::vec::Vec; use subtensor_macros::freeze_struct; use subtensor_runtime_common::{NetUid, NetUidStorageIndex}; +const ADD_STAKE_BURN_PRIORITY_BOOST: u64 = 100; + +type CallOf = ::RuntimeCall; +type OriginOf = ::RuntimeOrigin; + #[freeze_struct("2e02eb32e5cb25d3")] #[derive(Default, Encode, Decode, DecodeWithMemTracking, Clone, Eq, PartialEq, TypeInfo)] pub struct SubtensorTransactionExtension(pub PhantomData); @@ -31,9 +37,7 @@ impl sp_std::fmt::Debug for SubtensorTransac impl SubtensorTransactionExtension where - ::RuntimeCall: - Dispatchable, - ::RuntimeCall: IsSubType>, + CallOf: Dispatchable + IsSubType>, { pub fn new() -> Self { Self(Default::default()) @@ -52,30 +56,29 @@ where pub fn result_to_validity(result: Result<(), Error>, priority: u64) -> TransactionValidity { if let Err(err) = result { Err(match err { - Error::::AmountTooLow => CustomTransactionError::StakeAmountTooLow.into(), - Error::::SubnetNotExists => CustomTransactionError::SubnetNotExists.into(), - Error::::NotEnoughBalanceToStake => CustomTransactionError::BalanceTooLow.into(), + Error::::AmountTooLow => CustomTransactionError::StakeAmountTooLow, + Error::::SubnetNotExists => CustomTransactionError::SubnetNotExists, + Error::::NotEnoughBalanceToStake => CustomTransactionError::BalanceTooLow, Error::::HotKeyAccountNotExists => { - CustomTransactionError::HotkeyAccountDoesntExist.into() + CustomTransactionError::HotkeyAccountDoesntExist } Error::::NotEnoughStakeToWithdraw => { - CustomTransactionError::NotEnoughStakeToWithdraw.into() - } - Error::::InsufficientLiquidity => { - CustomTransactionError::InsufficientLiquidity.into() + CustomTransactionError::NotEnoughStakeToWithdraw } - Error::::SlippageTooHigh => CustomTransactionError::SlippageTooHigh.into(), - Error::::TransferDisallowed => CustomTransactionError::TransferDisallowed.into(), + Error::::InsufficientLiquidity => CustomTransactionError::InsufficientLiquidity, + Error::::SlippageTooHigh => CustomTransactionError::SlippageTooHigh, + Error::::TransferDisallowed => CustomTransactionError::TransferDisallowed, Error::::HotKeyNotRegisteredInNetwork => { - CustomTransactionError::HotKeyNotRegisteredInNetwork.into() + CustomTransactionError::HotKeyNotRegisteredInNetwork } - Error::::InvalidIpAddress => CustomTransactionError::InvalidIpAddress.into(), + Error::::InvalidIpAddress => CustomTransactionError::InvalidIpAddress, Error::::ServingRateLimitExceeded => { - CustomTransactionError::ServingRateLimitExceeded.into() + CustomTransactionError::ServingRateLimitExceeded } - Error::::InvalidPort => CustomTransactionError::InvalidPort.into(), - _ => CustomTransactionError::BadRequest.into(), - }) + Error::::InvalidPort => CustomTransactionError::InvalidPort, + _ => CustomTransactionError::BadRequest, + } + .into()) } else { Ok(ValidTransaction { priority, @@ -85,56 +88,58 @@ where } } -impl - TransactionExtension<::RuntimeCall> - for SubtensorTransactionExtension +impl TransactionExtension> for SubtensorTransactionExtension where - ::RuntimeCall: - Dispatchable, - ::RuntimeOrigin: AsSystemOriginSigner + Clone, - ::RuntimeCall: IsSubType>, - ::RuntimeCall: IsSubType>, + T: Config + + Send + + Sync + + TypeInfo + + pallet_balances::Config + + pallet_subtensor_proxy::Config + + pallet_shield::Config, + CallOf: Dispatchable + + IsSubType> + + IsSubType> + + IsSubType> + + IsSubType>, + OriginOf: AsSystemOriginSigner + Clone, { const IDENTIFIER: &'static str = "SubtensorTransactionExtension"; type Implicit = (); - type Val = Option; + type Val = (); type Pre = (); - fn weight(&self, _call: &::RuntimeCall) -> Weight { - // TODO: benchmark transaction extension - Weight::zero() - } - fn validate( &self, - origin: ::RuntimeOrigin, - call: &::RuntimeCall, - _info: &DispatchInfoOf<::RuntimeCall>, + origin: OriginOf, + call: &CallOf, + _info: &DispatchInfoOf>, _len: usize, _self_implicit: Self::Implicit, _inherited_implication: &impl Implication, _source: TransactionSource, - ) -> ValidateResult::RuntimeCall> { + ) -> ValidateResult> { // Ensure the transaction is signed, else we just skip the extension. let Some(who) = origin.as_system_origin_signer() else { - return Ok((Default::default(), None, origin)); + return Ok((Default::default(), (), origin)); }; - // Verify ColdkeySwapScheduled map for coldkey - match call.is_sub_type() { - // Whitelist - Some(Call::schedule_swap_coldkey { .. }) => {} - _ => { - if ColdkeySwapScheduled::::contains_key(who) { - return Err(CustomTransactionError::ColdkeyInSwapSchedule.into()); - } - } - } + // TODO: move into tx extension pipeline but require node upgrade + CheckColdkeySwap::::new().validate( + origin.clone(), + call, + _info, + _len, + _self_implicit, + _inherited_implication, + _source, + )?; + match call.is_sub_type() { Some(Call::commit_weights { netuid, .. }) => { if Self::check_weights_min_stake(who, *netuid) { - Ok((Default::default(), Some(who.clone()), origin)) + Ok((Default::default(), (), origin)) } else { Err(CustomTransactionError::StakeAmountTooLow.into()) } @@ -158,7 +163,7 @@ where match Pallet::::find_commit_block_via_hash(provided_hash) { Some(commit_block) => { if Pallet::::is_reveal_block_range(*netuid, commit_block) { - Ok((Default::default(), Some(who.clone()), origin)) + Ok((Default::default(), (), origin)) } else { Err(CustomTransactionError::CommitBlockNotInRevealRange.into()) } @@ -203,7 +208,7 @@ where if provided_hashes.len() == batch_reveal_block.len() { if Pallet::::is_batch_reveal_block_range(*netuid, batch_reveal_block) { - Ok((Default::default(), Some(who.clone()), origin)) + Ok((Default::default(), (), origin)) } else { Err(CustomTransactionError::CommitBlockNotInRevealRange.into()) } @@ -219,7 +224,7 @@ where } Some(Call::set_weights { netuid, .. }) => { if Self::check_weights_min_stake(who, *netuid) { - Ok((Default::default(), Some(who.clone()), origin)) + Ok((Default::default(), (), origin)) } else { Err(CustomTransactionError::StakeAmountTooLow.into()) } @@ -233,7 +238,7 @@ where if *reveal_round < pallet_drand::LastStoredRound::::get() { return Err(CustomTransactionError::InvalidRevealRound.into()); } - Ok((Default::default(), Some(who.clone()), origin)) + Ok((Default::default(), (), origin)) } else { Err(CustomTransactionError::StakeAmountTooLow.into()) } @@ -249,7 +254,7 @@ where return Err(CustomTransactionError::RateLimitExceeded.into()); } - Ok((Default::default(), Some(who.clone()), origin)) + Ok((Default::default(), (), origin)) } Some(Call::serve_axon { netuid, @@ -276,41 +281,32 @@ where ), 0u64, ) - .map(|validity| (validity, Some(who.clone()), origin.clone())) + .map(|validity| (validity, (), origin.clone())) } Some(Call::register_network { .. }) => { if !TransactionType::RegisterNetwork.passes_rate_limit::(who) { return Err(CustomTransactionError::RateLimitExceeded.into()); } - Ok((Default::default(), Some(who.clone()), origin)) + Ok((Default::default(), (), origin)) } Some(Call::associate_evm_key { netuid, .. }) => { - match Pallet::::get_uid_for_net_and_hotkey(*netuid, who) { - Ok(uid) => { - match Pallet::::ensure_evm_key_associate_rate_limit(*netuid, uid) { - Ok(_) => Ok((Default::default(), Some(who.clone()), origin)), - Err(_) => { - Err(CustomTransactionError::EvmKeyAssociateRateLimitExceeded.into()) - } - } - } - Err(_) => Err(CustomTransactionError::UidNotFound.into()), - } + let uid = Pallet::::get_uid_for_net_and_hotkey(*netuid, who) + .map_err(|_| CustomTransactionError::UidNotFound)?; + Pallet::::ensure_evm_key_associate_rate_limit(*netuid, uid) + .map_err(|_| CustomTransactionError::EvmKeyAssociateRateLimitExceeded)?; + Ok((Default::default(), (), origin)) + } + Some(Call::add_stake_burn { netuid, .. }) => { + Pallet::::ensure_subnet_owner(origin.clone(), *netuid).map_err(|_| { + TransactionValidityError::Invalid(InvalidTransaction::BadSigner) + })?; + + Ok((Self::validity_ok(ADD_STAKE_BURN_PRIORITY_BOOST), (), origin)) } - _ => Ok((Default::default(), Some(who.clone()), origin)), + _ => Ok((Default::default(), (), origin)), } } - // NOTE: Add later when we put in a pre and post dispatch step. - fn prepare( - self, - _val: Self::Val, - _origin: &::RuntimeOrigin, - _call: &::RuntimeCall, - _info: &DispatchInfoOf<::RuntimeCall>, - _len: usize, - ) -> Result { - Ok(()) - } + impl_tx_ext_default!(CallOf; weight prepare); } diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index 6ae43ac384..c137b92371 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -1,6 +1,7 @@ #![cfg_attr(not(feature = "std"), no_std)] #![recursion_limit = "512"] #![allow(clippy::too_many_arguments)] +#![allow(clippy::zero_prefixed_literal)] // Edit this file to define custom logic or remove it if it is not needed. // Learn more about FRAME and the core library of Substrate FRAME pallets: // @@ -35,6 +36,7 @@ mod benchmarks; // ========================= pub mod coinbase; pub mod epoch; +pub mod extensions; pub mod macros; pub mod migrations; pub mod rpc_info; @@ -45,9 +47,10 @@ pub mod utils; use crate::utils::rate_limiting::{Hyperparameter, TransactionType}; use macros::{config, dispatches, errors, events, genesis, hooks}; +pub use extensions::*; + #[cfg(test)] mod tests; -pub mod transaction_extension; // apparently this is stabilized since rust 1.36 extern crate alloc; @@ -944,16 +947,16 @@ pub mod pallet { (45875, 58982) } - /// Default value for coldkey swap schedule duration + /// Default value for coldkey swap announcement delay. #[pallet::type_value] - pub fn DefaultColdkeySwapScheduleDuration() -> BlockNumberFor { - T::InitialColdkeySwapScheduleDuration::get() + pub fn DefaultColdkeySwapAnnouncementDelay() -> BlockNumberFor { + T::InitialColdkeySwapAnnouncementDelay::get() } - /// Default value for coldkey swap reschedule duration + /// Default value for coldkey swap reannouncement delay. #[pallet::type_value] - pub fn DefaultColdkeySwapRescheduleDuration() -> BlockNumberFor { - T::InitialColdkeySwapRescheduleDuration::get() + pub fn DefaultColdkeySwapReannouncementDelay() -> BlockNumberFor { + T::InitialColdkeySwapReannouncementDelay::get() } /// Default value for applying pending items (e.g. childkeys). @@ -1018,15 +1021,6 @@ pub mod pallet { 360 } - /// Default value for coldkey swap scheduled - #[pallet::type_value] - pub fn DefaultColdkeySwapScheduled() -> (BlockNumberFor, T::AccountId) { - #[allow(clippy::expect_used)] - let default_account = T::AccountId::decode(&mut TrailingZeroInput::zeroes()) - .expect("trailing zeroes always produce a valid account ID; qed"); - (BlockNumberFor::::from(0_u32), default_account) - } - /// Default value for setting subnet owner hotkey rate limit #[pallet::type_value] pub fn DefaultSetSNOwnerHotkeyRateLimit() -> u64 { @@ -1083,16 +1077,6 @@ pub mod pallet { pub type OwnerHyperparamRateLimit = StorageValue<_, u16, ValueQuery, DefaultOwnerHyperparamRateLimit>; - /// Duration of coldkey swap schedule before execution - #[pallet::storage] - pub type ColdkeySwapScheduleDuration = - StorageValue<_, BlockNumberFor, ValueQuery, DefaultColdkeySwapScheduleDuration>; - - /// Duration of coldkey swap reschedule before execution - #[pallet::storage] - pub type ColdkeySwapRescheduleDuration = - StorageValue<_, BlockNumberFor, ValueQuery, DefaultColdkeySwapRescheduleDuration>; - /// Duration of dissolve network schedule before execution #[pallet::storage] pub type DissolveNetworkScheduleDuration = @@ -1305,11 +1289,6 @@ pub mod pallet { pub type SubnetTAO = StorageMap<_, Identity, NetUid, TaoCurrency, ValueQuery, DefaultZeroTao>; - /// --- MAP ( netuid ) --> tao_in_user_subnet | Returns the amount of TAO in the subnet reserve provided by users as liquidity. - #[pallet::storage] - pub type SubnetTaoProvided = - StorageMap<_, Identity, NetUid, TaoCurrency, ValueQuery, DefaultZeroTao>; - /// --- MAP ( netuid ) --> alpha_in_emission | Returns the amount of alph in emission into the pool per block. #[pallet::storage] pub type SubnetAlphaInEmission = @@ -1330,11 +1309,6 @@ pub mod pallet { pub type SubnetAlphaIn = StorageMap<_, Identity, NetUid, AlphaCurrency, ValueQuery, DefaultZeroAlpha>; - /// --- MAP ( netuid ) --> alpha_supply_user_in_pool | Returns the amount of alpha in the pool provided by users as liquidity. - #[pallet::storage] - pub type SubnetAlphaInProvided = - StorageMap<_, Identity, NetUid, AlphaCurrency, ValueQuery, DefaultZeroAlpha>; - /// --- MAP ( netuid ) --> alpha_supply_in_subnet | Returns the amount of alpha in the subnet. #[pallet::storage] pub type SubnetAlphaOut = @@ -1374,16 +1348,27 @@ pub mod pallet { ValueQuery, >; - /// --- DMAP ( cold ) --> (block_expected, new_coldkey), Maps coldkey to the block to swap at and new coldkey. + /// The delay after an announcement before a coldkey swap can be performed. #[pallet::storage] - pub type ColdkeySwapScheduled = StorageMap< - _, - Blake2_128Concat, - T::AccountId, - (BlockNumberFor, T::AccountId), - ValueQuery, - DefaultColdkeySwapScheduled, - >; + pub type ColdkeySwapAnnouncementDelay = + StorageValue<_, BlockNumberFor, ValueQuery, DefaultColdkeySwapAnnouncementDelay>; + + /// The delay after the initial delay has passed before a new announcement can be made. + #[pallet::storage] + pub type ColdkeySwapReannouncementDelay = + StorageValue<_, BlockNumberFor, ValueQuery, DefaultColdkeySwapReannouncementDelay>; + + /// A map of the coldkey swap announcements from a coldkey + /// to the block number the coldkey swap can be performed. + #[pallet::storage] + pub type ColdkeySwapAnnouncements = + StorageMap<_, Twox64Concat, T::AccountId, (BlockNumberFor, T::Hash), OptionQuery>; + + /// A map of the coldkey swap disputes from a coldkey to the + /// block number the coldkey swap was disputed. + #[pallet::storage] + pub type ColdkeySwapDisputes = + StorageMap<_, Twox64Concat, T::AccountId, BlockNumberFor, OptionQuery>; /// --- DMAP ( hot, netuid ) --> alpha | Returns the total amount of alpha a hotkey owns. #[pallet::storage] @@ -1894,8 +1879,52 @@ pub mod pallet { pub type SubtokenEnabled = StorageMap<_, Identity, NetUid, bool, ValueQuery, DefaultFalse>; - /// Default value for burn keys limit + // ======================================= + // ==== VotingPower Storage ==== + // ======================================= + #[pallet::type_value] + /// Default VotingPower EMA alpha value (0.1 represented as u64 with 18 decimals) + /// alpha = 0.1 means slow response, 10% weight to new values per epoch + pub fn DefaultVotingPowerEmaAlpha() -> u64 { + 0_003_570_000_000_000_000 // 0.00357 * 10^18 = 2 weeks e-folding (time-constant) @ 361 + // blocks per tempo + // After 2 weeks -> EMA reaches 63.2% of a step change + // After ~4 weeks -> 86.5% + // After ~6 weeks -> 95% + } + + #[pallet::storage] + /// --- DMAP ( netuid, hotkey ) --> voting_power | EMA of stake for voting + /// This tracks stake EMA updated every epoch when VotingPowerTrackingEnabled is true. + /// Used by smart contracts to determine validator voting power for subnet governance. + pub type VotingPower = + StorageDoubleMap<_, Identity, NetUid, Blake2_128Concat, T::AccountId, u64, ValueQuery>; + + #[pallet::storage] + /// --- MAP ( netuid ) --> bool | Whether voting power tracking is enabled for this subnet. + /// When enabled, VotingPower EMA is updated every epoch. Default is false. + /// When disabled with disable_at_block set, tracking continues until that block. + pub type VotingPowerTrackingEnabled = + StorageMap<_, Identity, NetUid, bool, ValueQuery, DefaultFalse>; + + #[pallet::storage] + /// --- MAP ( netuid ) --> block_number | Block at which voting power tracking will be disabled. + /// When set (non-zero), tracking continues until this block, then automatically disables + /// and clears VotingPower entries for the subnet. Provides a 14-day grace period. + pub type VotingPowerDisableAtBlock = + StorageMap<_, Identity, NetUid, u64, ValueQuery>; + + #[pallet::storage] + /// --- MAP ( netuid ) --> u64 | EMA alpha value for voting power calculation. + /// Higher alpha = faster response to stake changes. + /// Stored as u64 with 18 decimal precision (1.0 = 10^18). + /// Only settable by sudo/root. + pub type VotingPowerEmaAlpha = + StorageMap<_, Identity, NetUid, u64, ValueQuery, DefaultVotingPowerEmaAlpha>; + + #[pallet::type_value] + /// Default value for burn keys limit pub fn DefaultImmuneOwnerUidsLimit() -> u16 { 1 } @@ -2328,12 +2357,17 @@ pub mod pallet { MechId::from(1) } - /// -- ITEM (Maximum number of sub-subnets) + /// -- ITEM (Maximum number of mechanisms) #[pallet::type_value] - pub fn MaxMechanismCount() -> MechId { + pub fn DefaultMaxMechanismCount() -> MechId { MechId::from(2) } + /// ITEM( max_mechanism_count ) + #[pallet::storage] + pub type MaxMechanismCount = + StorageValue<_, MechId, ValueQuery, DefaultMaxMechanismCount>; + /// -- ITEM (Rate limit for mechanism count updates) #[pallet::type_value] pub fn MechanismCountSetRateLimit() -> u64 { @@ -2441,7 +2475,7 @@ pub mod pallet { #[derive(Debug, PartialEq)] pub enum CustomTransactionError { - ColdkeyInSwapSchedule, + ColdkeySwapAnnounced, StakeAmountTooLow, BalanceTooLow, SubnetNotExists, @@ -2463,12 +2497,14 @@ pub enum CustomTransactionError { InputLengthsUnequal, UidNotFound, EvmKeyAssociateRateLimitExceeded, + ColdkeySwapDisputed, + InvalidRealAccount, } impl From for u8 { fn from(variant: CustomTransactionError) -> u8 { match variant { - CustomTransactionError::ColdkeyInSwapSchedule => 0, + CustomTransactionError::ColdkeySwapAnnounced => 0, CustomTransactionError::StakeAmountTooLow => 1, CustomTransactionError::BalanceTooLow => 2, CustomTransactionError::SubnetNotExists => 3, @@ -2490,6 +2526,8 @@ impl From for u8 { CustomTransactionError::InputLengthsUnequal => 18, CustomTransactionError::UidNotFound => 19, CustomTransactionError::EvmKeyAssociateRateLimitExceeded => 20, + CustomTransactionError::ColdkeySwapDisputed => 21, + CustomTransactionError::InvalidRealAccount => 22, } } } @@ -2514,7 +2552,7 @@ pub struct TaoCurrencyReserve(PhantomData); impl CurrencyReserve for TaoCurrencyReserve { #![deny(clippy::expect_used)] fn reserve(netuid: NetUid) -> TaoCurrency { - SubnetTAO::::get(netuid).saturating_add(SubnetTaoProvided::::get(netuid)) + SubnetTAO::::get(netuid) } fn increase_provided(netuid: NetUid, tao: TaoCurrency) { @@ -2532,7 +2570,7 @@ pub struct AlphaCurrencyReserve(PhantomData); impl CurrencyReserve for AlphaCurrencyReserve { #![deny(clippy::expect_used)] fn reserve(netuid: NetUid) -> AlphaCurrency { - SubnetAlphaIn::::get(netuid).saturating_add(SubnetAlphaInProvided::::get(netuid)) + SubnetAlphaIn::::get(netuid) } fn increase_provided(netuid: NetUid, alpha: AlphaCurrency) { @@ -2674,6 +2712,9 @@ pub enum RateLimitKey { // Last tx block delegate key limit per account ID #[codec(index = 5)] LastTxBlockDelegateTake(AccountId), + // "Add stake and burn" rate limit + #[codec(index = 6)] + AddStakeBurn(NetUid), } pub trait ProxyInterface { diff --git a/pallets/subtensor/src/macros/config.rs b/pallets/subtensor/src/macros/config.rs index 2124ec5f3f..528e5c9fd5 100644 --- a/pallets/subtensor/src/macros/config.rs +++ b/pallets/subtensor/src/macros/config.rs @@ -8,6 +8,7 @@ mod config { use crate::{CommitmentsInterface, GetAlphaForTao, GetTaoForAlpha}; use pallet_commitments::GetCommitments; + use subtensor_runtime_common::AuthorshipInfo; use subtensor_swap_interface::{SwapEngine, SwapHandler}; /// Configure the pallet by specifying the parameters and types on which it depends. @@ -59,6 +60,9 @@ mod config { /// Rate limit for associating an EVM key. type EvmKeyAssociateRateLimit: Get; + /// Provider of current block author + type AuthorshipProvider: AuthorshipInfo; + /// ================================= /// ==== Initial Value Constants ==== /// ================================= @@ -214,16 +218,14 @@ mod config { #[pallet::constant] type LiquidAlphaOn: Get; /// A flag to indicate if Yuma3 is enabled. + #[pallet::constant] type Yuma3On: Get; - // /// Initial hotkey emission tempo. - // #[pallet::constant] - // type InitialHotkeyEmissionTempo: Get; - /// Coldkey swap schedule duartion. + /// Coldkey swap announcement delay. #[pallet::constant] - type InitialColdkeySwapScheduleDuration: Get>; - /// Coldkey swap reschedule duration. + type InitialColdkeySwapAnnouncementDelay: Get>; + /// Coldkey swap reannouncement delay. #[pallet::constant] - type InitialColdkeySwapRescheduleDuration: Get>; + type InitialColdkeySwapReannouncementDelay: Get>; /// Dissolve network schedule duration #[pallet::constant] type InitialDissolveNetworkScheduleDuration: Get>; diff --git a/pallets/subtensor/src/macros/dispatches.rs b/pallets/subtensor/src/macros/dispatches.rs index 5c5d5ed1a7..6330e07d1c 100644 --- a/pallets/subtensor/src/macros/dispatches.rs +++ b/pallets/subtensor/src/macros/dispatches.rs @@ -6,11 +6,10 @@ use frame_support::pallet_macros::pallet_section; #[pallet_section] mod dispatches { use crate::subnets::leasing::SubnetLeasingWeightInfo; - use frame_support::traits::schedule::DispatchTime; use frame_support::traits::schedule::v3::Anon as ScheduleAnon; use frame_system::pallet_prelude::BlockNumberFor; use sp_core::ecdsa::Signature; - use sp_runtime::{Percent, traits::Saturating}; + use sp_runtime::{Percent, Saturating, traits::Hash}; use crate::MAX_CRV3_COMMIT_SIZE_BYTES; use crate::MAX_NUM_ROOT_CLAIMS; @@ -711,16 +710,16 @@ mod dispatches { /// - Errors stemming from transaction pallet. /// #[pallet::call_index(2)] - #[pallet::weight((Weight::from_parts(340_800_000, 0) - .saturating_add(T::DbWeight::get().reads(25_u64)) - .saturating_add(T::DbWeight::get().writes(16_u64)), DispatchClass::Normal, Pays::Yes))] + #[pallet::weight((Weight::from_parts(523_200_000, 0) + .saturating_add(T::DbWeight::get().reads(20_u64)) + .saturating_add(T::DbWeight::get().writes(14_u64)), DispatchClass::Normal, Pays::Yes))] pub fn add_stake( origin: OriginFor, hotkey: T::AccountId, netuid: NetUid, amount_staked: TaoCurrency, ) -> DispatchResult { - Self::do_add_stake(origin, hotkey, netuid, amount_staked) + Self::do_add_stake(origin, hotkey, netuid, amount_staked).map(|_| ()) } /// Remove stake from the staking account. The call must be made @@ -1041,9 +1040,9 @@ mod dispatches { /// User register a new subnetwork via burning token #[pallet::call_index(7)] - #[pallet::weight((Weight::from_parts(354_200_000, 0) - .saturating_add(T::DbWeight::get().reads(47_u64)) - .saturating_add(T::DbWeight::get().writes(40_u64)), DispatchClass::Normal, Pays::Yes))] + #[pallet::weight((Weight::from_parts(315_200_000, 0) + .saturating_add(T::DbWeight::get().reads(34_u64)) + .saturating_add(T::DbWeight::get().writes(29_u64)), DispatchClass::Normal, Pays::Yes))] pub fn burned_register( origin: OriginFor, netuid: NetUid, @@ -1055,7 +1054,7 @@ mod dispatches { /// The extrinsic for user to change its hotkey in subnet or all subnets. #[pallet::call_index(70)] #[pallet::weight((Weight::from_parts(275_300_000, 0) - .saturating_add(T::DbWeight::get().reads(50_u64)) + .saturating_add(T::DbWeight::get().reads(52_u64)) .saturating_add(T::DbWeight::get().writes(35_u64)), DispatchClass::Normal, Pays::No))] pub fn swap_hotkey( origin: OriginFor, @@ -1066,21 +1065,9 @@ mod dispatches { Self::do_swap_hotkey(origin, &hotkey, &new_hotkey, netuid) } - /// The extrinsic for user to change the coldkey associated with their account. - /// - /// # Arguments - /// - /// * `origin` - The origin of the call, must be signed by the old coldkey. - /// * `old_coldkey` - The current coldkey associated with the account. - /// * `new_coldkey` - The new coldkey to be associated with the account. - /// - /// # Returns + /// Performs an arbitrary coldkey swap for any coldkey. /// - /// Returns a `DispatchResultWithPostInfo` indicating success or failure of the operation. - /// - /// # Weight - /// - /// Weight is calculated based on the number of database reads and writes. + /// Only callable by root as it doesn't require an announcement and can be used to swap any coldkey. #[pallet::call_index(71)] #[pallet::weight(Weight::from_parts(161_700_000, 0) .saturating_add(T::DbWeight::get().reads(16_u64)) @@ -1090,12 +1077,19 @@ mod dispatches { old_coldkey: T::AccountId, new_coldkey: T::AccountId, swap_cost: TaoCurrency, - ) -> DispatchResultWithPostInfo { - // Ensure it's called with root privileges (scheduler has root privileges) + ) -> DispatchResult { ensure_root(origin)?; - log::debug!("swap_coldkey: {:?} -> {:?}", old_coldkey, new_coldkey); - Self::do_swap_coldkey(&old_coldkey, &new_coldkey, swap_cost) + if swap_cost.to_u64() > 0 { + Self::charge_swap_cost(&old_coldkey, swap_cost)?; + } + Self::do_swap_coldkey(&old_coldkey, &new_coldkey)?; + + // We also clear any announcement or dispute for security reasons + ColdkeySwapAnnouncements::::remove(&old_coldkey); + ColdkeySwapDisputes::::remove(old_coldkey); + + Ok(()) } /// Sets the childkey take for a given hotkey. @@ -1217,9 +1211,9 @@ mod dispatches { /// User register a new subnetwork #[pallet::call_index(59)] - #[pallet::weight((Weight::from_parts(235_400_000, 0) + #[pallet::weight((Weight::from_parts(238_500_000, 0) .saturating_add(T::DbWeight::get().reads(36_u64)) - .saturating_add(T::DbWeight::get().writes(52_u64)), DispatchClass::Normal, Pays::Yes))] + .saturating_add(T::DbWeight::get().writes(50_u64)), DispatchClass::Normal, Pays::Yes))] pub fn register_network(origin: OriginFor, hotkey: T::AccountId) -> DispatchResult { Self::do_register_network(origin, &hotkey, 1, None) } @@ -1321,94 +1315,15 @@ mod dispatches { /// Schedules a coldkey swap operation to be executed at a future block. /// - /// This function allows a user to schedule the swapping of their coldkey to a new one - /// at a specified future block. The swap is not executed immediately but is scheduled - /// to occur at the specified block number. - /// - /// # Arguments - /// - /// * `origin` - The origin of the call, which should be signed by the current coldkey owner. - /// * `new_coldkey` - The account ID of the new coldkey that will replace the current one. - /// * `when` - The block number at which the coldkey swap should be executed. - /// - /// # Returns - /// - /// Returns a `DispatchResultWithPostInfo` indicating whether the scheduling was successful. - /// - /// # Errors - /// - /// This function may return an error if: - /// * The origin is not signed. - /// * The scheduling fails due to conflicts or system constraints. - /// - /// # Notes - /// - /// - The actual swap is not performed by this function. It merely schedules the swap operation. - /// - The weight of this call is set to a fixed value and may need adjustment based on benchmarking. - /// - /// # TODO - /// - /// - Implement proper weight calculation based on the complexity of the operation. - /// - Consider adding checks to prevent scheduling too far into the future. - /// TODO: Benchmark this call + /// WARNING: This function is deprecated, please migrate to `announce_coldkey_swap`/`coldkey_swap` #[pallet::call_index(73)] - #[pallet::weight((Weight::from_parts(37_830_000, 0) - .saturating_add(T::DbWeight::get().reads(4)) - .saturating_add(T::DbWeight::get().writes(2)), DispatchClass::Normal, Pays::Yes))] + #[pallet::weight(T::DbWeight::get().reads(5))] + #[deprecated(note = "Deprecated, please migrate to `announce_coldkey_swap`/`coldkey_swap`")] pub fn schedule_swap_coldkey( - origin: OriginFor, - new_coldkey: T::AccountId, - ) -> DispatchResultWithPostInfo { - let who = ensure_signed(origin)?; - let current_block = >::block_number(); - - // If the coldkey has a scheduled swap, check if we can reschedule it - if ColdkeySwapScheduled::::contains_key(&who) { - let (scheduled_block, _scheduled_coldkey) = ColdkeySwapScheduled::::get(&who); - let reschedule_duration = ColdkeySwapRescheduleDuration::::get(); - let redo_when = scheduled_block.saturating_add(reschedule_duration); - ensure!(redo_when <= current_block, Error::::SwapAlreadyScheduled); - } - - // Calculate the swap cost and ensure sufficient balance - let swap_cost = Self::get_key_swap_cost(); - ensure!( - Self::can_remove_balance_from_coldkey_account(&who, swap_cost.into()), - Error::::NotEnoughBalanceToPaySwapColdKey - ); - - let current_block: BlockNumberFor = >::block_number(); - let duration: BlockNumberFor = ColdkeySwapScheduleDuration::::get(); - let when: BlockNumberFor = current_block.saturating_add(duration); - - let call = Call::::swap_coldkey { - old_coldkey: who.clone(), - new_coldkey: new_coldkey.clone(), - swap_cost, - }; - - let bound_call = ::Preimages::bound(LocalCallOf::::from(call.clone())) - .map_err(|_| Error::::FailedToSchedule)?; - - T::Scheduler::schedule( - DispatchTime::At(when), - None, - 63, - frame_system::RawOrigin::Root.into(), - bound_call, - ) - .map_err(|_| Error::::FailedToSchedule)?; - - ColdkeySwapScheduled::::insert(&who, (when, new_coldkey.clone())); - // Emit the SwapScheduled event - Self::deposit_event(Event::ColdkeySwapScheduled { - old_coldkey: who.clone(), - new_coldkey: new_coldkey.clone(), - execution_block: when, - swap_cost, - }); - - Ok(().into()) + _origin: OriginFor, + _new_coldkey: T::AccountId, + ) -> DispatchResult { + Err(Error::::Deprecated.into()) } /// ---- Set prometheus information for the neuron. @@ -1505,9 +1420,9 @@ mod dispatches { /// User register a new subnetwork #[pallet::call_index(79)] - #[pallet::weight((Weight::from_parts(234_200_000, 0) + #[pallet::weight((Weight::from_parts(235_700_000, 0) .saturating_add(T::DbWeight::get().reads(35_u64)) - .saturating_add(T::DbWeight::get().writes(51_u64)), DispatchClass::Normal, Pays::Yes))] + .saturating_add(T::DbWeight::get().writes(49_u64)), DispatchClass::Normal, Pays::Yes))] pub fn register_network_with_identity( origin: OriginFor, hotkey: T::AccountId, @@ -1575,9 +1490,9 @@ mod dispatches { /// * `TxRateLimitExceeded`: /// - Thrown if key has hit transaction rate limit #[pallet::call_index(84)] - #[pallet::weight((Weight::from_parts(358_500_000, 0) - .saturating_add(T::DbWeight::get().reads(41_u64)) - .saturating_add(T::DbWeight::get().writes(26_u64)), DispatchClass::Normal, Pays::Yes))] + #[pallet::weight((Weight::from_parts(486_500_000, 0) + .saturating_add(T::DbWeight::get().reads(35_u64)) + .saturating_add(T::DbWeight::get().writes(23_u64)), DispatchClass::Normal, Pays::Yes))] pub fn unstake_all_alpha(origin: OriginFor, hotkey: T::AccountId) -> DispatchResult { Self::do_unstake_all_alpha(origin, hotkey) } @@ -1604,8 +1519,8 @@ mod dispatches { /// - The alpha stake amount to move. /// #[pallet::call_index(85)] - #[pallet::weight((Weight::from_parts(164_300_000, 0) - .saturating_add(T::DbWeight::get().reads(15_u64)) + #[pallet::weight((Weight::from_parts(168_200_000, 0) + .saturating_add(T::DbWeight::get().reads(16_u64)) .saturating_add(T::DbWeight::get().writes(7_u64)), DispatchClass::Normal, Pays::Yes))] pub fn move_stake( origin: T::RuntimeOrigin, @@ -1647,8 +1562,8 @@ mod dispatches { /// # Events /// May emit a `StakeTransferred` event on success. #[pallet::call_index(86)] - #[pallet::weight((Weight::from_parts(160_300_000, 0) - .saturating_add(T::DbWeight::get().reads(13_u64)) + #[pallet::weight((Weight::from_parts(163_400_000, 0) + .saturating_add(T::DbWeight::get().reads(14_u64)) .saturating_add(T::DbWeight::get().writes(6_u64)), DispatchClass::Normal, Pays::Yes))] pub fn transfer_stake( origin: T::RuntimeOrigin, @@ -1689,9 +1604,9 @@ mod dispatches { /// May emit a `StakeSwapped` event on success. #[pallet::call_index(87)] #[pallet::weight(( - Weight::from_parts(351_300_000, 0) - .saturating_add(T::DbWeight::get().reads(37_u64)) - .saturating_add(T::DbWeight::get().writes(24_u64)), + Weight::from_parts(453_800_000, 0) + .saturating_add(T::DbWeight::get().reads(31_u64)) + .saturating_add(T::DbWeight::get().writes(21_u64)), DispatchClass::Normal, Pays::Yes ))] @@ -1754,9 +1669,9 @@ mod dispatches { /// - Errors stemming from transaction pallet. /// #[pallet::call_index(88)] - #[pallet::weight((Weight::from_parts(402_900_000, 0) - .saturating_add(T::DbWeight::get().reads(25_u64)) - .saturating_add(T::DbWeight::get().writes(16_u64)), DispatchClass::Normal, Pays::Yes))] + #[pallet::weight((Weight::from_parts(713_200_000, 0) + .saturating_add(T::DbWeight::get().reads(20_u64)) + .saturating_add(T::DbWeight::get().writes(14_u64)), DispatchClass::Normal, Pays::Yes))] pub fn add_stake_limit( origin: OriginFor, hotkey: T::AccountId, @@ -1773,6 +1688,7 @@ mod dispatches { limit_price, allow_partial, ) + .map(|_| ()) } /// --- Removes stake from a hotkey on a subnet with a price limit. @@ -1818,9 +1734,9 @@ mod dispatches { /// - Thrown if there is not enough stake on the hotkey to withdwraw this amount. /// #[pallet::call_index(89)] - #[pallet::weight((Weight::from_parts(377_400_000, 0) - .saturating_add(T::DbWeight::get().reads(29_u64)) - .saturating_add(T::DbWeight::get().writes(15_u64)), DispatchClass::Normal, Pays::Yes))] + #[pallet::weight((Weight::from_parts(611_100_000, 0) + .saturating_add(T::DbWeight::get().reads(23_u64)) + .saturating_add(T::DbWeight::get().writes(12_u64)), DispatchClass::Normal, Pays::Yes))] pub fn remove_stake_limit( origin: OriginFor, hotkey: T::AccountId, @@ -1862,9 +1778,9 @@ mod dispatches { /// May emit a `StakeSwapped` event on success. #[pallet::call_index(90)] #[pallet::weight(( - Weight::from_parts(411_500_000, 0) - .saturating_add(T::DbWeight::get().reads(37_u64)) - .saturating_add(T::DbWeight::get().writes(24_u64)), + Weight::from_parts(661_800_000, 0) + .saturating_add(T::DbWeight::get().reads(31_u64)) + .saturating_add(T::DbWeight::get().writes(21_u64)), DispatchClass::Normal, Pays::Yes ))] @@ -2040,9 +1956,9 @@ mod dispatches { /// at which or better (higher) the staking should execute. /// Without limit_price it remove all the stake similar to `remove_stake` extrinsic #[pallet::call_index(103)] - #[pallet::weight((Weight::from_parts(395_300_000, 10142) - .saturating_add(T::DbWeight::get().reads(29_u64)) - .saturating_add(T::DbWeight::get().writes(15_u64)), DispatchClass::Normal, Pays::Yes))] + #[pallet::weight((Weight::from_parts(615_000_000, 10142) + .saturating_add(T::DbWeight::get().reads(23_u64)) + .saturating_add(T::DbWeight::get().writes(12_u64)), DispatchClass::Normal, Pays::Yes))] pub fn remove_stake_full_limit( origin: T::RuntimeOrigin, hotkey: T::AccountId, @@ -2416,5 +2332,242 @@ mod dispatches { Ok(()) } + + /// Announces a coldkey swap using BlakeTwo256 hash of the new coldkey. + /// + /// This is required before the coldkey swap can be performed + /// after the delay period. + /// + /// It can be reannounced after a delay of `ColdkeySwapReannouncementDelay` following + /// the first valid execution block of the original announcement. + /// + /// The dispatch origin of this call must be the original coldkey that made the announcement. + /// + /// - `new_coldkey_hash`: The hash of the new coldkey using BlakeTwo256. + /// + /// The `ColdkeySwapAnnounced` event is emitted on successful announcement. + /// + #[pallet::call_index(125)] + #[pallet::weight( + Weight::from_parts(55_700_000, 0) + .saturating_add(T::DbWeight::get().reads(4_u64)) + .saturating_add(T::DbWeight::get().writes(3_u64)) + )] + pub fn announce_coldkey_swap( + origin: OriginFor, + new_coldkey_hash: T::Hash, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + let now = >::block_number(); + + if let Some((when, _)) = ColdkeySwapAnnouncements::::get(who.clone()) { + let reannouncement_delay = ColdkeySwapReannouncementDelay::::get(); + let new_when = when.saturating_add(reannouncement_delay); + ensure!(now >= new_when, Error::::ColdkeySwapReannouncedTooEarly); + } else { + // Only charge the swap cost on the first announcement + let swap_cost = Self::get_key_swap_cost(); + Self::charge_swap_cost(&who, swap_cost)?; + } + + let delay = ColdkeySwapAnnouncementDelay::::get(); + let when = now.saturating_add(delay); + ColdkeySwapAnnouncements::::insert(who.clone(), (when, new_coldkey_hash.clone())); + + Self::deposit_event(Event::ColdkeySwapAnnounced { + who, + new_coldkey_hash, + }); + Ok(()) + } + + /// Performs a coldkey swap if an announcement has been made. + /// + /// The dispatch origin of this call must be the original coldkey that made the announcement. + /// + /// - `new_coldkey`: The new coldkey to swap to. The BlakeTwo256 hash of the new coldkey must be + /// the same as the announced coldkey hash. + /// + /// The `ColdkeySwapped` event is emitted on successful swap. + #[pallet::call_index(126)] + #[pallet::weight( + Weight::from_parts(110_700_000, 0) + .saturating_add(T::DbWeight::get().reads(16_u64)) + .saturating_add(T::DbWeight::get().writes(6_u64)) + )] + pub fn swap_coldkey_announced( + origin: OriginFor, + new_coldkey: T::AccountId, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + + let (when, new_coldkey_hash) = ColdkeySwapAnnouncements::::take(who.clone()) + .ok_or(Error::::ColdkeySwapAnnouncementNotFound)?; + + ensure!( + new_coldkey_hash == T::Hashing::hash_of(&new_coldkey), + Error::::AnnouncedColdkeyHashDoesNotMatch + ); + + let now = >::block_number(); + ensure!(now >= when, Error::::ColdkeySwapTooEarly); + + Self::do_swap_coldkey(&who, &new_coldkey)?; + + Ok(()) + } + + /// Dispute a coldkey swap. + /// + /// This will prevent any further actions on the coldkey swap + /// until triumvirate step in to resolve the issue. + /// + /// - `coldkey`: The coldkey to dispute the swap for. + /// + #[pallet::call_index(127)] + #[pallet::weight( + Weight::from_parts(20_750_000, 0) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + )] + pub fn dispute_coldkey_swap(origin: OriginFor) -> DispatchResult { + let coldkey = ensure_signed(origin)?; + + ensure!( + ColdkeySwapAnnouncements::::contains_key(&coldkey), + Error::::ColdkeySwapAnnouncementNotFound + ); + ensure!( + !ColdkeySwapDisputes::::contains_key(&coldkey), + Error::::ColdkeySwapAlreadyDisputed + ); + + let now = >::block_number(); + ColdkeySwapDisputes::::insert(&coldkey, now); + + Self::deposit_event(Event::ColdkeySwapDisputed { coldkey }); + Ok(()) + } + + /// Reset a coldkey swap by clearing the announcement and dispute status. + /// + /// The dispatch origin of this call must be root. + /// + /// - `coldkey`: The coldkey to reset the swap for. + /// + #[pallet::call_index(128)] + #[pallet::weight( + Weight::from_parts(8_977_000, 0) + .saturating_add(T::DbWeight::get().reads(0_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + )] + pub fn reset_coldkey_swap(origin: OriginFor, coldkey: T::AccountId) -> DispatchResult { + ensure_root(origin)?; + + ColdkeySwapAnnouncements::::remove(&coldkey); + ColdkeySwapDisputes::::remove(&coldkey); + + Self::deposit_event(Event::ColdkeySwapReset { who: coldkey }); + Ok(()) + } + + /// Enables voting power tracking for a subnet. + /// + /// This function can be called by the subnet owner or root. + /// When enabled, voting power EMA is updated every epoch for all validators. + /// Voting power starts at 0 and increases over epochs. + /// + /// # Arguments: + /// * `origin` - The origin of the call, must be subnet owner or root. + /// * `netuid` - The subnet to enable voting power tracking for. + /// + /// # Errors: + /// * `SubnetNotExist` - If the subnet does not exist. + /// * `NotSubnetOwner` - If the caller is not the subnet owner or root. + #[pallet::call_index(129)] + #[pallet::weight(Weight::from_parts(10_000, 0) + .saturating_add(T::DbWeight::get().reads(2)) + .saturating_add(T::DbWeight::get().writes(2)))] + pub fn enable_voting_power_tracking( + origin: OriginFor, + netuid: NetUid, + ) -> DispatchResult { + Self::ensure_subnet_owner_or_root(origin, netuid)?; + Self::do_enable_voting_power_tracking(netuid) + } + + /// Schedules disabling of voting power tracking for a subnet. + /// + /// This function can be called by the subnet owner or root. + /// Voting power tracking will continue for 14 days (grace period) after this call, + /// then automatically disable and clear all VotingPower entries for the subnet. + /// + /// # Arguments: + /// * `origin` - The origin of the call, must be subnet owner or root. + /// * `netuid` - The subnet to schedule disabling voting power tracking for. + /// + /// # Errors: + /// * `SubnetNotExist` - If the subnet does not exist. + /// * `NotSubnetOwner` - If the caller is not the subnet owner or root. + /// * `VotingPowerTrackingNotEnabled` - If voting power tracking is not enabled. + #[pallet::call_index(130)] + #[pallet::weight(Weight::from_parts(10_000, 0) + .saturating_add(T::DbWeight::get().reads(2)) + .saturating_add(T::DbWeight::get().writes(1)))] + pub fn disable_voting_power_tracking( + origin: OriginFor, + netuid: NetUid, + ) -> DispatchResult { + Self::ensure_subnet_owner_or_root(origin, netuid)?; + Self::do_disable_voting_power_tracking(netuid) + } + + /// Sets the EMA alpha value for voting power calculation on a subnet. + /// + /// This function can only be called by root (sudo). + /// Higher alpha = faster response to stake changes. + /// Alpha is stored as u64 with 18 decimal precision (1.0 = 10^18). + /// + /// # Arguments: + /// * `origin` - The origin of the call, must be root. + /// * `netuid` - The subnet to set the alpha for. + /// * `alpha` - The new alpha value (u64 with 18 decimal precision). + /// + /// # Errors: + /// * `BadOrigin` - If the origin is not root. + /// * `SubnetNotExist` - If the subnet does not exist. + /// * `InvalidVotingPowerEmaAlpha` - If alpha is greater than 10^18 (1.0). + #[pallet::call_index(131)] + #[pallet::weight(Weight::from_parts(6_000, 0) + .saturating_add(T::DbWeight::get().reads(1)) + .saturating_add(T::DbWeight::get().writes(1)))] + pub fn sudo_set_voting_power_ema_alpha( + origin: OriginFor, + netuid: NetUid, + alpha: u64, + ) -> DispatchResult { + ensure_root(origin)?; + Self::do_set_voting_power_ema_alpha(netuid, alpha) + } + + /// --- The extrinsic is a combination of add_stake(add_stake_limit) and burn_alpha. We buy + /// alpha token first and immediately burn the acquired amount of alpha (aka Subnet buyback). + #[pallet::call_index(132)] + #[pallet::weight(( + Weight::from_parts(757_700_000, 8556) + .saturating_add(T::DbWeight::get().reads(23_u64)) + .saturating_add(T::DbWeight::get().writes(15_u64)), + DispatchClass::Normal, + Pays::Yes + ))] + pub fn add_stake_burn( + origin: T::RuntimeOrigin, + hotkey: T::AccountId, + netuid: NetUid, + amount: TaoCurrency, + limit: Option, + ) -> DispatchResult { + Self::do_add_stake_burn(origin, hotkey, netuid, amount, limit) + } } } diff --git a/pallets/subtensor/src/macros/errors.rs b/pallets/subtensor/src/macros/errors.rs index 6c3d7a35df..7d50373f19 100644 --- a/pallets/subtensor/src/macros/errors.rs +++ b/pallets/subtensor/src/macros/errors.rs @@ -138,8 +138,6 @@ mod errors { ColdKeyAlreadyAssociated, /// The coldkey balance is not enough to pay for the swap NotEnoughBalanceToPaySwapColdKey, - /// The coldkey is in arbitration - ColdkeyIsInArbitration, /// Attempting to set an invalid child for a hotkey on a network. InvalidChild, /// Duplicate child when setting children. @@ -150,10 +148,16 @@ mod errors { TooManyChildren, /// Default transaction rate limit exceeded. TxRateLimitExceeded, - /// Swap already scheduled. - SwapAlreadyScheduled, - /// failed to swap coldkey - FailedToSchedule, + /// Coldkey swap announcement not found + ColdkeySwapAnnouncementNotFound, + /// Coldkey swap too early. + ColdkeySwapTooEarly, + /// Coldkey swap reannounced too early. + ColdkeySwapReannouncedTooEarly, + /// The announced coldkey hash does not match the new coldkey hash. + AnnouncedColdkeyHashDoesNotMatch, + /// Coldkey swap already disputed + ColdkeySwapAlreadyDisputed, /// New coldkey is hotkey NewColdKeyIsHotkey, /// Childkey take is invalid. @@ -266,7 +270,17 @@ mod errors { InvalidRootClaimThreshold, /// Exceeded subnet limit number or zero. InvalidSubnetNumber, + /// The maximum allowed UIDs times mechanism count should not exceed 256. + TooManyUIDsPerMechanism, + /// Voting power tracking is not enabled for this subnet. + VotingPowerTrackingNotEnabled, + /// Invalid voting power EMA alpha value (must be <= 10^18). + InvalidVotingPowerEmaAlpha, /// Unintended precision loss when unstaking alpha PrecisionLoss, + /// Deprecated call. + Deprecated, + /// "Add stake and burn" exceeded the operation rate limit + AddStakeBurnRateLimitExceeded, } } diff --git a/pallets/subtensor/src/macros/events.rs b/pallets/subtensor/src/macros/events.rs index c86cc1a1e5..65c33aee87 100644 --- a/pallets/subtensor/src/macros/events.rs +++ b/pallets/subtensor/src/macros/events.rs @@ -172,14 +172,29 @@ mod events { MaxDelegateTakeSet(u16), /// minimum delegate take is set by sudo/admin transaction MinDelegateTakeSet(u16), - /// A coldkey has been swapped + /// A coldkey swap announcement has been made. + ColdkeySwapAnnounced { + /// The account ID of the coldkey that made the announcement. + who: T::AccountId, + /// The hash of the new coldkey. + new_coldkey_hash: T::Hash, + }, + /// A coldkey swap has been reset. + ColdkeySwapReset { + /// The account ID of the coldkey for which the swap has been reset. + who: T::AccountId, + }, + /// A coldkey has been swapped. ColdkeySwapped { - /// the account ID of old coldkey + /// The account ID of old coldkey. old_coldkey: T::AccountId, - /// the account ID of new coldkey + /// The account ID of new coldkey. new_coldkey: T::AccountId, - /// the swap cost - swap_cost: TaoCurrency, + }, + /// A coldkey swap has been disputed. + ColdkeySwapDisputed { + /// The account ID of the coldkey that was disputed. + coldkey: T::AccountId, }, /// All balance of a hotkey has been unstaked and transferred to a new coldkey AllBalanceUnstakedAndTransferredToNewColdkey { @@ -192,17 +207,6 @@ mod events { ::AccountId, >>::Balance, }, - /// A coldkey swap has been scheduled - ColdkeySwapScheduled { - /// The account ID of the old coldkey - old_coldkey: T::AccountId, - /// The account ID of the new coldkey - new_coldkey: T::AccountId, - /// The arbitration block for the coldkey swap - execution_block: BlockNumberFor, - /// The swap cost - swap_cost: TaoCurrency, - }, /// The arbitration period has been extended ArbitrationPeriodExtended { /// The account ID of the coldkey @@ -224,15 +228,17 @@ mod events { SubnetIdentityRemoved(NetUid), /// A dissolve network extrinsic scheduled. DissolveNetworkScheduled { - /// The account ID schedule the dissolve network extrisnic + /// The account ID schedule the dissolve network extrinsic account: T::AccountId, /// network ID will be dissolved netuid: NetUid, /// extrinsic execution block number execution_block: BlockNumberFor, }, - /// The duration of schedule coldkey swap has been set - ColdkeySwapScheduleDurationSet(BlockNumberFor), + /// The coldkey swap announcement delay has been set. + ColdkeySwapAnnouncementDelaySet(BlockNumberFor), + /// The coldkey swap reannouncement delay has been set. + ColdkeySwapReannouncementDelaySet(BlockNumberFor), /// The duration of dissolve network has been set DissolveNetworkScheduleDurationSet(BlockNumberFor), /// Commit-reveal v3 weights have been successfully committed. @@ -472,6 +478,35 @@ mod events { root_claim_type: RootClaimTypeEnum, }, + /// Voting power tracking has been enabled for a subnet. + VotingPowerTrackingEnabled { + /// The subnet ID + netuid: NetUid, + }, + + /// Voting power tracking has been scheduled for disabling. + /// Tracking will continue until disable_at_block, then stop and clear entries. + VotingPowerTrackingDisableScheduled { + /// The subnet ID + netuid: NetUid, + /// Block at which tracking will be disabled + disable_at_block: u64, + }, + + /// Voting power tracking has been fully disabled and entries cleared. + VotingPowerTrackingDisabled { + /// The subnet ID + netuid: NetUid, + }, + + /// Voting power EMA alpha has been set for a subnet. + VotingPowerEmaAlphaSet { + /// The subnet ID + netuid: NetUid, + /// The new alpha value (u64 with 18 decimal precision) + alpha: u64, + }, + /// Subnet lease dividends have been distributed. SubnetLeaseDividendsDistributed { /// The lease ID @@ -481,5 +516,17 @@ mod events { /// The amount of alpha distributed alpha: AlphaCurrency, }, + + /// "Add stake and burn" event: alpha token was purchased and burned. + AddStakeBurn { + /// The subnet ID + netuid: NetUid, + /// hotky account ID + hotkey: T::AccountId, + /// Tao provided + amount: TaoCurrency, + /// Alpha burned + alpha: AlphaCurrency, + }, } } diff --git a/pallets/subtensor/src/macros/genesis.rs b/pallets/subtensor/src/macros/genesis.rs index 0014b63540..f16f2f7a3a 100644 --- a/pallets/subtensor/src/macros/genesis.rs +++ b/pallets/subtensor/src/macros/genesis.rs @@ -106,7 +106,6 @@ mod genesis { netuid, U64F64::saturating_from_num(1_000_000_000), ); - // TotalColdkeyAlpha::::insert(hotkey.clone(), netuid, 1_000_000_000); SubnetAlphaOut::::insert(netuid, AlphaCurrency::from(1_000_000_000)); let mut staking_hotkeys = StakingHotkeys::::get(hotkey.clone()); if !staking_hotkeys.contains(&hotkey) { diff --git a/pallets/subtensor/src/macros/hooks.rs b/pallets/subtensor/src/macros/hooks.rs index ed57d52c8b..899e8d32f2 100644 --- a/pallets/subtensor/src/macros/hooks.rs +++ b/pallets/subtensor/src/macros/hooks.rs @@ -164,7 +164,9 @@ mod hooks { // Remove unknown neuron axon, certificate prom .saturating_add(migrations::migrate_remove_unknown_neuron_axon_cert_prom::migrate_remove_unknown_neuron_axon_cert_prom::()) // Fix staking hot keys - .saturating_add(migrations::migrate_fix_staking_hot_keys::migrate_fix_staking_hot_keys::()); + .saturating_add(migrations::migrate_fix_staking_hot_keys::migrate_fix_staking_hot_keys::()) + // Migrate coldkey swap scheduled to announcements + .saturating_add(migrations::migrate_coldkey_swap_scheduled_to_announcements::migrate_coldkey_swap_scheduled_to_announcements::()); weight } diff --git a/pallets/subtensor/src/migrations/migrate_cleanup_swap_v3.rs b/pallets/subtensor/src/migrations/migrate_cleanup_swap_v3.rs new file mode 100644 index 0000000000..13edf21033 --- /dev/null +++ b/pallets/subtensor/src/migrations/migrate_cleanup_swap_v3.rs @@ -0,0 +1,70 @@ +use super::*; +use crate::HasMigrationRun; +use frame_support::{storage_alias, traits::Get, weights::Weight}; +use scale_info::prelude::string::String; + +pub mod deprecated_swap_maps { + use super::*; + + /// --- MAP ( netuid ) --> tao_in_user_subnet | Returns the amount of TAO in the subnet reserve provided by users as liquidity. + #[storage_alias] + pub type SubnetTaoProvided = + StorageMap, Identity, NetUid, TaoCurrency, ValueQuery>; + + /// --- MAP ( netuid ) --> alpha_supply_user_in_pool | Returns the amount of alpha in the pool provided by users as liquidity. + #[storage_alias] + pub type SubnetAlphaInProvided = + StorageMap, Identity, NetUid, AlphaCurrency, ValueQuery>; +} + +pub fn migrate_cleanup_swap_v3() -> Weight { + let migration_name = b"migrate_cleanup_swap_v3".to_vec(); + let mut weight = T::DbWeight::get().reads(1); + + if HasMigrationRun::::get(&migration_name) { + log::info!( + "Migration '{:?}' has already run. Skipping.", + String::from_utf8_lossy(&migration_name) + ); + return weight; + } + + log::info!( + "Running migration '{}'", + String::from_utf8_lossy(&migration_name), + ); + + // ------------------------------ + // Step 1: Move provided to reserves + // ------------------------------ + for (netuid, tao_provided) in deprecated_swap_maps::SubnetTaoProvided::::iter() { + SubnetTAO::::mutate(netuid, |total| { + *total = total.saturating_add(tao_provided); + }); + } + for (netuid, alpha_provided) in deprecated_swap_maps::SubnetAlphaInProvided::::iter() { + SubnetAlphaIn::::mutate(netuid, |total| { + *total = total.saturating_add(alpha_provided); + }); + } + + // ------------------------------ + // Step 2: Remove Map entries + // ------------------------------ + remove_prefix::("SubtensorModule", "SubnetTaoProvided", &mut weight); + remove_prefix::("SubtensorModule", "SubnetAlphaInProvided", &mut weight); + + // ------------------------------ + // Step 3: Mark Migration as Completed + // ------------------------------ + + HasMigrationRun::::insert(&migration_name, true); + weight = weight.saturating_add(T::DbWeight::get().writes(1)); + + log::info!( + "Migration '{:?}' completed successfully.", + String::from_utf8_lossy(&migration_name) + ); + + weight +} diff --git a/pallets/subtensor/src/migrations/migrate_coldkey_swap_scheduled.rs b/pallets/subtensor/src/migrations/migrate_coldkey_swap_scheduled.rs index 8854f76387..243d953ac1 100644 --- a/pallets/subtensor/src/migrations/migrate_coldkey_swap_scheduled.rs +++ b/pallets/subtensor/src/migrations/migrate_coldkey_swap_scheduled.rs @@ -55,13 +55,6 @@ pub fn migrate_coldkey_swap_scheduled() -> Weight { } } - let default_value = DefaultColdkeySwapScheduled::::get(); - ColdkeySwapScheduled::::translate::<(), _>(|_coldkey: AccountIdOf, _: ()| { - Some((default_value.0, default_value.1.clone())) - }); - // write once for each item in the map, no matter remove or translate - weight.saturating_accrue(T::DbWeight::get().writes(curr_keys.len() as u64)); - // ------------------------------ // Step 2: Mark Migration as Completed // ------------------------------ diff --git a/pallets/subtensor/src/migrations/migrate_coldkey_swap_scheduled_to_announcements.rs b/pallets/subtensor/src/migrations/migrate_coldkey_swap_scheduled_to_announcements.rs new file mode 100644 index 0000000000..12fa3f5768 --- /dev/null +++ b/pallets/subtensor/src/migrations/migrate_coldkey_swap_scheduled_to_announcements.rs @@ -0,0 +1,91 @@ +use super::*; +use crate::AccountIdOf; +use frame_support::{pallet_prelude::Blake2_128Concat, traits::Get, weights::Weight}; +use frame_system::pallet_prelude::BlockNumberFor; +use scale_info::prelude::string::String; +use sp_io::storage::clear; +use sp_runtime::{Saturating, traits::Hash}; + +pub mod deprecated { + use super::*; + use frame_support::storage_alias; + + #[storage_alias] + pub type ColdkeySwapScheduleDuration = + StorageValue, BlockNumberFor, OptionQuery>; + + #[storage_alias] + pub type ColdkeySwapRescheduleDuration = + StorageValue, BlockNumberFor, OptionQuery>; + + #[storage_alias] + pub type ColdkeySwapScheduled = StorageMap< + Pallet, + Blake2_128Concat, + AccountIdOf, + (BlockNumberFor, AccountIdOf), + OptionQuery, + >; +} + +pub fn migrate_coldkey_swap_scheduled_to_announcements() -> Weight { + let migration_name = b"migrate_coldkey_swap_scheduled_to_announcements".to_vec(); + let mut weight = T::DbWeight::get().reads(1); + + if HasMigrationRun::::get(&migration_name) { + log::info!( + "Migration '{:?}' has already run. Skipping.", + String::from_utf8_lossy(&migration_name) + ); + return weight; + } + + log::info!( + "Running migration '{}'", + String::from_utf8_lossy(&migration_name) + ); + + // Remove ColdkeySwapScheduleDuration and ColdkeySwapRescheduleDuration + let pallet_name = twox_128(b"SubtensorModule"); + let storage_name1 = twox_128(b"ColdkeySwapScheduleDuration"); + let storage_name2 = twox_128(b"ColdkeySwapRescheduleDuration"); + clear(&[pallet_name, storage_name1].concat()); + clear(&[pallet_name, storage_name2].concat()); + weight.saturating_accrue(T::DbWeight::get().writes(2)); + + // Migrate the ColdkeySwapScheduled entries to ColdkeySwapAnnouncements entries + let now = >::block_number(); + let scheduled = deprecated::ColdkeySwapScheduled::::iter(); + let delay = ColdkeySwapAnnouncementDelay::::get(); + + for (who, (when, new_coldkey)) in scheduled { + // Only migrate the scheduled coldkey swaps that are in the future + if when > now { + let coldkey_hash = ::Hashing::hash_of(&new_coldkey); + // The announcement should be at the scheduled time - delay to be able to call + // the swap_coldkey_announced call at the old scheduled time + ColdkeySwapAnnouncements::::insert(who, (when.saturating_sub(delay), coldkey_hash)); + weight.saturating_accrue(T::DbWeight::get().writes(1)); + } + weight.saturating_accrue(T::DbWeight::get().reads(1)); + } + + let results = deprecated::ColdkeySwapScheduled::::clear(u32::MAX, None); + weight.saturating_accrue( + T::DbWeight::get().reads_writes(results.loops as u64, results.backend as u64), + ); + + // ------------------------------ + // Step 2: Mark Migration as Completed + // ------------------------------ + + HasMigrationRun::::insert(&migration_name, true); + weight = weight.saturating_add(T::DbWeight::get().writes(1)); + + log::info!( + "Migration '{:?}' completed successfully.", + String::from_utf8_lossy(&migration_name) + ); + + weight +} diff --git a/pallets/subtensor/src/migrations/mod.rs b/pallets/subtensor/src/migrations/mod.rs index a03da9289e..087c787424 100644 --- a/pallets/subtensor/src/migrations/mod.rs +++ b/pallets/subtensor/src/migrations/mod.rs @@ -5,8 +5,10 @@ use sp_io::KillStorageResult; use sp_io::hashing::twox_128; use sp_io::storage::clear_prefix; pub mod migrate_auto_stake_destination; +pub mod migrate_cleanup_swap_v3; pub mod migrate_clear_rank_trust_pruning_maps; pub mod migrate_coldkey_swap_scheduled; +pub mod migrate_coldkey_swap_scheduled_to_announcements; pub mod migrate_commit_reveal_settings; pub mod migrate_commit_reveal_v2; pub mod migrate_create_root_network; diff --git a/pallets/subtensor/src/rpc_info/subnet_info.rs b/pallets/subtensor/src/rpc_info/subnet_info.rs index 6a7966b4fb..7169561d30 100644 --- a/pallets/subtensor/src/rpc_info/subnet_info.rs +++ b/pallets/subtensor/src/rpc_info/subnet_info.rs @@ -365,7 +365,6 @@ impl Pallet { let subnet_token_enabled = Self::get_subtoken_enabled(netuid); let transfers_enabled = Self::get_transfer_toggle(netuid); let bonds_reset = Self::get_bonds_reset(netuid); - let user_liquidity_enabled: bool = Self::is_user_liquidity_enabled(netuid); Some(SubnetHyperparamsV2 { rho: rho.into(), @@ -400,7 +399,7 @@ impl Pallet { subnet_is_active: subnet_token_enabled, transfers_enabled, bonds_reset_enabled: bonds_reset, - user_liquidity_enabled, + user_liquidity_enabled: false, }) } diff --git a/pallets/subtensor/src/staking/add_stake.rs b/pallets/subtensor/src/staking/add_stake.rs index ea33912bf1..b0c71a3a48 100644 --- a/pallets/subtensor/src/staking/add_stake.rs +++ b/pallets/subtensor/src/staking/add_stake.rs @@ -42,7 +42,7 @@ impl Pallet { hotkey: T::AccountId, netuid: NetUid, stake_to_be_added: TaoCurrency, - ) -> dispatch::DispatchResult { + ) -> Result { // 1. We check that the transaction is signed by the caller and retrieve the T::AccountId coldkey information. let coldkey = ensure_signed(origin)?; log::debug!( @@ -77,10 +77,7 @@ impl Pallet { T::SwapInterface::max_price(), true, false, - )?; - - // Ok and return. - Ok(()) + ) } /// ---- The implementation for the extrinsic add_stake_limit: Adds stake to a hotkey @@ -130,7 +127,7 @@ impl Pallet { stake_to_be_added: TaoCurrency, limit_price: TaoCurrency, allow_partial: bool, - ) -> dispatch::DispatchResult { + ) -> Result { // 1. We check that the transaction is signed by the caller and retrieve the T::AccountId coldkey information. let coldkey = ensure_signed(origin)?; log::debug!( @@ -173,10 +170,7 @@ impl Pallet { limit_price, true, false, - )?; - - // Ok and return. - Ok(()) + ) } // Returns the maximum amount of RAO that can be executed with price limit diff --git a/pallets/subtensor/src/staking/helpers.rs b/pallets/subtensor/src/staking/helpers.rs index 099f8e26b6..998d4f0086 100644 --- a/pallets/subtensor/src/staking/helpers.rs +++ b/pallets/subtensor/src/staking/helpers.rs @@ -6,7 +6,7 @@ use frame_support::traits::{ }, }; use safe_math::*; -use substrate_fixed::types::U96F32; +use substrate_fixed::types::{U64F64, U96F32}; use subtensor_runtime_common::{NetUid, TaoCurrency}; use subtensor_swap_interface::{Order, SwapHandler}; @@ -48,15 +48,13 @@ impl Pallet { Self::get_all_subnet_netuids() .into_iter() .map(|netuid| { - let alpha = U96F32::saturating_from_num(Self::get_stake_for_hotkey_on_subnet( + let alpha = U64F64::saturating_from_num(Self::get_stake_for_hotkey_on_subnet( hotkey, netuid, )); - let alpha_price = U96F32::saturating_from_num( - T::SwapInterface::current_alpha_price(netuid.into()), - ); + let alpha_price = T::SwapInterface::current_alpha_price(netuid.into()); alpha.saturating_mul(alpha_price) }) - .sum::() + .sum::() .saturating_to_num::() .into() } @@ -76,7 +74,7 @@ impl Pallet { let order = GetTaoForAlpha::::with_amount(alpha_stake); T::SwapInterface::sim_swap(netuid.into(), order) .map(|r| { - let fee: u64 = U96F32::saturating_from_num(r.fee_paid) + let fee: u64 = U64F64::saturating_from_num(r.fee_paid) .saturating_mul(T::SwapInterface::current_alpha_price( netuid.into(), )) @@ -110,7 +108,7 @@ impl Pallet { let order = GetTaoForAlpha::::with_amount(alpha_stake); T::SwapInterface::sim_swap(netuid.into(), order) .map(|r| { - let fee: u64 = U96F32::saturating_from_num(r.fee_paid) + let fee: u64 = U64F64::saturating_from_num(r.fee_paid) .saturating_mul(T::SwapInterface::current_alpha_price( netuid.into(), )) @@ -223,7 +221,7 @@ impl Pallet { let alpha_stake = Self::get_stake_for_hotkey_and_coldkey_on_subnet(hotkey, coldkey, netuid); let min_alpha_stake = - U96F32::saturating_from_num(Self::get_nominator_min_required_stake()) + U64F64::saturating_from_num(Self::get_nominator_min_required_stake()) .safe_div(T::SwapInterface::current_alpha_price(netuid)) .saturating_to_num::(); if alpha_stake > 0.into() && alpha_stake < min_alpha_stake.into() { @@ -352,10 +350,6 @@ impl Pallet { Ok(credit) } - pub fn is_user_liquidity_enabled(netuid: NetUid) -> bool { - T::SwapInterface::is_user_liquidity_enabled(netuid) - } - pub fn recycle_subnet_alpha(netuid: NetUid, amount: AlphaCurrency) { // TODO: record recycled alpha in a tracker SubnetAlphaOut::::mutate(netuid, |total| { diff --git a/pallets/subtensor/src/staking/move_stake.rs b/pallets/subtensor/src/staking/move_stake.rs index a1d9b46d5b..76d9c95d3a 100644 --- a/pallets/subtensor/src/staking/move_stake.rs +++ b/pallets/subtensor/src/staking/move_stake.rs @@ -418,7 +418,8 @@ impl Pallet { /// /// In the corner case when SubnetTAO(2) == SubnetTAO(1), no slippage is going to occur. /// - /// TODO: This formula only works for a single swap step, so it is not 100% correct for swap v3. We need an updated one. + /// TODO: This formula only works for a single swap step, so it is not 100% correct for swap v3 or balancers. + /// We need an updated one. /// pub fn get_max_amount_move( origin_netuid: NetUid, @@ -471,10 +472,8 @@ impl Pallet { } // Corner case: SubnetTAO for any of two subnets is zero - let subnet_tao_1 = SubnetTAO::::get(origin_netuid) - .saturating_add(SubnetTaoProvided::::get(origin_netuid)); - let subnet_tao_2 = SubnetTAO::::get(destination_netuid) - .saturating_add(SubnetTaoProvided::::get(destination_netuid)); + let subnet_tao_1 = SubnetTAO::::get(origin_netuid); + let subnet_tao_2 = SubnetTAO::::get(destination_netuid); if subnet_tao_1.is_zero() || subnet_tao_2.is_zero() { return Err(Error::::ZeroMaxStakeAmount.into()); } @@ -482,10 +481,8 @@ impl Pallet { let subnet_tao_2_float: U64F64 = U64F64::saturating_from_num(subnet_tao_2); // Corner case: SubnetAlphaIn for any of two subnets is zero - let alpha_in_1 = SubnetAlphaIn::::get(origin_netuid) - .saturating_add(SubnetAlphaInProvided::::get(origin_netuid)); - let alpha_in_2 = SubnetAlphaIn::::get(destination_netuid) - .saturating_add(SubnetAlphaInProvided::::get(destination_netuid)); + let alpha_in_1 = SubnetAlphaIn::::get(origin_netuid); + let alpha_in_2 = SubnetAlphaIn::::get(destination_netuid); if alpha_in_1.is_zero() || alpha_in_2.is_zero() { return Err(Error::::ZeroMaxStakeAmount.into()); } diff --git a/pallets/subtensor/src/staking/recycle_alpha.rs b/pallets/subtensor/src/staking/recycle_alpha.rs index 5229971ed0..b77982fa31 100644 --- a/pallets/subtensor/src/staking/recycle_alpha.rs +++ b/pallets/subtensor/src/staking/recycle_alpha.rs @@ -134,6 +134,43 @@ impl Pallet { netuid, )); + Ok(()) + } + pub(crate) fn do_add_stake_burn( + origin: T::RuntimeOrigin, + hotkey: T::AccountId, + netuid: NetUid, + amount: TaoCurrency, + limit: Option, + ) -> DispatchResult { + Self::ensure_subnet_owner(origin.clone(), netuid)?; + + let current_block = Self::get_current_block_as_u64(); + let last_block = Self::get_rate_limited_last_block(&RateLimitKey::AddStakeBurn(netuid)); + let rate_limit = TransactionType::AddStakeBurn.rate_limit_on_subnet::(netuid); + + ensure!( + last_block.is_zero() || current_block.saturating_sub(last_block) >= rate_limit, + Error::::AddStakeBurnRateLimitExceeded + ); + + let alpha = if let Some(limit) = limit { + Self::do_add_stake_limit(origin.clone(), hotkey.clone(), netuid, amount, limit, false)? + } else { + Self::do_add_stake(origin.clone(), hotkey.clone(), netuid, amount)? + }; + + Self::do_burn_alpha(origin, hotkey.clone(), alpha, netuid)?; + + Self::set_rate_limited_last_block(&RateLimitKey::AddStakeBurn(netuid), current_block); + + Self::deposit_event(Event::AddStakeBurn { + netuid, + hotkey, + amount, + alpha, + }); + Ok(()) } } diff --git a/pallets/subtensor/src/staking/remove_stake.rs b/pallets/subtensor/src/staking/remove_stake.rs index 74a6bf34a6..735ec804df 100644 --- a/pallets/subtensor/src/staking/remove_stake.rs +++ b/pallets/subtensor/src/staking/remove_stake.rs @@ -463,7 +463,9 @@ impl Pallet { .saturating_to_num::(); owner_emission_tao = if owner_alpha_u64 > 0 { - let cur_price: U96F32 = T::SwapInterface::current_alpha_price(netuid.into()); + let cur_price: U96F32 = U96F32::saturating_from_num( + T::SwapInterface::current_alpha_price(netuid.into()), + ); let val_u64 = U96F32::from_num(owner_alpha_u64) .saturating_mul(cur_price) .floor() @@ -581,7 +583,6 @@ impl Pallet { } // 7.c) Remove α‑in/α‑out counters (fully destroyed). SubnetAlphaIn::::remove(netuid); - SubnetAlphaInProvided::::remove(netuid); SubnetAlphaOut::::remove(netuid); // Clear the locked balance on the subnet. diff --git a/pallets/subtensor/src/staking/stake_utils.rs b/pallets/subtensor/src/staking/stake_utils.rs index 1aeeacc33c..d030ee2d76 100644 --- a/pallets/subtensor/src/staking/stake_utils.rs +++ b/pallets/subtensor/src/staking/stake_utils.rs @@ -3,7 +3,7 @@ use safe_math::*; use share_pool::{SharePool, SharePoolDataOperations}; use sp_std::ops::Neg; use substrate_fixed::types::{I64F64, I96F32, U64F64, U96F32}; -use subtensor_runtime_common::{AlphaCurrency, Currency, NetUid, TaoCurrency}; +use subtensor_runtime_common::{AlphaCurrency, AuthorshipInfo, Currency, NetUid, TaoCurrency}; use subtensor_swap_interface::{Order, SwapHandler, SwapResult}; impl Pallet { @@ -18,13 +18,7 @@ impl Pallet { /// # Returns /// * `u64` - The total alpha issuance for the specified subnet. pub fn get_alpha_issuance(netuid: NetUid) -> AlphaCurrency { - SubnetAlphaIn::::get(netuid) - .saturating_add(SubnetAlphaInProvided::::get(netuid)) - .saturating_add(SubnetAlphaOut::::get(netuid)) - } - - pub fn get_protocol_tao(netuid: NetUid) -> TaoCurrency { - T::SwapInterface::get_protocol_tao(netuid) + SubnetAlphaIn::::get(netuid).saturating_add(SubnetAlphaOut::::get(netuid)) } pub fn get_moving_alpha_price(netuid: NetUid) -> U96F32 { @@ -63,10 +57,10 @@ impl Pallet { // Because alpha = b / (b + h), where b and h > 0, alpha < 1, so 1 - alpha > 0. // We can use unsigned type here: U96F32 let one_minus_alpha: U96F32 = U96F32::saturating_from_num(1.0).saturating_sub(alpha); - let current_price: U96F32 = alpha.saturating_mul( + let current_price: U96F32 = alpha.saturating_mul(U96F32::saturating_from_num( T::SwapInterface::current_alpha_price(netuid.into()) - .min(U96F32::saturating_from_num(1.0)), - ); + .min(U64F64::saturating_from_num(1.0)), + )); let current_moving: U96F32 = one_minus_alpha.saturating_mul(Self::get_moving_alpha_price(netuid)); // Convert batch to signed I96F32 to avoid migration of SubnetMovingPrice for now`` @@ -596,6 +590,7 @@ impl Pallet { amount_paid_in: tao, amount_paid_out: tao.to_u64().into(), fee_paid: TaoCurrency::ZERO, + fee_to_block_author: TaoCurrency::ZERO, } }; @@ -649,19 +644,17 @@ impl Pallet { amount_paid_in: alpha, amount_paid_out: alpha.to_u64().into(), fee_paid: AlphaCurrency::ZERO, + fee_to_block_author: AlphaCurrency::ZERO, } }; - // Increase only the protocol Alpha reserve. We only use the sum of - // (SubnetAlphaIn + SubnetAlphaInProvided) in alpha_reserve(), so it is irrelevant - // which one to increase. + // Increase only the protocol Alpha reserve let alpha_delta = swap_result.paid_in_reserve_delta_i64().unsigned_abs(); SubnetAlphaIn::::mutate(netuid, |total| { *total = total.saturating_add(alpha_delta.into()); }); // Decrease Alpha outstanding. - // TODO: Deprecate, not accurate in v3 anymore SubnetAlphaOut::::mutate(netuid, |total| { *total = total.saturating_sub(alpha_delta.into()); }); @@ -712,6 +705,26 @@ impl Pallet { Self::increase_stake_for_hotkey_and_coldkey_on_subnet(hotkey, coldkey, netuid, refund); } + // Swap (in a fee-less way) the block builder alpha fee + let mut fee_outflow = 0_u64; + let maybe_block_author_coldkey = T::AuthorshipProvider::author(); + if let Some(block_author_coldkey) = maybe_block_author_coldkey { + let bb_swap_result = Self::swap_alpha_for_tao( + netuid, + swap_result.fee_to_block_author, + T::SwapInterface::min_price::(), + true, + )?; + Self::add_balance_to_coldkey_account( + &block_author_coldkey, + bb_swap_result.amount_paid_out.into(), + ); + fee_outflow = bb_swap_result.amount_paid_out.into(); + } else { + // block author is not found, burn this alpha + Self::burn_subnet_alpha(netuid, swap_result.fee_to_block_author); + } + // If this is a root-stake if netuid == NetUid::ROOT { // Adjust root claimed value for this hotkey and coldkey. @@ -727,7 +740,12 @@ impl Pallet { // } // Record TAO outflow - Self::record_tao_outflow(netuid, swap_result.amount_paid_out.into()); + Self::record_tao_outflow( + netuid, + swap_result + .amount_paid_out + .saturating_add(fee_outflow.into()), + ); LastColdkeyHotkeyStakeBlock::::insert(coldkey, hotkey, Self::get_current_block_as_u64()); @@ -803,6 +821,21 @@ impl Pallet { StakingHotkeys::::insert(coldkey, staking_hotkeys.clone()); } + // Increase the balance of the block author + let maybe_block_author_coldkey = T::AuthorshipProvider::author(); + if let Some(block_author_coldkey) = maybe_block_author_coldkey { + Self::add_balance_to_coldkey_account( + &block_author_coldkey, + swap_result.fee_to_block_author.into(), + ); + } else { + // Block author is not found - burn this TAO + // Pallet balances total issuance was taken care of when balance was withdrawn for this swap + TotalIssuance::::mutate(|ti| { + *ti = ti.saturating_sub(swap_result.fee_to_block_author); + }); + } + // Record TAO inflow Self::record_tao_inflow(netuid, swap_result.amount_paid_in.into()); @@ -890,7 +923,7 @@ impl Pallet { let current_price = ::SwapInterface::current_alpha_price(netuid.into()); let tao_equivalent: TaoCurrency = current_price - .saturating_mul(U96F32::saturating_from_num(actual_alpha_moved)) + .saturating_mul(U64F64::saturating_from_num(actual_alpha_moved)) .saturating_to_num::() .into(); @@ -1219,42 +1252,34 @@ impl Pallet { } pub fn increase_provided_tao_reserve(netuid: NetUid, tao: TaoCurrency) { - SubnetTaoProvided::::mutate(netuid, |total| { - *total = total.saturating_add(tao); - }); + if !tao.is_zero() { + SubnetTAO::::mutate(netuid, |total| { + *total = total.saturating_add(tao); + }); + } } pub fn decrease_provided_tao_reserve(netuid: NetUid, tao: TaoCurrency) { - // First, decrease SubnetTaoProvided, then deduct the rest from SubnetTAO - let subnet_tao = SubnetTAO::::get(netuid); - let subnet_tao_provided = SubnetTaoProvided::::get(netuid); - let remainder = subnet_tao_provided.saturating_sub(tao); - let carry_over = tao.saturating_sub(subnet_tao_provided); - if carry_over.is_zero() { - SubnetTaoProvided::::set(netuid, remainder); - } else { - SubnetTaoProvided::::set(netuid, TaoCurrency::ZERO); - SubnetTAO::::set(netuid, subnet_tao.saturating_sub(carry_over)); + if !tao.is_zero() { + SubnetTAO::::mutate(netuid, |total| { + *total = total.saturating_sub(tao); + }); } } pub fn increase_provided_alpha_reserve(netuid: NetUid, alpha: AlphaCurrency) { - SubnetAlphaInProvided::::mutate(netuid, |total| { - *total = total.saturating_add(alpha); - }); + if !alpha.is_zero() { + SubnetAlphaIn::::mutate(netuid, |total| { + *total = total.saturating_add(alpha); + }); + } } pub fn decrease_provided_alpha_reserve(netuid: NetUid, alpha: AlphaCurrency) { - // First, decrease SubnetAlphaInProvided, then deduct the rest from SubnetAlphaIn - let subnet_alpha = SubnetAlphaIn::::get(netuid); - let subnet_alpha_provided = SubnetAlphaInProvided::::get(netuid); - let remainder = subnet_alpha_provided.saturating_sub(alpha); - let carry_over = alpha.saturating_sub(subnet_alpha_provided); - if carry_over.is_zero() { - SubnetAlphaInProvided::::set(netuid, remainder); - } else { - SubnetAlphaInProvided::::set(netuid, AlphaCurrency::ZERO); - SubnetAlphaIn::::set(netuid, subnet_alpha.saturating_sub(carry_over)); + if !alpha.is_zero() { + SubnetAlphaIn::::mutate(netuid, |total| { + *total = total.saturating_sub(alpha); + }); } } diff --git a/pallets/subtensor/src/subnets/mechanism.rs b/pallets/subtensor/src/subnets/mechanism.rs index 481974ef05..55e459c9ba 100644 --- a/pallets/subtensor/src/subnets/mechanism.rs +++ b/pallets/subtensor/src/subnets/mechanism.rs @@ -95,7 +95,20 @@ impl Pallet { Ok(()) } - /// Set the desired valus of sub-subnet count for a subnet identified + pub fn ensure_max_uids_over_all_mechanisms( + max_uids: u16, + mechanism_count: MechId, + ) -> DispatchResult { + let max_uids_over_all_mechanisms = + max_uids.saturating_mul(u8::from(mechanism_count) as u16); + ensure!( + max_uids_over_all_mechanisms <= DefaultMaxAllowedUids::::get(), + Error::::TooManyUIDsPerMechanism + ); + Ok(()) + } + + /// Set the desired value of mechanism count for a subnet identified /// by netuid pub fn do_set_mechanism_count(netuid: NetUid, mechanism_count: MechId) -> DispatchResult { // Make sure the subnet exists @@ -113,6 +126,10 @@ impl Pallet { Error::::InvalidValue ); + // Prevent chain bloat: Require max UIDs to be limited + let max_uids = MaxAllowedUids::::get(netuid); + Self::ensure_max_uids_over_all_mechanisms(max_uids, mechanism_count)?; + // Make sure we are not allowing numbers that will break the math ensure!( mechanism_count <= MechId::from(MAX_MECHANISM_COUNT_PER_SUBNET), @@ -124,6 +141,22 @@ impl Pallet { Ok(()) } + /// Set the global maximum number of mechanisms per subnet + pub fn do_set_max_mechanism_count(max_mechanism_count: MechId) -> DispatchResult { + // Max count cannot be zero + ensure!(max_mechanism_count > 0.into(), Error::::InvalidValue); + + // Make sure we are not allowing numbers that will break the math + ensure!( + max_mechanism_count <= MechId::from(MAX_MECHANISM_COUNT_PER_SUBNET), + Error::::InvalidValue + ); + + MaxMechanismCount::::set(max_mechanism_count); + + Ok(()) + } + /// Update current count for a subnet identified by netuid /// - Cleans up all sub-subnet maps if count is reduced /// @@ -322,6 +355,7 @@ impl Pallet { sub_weight, ); acc_terms.new_validator_permit |= terms.new_validator_permit; + acc_terms.stake = acc_terms.stake.saturating_add(terms.stake); }) .or_insert_with(|| { // weighted insert for the first sub-subnet seen for this hotkey @@ -349,7 +383,8 @@ impl Pallet { sub_weight, ), new_validator_permit: terms.new_validator_permit, - bond: Vec::new(), // aggregated map doesn’t use bonds; keep empty + bond: Vec::new(), // aggregated map doesn't use bonds; keep empty + stake: terms.stake, } }); acc @@ -358,6 +393,9 @@ impl Pallet { // State updates from epoch function Self::persist_netuid_epoch_terms(netuid, &aggregated); + // Update voting power EMA for all validators on this subnet + Self::update_voting_power_for_subnet(netuid, &aggregated); + // Remap BTreeMap back to Vec<(T::AccountId, AlphaCurrency, AlphaCurrency)> format // for processing emissions in run_coinbase // Emission tuples ( hotkeys, server_emission, validator_emission ) diff --git a/pallets/subtensor/src/subnets/subnet.rs b/pallets/subtensor/src/subnets/subnet.rs index ecb5ce0452..86462dd06d 100644 --- a/pallets/subtensor/src/subnets/subnet.rs +++ b/pallets/subtensor/src/subnets/subnet.rs @@ -217,8 +217,6 @@ impl Pallet { SubnetOwner::::insert(netuid_to_register, coldkey.clone()); SubnetOwnerHotkey::::insert(netuid_to_register, hotkey.clone()); SubnetLocked::::insert(netuid_to_register, actual_tao_lock_amount); - SubnetTaoProvided::::insert(netuid_to_register, TaoCurrency::ZERO); - SubnetAlphaInProvided::::insert(netuid_to_register, AlphaCurrency::ZERO); SubnetAlphaOut::::insert(netuid_to_register, AlphaCurrency::ZERO); SubnetVolume::::insert(netuid_to_register, 0u128); RAORecycledForRegistration::::insert( diff --git a/pallets/subtensor/src/swap/swap_coldkey.rs b/pallets/subtensor/src/swap/swap_coldkey.rs index c81138b58c..54b07d9dbf 100644 --- a/pallets/subtensor/src/swap/swap_coldkey.rs +++ b/pallets/subtensor/src/swap/swap_coldkey.rs @@ -1,228 +1,134 @@ use super::*; -use frame_support::weights::Weight; -use sp_core::Get; use substrate_fixed::types::U64F64; impl Pallet { - /// Swaps the coldkey associated with a set of hotkeys from an old coldkey to a new coldkey. - /// - /// # Arguments - /// - /// * `origin` - The origin of the call, which must be signed by the old coldkey. - /// * `new_coldkey` - The account ID of the new coldkey. - /// - /// # Returns - /// - /// Returns a `DispatchResultWithPostInfo` indicating success or failure, along with the weight consumed. - /// - /// # Errors - /// - /// This function will return an error if: - /// - The caller is not a valid signed origin. - /// - The old coldkey (caller) is in arbitration. - /// - The new coldkey is already associated with other hotkeys or is a hotkey itself. - /// - There's not enough balance to pay for the swap. - /// - /// # Events - /// - /// Emits a `ColdkeySwapped` event when successful. - /// - /// # Weight - /// - /// Weight is tracked and updated throughout the function execution. + /// Transfer all assets, stakes, subnet ownerships, and hotkey associations from `old_coldkey` to + /// to `new_coldkey`. pub fn do_swap_coldkey( old_coldkey: &T::AccountId, new_coldkey: &T::AccountId, - swap_cost: TaoCurrency, - ) -> DispatchResultWithPostInfo { - // 2. Initialize the weight for this operation - let mut weight: Weight = T::DbWeight::get().reads(2); - // 3. Ensure the new coldkey is not associated with any hotkeys + ) -> DispatchResult { ensure!( StakingHotkeys::::get(new_coldkey).is_empty(), Error::::ColdKeyAlreadyAssociated ); - weight = weight.saturating_add(T::DbWeight::get().reads(1)); - - // 4. Ensure the new coldkey is not a hotkey ensure!( !Self::hotkey_account_exists(new_coldkey), Error::::NewColdKeyIsHotkey ); - weight = weight.saturating_add(T::DbWeight::get().reads(1)); - // 5. Swap the identity if the old coldkey has one - if let Some(identity) = IdentitiesV2::::take(old_coldkey) { - IdentitiesV2::::insert(new_coldkey, identity); + // Swap the identity if the old coldkey has one and the new coldkey doesn't + if IdentitiesV2::::get(new_coldkey).is_none() + && let Some(identity) = IdentitiesV2::::take(old_coldkey) + { + IdentitiesV2::::insert(new_coldkey.clone(), identity); } - // 6. Ensure sufficient balance for the swap cost - ensure!( - Self::can_remove_balance_from_coldkey_account(old_coldkey, swap_cost.into()), - Error::::NotEnoughBalanceToPaySwapColdKey - ); - - // 7. Remove and recycle the swap cost from the old coldkey's account - let actual_burn_amount = - Self::remove_balance_from_coldkey_account(old_coldkey, swap_cost.into())?; - Self::recycle_tao(actual_burn_amount); - - // 8. Update the weight for the balance operations - weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 1)); + for netuid in Self::get_all_subnet_netuids() { + Self::transfer_subnet_ownership(netuid, old_coldkey, new_coldkey); + Self::transfer_auto_stake_destination(netuid, old_coldkey, new_coldkey); + Self::transfer_coldkey_stake(netuid, old_coldkey, new_coldkey); + } + Self::transfer_staking_hotkeys(old_coldkey, new_coldkey); + Self::transfer_hotkeys_ownership(old_coldkey, new_coldkey); - // 9. Perform the actual coldkey swap - let _ = Self::perform_swap_coldkey(old_coldkey, new_coldkey, &mut weight); + // Transfer any remaining balance from old_coldkey to new_coldkey + let remaining_balance = Self::get_coldkey_balance(old_coldkey); + if remaining_balance > 0 { + Self::kill_coldkey_account(old_coldkey, remaining_balance)?; + Self::add_balance_to_coldkey_account(new_coldkey, remaining_balance); + } - // 10. Update the last transaction block for the new coldkey Self::set_last_tx_block(new_coldkey, Self::get_current_block_as_u64()); - weight.saturating_accrue(T::DbWeight::get().writes(1)); - // 11. Remove the coldkey swap scheduled record - ColdkeySwapScheduled::::remove(old_coldkey); - - // 12. Emit the ColdkeySwapped event Self::deposit_event(Event::ColdkeySwapped { old_coldkey: old_coldkey.clone(), new_coldkey: new_coldkey.clone(), - swap_cost, }); + Ok(()) + } + + /// Charges the swap cost from the coldkey's account and recycles the tokens. + pub fn charge_swap_cost(coldkey: &T::AccountId, swap_cost: TaoCurrency) -> DispatchResult { + let burn_amount = Self::remove_balance_from_coldkey_account(coldkey, swap_cost.into()) + .map_err(|_| Error::::NotEnoughBalanceToPaySwapColdKey)?; + + if burn_amount < swap_cost { + return Err(Error::::NotEnoughBalanceToPaySwapColdKey.into()); + } + + Self::recycle_tao(burn_amount); - // 12. Return the result with the updated weight - Ok(Some(weight).into()) + Ok(()) } - /// Performs the actual coldkey swap operation, transferring all associated data and balances from the old coldkey to the new coldkey. - /// - /// # Arguments - /// - /// * `old_coldkey` - The account ID of the old coldkey. - /// * `new_coldkey` - The account ID of the new coldkey. - /// * `weight` - A mutable reference to the current transaction weight. - /// - /// # Returns - /// - /// Returns a `DispatchResult` indicating success or failure of the operation. - /// - /// # Steps - /// - /// 1. Swap TotalHotkeyColdkeyStakesThisInterval: - /// - For each hotkey owned by the old coldkey, transfer its stake and block data to the new coldkey. - /// - /// 2. Swap subnet ownership: - /// - For each subnet, if the old coldkey is the owner, transfer ownership to the new coldkey. - /// - /// 3. Swap Stakes: - /// - For each hotkey staking for the old coldkey, transfer its stake to the new coldkey. - /// - /// 4. Swap total coldkey stake: - /// - Transfer the total stake from the old coldkey to the new coldkey. - /// - /// 5. Swap StakingHotkeys: - /// - Transfer the list of staking hotkeys from the old coldkey to the new coldkey. - /// - /// 6. Swap hotkey owners: - /// - For each hotkey owned by the old coldkey, transfer ownership to the new coldkey. - /// - Update the list of owned hotkeys for both old and new coldkeys. - /// - /// 7. Transfer remaining balance: - /// - Transfer any remaining balance from the old coldkey to the new coldkey. - /// - /// Throughout the process, the function updates the transaction weight to reflect the operations performed. - /// - /// # Notes - /// - /// This function is a critical part of the coldkey swap process and should be called only after all necessary checks and validations have been performed. - pub fn perform_swap_coldkey( + /// Transfer the ownership of the subnet to the new coldkey if it is owned by the old coldkey. + fn transfer_subnet_ownership( + netuid: NetUid, old_coldkey: &T::AccountId, new_coldkey: &T::AccountId, - weight: &mut Weight, - ) -> DispatchResult { - // 1. Swap TotalHotkeyColdkeyStakesThisInterval - // TotalHotkeyColdkeyStakesThisInterval: MAP ( hotkey, coldkey ) --> ( stake, block ) | Stake of the hotkey for the coldkey. - // for hotkey in OwnedHotkeys::::get(old_coldkey).iter() { - // let (stake, block) = - // TotalHotkeyColdkeyStakesThisInterval::::get(&hotkey, old_coldkey); - // TotalHotkeyColdkeyStakesThisInterval::::remove(&hotkey, old_coldkey); - // TotalHotkeyColdkeyStakesThisInterval::::insert(&hotkey, new_coldkey, (stake, block)); - // weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 2)); - // } (DEPRECATED) - - // 2. Swap subnet owner. - // SubnetOwner: MAP ( netuid ) --> (coldkey) | Owner of the subnet. - for netuid in Self::get_all_subnet_netuids() { - let subnet_owner = SubnetOwner::::get(netuid); - if subnet_owner == *old_coldkey { - SubnetOwner::::insert(netuid, new_coldkey.clone()); - } - weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 1)); + ) { + let subnet_owner = SubnetOwner::::get(netuid); + if subnet_owner == *old_coldkey { + SubnetOwner::::insert(netuid, new_coldkey.clone()); + } + } - if let Some(old_auto_stake_hotkey) = AutoStakeDestination::::get(old_coldkey, netuid) - { - AutoStakeDestination::::remove(old_coldkey, netuid); - AutoStakeDestination::::insert( - new_coldkey, - netuid, - old_auto_stake_hotkey.clone(), - ); - AutoStakeDestinationColdkeys::::mutate(old_auto_stake_hotkey, netuid, |v| { - // Remove old/new coldkeys (avoid duplicates), then add the new one. - v.retain(|c| *c != *old_coldkey && *c != *new_coldkey); - v.push(new_coldkey.clone()); - }); - } + /// Transfer the auto stake destination from the old coldkey to the new coldkey if it is set. + fn transfer_auto_stake_destination( + netuid: NetUid, + old_coldkey: &T::AccountId, + new_coldkey: &T::AccountId, + ) { + if let Some(old_auto_stake_hotkey) = AutoStakeDestination::::get(old_coldkey, netuid) { + AutoStakeDestination::::remove(old_coldkey, netuid); + AutoStakeDestination::::insert(new_coldkey, netuid, old_auto_stake_hotkey.clone()); + AutoStakeDestinationColdkeys::::mutate(old_auto_stake_hotkey, netuid, |v| { + // Remove old/new coldkeys (avoid duplicates), then add the new one. + v.retain(|c| *c != *old_coldkey && *c != *new_coldkey); + v.push(new_coldkey.clone()); + }); } + } - // 3. Swap Stake. - // StakingHotkeys: MAP ( coldkey ) --> Vec( hotkey ) + /// Transfer the stake of all staking hotkeys linked to the old coldkey to the new coldkey. + fn transfer_coldkey_stake( + netuid: NetUid, + old_coldkey: &T::AccountId, + new_coldkey: &T::AccountId, + ) { for hotkey in StakingHotkeys::::get(old_coldkey) { - // 3.1 Swap Alpha - for netuid in Self::get_all_subnet_netuids() { - // Get the stake on the old (hot,coldkey) account. - let old_alpha: U64F64 = Alpha::::get((&hotkey, old_coldkey, netuid)); - // Get the stake on the new (hot,coldkey) account. - let new_alpha: U64F64 = Alpha::::get((&hotkey, new_coldkey, netuid)); - // Add the stake to new account. - Alpha::::insert( - (&hotkey, new_coldkey, netuid), - new_alpha.saturating_add(old_alpha), + // Get the stake on the old (hot,coldkey) account. + let old_alpha: U64F64 = Alpha::::get((&hotkey, old_coldkey, netuid)); + // Get the stake on the new (hot,coldkey) account. + let new_alpha: U64F64 = Alpha::::get((&hotkey, new_coldkey, netuid)); + // Add the stake to new account. + Alpha::::insert( + (&hotkey, new_coldkey, netuid), + new_alpha.saturating_add(old_alpha), + ); + // Remove the value from the old account. + Alpha::::remove((&hotkey, old_coldkey, netuid)); + + if new_alpha.saturating_add(old_alpha) > U64F64::from(0u64) { + Self::transfer_root_claimed_for_new_keys( + netuid, + &hotkey, + &hotkey, + old_coldkey, + new_coldkey, ); - // Remove the value from the old account. - Alpha::::remove((&hotkey, old_coldkey, netuid)); - - if new_alpha.saturating_add(old_alpha) > U64F64::from(0u64) { - Self::transfer_root_claimed_for_new_keys( - netuid, - &hotkey, - &hotkey, - old_coldkey, - new_coldkey, - ); - if netuid == NetUid::ROOT { - // Register new coldkey with root stake - Self::maybe_add_coldkey_index(new_coldkey); - } + if netuid == NetUid::ROOT { + // Register new coldkey with root stake + Self::maybe_add_coldkey_index(new_coldkey); } } - // Add the weight for the read and write. - weight.saturating_accrue(T::DbWeight::get().reads_writes(2, 2)); } + } - // 4. Swap TotalColdkeyAlpha (DEPRECATED) - // for netuid in Self::get_all_subnet_netuids() { - // let old_alpha_stake: u64 = TotalColdkeyAlpha::::get(old_coldkey, netuid); - // let new_alpha_stake: u64 = TotalColdkeyAlpha::::get(new_coldkey, netuid); - // TotalColdkeyAlpha::::insert( - // new_coldkey, - // netuid, - // new_alpha_stake.saturating_add(old_alpha_stake), - // ); - // TotalColdkeyAlpha::::remove(old_coldkey, netuid); - // } - // weight.saturating_accrue(T::DbWeight::get().reads_writes(2, 2)); - - // 5. Swap StakingHotkeys. - // StakingHotkeys: MAP ( coldkey ) --> Vec | Hotkeys staking for the coldkey. + /// Transfer staking hotkeys from the old coldkey to the new coldkey. + fn transfer_staking_hotkeys(old_coldkey: &T::AccountId, new_coldkey: &T::AccountId) { let old_staking_hotkeys: Vec = StakingHotkeys::::get(old_coldkey); let mut new_staking_hotkeys: Vec = StakingHotkeys::::get(new_coldkey); for hotkey in old_staking_hotkeys { @@ -231,13 +137,13 @@ impl Pallet { new_staking_hotkeys.push(hotkey); } } + StakingHotkeys::::remove(old_coldkey); StakingHotkeys::::insert(new_coldkey, new_staking_hotkeys); - weight.saturating_accrue(T::DbWeight::get().reads_writes(2, 2)); + } - // 6. Swap hotkey owners. - // Owner: MAP ( hotkey ) --> coldkey | Owner of the hotkey. - // OwnedHotkeys: MAP ( coldkey ) --> Vec | Hotkeys owned by the coldkey. + /// Transfer the ownership of the hotkeys owned by the old coldkey to the new coldkey. + fn transfer_hotkeys_ownership(old_coldkey: &T::AccountId, new_coldkey: &T::AccountId) { let old_owned_hotkeys: Vec = OwnedHotkeys::::get(old_coldkey); let mut new_owned_hotkeys: Vec = OwnedHotkeys::::get(new_coldkey); for owned_hotkey in old_owned_hotkeys.iter() { @@ -252,19 +158,5 @@ impl Pallet { } OwnedHotkeys::::remove(old_coldkey); OwnedHotkeys::::insert(new_coldkey, new_owned_hotkeys); - weight.saturating_accrue(T::DbWeight::get().reads_writes(2, 2)); - - // 7. Transfer remaining balance. - // Balance: MAP ( coldkey ) --> u64 | Balance of the coldkey. - // Transfer any remaining balance from old_coldkey to new_coldkey - let remaining_balance = Self::get_coldkey_balance(old_coldkey); - if remaining_balance > 0 { - Self::kill_coldkey_account(old_coldkey, remaining_balance)?; - Self::add_balance_to_coldkey_account(new_coldkey, remaining_balance); - } - weight.saturating_accrue(T::DbWeight::get().reads_writes(2, 2)); - - // Return ok. - Ok(()) } } diff --git a/pallets/subtensor/src/swap/swap_hotkey.rs b/pallets/subtensor/src/swap/swap_hotkey.rs index 4fdf87fb7b..a54a02a750 100644 --- a/pallets/subtensor/src/swap/swap_hotkey.rs +++ b/pallets/subtensor/src/swap/swap_hotkey.rs @@ -493,6 +493,11 @@ impl Pallet { // 8.3 Swap TaoDividendsPerSubnet // Tao dividends were removed + // 8.4 Swap VotingPower + // VotingPower( netuid, hotkey ) --> u64 -- the voting power EMA for the hotkey. + Self::swap_voting_power_for_hotkey(old_hotkey, new_hotkey, netuid); + weight.saturating_accrue(T::DbWeight::get().reads_writes(2, 2)); + // 9. Swap Alpha // Alpha( hotkey, coldkey, netuid ) -> alpha let old_alpha_values: Vec<((T::AccountId, NetUid), U64F64)> = diff --git a/pallets/subtensor/src/tests/claim_root.rs b/pallets/subtensor/src/tests/claim_root.rs index 717f3a5d28..9d08c65e3c 100644 --- a/pallets/subtensor/src/tests/claim_root.rs +++ b/pallets/subtensor/src/tests/claim_root.rs @@ -19,7 +19,7 @@ use frame_support::{assert_err, assert_noop, assert_ok}; use sp_core::{H256, U256}; use sp_runtime::DispatchError; use std::collections::BTreeSet; -use substrate_fixed::types::{I96F32, U64F64, U96F32}; +use substrate_fixed::types::{I96F32, U96F32}; use subtensor_runtime_common::{AlphaCurrency, Currency, NetUid, TaoCurrency}; use subtensor_swap_interface::SwapHandler; @@ -758,6 +758,7 @@ fn test_claim_root_with_drain_emissions_and_swap_claim_type() { }); } +/// cargo test --package pallet-subtensor --lib -- tests::claim_root::test_claim_root_with_run_coinbase --exact --nocapture #[test] fn test_claim_root_with_run_coinbase() { new_test_ext(1).execute_with(|| { @@ -790,10 +791,15 @@ fn test_claim_root_with_run_coinbase() { // Set moving price > 1.0 and price > 1.0 // So we turn ON root sell SubnetMovingPrice::::insert(netuid, I96F32::from_num(2)); - pallet_subtensor_swap::AlphaSqrtPrice::::insert( - netuid, - U64F64::saturating_from_num(10.0), - ); + let tao = TaoCurrency::from(10_000_000_000_000_u64); + let alpha = AlphaCurrency::from(1_000_000_000_000_u64); + SubnetTAO::::insert(netuid, tao); + SubnetAlphaIn::::insert(netuid, alpha); + let current_price = + ::SwapInterface::current_alpha_price(netuid.into()) + .saturating_to_num::(); + assert_eq!(current_price, 10.0f64); + RootClaimableThreshold::::insert(netuid, I96F32::from_num(0)); // Make sure we are root selling, so we have root alpha divs. let root_sell_flag = SubtensorModule::get_network_root_sell_flag(&[netuid]); @@ -901,10 +907,15 @@ fn test_claim_root_with_block_emissions() { // Set moving price > 1.0 and price > 1.0 // So we turn ON root sell SubnetMovingPrice::::insert(netuid, I96F32::from_num(2)); - pallet_subtensor_swap::AlphaSqrtPrice::::insert( - netuid, - U64F64::saturating_from_num(10.0), - ); + let tao = TaoCurrency::from(10_000_000_000_000_u64); + let alpha = AlphaCurrency::from(1_000_000_000_000_u64); + SubnetTAO::::insert(netuid, tao); + SubnetAlphaIn::::insert(netuid, alpha); + let current_price = + ::SwapInterface::current_alpha_price(netuid.into()) + .saturating_to_num::(); + assert_eq!(current_price, 10.0f64); + RootClaimableThreshold::::insert(netuid, I96F32::from_num(0)); // Make sure we are root selling, so we have root alpha divs. let root_sell_flag = SubtensorModule::get_network_root_sell_flag(&[netuid]); @@ -1020,16 +1031,21 @@ fn test_claim_root_coinbase_distribution() { initial_total_hotkey_alpha.into(), ); - let initial_alpha_issuance = SubtensorModule::get_alpha_issuance(netuid); - let alpha_emissions: AlphaCurrency = 1_000_000_000u64.into(); - // Set moving price > 1.0 and price > 1.0 // So we turn ON root sell SubnetMovingPrice::::insert(netuid, I96F32::from_num(2)); - pallet_subtensor_swap::AlphaSqrtPrice::::insert( - netuid, - U64F64::saturating_from_num(10.0), - ); + let tao = TaoCurrency::from(100_000_000_000_u64); + let alpha = AlphaCurrency::from(100_000_000_000_u64); + SubnetTAO::::insert(netuid, tao); + SubnetAlphaIn::::insert(netuid, alpha); + // let current_price = + // ::SwapInterface::current_alpha_price(netuid.into()) + // .saturating_to_num::(); + // assert_eq!(current_price, 2.0f64); + RootClaimableThreshold::::insert(netuid, I96F32::from_num(0)); + + let initial_alpha_issuance = SubtensorModule::get_alpha_issuance(netuid); + let alpha_emissions: AlphaCurrency = 1_000_000_000u64.into(); // Make sure we are root selling, so we have root alpha divs. let root_sell_flag = SubtensorModule::get_network_root_sell_flag(&[netuid]); @@ -1184,13 +1200,7 @@ fn test_claim_root_with_swap_coldkey() { ); // Swap coldkey - let mut weight = Weight::zero(); - - assert_ok!(SubtensorModule::perform_swap_coldkey( - &coldkey, - &new_coldkey, - &mut weight - )); + assert_ok!(SubtensorModule::do_swap_coldkey(&coldkey, &new_coldkey,)); // Check swapped keys claimed values diff --git a/pallets/subtensor/src/tests/coinbase.rs b/pallets/subtensor/src/tests/coinbase.rs index a79f4b713a..4e4caf2e9c 100644 --- a/pallets/subtensor/src/tests/coinbase.rs +++ b/pallets/subtensor/src/tests/coinbase.rs @@ -12,7 +12,6 @@ use crate::*; use alloc::collections::BTreeMap; use approx::assert_abs_diff_eq; use frame_support::assert_ok; -use pallet_subtensor_swap::position::PositionId; use sp_core::U256; use substrate_fixed::{ transcendental::sqrt, @@ -46,36 +45,6 @@ fn test_hotkey_take() { }); } -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::test_dynamic_function_various_values --exact --show-output --nocapture -#[test] -fn test_dynamic_function_various_values() { - new_test_ext(1).execute_with(|| { - let price_values: [f64; 9] = [0.001, 0.1, 0.5, 1.0, 2.0, 10.0, 100.0, 200.0, 1000.0]; - let tao_in_values: [u64; 9] = [0, 1, 10, 100, 1_000, 1_000_000, 1_000_000_000, 1_000_000_000_000, 1_000_000_000_000_000 ]; - let alpha_emission_values: [u64; 9] = [0, 1, 10, 100, 1_000, 1_000_000, 1_000_000_000, 1_000_000_000_000, 1_000_000_000_000_000 ]; - - for &price in price_values.iter() { - for &tao_in in tao_in_values.iter() { - for &alpha_emission in alpha_emission_values.iter() { - // Set the price. - SubnetMechanism::::insert(NetUid::from(1), 1); - SubnetTAO::::insert(NetUid::from(1), TaoCurrency::from((price * 1_000_000_000.0) as u64)); - SubnetAlphaIn::::insert(NetUid::from(1), AlphaCurrency::from(1_000_000_000)); - let (tao_in_emission, alpha_in_emission, alpha_out_emission) = SubtensorModule::get_dynamic_tao_emission(1.into(), tao_in, alpha_emission); - assert!(tao_in_emission <= tao_in, "tao_in_emission is greater than tao_in"); - assert!(alpha_in_emission <= alpha_emission, "alpha_in_emission is greater than alpha_emission"); - assert!(alpha_out_emission <= 2 * alpha_emission, "alpha_out_emission is greater than 2 * alpha_emission"); - assert!((alpha_in_emission + alpha_out_emission) <= 2 * alpha_emission, "Sum of alpha_in_emission and alpha_out_emission is less than or equal to. 2 * alpha_emission"); - close( alpha_in_emission + alpha_out_emission, alpha_in_emission + alpha_emission, 10 ); - // if alpha_in_emission > 0 || tao_in_emission > 0 { - // assert!((tao_in_emission as f64 / alpha_in_emission as f64 - price).abs() < 1e-1, "Ratio of tao_in_emission to alpha_in_emission is not equal to price"); - // } - } - } - } - }); -} - // Test the base case of running coinbase with zero emission. // This test verifies that the coinbase mechanism can handle the edge case // of zero emission without errors or unexpected behavior. @@ -222,20 +191,8 @@ fn test_coinbase_tao_issuance_different_prices() { mock::setup_reserves(netuid2, initial_tao.into(), initial_alpha2.into()); // Force the swap to initialize - SubtensorModule::swap_tao_for_alpha( - netuid1, - TaoCurrency::ZERO, - 1_000_000_000_000.into(), - false, - ) - .unwrap(); - SubtensorModule::swap_tao_for_alpha( - netuid2, - TaoCurrency::ZERO, - 1_000_000_000_000.into(), - false, - ) - .unwrap(); + ::SwapInterface::init_swap(netuid1, None); + ::SwapInterface::init_swap(netuid2, None); // Make subnets dynamic. SubnetMechanism::::insert(netuid1, 1); @@ -298,20 +255,8 @@ fn test_coinbase_tao_issuance_different_prices() { // mock::setup_reserves(netuid2, initial_tao.into(), initial_alpha2.into()); // // Force the swap to initialize -// SubtensorModule::swap_tao_for_alpha( -// netuid1, -// TaoCurrency::ZERO, -// 1_000_000_000_000.into(), -// false, -// ) -// .unwrap(); -// SubtensorModule::swap_tao_for_alpha( -// netuid2, -// TaoCurrency::ZERO, -// 1_000_000_000_000.into(), -// false, -// ) -// .unwrap(); +// ::SwapInterface::init_swap(netuid1); +// ::SwapInterface::init_swap(netuid2); // // Set subnet prices to reversed proportion to ensure they don't affect emissions. // SubnetMovingPrice::::insert(netuid1, I96F32::from_num(2)); @@ -616,20 +561,8 @@ fn test_coinbase_alpha_issuance_with_cap_trigger_and_block_emission() { SubnetTaoFlow::::insert(netuid2, 200_000_000_i64); // Force the swap to initialize - SubtensorModule::swap_tao_for_alpha( - netuid1, - TaoCurrency::ZERO, - 1_000_000_000_000.into(), - false, - ) - .unwrap(); - SubtensorModule::swap_tao_for_alpha( - netuid2, - TaoCurrency::ZERO, - 1_000_000_000_000.into(), - false, - ) - .unwrap(); + ::SwapInterface::init_swap(netuid1, None); + ::SwapInterface::init_swap(netuid2, None); // Get the prices before the run_coinbase let price_1_before = ::SwapInterface::current_alpha_price(netuid1); @@ -2729,54 +2662,6 @@ fn test_run_coinbase_not_started_start_after() { }); } -/// Test that coinbase updates protocol position liquidity -/// cargo test --package pallet-subtensor --lib -- tests::coinbase::test_coinbase_v3_liquidity_update --exact --show-output -#[test] -fn test_coinbase_v3_liquidity_update() { - new_test_ext(1).execute_with(|| { - let owner_hotkey = U256::from(1); - let owner_coldkey = U256::from(2); - - // add network - let netuid = add_dynamic_network(&owner_hotkey, &owner_coldkey); - - // Force the swap to initialize - SubtensorModule::swap_tao_for_alpha( - netuid, - TaoCurrency::ZERO, - 1_000_000_000_000.into(), - false, - ) - .unwrap(); - - let protocol_account_id = pallet_subtensor_swap::Pallet::::protocol_account_id(); - let position = pallet_subtensor_swap::Positions::::get(( - netuid, - protocol_account_id, - PositionId::from(1), - )) - .unwrap(); - let liquidity_before = position.liquidity; - - // Enable emissions and run coinbase (which will increase position liquidity) - let emission: u64 = 1_234_567; - // Set the TAO flow to non-zero - SubnetTaoFlow::::insert(netuid, 8348383_i64); - FirstEmissionBlockNumber::::insert(netuid, 0); - SubtensorModule::run_coinbase(U96F32::from_num(emission)); - - let position_after = pallet_subtensor_swap::Positions::::get(( - netuid, - protocol_account_id, - PositionId::from(1), - )) - .unwrap(); - let liquidity_after = position_after.liquidity; - - assert!(liquidity_before < liquidity_after); - }); -} - // SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::test_drain_alpha_childkey_parentkey_with_burn --exact --show-output --nocapture #[test] fn test_drain_alpha_childkey_parentkey_with_burn() { @@ -3070,10 +2955,8 @@ fn test_mining_emission_distribution_with_no_root_sell() { // Make root sell NOT happen // set price very low, e.g. a lot of alpha in //SubnetAlphaIn::::insert(netuid, AlphaCurrency::from(1_000_000_000_000_000)); - pallet_subtensor_swap::AlphaSqrtPrice::::insert( - netuid, - U64F64::saturating_from_num(0.01), - ); + let alpha = AlphaCurrency::from(1_000_000_000_000_000_000_u64); + SubnetAlphaIn::::insert(netuid, alpha); // Make sure we ARE NOT root selling, so we do not have root alpha divs. let root_sell_flag = SubtensorModule::get_network_root_sell_flag(&[netuid]); @@ -3265,10 +3148,8 @@ fn test_mining_emission_distribution_with_root_sell() { // Make root sell happen // Set moving price > 1.0 // Set price > 1.0 - pallet_subtensor_swap::AlphaSqrtPrice::::insert( - netuid, - U64F64::saturating_from_num(10.0), - ); + let alpha = AlphaCurrency::from(100_000_000_000_000); + SubnetAlphaIn::::insert(netuid, alpha); SubnetMovingPrice::::insert(netuid, I96F32::from_num(2)); @@ -3394,7 +3275,7 @@ fn test_coinbase_subnet_terms_with_alpha_in_gt_alpha_emission() { AlphaCurrency::from(1_000_000_000_000_000), ); // Initialize swap v3 - Swap::maybe_initialize_v3(netuid0); + Swap::maybe_initialize_palswap(netuid0, None); // Set netuid0 to have price tao_emission / price > alpha_emission let alpha_emission = U96F32::saturating_from_num( @@ -3405,14 +3286,19 @@ fn test_coinbase_subnet_terms_with_alpha_in_gt_alpha_emission() { ); let price_to_set: U64F64 = U64F64::saturating_from_num(0.01); let price_to_set_fixed: U96F32 = U96F32::saturating_from_num(price_to_set); - let sqrt_price_to_set: U64F64 = sqrt(price_to_set).unwrap(); let tao_emission: U96F32 = U96F32::saturating_from_num(alpha_emission) .saturating_mul(price_to_set_fixed) .saturating_add(U96F32::saturating_from_num(0.01)); // Set the price - pallet_subtensor_swap::AlphaSqrtPrice::::insert(netuid0, sqrt_price_to_set); + let tao = TaoCurrency::from(1_000_000_000_u64); + let alpha = AlphaCurrency::from( + (U64F64::saturating_from_num(u64::from(tao)) / price_to_set).to_num::(), + ); + SubnetTAO::::insert(netuid0, tao); + SubnetAlphaIn::::insert(netuid0, alpha); + // Check the price is set assert_abs_diff_eq!( pallet_subtensor_swap::Pallet::::current_alpha_price(netuid0).to_num::(), @@ -3470,7 +3356,7 @@ fn test_coinbase_subnet_terms_with_alpha_in_lte_alpha_emission() { AlphaCurrency::from(1_000_000_000_000_000), ); // Initialize swap v3 - Swap::maybe_initialize_v3(netuid0); + Swap::maybe_initialize_palswap(netuid0, None); let alpha_emission = U96F32::saturating_from_num( SubtensorModule::get_block_emission_for_issuance( @@ -3480,7 +3366,7 @@ fn test_coinbase_subnet_terms_with_alpha_in_lte_alpha_emission() { ); let tao_emission = U96F32::saturating_from_num(34566756_u64); - let price: U96F32 = Swap::current_alpha_price(netuid0); + let price: U96F32 = U96F32::saturating_from_num(Swap::current_alpha_price(netuid0)); let subnet_emissions = BTreeMap::from([(netuid0, tao_emission)]); @@ -3533,7 +3419,7 @@ fn test_coinbase_inject_and_maybe_swap_does_not_skew_reserves() { AlphaCurrency::from(1_000_000_000_000_000), ); // Initialize swap v3 - Swap::maybe_initialize_v3(netuid0); + Swap::maybe_initialize_palswap(netuid0, None); let tao_in = BTreeMap::from([(netuid0, U96F32::saturating_from_num(123))]); let alpha_in = BTreeMap::from([(netuid0, U96F32::saturating_from_num(456))]); @@ -3667,7 +3553,7 @@ fn test_coinbase_emit_to_subnets_with_no_root_sell() { AlphaCurrency::from(1_000_000_000_000_000), ); // Initialize swap v3 - Swap::maybe_initialize_v3(netuid0); + Swap::maybe_initialize_palswap(netuid0, None); let tao_emission = U96F32::saturating_from_num(12345678); let subnet_emissions = BTreeMap::from([(netuid0, tao_emission)]); @@ -3681,7 +3567,7 @@ fn test_coinbase_emit_to_subnets_with_no_root_sell() { ) .unwrap_or(0), ); - let price: U96F32 = Swap::current_alpha_price(netuid0); + let price: U96F32 = U96F32::saturating_from_num(Swap::current_alpha_price(netuid0)); let (tao_in, alpha_in, alpha_out, excess_tao) = SubtensorModule::get_subnet_terms(&subnet_emissions); // Based on the price, we should have NO excess TAO @@ -3758,7 +3644,7 @@ fn test_coinbase_emit_to_subnets_with_root_sell() { AlphaCurrency::from(1_000_000_000_000_000), ); // Initialize swap v3 - Swap::maybe_initialize_v3(netuid0); + Swap::maybe_initialize_palswap(netuid0, None); let tao_emission = U96F32::saturating_from_num(12345678); let subnet_emissions = BTreeMap::from([(netuid0, tao_emission)]); @@ -3772,7 +3658,7 @@ fn test_coinbase_emit_to_subnets_with_root_sell() { ) .unwrap_or(0), ); - let price: U96F32 = Swap::current_alpha_price(netuid0); + let price: U96F32 = U96F32::saturating_from_num(Swap::current_alpha_price(netuid0)); let (tao_in, alpha_in, alpha_out, excess_tao) = SubtensorModule::get_subnet_terms(&subnet_emissions); // Based on the price, we should have NO excess TAO @@ -3887,10 +3773,10 @@ fn test_pending_emission_start_call_not_done() { // Make root sell happen // Set moving price > 1.0 // Set price > 1.0 - pallet_subtensor_swap::AlphaSqrtPrice::::insert( - netuid, - U64F64::saturating_from_num(10.0), - ); + let tao = TaoCurrency::from(10_000_000_000_u64); + let alpha = AlphaCurrency::from(1_000_000_000_u64); + SubnetTAO::::insert(netuid, tao); + SubnetAlphaIn::::insert(netuid, alpha); SubnetMovingPrice::::insert(netuid, I96F32::from_num(2)); diff --git a/pallets/subtensor/src/tests/mechanism.rs b/pallets/subtensor/src/tests/mechanism.rs index 9e6450e09c..7f0ead8918 100644 --- a/pallets/subtensor/src/tests/mechanism.rs +++ b/pallets/subtensor/src/tests/mechanism.rs @@ -210,6 +210,7 @@ fn do_set_mechanism_count_ok_at_effective_cap() { new_test_ext(1).execute_with(|| { let netuid = NetUid::from(4u16); NetworksAdded::::insert(NetUid::from(4u16), true); // base subnet exists + MaxAllowedUids::::insert(netuid, 128u16); // Effective bound is min(runtime cap, compile-time cap) let runtime_cap = MaxMechanismCount::::get(); // e.g., MechId::from(8) diff --git a/pallets/subtensor/src/tests/migration.rs b/pallets/subtensor/src/tests/migration.rs index bed77e797f..04c9ffbaf6 100644 --- a/pallets/subtensor/src/tests/migration.rs +++ b/pallets/subtensor/src/tests/migration.rs @@ -25,7 +25,7 @@ use pallet_drand::types::RoundNumber; use scale_info::prelude::collections::VecDeque; use sp_core::{H256, U256, crypto::Ss58Codec}; use sp_io::hashing::twox_128; -use sp_runtime::traits::Zero; +use sp_runtime::{traits::Hash, traits::Zero}; use substrate_fixed::types::extra::U2; use substrate_fixed::types::{I96F32, U64F64}; use subtensor_runtime_common::{NetUidStorageIndex, TaoCurrency}; @@ -2727,9 +2727,6 @@ fn test_migrate_reset_unactive_sn() { RAORecycledForRegistration::::get(netuid), actual_tao_lock_amount_less_pool_tao ); - assert!(pallet_subtensor_swap::AlphaSqrtPrice::::contains_key( - *netuid - )); assert_eq!(PendingOwnerCut::::get(netuid), AlphaCurrency::ZERO); assert_ne!(SubnetTAO::::get(netuid), initial_tao); assert_ne!(SubnetAlphaIn::::get(netuid), initial_alpha); @@ -2800,9 +2797,6 @@ fn test_migrate_reset_unactive_sn() { SubnetAlphaOutEmission::::get(netuid), AlphaCurrency::ZERO ); - assert!(pallet_subtensor_swap::AlphaSqrtPrice::::contains_key( - *netuid - )); assert_ne!(PendingOwnerCut::::get(netuid), AlphaCurrency::ZERO); assert_ne!(SubnetTAO::::get(netuid), initial_tao); assert_ne!(SubnetAlphaIn::::get(netuid), initial_alpha); @@ -2875,7 +2869,6 @@ fn test_migrate_reset_unactive_sn_idempotence() { }); } -#[test] fn test_migrate_remove_old_identity_maps() { let migration = crate::migrations::migrate_remove_old_identity_maps::migrate_remove_old_identity_maps::; @@ -2968,3 +2961,123 @@ fn test_migrate_remove_unknown_neuron_axon_cert_prom() { } } } + +// cargo test --package pallet-subtensor --lib -- tests::migration::test_migrate_cleanup_swap_v3 --exact --nocapture +#[test] +fn test_migrate_cleanup_swap_v3() { + use crate::migrations::migrate_cleanup_swap_v3::deprecated_swap_maps; + use substrate_fixed::types::U64F64; + + new_test_ext(1).execute_with(|| { + let migration = crate::migrations::migrate_cleanup_swap_v3::migrate_cleanup_swap_v3::; + + const MIGRATION_NAME: &str = "migrate_cleanup_swap_v3"; + + let provided: u64 = 9876; + let reserves: u64 = 1_000_000; + + SubnetTAO::::insert(NetUid::from(1), TaoCurrency::from(reserves)); + SubnetAlphaIn::::insert(NetUid::from(1), AlphaCurrency::from(reserves)); + + // Insert deprecated maps values + deprecated_swap_maps::SubnetTaoProvided::::insert( + NetUid::from(1), + TaoCurrency::from(provided), + ); + deprecated_swap_maps::SubnetAlphaInProvided::::insert( + NetUid::from(1), + AlphaCurrency::from(provided), + ); + + // Run migration + let weight = migration(); + + // Test that values are removed from state + assert!(!deprecated_swap_maps::SubnetTaoProvided::::contains_key(NetUid::from(1)),); + assert!( + !deprecated_swap_maps::SubnetAlphaInProvided::::contains_key(NetUid::from(1)), + ); + + // Provided got added to reserves + assert_eq!( + u64::from(SubnetTAO::::get(NetUid::from(1))), + reserves + provided + ); + assert_eq!( + u64::from(SubnetAlphaIn::::get(NetUid::from(1))), + reserves + provided + ); + }); +} + +#[test] +fn test_migrate_coldkey_swap_scheduled_to_announcements() { + new_test_ext(1000).execute_with(|| { + const MIGRATION_NAME: &[u8] = b"migrate_coldkey_swap_scheduled_to_announcements"; + use crate::migrations::migrate_coldkey_swap_scheduled_to_announcements::*; + let now = frame_system::Pallet::::block_number(); + + // Set the schedule duration and reschedule duration + deprecated::ColdkeySwapScheduleDuration::::set(Some(now + 100)); + deprecated::ColdkeySwapRescheduleDuration::::set(Some(now + 200)); + + // Set some scheduled coldkey swaps + deprecated::ColdkeySwapScheduled::::insert( + U256::from(1), + (now + 100, U256::from(10)), + ); + deprecated::ColdkeySwapScheduled::::insert( + U256::from(2), + (now - 200, U256::from(20)), + ); + deprecated::ColdkeySwapScheduled::::insert( + U256::from(3), + (now + 200, U256::from(30)), + ); + deprecated::ColdkeySwapScheduled::::insert( + U256::from(4), + (now - 400, U256::from(40)), + ); + deprecated::ColdkeySwapScheduled::::insert( + U256::from(5), + (now + 300, U256::from(50)), + ); + + let w = migrate_coldkey_swap_scheduled_to_announcements::(); + + assert!(!w.is_zero(), "weight must be non-zero"); + assert!(HasMigrationRun::::get(MIGRATION_NAME)); + + // Ensure the deprecated storage is cleared + assert!(!deprecated::ColdkeySwapScheduleDuration::::exists()); + assert!(!deprecated::ColdkeySwapRescheduleDuration::::exists()); + assert_eq!(deprecated::ColdkeySwapScheduled::::iter().count(), 0); + + // Ensure scheduled have been migrated to announcements if not executed yet + // The announcement should be at the scheduled time - delay to be able to call + // the swap_coldkey_announced call at the old scheduled time + let delay = ColdkeySwapAnnouncementDelay::::get(); + assert_eq!(ColdkeySwapAnnouncements::::iter().count(), 3); + assert_eq!( + ColdkeySwapAnnouncements::::get(U256::from(1)), + Some(( + now + 100 - delay, + ::Hashing::hash_of(&U256::from(10)) + )) + ); + assert_eq!( + ColdkeySwapAnnouncements::::get(U256::from(3)), + Some(( + now + 200 - delay, + ::Hashing::hash_of(&U256::from(30)) + )) + ); + assert_eq!( + ColdkeySwapAnnouncements::::get(U256::from(5)), + Some(( + now + 300 - delay, + ::Hashing::hash_of(&U256::from(50)) + )) + ); + }); +} diff --git a/pallets/subtensor/src/tests/mock.rs b/pallets/subtensor/src/tests/mock.rs index b744c9b771..94ae2d240d 100644 --- a/pallets/subtensor/src/tests/mock.rs +++ b/pallets/subtensor/src/tests/mock.rs @@ -20,15 +20,16 @@ use frame_system as system; use frame_system::{EnsureRoot, RawOrigin, limits, offchain::CreateTransactionBase}; use pallet_subtensor_proxy as pallet_proxy; use pallet_subtensor_utility as pallet_utility; +use sp_consensus_aura::sr25519::AuthorityId as AuraId; use sp_core::{ConstU64, Get, H256, U256, offchain::KeyTypeId}; use sp_runtime::Perbill; use sp_runtime::{ BuildStorage, Percent, - traits::{BlakeTwo256, IdentityLookup}, + traits::{BadOrigin, BlakeTwo256, IdentityLookup}, }; use sp_std::{cell::RefCell, cmp::Ordering, sync::OnceLock}; use sp_tracing::tracing_subscriber; -use subtensor_runtime_common::{NetUid, TaoCurrency}; +use subtensor_runtime_common::{AuthorshipInfo, NetUid, TaoCurrency}; use subtensor_swap_interface::{Order, SwapHandler}; use tracing_subscriber::{EnvFilter, layer::SubscriberExt, util::SubscriberInitExt}; type Block = frame_system::mocking::MockBlock; @@ -37,16 +38,19 @@ type Block = frame_system::mocking::MockBlock; frame_support::construct_runtime!( pub enum Test { - System: frame_system::{Pallet, Call, Config, Storage, Event} = 1, - Balances: pallet_balances::{Pallet, Call, Config, Storage, Event} = 2, - SubtensorModule: crate::{Pallet, Call, Storage, Event} = 7, - Utility: pallet_utility::{Pallet, Call, Storage, Event} = 8, - Scheduler: pallet_scheduler::{Pallet, Call, Storage, Event} = 9, - Preimage: pallet_preimage::{Pallet, Call, Storage, Event} = 10, - Drand: pallet_drand::{Pallet, Call, Storage, Event} = 11, - Swap: pallet_subtensor_swap::{Pallet, Call, Storage, Event} = 12, - Crowdloan: pallet_crowdloan::{Pallet, Call, Storage, Event} = 13, - Proxy: pallet_subtensor_proxy = 14, + System: frame_system = 1, + Balances: pallet_balances = 2, + Timestamp: pallet_timestamp = 3, + Aura: pallet_aura = 4, + Shield: pallet_shield = 5, + SubtensorModule: crate = 6, + Utility: pallet_utility = 7, + Scheduler: pallet_scheduler = 8, + Preimage: pallet_preimage = 9, + Drand: pallet_drand = 10, + Swap: pallet_subtensor_swap = 11, + Crowdloan: pallet_crowdloan = 12, + Proxy: pallet_subtensor_proxy = 13, } ); @@ -149,6 +153,14 @@ parameter_types! { pub const SS58Prefix: u8 = 42; } +pub struct MockAuthorshipProvider; + +impl AuthorshipInfo for MockAuthorshipProvider { + fn author() -> Option { + Some(U256::from(12345u64)) + } +} + parameter_types! { pub const InitialMinAllowedWeights: u16 = 0; pub const InitialEmissionValue: u16 = 0; @@ -166,7 +178,7 @@ parameter_types! { pub const SelfOwnership: u64 = 2; pub const InitialImmunityPeriod: u16 = 2; pub const InitialMinAllowedUids: u16 = 2; - pub const InitialMaxAllowedUids: u16 = 4; + pub const InitialMaxAllowedUids: u16 = 256; pub const InitialBondsMovingAverage: u64 = 900_000; pub const InitialBondsPenalty:u16 = u16::MAX; pub const InitialBondsResetOn: bool = false; @@ -212,9 +224,8 @@ parameter_types! { pub const InitialAlphaLow: u16 = 45875; // Represents 0.7 as per the production default pub const InitialLiquidAlphaOn: bool = false; // Default value for LiquidAlphaOn pub const InitialYuma3On: bool = false; // Default value for Yuma3On - // pub const InitialNetworkMaxStake: u64 = u64::MAX; // (DEPRECATED) - pub const InitialColdkeySwapScheduleDuration: u64 = 5 * 24 * 60 * 60 / 12; // Default as 5 days - pub const InitialColdkeySwapRescheduleDuration: u64 = 24 * 60 * 60 / 12; // Default as 1 day + pub const InitialColdkeySwapAnnouncementDelay: u64 = 50; + pub const InitialColdkeySwapReannouncementDelay: u64 = 10; pub const InitialDissolveNetworkScheduleDuration: u64 = 5 * 24 * 60 * 60 / 12; // Default as 5 days pub const InitialTaoWeight: u64 = 0; // 100% global weight. pub const InitialEmaPriceHalvingPeriod: u64 = 201_600_u64; // 4 weeks @@ -284,8 +295,8 @@ impl crate::Config for Test { type LiquidAlphaOn = InitialLiquidAlphaOn; type Yuma3On = InitialYuma3On; type Preimages = Preimage; - type InitialColdkeySwapScheduleDuration = InitialColdkeySwapScheduleDuration; - type InitialColdkeySwapRescheduleDuration = InitialColdkeySwapRescheduleDuration; + type InitialColdkeySwapAnnouncementDelay = InitialColdkeySwapAnnouncementDelay; + type InitialColdkeySwapReannouncementDelay = InitialColdkeySwapReannouncementDelay; type InitialDissolveNetworkScheduleDuration = InitialDissolveNetworkScheduleDuration; type InitialTaoWeight = InitialTaoWeight; type InitialEmaPriceHalvingPeriod = InitialEmaPriceHalvingPeriod; @@ -299,13 +310,13 @@ impl crate::Config for Test { type MaxImmuneUidsPercentage = MaxImmuneUidsPercentage; type CommitmentsInterface = CommitmentsI; type EvmKeyAssociateRateLimit = EvmKeyAssociateRateLimit; + type AuthorshipProvider = MockAuthorshipProvider; } // Swap-related parameter types parameter_types! { pub const SwapProtocolId: PalletId = PalletId(*b"ten/swap"); pub const SwapMaxFeeRate: u16 = 10000; // 15.26% - pub const SwapMaxPositions: u32 = 100; pub const SwapMinimumLiquidity: u64 = 1_000; pub const SwapMinimumReserve: NonZeroU64 = NonZeroU64::new(100).unwrap(); } @@ -317,7 +328,6 @@ impl pallet_subtensor_swap::Config for Test { type TaoReserve = TaoCurrencyReserve; type AlphaReserve = AlphaCurrencyReserve; type MaxFeeRate = SwapMaxFeeRate; - type MaxPositions = SwapMaxPositions; type MinimumLiquidity = SwapMinimumLiquidity; type MinimumReserve = SwapMinimumReserve; type WeightInfo = (); @@ -552,6 +562,41 @@ where } } +#[derive_impl(pallet_timestamp::config_preludes::TestDefaultConfig)] +impl pallet_timestamp::Config for Test { + type MinimumPeriod = ConstU64<0>; +} + +parameter_types! { + pub const MaxAuthorities: u32 = 32; + pub const AllowMultipleBlocksPerSlot: bool = false; + pub const SlotDuration: u64 = 6000; +} + +impl pallet_aura::Config for Test { + type AuthorityId = AuraId; + // For tests we don't need dynamic disabling; just use unit type. + type DisabledValidators = (); + type MaxAuthorities = MaxAuthorities; + type AllowMultipleBlocksPerSlot = AllowMultipleBlocksPerSlot; + type SlotDuration = SlotDuration; +} + +pub struct TestAuthorityOrigin; + +impl pallet_shield::AuthorityOriginExt for TestAuthorityOrigin { + type AccountId = U256; + + fn ensure_validator(_origin: RuntimeOrigin) -> Result { + Ok(U256::from(0)) + } +} + +impl pallet_shield::Config for Test { + type RuntimeCall = RuntimeCall; + type AuthorityOrigin = TestAuthorityOrigin; +} + static TEST_LOGS_INIT: OnceLock<()> = OnceLock::new(); pub fn init_logs_for_tests() { diff --git a/pallets/subtensor/src/tests/mod.rs b/pallets/subtensor/src/tests/mod.rs index bbaf25af58..8f07572e25 100644 --- a/pallets/subtensor/src/tests/mod.rs +++ b/pallets/subtensor/src/tests/mod.rs @@ -15,7 +15,7 @@ mod leasing; mod math; mod mechanism; mod migration; -mod mock; +pub(crate) mod mock; mod move_stake; mod networks; mod neuron_info; @@ -30,4 +30,5 @@ mod swap_coldkey; mod swap_hotkey; mod swap_hotkey_with_subnet; mod uids; +mod voting_power; mod weights; diff --git a/pallets/subtensor/src/tests/move_stake.rs b/pallets/subtensor/src/tests/move_stake.rs index dfd9927da4..9c9f92d8ac 100644 --- a/pallets/subtensor/src/tests/move_stake.rs +++ b/pallets/subtensor/src/tests/move_stake.rs @@ -619,8 +619,9 @@ fn test_do_move_event_emission() { // Move stake and capture events System::reset_events(); - let current_price = - ::SwapInterface::current_alpha_price(netuid.into()); + let current_price = U96F32::from_num( + ::SwapInterface::current_alpha_price(netuid.into()), + ); let tao_equivalent = (current_price * U96F32::from_num(alpha)).to_num::(); // no fee conversion assert_ok!(SubtensorModule::do_move_stake( RuntimeOrigin::signed(coldkey), @@ -710,7 +711,7 @@ fn test_do_move_storage_updates() { destination_netuid ), alpha2, - epsilon = 2.into() + epsilon = 50.into() ); }); } diff --git a/pallets/subtensor/src/tests/networks.rs b/pallets/subtensor/src/tests/networks.rs index 4605ac8bef..fe141c040f 100644 --- a/pallets/subtensor/src/tests/networks.rs +++ b/pallets/subtensor/src/tests/networks.rs @@ -6,10 +6,16 @@ use crate::*; use frame_support::{assert_err, assert_ok}; use frame_system::Config; use sp_core::U256; -use sp_std::collections::{btree_map::BTreeMap, vec_deque::VecDeque}; +use sp_std::collections::{ + //btree_map::BTreeMap, + vec_deque::VecDeque, +}; use substrate_fixed::types::{I96F32, U64F64, U96F32}; use subtensor_runtime_common::{MechId, NetUidStorageIndex, TaoCurrency}; -use subtensor_swap_interface::{Order, SwapHandler}; +use subtensor_swap_interface::{ + //Order, + SwapHandler, +}; #[test] fn test_registration_ok() { @@ -247,8 +253,9 @@ fn dissolve_owner_cut_refund_logic() { // Use the current alpha price to estimate the TAO equivalent. let owner_emission_tao = { - let price: U96F32 = - ::SwapInterface::current_alpha_price(net.into()); + let price: U96F32 = U96F32::from_num( + ::SwapInterface::current_alpha_price(net.into()), + ); U96F32::from_num(owner_alpha_u64) .saturating_mul(price) .floor() @@ -365,8 +372,6 @@ fn dissolve_clears_all_per_subnet_storages() { // Token / price / provided reserves TokenSymbol::::insert(net, b"XX".to_vec()); SubnetMovingPrice::::insert(net, substrate_fixed::types::I96F32::from_num(1)); - SubnetTaoProvided::::insert(net, TaoCurrency::from(1)); - SubnetAlphaInProvided::::insert(net, AlphaCurrency::from(1)); // TAO Flow SubnetTaoFlow::::insert(net, 0i64); @@ -529,8 +534,6 @@ fn dissolve_clears_all_per_subnet_storages() { // Token / price / provided reserves assert!(!TokenSymbol::::contains_key(net)); assert!(!SubnetMovingPrice::::contains_key(net)); - assert!(!SubnetTaoProvided::::contains_key(net)); - assert!(!SubnetAlphaInProvided::::contains_key(net)); // Subnet locks assert!(!TransferToggle::::contains_key(net)); @@ -826,7 +829,8 @@ fn destroy_alpha_out_many_stakers_complex_distribution() { netuid.into(), min_stake, ); - min_stake.saturating_add(fee) + // Double the fees because fee is calculated for min_stake, not for min_amount + min_stake + fee * 2.into() }; const N: usize = 20; @@ -906,8 +910,9 @@ fn destroy_alpha_out_many_stakers_complex_distribution() { let owner_emission_tao: u64 = { // Fallback matches the pallet's fallback - let price: U96F32 = - ::SwapInterface::current_alpha_price(netuid.into()); + let price: U96F32 = U96F32::from_num( + ::SwapInterface::current_alpha_price(netuid.into()), + ); U96F32::from_num(owner_alpha_u64) .saturating_mul(price) .floor() @@ -986,8 +991,9 @@ fn destroy_alpha_out_refund_gating_by_registration_block() { .saturating_to_num::(); let owner_emission_tao_u64 = { - let price: U96F32 = - ::SwapInterface::current_alpha_price(netuid.into()); + let price: U96F32 = U96F32::from_num( + ::SwapInterface::current_alpha_price(netuid.into()), + ); U96F32::from_num(owner_alpha_u64) .saturating_mul(price) .floor() @@ -1774,458 +1780,6 @@ fn test_tempo_greater_than_weight_set_rate_limit() { }) } -#[allow(clippy::indexing_slicing)] -#[test] -fn massive_dissolve_refund_and_reregistration_flow_is_lossless_and_cleans_state() { - new_test_ext(0).execute_with(|| { - // ──────────────────────────────────────────────────────────────────── - // 0) Constants and helpers (distinct hotkeys & coldkeys) - // ──────────────────────────────────────────────────────────────────── - const NUM_NETS: usize = 4; - - // Six LP coldkeys - let cold_lps: [U256; 6] = [ - U256::from(3001), - U256::from(3002), - U256::from(3003), - U256::from(3004), - U256::from(3005), - U256::from(3006), - ]; - - // For each coldkey, define two DISTINCT hotkeys it owns. - let mut cold_to_hots: BTreeMap = BTreeMap::new(); - for &c in cold_lps.iter() { - let h1 = U256::from(c.low_u64().saturating_add(100_000)); - let h2 = U256::from(c.low_u64().saturating_add(200_000)); - cold_to_hots.insert(c, [h1, h2]); - } - - // Distinct τ pot sizes per net. - let pots: [u64; NUM_NETS] = [12_345, 23_456, 34_567, 45_678]; - - let lp_sets_per_net: [&[U256]; NUM_NETS] = [ - &cold_lps[0..4], // net0: A,B,C,D - &cold_lps[2..6], // net1: C,D,E,F - &cold_lps[0..6], // net2: A..F - &cold_lps[1..5], // net3: B,C,D,E - ]; - - // Multiple bands/sizes → many positions per cold across nets, using mixed hotkeys. - // let bands: [i32; 3] = [5, 13, 30]; - // let liqs: [u64; 3] = [400_000, 700_000, 1_100_000]; - - // TODO: Revise when user liquidity is available - // Helper: add a V3 position via a (hot, cold) pair. - // let add_pos = |net: NetUid, hot: U256, cold: U256, band: i32, liq: u64| { - // let ct = pallet_subtensor_swap::CurrentTick::::get(net); - // let lo = ct.saturating_sub(band); - // let hi = ct.saturating_add(band); - // pallet_subtensor_swap::EnabledUserLiquidity::::insert(net, true); - // assert_ok!(pallet_subtensor_swap::Pallet::::add_liquidity( - // RuntimeOrigin::signed(cold), - // hot, - // net, - // lo, - // hi, - // liq - // )); - // }; - - // ──────────────────────────────────────────────────────────────────── - // 1) Create many subnets, enable V3, fix price at tick=0 (sqrt≈1) - // ──────────────────────────────────────────────────────────────────── - let mut nets: Vec = Vec::new(); - for i in 0..NUM_NETS { - let owner_hot = U256::from(10_000 + (i as u64)); - let owner_cold = U256::from(20_000 + (i as u64)); - let net = add_dynamic_network(&owner_hot, &owner_cold); - SubtensorModule::set_max_registrations_per_block(net, 1_000u16); - SubtensorModule::set_target_registrations_per_interval(net, 1_000u16); - Emission::::insert(net, Vec::::new()); - SubtensorModule::set_subnet_locked_balance(net, TaoCurrency::from(0)); - - assert_ok!( - pallet_subtensor_swap::Pallet::::toggle_user_liquidity( - RuntimeOrigin::root(), - net, - true - ) - ); - - // Price/tick pinned so LP math stays stable (sqrt(1)). - let ct0 = pallet_subtensor_swap::tick::TickIndex::new_unchecked(0); - let sqrt1 = ct0.try_to_sqrt_price().expect("sqrt(1) price"); - pallet_subtensor_swap::CurrentTick::::set(net, ct0); - pallet_subtensor_swap::AlphaSqrtPrice::::set(net, sqrt1); - - nets.push(net); - } - - // Map net → index for quick lookups. - let mut net_index: BTreeMap = BTreeMap::new(); - for (i, &n) in nets.iter().enumerate() { - net_index.insert(n, i); - } - - // ──────────────────────────────────────────────────────────────────── - // 2) Pre-create a handful of small (hot, cold) pairs so accounts exist - // ──────────────────────────────────────────────────────────────────── - for id in 0u64..10 { - let cold_acc = U256::from(1_000_000 + id); - let hot_acc = U256::from(2_000_000 + id); - for &net in nets.iter() { - register_ok_neuron(net, hot_acc, cold_acc, 100_000 + id); - } - } - - // ──────────────────────────────────────────────────────────────────── - // 3) LPs per net: register each (hot, cold), massive τ prefund, and stake - // ──────────────────────────────────────────────────────────────────── - for &cold in cold_lps.iter() { - SubtensorModule::add_balance_to_coldkey_account(&cold, u64::MAX); - } - - // τ balances before LP adds (after staking): - let mut tao_before: BTreeMap = BTreeMap::new(); - - // Ordered α snapshot per net at **pair granularity** (pre‑LP): - let mut alpha_pairs_per_net: BTreeMap> = BTreeMap::new(); - - // Register both hotkeys for each participating cold on each net and stake τ→α. - for (ni, &net) in nets.iter().enumerate() { - let participants = lp_sets_per_net[ni]; - for &cold in participants.iter() { - let [hot1, hot2] = cold_to_hots[&cold]; - - // Ensure (hot, cold) neurons exist on this net. - register_ok_neuron( - net, - hot1, - cold, - (ni as u64) * 10_000 + (hot1.low_u64() % 10_000), - ); - register_ok_neuron( - net, - hot2, - cold, - (ni as u64) * 10_000 + (hot2.low_u64() % 10_000) + 1, - ); - - // Stake τ (split across the two hotkeys). - let base: u64 = - 5_000_000 + ((ni as u64) * 1_000_000) + ((cold.low_u64() % 10) * 250_000); - let stake1: u64 = base.saturating_mul(3) / 5; // 60% - let stake2: u64 = base.saturating_sub(stake1); // 40% - - assert_ok!(SubtensorModule::do_add_stake( - RuntimeOrigin::signed(cold), - hot1, - net, - stake1.into() - )); - assert_ok!(SubtensorModule::do_add_stake( - RuntimeOrigin::signed(cold), - hot2, - net, - stake2.into() - )); - } - } - - // Record τ balances now (post‑stake, pre‑LP). - for &cold in cold_lps.iter() { - tao_before.insert(cold, SubtensorModule::get_coldkey_balance(&cold)); - } - - // Capture **pair‑level** α snapshot per net (pre‑LP). - for ((hot, cold, net), amt) in Alpha::::iter() { - if let Some(&ni) = net_index.get(&net) - && lp_sets_per_net[ni].contains(&cold) { - let a: u128 = amt.saturating_to_num(); - if a > 0 { - alpha_pairs_per_net - .entry(net) - .or_default() - .push(((hot, cold), a)); - } - } - } - - // ──────────────────────────────────────────────────────────────────── - // 4) Add many V3 positions per cold across nets, alternating hotkeys - // ──────────────────────────────────────────────────────────────────── - // TODO: Revise when user liquidity is available - // for (ni, &net) in nets.iter().enumerate() { - // let participants = lp_sets_per_net[ni]; - // for (pi, &cold) in participants.iter().enumerate() { - // let [hot1, hot2] = cold_to_hots[&cold]; - // let hots = [hot1, hot2]; - // for k in 0..3 { - // let band = bands[(pi + k) % bands.len()]; - // let liq = liqs[(ni + k) % liqs.len()]; - // let hot = hots[k % hots.len()]; - // add_pos(net, hot, cold, band, liq); - // } - // } - // } - - // Snapshot τ balances AFTER LP adds (to measure actual principal debit). - let mut tao_after_adds: BTreeMap = BTreeMap::new(); - for &cold in cold_lps.iter() { - tao_after_adds.insert(cold, SubtensorModule::get_coldkey_balance(&cold)); - } - - // ──────────────────────────────────────────────────────────────────── - // 5) Compute Hamilton-apportionment BASE shares per cold and total leftover - // from the **pair-level** pre‑LP α snapshot; also count pairs per cold. - // ──────────────────────────────────────────────────────────────────── - let mut base_share_cold: BTreeMap = - cold_lps.iter().copied().map(|c| (c, 0_u64)).collect(); - let mut pair_count_cold: BTreeMap = - cold_lps.iter().copied().map(|c| (c, 0_u32)).collect(); - - let mut leftover_total: u64 = 0; - - for (ni, &net) in nets.iter().enumerate() { - let pot = pots[ni]; - let pairs = alpha_pairs_per_net.get(&net).cloned().unwrap_or_default(); - if pot == 0 || pairs.is_empty() { - continue; - } - let total_alpha: u128 = pairs.iter().map(|(_, a)| *a).sum(); - if total_alpha == 0 { - continue; - } - - let mut base_sum_net: u64 = 0; - for ((_, cold), a) in pairs.iter().copied() { - // quota = a * pot / total_alpha - let prod: u128 = a.saturating_mul(pot as u128); - let base: u64 = (prod / total_alpha) as u64; - base_sum_net = base_sum_net.saturating_add(base); - *base_share_cold.entry(cold).or_default() = - base_share_cold[&cold].saturating_add(base); - *pair_count_cold.entry(cold).or_default() += 1; - } - let leftover_net = pot.saturating_sub(base_sum_net); - leftover_total = leftover_total.saturating_add(leftover_net); - } - - // ──────────────────────────────────────────────────────────────────── - // 6) Seed τ pots and dissolve *all* networks (liquidates LPs + refunds) - // ──────────────────────────────────────────────────────────────────── - for (ni, &net) in nets.iter().enumerate() { - SubnetTAO::::insert(net, TaoCurrency::from(pots[ni])); - } - for &net in nets.iter() { - assert_ok!(SubtensorModule::do_dissolve_network(net)); - } - - // ──────────────────────────────────────────────────────────────────── - // 7) Assertions: τ balances, α gone, nets removed, swap state clean - // (Hamilton invariants enforced at cold-level without relying on tie-break) - // ──────────────────────────────────────────────────────────────────── - // Collect actual pot credits per cold (principal cancels out against adds when comparing before→after). - let mut actual_pot_cold: BTreeMap = - cold_lps.iter().copied().map(|c| (c, 0_u64)).collect(); - for &cold in cold_lps.iter() { - let before = tao_before[&cold]; - let after = SubtensorModule::get_coldkey_balance(&cold); - actual_pot_cold.insert(cold, after.saturating_sub(before)); - } - - // (a) Sum of actual pot credits equals total pots. - let total_actual: u64 = actual_pot_cold.values().copied().sum(); - let total_pots: u64 = pots.iter().copied().sum(); - assert_eq!( - total_actual, total_pots, - "total τ pot credited across colds must equal sum of pots" - ); - - // (b) Each cold’s pot is within Hamilton bounds: base ≤ actual ≤ base + #pairs. - let mut extra_accum: u64 = 0; - for &cold in cold_lps.iter() { - let base = *base_share_cold.get(&cold).unwrap_or(&0); - let pairs = *pair_count_cold.get(&cold).unwrap_or(&0) as u64; - let actual = *actual_pot_cold.get(&cold).unwrap_or(&0); - - assert!( - actual >= base, - "cold {cold:?} actual pot {actual} is below base {base}" - ); - assert!( - actual <= base.saturating_add(pairs), - "cold {cold:?} actual pot {actual} exceeds base + pairs ({base} + {pairs})" - ); - - extra_accum = extra_accum.saturating_add(actual.saturating_sub(base)); - } - - // (c) The total “extra beyond base” equals the computed leftover_total across nets. - assert_eq!( - extra_accum, leftover_total, - "sum of extras beyond base must equal total leftover" - ); - - // (d) τ principal was fully refunded (compare after_adds → after). - for &cold in cold_lps.iter() { - let before = tao_before[&cold]; - let mid = tao_after_adds[&cold]; - let after = SubtensorModule::get_coldkey_balance(&cold); - let principal_actual = before.saturating_sub(mid); - let actual_pot = after.saturating_sub(before); - assert_eq!( - after.saturating_sub(mid), - principal_actual.saturating_add(actual_pot), - "cold {cold:?} τ balance incorrect vs 'after_adds'" - ); - } - - // For each dissolved net, check α ledgers gone, network removed, and swap state clean. - for &net in nets.iter() { - assert!( - Alpha::::iter().all(|((_h, _c, n), _)| n != net), - "alpha ledger not fully cleared for net {net:?}" - ); - assert!( - !SubtensorModule::if_subnet_exist(net), - "subnet {net:?} still exists" - ); - assert!( - pallet_subtensor_swap::Ticks::::iter_prefix(net) - .next() - .is_none(), - "ticks not cleared for net {net:?}" - ); - assert!( - !pallet_subtensor_swap::Positions::::iter() - .any(|((n, _owner, _pid), _)| n == net), - "swap positions not fully cleared for net {net:?}" - ); - assert_eq!( - pallet_subtensor_swap::FeeGlobalTao::::get(net).saturating_to_num::(), - 0, - "FeeGlobalTao nonzero for net {net:?}" - ); - assert_eq!( - pallet_subtensor_swap::FeeGlobalAlpha::::get(net).saturating_to_num::(), - 0, - "FeeGlobalAlpha nonzero for net {net:?}" - ); - assert_eq!( - pallet_subtensor_swap::CurrentLiquidity::::get(net), - 0, - "CurrentLiquidity not zero for net {net:?}" - ); - assert!( - !pallet_subtensor_swap::SwapV3Initialized::::get(net), - "SwapV3Initialized still set" - ); - assert!( - !pallet_subtensor_swap::EnabledUserLiquidity::::get(net), - "EnabledUserLiquidity still set" - ); - assert!( - pallet_subtensor_swap::TickIndexBitmapWords::::iter_prefix((net,)) - .next() - .is_none(), - "TickIndexBitmapWords not cleared for net {net:?}" - ); - } - - // ──────────────────────────────────────────────────────────────────── - // 8) Re-register a fresh subnet and re‑stake using the pallet’s min rule - // Assert αΔ equals the sim-swap result for the exact τ staked. - // ──────────────────────────────────────────────────────────────────── - let new_owner_hot = U256::from(99_000); - let new_owner_cold = U256::from(99_001); - let net_new = add_dynamic_network(&new_owner_hot, &new_owner_cold); - SubtensorModule::set_max_registrations_per_block(net_new, 1_000u16); - SubtensorModule::set_target_registrations_per_interval(net_new, 1_000u16); - Emission::::insert(net_new, Vec::::new()); - SubtensorModule::set_subnet_locked_balance(net_new, TaoCurrency::from(0)); - - assert_ok!( - pallet_subtensor_swap::Pallet::::toggle_user_liquidity( - RuntimeOrigin::root(), - net_new, - true - ) - ); - let ct0 = pallet_subtensor_swap::tick::TickIndex::new_unchecked(0); - let sqrt1 = ct0.try_to_sqrt_price().expect("sqrt(1)"); - pallet_subtensor_swap::CurrentTick::::set(net_new, ct0); - pallet_subtensor_swap::AlphaSqrtPrice::::set(net_new, sqrt1); - - // Compute the exact min stake per the pallet rule: DefaultMinStake + fee(DefaultMinStake). - let min_stake = DefaultMinStake::::get(); - let order = GetAlphaForTao::::with_amount(min_stake); - let fee_for_min = pallet_subtensor_swap::Pallet::::sim_swap( - net_new, - order, - ) - .map(|r| r.fee_paid) - .unwrap_or_else(|_e| { - as subtensor_swap_interface::SwapHandler>::approx_fee_amount(net_new, min_stake) - }); - let min_amount_required = min_stake.saturating_add(fee_for_min).to_u64(); - - // Re‑stake from three coldkeys; choose a specific DISTINCT hotkey per cold. - for &cold in &cold_lps[0..3] { - let [hot1, _hot2] = cold_to_hots[&cold]; - register_ok_neuron(net_new, hot1, cold, 7777); - - let before_tao = SubtensorModule::get_coldkey_balance(&cold); - let a_prev: u64 = Alpha::::get((hot1, cold, net_new)).saturating_to_num(); - - // Expected α for this exact τ, using the same sim path as the pallet. - let order = GetAlphaForTao::::with_amount(min_amount_required); - let expected_alpha_out = pallet_subtensor_swap::Pallet::::sim_swap( - net_new, - order, - ) - .map(|r| r.amount_paid_out) - .expect("sim_swap must succeed for fresh net and min amount"); - - assert_ok!(SubtensorModule::do_add_stake( - RuntimeOrigin::signed(cold), - hot1, - net_new, - min_amount_required.into() - )); - - let after_tao = SubtensorModule::get_coldkey_balance(&cold); - let a_new: u64 = Alpha::::get((hot1, cold, net_new)).saturating_to_num(); - let a_delta = a_new.saturating_sub(a_prev); - - // τ decreased by exactly the amount we sent. - assert_eq!( - after_tao, - before_tao.saturating_sub(min_amount_required), - "τ did not decrease by the min required restake amount for cold {cold:?}" - ); - - // α minted equals the simulated swap’s net out for that same τ. - assert_eq!( - a_delta, expected_alpha_out.to_u64(), - "α minted mismatch for cold {cold:?} (hot {hot1:?}) on new net (αΔ {a_delta}, expected {expected_alpha_out})" - ); - } - - // Ensure V3 still functional on new net: add a small position for the first cold using its hot1 - // TODO: Revise when user liquidity is available - // let who_cold = cold_lps[0]; - // let [who_hot, _] = cold_to_hots[&who_cold]; - // add_pos(net_new, who_hot, who_cold, 8, 123_456); - // assert!( - // pallet_subtensor_swap::Positions::::iter() - // .any(|((n, owner, _pid), _)| n == net_new && owner == who_cold), - // "new position not recorded on the re-registered net" - // ); - }); -} - #[test] fn dissolve_clears_all_mechanism_scoped_maps_for_all_mechanisms() { new_test_ext(0).execute_with(|| { diff --git a/pallets/subtensor/src/tests/recycle_alpha.rs b/pallets/subtensor/src/tests/recycle_alpha.rs index 32a95c700d..5cf589de97 100644 --- a/pallets/subtensor/src/tests/recycle_alpha.rs +++ b/pallets/subtensor/src/tests/recycle_alpha.rs @@ -1,12 +1,12 @@ +use super::mock; +use super::mock::*; +use crate::*; use approx::assert_abs_diff_eq; use frame_support::{assert_noop, assert_ok, traits::Currency}; use sp_core::U256; -use substrate_fixed::types::U64F64; +use substrate_fixed::types::{U64F64, U96F32}; use subtensor_runtime_common::{AlphaCurrency, Currency as CurrencyT}; - -use super::mock; -use super::mock::*; -use crate::*; +use subtensor_swap_interface::SwapHandler; #[test] fn test_recycle_success() { @@ -618,3 +618,288 @@ fn test_burn_precision_loss() { ); }); } + +#[test] +fn test_add_stake_burn_success() { + new_test_ext(1).execute_with(|| { + let hotkey_account_id = U256::from(533453); + let coldkey_account_id = U256::from(55453); + let amount = DefaultMinStake::::get().to_u64() * 10; + + let netuid = add_dynamic_network(&hotkey_account_id, &coldkey_account_id); + + mock::setup_reserves( + netuid, + (amount * 1_000_000).into(), + (amount * 10_000_000).into(), + ); + + SubtensorModule::add_balance_to_coldkey_account(&coldkey_account_id, amount); + + // Check we have zero staked before transfer + assert_eq!( + SubtensorModule::get_total_stake_for_hotkey(&hotkey_account_id), + TaoCurrency::ZERO + ); + + // Execute add_stake_burn - this stakes TAO to get Alpha, then burns the Alpha + assert_ok!(SubtensorModule::add_stake_burn( + RuntimeOrigin::signed(coldkey_account_id), + hotkey_account_id, + netuid, + amount.into(), + None, + )); + + // After "add stake and burn", hotkey should have zero stake since alpha is burned immediately + assert_eq!( + SubtensorModule::get_total_stake_for_hotkey(&hotkey_account_id), + TaoCurrency::ZERO + ); + + // We spent TAO + assert_abs_diff_eq!( + SubtensorModule::get_coldkey_balance(&coldkey_account_id), + 0u64, + epsilon = 1u64 + ); + + // Verify AlphaBurned event was emitted + assert!(System::events().iter().any(|e| { + matches!( + &e.event, + RuntimeEvent::SubtensorModule(Event::AlphaBurned(..)) + ) + })); + + // Verify AddStakeBurn event was emitted + assert!(System::events().iter().any(|e| { + matches!( + &e.event, + RuntimeEvent::SubtensorModule(Event::AddStakeBurn { .. }) + ) + })); + }); +} + +#[test] +fn test_add_stake_burn_with_limit_success() { + new_test_ext(1).execute_with(|| { + let hotkey_account_id = U256::from(533453); + let coldkey_account_id = U256::from(55453); + let amount: u64 = 100_000_000_000; // 100 TAO - moderate amount + + // Add network + let netuid = add_dynamic_network(&hotkey_account_id, &coldkey_account_id); + + // Setup reserves with large liquidity to minimize slippage + let tao_reserve = TaoCurrency::from(1_000_000_000_000); // 1000 TAO + let alpha_in = AlphaCurrency::from(1_000_000_000_000); // 1000 Alpha + mock::setup_reserves(netuid, tao_reserve, alpha_in); + + // Verify current price is 1.0 + let current_price = + ::SwapInterface::current_alpha_price(netuid.into()); + assert_eq!(current_price, U96F32::from_num(1.0)); + + // Give coldkey sufficient balance + SubtensorModule::add_balance_to_coldkey_account(&coldkey_account_id, amount); + + let initial_balance = SubtensorModule::get_coldkey_balance(&coldkey_account_id); + + // Setup limit price at 2.0 TAO per Alpha + // With 100 TAO into 1000/1000 pool, price moves from 1.0 to ~1.21 + let limit_price = TaoCurrency::from(2_000_000_000); // 2.0 TAO per Alpha + + // Execute add_stake_burn with limit + assert_ok!(SubtensorModule::add_stake_burn( + RuntimeOrigin::signed(coldkey_account_id), + hotkey_account_id, + netuid, + amount.into(), + Some(limit_price), + )); + + // After "add stake and burn", hotkey should have zero stake since alpha is burned immediately + assert_eq!( + SubtensorModule::get_total_stake_for_hotkey(&hotkey_account_id), + TaoCurrency::ZERO + ); + + // TAO should have been spent + let final_balance = SubtensorModule::get_coldkey_balance(&coldkey_account_id); + assert!( + final_balance < initial_balance, + "TAO should have been spent" + ); + + // Final price should be between initial (1.0) and limit (2.0) + let final_price = + ::SwapInterface::current_alpha_price(netuid.into()); + assert!( + final_price.to_num::() >= 1.0 && final_price.to_num::() <= 2.0, + "Final price {} should be between 1.0 and 2.0", + final_price.to_num::() + ); + + // Verify AlphaBurned event was emitted + assert!(System::events().iter().any(|e| { + matches!( + &e.event, + RuntimeEvent::SubtensorModule(Event::AlphaBurned(..)) + ) + })); + }); +} + +#[test] +fn test_add_stake_burn_non_owner_fails() { + new_test_ext(1).execute_with(|| { + let hotkey_account_id = U256::from(1); + let coldkey_account_id = U256::from(2); + let non_owner_coldkey = U256::from(3); + let amount = DefaultMinStake::::get().to_u64() * 10; + + // Add network with coldkey_account_id as owner + let netuid = add_dynamic_network(&hotkey_account_id, &coldkey_account_id); + + mock::setup_reserves( + netuid, + (amount * 1_000_000).into(), + (amount * 10_000_000).into(), + ); + + // Give non-owner some balance + SubtensorModule::add_balance_to_coldkey_account(&non_owner_coldkey, amount); + + // Non-owner trying to call add_stake_burn should fail with BadOrigin + assert_noop!( + SubtensorModule::add_stake_burn( + RuntimeOrigin::signed(non_owner_coldkey), + hotkey_account_id, + netuid, + amount.into(), + None, + ), + DispatchError::BadOrigin + ); + }); +} + +#[test] +fn test_add_stake_burn_nonexistent_subnet_fails() { + new_test_ext(1).execute_with(|| { + let hotkey_account_id = U256::from(1); + let coldkey_account_id = U256::from(2); + let amount = DefaultMinStake::::get().to_u64() * 10; + + // Give some balance + SubtensorModule::add_balance_to_coldkey_account(&coldkey_account_id, amount); + + // Try to call add_stake_burn on non-existent subnet + let nonexistent_netuid = NetUid::from(999); + assert_noop!( + SubtensorModule::add_stake_burn( + RuntimeOrigin::signed(coldkey_account_id), + hotkey_account_id, + nonexistent_netuid, + amount.into(), + None, + ), + DispatchError::BadOrigin + ); + }); +} + +#[test] +fn test_add_stake_burn_insufficient_balance_fails() { + new_test_ext(1).execute_with(|| { + let hotkey_account_id = U256::from(1); + let coldkey_account_id = U256::from(2); + let amount = DefaultMinStake::::get().to_u64() * 10; + + // Add network + let netuid = add_dynamic_network(&hotkey_account_id, &coldkey_account_id); + + mock::setup_reserves( + netuid, + (amount * 1_000_000).into(), + (amount * 10_000_000).into(), + ); + + // Try to call add_stake_burn without sufficient balance + assert_noop!( + SubtensorModule::add_stake_burn( + RuntimeOrigin::signed(coldkey_account_id), + hotkey_account_id, + netuid, + amount.into(), + None, + ), + Error::::NotEnoughBalanceToStake + ); + }); +} + +#[test] +fn test_add_stake_burn_rate_limit_exceeded() { + new_test_ext(1).execute_with(|| { + let hotkey_account_id = U256::from(533453); + let coldkey_account_id = U256::from(55453); + let amount: u64 = 10_000_000_000; // 10 TAO + + // Add network + let netuid = add_dynamic_network(&hotkey_account_id, &coldkey_account_id); + + // Setup reserves with large liquidity + let tao_reserve = TaoCurrency::from(1_000_000_000_000); + let alpha_in = AlphaCurrency::from(1_000_000_000_000); + mock::setup_reserves(netuid, tao_reserve, alpha_in); + + // Give coldkey sufficient balance for multiple "add stake and burn" operations. + SubtensorModule::add_balance_to_coldkey_account(&coldkey_account_id, amount * 10); + + assert_eq!( + SubtensorModule::get_rate_limited_last_block(&RateLimitKey::AddStakeBurn(netuid)), + 0 + ); + + // First "add stake and burn" should succeed + assert_ok!(SubtensorModule::add_stake_burn( + RuntimeOrigin::signed(coldkey_account_id), + hotkey_account_id, + netuid, + amount.into(), + None, + )); + + assert_eq!( + SubtensorModule::get_rate_limited_last_block(&RateLimitKey::AddStakeBurn(netuid)), + SubtensorModule::get_current_block_as_u64() + ); + + // Second "add stake and burn" immediately after should fail due to rate limit + assert_noop!( + SubtensorModule::add_stake_burn( + RuntimeOrigin::signed(coldkey_account_id), + hotkey_account_id, + netuid, + amount.into(), + None, + ), + Error::::AddStakeBurnRateLimitExceeded + ); + + // After stepping past the rate limit, "add stake and burn" should succeed again + let rate_limit = TransactionType::AddStakeBurn.rate_limit_on_subnet::(netuid); + step_block(rate_limit as u16); + + assert_ok!(SubtensorModule::add_stake_burn( + RuntimeOrigin::signed(coldkey_account_id), + hotkey_account_id, + netuid, + amount.into(), + None, + )); + }); +} diff --git a/pallets/subtensor/src/tests/registration.rs b/pallets/subtensor/src/tests/registration.rs index c82e173907..635b996cea 100644 --- a/pallets/subtensor/src/tests/registration.rs +++ b/pallets/subtensor/src/tests/registration.rs @@ -13,7 +13,7 @@ use subtensor_runtime_common::{AlphaCurrency, Currency as CurrencyT, NetUid, Net use super::mock; use super::mock::*; -use crate::transaction_extension::SubtensorTransactionExtension; +use crate::extensions::SubtensorTransactionExtension; use crate::{AxonInfoOf, CustomTransactionError, Error}; /******************************************** diff --git a/pallets/subtensor/src/tests/serving.rs b/pallets/subtensor/src/tests/serving.rs index b52666bf26..552af372e3 100644 --- a/pallets/subtensor/src/tests/serving.rs +++ b/pallets/subtensor/src/tests/serving.rs @@ -2,7 +2,7 @@ use super::mock::*; use crate::Error; -use crate::transaction_extension::SubtensorTransactionExtension; +use crate::extensions::SubtensorTransactionExtension; use crate::*; use frame_support::assert_noop; use frame_support::{ diff --git a/pallets/subtensor/src/tests/staking.rs b/pallets/subtensor/src/tests/staking.rs index 6c7e18b707..86ba26c2e2 100644 --- a/pallets/subtensor/src/tests/staking.rs +++ b/pallets/subtensor/src/tests/staking.rs @@ -6,9 +6,9 @@ use frame_support::dispatch::{DispatchClass, DispatchInfo, GetDispatchInfo, Pays use frame_support::sp_runtime::DispatchError; use frame_support::{assert_err, assert_noop, assert_ok, traits::Currency}; use frame_system::RawOrigin; -use pallet_subtensor_swap::tick::TickIndex; use safe_math::FixedExt; use sp_core::{Get, H256, U256}; +// use sp_runtime::traits::Dispatchable; use substrate_fixed::traits::FromFixed; use substrate_fixed::types::{I96F32, I110F18, U64F64, U96F32}; use subtensor_runtime_common::{ @@ -572,13 +572,7 @@ fn test_add_stake_partial_below_min_stake_fails() { mock::setup_reserves(netuid, (amount * 10).into(), (amount * 10).into()); // Force the swap to initialize - SubtensorModule::swap_tao_for_alpha( - netuid, - TaoCurrency::ZERO, - 1_000_000_000_000.into(), - false, - ) - .unwrap(); + ::SwapInterface::init_swap(netuid, None); // Get the current price (should be 1.0) let current_price = @@ -692,6 +686,9 @@ fn test_remove_stake_total_balance_no_change() { let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); register_ok_neuron(netuid, hotkey_account_id, coldkey_account_id, 192213123); + // Set fee rate to 0 so that alpha fee is not moved to block producer + pallet_subtensor_swap::FeeRate::::insert(netuid, 0); + // Some basic assertions assert_eq!( SubtensorModule::get_total_stake(), @@ -714,8 +711,10 @@ fn test_remove_stake_total_balance_no_change() { ); // Add subnet TAO for the equivalent amount added at price - let amount_tao = U96F32::saturating_from_num(amount) - * ::SwapInterface::current_alpha_price(netuid.into()); + let amount_tao = U96F32::from_num(amount) + * U96F32::from_num( + ::SwapInterface::current_alpha_price(netuid.into()), + ); SubnetTAO::::mutate(netuid, |v| { *v += amount_tao.saturating_to_num::().into() }); @@ -800,7 +799,7 @@ fn test_add_stake_insufficient_liquidity_one_side_ok() { SubtensorModule::add_balance_to_coldkey_account(&coldkey, amount_staked); // Set the liquidity at lowest possible value so that all staking requests fail - let reserve_alpha = u64::from(mock::SwapMinimumReserve::get()); + let reserve_alpha = 1_000_000_000_u64; let reserve_tao = u64::from(mock::SwapMinimumReserve::get()) - 1; mock::setup_reserves(netuid, reserve_tao.into(), reserve_alpha.into()); @@ -884,9 +883,9 @@ fn test_remove_stake_insufficient_liquidity() { Error::::InsufficientLiquidity ); - // Mock provided liquidity - remove becomes successful - SubnetTaoProvided::::insert(netuid, TaoCurrency::from(amount_staked + 1)); - SubnetAlphaInProvided::::insert(netuid, AlphaCurrency::from(1)); + // Mock more liquidity - remove becomes successful + SubnetTAO::::insert(netuid, TaoCurrency::from(amount_staked + 1)); + SubnetAlphaIn::::insert(netuid, AlphaCurrency::from(1)); assert_ok!(SubtensorModule::remove_stake( RuntimeOrigin::signed(coldkey), hotkey, @@ -910,6 +909,9 @@ fn test_remove_stake_total_issuance_no_change() { let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); register_ok_neuron(netuid, hotkey_account_id, coldkey_account_id, 192213123); + // Set fee rate to 0 so that alpha fee is not moved to block producer + pallet_subtensor_swap::FeeRate::::insert(netuid, 0); + // Give it some $$$ in his coldkey balance SubtensorModule::add_balance_to_coldkey_account(&coldkey_account_id, amount); @@ -975,7 +977,7 @@ fn test_remove_stake_total_issuance_no_change() { assert_abs_diff_eq!( SubtensorModule::get_total_stake(), SubtensorModule::get_network_min_lock() + total_fee.into(), - epsilon = TaoCurrency::from(fee) / 1000.into() + epsilon = TaoCurrency::from(fee) / 1000.into() + 1.into() ); // Check if total issuance is equal to the added stake, even after remove stake (no fee, @@ -1637,6 +1639,9 @@ fn test_clear_small_nominations() { let fee = DefaultMinStake::::get().to_u64(); let init_balance = amount + fee + ExistentialDeposit::get(); + // Set fee rate to 0 so that alpha fee is not moved to block producer + pallet_subtensor_swap::FeeRate::::insert(netuid, 0); + // Register hot1. register_ok_neuron(netuid, hot1, cold1, 0); Delegates::::insert(hot1, SubtensorModule::get_min_delegate_take()); @@ -2176,8 +2181,9 @@ fn test_get_total_delegated_stake_after_unstaking() { netuid, unstake_amount_alpha.into() )); - let current_price = - ::SwapInterface::current_alpha_price(netuid.into()); + let current_price = U96F32::from_num( + ::SwapInterface::current_alpha_price(netuid.into()), + ); // Calculate the expected delegated stake let unstake_amount = @@ -2797,7 +2803,7 @@ fn test_max_amount_add_stable() { // cargo test --package pallet-subtensor --lib -- tests::staking::test_max_amount_add_dynamic --exact --show-output #[test] fn test_max_amount_add_dynamic() { - // tao_in, alpha_in, limit_price, expected_max_swappable + // tao_in, alpha_in, limit_price, expected_max_swappable (with 0.05% fees) [ // Zero handling (no panics) ( @@ -2811,16 +2817,16 @@ fn test_max_amount_add_dynamic() { // Low bounds (100, 100, 1_100_000_000, Ok(4)), (1_000, 1_000, 1_100_000_000, Ok(48)), - (10_000, 10_000, 1_100_000_000, Ok(489)), + (10_000, 10_000, 1_100_000_000, Ok(488)), // Basic math - (1_000_000, 1_000_000, 4_000_000_000, Ok(1_000_000)), - (1_000_000, 1_000_000, 9_000_000_000, Ok(2_000_000)), - (1_000_000, 1_000_000, 16_000_000_000, Ok(3_000_000)), + (1_000_000, 1_000_000, 4_000_000_000, Ok(1_000_500)), + (1_000_000, 1_000_000, 9_000_000_000, Ok(2_001_000)), + (1_000_000, 1_000_000, 16_000_000_000, Ok(3_001_500)), ( 1_000_000_000_000, 1_000_000_000_000, 16_000_000_000, - Ok(3_000_000_000_000), + Ok(3_001_500_000_000), ), // Normal range values with edge cases ( @@ -2855,13 +2861,20 @@ fn test_max_amount_add_dynamic() { pallet_subtensor_swap::Error::::PriceLimitExceeded, )), ), - (150_000_000_000, 100_000_000_000, 1_500_000_000, Ok(5)), - (150_000_000_000, 100_000_000_000, 1_500_000_001, Ok(51)), + ( + 150_000_000_000, + 100_000_000_000, + 1_500_000_000, + Err(DispatchError::from( + pallet_subtensor_swap::Error::::PriceLimitExceeded, + )), + ), + (150_000_000_000, 100_000_000_000, 1_500_000_001, Ok(49)), ( 150_000_000_000, 100_000_000_000, 6_000_000_000, - Ok(150_000_000_000), + Ok(150_075_000_000), ), // Miscellaneous overflows and underflows (u64::MAX / 2, u64::MAX, u64::MAX, Ok(u64::MAX)), @@ -2879,13 +2892,7 @@ fn test_max_amount_add_dynamic() { SubnetAlphaIn::::insert(netuid, alpha_in); // Force the swap to initialize - SubtensorModule::swap_tao_for_alpha( - netuid, - TaoCurrency::ZERO, - 1_000_000_000_000.into(), - false, - ) - .unwrap(); + ::SwapInterface::init_swap(netuid, None); if !alpha_in.is_zero() { let expected_price = U96F32::from_num(tao_in) / U96F32::from_num(alpha_in); @@ -2905,7 +2912,7 @@ fn test_max_amount_add_dynamic() { Ok(v) => assert_abs_diff_eq!( SubtensorModule::get_max_amount_add(netuid, limit_price.into()).unwrap(), v, - epsilon = v / 100 + epsilon = v / 10000 ), } }); @@ -2999,7 +3006,7 @@ fn test_max_amount_remove_dynamic() { let subnet_owner_hotkey = U256::from(1002); let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); - // tao_in, alpha_in, limit_price, expected_max_swappable + // tao_in, alpha_in, limit_price, expected_max_swappable (+ 0.05% fee) [ // Zero handling (no panics) ( @@ -3021,21 +3028,24 @@ fn test_max_amount_remove_dynamic() { (10_000_000_000, 10_000_000_000, 0, Ok(u64::MAX)), // Low bounds (numbers are empirical, it is only important that result // is sharply decreasing when limit price increases) - (1_000, 1_000, 0, Ok(4_308_000_000_000)), - (1_001, 1_001, 0, Ok(4_310_000_000_000)), - (1_001, 1_001, 1, Ok(31_750_000)), - (1_001, 1_001, 2, Ok(22_500_000)), - (1_001, 1_001, 1_001, Ok(1_000_000)), - (1_001, 1_001, 10_000, Ok(316_000)), - (1_001, 1_001, 100_000, Ok(100_000)), + (1_000, 1_000, 0, Ok(u64::MAX)), + (1_001, 1_001, 0, Ok(u64::MAX)), + (1_001, 1_001, 1, Ok(17_646)), + (1_001, 1_001, 2, Ok(17_646)), + (1_001, 1_001, 1_001, Ok(17_646)), + (1_001, 1_001, 10_000, Ok(17_646)), + (1_001, 1_001, 100_000, Ok(17_646)), + (1_001, 1_001, 1_000_000, Ok(17_646)), + (1_001, 1_001, 10_000_000, Ok(9_103)), + (1_001, 1_001, 100_000_000, Ok(2_186)), // Basic math - (1_000_000, 1_000_000, 250_000_000, Ok(1_000_000)), - (1_000_000, 1_000_000, 62_500_000, Ok(3_000_000)), + (1_000_000, 1_000_000, 250_000_000, Ok(1_010_000)), + (1_000_000, 1_000_000, 62_500_000, Ok(3_030_000)), ( 1_000_000_000_000, 1_000_000_000_000, 62_500_000, - Ok(3_000_000_000_000), + Ok(3_030_000_000_000), ), // Normal range values with edge cases and sanity checks (200_000_000_000, 100_000_000_000, 0, Ok(u64::MAX)), @@ -3043,13 +3053,13 @@ fn test_max_amount_remove_dynamic() { 200_000_000_000, 100_000_000_000, 500_000_000, - Ok(100_000_000_000), + Ok(101_000_000_000), ), ( 200_000_000_000, 100_000_000_000, 125_000_000, - Ok(300_000_000_000), + Ok(303_000_000_000), ), ( 200_000_000_000, @@ -3068,15 +3078,15 @@ fn test_max_amount_remove_dynamic() { )), ), (200_000_000_000, 100_000_000_000, 1_999_999_999, Ok(24)), - (200_000_000_000, 100_000_000_000, 1_999_999_990, Ok(252)), + (200_000_000_000, 100_000_000_000, 1_999_999_990, Ok(250)), // Miscellaneous overflows and underflows ( 21_000_000_000_000_000, 1_000_000, 21_000_000_000_000_000, - Ok(30_700_000), + Ok(17_630_088), ), - (21_000_000_000_000_000, 1_000_000, u64::MAX, Ok(67_164)), + (21_000_000_000_000_000, 1_000_000, u64::MAX, Ok(67_000)), ( 21_000_000_000_000_000, 1_000_000_000_000_000_000, @@ -3089,13 +3099,13 @@ fn test_max_amount_remove_dynamic() { 21_000_000_000_000_000, 1_000_000_000_000_000_000, 20_000_000, - Ok(24_800_000_000_000_000), + Ok(24_700_000_000_000_000), ), ( 21_000_000_000_000_000, 21_000_000_000_000_000, 999_999_999, - Ok(10_500_000), + Ok(10_605_000), ), ( 21_000_000_000_000_000, @@ -3112,7 +3122,7 @@ fn test_max_amount_remove_dynamic() { SubnetAlphaIn::::insert(netuid, alpha_in); if !alpha_in.is_zero() { - let expected_price = I96F32::from_num(tao_in) / I96F32::from_num(alpha_in); + let expected_price = U64F64::from_num(tao_in) / U64F64::from_num(alpha_in); assert_eq!( ::SwapInterface::current_alpha_price(netuid.into()), expected_price @@ -3301,7 +3311,7 @@ fn test_max_amount_move_stable_dynamic() { dynamic_netuid, TaoCurrency::from(2_000_000_000) ), - Err(Error::::ZeroMaxStakeAmount.into()) + Err(pallet_subtensor_swap::Error::::PriceLimitExceeded.into()) ); // 3.0 price => max is 0 @@ -3434,8 +3444,8 @@ fn test_max_amount_move_dynamic_stable() { assert_abs_diff_eq!( SubtensorModule::get_max_amount_move(dynamic_netuid, stable_netuid, 375_000_000.into()) .unwrap(), - alpha_in, - epsilon = alpha_in / 1000.into(), + alpha_in + alpha_in / 2000.into(), // + 0.05% fee + epsilon = alpha_in / 10_000.into(), ); // Precision test: @@ -3671,29 +3681,27 @@ fn test_max_amount_move_dynamic_dynamic() { expected_max_swappable, precision, )| { - let alpha_in_1 = AlphaCurrency::from(alpha_in_1); - let alpha_in_2 = AlphaCurrency::from(alpha_in_2); let expected_max_swappable = AlphaCurrency::from(expected_max_swappable); // Forse-set alpha in and tao reserve to achieve relative price of subnets SubnetTAO::::insert(origin_netuid, TaoCurrency::from(tao_in_1)); - SubnetAlphaIn::::insert(origin_netuid, alpha_in_1); + SubnetAlphaIn::::insert(origin_netuid, AlphaCurrency::from(alpha_in_1)); SubnetTAO::::insert(destination_netuid, TaoCurrency::from(tao_in_2)); - SubnetAlphaIn::::insert(destination_netuid, alpha_in_2); + SubnetAlphaIn::::insert(destination_netuid, AlphaCurrency::from(alpha_in_2)); if !alpha_in_1.is_zero() && !alpha_in_2.is_zero() { - let origin_price = - I96F32::from_num(tao_in_1) / I96F32::from_num(u64::from(alpha_in_1)); - let dest_price = - I96F32::from_num(tao_in_2) / I96F32::from_num(u64::from(alpha_in_2)); - if dest_price != 0 { + let origin_price = tao_in_1 as f64 / alpha_in_1 as f64; + let dest_price = tao_in_2 as f64 / alpha_in_2 as f64; + if dest_price != 0. { let expected_price = origin_price / dest_price; - assert_eq!( - ::SwapInterface::current_alpha_price( + assert_abs_diff_eq!( + (::SwapInterface::current_alpha_price( origin_netuid.into() ) / ::SwapInterface::current_alpha_price( destination_netuid.into() - ), - expected_price + )) + .to_num::(), + expected_price, + epsilon = 0.000_000_001 ); } } @@ -3790,7 +3798,7 @@ fn test_add_stake_limit_fill_or_kill() { new_test_ext(1).execute_with(|| { let hotkey_account_id = U256::from(533453); let coldkey_account_id = U256::from(55453); - let amount = 900_000_000_000; // over the maximum + let amount = 300_000_000_000; // over the maximum // add network let netuid = add_dynamic_network(&hotkey_account_id, &coldkey_account_id); @@ -3810,9 +3818,7 @@ fn test_add_stake_limit_fill_or_kill() { SubtensorModule::add_balance_to_coldkey_account(&coldkey_account_id, amount); // Setup limit price so that it doesn't peak above 4x of current price - // The amount that can be executed at this price is 450 TAO only - // Alpha produced will be equal to 25 = 100 - 450*100/(150+450) - let limit_price = TaoCurrency::from(24_000_000_000); + let limit_price = TaoCurrency::from(6_000_000_000); // Add stake with slippage safety and check if it fails assert_noop!( @@ -3828,7 +3834,7 @@ fn test_add_stake_limit_fill_or_kill() { ); // Lower the amount and it should succeed now - let amount_ok = TaoCurrency::from(450_000_000_000); // fits the maximum + let amount_ok = TaoCurrency::from(150_000_000_000); // fits the maximum assert_ok!(SubtensorModule::add_stake_limit( RuntimeOrigin::signed(coldkey_account_id), hotkey_account_id, @@ -4091,6 +4097,10 @@ fn test_remove_99_9991_per_cent_stake_removes_all() { let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); register_ok_neuron(netuid, hotkey_account_id, coldkey_account_id, 192213123); + // Set fee rate to 0 so that alpha fee is not moved to block producer + // and the hotkey stake does drop to 0 + pallet_subtensor_swap::FeeRate::::insert(netuid, 0); + // Give it some $$$ in his coldkey balance SubtensorModule::add_balance_to_coldkey_account(&coldkey_account_id, amount); @@ -4152,6 +4162,10 @@ fn test_remove_99_9989_per_cent_stake_leaves_a_little() { let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); register_ok_neuron(netuid, hotkey_account_id, coldkey_account_id, 192213123); + // Set fee rate to 0 so that alpha fee is not moved to block producer + // to avoid false success in this test + pallet_subtensor_swap::FeeRate::::insert(netuid, 0); + // Give it some $$$ in his coldkey balance SubtensorModule::add_balance_to_coldkey_account(&coldkey_account_id, amount); @@ -4560,13 +4574,15 @@ fn test_stake_into_subnet_low_amount() { false, false, )); - let expected_stake = AlphaCurrency::from(((amount as f64) * 0.997 / current_price) as u64); + let expected_stake = (amount as f64) * 0.997 / current_price; // Check if stake has increased assert_abs_diff_eq!( - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &coldkey, netuid), + u64::from(SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, &coldkey, netuid + )) as f64, expected_stake, - epsilon = expected_stake / 100.into() + epsilon = expected_stake / 100. ); }); } @@ -4838,40 +4854,16 @@ fn test_unstake_full_amount() { }); } -fn price_to_tick(price: f64) -> TickIndex { - let price_sqrt: U64F64 = U64F64::from_num(price.sqrt()); - // Handle potential errors in the conversion - match TickIndex::try_from_sqrt_price(price_sqrt) { - Ok(mut tick) => { - // Ensure the tick is within bounds - if tick > TickIndex::MAX { - tick = TickIndex::MAX; - } else if tick < TickIndex::MIN { - tick = TickIndex::MIN; - } - tick - } - // Default to a reasonable value when conversion fails - Err(_) => { - if price > 1.0 { - TickIndex::MAX - } else { - TickIndex::MIN - } - } - } -} - /// Test correctness of swap fees: /// 1. TAO is not minted or burned /// 2. Fees match FeeRate -/// #[test] fn test_swap_fees_tao_correctness() { new_test_ext(1).execute_with(|| { let owner_hotkey = U256::from(1); let owner_coldkey = U256::from(2); let coldkey = U256::from(4); + let block_builder = U256::from(12345u64); let amount = 1_000_000_000; let owner_balance_before = amount * 10; let user_balance_before = amount * 100; @@ -4880,9 +4872,6 @@ fn test_swap_fees_tao_correctness() { let netuid = add_dynamic_network(&owner_hotkey, &owner_coldkey); SubtensorModule::add_balance_to_coldkey_account(&owner_coldkey, owner_balance_before); SubtensorModule::add_balance_to_coldkey_account(&coldkey, user_balance_before); - let fee_rate = pallet_subtensor_swap::FeeRate::::get(NetUid::from(netuid)) as f64 - / u16::MAX as f64; - pallet_subtensor_swap::EnabledUserLiquidity::::insert(NetUid::from(netuid), true); // Forse-set alpha in and tao reserve to make price equal 0.25 let tao_reserve = TaoCurrency::from(100_000_000_000); @@ -4890,8 +4879,11 @@ fn test_swap_fees_tao_correctness() { mock::setup_reserves(netuid, tao_reserve, alpha_in); // Check starting "total TAO" - let total_tao_before = - user_balance_before + owner_balance_before + SubnetTAO::::get(netuid).to_u64(); + let block_builder_balance_before = SubtensorModule::get_coldkey_balance(&block_builder); + let total_tao_before = user_balance_before + + owner_balance_before + + SubnetTAO::::get(netuid).to_u64() + + block_builder_balance_before; // Get alpha for owner assert_ok!(SubtensorModule::add_stake( @@ -4900,7 +4892,6 @@ fn test_swap_fees_tao_correctness() { netuid, amount.into(), )); - let mut fees = (fee_rate * amount as f64) as u64; // Add owner coldkey Alpha as concentrated liquidity // between current price current price + 0.01 @@ -4909,18 +4900,6 @@ fn test_swap_fees_tao_correctness() { .to_num::() + 0.0001; let limit_price = current_price + 0.01; - let tick_low = price_to_tick(current_price); - let tick_high = price_to_tick(limit_price); - let liquidity = amount; - - assert_ok!(::SwapInterface::do_add_liquidity( - netuid.into(), - &owner_coldkey, - &owner_hotkey, - tick_low, - tick_high, - liquidity, - )); // Limit-buy and then sell all alpha for user to hit owner liquidity assert_ok!(SubtensorModule::add_stake_limit( @@ -4931,7 +4910,6 @@ fn test_swap_fees_tao_correctness() { ((limit_price * u64::MAX as f64) as u64).into(), true )); - fees += (fee_rate * amount as f64) as u64; let user_alpha = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( &owner_hotkey, @@ -4945,15 +4923,25 @@ fn test_swap_fees_tao_correctness() { netuid, user_alpha, )); - // Do not add fees because selling fees are in alpha + + // Cause tao fees to propagate to SubnetTAO + let (claimed_tao_fees, _) = + ::SwapInterface::adjust_protocol_liquidity( + netuid, + 0.into(), + 0.into(), + ); + SubnetTAO::::mutate(netuid, |tao| *tao += claimed_tao_fees); // Check ending "total TAO" let owner_balance_after = SubtensorModule::get_coldkey_balance(&owner_coldkey); let user_balance_after = SubtensorModule::get_coldkey_balance(&coldkey); + let block_builder_balance_after = SubtensorModule::get_coldkey_balance(&block_builder); + let total_tao_after = user_balance_after + owner_balance_after + SubnetTAO::::get(netuid).to_u64() - + fees; + + block_builder_balance_after; // Total TAO does not change, leave some epsilon for rounding assert_abs_diff_eq!(total_tao_before, total_tao_after, epsilon = 2); @@ -5051,7 +5039,7 @@ fn test_remove_stake_full_limit_ok() { ); let new_balance = SubtensorModule::get_coldkey_balance(&coldkey_account_id); - assert_abs_diff_eq!(new_balance, 9_086_000_000, epsilon = 1_000_000); + assert_abs_diff_eq!(new_balance, 9_086_700_000, epsilon = 1_000_000); }); } @@ -5135,9 +5123,10 @@ fn test_remove_stake_full_limit_ok_with_no_limit_price() { ); let new_balance = SubtensorModule::get_coldkey_balance(&coldkey_account_id); - assert_abs_diff_eq!(new_balance, 9_086_000_000, epsilon = 1_000_000); + assert_abs_diff_eq!(new_balance, 9_086_700_000, epsilon = 1_000_000); }); } + /// This test verifies that minimum stake amount is sufficient to move price and apply /// non-zero staking fees #[test] @@ -5200,181 +5189,6 @@ fn test_default_min_stake_sufficiency() { }); } -/// Test that modify_position always credits fees -/// -/// cargo test --package pallet-subtensor --lib -- tests::staking::test_update_position_fees --exact --show-output -#[test] -fn test_update_position_fees() { - // Test cases: add or remove liquidity during modification - [false, true].into_iter().for_each(|add| { - new_test_ext(1).execute_with(|| { - let owner_hotkey = U256::from(1); - let owner_coldkey = U256::from(2); - let coldkey = U256::from(4); - let amount = 1_000_000_000; - - // add network - let netuid = add_dynamic_network(&owner_hotkey, &owner_coldkey); - SubtensorModule::add_balance_to_coldkey_account(&owner_coldkey, amount * 10); - SubtensorModule::add_balance_to_coldkey_account(&coldkey, amount * 100); - pallet_subtensor_swap::EnabledUserLiquidity::::insert(NetUid::from(netuid), true); - - // Forse-set alpha in and tao reserve to make price equal 0.25 - let tao_reserve = TaoCurrency::from(100_000_000_000); - let alpha_in = AlphaCurrency::from(400_000_000_000); - mock::setup_reserves(netuid, tao_reserve, alpha_in); - - // Get alpha for owner - assert_ok!(SubtensorModule::add_stake( - RuntimeOrigin::signed(owner_coldkey), - owner_hotkey, - netuid, - amount.into(), - )); - - // Add owner coldkey Alpha as concentrated liquidity - // between current price current price + 0.01 - let current_price = - ::SwapInterface::current_alpha_price(netuid.into()) - .to_num::() - + 0.0001; - let limit_price = current_price + 0.001; - let tick_low = price_to_tick(current_price); - let tick_high = price_to_tick(limit_price); - let liquidity = amount; - - let (position_id, _, _) = ::SwapInterface::do_add_liquidity( - NetUid::from(netuid), - &owner_coldkey, - &owner_hotkey, - tick_low, - tick_high, - liquidity, - ) - .unwrap(); - - // Buy and then sell all alpha for user to hit owner liquidity - assert_ok!(SubtensorModule::add_stake( - RuntimeOrigin::signed(coldkey), - owner_hotkey, - netuid, - amount.into(), - )); - - remove_stake_rate_limit_for_tests(&owner_hotkey, &coldkey, netuid); - - let user_alpha = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &owner_hotkey, - &coldkey, - netuid, - ); - assert_ok!(SubtensorModule::remove_stake( - RuntimeOrigin::signed(coldkey), - owner_hotkey, - netuid, - user_alpha, - )); - - // Modify position - fees should be collected and paid to the owner - let owner_tao_before = SubtensorModule::get_coldkey_balance(&owner_coldkey); - let owner_alpha_before = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &owner_hotkey, - &owner_coldkey, - netuid, - ); - - // Make small modification - let delta = - ::MinimumLiquidity::get() - as i64 - * (if add { 1 } else { -1 }); - assert_ok!(Swap::modify_position( - RuntimeOrigin::signed(owner_coldkey), - owner_hotkey, - netuid.into(), - position_id.into(), - delta, - )); - - // Check ending owner TAO and alpha - let owner_tao_after_add = SubtensorModule::get_coldkey_balance(&owner_coldkey); - let owner_alpha_after_add = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &owner_hotkey, - &owner_coldkey, - netuid, - ); - - assert!(owner_tao_after_add > owner_tao_before); - assert!(owner_alpha_after_add > owner_alpha_before); // always greater because of claimed fees - - // Make small modification again - should not claim more fees - assert_ok!(Swap::modify_position( - RuntimeOrigin::signed(owner_coldkey), - owner_hotkey, - netuid.into(), - position_id.into(), - delta, - )); - - // Check ending owner TAO and alpha - let owner_tao_after_repeat = SubtensorModule::get_coldkey_balance(&owner_coldkey); - let owner_alpha_after_repeat = - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &owner_hotkey, - &owner_coldkey, - netuid, - ); - - assert!(owner_tao_after_add == owner_tao_after_repeat); - if add { - assert!(owner_alpha_after_add > owner_alpha_after_repeat); - } else { - assert!(owner_alpha_after_add < owner_alpha_after_repeat); - } - }); - }); -} - -// TODO: Revise when user liquidity is available -// fn setup_positions(netuid: NetUid) { -// for (coldkey, hotkey, low_price, high_price, liquidity) in [ -// (2, 12, 0.1, 0.20, 1_000_000_000_000_u64), -// (3, 13, 0.15, 0.25, 200_000_000_000_u64), -// (4, 14, 0.25, 0.5, 3_000_000_000_000_u64), -// (5, 15, 0.3, 0.6, 300_000_000_000_u64), -// (6, 16, 0.4, 0.7, 8_000_000_000_000_u64), -// (7, 17, 0.5, 0.8, 600_000_000_000_u64), -// (8, 18, 0.6, 0.9, 700_000_000_000_u64), -// (9, 19, 0.7, 1.0, 100_000_000_000_u64), -// (10, 20, 0.8, 1.1, 300_000_000_000_u64), -// ] { -// SubtensorModule::create_account_if_non_existent(&U256::from(coldkey), &U256::from(hotkey)); -// SubtensorModule::add_balance_to_coldkey_account( -// &U256::from(coldkey), -// 1_000_000_000_000_000, -// ); -// SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( -// &U256::from(hotkey), -// &U256::from(coldkey), -// netuid.into(), -// 1_000_000_000_000_000.into(), -// ); - -// let tick_low = price_to_tick(low_price); -// let tick_high = price_to_tick(high_price); -// let add_lq_call = SwapCall::::add_liquidity { -// hotkey: U256::from(hotkey), -// netuid: netuid.into(), -// tick_low, -// tick_high, -// liquidity, -// }; -// assert_ok!( -// RuntimeCall::Swap(add_lq_call).dispatch(RuntimeOrigin::signed(U256::from(coldkey))) -// ); -// } -// } - #[test] fn test_large_swap() { new_test_ext(1).execute_with(|| { @@ -5385,19 +5199,13 @@ fn test_large_swap() { // add network let netuid = add_dynamic_network(&owner_hotkey, &owner_coldkey); SubtensorModule::add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_000); - pallet_subtensor_swap::EnabledUserLiquidity::::insert(NetUid::from(netuid), true); + let tao = TaoCurrency::from(100_000_000u64); + let alpha = AlphaCurrency::from(1_000_000_000_000_000_u64); + SubnetTAO::::insert(netuid, tao); + SubnetAlphaIn::::insert(netuid, alpha); // Force the swap to initialize - SubtensorModule::swap_tao_for_alpha( - netuid, - TaoCurrency::ZERO, - 1_000_000_000_000.into(), - false, - ) - .unwrap(); - - // TODO: Revise when user liquidity is available - // setup_positions(netuid.into()); + ::SwapInterface::init_swap(netuid, None); let swap_amount = TaoCurrency::from(100_000_000_000_000); assert_ok!(SubtensorModule::add_stake( @@ -5638,11 +5446,15 @@ fn test_staking_records_flow() { )); // Check that outflow has been recorded (less unstaking fees) - let expected_unstake_fee = expected_flow * fee_rate; + // The block builder will receive a fraction of the fees in alpha and will be forced + // to unstake it. So, the additional out-flow is recorded for this. + let unstaked_block_builder_fraction = 1.; + let expected_unstake_fee = + expected_flow * fee_rate * (1. - unstaked_block_builder_fraction); assert_abs_diff_eq!( SubnetTaoFlow::::get(netuid), expected_unstake_fee as i64, - epsilon = (expected_unstake_fee / 100.0) as i64 + epsilon = ((expected_unstake_fee / 100.0) as i64).max(1) ); }); } diff --git a/pallets/subtensor/src/tests/subnet.rs b/pallets/subtensor/src/tests/subnet.rs index a547b30a14..4eb13fe5fb 100644 --- a/pallets/subtensor/src/tests/subnet.rs +++ b/pallets/subtensor/src/tests/subnet.rs @@ -714,63 +714,6 @@ fn test_subtoken_enable_ok_for_burn_register_before_enable() { }); } -// #[test] -// fn test_user_liquidity_access_control() { -// new_test_ext(1).execute_with(|| { -// let owner_hotkey = U256::from(1); -// let owner_coldkey = U256::from(2); -// let not_owner = U256::from(999); // arbitrary non-owner - -// // add network -// let netuid = add_dynamic_network(&owner_hotkey, &owner_coldkey); - -// // Not owner, not root: should fail -// assert_noop!( -// Swap::toggle_user_liquidity(RuntimeOrigin::signed(not_owner), netuid, true), -// DispatchError::BadOrigin -// ); - -// // Subnet owner can enable -// assert_ok!(Swap::toggle_user_liquidity( -// RuntimeOrigin::signed(owner_coldkey), -// netuid, -// true -// )); -// assert!(pallet_subtensor_swap::EnabledUserLiquidity::::get( -// NetUid::from(netuid) -// )); - -// // Root can disable -// assert_ok!(Swap::toggle_user_liquidity( -// RuntimeOrigin::root(), -// netuid, -// false -// )); -// assert!(!pallet_subtensor_swap::EnabledUserLiquidity::::get( -// NetUid::from(netuid) -// )); - -// // Root can enable again -// assert_ok!(Swap::toggle_user_liquidity( -// RuntimeOrigin::root(), -// netuid, -// true -// )); -// assert!(pallet_subtensor_swap::EnabledUserLiquidity::::get( -// NetUid::from(netuid) -// )); - -// // Subnet owner cannot disable (only root can disable) -// assert_noop!( -// Swap::toggle_user_liquidity(RuntimeOrigin::signed(owner_coldkey), netuid, false), -// DispatchError::BadOrigin -// ); -// assert!(pallet_subtensor_swap::EnabledUserLiquidity::::get( -// NetUid::from(netuid) -// )); -// }); -// } - // cargo test --package pallet-subtensor --lib -- tests::subnet::test_no_duplicates_in_symbol_static --exact --show-output #[test] fn test_no_duplicates_in_symbol_static() { diff --git a/pallets/subtensor/src/tests/swap_coldkey.rs b/pallets/subtensor/src/tests/swap_coldkey.rs index 9d3bdbfc62..36d083344c 100644 --- a/pallets/subtensor/src/tests/swap_coldkey.rs +++ b/pallets/subtensor/src/tests/swap_coldkey.rs @@ -3,21 +3,22 @@ clippy::expect_used, clippy::indexing_slicing, clippy::panic, - clippy::unwrap_used + clippy::unwrap_used, + clippy::arithmetic_side_effects )] use approx::assert_abs_diff_eq; use codec::Encode; -use frame_support::dispatch::DispatchInfo; +use frame_support::dispatch::{DispatchInfo, GetDispatchInfo}; use frame_support::error::BadOrigin; use frame_support::traits::OnInitialize; use frame_support::traits::schedule::DispatchTime; use frame_support::traits::schedule::v3::Named as ScheduleNamed; -use frame_support::weights::Weight; use frame_support::{assert_err, assert_noop, assert_ok}; use frame_system::{Config, RawOrigin}; use sp_core::{Get, H256, U256}; -use sp_runtime::traits::{DispatchInfoOf, TransactionExtension}; +use sp_runtime::traits::Hash; +use sp_runtime::traits::{DispatchInfoOf, DispatchTransaction, TransactionExtension}; use sp_runtime::{DispatchError, traits::TxBaseImplication}; use substrate_fixed::types::U96F32; use subtensor_runtime_common::{AlphaCurrency, Currency, SubnetInfo, TaoCurrency}; @@ -25,501 +26,767 @@ use subtensor_swap_interface::{SwapEngine, SwapHandler}; use super::mock; use super::mock::*; -use crate::transaction_extension::SubtensorTransactionExtension; +use crate::extensions::SubtensorTransactionExtension; use crate::*; -use crate::{Call, ColdkeySwapScheduleDuration, Error}; -// // SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --test swap_coldkey -- test_swap_total_hotkey_coldkey_stakes_this_interval --exact --nocapture -// #[test] -// fn test_swap_total_hotkey_coldkey_stakes_this_interval() { -// new_test_ext(1).execute_with(|| { -// let old_coldkey = U256::from(1); -// let new_coldkey = U256::from(2); -// let hotkey = U256::from(3); -// let stake = 100; -// let block = 42; - -// OwnedHotkeys::::insert(old_coldkey, vec![hotkey]); -// TotalHotkeyColdkeyStakesThisInterval::::insert(hotkey, old_coldkey, (stake, block)); - -// let mut weight = Weight::zero(); -// assert_ok!(SubtensorModule::perform_swap_coldkey( -// &old_coldkey, -// &new_coldkey, -// &mut weight -// )); - -// assert!(!TotalHotkeyColdkeyStakesThisInterval::::contains_key( -// hotkey, -// old_coldkey -// )); -// assert_eq!( -// TotalHotkeyColdkeyStakesThisInterval::::get(hotkey, new_coldkey), -// (stake, block) -// ); -// }); -// } - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --test swap_coldkey -- test_swap_subnet_owner --exact --nocapture +use crate::{Call, Error}; + +fn run_to_block(n: u64) { + System::run_to_block_with::( + n, + frame_system::RunToBlockHooks::default().before_finalize(|bn| { + Timestamp::set_timestamp(bn); + }), + ); +} + #[test] -fn test_swap_subnet_owner() { +fn test_announce_coldkey_swap_works() { new_test_ext(1).execute_with(|| { - let old_coldkey = U256::from(1); + let who = U256::from(1); let new_coldkey = U256::from(2); - let netuid = NetUid::from(1u16); + let new_coldkey_hash = ::Hashing::hash_of(&new_coldkey); + let ed = ExistentialDeposit::get(); - add_network(netuid, 1, 0); - SubnetOwner::::insert(netuid, old_coldkey); + assert_eq!(ColdkeySwapAnnouncements::::iter().count(), 0); - let mut weight = Weight::zero(); - assert_ok!(SubtensorModule::perform_swap_coldkey( - &old_coldkey, - &new_coldkey, - &mut weight + let swap_cost = SubtensorModule::get_key_swap_cost().to_u64(); + SubtensorModule::add_balance_to_coldkey_account(&who, swap_cost + ed); + assert_eq!(SubtensorModule::get_coldkey_balance(&who), swap_cost + ed); + + assert_ok!(SubtensorModule::announce_coldkey_swap( + RuntimeOrigin::signed(who), + new_coldkey_hash, )); - assert_eq!(SubnetOwner::::get(netuid), new_coldkey); + let delay = ColdkeySwapAnnouncementDelay::::get(); + let now = System::block_number(); + assert_eq!( + ColdkeySwapAnnouncements::::iter().collect::>(), + vec![(who, (now + delay, new_coldkey_hash))] + ); + assert_eq!(SubtensorModule::get_coldkey_balance(&who), ed); + assert_eq!( + last_event(), + RuntimeEvent::SubtensorModule(Event::ColdkeySwapAnnounced { + who, + new_coldkey_hash, + }) + ); }); } -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::swap_coldkey::test_swap_total_coldkey_stake --exact --show-output #[test] -fn test_swap_total_coldkey_stake() { +fn test_announce_coldkey_swap_with_existing_announcement_past_delay_works() { new_test_ext(1).execute_with(|| { - let old_coldkey = U256::from(1); + let who = U256::from(1); let new_coldkey = U256::from(2); - let other_coldkey = U256::from(3); - let hotkey = U256::from(4); - let other_hotkey = U256::from(5); - let stake = DefaultMinStake::::get().to_u64() * 10; - - let netuid = NetUid::from(1u16); - add_network(netuid, 1, 0); - SubtensorModule::add_balance_to_coldkey_account(&old_coldkey, stake * 2 + 1_000); - register_ok_neuron(netuid, hotkey, old_coldkey, 1001000); - register_ok_neuron(netuid, other_hotkey, other_coldkey, 1001000); + let new_coldkey_hash = ::Hashing::hash_of(&new_coldkey); + let new_coldkey_2 = U256::from(3); + let new_coldkey_2_hash = ::Hashing::hash_of(&new_coldkey_2); - let reserve = stake * 10; - mock::setup_reserves(netuid, reserve.into(), reserve.into()); + assert_eq!(ColdkeySwapAnnouncements::::iter().count(), 0); - assert_ok!(SubtensorModule::add_stake( - <::RuntimeOrigin>::signed(old_coldkey), - hotkey, - netuid, - stake.into() - )); - assert_ok!(SubtensorModule::add_stake( - <::RuntimeOrigin>::signed(old_coldkey), - other_hotkey, - netuid, - stake.into() - )); - let total_stake_before_swap = SubtensorModule::get_total_stake_for_coldkey(&old_coldkey); + let swap_cost = SubtensorModule::get_key_swap_cost().to_u64(); + SubtensorModule::add_balance_to_coldkey_account(&who, 2 * swap_cost); - let mut weight = Weight::zero(); - assert_ok!(SubtensorModule::perform_swap_coldkey( - &old_coldkey, - &new_coldkey, - &mut weight + assert_ok!(SubtensorModule::announce_coldkey_swap( + RuntimeOrigin::signed(who), + new_coldkey_hash, )); + let now = System::block_number(); + let delay = ColdkeySwapAnnouncementDelay::::get(); assert_eq!( - SubtensorModule::get_total_stake_for_coldkey(&old_coldkey), - TaoCurrency::ZERO + ColdkeySwapAnnouncements::::iter().collect::>(), + vec![(who, (now + delay, new_coldkey_hash))] ); + + let reannouncement_delay = ColdkeySwapReannouncementDelay::::get(); + run_to_block(now + delay + reannouncement_delay); + + assert_ok!(SubtensorModule::announce_coldkey_swap( + RuntimeOrigin::signed(who), + new_coldkey_2_hash, + )); + + let now = System::block_number(); assert_eq!( - SubtensorModule::get_total_stake_for_coldkey(&new_coldkey), - total_stake_before_swap + ColdkeySwapAnnouncements::::iter().collect::>(), + vec![(who, (now + delay, new_coldkey_2_hash))] ); }); } -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --test swap_coldkey -- test_swap_staking_hotkeys --exact --nocapture #[test] -fn test_swap_staking_hotkeys() { +fn test_announce_coldkey_swap_only_pays_swap_cost_if_no_announcement_exists() { new_test_ext(1).execute_with(|| { - let old_coldkey = U256::from(1); + let who = U256::from(1); let new_coldkey = U256::from(2); - let hotkey = U256::from(3); + let new_coldkey_hash = ::Hashing::hash_of(&new_coldkey); + let new_coldkey_2 = U256::from(3); + let new_coldkey_2_hash = ::Hashing::hash_of(&new_coldkey_2); + let ed = ExistentialDeposit::get(); - StakingHotkeys::::insert(old_coldkey, vec![hotkey]); + let swap_cost = SubtensorModule::get_key_swap_cost().to_u64(); + SubtensorModule::add_balance_to_coldkey_account(&who, swap_cost + ed); + assert_eq!(SubtensorModule::get_coldkey_balance(&who), swap_cost + ed); - let mut weight = Weight::zero(); - assert_ok!(SubtensorModule::perform_swap_coldkey( - &old_coldkey, - &new_coldkey, - &mut weight + assert_ok!(SubtensorModule::announce_coldkey_swap( + RuntimeOrigin::signed(who), + new_coldkey_hash, )); + assert_eq!(SubtensorModule::get_coldkey_balance(&who), ed); + + let now = System::block_number(); + let base_delay = ColdkeySwapAnnouncementDelay::::get(); + let reannouncement_delay = ColdkeySwapReannouncementDelay::::get(); + run_to_block(now + base_delay + reannouncement_delay); - assert!(StakingHotkeys::::get(old_coldkey).is_empty()); - assert_eq!(StakingHotkeys::::get(new_coldkey), vec![hotkey]); + assert_ok!(SubtensorModule::announce_coldkey_swap( + RuntimeOrigin::signed(who), + new_coldkey_2_hash, + )); + assert_eq!(SubtensorModule::get_coldkey_balance(&who), ed); }); } -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --test swap_coldkey -- test_swap_hotkey_owners --exact --nocapture #[test] -fn test_swap_hotkey_owners() { +fn test_announce_coldkey_swap_with_bad_origin_fails() { new_test_ext(1).execute_with(|| { - let old_coldkey = U256::from(1); - let new_coldkey = U256::from(2); - let hotkey = U256::from(3); - - Owner::::insert(hotkey, old_coldkey); - OwnedHotkeys::::insert(old_coldkey, vec![hotkey]); + let new_coldkey = U256::from(1); + let new_coldkey_hash = ::Hashing::hash_of(&new_coldkey); - let mut weight = Weight::zero(); - assert_ok!(SubtensorModule::perform_swap_coldkey( - &old_coldkey, - &new_coldkey, - &mut weight - )); + assert_noop!( + SubtensorModule::announce_coldkey_swap(RuntimeOrigin::none(), new_coldkey_hash), + BadOrigin + ); - assert_eq!(Owner::::get(hotkey), new_coldkey); - assert!(OwnedHotkeys::::get(old_coldkey).is_empty()); - assert_eq!(OwnedHotkeys::::get(new_coldkey), vec![hotkey]); + assert_noop!( + SubtensorModule::announce_coldkey_swap(RuntimeOrigin::root(), new_coldkey_hash), + BadOrigin + ); }); } -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --test swap_coldkey -- test_transfer_remaining_balance --exact --nocapture + #[test] -fn test_transfer_remaining_balance() { +fn test_announce_coldkey_swap_with_existing_announcement_not_past_delay_fails() { new_test_ext(1).execute_with(|| { - let old_coldkey = U256::from(1); + let who = U256::from(1); let new_coldkey = U256::from(2); - let balance = 100; + let new_coldkey_hash = ::Hashing::hash_of(&new_coldkey); + let new_coldkey_2 = U256::from(3); + let new_coldkey_2_hash = ::Hashing::hash_of(&new_coldkey_2); - SubtensorModule::add_balance_to_coldkey_account(&old_coldkey, balance); + assert_eq!(ColdkeySwapAnnouncements::::iter().count(), 0); - let mut weight = Weight::zero(); - assert_ok!(SubtensorModule::perform_swap_coldkey( - &old_coldkey, - &new_coldkey, - &mut weight + let swap_cost = SubtensorModule::get_key_swap_cost().to_u64(); + let ed = ExistentialDeposit::get(); + SubtensorModule::add_balance_to_coldkey_account(&who, swap_cost + ed); + + assert_ok!(SubtensorModule::announce_coldkey_swap( + RuntimeOrigin::signed(who), + new_coldkey_hash, )); - assert_eq!(SubtensorModule::get_coldkey_balance(&old_coldkey), 0); - assert_eq!(SubtensorModule::get_coldkey_balance(&new_coldkey), balance); + let now = System::block_number(); + let delay = ColdkeySwapAnnouncementDelay::::get(); + assert_eq!( + ColdkeySwapAnnouncements::::iter().collect::>(), + vec![(who, (now + delay, new_coldkey_hash))] + ); + + assert_noop!( + SubtensorModule::announce_coldkey_swap(RuntimeOrigin::signed(who), new_coldkey_2_hash,), + Error::::ColdkeySwapReannouncedTooEarly + ); }); } -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::swap_coldkey::test_swap_with_no_stake --exact --show-output #[test] -fn test_swap_with_no_stake() { +fn test_swap_coldkey_announced_works() { new_test_ext(1).execute_with(|| { - let old_coldkey = U256::from(1); + let who = U256::from(1); let new_coldkey = U256::from(2); + let new_coldkey_hash = ::Hashing::hash_of(&new_coldkey); + let hotkey1 = U256::from(1001); + let hotkey2 = U256::from(1002); + let hotkey3 = U256::from(1003); + let ed = ExistentialDeposit::get(); + let min_stake = DefaultMinStake::::get().to_u64(); + let stake1 = min_stake * 10; + let stake2 = min_stake * 20; + let stake3 = min_stake * 30; + let now = System::block_number(); + + ColdkeySwapAnnouncements::::insert(who, (now, new_coldkey_hash)); + + // Run some blocks for the announcement to be past the delay + let delay = ColdkeySwapAnnouncementDelay::::get() + 1; + run_to_block(now + delay); + + SubtensorModule::add_balance_to_coldkey_account(&who, stake1 + stake2 + stake3 + ed); + + let ( + netuid1, + netuid2, + hotkeys, + hk1_alpha, + hk2_alpha, + hk3_alpha, + total_ck_stake, + identity, + balance_before, + total_stake_before, + ) = comprehensive_setup!( + who, + new_coldkey, + new_coldkey_hash, + stake1, + stake2, + stake3, + hotkey1, + hotkey2, + hotkey3 + ); - let mut weight = Weight::zero(); - assert_ok!(SubtensorModule::perform_swap_coldkey( - &old_coldkey, - &new_coldkey, - &mut weight + assert_ok!(SubtensorModule::swap_coldkey_announced( + ::RuntimeOrigin::signed(who), + new_coldkey )); - assert_eq!( - SubtensorModule::get_total_stake_for_coldkey(&old_coldkey), - TaoCurrency::ZERO - ); - assert_eq!( - SubtensorModule::get_total_stake_for_coldkey(&new_coldkey), - TaoCurrency::ZERO + comprehensive_checks!( + who, + hotkey1, + hotkey2, + hotkey3, + hotkeys, + new_coldkey, + balance_before, + identity, + netuid1, + netuid2, + hk1_alpha, + hk2_alpha, + hk3_alpha, + total_ck_stake, + total_stake_before, + 0_u64 // Charged on announcement ); }); } -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --test swap_coldkey -- test_swap_with_multiple_hotkeys --exact --nocapture #[test] -fn test_swap_with_multiple_hotkeys() { +fn test_swap_coldkey_announced_with_bad_origin_fails() { new_test_ext(1).execute_with(|| { - let old_coldkey = U256::from(1); + let who = U256::from(1); let new_coldkey = U256::from(2); - let hotkey1 = U256::from(3); - let hotkey2 = U256::from(4); - OwnedHotkeys::::insert(old_coldkey, vec![hotkey1, hotkey2]); - - let mut weight = Weight::zero(); - assert_ok!(SubtensorModule::perform_swap_coldkey( - &old_coldkey, - &new_coldkey, - &mut weight - )); + assert_noop!( + SubtensorModule::swap_coldkey_announced(RuntimeOrigin::none(), new_coldkey), + BadOrigin + ); - assert!(OwnedHotkeys::::get(old_coldkey).is_empty()); - assert_eq!( - OwnedHotkeys::::get(new_coldkey), - vec![hotkey1, hotkey2] + assert_noop!( + SubtensorModule::swap_coldkey_announced(RuntimeOrigin::root(), new_coldkey), + BadOrigin ); }); } -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --test swap_coldkey -- test_swap_with_multiple_subnets --exact --nocapture #[test] -fn test_swap_with_multiple_subnets() { +fn test_swap_coldkey_announced_without_announcement_fails() { new_test_ext(1).execute_with(|| { - let old_coldkey = U256::from(1); + let who = U256::from(1); let new_coldkey = U256::from(2); - let netuid1 = NetUid::from(1); - let netuid2 = NetUid::from(2); - add_network(netuid1, 1, 0); - add_network(netuid2, 1, 0); - SubnetOwner::::insert(netuid1, old_coldkey); - SubnetOwner::::insert(netuid2, old_coldkey); + assert_noop!( + SubtensorModule::swap_coldkey_announced(RuntimeOrigin::signed(who), new_coldkey), + Error::::ColdkeySwapAnnouncementNotFound + ); + }) +} - let mut weight = Weight::zero(); - assert_ok!(SubtensorModule::perform_swap_coldkey( - &old_coldkey, - &new_coldkey, - &mut weight - )); +#[test] +fn test_swap_coldkey_announced_with_mismatched_coldkey_hash_fails() { + new_test_ext(1).execute_with(|| { + let who = U256::from(1); + let new_coldkey = U256::from(2); + let new_coldkey_hash = ::Hashing::hash_of(&new_coldkey); + let other_coldkey = U256::from(3); + let now = System::block_number(); - assert_eq!(SubnetOwner::::get(netuid1), new_coldkey); - assert_eq!(SubnetOwner::::get(netuid2), new_coldkey); - }); + ColdkeySwapAnnouncements::::insert(who, (now, new_coldkey_hash)); + + assert_noop!( + SubtensorModule::swap_coldkey_announced(RuntimeOrigin::signed(who), other_coldkey), + Error::::AnnouncedColdkeyHashDoesNotMatch + ); + }) } -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --test swap_coldkey -- test_swap_with_zero_balance --exact --nocapture #[test] -fn test_swap_with_zero_balance() { +fn test_swap_coldkey_announced_too_early_fails() { new_test_ext(1).execute_with(|| { - let old_coldkey = U256::from(1); + let who = U256::from(1); let new_coldkey = U256::from(2); + let new_coldkey_hash = ::Hashing::hash_of(&new_coldkey); - let mut weight = Weight::zero(); - assert_ok!(SubtensorModule::perform_swap_coldkey( - &old_coldkey, - &new_coldkey, - &mut weight - )); + // Now case + let now = System::block_number(); + let delay = ColdkeySwapAnnouncementDelay::::get(); + ColdkeySwapAnnouncements::::insert(who, (now + delay, new_coldkey_hash)); - assert_eq!(Balances::free_balance(old_coldkey), 0); - assert_eq!(Balances::free_balance(new_coldkey), 0); - }); + assert_noop!( + SubtensorModule::swap_coldkey_announced( + ::RuntimeOrigin::signed(who), + new_coldkey + ), + Error::::ColdkeySwapTooEarly + ); + + // Now + delay - 1 case + run_to_block(now + delay - 1); + + assert_noop!( + SubtensorModule::swap_coldkey_announced( + ::RuntimeOrigin::signed(who), + new_coldkey + ), + Error::::ColdkeySwapTooEarly + ); + }) } -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::swap_coldkey::test_swap_idempotency --exact --show-output #[test] -fn test_swap_idempotency() { +fn test_swap_coldkey_announced_with_already_associated_coldkey_fails() { new_test_ext(1).execute_with(|| { - let old_coldkey = U256::from(1); + let who = U256::from(1); let new_coldkey = U256::from(2); + let new_coldkey_hash = ::Hashing::hash_of(&new_coldkey); let hotkey = U256::from(3); - let netuid = NetUid::from(1u16); - let stake = DefaultMinStake::::get().to_u64() * 10; - let reserve = stake * 10; - mock::setup_reserves(netuid, reserve.into(), reserve.into()); + let swap_cost = SubtensorModule::get_key_swap_cost().to_u64(); + let ed = ExistentialDeposit::get(); + SubtensorModule::add_balance_to_coldkey_account(&who, swap_cost + ed); - // Add a network - add_network(netuid, 1, 0); - SubtensorModule::add_balance_to_coldkey_account(&old_coldkey, stake); // Give old coldkey some balance - // Stake to a hotkey - register_ok_neuron(netuid, hotkey, old_coldkey, 1001000); - assert_ok!(SubtensorModule::add_stake( - <::RuntimeOrigin>::signed(old_coldkey), - hotkey, - netuid, - stake.into() + assert_ok!(SubtensorModule::announce_coldkey_swap( + RuntimeOrigin::signed(who), + new_coldkey_hash, )); - // Get stake before swap - let stake_before_swap = SubtensorModule::get_total_stake_for_coldkey(&old_coldkey); + let now = System::block_number(); + let delay = ColdkeySwapAnnouncementDelay::::get() + 1; + run_to_block(now + delay); - let mut weight = Weight::zero(); - assert_ok!(SubtensorModule::perform_swap_coldkey( - &old_coldkey, - &new_coldkey, - &mut weight - )); - assert_ok!(SubtensorModule::perform_swap_coldkey( - &old_coldkey, - &new_coldkey, - &mut weight - )); + SubtensorModule::create_account_if_non_existent(&new_coldkey, &hotkey); - assert_eq!( - SubtensorModule::get_total_stake_for_coldkey(&old_coldkey), - TaoCurrency::ZERO - ); - assert_eq!( - SubtensorModule::get_total_stake_for_coldkey(&new_coldkey), - stake_before_swap + assert_noop!( + SubtensorModule::swap_coldkey_announced( + ::RuntimeOrigin::signed(who), + new_coldkey + ), + Error::::ColdKeyAlreadyAssociated ); - }); + }) } -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::swap_coldkey::test_swap_with_max_values --exact --show-output #[test] -fn test_swap_with_max_values() { +fn test_swap_coldkey_announced_with_hotkey_fails() { new_test_ext(1).execute_with(|| { - let old_coldkey = U256::from(1); + let who = U256::from(1); let new_coldkey = U256::from(2); - let old_coldkey2 = U256::from(3); - let new_coldkey2 = U256::from(4); - let hotkey = U256::from(5); - let hotkey2 = U256::from(6); - let other_coldkey = U256::from(7); - let netuid = NetUid::from(1); - let netuid2 = NetUid::from(2); - let stake = 10_000; - let max_stake = 21_000_000_000_000_000; // 21 Million TAO; max possible balance. + let hotkey = U256::from(3); + let hotkey_hash = ::Hashing::hash_of(&hotkey); + let now = System::block_number(); - // Add a network - add_network(netuid, 1, 0); - add_network(netuid2, 1, 0); + ColdkeySwapAnnouncements::::insert(who, (now, hotkey_hash)); - // Register hotkey on each subnet. - // hotkey2 is owned by other_coldkey. - register_ok_neuron(netuid, hotkey, old_coldkey, 1001000); - register_ok_neuron(netuid2, hotkey2, other_coldkey, 1001000); + let now = System::block_number(); + let delay = ColdkeySwapAnnouncementDelay::::get() + 1; + run_to_block(now + delay); - // Give balance to old_coldkey and old_coldkey2. - SubtensorModule::add_balance_to_coldkey_account(&old_coldkey, max_stake + 1_000); - SubtensorModule::add_balance_to_coldkey_account(&old_coldkey2, max_stake + 1_000); + SubtensorModule::create_account_if_non_existent(&new_coldkey, &hotkey); - let reserve = max_stake * 10; - mock::setup_reserves(netuid, reserve.into(), reserve.into()); - mock::setup_reserves(netuid2, reserve.into(), reserve.into()); + assert_noop!( + SubtensorModule::swap_coldkey_announced( + ::RuntimeOrigin::signed(who), + hotkey + ), + Error::::NewColdKeyIsHotkey + ); + }) +} - // Stake to hotkey on each subnet. - assert_ok!(SubtensorModule::add_stake( - <::RuntimeOrigin>::signed(old_coldkey), - hotkey, - netuid, - max_stake.into() - )); - let expected_stake1 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, +#[test] +fn test_swap_coldkey_works() { + new_test_ext(1000).execute_with(|| { + let old_coldkey = U256::from(1); + let new_coldkey = U256::from(2); + let new_coldkey_hash = ::Hashing::hash_of(&new_coldkey); + let hotkey1 = U256::from(1001); + let hotkey2 = U256::from(1002); + let hotkey3 = U256::from(1003); + let ed = ExistentialDeposit::get(); + let swap_cost = SubtensorModule::get_key_swap_cost(); + let min_stake = DefaultMinStake::::get().to_u64(); + let stake1 = min_stake * 10; + let stake2 = min_stake * 20; + let stake3 = min_stake * 30; + + SubtensorModule::add_balance_to_coldkey_account( &old_coldkey, - netuid, + swap_cost.to_u64() + stake1 + stake2 + stake3 + ed, ); - assert_ok!(SubtensorModule::add_stake( - <::RuntimeOrigin>::signed(old_coldkey2), - hotkey2, - netuid2, - max_stake.into() - )); - let expected_stake2 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey2, - &old_coldkey2, + // Some old announcement and dispute that will be cleared + let now = System::block_number() - 100; + ColdkeySwapAnnouncements::::insert(old_coldkey, (now, new_coldkey_hash)); + ColdkeySwapDisputes::::insert(old_coldkey, now); + + let ( + netuid1, netuid2, + hotkeys, + hk1_alpha, + hk2_alpha, + hk3_alpha, + total_ck_stake, + identity, + balance_before, + total_stake_before, + ) = comprehensive_setup!( + old_coldkey, + new_coldkey, + new_coldkey_hash, + stake1, + stake2, + stake3, + hotkey1, + hotkey2, + hotkey3 ); - let mut weight = Weight::zero(); - assert_ok!(SubtensorModule::perform_swap_coldkey( - &old_coldkey, - &new_coldkey, - &mut weight - )); - assert_ok!(SubtensorModule::perform_swap_coldkey( - &old_coldkey2, - &new_coldkey2, - &mut weight + assert_ok!(SubtensorModule::swap_coldkey( + ::RuntimeOrigin::root(), + old_coldkey, + new_coldkey, + swap_cost, )); - assert_eq!( - SubtensorModule::get_total_stake_for_coldkey(&old_coldkey), - TaoCurrency::ZERO - ); - assert_abs_diff_eq!( - SubtensorModule::get_total_stake_for_coldkey(&new_coldkey), - expected_stake1.to_u64().into(), - epsilon = TaoCurrency::from(expected_stake1.to_u64()) / 1000.into() - ); - assert_eq!( - SubtensorModule::get_total_stake_for_coldkey(&old_coldkey2), - TaoCurrency::ZERO - ); - assert_abs_diff_eq!( - SubtensorModule::get_total_stake_for_coldkey(&new_coldkey2), - expected_stake2.to_u64().into(), - epsilon = TaoCurrency::from(expected_stake2.to_u64()) / 1000.into() + comprehensive_checks!( + old_coldkey, + hotkey1, + hotkey2, + hotkey3, + hotkeys, + new_coldkey, + balance_before, + identity, + netuid1, + netuid2, + hk1_alpha, + hk2_alpha, + hk3_alpha, + total_ck_stake, + total_stake_before, + swap_cost.to_u64() ); + + // Check that the old announcement and dispute are cleared + assert!(!ColdkeySwapAnnouncements::::contains_key(old_coldkey)); + assert!(!ColdkeySwapDisputes::::contains_key(old_coldkey)); }); } -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::swap_coldkey::test_swap_with_non_existent_new_coldkey --exact --show-output #[test] -fn test_swap_with_non_existent_new_coldkey() { +fn test_swap_coldkey_works_with_zero_cost() { new_test_ext(1).execute_with(|| { let old_coldkey = U256::from(1); let new_coldkey = U256::from(2); - let hotkey = U256::from(3); - let stake = DefaultMinStake::::get().to_u64() * 10; - let netuid = NetUid::from(1); + let new_coldkey_hash = ::Hashing::hash_of(&new_coldkey); + let hotkey1 = U256::from(1001); + let hotkey2 = U256::from(1002); + let hotkey3 = U256::from(1003); + let ed = ExistentialDeposit::get(); + let swap_cost = 0u64; + let min_stake = DefaultMinStake::::get().to_u64(); + let stake1 = min_stake * 10; + let stake2 = min_stake * 20; + let stake3 = min_stake * 30; - add_network(netuid, 1, 0); - register_ok_neuron(netuid, hotkey, old_coldkey, 1001000); - // Give old coldkey some balance. - SubtensorModule::add_balance_to_coldkey_account(&old_coldkey, stake + 1_000); + SubtensorModule::add_balance_to_coldkey_account( + &old_coldkey, + stake1 + stake2 + stake3 + ed, + ); - let reserve = stake * 10; - mock::setup_reserves(netuid, reserve.into(), reserve.into()); + let ( + netuid1, + netuid2, + hotkeys, + hk1_alpha, + hk2_alpha, + hk3_alpha, + total_ck_stake, + identity, + balance_before, + total_stake_before, + ) = comprehensive_setup!( + old_coldkey, + new_coldkey, + new_coldkey_hash, + stake1, + stake2, + stake3, + hotkey1, + hotkey2, + hotkey3 + ); - // Stake to hotkey. - assert_ok!(SubtensorModule::add_stake( - <::RuntimeOrigin>::signed(old_coldkey), - hotkey, - netuid, - stake.into() + assert_ok!(SubtensorModule::swap_coldkey( + ::RuntimeOrigin::root(), + old_coldkey, + new_coldkey, + swap_cost.into(), )); - let expected_stake = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, - &old_coldkey, - netuid, + + comprehensive_checks!( + old_coldkey, + hotkey1, + hotkey2, + hotkey3, + hotkeys, + new_coldkey, + balance_before, + identity, + netuid1, + netuid2, + hk1_alpha, + hk2_alpha, + hk3_alpha, + total_ck_stake, + total_stake_before, + swap_cost ); + }); +} - let mut weight = Weight::zero(); - assert_ok!(SubtensorModule::perform_swap_coldkey( - &old_coldkey, - &new_coldkey, - &mut weight - )); +#[test] +fn test_swap_coldkey_with_bad_origin_fails() { + new_test_ext(1).execute_with(|| { + let who = U256::from(1); + let old_coldkey = U256::from(2); + let new_coldkey = U256::from(3); + let swap_cost = SubtensorModule::get_key_swap_cost(); + + assert_noop!( + SubtensorModule::swap_coldkey( + ::RuntimeOrigin::signed(who), + old_coldkey, + new_coldkey, + swap_cost, + ), + BadOrigin + ); + + assert_noop!( + SubtensorModule::swap_coldkey( + ::RuntimeOrigin::none(), + old_coldkey, + new_coldkey, + swap_cost + ), + BadOrigin + ); + }); +} + +#[test] +fn test_swap_coldkey_with_not_enough_balance_to_pay_swap_cost_fails() { + new_test_ext(1).execute_with(|| { + let old_coldkey = U256::from(1); + let new_coldkey = U256::from(2); + let swap_cost = SubtensorModule::get_key_swap_cost(); + + // No balance to pay swap cost + assert_noop!( + SubtensorModule::swap_coldkey( + RuntimeOrigin::root(), + old_coldkey, + new_coldkey, + swap_cost + ), + Error::::NotEnoughBalanceToPaySwapColdKey + ); + + // Needs to preserve ED + let balance = SubtensorModule::get_key_swap_cost().to_u64() + ExistentialDeposit::get() - 1; + SubtensorModule::add_balance_to_coldkey_account(&old_coldkey, balance); + assert_noop!( + SubtensorModule::swap_coldkey( + RuntimeOrigin::root(), + old_coldkey, + new_coldkey, + swap_cost + ), + Error::::NotEnoughBalanceToPaySwapColdKey + ); + }); +} + +#[test] +fn test_do_swap_coldkey_preserves_new_coldkey_identity() { + new_test_ext(1).execute_with(|| { + let who = U256::from(1); + let new_coldkey = U256::from(2); + + let old_identity = ChainIdentityV2 { + name: b"Old identity".to_vec(), + ..Default::default() + }; + IdentitiesV2::::insert(who, old_identity.clone()); + + let new_identity = ChainIdentityV2 { + name: b"New identity".to_vec(), + ..Default::default() + }; + IdentitiesV2::::insert(new_coldkey, new_identity.clone()); + + assert_ok!(SubtensorModule::do_swap_coldkey(&who, &new_coldkey,)); + + // Identity is preserved + assert_eq!(IdentitiesV2::::get(who), Some(old_identity)); + assert_eq!(IdentitiesV2::::get(new_coldkey), Some(new_identity)); + }); +} + +#[test] +fn test_announce_coldkey_swap_with_not_enough_balance_to_pay_swap_cost_fails() { + new_test_ext(1).execute_with(|| { + let who = U256::from(1); + let new_coldkey = U256::from(2); + let new_coldkey_hash = ::Hashing::hash_of(&new_coldkey); + + // No balance to pay swap cost + assert_noop!( + SubtensorModule::announce_coldkey_swap(RuntimeOrigin::signed(who), new_coldkey_hash), + Error::::NotEnoughBalanceToPaySwapColdKey + ); + + // Needs to preserve ED + let balance = SubtensorModule::get_key_swap_cost().to_u64() + ExistentialDeposit::get() - 1; + SubtensorModule::add_balance_to_coldkey_account(&who, balance); + assert_noop!( + SubtensorModule::announce_coldkey_swap(RuntimeOrigin::signed(who), new_coldkey_hash), + Error::::NotEnoughBalanceToPaySwapColdKey + ); + }); +} + +#[test] +fn test_do_swap_coldkey_with_no_stake() { + new_test_ext(1).execute_with(|| { + let old_coldkey = U256::from(1); + let new_coldkey = U256::from(2); + + assert_ok!(SubtensorModule::do_swap_coldkey(&old_coldkey, &new_coldkey)); assert_eq!( SubtensorModule::get_total_stake_for_coldkey(&old_coldkey), TaoCurrency::ZERO ); - - let actual_stake = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, - &new_coldkey, - netuid, - ); - assert_abs_diff_eq!( - actual_stake, - expected_stake, - epsilon = expected_stake / 1000.into() + assert_eq!( + SubtensorModule::get_total_stake_for_coldkey(&new_coldkey), + TaoCurrency::ZERO ); }); } -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --test swap_coldkey -- test_swap_with_max_hotkeys --exact --nocapture #[test] -fn test_swap_with_max_hotkeys() { +fn test_do_swap_coldkey_with_max_values() { new_test_ext(1).execute_with(|| { let old_coldkey = U256::from(1); let new_coldkey = U256::from(2); - let max_hotkeys = 1000; - let hotkeys: Vec = (0..max_hotkeys).map(U256::from).collect(); + let old_coldkey2 = U256::from(3); + let new_coldkey2 = U256::from(4); + let hotkey = U256::from(5); + let hotkey2 = U256::from(6); + let other_coldkey = U256::from(7); + let netuid = NetUid::from(1); + let netuid2 = NetUid::from(2); + let stake = 10_000; + let max_stake = 21_000_000_000_000_000; // 21 Million TAO; max possible balance. + + // Add a network + add_network(netuid, 1, 0); + add_network(netuid2, 1, 0); + + // Register hotkey on each subnet. + // hotkey2 is owned by other_coldkey. + register_ok_neuron(netuid, hotkey, old_coldkey, 1001000); + register_ok_neuron(netuid2, hotkey2, other_coldkey, 1001000); + + // Give balance to old_coldkey and old_coldkey2. + SubtensorModule::add_balance_to_coldkey_account(&old_coldkey, max_stake + 1_000); + SubtensorModule::add_balance_to_coldkey_account(&old_coldkey2, max_stake + 1_000); - OwnedHotkeys::::insert(old_coldkey, hotkeys.clone()); + let reserve = max_stake * 10; + mock::setup_reserves(netuid, reserve.into(), reserve.into()); + mock::setup_reserves(netuid2, reserve.into(), reserve.into()); - let mut weight = Weight::zero(); - assert_ok!(SubtensorModule::perform_swap_coldkey( + // Stake to hotkey on each subnet. + assert_ok!(SubtensorModule::add_stake( + <::RuntimeOrigin>::signed(old_coldkey), + hotkey, + netuid, + max_stake.into() + )); + let expected_stake1 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, &old_coldkey, - &new_coldkey, - &mut weight + netuid, + ); + + assert_ok!(SubtensorModule::add_stake( + <::RuntimeOrigin>::signed(old_coldkey2), + hotkey2, + netuid2, + max_stake.into() + )); + let expected_stake2 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey2, + &old_coldkey2, + netuid2, + ); + + assert_ok!(SubtensorModule::do_swap_coldkey(&old_coldkey, &new_coldkey,)); + assert_ok!(SubtensorModule::do_swap_coldkey( + &old_coldkey2, + &new_coldkey2, )); - assert!(OwnedHotkeys::::get(old_coldkey).is_empty()); - assert_eq!(OwnedHotkeys::::get(new_coldkey), hotkeys); + assert_eq!( + SubtensorModule::get_total_stake_for_coldkey(&old_coldkey), + TaoCurrency::ZERO + ); + assert_abs_diff_eq!( + SubtensorModule::get_total_stake_for_coldkey(&new_coldkey), + expected_stake1.to_u64().into(), + epsilon = TaoCurrency::from(expected_stake1.to_u64()) / 1000.into() + ); + assert_eq!( + SubtensorModule::get_total_stake_for_coldkey(&old_coldkey2), + TaoCurrency::ZERO + ); + assert_abs_diff_eq!( + SubtensorModule::get_total_stake_for_coldkey(&new_coldkey2), + expected_stake2.to_u64().into(), + epsilon = TaoCurrency::from(expected_stake2.to_u64()) / 1000.into() + ); }); } -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --test swap_coldkey -- test_swap_effect_on_delegated_stake --exact --nocapture #[test] -fn test_swap_effect_on_delegated_stake() { +fn test_do_swap_coldkey_effect_on_delegated_stake() { new_test_ext(1).execute_with(|| { let subnet_owner_coldkey = U256::from(1001); let subnet_owner_hotkey = U256::from(1002); @@ -552,12 +819,7 @@ fn test_swap_effect_on_delegated_stake() { let coldkey_stake_before = SubtensorModule::get_total_stake_for_coldkey(&old_coldkey); let delegator_stake_before = SubtensorModule::get_total_stake_for_coldkey(&delegator); - let mut weight = Weight::zero(); - assert_ok!(SubtensorModule::perform_swap_coldkey( - &old_coldkey, - &new_coldkey, - &mut weight - )); + assert_ok!(SubtensorModule::do_swap_coldkey(&old_coldkey, &new_coldkey,)); assert_abs_diff_eq!( SubtensorModule::get_total_stake_for_coldkey(&new_coldkey), @@ -577,589 +839,89 @@ fn test_swap_effect_on_delegated_stake() { }); } -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::swap_coldkey::test_swap_concurrent_modifications --exact --show-output #[test] -fn test_swap_concurrent_modifications() { +fn test_swap_delegated_stake_for_coldkey() { new_test_ext(1).execute_with(|| { let old_coldkey = U256::from(1); let new_coldkey = U256::from(2); - let hotkey = U256::from(3); + let other_coldkey = U256::from(3); + let hotkey1 = U256::from(4); + let hotkey2 = U256::from(5); + let stake_amount1 = DefaultMinStake::::get().to_u64() * 10; + let stake_amount2 = DefaultMinStake::::get().to_u64() * 20; let netuid = NetUid::from(1); - let initial_stake = 1_000_000_000_000; - let additional_stake = 500_000_000_000; - let reserve = (initial_stake + additional_stake) * 1000; + // Setup initial state + add_network(netuid, 1, 0); + register_ok_neuron(netuid, hotkey1, other_coldkey, 0); + register_ok_neuron(netuid, hotkey2, other_coldkey, 0); + + let reserve = (stake_amount1 + stake_amount2) * 10; mock::setup_reserves(netuid, reserve.into(), reserve.into()); - // Setup initial state - add_network(netuid, 1, 1); + // Notice hotkey1 and hotkey2 are Owned by other_coldkey + // old_coldkey and new_coldkey therefore delegates stake to them + // === Give old_coldkey some balance === SubtensorModule::add_balance_to_coldkey_account( - &new_coldkey, - initial_stake + additional_stake + 1_000_000, + &old_coldkey, + stake_amount1 + stake_amount2 + 1_000_000, ); - register_ok_neuron(netuid, hotkey, new_coldkey, 1001000); + // === Stake to hotkeys === assert_ok!(SubtensorModule::add_stake( - <::RuntimeOrigin>::signed(new_coldkey), - hotkey, + <::RuntimeOrigin>::signed(old_coldkey), + hotkey1, netuid, - initial_stake.into() + stake_amount1.into() )); - - // Verify initial stake - let stake_before_swap = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, - &new_coldkey, + let expected_stake_alpha1 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey1, + &old_coldkey, netuid, ); - // Wait some blocks - step_block(10); - - // Simulate concurrent stake addition + let (expected_stake_alpha2, fee) = mock::swap_tao_to_alpha(netuid, stake_amount2.into()); assert_ok!(SubtensorModule::add_stake( - <::RuntimeOrigin>::signed(new_coldkey), - hotkey, + <::RuntimeOrigin>::signed(old_coldkey), + hotkey2, netuid, - additional_stake.into() + stake_amount2.into() )); - - let stake_with_additional = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, - &new_coldkey, + let expected_stake_alpha2 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey2, + &old_coldkey, netuid, ); + let fee = (expected_stake_alpha2.to_u64() as f64 * 0.003) as u64; - let mut weight = Weight::zero(); - assert_ok!(SubtensorModule::perform_swap_coldkey( + // Record initial values + let initial_total_issuance = SubtensorModule::get_total_issuance(); + let initial_total_stake = SubtensorModule::get_total_stake(); + let coldkey_stake = SubtensorModule::get_total_stake_for_coldkey(&old_coldkey); + let stake_coldkey_hotkey1 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey1, &old_coldkey, - &new_coldkey, - &mut weight - )); + netuid, + ); + let stake_coldkey_hotkey2 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey2, + &old_coldkey, + netuid, + ); + let total_hotkey1_stake = SubtensorModule::get_total_stake_for_hotkey(&hotkey1); + let total_hotkey2_stake = SubtensorModule::get_total_stake_for_hotkey(&hotkey2); + + // Perform the swap + assert_ok!(SubtensorModule::do_swap_coldkey(&old_coldkey, &new_coldkey,)); + // Verify stake transfer assert_eq!( SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, + &hotkey1, &new_coldkey, netuid ), - stake_with_additional - ); - assert!(stake_with_additional > stake_before_swap); - assert!(!Alpha::::contains_key((hotkey, old_coldkey, netuid))); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --test swap_coldkey -- test_swap_with_invalid_subnet_ownership --exact --nocapture -#[test] -fn test_swap_with_invalid_subnet_ownership() { - new_test_ext(1).execute_with(|| { - let old_coldkey = U256::from(1); - let new_coldkey = U256::from(2); - let netuid = NetUid::from(1u16); - - SubnetOwner::::insert(netuid, old_coldkey); - - // Simulate an invalid state where the subnet owner doesn't match the old_coldkey - SubnetOwner::::insert(netuid, U256::from(3)); - - let mut weight = Weight::zero(); - assert_ok!(SubtensorModule::perform_swap_coldkey( - &old_coldkey, - &new_coldkey, - &mut weight - )); - - // The swap should not affect the mismatched subnet ownership - assert_eq!(SubnetOwner::::get(netuid), U256::from(3)); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=info cargo test --package pallet-subtensor --lib -- tests::swap_coldkey::test_do_swap_coldkey_success --exact --show-output -#[test] -fn test_do_swap_coldkey_success() { - new_test_ext(1).execute_with(|| { - let old_coldkey = U256::from(1); - let new_coldkey = U256::from(2); - let hotkey1 = U256::from(3); - let hotkey2 = U256::from(4); - let netuid = NetUid::from(1u16); - let stake_amount1 = DefaultMinStake::::get().to_u64() * 10; - let stake_amount2 = DefaultMinStake::::get().to_u64() * 20; - let swap_cost = SubtensorModule::get_key_swap_cost(); - let free_balance_old = 12345 + swap_cost.to_u64(); - - let reserve = (stake_amount1 + stake_amount2) * 10; - mock::setup_reserves(netuid, reserve.into(), reserve.into()); - - // Setup initial state - add_network(netuid, 13, 0); - register_ok_neuron(netuid, hotkey1, old_coldkey, 0); - register_ok_neuron(netuid, hotkey2, old_coldkey, 0); - - // Add balance to old coldkey - SubtensorModule::add_balance_to_coldkey_account( - &old_coldkey, - stake_amount1 + stake_amount2 + free_balance_old, - ); - - // Log initial state - log::info!( - "Initial total stake: {}", - SubtensorModule::get_total_stake() - ); - log::info!( - "Initial old coldkey stake: {}", - SubtensorModule::get_total_stake_for_coldkey(&old_coldkey) - ); - log::info!( - "Initial new coldkey stake: {}", - SubtensorModule::get_total_stake_for_coldkey(&new_coldkey) - ); - - // Add stake to the neurons - assert_ok!(SubtensorModule::add_stake( - <::RuntimeOrigin>::signed(old_coldkey), - hotkey1, - netuid, - stake_amount1.into() - )); - assert_ok!(SubtensorModule::add_stake( - <::RuntimeOrigin>::signed(old_coldkey), - hotkey2, - netuid, - stake_amount2.into() - )); - - // Insert an Identity - let name: Vec = b"The fourth Coolest Identity".to_vec(); - let identity: ChainIdentityV2 = ChainIdentityV2 { - name: name.clone(), - url: vec![], - github_repo: vec![], - image: vec![], - discord: vec![], - description: vec![], - additional: vec![], - }; - - IdentitiesV2::::insert(old_coldkey, identity.clone()); - - assert!(IdentitiesV2::::get(old_coldkey).is_some()); - assert!(IdentitiesV2::::get(new_coldkey).is_none()); - - // Log state after adding stake - log::info!( - "Total stake after adding: {}", - SubtensorModule::get_total_stake() - ); - log::info!( - "Old coldkey stake after adding: {}", - SubtensorModule::get_total_stake_for_coldkey(&old_coldkey) - ); - log::info!( - "New coldkey stake after adding: {}", - SubtensorModule::get_total_stake_for_coldkey(&new_coldkey) - ); - - // Record total stake before swap - let total_stake_before_swap = SubtensorModule::get_total_stake(); - - let hk1_alpha = Alpha::::get((hotkey1, old_coldkey, netuid)); - let hk2_alpha = Alpha::::get((hotkey2, old_coldkey, netuid)); - let total_ck_stake = SubtensorModule::get_total_stake_for_coldkey(&old_coldkey); - - // Perform the swap - assert_ok!(SubtensorModule::do_swap_coldkey( - // <::RuntimeOrigin>::signed(old_coldkey), - &old_coldkey, - &new_coldkey, - swap_cost - )); - - // Log state after swap - log::info!( - "Total stake after swap: {}", - SubtensorModule::get_total_stake() - ); - log::info!( - "Old coldkey stake after swap: {}", - SubtensorModule::get_total_stake_for_coldkey(&old_coldkey) - ); - log::info!( - "New coldkey stake after swap: {}", - SubtensorModule::get_total_stake_for_coldkey(&new_coldkey) - ); - - // Verify the swap - assert_eq!(Owner::::get(hotkey1), new_coldkey); - assert_eq!(Owner::::get(hotkey2), new_coldkey); - assert_eq!( - SubtensorModule::get_total_stake_for_coldkey(&new_coldkey), - total_ck_stake - ); - assert_eq!( - SubtensorModule::get_total_stake_for_coldkey(&old_coldkey), - TaoCurrency::ZERO - ); - assert_eq!( - Alpha::::get((hotkey1, new_coldkey, netuid)), - hk1_alpha - ); - assert_eq!( - Alpha::::get((hotkey2, new_coldkey, netuid)), - hk2_alpha - ); - assert!(!Alpha::::contains_key((hotkey1, old_coldkey, netuid))); - assert!(!Alpha::::contains_key((hotkey2, old_coldkey, netuid))); - - // Verify OwnedHotkeys - let new_owned_hotkeys = OwnedHotkeys::::get(new_coldkey); - assert!(new_owned_hotkeys.contains(&hotkey1)); - assert!(new_owned_hotkeys.contains(&hotkey2)); - assert_eq!(new_owned_hotkeys.len(), 2); - assert!(!OwnedHotkeys::::contains_key(old_coldkey)); - - // Verify balance transfer - assert_eq!( - SubtensorModule::get_coldkey_balance(&new_coldkey), - free_balance_old - swap_cost.to_u64() - ); - assert_eq!(SubtensorModule::get_coldkey_balance(&old_coldkey), 0); - - // Verify total stake remains unchanged - assert_eq!( - SubtensorModule::get_total_stake(), - total_stake_before_swap, - "Total stake changed unexpectedly" - ); - - // Verify identities were swapped - assert!(IdentitiesV2::::get(old_coldkey).is_none()); - assert!(IdentitiesV2::::get(new_coldkey).is_some()); - assert_eq!( - IdentitiesV2::::get(new_coldkey).expect("Expected an Identity"), - identity - ); - - // Verify event emission - System::assert_last_event( - Event::ColdkeySwapped { - old_coldkey, - new_coldkey, - swap_cost, - } - .into(), - ); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=info cargo test --package pallet-subtensor --lib -- tests::swap_coldkey::test_swap_stake_for_coldkey --exact --show-output -#[test] -fn test_swap_stake_for_coldkey() { - new_test_ext(1).execute_with(|| { - let old_coldkey = U256::from(1); - let new_coldkey = U256::from(2); - let hotkey1 = U256::from(3); - let hotkey2 = U256::from(4); - let stake_amount1 = DefaultMinStake::::get().to_u64() * 10; - let stake_amount2 = DefaultMinStake::::get().to_u64() * 20; - let stake_amount3 = DefaultMinStake::::get().to_u64() * 30; - let mut weight = Weight::zero(); - - // Setup initial state - // Add a network - let netuid = NetUid::from(1u16); - add_network(netuid, 1, 0); - - // Register hotkeys - register_ok_neuron(netuid, hotkey1, old_coldkey, 0); - register_ok_neuron(netuid, hotkey2, old_coldkey, 0); - // Give some balance to old coldkey - SubtensorModule::add_balance_to_coldkey_account( - &old_coldkey, - stake_amount1 + stake_amount2 + 1_000_000, - ); - - let reserve = (stake_amount1 + stake_amount2 + stake_amount3) * 10; - mock::setup_reserves(netuid, reserve.into(), reserve.into()); - - // Stake to hotkeys - assert_ok!(SubtensorModule::add_stake( - <::RuntimeOrigin>::signed(old_coldkey), - hotkey1, - netuid, - stake_amount1.into() - )); - let expected_stake_alpha1 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey1, - &old_coldkey, - netuid, - ); - - assert_ok!(SubtensorModule::add_stake( - <::RuntimeOrigin>::signed(old_coldkey), - hotkey2, - netuid, - stake_amount2.into() - )); - let expected_stake_alpha2 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey2, - &old_coldkey, - netuid, - ); - - // Insert existing for same hotkey1 - // give new coldkey some balance - SubtensorModule::add_balance_to_coldkey_account(&new_coldkey, stake_amount3 + 1_000_000); - // Stake to hotkey1 - assert_ok!(SubtensorModule::add_stake( - <::RuntimeOrigin>::signed(new_coldkey), - hotkey1, - netuid, - stake_amount3.into() - )); - let expected_stake_alpha3 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey1, - &new_coldkey, - netuid, - ); - - // Record initial values - let initial_total_issuance = SubtensorModule::get_total_issuance(); - let initial_total_stake = SubtensorModule::get_total_stake(); - let initial_total_stake_for_old_coldkey = - SubtensorModule::get_total_stake_for_coldkey(&old_coldkey); - let initial_total_stake_for_new_coldkey = - SubtensorModule::get_total_stake_for_coldkey(&new_coldkey); - let initial_total_hotkey1_stake = SubtensorModule::get_total_stake_for_hotkey(&hotkey1); - let initial_total_hotkey2_stake = SubtensorModule::get_total_stake_for_hotkey(&hotkey2); - - // Perform the swap - SubtensorModule::perform_swap_coldkey(&old_coldkey, &new_coldkey, &mut weight); - - // Verify stake is additive, not replaced - assert_abs_diff_eq!( - SubtensorModule::get_total_stake_for_coldkey(&new_coldkey), - initial_total_stake_for_old_coldkey + initial_total_stake_for_new_coldkey, - epsilon = 2.into() - ); - - // Verify ownership transfer - assert_eq!( - SubtensorModule::get_owned_hotkeys(&new_coldkey), - vec![hotkey1, hotkey2] - ); - assert_eq!(SubtensorModule::get_owned_hotkeys(&old_coldkey), vec![]); - - // Verify stake transfer - assert_eq!( - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey1, - &new_coldkey, - netuid - ), - expected_stake_alpha1 + expected_stake_alpha3 - ); - assert_eq!( - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey2, - &new_coldkey, - netuid - ), - expected_stake_alpha2 - ); - assert_eq!( - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey1, - &old_coldkey, - netuid - ), - AlphaCurrency::ZERO - ); - assert_eq!( - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey2, - &old_coldkey, - netuid - ), - AlphaCurrency::ZERO - ); - - // Verify TotalHotkeyStake remains unchanged - assert_eq!( - SubtensorModule::get_total_stake_for_hotkey(&hotkey1), - initial_total_hotkey1_stake - ); - assert_eq!( - SubtensorModule::get_total_stake_for_hotkey(&hotkey2), - initial_total_hotkey2_stake - ); - - // Verify total stake and issuance remain unchanged - assert_eq!( - SubtensorModule::get_total_stake(), - initial_total_stake, - "Total stake changed unexpectedly" - ); - assert_eq!( - SubtensorModule::get_total_issuance(), - initial_total_issuance, - "Total issuance changed unexpectedly" - ); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=info cargo test --package pallet-subtensor --lib -- tests::swap_coldkey::test_swap_staking_hotkeys_for_coldkey --exact --show-output -#[test] -fn test_swap_staking_hotkeys_for_coldkey() { - new_test_ext(1).execute_with(|| { - let old_coldkey = U256::from(1); - let new_coldkey = U256::from(2); - let other_coldkey = U256::from(3); - let hotkey1 = U256::from(4); - let hotkey2 = U256::from(5); - let stake_amount1 = DefaultMinStake::::get().to_u64() * 10; - let stake_amount2 = DefaultMinStake::::get().to_u64() * 20; - let mut weight = Weight::zero(); - - // Setup initial state - // Add a network - let netuid = NetUid::from(1u16); - add_network(netuid, 1, 0); - // Give some balance to old coldkey - SubtensorModule::add_balance_to_coldkey_account( - &old_coldkey, - stake_amount1 + stake_amount2 + 1_000_000, - ); // Register hotkeys - register_ok_neuron(netuid, hotkey1, old_coldkey, 0); - register_ok_neuron(netuid, hotkey2, other_coldkey, 0); - - let reserve = (stake_amount1 + stake_amount2) * 10; - mock::setup_reserves(netuid, reserve.into(), reserve.into()); - - // Stake to hotkeys - assert_ok!(SubtensorModule::add_stake( - <::RuntimeOrigin>::signed(old_coldkey), - hotkey1, - netuid, - stake_amount1.into() - )); - let expected_stake_alpha1 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey1, - &old_coldkey, - netuid, - ); - - assert_ok!(SubtensorModule::add_stake( - <::RuntimeOrigin>::signed(old_coldkey), - hotkey2, - netuid, - stake_amount2.into() - )); - let expected_stake_alpha2 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey2, - &old_coldkey, - netuid, - ); - - // Perform the swap - SubtensorModule::perform_swap_coldkey(&old_coldkey, &new_coldkey, &mut weight); - - // Verify StakingHotkeys transfer - assert_eq!( - StakingHotkeys::::get(new_coldkey), - vec![hotkey1, hotkey2] - ); - assert_eq!(StakingHotkeys::::get(old_coldkey), vec![]); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=info cargo test --package pallet-subtensor --lib -- tests::swap_coldkey::test_swap_delegated_stake_for_coldkey --exact --show-output -#[test] -fn test_swap_delegated_stake_for_coldkey() { - new_test_ext(1).execute_with(|| { - let old_coldkey = U256::from(1); - let new_coldkey = U256::from(2); - let other_coldkey = U256::from(3); - let hotkey1 = U256::from(4); - let hotkey2 = U256::from(5); - let stake_amount1 = DefaultMinStake::::get().to_u64() * 10; - let stake_amount2 = DefaultMinStake::::get().to_u64() * 20; - let mut weight = Weight::zero(); - let netuid = NetUid::from(1); - - // Setup initial state - add_network(netuid, 1, 0); - register_ok_neuron(netuid, hotkey1, other_coldkey, 0); - register_ok_neuron(netuid, hotkey2, other_coldkey, 0); - - let reserve = (stake_amount1 + stake_amount2) * 10; - mock::setup_reserves(netuid, reserve.into(), reserve.into()); - - // Notice hotkey1 and hotkey2 are Owned by other_coldkey - // old_coldkey and new_coldkey therefore delegates stake to them - // === Give old_coldkey some balance === - SubtensorModule::add_balance_to_coldkey_account( - &old_coldkey, - stake_amount1 + stake_amount2 + 1_000_000, - ); - - // === Stake to hotkeys === - assert_ok!(SubtensorModule::add_stake( - <::RuntimeOrigin>::signed(old_coldkey), - hotkey1, - netuid, - stake_amount1.into() - )); - let expected_stake_alpha1 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey1, - &old_coldkey, - netuid, - ); - - let (expected_stake_alpha2, fee) = mock::swap_tao_to_alpha(netuid, stake_amount2.into()); - assert_ok!(SubtensorModule::add_stake( - <::RuntimeOrigin>::signed(old_coldkey), - hotkey2, - netuid, - stake_amount2.into() - )); - let expected_stake_alpha2 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey2, - &old_coldkey, - netuid, - ); - let fee = (expected_stake_alpha2.to_u64() as f64 * 0.003) as u64; - - // Record initial values - let initial_total_issuance = SubtensorModule::get_total_issuance(); - let initial_total_stake = SubtensorModule::get_total_stake(); - let coldkey_stake = SubtensorModule::get_total_stake_for_coldkey(&old_coldkey); - let stake_coldkey_hotkey1 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey1, - &old_coldkey, - netuid, - ); - let stake_coldkey_hotkey2 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey2, - &old_coldkey, - netuid, - ); - let total_hotkey1_stake = SubtensorModule::get_total_stake_for_hotkey(&hotkey1); - let total_hotkey2_stake = SubtensorModule::get_total_stake_for_hotkey(&hotkey2); - - // Perform the swap - SubtensorModule::perform_swap_coldkey(&old_coldkey, &new_coldkey, &mut weight); - - // Verify stake transfer - assert_eq!( - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey1, - &new_coldkey, - netuid - ), - expected_stake_alpha1 + expected_stake_alpha1 ); assert_eq!( SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( @@ -1220,85 +982,6 @@ fn test_swap_delegated_stake_for_coldkey() { }); } -// SKIP_WASM_BUILD=1 RUST_LOG=info cargo test --test swap_coldkey -- test_swap_subnet_owner_for_coldkey --exact --nocapture -#[test] -fn test_swap_subnet_owner_for_coldkey() { - new_test_ext(1).execute_with(|| { - let old_coldkey = U256::from(1); - let new_coldkey = U256::from(2); - let netuid1 = NetUid::from(1); - let netuid2 = NetUid::from(2); - let mut weight = Weight::zero(); - - // Initialize SubnetOwner for old_coldkey - add_network(netuid1, 13, 0); - add_network(netuid2, 14, 0); - SubnetOwner::::insert(netuid1, old_coldkey); - SubnetOwner::::insert(netuid2, old_coldkey); - - // Set up TotalNetworks - TotalNetworks::::put(3); - - // Perform the swap - SubtensorModule::perform_swap_coldkey(&old_coldkey, &new_coldkey, &mut weight); - - // Verify the swap - assert_eq!(SubnetOwner::::get(netuid1), new_coldkey); - assert_eq!(SubnetOwner::::get(netuid2), new_coldkey); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=info cargo test --test swap_coldkey -- test_do_swap_coldkey_with_subnet_ownership --exact --nocapture -#[test] -fn test_do_swap_coldkey_with_subnet_ownership() { - new_test_ext(1).execute_with(|| { - let old_coldkey = U256::from(1); - let new_coldkey = U256::from(2); - let hotkey = U256::from(3); - let netuid = NetUid::from(1u16); - let stake_amount = 1000; - let swap_cost = SubtensorModule::get_key_swap_cost().to_u64(); - - // Setup initial state - add_network(netuid, 13, 0); - register_ok_neuron(netuid, hotkey, old_coldkey, 0); - - // Set TotalNetworks because swap relies on it - crate::TotalNetworks::::set(1); - - SubtensorModule::add_balance_to_coldkey_account(&old_coldkey, stake_amount + swap_cost); - SubnetOwner::::insert(netuid, old_coldkey); - - // Populate OwnedHotkeys map - OwnedHotkeys::::insert(old_coldkey, vec![hotkey]); - - // Perform the swap - assert_ok!(SubtensorModule::do_swap_coldkey( - &old_coldkey, - &new_coldkey, - swap_cost.into() - )); - - // Verify subnet ownership transfer - assert_eq!(SubnetOwner::::get(netuid), new_coldkey); - }); -} -// SKIP_WASM_BUILD=1 RUST_LOG=info cargo test --test swap_coldkey -- test_coldkey_has_associated_hotkeys --exact --nocapture -#[test] -fn test_coldkey_has_associated_hotkeys() { - new_test_ext(1).execute_with(|| { - let coldkey = U256::from(1); - let hotkey = U256::from(2); - let netuid = NetUid::from(1u16); - - // Setup initial state - add_network(netuid, 13, 0); - register_ok_neuron(netuid, hotkey, coldkey, 0); - SubtensorModule::add_balance_to_coldkey_account(&coldkey, 1000); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=info cargo test --package pallet-subtensor --lib -- tests::swap_coldkey::test_coldkey_swap_total --exact --show-output #[test] fn test_coldkey_swap_total() { new_test_ext(1).execute_with(|| { @@ -1523,12 +1206,7 @@ fn test_coldkey_swap_total() { SubtensorModule::get_total_stake_for_coldkey(&coldkey), ck_stake ); - let mut weight = Weight::zero(); - assert_ok!(SubtensorModule::perform_swap_coldkey( - &coldkey, - &new_coldkey, - &mut weight - )); + assert_ok!(SubtensorModule::do_swap_coldkey(&coldkey, &new_coldkey,)); assert_eq!( SubtensorModule::get_total_stake_for_coldkey(&new_coldkey), ck_stake @@ -1617,9 +1295,8 @@ fn test_coldkey_swap_total() { }); } -// SKIP_WASM_BUILD=1 RUST_LOG=info cargo test --package pallet-subtensor --lib -- tests::swap_coldkey::test_coldkey_delegations --exact --show-output #[test] -fn test_coldkey_delegations() { +fn test_do_swap_coldkey_effect_on_delegations() { new_test_ext(1).execute_with(|| { let new_coldkey = U256::from(0); let owner = U256::from(1); @@ -1656,961 +1333,424 @@ fn test_coldkey_delegations() { )); // Add stake to netuid2 - assert_ok!(SubtensorModule::add_stake( - <::RuntimeOrigin>::signed(coldkey), - delegate, - netuid2, - stake.into() - )); - - // Perform the swap - let mut weight = Weight::zero(); - assert_ok!(SubtensorModule::perform_swap_coldkey( - &coldkey, - &new_coldkey, - &mut weight - )); - - // Verify stake was moved for the delegate - let approx_total_stake = TaoCurrency::from(stake * 2 - fee * 2); - assert_abs_diff_eq!( - SubtensorModule::get_total_stake_for_hotkey(&delegate), - approx_total_stake, - epsilon = approx_total_stake / 100.into() - ); - assert_eq!( - SubtensorModule::get_total_stake_for_coldkey(&coldkey), - TaoCurrency::ZERO - ); - assert_abs_diff_eq!( - SubtensorModule::get_total_stake_for_coldkey(&new_coldkey), - approx_total_stake, - epsilon = approx_total_stake / 100.into() - ); - assert_eq!( - expected_stake, - Alpha::::get((delegate, new_coldkey, netuid)) - .to_num::() - .into(), - ); - assert_eq!(Alpha::::get((delegate, coldkey, netuid)), 0); - - assert_eq!( - expected_stake, - Alpha::::get((delegate, new_coldkey, netuid2)) - .to_num::() - .into() - ); - assert_eq!(Alpha::::get((delegate, coldkey, netuid2)), 0); - }); -} - -#[test] -fn test_schedule_swap_coldkey_success() { - new_test_ext(1).execute_with(|| { - // Initialize test accounts - let old_coldkey: U256 = U256::from(1); - let new_coldkey: U256 = U256::from(2); - - let swap_cost = SubtensorModule::get_key_swap_cost(); - - // Add balance to the old coldkey account - SubtensorModule::add_balance_to_coldkey_account(&old_coldkey, swap_cost.to_u64() + 1_000); - - // Schedule the coldkey swap - assert_ok!(SubtensorModule::schedule_swap_coldkey( - <::RuntimeOrigin>::signed(old_coldkey), - new_coldkey - )); - - // Get the current block number - let current_block: u64 = System::block_number(); - - // Calculate the expected execution block (5 days from now) - let expected_execution_block: u64 = current_block + 5 * 24 * 60 * 60 / 12; - - // Check for the SwapScheduled event - System::assert_last_event( - Event::ColdkeySwapScheduled { - old_coldkey, - new_coldkey, - execution_block: expected_execution_block, - swap_cost, - } - .into(), - ); - - // TODO: Add additional checks to ensure the swap is correctly scheduled in the system - // For example, verify that the swap is present in the appropriate storage or scheduler - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=info cargo test --test swap_coldkey -- test_schedule_swap_coldkey_duplicate --exact --nocapture -#[test] -fn test_schedule_swap_coldkey_duplicate() { - new_test_ext(1).execute_with(|| { - let old_coldkey = U256::from(1); - let new_coldkey = U256::from(2); - - let swap_cost = SubtensorModule::get_key_swap_cost(); - - SubtensorModule::add_balance_to_coldkey_account(&old_coldkey, swap_cost.to_u64() + 2_000); - - assert_ok!(SubtensorModule::schedule_swap_coldkey( - <::RuntimeOrigin>::signed(old_coldkey), - new_coldkey - )); - - // Attempt to schedule again - assert_noop!( - SubtensorModule::schedule_swap_coldkey( - <::RuntimeOrigin>::signed(old_coldkey), - new_coldkey - ), - Error::::SwapAlreadyScheduled - ); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=info cargo test --package pallet-subtensor --lib -- tests::swap_coldkey::test_schedule_swap_coldkey_execution --exact --show-output --nocapture -#[test] -fn test_schedule_swap_coldkey_execution() { - new_test_ext(1).execute_with(|| { - let old_coldkey = U256::from(1); - let new_coldkey = U256::from(2); - let hotkey = U256::from(3); - let netuid = NetUid::from(1u16); - let stake_amount = DefaultMinStake::::get().to_u64() * 10; - let reserve = stake_amount * 10; - - mock::setup_reserves(netuid, reserve.into(), reserve.into()); - - add_network(netuid, 13, 0); - register_ok_neuron(netuid, hotkey, old_coldkey, 0); - SubtensorModule::add_balance_to_coldkey_account(&old_coldkey, 1000000000000000); - assert_ok!(SubtensorModule::add_stake( - <::RuntimeOrigin>::signed(old_coldkey), - hotkey, - netuid, - stake_amount.into() - )); - - // Check initial ownership - assert_eq!( - Owner::::get(hotkey), - old_coldkey, - "Initial ownership check failed" - ); - - let swap_cost = SubtensorModule::get_key_swap_cost(); - - // Schedule the swap - assert_ok!(SubtensorModule::schedule_swap_coldkey( - <::RuntimeOrigin>::signed(old_coldkey), - new_coldkey - )); - - // Get the scheduled execution block - let current_block = System::block_number(); - let execution_block = current_block + ColdkeySwapScheduleDuration::::get(); - - System::assert_last_event( - Event::ColdkeySwapScheduled { - old_coldkey, - new_coldkey, - execution_block, - swap_cost, - } - .into(), - ); - - run_to_block(execution_block - 1); - - let stake_before_swap = SubtensorModule::get_total_stake_for_coldkey(&old_coldkey); - - run_to_block(execution_block); + assert_ok!(SubtensorModule::add_stake( + <::RuntimeOrigin>::signed(coldkey), + delegate, + netuid2, + stake.into() + )); - // Run on_initialize for the execution block - >::on_initialize(execution_block); + // Perform the swap + assert_ok!(SubtensorModule::do_swap_coldkey(&coldkey, &new_coldkey,)); - // Also run Scheduler's on_initialize - as OnInitialize>::on_initialize( - execution_block, + // Verify stake was moved for the delegate + let approx_total_stake = TaoCurrency::from(stake * 2 - fee * 2); + assert_abs_diff_eq!( + SubtensorModule::get_total_stake_for_hotkey(&delegate), + approx_total_stake, + epsilon = approx_total_stake / 100.into() ); - - // Check if the swap has occurred - let new_owner = Owner::::get(hotkey); assert_eq!( - new_owner, new_coldkey, - "Ownership was not updated as expected" + SubtensorModule::get_total_stake_for_coldkey(&coldkey), + TaoCurrency::ZERO ); - - assert_eq!( + assert_abs_diff_eq!( SubtensorModule::get_total_stake_for_coldkey(&new_coldkey), - stake_before_swap, - "Stake was not transferred to new coldkey" + approx_total_stake, + epsilon = approx_total_stake / 100.into() ); assert_eq!( - SubtensorModule::get_total_stake_for_coldkey(&old_coldkey), - TaoCurrency::ZERO, - "Old coldkey still has stake" + expected_stake, + Alpha::::get((delegate, new_coldkey, netuid)) + .to_num::() + .into(), ); + assert_eq!(Alpha::::get((delegate, coldkey, netuid)), 0); - // Check for the SwapExecuted event - System::assert_has_event( - Event::ColdkeySwapped { - old_coldkey, - new_coldkey, - swap_cost, - } - .into(), + assert_eq!( + expected_stake, + Alpha::::get((delegate, new_coldkey, netuid2)) + .to_num::() + .into() ); + assert_eq!(Alpha::::get((delegate, coldkey, netuid2)), 0); }); } -// SKIP_WASM_BUILD=1 RUST_LOG=info cargo test --test swap_coldkey -- test_direct_swap_coldkey_call_fails --exact --nocapture #[test] -fn test_direct_swap_coldkey_call_fails() { +fn test_dispute_coldkey_swap_works() { new_test_ext(1).execute_with(|| { - let old_coldkey = U256::from(1); + let who = U256::from(1); let new_coldkey = U256::from(2); + let new_coldkey_hash = ::Hashing::hash_of(&new_coldkey); + let now = System::block_number(); - assert_noop!( - SubtensorModule::swap_coldkey( - <::RuntimeOrigin>::signed(old_coldkey), - old_coldkey, - new_coldkey, - TaoCurrency::ZERO - ), - BadOrigin - ); + ColdkeySwapAnnouncements::::insert(who, (now, new_coldkey_hash)); + + assert_ok!(SubtensorModule::dispute_coldkey_swap( + RuntimeOrigin::signed(who) + )); + + assert_eq!(ColdkeySwapDisputes::::get(who), Some(now)); + assert!(matches!( + last_event(), + RuntimeEvent::SubtensorModule(Event::ColdkeySwapDisputed { coldkey: _ }) + )); }); } -// SKIP_WASM_BUILD=1 RUST_LOG=info cargo test --test swap_coldkey -- test_schedule_swap_coldkey_with_pending_swap --exact --nocapture #[test] -fn test_schedule_swap_coldkey_with_pending_swap() { +fn test_dispute_coldkey_swap_with_bad_origin_fails() { new_test_ext(1).execute_with(|| { - let old_coldkey = U256::from(1); - let new_coldkey1 = U256::from(2); - let new_coldkey2 = U256::from(3); - - let swap_cost = SubtensorModule::get_key_swap_cost(); + let who = U256::from(1); + let new_coldkey = U256::from(2); + let new_coldkey_hash = ::Hashing::hash_of(&new_coldkey); + let now = System::block_number(); - SubtensorModule::add_balance_to_coldkey_account(&old_coldkey, swap_cost.to_u64() + 1_000); + ColdkeySwapAnnouncements::::insert(who, (now, new_coldkey_hash)); - assert_ok!(SubtensorModule::schedule_swap_coldkey( - <::RuntimeOrigin>::signed(old_coldkey), - new_coldkey1 - )); + assert_noop!( + SubtensorModule::dispute_coldkey_swap(RuntimeOrigin::root()), + BadOrigin + ); - // Attempt to schedule another swap before the first one executes assert_noop!( - SubtensorModule::schedule_swap_coldkey( - <::RuntimeOrigin>::signed(old_coldkey), - new_coldkey2 - ), - Error::::SwapAlreadyScheduled + SubtensorModule::dispute_coldkey_swap(RuntimeOrigin::none()), + BadOrigin ); }); } -// SKIP_WASM_BUILD=1 RUST_LOG=info cargo test --test swap_coldkey -- test_schedule_swap_coldkey_failure_and_reschedule --exact --nocapture #[test] -fn test_schedule_swap_coldkey_failure_and_reschedule() { +fn test_dispute_coldkey_swap_without_announcement_fails() { new_test_ext(1).execute_with(|| { - let old_coldkey = U256::from(1); - let new_coldkey1 = U256::from(2); - let new_coldkey2 = U256::from(3); - - let swap_cost = SubtensorModule::get_key_swap_cost(); - - // Two swaps - SubtensorModule::add_balance_to_coldkey_account( - &old_coldkey, - swap_cost.to_u64() + 1_000 * 2, - ); - - assert_ok!(SubtensorModule::schedule_swap_coldkey( - <::RuntimeOrigin>::signed(old_coldkey), - new_coldkey1 - )); - - let current_block = >::block_number(); - let duration = ColdkeySwapScheduleDuration::::get(); - let when = current_block.saturating_add(duration); - - // Setup first key to fail - // -- will fail if the new coldkey is already a hotkey (has an Owner) - Owner::::insert(new_coldkey1, U256::from(4)); - - // First swap fails - run_to_block(when - 1); - next_block(); - - // Check the failure - next_block(); // Still in the scheduled-swap map - assert!(ColdkeySwapScheduled::::contains_key(old_coldkey)); + let who = U256::from(1); + let new_coldkey = U256::from(2); + let new_coldkey_hash = ::Hashing::hash_of(&new_coldkey); + let now = System::block_number(); - // Try to schedule the second swap assert_noop!( - SubtensorModule::schedule_swap_coldkey( - <::RuntimeOrigin>::signed(old_coldkey), - new_coldkey2 - ), - Error::::SwapAlreadyScheduled + SubtensorModule::dispute_coldkey_swap(RuntimeOrigin::signed(who)), + Error::::ColdkeySwapAnnouncementNotFound ); - - // Wait for correct duration after first swap fails - let fail_duration = ColdkeySwapRescheduleDuration::::get(); - run_to_block(when + fail_duration); - - // Schedule the second swap - assert_ok!(SubtensorModule::schedule_swap_coldkey( - <::RuntimeOrigin>::signed(old_coldkey), - new_coldkey2 - )); - - let current_block = >::block_number(); - let duration = ColdkeySwapScheduleDuration::::get(); - let when = current_block.saturating_add(duration); - run_to_block(when - 1); - next_block(); - - // Check the success - next_block(); // Now in the scheduled-swap map - assert!(!ColdkeySwapScheduled::::contains_key(old_coldkey)); }); } #[test] -fn test_coldkey_swap_delegate_identity_updated() { +fn test_dispute_coldkey_swap_already_disputed_fails() { new_test_ext(1).execute_with(|| { - let old_coldkey = U256::from(1); + let who = U256::from(1); let new_coldkey = U256::from(2); + let new_coldkey_hash = ::Hashing::hash_of(&new_coldkey); + let now = System::block_number(); - let netuid = NetUid::from(1); - let burn_cost = TaoCurrency::from(10); - let tempo = 1; - - SubtensorModule::set_burn(netuid, burn_cost); - add_network(netuid, tempo, 0); - - SubtensorModule::add_balance_to_coldkey_account(&old_coldkey, 100_000_000_000); - mock::setup_reserves(netuid, 1_000_000_000_000.into(), 1_000_000_000_000.into()); - - assert_ok!(SubtensorModule::burned_register( - <::RuntimeOrigin>::signed(old_coldkey), - netuid, - old_coldkey - )); - - let name: Vec = b"The Third Coolest Identity".to_vec(); - let identity: ChainIdentityV2 = ChainIdentityV2 { - name: name.clone(), - url: vec![], - image: vec![], - github_repo: vec![], - discord: vec![], - description: vec![], - additional: vec![], - }; - - IdentitiesV2::::insert(old_coldkey, identity.clone()); - - assert!(IdentitiesV2::::get(old_coldkey).is_some()); - assert!(IdentitiesV2::::get(new_coldkey).is_none()); - - assert_ok!(SubtensorModule::do_swap_coldkey( - &old_coldkey, - &new_coldkey, - burn_cost - )); + ColdkeySwapAnnouncements::::insert(who, (now, new_coldkey_hash)); + ColdkeySwapDisputes::::insert(who, now); - assert!(IdentitiesV2::::get(old_coldkey).is_none()); - assert!(IdentitiesV2::::get(new_coldkey).is_some()); - assert_eq!( - IdentitiesV2::::get(new_coldkey).expect("Expected an Identity"), - identity + assert_noop!( + SubtensorModule::dispute_coldkey_swap(RuntimeOrigin::signed(who)), + Error::::ColdkeySwapAlreadyDisputed ); }); } #[test] -fn test_coldkey_swap_no_identity_no_changes() { +fn test_reset_coldkey_swap_works() { new_test_ext(1).execute_with(|| { - let old_coldkey = U256::from(1); + let who = U256::from(1); let new_coldkey = U256::from(2); + let new_coldkey_hash = ::Hashing::hash_of(&new_coldkey); + let now = System::block_number(); - let netuid = NetUid::from(1); - let burn_cost = TaoCurrency::from(10); - let tempo = 1; - - SubtensorModule::set_burn(netuid, burn_cost); - add_network(netuid, tempo, 0); + ColdkeySwapAnnouncements::::insert(who, (now, new_coldkey_hash)); + ColdkeySwapDisputes::::insert(who, now); - SubtensorModule::add_balance_to_coldkey_account(&old_coldkey, 100_000_000_000); - mock::setup_reserves(netuid, 1_000_000_000_000.into(), 1_000_000_000_000.into()); - - assert_ok!(SubtensorModule::burned_register( - <::RuntimeOrigin>::signed(old_coldkey), - netuid, - old_coldkey + assert_ok!(SubtensorModule::reset_coldkey_swap( + RuntimeOrigin::root(), + who, )); - // Ensure the old coldkey does not have an identity before the swap - assert!(IdentitiesV2::::get(old_coldkey).is_none()); - - // Perform the coldkey swap - assert_ok!(SubtensorModule::do_swap_coldkey( - &old_coldkey, - &new_coldkey, - burn_cost + assert!(!ColdkeySwapAnnouncements::::contains_key(who)); + assert!(!ColdkeySwapDisputes::::contains_key(who)); + assert!(matches!( + last_event(), + RuntimeEvent::SubtensorModule(Event::ColdkeySwapReset { who }) )); - - // Ensure no identities have been changed - assert!(IdentitiesV2::::get(old_coldkey).is_none()); - assert!(IdentitiesV2::::get(new_coldkey).is_none()); }); } #[test] -fn test_coldkey_swap_no_identity_no_changes_newcoldkey_exists() { +fn test_reset_coldkey_swap_with_bad_origin_fails() { new_test_ext(1).execute_with(|| { - let old_coldkey = U256::from(3); - let new_coldkey = U256::from(4); - - let netuid = NetUid::from(1); - let burn_cost = TaoCurrency::from(10); - let tempo = 1; - - SubtensorModule::set_burn(netuid, burn_cost); - add_network(netuid, tempo, 0); - SubtensorModule::add_balance_to_coldkey_account(&old_coldkey, 100_000_000_000); - mock::setup_reserves(netuid, 1_000_000_000_000.into(), 1_000_000_000_000.into()); - - assert_ok!(SubtensorModule::burned_register( - <::RuntimeOrigin>::signed(old_coldkey), - netuid, - old_coldkey - )); - - let name: Vec = b"The Coolest Identity".to_vec(); - let identity: ChainIdentityV2 = ChainIdentityV2 { - name: name.clone(), - url: vec![], - github_repo: vec![], - image: vec![], - discord: vec![], - description: vec![], - additional: vec![], - }; + let who = U256::from(1); + let coldkey = U256::from(2); - IdentitiesV2::::insert(new_coldkey, identity.clone()); - // Ensure the new coldkey does have an identity before the swap - assert!(IdentitiesV2::::get(new_coldkey).is_some()); - assert!(IdentitiesV2::::get(old_coldkey).is_none()); - - // Perform the coldkey swap - assert_ok!(SubtensorModule::do_swap_coldkey( - &old_coldkey, - &new_coldkey, - burn_cost - )); + assert_noop!( + SubtensorModule::reset_coldkey_swap(RuntimeOrigin::signed(who), coldkey), + BadOrigin + ); - // Ensure no identities have been changed - assert!(IdentitiesV2::::get(old_coldkey).is_none()); - assert!(IdentitiesV2::::get(new_coldkey).is_some()); + assert_noop!( + SubtensorModule::reset_coldkey_swap(RuntimeOrigin::none(), coldkey), + BadOrigin + ); }); } -// SKIP_WASM_BUILD=1 RUST_LOG=info cargo test --test swap_coldkey -- test_cant_schedule_swap_without_enough_to_burn --exact --nocapture #[test] -fn test_cant_schedule_swap_without_enough_to_burn() { +#[allow(deprecated)] +fn test_schedule_swap_coldkey_deprecated() { new_test_ext(1).execute_with(|| { - let old_coldkey = U256::from(3); - let new_coldkey = U256::from(4); - let hotkey = U256::from(5); + let old_coldkey = U256::from(1); + let new_coldkey = U256::from(2); - let burn_cost = SubtensorModule::get_key_swap_cost(); assert_noop!( SubtensorModule::schedule_swap_coldkey( - <::RuntimeOrigin>::signed(old_coldkey), - new_coldkey + <::RuntimeOrigin>::root(), + new_coldkey, ), - Error::::NotEnoughBalanceToPaySwapColdKey + Error::::Deprecated ); }); } -// SKIP_WASM_BUILD=1 RUST_LOG=info cargo test --package pallet-subtensor --lib -- tests::swap_coldkey::test_coldkey_in_swap_schedule_prevents_funds_usage --exact --show-output --nocapture -#[test] -fn test_coldkey_in_swap_schedule_prevents_funds_usage() { - // Testing the signed extension validate function - // correctly filters transactions that attempt to use funds - // while a coldkey swap is scheduled. - - new_test_ext(0).execute_with(|| { - let netuid = NetUid::from(1); - let version_key: u64 = 0; - let coldkey = U256::from(0); - let new_coldkey = U256::from(1); - let hotkey: U256 = U256::from(2); // Add the hotkey field - assert_ne!(hotkey, coldkey); // Ensure hotkey is NOT the same as coldkey !!! - - let stake = 100_000_000_000; - let reserve = stake * 100; - - mock::setup_reserves(netuid, reserve.into(), reserve.into()); +#[macro_export] +macro_rules! comprehensive_setup { + ( + $who:expr, + $new_coldkey:expr, + $new_coldkey_hash:expr, + $stake1:expr, + $stake2:expr, + $stake3:expr, + $hotkey1:expr, + $hotkey2:expr, + $hotkey3:expr + ) => {{ + // Setup networks and subnet ownerships + let netuid1 = NetUid::from(1); + let netuid2 = NetUid::from(2); + add_network(netuid1, 1, 0); + add_network(netuid2, 1, 0); + SubnetOwner::::insert(netuid1, $who); + SubnetOwner::::insert(netuid2, $who); + + // Setup reserves + let reserve1 = ($stake1 + $stake3) * 10; + let reserve2 = $stake2 * 10; + mock::setup_reserves(netuid1, reserve1.into(), reserve1.into()); + mock::setup_reserves(netuid2, reserve2.into(), reserve2.into()); + + // Setup auto stake destinations + AutoStakeDestination::::insert($who, netuid1, $hotkey1); + AutoStakeDestination::::insert($who, netuid2, $hotkey2); + AutoStakeDestinationColdkeys::::insert( + $hotkey1, + netuid1, + vec![$who, U256::from(3), U256::from(4)], + ); + AutoStakeDestinationColdkeys::::insert( + $hotkey2, + netuid2, + vec![U256::from(7), U256::from(8), $who], + ); - let who = coldkey; // The coldkey signs this transaction - - // Disallowed transactions are - // - add_stake - // - add_stake_limit - // - swap_stake - // - swap_stake_limit - // - move_stake - // - transfer_stake - // - balances.transfer_all - // - balances.transfer_allow_death - // - balances.transfer_keep_alive - - // Allowed transactions are: - // - remove_stake - // - remove_stake_limit - // others... - - // Create netuid - add_network(netuid, 1, 0); - // Register the hotkey - SubtensorModule::append_neuron(netuid, &hotkey, 0); - crate::Owner::::insert(hotkey, coldkey); + // Setup neurons with stake + register_ok_neuron(netuid1, $hotkey1, $who, 0); + register_ok_neuron(netuid2, $hotkey2, $who, 0); + register_ok_neuron(netuid1, $hotkey3, $who, 0); - SubtensorModule::add_balance_to_coldkey_account(&who, u64::MAX); + let hotkeys = vec![$hotkey1, $hotkey2, $hotkey3]; + assert_eq!(StakingHotkeys::::get($who), hotkeys); + assert_eq!(OwnedHotkeys::::get($who), hotkeys); + assert_eq!(Owner::::get($hotkey1), $who); + assert_eq!(Owner::::get($hotkey2), $who); + assert_eq!(Owner::::get($hotkey3), $who); - // Set the minimum stake to 0. - SubtensorModule::set_stake_threshold(0); - // Add stake to the hotkey assert_ok!(SubtensorModule::add_stake( - <::RuntimeOrigin>::signed(who), - hotkey, - netuid, - stake.into() - )); - - // Schedule the coldkey for a swap - assert_ok!(SubtensorModule::schedule_swap_coldkey( - <::RuntimeOrigin>::signed(who), - new_coldkey + <::RuntimeOrigin>::signed($who), + $hotkey1, + netuid1, + $stake1.into() )); - - assert!(ColdkeySwapScheduled::::contains_key(who)); - - // Setup the extension - let info: DispatchInfo = - DispatchInfoOf::<::RuntimeCall>::default(); - let extension = SubtensorTransactionExtension::::new(); - - // Try each call - - // Add stake - let call = RuntimeCall::SubtensorModule(SubtensorCall::add_stake { - hotkey, - netuid, - amount_staked: stake.into(), - }); - let result = extension.validate( - RawOrigin::Signed(who).into(), - &call.clone(), - &info, - 10, - (), - &TxBaseImplication(()), - TransactionSource::External, - ); - // Should fail - assert_eq!( - // Should get an invalid transaction error - result.unwrap_err(), - CustomTransactionError::ColdkeyInSwapSchedule.into() - ); - - // Add stake limit - let call = RuntimeCall::SubtensorModule(SubtensorCall::add_stake_limit { - hotkey, - netuid, - amount_staked: stake.into(), - limit_price: stake.into(), - allow_partial: false, - }); - let result = extension.validate( - RawOrigin::Signed(who).into(), - &call.clone(), - &info, - 10, - (), - &TxBaseImplication(()), - TransactionSource::External, - ); - // Should fail - assert_eq!( - // Should get an invalid transaction error - result.unwrap_err(), - CustomTransactionError::ColdkeyInSwapSchedule.into() - ); - - // Swap stake - let call = RuntimeCall::SubtensorModule(SubtensorCall::swap_stake { - hotkey, - origin_netuid: netuid, - destination_netuid: netuid, - alpha_amount: stake.into(), - }); - let result = extension.validate( - RawOrigin::Signed(who).into(), - &call.clone(), - &info, - 10, - (), - &TxBaseImplication(()), - TransactionSource::External, - ); - // Should fail - assert_eq!( - // Should get an invalid transaction error - result.unwrap_err(), - CustomTransactionError::ColdkeyInSwapSchedule.into() - ); - - // Swap stake limit - let call = RuntimeCall::SubtensorModule(SubtensorCall::swap_stake_limit { - hotkey, - origin_netuid: netuid, - destination_netuid: netuid, - alpha_amount: stake.into(), - limit_price: stake.into(), - allow_partial: false, - }); - let result = extension.validate( - RawOrigin::Signed(who).into(), - &call.clone(), - &info, - 10, - (), - &TxBaseImplication(()), - TransactionSource::External, - ); - // Should fail - assert_eq!( - // Should get an invalid transaction error - result.unwrap_err(), - CustomTransactionError::ColdkeyInSwapSchedule.into() - ); - - // Move stake - let call = RuntimeCall::SubtensorModule(SubtensorCall::move_stake { - origin_hotkey: hotkey, - destination_hotkey: hotkey, - origin_netuid: netuid, - destination_netuid: netuid, - alpha_amount: stake.into(), - }); - let result = extension.validate( - RawOrigin::Signed(who).into(), - &call.clone(), - &info, - 10, - (), - &TxBaseImplication(()), - TransactionSource::External, - ); - // Should fail - assert_eq!( - // Should get an invalid transaction error - result.unwrap_err(), - CustomTransactionError::ColdkeyInSwapSchedule.into() - ); - - // Transfer stake - let call = RuntimeCall::SubtensorModule(SubtensorCall::transfer_stake { - destination_coldkey: new_coldkey, - hotkey, - origin_netuid: netuid, - destination_netuid: netuid, - alpha_amount: stake.into(), - }); - let result = extension.validate( - RawOrigin::Signed(who).into(), - &call.clone(), - &info, - 10, - (), - &TxBaseImplication(()), - TransactionSource::External, - ); - // Should fail - assert_eq!( - // Should get an invalid transaction error - result.unwrap_err(), - CustomTransactionError::ColdkeyInSwapSchedule.into() - ); - - // Transfer all - let call = RuntimeCall::Balances(BalancesCall::transfer_all { - dest: new_coldkey, - keep_alive: false, - }); - let result = extension.validate( - RawOrigin::Signed(who).into(), - &call.clone(), - &info, - 10, - (), - &TxBaseImplication(()), - TransactionSource::External, - ); - // Should fail - assert_eq!( - // Should get an invalid transaction error - result.unwrap_err(), - CustomTransactionError::ColdkeyInSwapSchedule.into() - ); - - // Transfer keep alive - let call = RuntimeCall::Balances(BalancesCall::transfer_keep_alive { - dest: new_coldkey, - value: 100_000_000_000, - }); - let result = extension.validate( - RawOrigin::Signed(who).into(), - &call.clone(), - &info, - 10, - (), - &TxBaseImplication(()), - TransactionSource::External, - ); - // Should fail - assert_eq!( - // Should get an invalid transaction error - result.unwrap_err(), - CustomTransactionError::ColdkeyInSwapSchedule.into() - ); - - // Transfer allow death - let call = RuntimeCall::Balances(BalancesCall::transfer_allow_death { - dest: new_coldkey, - value: 100_000_000_000, - }); - let result = extension.validate( - RawOrigin::Signed(who).into(), - &call.clone(), - &info, - 10, - (), - &TxBaseImplication(()), - TransactionSource::External, - ); - // Should fail - assert_eq!( - // Should get an invalid transaction error - result.unwrap_err(), - CustomTransactionError::ColdkeyInSwapSchedule.into() - ); - - // Burned register - let call = RuntimeCall::SubtensorModule(SubtensorCall::burned_register { netuid, hotkey }); - let result = extension.validate( - RawOrigin::Signed(who).into(), - &call.clone(), - &info, - 10, - (), - &TxBaseImplication(()), - TransactionSource::External, - ); - // Should fail - assert_eq!( - // Should get an invalid transaction error - result.unwrap_err(), - CustomTransactionError::ColdkeyInSwapSchedule.into() - ); - - remove_stake_rate_limit_for_tests(&hotkey, &coldkey, netuid); - - // Remove stake - let call = RuntimeCall::SubtensorModule(SubtensorCall::remove_stake { - hotkey, - netuid, - amount_unstaked: (DefaultMinStake::::get().to_u64() * 2).into(), - }); - let result = extension.validate( - RawOrigin::Signed(who).into(), - &call.clone(), - &info, - 10, - (), - &TxBaseImplication(()), - TransactionSource::External, - ); - // Should fail - assert_eq!( - // Should get an invalid transaction error - result.unwrap_err(), - CustomTransactionError::ColdkeyInSwapSchedule.into() - ); - - // Remove stake limit - let call = RuntimeCall::SubtensorModule(SubtensorCall::remove_stake_limit { - hotkey, - netuid, - amount_unstaked: (DefaultMinStake::::get().to_u64() * 2).into(), - limit_price: 123456789.into(), // should be low enough - allow_partial: true, - }); - let result = extension.validate( - RawOrigin::Signed(who).into(), - &call.clone(), - &info, - 10, - (), - &TxBaseImplication(()), - TransactionSource::External, - ); - // Should fail - assert_eq!( - // Should get an invalid transaction error - result.unwrap_err(), - CustomTransactionError::ColdkeyInSwapSchedule.into() - ); - - // Schedule swap should succeed - let call = RuntimeCall::SubtensorModule(SubtensorCall::schedule_swap_coldkey { - new_coldkey: hotkey, - }); - let result = extension.validate( - RawOrigin::Signed(who).into(), - &call.clone(), - &info, - 10, - (), - &TxBaseImplication(()), - TransactionSource::External, - ); - // Should be ok - assert_ok!(result); - }); -} - -// SKIP_WASM_BUILD=1 RUST_LOG=info cargo test --package pallet-subtensor --lib -- tests::swap_coldkey::test_coldkey_in_swap_schedule_prevents_critical_calls --exact --show-output --nocapture -#[test] -fn test_coldkey_in_swap_schedule_prevents_critical_calls() { - // Testing the signed extension validate function - // correctly filters transactions that are critical - // while a coldkey swap is scheduled. - - new_test_ext(0).execute_with(|| { - let netuid = NetUid::from(1); - let version_key: u64 = 0; - let coldkey = U256::from(0); - let new_coldkey = U256::from(1); - let hotkey: U256 = U256::from(2); // Add the hotkey field - assert_ne!(hotkey, coldkey); // Ensure hotkey is NOT the same as coldkey !!! - let stake = 100_000_000_000; - let reserve = stake * 10; - - mock::setup_reserves(netuid, reserve.into(), reserve.into()); - - let who = coldkey; // The coldkey signs this transaction - - // Disallowed transactions are - // - dissolve_network - - // Create netuid - add_network(netuid, 1, 0); - // Register the hotkey - SubtensorModule::append_neuron(netuid, &hotkey, 0); - crate::Owner::::insert(hotkey, coldkey); - - SubtensorModule::add_balance_to_coldkey_account(&who, u64::MAX); - - // Set the minimum stake to 0. - SubtensorModule::set_stake_threshold(0); - // Add stake to the hotkey assert_ok!(SubtensorModule::add_stake( - <::RuntimeOrigin>::signed(who), - hotkey, - netuid, - stake.into() + <::RuntimeOrigin>::signed($who), + $hotkey2, + netuid2, + $stake2.into() )); + assert_ok!(SubtensorModule::add_stake( + <::RuntimeOrigin>::signed($who), + $hotkey3, + netuid1, + $stake3.into() + )); + let hk1_alpha = + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&$hotkey1, &$who, netuid1); + let hk2_alpha = + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&$hotkey2, &$who, netuid2); + let hk3_alpha = + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&$hotkey3, &$who, netuid1); + let total_ck_stake = SubtensorModule::get_total_stake_for_coldkey(&$who); + + // Setup identity + let identity = ChainIdentityV2::default(); + IdentitiesV2::::insert($who, identity.clone()); + assert_eq!(IdentitiesV2::::get($who), Some(identity.clone())); + assert!(IdentitiesV2::::get($new_coldkey).is_none()); + + let balance_before = SubtensorModule::get_coldkey_balance(&$who); + let total_stake_before = SubtensorModule::get_total_stake(); + + ( + netuid1, + netuid2, + hotkeys, + hk1_alpha, + hk2_alpha, + hk3_alpha, + total_ck_stake, + identity, + balance_before, + total_stake_before, + ) + }}; +} - // Schedule the coldkey for a swap - assert_ok!(SubtensorModule::schedule_swap_coldkey( - <::RuntimeOrigin>::signed(who), - new_coldkey - )); +#[macro_export] +macro_rules! comprehensive_checks { + ( + $who:expr, + $hotkey1:expr, + $hotkey2:expr, + $hotkey3:expr, + $hotkeys:expr, + $new_coldkey:expr, + $balance_before:expr, + $identity:expr, + $netuid1:expr, + $netuid2:expr, + $hk1_alpha:expr, + $hk2_alpha:expr, + $hk3_alpha:expr, + $total_ck_stake:expr, + $total_stake_before:expr, + $swap_cost:expr + ) => { + // Ensure the announcement has been consumed + assert!(!ColdkeySwapAnnouncements::::contains_key($who)); - assert!(ColdkeySwapScheduled::::contains_key(who)); + // Ensure the cost has been withdrawn from the old coldkey and recycled + let balance_after = SubtensorModule::get_coldkey_balance(&$who); + let ed = ExistentialDeposit::get(); + assert_eq!($balance_before - $swap_cost, balance_after + ed); - // Setup the extension - let info: DispatchInfo = - DispatchInfoOf::<::RuntimeCall>::default(); - let extension = SubtensorTransactionExtension::::new(); + // Ensure the identity is correctly swapped + assert!(IdentitiesV2::::get($who).is_none()); + assert_eq!(IdentitiesV2::::get($new_coldkey), Some($identity)); - // Try each call + // Ensure the subnet ownerships are correctly swapped + assert_eq!(SubnetOwner::::get($netuid1), $new_coldkey); + assert_eq!(SubnetOwner::::get($netuid2), $new_coldkey); - // Dissolve network - let call = - RuntimeCall::SubtensorModule(SubtensorCall::dissolve_network { netuid, coldkey }); - let result = extension.validate( - RawOrigin::Signed(who).into(), - &call.clone(), - &info, - 10, - (), - &TxBaseImplication(()), - TransactionSource::External, + // Ensure the auto stake destinations are correctly swapped + assert!(AutoStakeDestination::::get($who, $netuid1).is_none()); + assert!(AutoStakeDestination::::get($who, $netuid2).is_none()); + assert_eq!( + AutoStakeDestination::::get($new_coldkey, $netuid1), + Some($hotkey1) ); - // Should fail assert_eq!( - // Should get an invalid transaction error - result.unwrap_err(), - CustomTransactionError::ColdkeyInSwapSchedule.into() + AutoStakeDestination::::get($new_coldkey, $netuid2), + Some($hotkey2) + ); + assert_eq!( + AutoStakeDestinationColdkeys::::get($hotkey1, $netuid1), + vec![U256::from(3), U256::from(4), $new_coldkey] + ); + assert_eq!( + AutoStakeDestinationColdkeys::::get($hotkey2, $netuid2), + vec![U256::from(7), U256::from(8), $new_coldkey] ); - }); -} -#[test] -fn test_swap_auto_stake_destination_coldkeys() { - new_test_ext(1).execute_with(|| { - let old_coldkey = U256::from(1); - let new_coldkey = U256::from(2); - let hotkey = U256::from(3); - let netuid = NetUid::from(1u16); - let coldkeys = vec![U256::from(4), U256::from(5), old_coldkey]; + // Ensure the coldkey stake is correctly swapped + assert_eq!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&$hotkey1, &$who, $netuid1), + 0.into(), + ); + assert_eq!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&$hotkey2, &$who, $netuid2), + 0.into(), + ); + assert_eq!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&$hotkey3, &$who, $netuid1), + 0.into(), + ); + assert_eq!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &$hotkey1, + &$new_coldkey, + $netuid1 + ), + $hk1_alpha + ); + assert_eq!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &$hotkey2, + &$new_coldkey, + $netuid2 + ), + $hk2_alpha + ); + assert_eq!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &$hotkey3, + &$new_coldkey, + $netuid1 + ), + $hk3_alpha + ); + assert_eq!( + SubtensorModule::get_total_stake_for_coldkey(&$who), + TaoCurrency::ZERO + ); + assert_eq!( + SubtensorModule::get_total_stake_for_coldkey(&$new_coldkey), + $total_ck_stake, + ); - add_network(netuid, 1, 0); - AutoStakeDestinationColdkeys::::insert(hotkey, netuid, coldkeys.clone()); - AutoStakeDestination::::insert(old_coldkey, netuid, hotkey); + // Ensure the staking hotkeys are correctly swapped + assert!(StakingHotkeys::::get($who).is_empty()); + assert_eq!(StakingHotkeys::::get($new_coldkey), $hotkeys); - let mut weight = Weight::zero(); - assert_ok!(SubtensorModule::perform_swap_coldkey( - &old_coldkey, - &new_coldkey, - &mut weight - )); + // Ensure the hotkey ownership is correctly swapped + assert!(OwnedHotkeys::::get($who).is_empty()); + assert_eq!(OwnedHotkeys::::get($new_coldkey), $hotkeys); + assert_eq!(Owner::::get($hotkey1), $new_coldkey); + assert_eq!(Owner::::get($hotkey2), $new_coldkey); + assert_eq!(Owner::::get($hotkey3), $new_coldkey); - let new_coldkeys = AutoStakeDestinationColdkeys::::get(hotkey, netuid); - assert!(new_coldkeys.contains(&new_coldkey)); - assert!(!new_coldkeys.contains(&old_coldkey)); + // Ensure the remaining balance is transferred to the new coldkey + assert_eq!(SubtensorModule::get_coldkey_balance(&$who), 0); assert_eq!( - AutoStakeDestination::::try_get(old_coldkey, netuid), - Err(()) + SubtensorModule::get_coldkey_balance(&$new_coldkey), + ExistentialDeposit::get() ); + + // Ensure total stake is unchanged assert_eq!( - AutoStakeDestination::::try_get(new_coldkey, netuid), - Ok(hotkey) + SubtensorModule::get_total_stake(), + $total_stake_before, + "Total stake changed unexpectedly" ); - }); + + // Verify event emission + System::assert_last_event( + Event::ColdkeySwapped { + old_coldkey: $who, + new_coldkey: $new_coldkey, + } + .into(), + ); + }; } diff --git a/pallets/subtensor/src/tests/voting_power.rs b/pallets/subtensor/src/tests/voting_power.rs new file mode 100644 index 0000000000..63418fb6a9 --- /dev/null +++ b/pallets/subtensor/src/tests/voting_power.rs @@ -0,0 +1,760 @@ +#![allow(unused, clippy::indexing_slicing, clippy::panic, clippy::unwrap_used)] + +use alloc::collections::BTreeMap; +use frame_support::weights::Weight; +use frame_support::{assert_err, assert_noop, assert_ok}; +use frame_system::RawOrigin; +use sp_core::U256; +use subtensor_runtime_common::NetUid; + +use super::mock; +use super::mock::*; +use crate::epoch::run_epoch::EpochTerms; +use crate::utils::voting_power::{ + MAX_VOTING_POWER_EMA_ALPHA, VOTING_POWER_DISABLE_GRACE_PERIOD_BLOCKS, +}; +use crate::*; + +// ============================================ +// === Test Helpers === +// ============================================ + +const DEFAULT_STAKE_AMOUNT: u64 = 1_000_000_000_000; // 1 million RAO + +/// Build epoch output from current state for testing voting power updates. +fn build_mock_epoch_output(netuid: NetUid) -> BTreeMap { + let n = SubtensorModule::get_subnetwork_n(netuid); + let validator_permits = ValidatorPermit::::get(netuid); + + let mut output = BTreeMap::new(); + for uid in 0..n { + if let Ok(hotkey) = SubtensorModule::get_hotkey_for_net_and_uid(netuid, uid) { + let has_permit = validator_permits + .get(uid as usize) + .copied() + .unwrap_or(false); + let stake = SubtensorModule::get_stake_for_hotkey_on_subnet(&hotkey, netuid).to_u64(); + output.insert( + hotkey, + EpochTerms { + uid: uid as usize, + new_validator_permit: has_permit, + stake: stake.into(), + ..Default::default() + }, + ); + } + } + output +} + +/// Test fixture containing common test setup data +struct VotingPowerTestFixture { + hotkey: U256, + coldkey: U256, + netuid: NetUid, +} + +impl VotingPowerTestFixture { + /// Create a basic fixture with a dynamic network + fn new() -> Self { + let hotkey = U256::from(1); + let coldkey = U256::from(2); + let netuid = add_dynamic_network(&hotkey, &coldkey); + Self { + hotkey, + coldkey, + netuid, + } + } + + /// Setup reserves and add balance to coldkey for staking + fn setup_for_staking(&self) { + self.setup_for_staking_with_amount(DEFAULT_STAKE_AMOUNT); + } + + /// Setup reserves and add balance with custom amount + #[allow(clippy::arithmetic_side_effects)] + fn setup_for_staking_with_amount(&self, amount: u64) { + mock::setup_reserves(self.netuid, (amount * 100).into(), (amount * 100).into()); + SubtensorModule::add_balance_to_coldkey_account(&self.coldkey, amount * 10); + } + + /// Enable voting power tracking for the subnet + fn enable_tracking(&self) { + assert_ok!(SubtensorModule::enable_voting_power_tracking( + RuntimeOrigin::signed(self.coldkey), + self.netuid + )); + } + + /// Add stake from coldkey to hotkey + fn add_stake(&self, amount: u64) { + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(self.coldkey), + self.hotkey, + self.netuid, + amount.into() + )); + } + + /// Set validator permit for the hotkey (uid 0) + fn set_validator_permit(&self, has_permit: bool) { + ValidatorPermit::::insert(self.netuid, vec![has_permit]); + } + + /// Run voting power update for N epochs + fn run_epochs(&self, n: u32) { + for _ in 0..n { + let epoch_output = build_mock_epoch_output(self.netuid); + SubtensorModule::update_voting_power_for_subnet(self.netuid, &epoch_output); + } + } + + /// Get current voting power for the hotkey + fn get_voting_power(&self) -> u64 { + SubtensorModule::get_voting_power(self.netuid, &self.hotkey) + } + + /// Full setup: reserves, balance, tracking enabled, stake added, validator permit + fn setup_full(&self) { + self.setup_for_staking(); + self.enable_tracking(); + self.add_stake(DEFAULT_STAKE_AMOUNT); + self.set_validator_permit(true); + } +} + +// ============================================ +// === Test Enable/Disable Voting Power === +// ============================================ + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::voting_power::test_enable_voting_power_tracking --exact --nocapture +#[test] +fn test_enable_voting_power_tracking() { + new_test_ext(1).execute_with(|| { + let f = VotingPowerTestFixture::new(); + + // Initially disabled + assert!(!SubtensorModule::get_voting_power_tracking_enabled( + f.netuid + )); + + // Enable tracking (subnet owner can do this) + f.enable_tracking(); + + // Now enabled + assert!(SubtensorModule::get_voting_power_tracking_enabled(f.netuid)); + assert_eq!( + SubtensorModule::get_voting_power_disable_at_block(f.netuid), + 0 + ); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::voting_power::test_enable_voting_power_tracking_root_can_enable --exact --nocapture +#[test] +fn test_enable_voting_power_tracking_root_can_enable() { + new_test_ext(1).execute_with(|| { + let f = VotingPowerTestFixture::new(); + + // Root can enable + assert_ok!(SubtensorModule::enable_voting_power_tracking( + RuntimeOrigin::root(), + f.netuid + )); + + assert!(SubtensorModule::get_voting_power_tracking_enabled(f.netuid)); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::voting_power::test_disable_voting_power_tracking_schedules_disable --exact --nocapture +#[test] +fn test_disable_voting_power_tracking_schedules_disable() { + new_test_ext(1).execute_with(|| { + let f = VotingPowerTestFixture::new(); + f.enable_tracking(); + + let current_block = SubtensorModule::get_current_block_as_u64(); + + // Schedule disable + assert_ok!(SubtensorModule::disable_voting_power_tracking( + RuntimeOrigin::signed(f.coldkey), + f.netuid + )); + + // Still enabled, but scheduled for disable + assert!(SubtensorModule::get_voting_power_tracking_enabled(f.netuid)); + let disable_at = SubtensorModule::get_voting_power_disable_at_block(f.netuid); + assert_eq!( + disable_at, + current_block + VOTING_POWER_DISABLE_GRACE_PERIOD_BLOCKS + ); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::voting_power::test_disable_voting_power_tracking_fails_when_not_enabled --exact --nocapture +#[test] +fn test_disable_voting_power_tracking_fails_when_not_enabled() { + new_test_ext(1).execute_with(|| { + let f = VotingPowerTestFixture::new(); + + // Try to disable when not enabled + assert_noop!( + SubtensorModule::disable_voting_power_tracking( + RuntimeOrigin::signed(f.coldkey), + f.netuid + ), + Error::::VotingPowerTrackingNotEnabled + ); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::voting_power::test_enable_voting_power_tracking_non_owner_fails --exact --nocapture +#[test] +fn test_enable_voting_power_tracking_non_owner_fails() { + new_test_ext(1).execute_with(|| { + let f = VotingPowerTestFixture::new(); + let random_account = U256::from(999); + + // Non-owner cannot enable (returns BadOrigin) + assert_noop!( + SubtensorModule::enable_voting_power_tracking( + RuntimeOrigin::signed(random_account), + f.netuid + ), + sp_runtime::DispatchError::BadOrigin + ); + + // Should still be disabled + assert!(!SubtensorModule::get_voting_power_tracking_enabled( + f.netuid + )); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::voting_power::test_disable_voting_power_tracking_non_owner_fails --exact --nocapture +#[test] +fn test_disable_voting_power_tracking_non_owner_fails() { + new_test_ext(1).execute_with(|| { + let f = VotingPowerTestFixture::new(); + let random_account = U256::from(999); + f.enable_tracking(); + + // Non-owner cannot disable (returns BadOrigin) + assert_noop!( + SubtensorModule::disable_voting_power_tracking( + RuntimeOrigin::signed(random_account), + f.netuid + ), + sp_runtime::DispatchError::BadOrigin + ); + + // Should still be enabled with no disable scheduled + assert!(SubtensorModule::get_voting_power_tracking_enabled(f.netuid)); + assert_eq!( + SubtensorModule::get_voting_power_disable_at_block(f.netuid), + 0 + ); + }); +} + +// ============================================ +// === Test EMA Alpha === +// ============================================ + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::voting_power::test_set_voting_power_ema_alpha --exact --nocapture +#[test] +fn test_set_voting_power_ema_alpha() { + new_test_ext(1).execute_with(|| { + let f = VotingPowerTestFixture::new(); + + // Get default alpha + let default_alpha = SubtensorModule::get_voting_power_ema_alpha(f.netuid); + assert_eq!(default_alpha, 3_570_000_000_000_000); // 0.00357 * 10^18 = 2 weeks e-folding + + // Set new alpha (only root can do this) + let new_alpha: u64 = 500_000_000_000_000_000; // 0.5 * 10^18 + assert_ok!(SubtensorModule::sudo_set_voting_power_ema_alpha( + RuntimeOrigin::root(), + f.netuid, + new_alpha + )); + + assert_eq!( + SubtensorModule::get_voting_power_ema_alpha(f.netuid), + new_alpha + ); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::voting_power::test_set_voting_power_ema_alpha_fails_above_one --exact --nocapture +#[test] +fn test_set_voting_power_ema_alpha_fails_above_one() { + new_test_ext(1).execute_with(|| { + let f = VotingPowerTestFixture::new(); + + // Try to set alpha > 1.0 (> 10^18) + let invalid_alpha: u64 = MAX_VOTING_POWER_EMA_ALPHA + 1; + assert_noop!( + SubtensorModule::sudo_set_voting_power_ema_alpha( + RuntimeOrigin::root(), + f.netuid, + invalid_alpha + ), + Error::::InvalidVotingPowerEmaAlpha + ); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::voting_power::test_set_voting_power_ema_alpha_non_root_fails --exact --nocapture +#[test] +fn test_set_voting_power_ema_alpha_non_root_fails() { + new_test_ext(1).execute_with(|| { + let f = VotingPowerTestFixture::new(); + + // Non-root cannot set alpha + assert_noop!( + SubtensorModule::sudo_set_voting_power_ema_alpha( + RuntimeOrigin::signed(f.coldkey), + f.netuid, + 500_000_000_000_000_000 + ), + sp_runtime::DispatchError::BadOrigin + ); + }); +} + +// ============================================ +// === Test EMA Calculation === +// ============================================ + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::voting_power::test_voting_power_ema_calculation --exact --nocapture +#[test] +fn test_voting_power_ema_calculation() { + new_test_ext(1).execute_with(|| { + let f = VotingPowerTestFixture::new(); + f.setup_full(); + + // Initially voting power is 0 + assert_eq!(f.get_voting_power(), 0); + + // Run epoch to update voting power + f.run_epochs(1); + + // Voting power should now be > 0 (but less than full stake due to EMA starting from 0) + let voting_power_after_first_epoch = f.get_voting_power(); + assert!(voting_power_after_first_epoch > 0); + + // Run more epochs - voting power should increase towards stake + f.run_epochs(10); + + let voting_power_after_many_epochs = f.get_voting_power(); + assert!(voting_power_after_many_epochs > voting_power_after_first_epoch); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::voting_power::test_voting_power_cleared_when_deregistered --exact --nocapture +#[test] +fn test_voting_power_cleared_when_deregistered() { + new_test_ext(1).execute_with(|| { + let f = VotingPowerTestFixture::new(); + f.setup_full(); + + // Run epochs to build up voting power + f.run_epochs(10); + + let voting_power_before = f.get_voting_power(); + assert!(voting_power_before > 0, "Voting power should be built up"); + + // Deregister the hotkey (simulate by removing from IsNetworkMember) + IsNetworkMember::::remove(f.hotkey, f.netuid); + + // Run epoch - voting power should be cleared for deregistered hotkey + f.run_epochs(1); + + // Should be removed from storage immediately when deregistered + assert_eq!(f.get_voting_power(), 0); + assert!( + !VotingPower::::contains_key(f.netuid, f.hotkey), + "Entry should be removed when hotkey is deregistered" + ); + }); +} + +// ============================================ +// === Test Validators Only === +// ============================================ + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::voting_power::test_only_validators_get_voting_power --exact --nocapture +#[test] +fn test_only_validators_get_voting_power() { + new_test_ext(1).execute_with(|| { + let validator_hotkey = U256::from(1); + let miner_hotkey = U256::from(2); + let coldkey = U256::from(3); + + let netuid = add_dynamic_network(&validator_hotkey, &coldkey); + + mock::setup_reserves( + netuid, + (DEFAULT_STAKE_AMOUNT * 100).into(), + (DEFAULT_STAKE_AMOUNT * 100).into(), + ); + SubtensorModule::add_balance_to_coldkey_account(&coldkey, DEFAULT_STAKE_AMOUNT * 20); + + // Register miner + register_ok_neuron(netuid, miner_hotkey, coldkey, 0); + + // Enable voting power tracking + assert_ok!(SubtensorModule::enable_voting_power_tracking( + RuntimeOrigin::signed(coldkey), + netuid + )); + + // Add stake to both + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(coldkey), + validator_hotkey, + netuid, + DEFAULT_STAKE_AMOUNT.into() + )); + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(coldkey), + miner_hotkey, + netuid, + DEFAULT_STAKE_AMOUNT.into() + )); + + // Set validator permit: uid 0 (validator) has permit, uid 1 (miner) does not + ValidatorPermit::::insert(netuid, vec![true, false]); + + // Run epoch + let epoch_output = build_mock_epoch_output(netuid); + SubtensorModule::update_voting_power_for_subnet(netuid, &epoch_output); + + // Only validator should have voting power + assert!(SubtensorModule::get_voting_power(netuid, &validator_hotkey) > 0); + assert_eq!(SubtensorModule::get_voting_power(netuid, &miner_hotkey), 0); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::voting_power::test_miner_voting_power_removed_when_loses_vpermit --exact --nocapture +#[test] +fn test_miner_voting_power_removed_when_loses_vpermit() { + new_test_ext(1).execute_with(|| { + let f = VotingPowerTestFixture::new(); + f.setup_full(); + + // Run epochs to build voting power + f.run_epochs(10); + + let voting_power_before = f.get_voting_power(); + assert!(voting_power_before > 0); + + // Remove validator permit (now they're a miner) + f.set_validator_permit(false); + + // Run epoch - voting power should be removed + f.run_epochs(1); + + assert_eq!(f.get_voting_power(), 0); + }); +} + +// ============================================ +// === Test Hotkey Swap === +// ============================================ + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::voting_power::test_voting_power_transfers_on_hotkey_swap --exact --nocapture +#[test] +fn test_voting_power_transfers_on_hotkey_swap() { + new_test_ext(1).execute_with(|| { + let f = VotingPowerTestFixture::new(); + let new_hotkey = U256::from(99); + let voting_power_value = 5_000_000_000_000_u64; + + // Set some voting power for the old hotkey + VotingPower::::insert(f.netuid, f.hotkey, voting_power_value); + + // Verify old hotkey has voting power + assert_eq!(f.get_voting_power(), voting_power_value); + assert_eq!(SubtensorModule::get_voting_power(f.netuid, &new_hotkey), 0); + + // Perform hotkey swap for this subnet + SubtensorModule::swap_voting_power_for_hotkey(&f.hotkey, &new_hotkey, f.netuid); + + // Old hotkey should have 0, new hotkey should have the voting power + assert_eq!(f.get_voting_power(), 0); + assert_eq!( + SubtensorModule::get_voting_power(f.netuid, &new_hotkey), + voting_power_value + ); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::voting_power::test_voting_power_swap_adds_to_existing --exact --nocapture +#[test] +fn test_voting_power_swap_adds_to_existing() { + new_test_ext(1).execute_with(|| { + let f = VotingPowerTestFixture::new(); + let new_hotkey = U256::from(99); + let old_voting_power = 5_000_000_000_000_u64; + let new_existing_voting_power = 2_000_000_000_000_u64; + + // Set voting power for both hotkeys + VotingPower::::insert(f.netuid, f.hotkey, old_voting_power); + VotingPower::::insert(f.netuid, new_hotkey, new_existing_voting_power); + + // Perform swap + SubtensorModule::swap_voting_power_for_hotkey(&f.hotkey, &new_hotkey, f.netuid); + + // New hotkey should have combined voting power + assert_eq!(f.get_voting_power(), 0); + assert_eq!( + SubtensorModule::get_voting_power(f.netuid, &new_hotkey), + old_voting_power + new_existing_voting_power + ); + }); +} + +// ============================================ +// === Test Threshold Logic === +// ============================================ +// Tests the rule: Only remove voting power entry if it decayed FROM above threshold TO below. +// New validators building up from 0 should NOT be removed even if below threshold. + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::voting_power::test_voting_power_not_removed_if_never_above_threshold --exact --nocapture +#[test] +fn test_voting_power_not_removed_if_never_above_threshold() { + new_test_ext(1).execute_with(|| { + let f = VotingPowerTestFixture::new(); + f.setup_full(); + + // Get the threshold + let min_stake = SubtensorModule::get_stake_threshold(); + + // Set voting power directly to a value below threshold (simulating building up) + // This is below threshold but was never above it + let below_threshold = min_stake.saturating_sub(1); + VotingPower::::insert(f.netuid, f.hotkey, below_threshold); + + // Run epoch + f.run_epochs(1); + + // Key assertion: Entry should NOT be removed because previous_ema was below threshold + // The removal rule only triggers when previous_ema >= threshold and new_ema < threshold + let voting_power = f.get_voting_power(); + assert!( + voting_power > 0, + "Voting power should still exist - it was never above threshold" + ); + assert!( + VotingPower::::contains_key(f.netuid, f.hotkey), + "Entry should exist - it was never above threshold so shouldn't be removed" + ); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::voting_power::test_voting_power_not_removed_with_small_dip_below_threshold --exact --nocapture +#[test] +fn test_voting_power_not_removed_with_small_dip_below_threshold() { + new_test_ext(1).execute_with(|| { + let f = VotingPowerTestFixture::new(); + f.setup_for_staking(); + f.enable_tracking(); + f.set_validator_permit(true); + + let min_stake = SubtensorModule::get_stake_threshold(); + + // Set voting power above threshold (validator was established) + let above_threshold = min_stake + 100; + VotingPower::::insert(f.netuid, f.hotkey, above_threshold); + + // Simulate a small dip: new EMA drops to 95% of threshold (within 10% buffer) + // This is above the removal threshold (90%) so should NOT be removed + let small_dip = min_stake * 95 / 100; + VotingPower::::insert(f.netuid, f.hotkey, small_dip); + + // Manually trigger the removal check by setting previous to above threshold + // and running with stake that would produce EMA in the buffer zone + VotingPower::::insert(f.netuid, f.hotkey, above_threshold); + + // Build epoch output with stake that will produce EMA around 95% of threshold + let mut epoch_output = build_mock_epoch_output(f.netuid); + if let Some(terms) = epoch_output.get_mut(&f.hotkey) { + terms.stake = small_dip.into(); // Stake drops but stays in buffer zone + } + + SubtensorModule::update_voting_power_for_subnet(f.netuid, &epoch_output); + + // Should NOT be removed - dip is within hysteresis buffer + assert!( + VotingPower::::contains_key(f.netuid, f.hotkey), + "Entry should exist - small dip within 10% buffer should not trigger removal" + ); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::voting_power::test_voting_power_removed_with_significant_drop_below_threshold --exact --nocapture +#[test] +fn test_voting_power_removed_with_significant_drop_below_threshold() { + new_test_ext(1).execute_with(|| { + let f = VotingPowerTestFixture::new(); + f.enable_tracking(); + + // Use explicit values since get_stake_threshold() may return 0 in tests + let min_stake: u64 = 1_000_000_000; + StakeThreshold::::put(min_stake); + + // Set voting power above threshold (validator was established) + VotingPower::::insert(f.netuid, f.hotkey, min_stake); + + // Set alpha to 100% so new_ema = current_stake directly (for testing removal) + VotingPowerEmaAlpha::::insert(f.netuid, MAX_VOTING_POWER_EMA_ALPHA); + + // Build epoch output manually with stake = 0 and validator permit = true + let mut epoch_output = BTreeMap::new(); + epoch_output.insert( + f.hotkey, + EpochTerms { + uid: 0, + new_validator_permit: true, + stake: 0.into(), // Complete unstake + ..Default::default() + }, + ); + + // With alpha = 1.0: new_ema = 1.0 * 0 + 0 * previous = 0 + // 0 < removal_threshold (90% of min_stake = 900M) AND previous (1B) >= min_stake (1B) + // Should trigger removal + SubtensorModule::update_voting_power_for_subnet(f.netuid, &epoch_output); + + assert!( + !VotingPower::::contains_key(f.netuid, f.hotkey), + "Entry should be removed - stake dropped to 0 with alpha=1.0" + ); + }); +} + +// ============================================ +// === Test Tracking Not Active === +// ============================================ + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::voting_power::test_voting_power_not_updated_when_disabled --exact --nocapture +#[test] +fn test_voting_power_not_updated_when_disabled() { + new_test_ext(1).execute_with(|| { + let f = VotingPowerTestFixture::new(); + f.setup_for_staking(); + // DON'T enable voting power tracking + f.add_stake(DEFAULT_STAKE_AMOUNT); + f.set_validator_permit(true); + + // Run epoch + f.run_epochs(1); + + // Voting power should still be 0 since tracking is disabled + assert_eq!(f.get_voting_power(), 0); + }); +} + +// ============================================ +// === Test Re-enable After Disable === +// ============================================ + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::voting_power::test_reenable_voting_power_clears_disable_schedule --exact --nocapture +#[test] +fn test_reenable_voting_power_clears_disable_schedule() { + new_test_ext(1).execute_with(|| { + let f = VotingPowerTestFixture::new(); + f.enable_tracking(); + + // Schedule disable + assert_ok!(SubtensorModule::disable_voting_power_tracking( + RuntimeOrigin::signed(f.coldkey), + f.netuid + )); + + assert!(SubtensorModule::get_voting_power_disable_at_block(f.netuid) > 0); + + // Re-enable should clear the disable schedule + f.enable_tracking(); + + assert!(SubtensorModule::get_voting_power_tracking_enabled(f.netuid)); + assert_eq!( + SubtensorModule::get_voting_power_disable_at_block(f.netuid), + 0 + ); + }); +} + +// ============================================ +// === Test Grace Period Finalization === +// ============================================ + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::voting_power::test_voting_power_finalized_after_grace_period --exact --nocapture +#[test] +fn test_voting_power_finalized_after_grace_period() { + new_test_ext(1).execute_with(|| { + let f = VotingPowerTestFixture::new(); + f.setup_full(); + + // Build up voting power + f.run_epochs(10); + + let voting_power_before = f.get_voting_power(); + assert!(voting_power_before > 0); + + // Schedule disable + assert_ok!(SubtensorModule::disable_voting_power_tracking( + RuntimeOrigin::signed(f.coldkey), + f.netuid + )); + + let disable_at = SubtensorModule::get_voting_power_disable_at_block(f.netuid); + + // Advance block past grace period (time travel!) + System::set_block_number(disable_at + 1); + + // Run epoch - should finalize disable + f.run_epochs(1); + + // Tracking should be disabled and all entries cleared + assert!(!SubtensorModule::get_voting_power_tracking_enabled( + f.netuid + )); + assert_eq!( + SubtensorModule::get_voting_power_disable_at_block(f.netuid), + 0 + ); + assert_eq!(f.get_voting_power(), 0); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::voting_power::test_voting_power_continues_during_grace_period --exact --nocapture +#[test] +fn test_voting_power_continues_during_grace_period() { + new_test_ext(1).execute_with(|| { + let f = VotingPowerTestFixture::new(); + f.setup_full(); + + // Schedule disable + assert_ok!(SubtensorModule::disable_voting_power_tracking( + RuntimeOrigin::signed(f.coldkey), + f.netuid + )); + + let disable_at = SubtensorModule::get_voting_power_disable_at_block(f.netuid); + + // Set block to middle of grace period (time travel!) + System::set_block_number(disable_at - 1000); + + // Run epoch - should still update voting power during grace period + f.run_epochs(1); + + // Tracking should still be enabled and voting power should exist + assert!(SubtensorModule::get_voting_power_tracking_enabled(f.netuid)); + assert!(f.get_voting_power() > 0); + }); +} diff --git a/pallets/subtensor/src/tests/weights.rs b/pallets/subtensor/src/tests/weights.rs index 20ace5ee0d..2c64b23d32 100644 --- a/pallets/subtensor/src/tests/weights.rs +++ b/pallets/subtensor/src/tests/weights.rs @@ -34,7 +34,7 @@ use w3f_bls::EngineBLS; use super::mock; use super::mock::*; use crate::coinbase::reveal_commits::{LegacyWeightsTlockPayload, WeightsTlockPayload}; -use crate::transaction_extension::SubtensorTransactionExtension; +use crate::extensions::SubtensorTransactionExtension; use crate::*; /*************************** pub fn set_weights() tests diff --git a/pallets/subtensor/src/utils/misc.rs b/pallets/subtensor/src/utils/misc.rs index 10fc0535f0..609e43cf63 100644 --- a/pallets/subtensor/src/utils/misc.rs +++ b/pallets/subtensor/src/utils/misc.rs @@ -808,19 +808,14 @@ impl Pallet { TransferToggle::::get(netuid) } - /// Set the duration for coldkey swap - /// - /// # Arguments - /// - /// * `duration` - The blocks for coldkey swap execution. - /// - /// # Effects - /// - /// * Update the ColdkeySwapScheduleDuration storage. - /// * Emits a ColdkeySwapScheduleDurationSet evnet. - pub fn set_coldkey_swap_schedule_duration(duration: BlockNumberFor) { - ColdkeySwapScheduleDuration::::set(duration); - Self::deposit_event(Event::ColdkeySwapScheduleDurationSet(duration)); + pub fn set_coldkey_swap_announcement_delay(duration: BlockNumberFor) { + ColdkeySwapAnnouncementDelay::::set(duration); + Self::deposit_event(Event::ColdkeySwapAnnouncementDelaySet(duration)); + } + + pub fn set_coldkey_swap_reannouncement_delay(duration: BlockNumberFor) { + ColdkeySwapReannouncementDelay::::set(duration); + Self::deposit_event(Event::ColdkeySwapReannouncementDelaySet(duration)); } /// Set the duration for dissolve network diff --git a/pallets/subtensor/src/utils/mod.rs b/pallets/subtensor/src/utils/mod.rs index 3eb8439959..a91875da59 100644 --- a/pallets/subtensor/src/utils/mod.rs +++ b/pallets/subtensor/src/utils/mod.rs @@ -5,3 +5,4 @@ pub mod misc; pub mod rate_limiting; #[cfg(feature = "try-runtime")] pub mod try_state; +pub mod voting_power; diff --git a/pallets/subtensor/src/utils/rate_limiting.rs b/pallets/subtensor/src/utils/rate_limiting.rs index 85f58cfc64..0649b3b9e3 100644 --- a/pallets/subtensor/src/utils/rate_limiting.rs +++ b/pallets/subtensor/src/utils/rate_limiting.rs @@ -16,6 +16,7 @@ pub enum TransactionType { MechanismCountUpdate, MechanismEmission, MaxUidsTrimming, + AddStakeBurn, } impl TransactionType { @@ -44,6 +45,7 @@ impl TransactionType { (Tempo::::get(netuid) as u64).saturating_mul(epochs) } Self::SetSNOwnerHotkey => DefaultSetSNOwnerHotkeyRateLimit::::get(), + Self::AddStakeBurn => Tempo::::get(netuid) as u64, _ => self.rate_limit::(), } @@ -141,6 +143,7 @@ impl From for u16 { TransactionType::MechanismCountUpdate => 7, TransactionType::MechanismEmission => 8, TransactionType::MaxUidsTrimming => 9, + TransactionType::AddStakeBurn => 10, } } } @@ -158,6 +161,7 @@ impl From for TransactionType { 7 => TransactionType::MechanismCountUpdate, 8 => TransactionType::MechanismEmission, 9 => TransactionType::MaxUidsTrimming, + 10 => TransactionType::AddStakeBurn, _ => TransactionType::Unknown, } } diff --git a/pallets/subtensor/src/utils/voting_power.rs b/pallets/subtensor/src/utils/voting_power.rs new file mode 100644 index 0000000000..55437f8885 --- /dev/null +++ b/pallets/subtensor/src/utils/voting_power.rs @@ -0,0 +1,280 @@ +use super::*; +use crate::epoch::run_epoch::EpochTerms; +use alloc::collections::BTreeMap; +use subtensor_runtime_common::{AlphaCurrency, NetUid}; + +/// 14 days in blocks (assuming ~12 second blocks) +/// 14 * 24 * 60 * 60 / 12 = 100800 blocks +pub const VOTING_POWER_DISABLE_GRACE_PERIOD_BLOCKS: u64 = 100800; + +/// Maximum alpha value (1.0 represented as u64 with 18 decimals) +pub const MAX_VOTING_POWER_EMA_ALPHA: u64 = 1_000_000_000_000_000_000; + +impl Pallet { + // ======================== + // === Getters === + // ======================== + + /// Get voting power for a hotkey on a subnet. + /// Returns 0 if not found or tracking disabled. + pub fn get_voting_power(netuid: NetUid, hotkey: &T::AccountId) -> u64 { + VotingPower::::get(netuid, hotkey) + } + + /// Check if voting power tracking is enabled for a subnet. + pub fn get_voting_power_tracking_enabled(netuid: NetUid) -> bool { + VotingPowerTrackingEnabled::::get(netuid) + } + + /// Get the block at which voting power tracking will be disabled. + /// Returns 0 if not scheduled for disabling. + pub fn get_voting_power_disable_at_block(netuid: NetUid) -> u64 { + VotingPowerDisableAtBlock::::get(netuid) + } + + /// Get the EMA alpha value for voting power calculation on a subnet. + pub fn get_voting_power_ema_alpha(netuid: NetUid) -> u64 { + VotingPowerEmaAlpha::::get(netuid) + } + + // ======================== + // === Extrinsic Handlers === + // ======================== + + /// Enable voting power tracking for a subnet. + pub fn do_enable_voting_power_tracking(netuid: NetUid) -> DispatchResult { + // Enable tracking + VotingPowerTrackingEnabled::::insert(netuid, true); + + // Clear any scheduled disable + VotingPowerDisableAtBlock::::remove(netuid); + + // Emit event + Self::deposit_event(Event::VotingPowerTrackingEnabled { netuid }); + + log::info!("VotingPower tracking enabled for netuid {netuid:?}"); + + Ok(()) + } + + /// Schedule disabling of voting power tracking for a subnet. + /// Tracking will continue for 14 days, then automatically disable. + pub fn do_disable_voting_power_tracking(netuid: NetUid) -> DispatchResult { + // Check if tracking is enabled + ensure!( + Self::get_voting_power_tracking_enabled(netuid), + Error::::VotingPowerTrackingNotEnabled + ); + + // Calculate the block at which tracking will be disabled + let current_block = Self::get_current_block_as_u64(); + let disable_at_block = + current_block.saturating_add(VOTING_POWER_DISABLE_GRACE_PERIOD_BLOCKS); + + // Schedule disable + VotingPowerDisableAtBlock::::insert(netuid, disable_at_block); + + // Emit event + Self::deposit_event(Event::VotingPowerTrackingDisableScheduled { + netuid, + disable_at_block, + }); + + log::info!( + "VotingPower tracking scheduled to disable at block {disable_at_block:?} for netuid {netuid:?}" + ); + + Ok(()) + } + + /// Set the EMA alpha value for voting power calculation on a subnet. + pub fn do_set_voting_power_ema_alpha(netuid: NetUid, alpha: u64) -> DispatchResult { + // Validate alpha (must be <= 1.0, represented as 10^18) + ensure!( + alpha <= MAX_VOTING_POWER_EMA_ALPHA, + Error::::InvalidVotingPowerEmaAlpha + ); + + // Set the alpha + VotingPowerEmaAlpha::::insert(netuid, alpha); + + // Emit event + Self::deposit_event(Event::VotingPowerEmaAlphaSet { netuid, alpha }); + + log::info!("VotingPower EMA alpha set to {alpha:?} for netuid {netuid:?}"); + + Ok(()) + } + + // ======================== + // === Epoch Processing === + // ======================== + + /// Update voting power for all validators on a subnet using pre-calculated epoch terms. + pub fn update_voting_power_for_subnet( + netuid: NetUid, + epoch_output: &BTreeMap, + ) { + // Early exit if tracking not enabled + if !Self::get_voting_power_tracking_enabled(netuid) { + return; + } + + // Check if past grace period and should finalize disable + let disable_at = Self::get_voting_power_disable_at_block(netuid); + if disable_at > 0 { + let current_block = Self::get_current_block_as_u64(); + if current_block >= disable_at { + Self::finalize_voting_power_disable(netuid); + return; + } + // Still in grace period - continue updating + } + + // Get the EMA alpha value for this subnet + let alpha = Self::get_voting_power_ema_alpha(netuid); + + // Get minimum stake threshold for validator permit + let min_stake = Self::get_stake_threshold(); + + // Iterate over epoch output using pre-calculated values + for (hotkey, terms) in epoch_output.iter() { + // Only validators (with vpermit) get voting power, not miners + if terms.new_validator_permit { + // Use the subnet-specific stake from epoch calculation + Self::update_voting_power_for_hotkey(netuid, hotkey, terms.stake, alpha, min_stake); + } else { + // Miner without vpermit - remove any existing voting power + VotingPower::::remove(netuid, hotkey); + } + } + + // Remove voting power for any hotkeys that are no longer registered on this subnet + Self::clear_voting_power_for_deregistered_hotkeys(netuid); + + log::trace!("VotingPower updated for validators on netuid {netuid:?}"); + } + + /// Clear voting power for hotkeys that are no longer registered on the subnet. + fn clear_voting_power_for_deregistered_hotkeys(netuid: NetUid) { + // Collect hotkeys to remove (can't mutate while iterating) + let hotkeys_to_remove: Vec = VotingPower::::iter_prefix(netuid) + .filter_map(|(hotkey, _)| { + // If the hotkey is not a network member, it's deregistered + if !IsNetworkMember::::get(&hotkey, netuid) { + Some(hotkey) + } else { + None + } + }) + .collect(); + + // Remove voting power for deregistered hotkeys + for hotkey in hotkeys_to_remove { + VotingPower::::remove(netuid, &hotkey); + log::trace!( + "VotingPower removed for deregistered hotkey {hotkey:?} on netuid {netuid:?}" + ); + } + } + + /// Update voting power EMA for a single hotkey using subnet-specific stake. + fn update_voting_power_for_hotkey( + netuid: NetUid, + hotkey: &T::AccountId, + current_stake: AlphaCurrency, + alpha: u64, + min_stake: u64, + ) { + // Get previous EMA value + let previous_ema = VotingPower::::get(netuid, hotkey); + + // Calculate new EMA value + // new_ema = alpha * current_stake + (1 - alpha) * previous_ema + // All values use 18 decimal precision for alpha (alpha is in range [0, 10^18]) + let new_ema = Self::calculate_voting_power_ema(current_stake.to_u64(), previous_ema, alpha); + + // Only remove if they previously had voting power ABOVE threshold and decayed below. + // This allows new validators to build up voting power from 0 without being removed. + if new_ema < min_stake && previous_ema >= min_stake { + // Was above threshold, now decayed below - remove + VotingPower::::remove(netuid, hotkey); + log::trace!( + "VotingPower removed for hotkey {hotkey:?} on netuid {netuid:?} (decayed below removal threshold: {new_ema:?} < {min_stake:?})" + ); + } else if new_ema > 0 { + // Update voting power (building up or maintaining) + VotingPower::::insert(netuid, hotkey, new_ema); + log::trace!( + "VotingPower updated for hotkey {hotkey:?} on netuid {netuid:?}: {previous_ema:?} -> {new_ema:?}" + ); + } + // If new_ema == 0 do nothing + } + + /// Calculate EMA for voting power. + /// new_ema = alpha * current_stake + (1 - alpha) * previous_ema + /// Alpha is in 18 decimal precision (10^18 = 1.0) + fn calculate_voting_power_ema(current_stake: u64, previous_ema: u64, alpha: u64) -> u64 { + // Use u128 for intermediate calculations to avoid overflow + let alpha_128 = alpha as u128; + let one_minus_alpha = (MAX_VOTING_POWER_EMA_ALPHA as u128).saturating_sub(alpha_128); + let current_128 = current_stake as u128; + let previous_128 = previous_ema as u128; + + // new_ema = (alpha * current_stake + (1 - alpha) * previous_ema) / 10^18 + let numerator = alpha_128 + .saturating_mul(current_128) + .saturating_add(one_minus_alpha.saturating_mul(previous_128)); + + let result = numerator + .checked_div(MAX_VOTING_POWER_EMA_ALPHA as u128) + .unwrap_or(0); + + // Safely convert back to u64, saturating at u64::MAX + result.min(u64::MAX as u128) as u64 + } + + /// Finalize the disabling of voting power tracking. + /// Clears all VotingPower entries for the subnet. + fn finalize_voting_power_disable(netuid: NetUid) { + // Clear all VotingPower entries for this subnet + let _ = VotingPower::::clear_prefix(netuid, u32::MAX, None); + + // Disable tracking + VotingPowerTrackingEnabled::::insert(netuid, false); + + // Clear disable schedule + VotingPowerDisableAtBlock::::remove(netuid); + + // Emit event + Self::deposit_event(Event::VotingPowerTrackingDisabled { netuid }); + + log::info!("VotingPower tracking disabled and entries cleared for netuid {netuid:?}"); + } + + // ======================== + // === Hotkey Swap === + // ======================== + + /// Transfer voting power from old hotkey to new hotkey during swap. + pub fn swap_voting_power_for_hotkey( + old_hotkey: &T::AccountId, + new_hotkey: &T::AccountId, + netuid: NetUid, + ) { + // Get voting power from old hotkey + let voting_power = VotingPower::::take(netuid, old_hotkey); + + // Transfer to new hotkey if non-zero + if voting_power > 0 { + // Add to any existing voting power on new hotkey (in case new hotkey already has some) + let existing = VotingPower::::get(netuid, new_hotkey); + VotingPower::::insert(netuid, new_hotkey, voting_power.saturating_add(existing)); + + log::trace!( + "VotingPower transferred from {old_hotkey:?} to {new_hotkey:?} on netuid {netuid:?}: {voting_power:?}" + ); + } + } +} diff --git a/pallets/swap-interface/src/lib.rs b/pallets/swap-interface/src/lib.rs index 19af1303c1..6e6ad7101a 100644 --- a/pallets/swap-interface/src/lib.rs +++ b/pallets/swap-interface/src/lib.rs @@ -2,7 +2,7 @@ use core::ops::Neg; use frame_support::pallet_prelude::*; -use substrate_fixed::types::U96F32; +use substrate_fixed::types::U64F64; use subtensor_macros::freeze_struct; use subtensor_runtime_common::{AlphaCurrency, Currency, NetUid, TaoCurrency}; @@ -38,19 +38,16 @@ pub trait SwapHandler { Self: SwapEngine; fn approx_fee_amount(netuid: NetUid, amount: T) -> T; - fn current_alpha_price(netuid: NetUid) -> U96F32; - fn get_protocol_tao(netuid: NetUid) -> TaoCurrency; + fn current_alpha_price(netuid: NetUid) -> U64F64; fn max_price() -> C; fn min_price() -> C; fn adjust_protocol_liquidity( netuid: NetUid, tao_delta: TaoCurrency, alpha_delta: AlphaCurrency, - ); - fn is_user_liquidity_enabled(netuid: NetUid) -> bool; - fn dissolve_all_liquidity_providers(netuid: NetUid) -> DispatchResult; - fn toggle_user_liquidity(netuid: NetUid, enabled: bool); + ) -> (TaoCurrency, AlphaCurrency); fn clear_protocol_liquidity(netuid: NetUid) -> DispatchResult; + fn init_swap(netuid: NetUid, maybe_price: Option); } pub trait DefaultPriceLimit @@ -61,7 +58,8 @@ where fn default_price_limit() -> C; } -#[freeze_struct("d3d0b124fe5a97c8")] +/// Externally used swap result (for RPC) +#[freeze_struct("58ff42da64adce1a")] #[derive(Decode, Encode, PartialEq, Eq, Clone, Debug, TypeInfo)] pub struct SwapResult where @@ -71,6 +69,7 @@ where pub amount_paid_in: PaidIn, pub amount_paid_out: PaidOut, pub fee_paid: PaidIn, + pub fee_to_block_author: PaidIn, } impl SwapResult diff --git a/pallets/swap-interface/src/order.rs b/pallets/swap-interface/src/order.rs index 1576283fd5..fc3f2acefe 100644 --- a/pallets/swap-interface/src/order.rs +++ b/pallets/swap-interface/src/order.rs @@ -11,7 +11,7 @@ pub trait Order: Clone { fn with_amount(amount: impl Into) -> Self; fn amount(&self) -> Self::PaidIn; - fn is_beyond_price_limit(&self, alpha_sqrt_price: U64F64, limit_sqrt_price: U64F64) -> bool; + fn is_beyond_price_limit(&self, current_price: U64F64, limit_price: U64F64) -> bool; } #[derive(Clone, Default)] @@ -45,8 +45,8 @@ where self.amount } - fn is_beyond_price_limit(&self, alpha_sqrt_price: U64F64, limit_sqrt_price: U64F64) -> bool { - alpha_sqrt_price < limit_sqrt_price + fn is_beyond_price_limit(&self, current_price: U64F64, limit_price: U64F64) -> bool { + current_price < limit_price } } @@ -81,7 +81,7 @@ where self.amount } - fn is_beyond_price_limit(&self, alpha_sqrt_price: U64F64, limit_sqrt_price: U64F64) -> bool { - alpha_sqrt_price > limit_sqrt_price + fn is_beyond_price_limit(&self, current_price: U64F64, limit_price: U64F64) -> bool { + current_price > limit_price } } diff --git a/pallets/swap/Cargo.toml b/pallets/swap/Cargo.toml index 7de8e49c1d..45389874a1 100644 --- a/pallets/swap/Cargo.toml +++ b/pallets/swap/Cargo.toml @@ -12,6 +12,7 @@ frame-support.workspace = true frame-system.workspace = true log.workspace = true safe-math.workspace = true +safe-bigmath.workspace = true scale-info = { workspace = true, features = ["derive"] } serde = { workspace = true, optional = true } sp-arithmetic.workspace = true @@ -28,6 +29,8 @@ subtensor-swap-interface.workspace = true [dev-dependencies] sp-tracing.workspace = true +rand = { version = "0.8", default-features = false } +rayon = "1.10" [lints] workspace = true @@ -42,7 +45,9 @@ std = [ "frame-system/std", "log/std", "pallet-subtensor-swap-runtime-api/std", + "rand/std", "safe-math/std", + "safe-bigmath/std", "scale-info/std", "serde/std", "sp-arithmetic/std", diff --git a/pallets/swap/rpc/src/lib.rs b/pallets/swap/rpc/src/lib.rs index b83a083ad5..a58334dea8 100644 --- a/pallets/swap/rpc/src/lib.rs +++ b/pallets/swap/rpc/src/lib.rs @@ -13,12 +13,14 @@ use sp_blockchain::HeaderBackend; use sp_runtime::traits::Block as BlockT; use subtensor_runtime_common::{AlphaCurrency, NetUid, TaoCurrency}; -pub use pallet_subtensor_swap_runtime_api::SwapRuntimeApi; +pub use pallet_subtensor_swap_runtime_api::{SubnetPrice, SwapRuntimeApi}; #[rpc(client, server)] pub trait SwapRpcApi { #[method(name = "swap_currentAlphaPrice")] fn current_alpha_price(&self, netuid: NetUid, at: Option) -> RpcResult; + #[method(name = "swap_currentAlphaPriceAll")] + fn current_alpha_price_all(&self, at: Option) -> RpcResult>; #[method(name = "swap_simSwapTaoForAlpha")] fn sim_swap_tao_for_alpha( &self, @@ -92,6 +94,18 @@ where }) } + fn current_alpha_price_all( + &self, + at: Option<::Hash>, + ) -> RpcResult> { + let api = self.client.runtime_api(); + let at = at.unwrap_or_else(|| self.client.info().best_hash); + + api.current_alpha_price_all(at).map_err(|e| { + Error::RuntimeError(format!("Unable to get all current alpha prices: {e:?}")).into() + }) + } + fn sim_swap_tao_for_alpha( &self, netuid: NetUid, diff --git a/pallets/swap/runtime-api/Cargo.toml b/pallets/swap/runtime-api/Cargo.toml index 042875fdd0..50f92d19e2 100644 --- a/pallets/swap/runtime-api/Cargo.toml +++ b/pallets/swap/runtime-api/Cargo.toml @@ -7,6 +7,7 @@ edition.workspace = true [dependencies] codec = { workspace = true, features = ["derive"] } frame-support.workspace = true +serde.workspace = true scale-info.workspace = true sp-api.workspace = true sp-std.workspace = true @@ -20,6 +21,7 @@ std = [ "codec/std", "frame-support/std", "scale-info/std", + "serde/std", "sp-api/std", "sp-std/std", "subtensor-runtime-common/std", diff --git a/pallets/swap/runtime-api/src/lib.rs b/pallets/swap/runtime-api/src/lib.rs index 01d2ccf23e..ce61a93dc6 100644 --- a/pallets/swap/runtime-api/src/lib.rs +++ b/pallets/swap/runtime-api/src/lib.rs @@ -1,21 +1,33 @@ #![cfg_attr(not(feature = "std"), no_std)] use frame_support::pallet_prelude::*; +use scale_info::prelude::vec::Vec; +use serde::{Deserialize, Serialize}; use subtensor_macros::freeze_struct; use subtensor_runtime_common::{AlphaCurrency, NetUid, TaoCurrency}; -#[freeze_struct("3a4fd213b5de5eb6")] +#[freeze_struct("ee2ba1ec4ee58ae6")] #[derive(Decode, Encode, PartialEq, Eq, Clone, Debug, TypeInfo)] pub struct SimSwapResult { pub tao_amount: TaoCurrency, pub alpha_amount: AlphaCurrency, pub tao_fee: TaoCurrency, pub alpha_fee: AlphaCurrency, + pub tao_slippage: TaoCurrency, + pub alpha_slippage: AlphaCurrency, +} + +#[freeze_struct("d7bbb761fc2b2eac")] +#[derive(Decode, Deserialize, Encode, PartialEq, Eq, Clone, Debug, Serialize, TypeInfo)] +pub struct SubnetPrice { + pub netuid: NetUid, + pub price: u64, } sp_api::decl_runtime_apis! { pub trait SwapRuntimeApi { fn current_alpha_price(netuid: NetUid) -> u64; + fn current_alpha_price_all() -> Vec; fn sim_swap_tao_for_alpha(netuid: NetUid, tao: TaoCurrency) -> SimSwapResult; fn sim_swap_alpha_for_tao(netuid: NetUid, alpha: AlphaCurrency) -> SimSwapResult; } diff --git a/pallets/swap/src/benchmarking.rs b/pallets/swap/src/benchmarking.rs index a17ac59141..c4a6afa87e 100644 --- a/pallets/swap/src/benchmarking.rs +++ b/pallets/swap/src/benchmarking.rs @@ -2,22 +2,16 @@ #![allow(clippy::unwrap_used)] #![allow(clippy::multiple_bound_locations)] -use core::marker::PhantomData; - use frame_benchmarking::v2::*; -use frame_support::traits::Get; use frame_system::RawOrigin; -use substrate_fixed::types::{I64F64, U64F64}; use subtensor_runtime_common::NetUid; -use crate::{ - pallet::{ - AlphaSqrtPrice, Call, Config, CurrentLiquidity, CurrentTick, Pallet, Positions, - SwapV3Initialized, - }, - position::{Position, PositionId}, - tick::TickIndex, -}; +use crate::pallet::{Call, Config, Pallet}; + +#[allow(dead_code)] +fn init_swap(netuid: NetUid) { + let _ = Pallet::::maybe_initialize_palswap(netuid, None); +} #[benchmarks(where T: Config)] mod benchmarks { @@ -32,117 +26,5 @@ mod benchmarks { set_fee_rate(RawOrigin::Root, netuid, rate); } - // TODO: Revise when user liquidity is available - // #[benchmark] - // fn add_liquidity() { - // let netuid = NetUid::from(1); - - // if !SwapV3Initialized::::get(netuid) { - // SwapV3Initialized::::insert(netuid, true); - // AlphaSqrtPrice::::insert(netuid, U64F64::from_num(1)); - // CurrentTick::::insert(netuid, TickIndex::new(0).unwrap()); - // CurrentLiquidity::::insert(netuid, T::MinimumLiquidity::get()); - // } - - // let caller: T::AccountId = whitelisted_caller(); - // let hotkey: T::AccountId = account("hotkey", 0, 0); - // let tick_low = TickIndex::new_unchecked(-1000); - // let tick_high = TickIndex::new_unchecked(1000); - - // #[extrinsic_call] - // add_liquidity( - // RawOrigin::Signed(caller), - // hotkey, - // netuid, - // tick_low, - // tick_high, - // 1000, - // ); - // } - - #[benchmark] - fn remove_liquidity() { - let netuid = NetUid::from(1); - - if !SwapV3Initialized::::get(netuid) { - SwapV3Initialized::::insert(netuid, true); - AlphaSqrtPrice::::insert(netuid, U64F64::from_num(1)); - CurrentTick::::insert(netuid, TickIndex::new(0).unwrap()); - CurrentLiquidity::::insert(netuid, T::MinimumLiquidity::get()); - } - - let caller: T::AccountId = whitelisted_caller(); - let hotkey: T::AccountId = account("hotkey", 0, 0); - let id = PositionId::from(1u128); - - Positions::::insert( - (netuid, caller.clone(), id), - Position { - id, - netuid, - tick_low: TickIndex::new(-10000).unwrap(), - tick_high: TickIndex::new(10000).unwrap(), - liquidity: 1000, - fees_tao: I64F64::from_num(0), - fees_alpha: I64F64::from_num(0), - _phantom: PhantomData, - }, - ); - - #[extrinsic_call] - remove_liquidity(RawOrigin::Signed(caller), hotkey, netuid.into(), id.into()); - } - - #[benchmark] - fn modify_position() { - let netuid = NetUid::from(1); - - if !SwapV3Initialized::::get(netuid) { - SwapV3Initialized::::insert(netuid, true); - AlphaSqrtPrice::::insert(netuid, U64F64::from_num(1)); - CurrentTick::::insert(netuid, TickIndex::new(0).unwrap()); - CurrentLiquidity::::insert(netuid, T::MinimumLiquidity::get()); - } - - let caller: T::AccountId = whitelisted_caller(); - let hotkey: T::AccountId = account("hotkey", 0, 0); - let id = PositionId::from(1u128); - - Positions::::insert( - (netuid, caller.clone(), id), - Position { - id, - netuid, - tick_low: TickIndex::new(-10000).unwrap(), - tick_high: TickIndex::new(10000).unwrap(), - liquidity: 10000, - fees_tao: I64F64::from_num(0), - fees_alpha: I64F64::from_num(0), - _phantom: PhantomData, - }, - ); - - #[extrinsic_call] - modify_position( - RawOrigin::Signed(caller), - hotkey, - netuid.into(), - id.into(), - -5000, - ); - } - - // #[benchmark] - // fn toggle_user_liquidity() { - // let netuid = NetUid::from(101); - - // assert!(!EnabledUserLiquidity::::get(netuid)); - - // #[extrinsic_call] - // toggle_user_liquidity(RawOrigin::Root, netuid.into(), true); - - // assert!(EnabledUserLiquidity::::get(netuid)); - // } - impl_benchmark_test_suite!(Pallet, crate::mock::new_test_ext(), crate::mock::Test); } diff --git a/pallets/swap/src/lib.rs b/pallets/swap/src/lib.rs index 6257df852b..b51c3351dc 100644 --- a/pallets/swap/src/lib.rs +++ b/pallets/swap/src/lib.rs @@ -1,10 +1,6 @@ #![cfg_attr(not(feature = "std"), no_std)] -use substrate_fixed::types::U64F64; - pub mod pallet; -pub mod position; -pub mod tick; pub mod weights; pub use pallet::*; @@ -14,5 +10,3 @@ pub mod benchmarking; #[cfg(test)] pub(crate) mod mock; - -type SqrtPrice = U64F64; diff --git a/pallets/swap/src/mock.rs b/pallets/swap/src/mock.rs index aacdf90835..3c5e424442 100644 --- a/pallets/swap/src/mock.rs +++ b/pallets/swap/src/mock.rs @@ -14,14 +14,18 @@ use sp_runtime::{ BuildStorage, Vec, traits::{BlakeTwo256, IdentityLookup}, }; -use substrate_fixed::types::U64F64; +use std::{cell::RefCell, collections::HashMap}; use subtensor_runtime_common::{ - AlphaCurrency, BalanceOps, Currency, CurrencyReserve, NetUid, SubnetInfo, TaoCurrency, + AlphaCurrency, + BalanceOps, + // Currency, + CurrencyReserve, + NetUid, + SubnetInfo, + TaoCurrency, }; use subtensor_swap_interface::Order; -use crate::pallet::{EnabledUserLiquidity, FeeGlobalAlpha, FeeGlobalTao}; - construct_runtime!( pub enum Test { System: frame_system = 0, @@ -82,16 +86,38 @@ impl system::Config for Test { parameter_types! { pub const SwapProtocolId: PalletId = PalletId(*b"ten/swap"); pub const MaxFeeRate: u16 = 10000; // 15.26% - pub const MaxPositions: u32 = 100; pub const MinimumLiquidity: u64 = 1_000; pub const MinimumReserves: NonZeroU64 = NonZeroU64::new(1).unwrap(); } +thread_local! { + // maps netuid -> mocked tao reserve + static MOCK_TAO_RESERVES: RefCell> = + RefCell::new(HashMap::new()); + // maps netuid -> mocked alpha reserve + static MOCK_ALPHA_RESERVES: RefCell> = + RefCell::new(HashMap::new()); +} + #[derive(Clone)] pub struct TaoReserve; +impl TaoReserve { + pub fn set_mock_reserve(netuid: NetUid, value: TaoCurrency) { + MOCK_TAO_RESERVES.with(|m| { + m.borrow_mut().insert(netuid, value); + }); + } +} + impl CurrencyReserve for TaoReserve { fn reserve(netuid: NetUid) -> TaoCurrency { + // If test has set an override, use it + if let Some(val) = MOCK_TAO_RESERVES.with(|m| m.borrow().get(&netuid).cloned()) { + return val; + } + + // Otherwise, fall back to our defaults match netuid.into() { 123u16 => 10_000, WRAPPING_FEES_NETUID => 100_000_000_000, @@ -107,8 +133,22 @@ impl CurrencyReserve for TaoReserve { #[derive(Clone)] pub struct AlphaReserve; +impl AlphaReserve { + pub fn set_mock_reserve(netuid: NetUid, value: AlphaCurrency) { + MOCK_ALPHA_RESERVES.with(|m| { + m.borrow_mut().insert(netuid, value); + }); + } +} + impl CurrencyReserve for AlphaReserve { fn reserve(netuid: NetUid) -> AlphaCurrency { + // If test has set an override, use it + if let Some(val) = MOCK_ALPHA_RESERVES.with(|m| m.borrow().get(&netuid).cloned()) { + return val; + } + + // Otherwise, fall back to our defaults match netuid.into() { 123u16 => 10_000.into(), WRAPPING_FEES_NETUID => 400_000_000_000.into(), @@ -123,22 +163,7 @@ impl CurrencyReserve for AlphaReserve { pub type GetAlphaForTao = subtensor_swap_interface::GetAlphaForTao; pub type GetTaoForAlpha = subtensor_swap_interface::GetTaoForAlpha; -pub(crate) trait GlobalFeeInfo: Currency { - fn global_fee(&self, netuid: NetUid) -> U64F64; -} - -impl GlobalFeeInfo for TaoCurrency { - fn global_fee(&self, netuid: NetUid) -> U64F64 { - FeeGlobalTao::::get(netuid) - } -} - -impl GlobalFeeInfo for AlphaCurrency { - fn global_fee(&self, netuid: NetUid) -> U64F64 { - FeeGlobalAlpha::::get(netuid) - } -} - +#[allow(dead_code)] pub(crate) trait TestExt { fn approx_expected_swap_output( sqrt_current_price: f64, @@ -277,7 +302,6 @@ impl crate::pallet::Config for Test { type BalanceOps = MockBalanceOps; type ProtocolId = SwapProtocolId; type MaxFeeRate = MaxFeeRate; - type MaxPositions = MaxPositions; type MinimumLiquidity = MinimumLiquidity; type MinimumReserve = MinimumReserves; type WeightInfo = (); @@ -292,12 +316,6 @@ pub fn new_test_ext() -> sp_io::TestExternalities { let mut ext = sp_io::TestExternalities::new(storage); ext.execute_with(|| { System::set_block_number(1); - - for netuid in 0u16..=100 { - // enable V3 for this range of netuids - EnabledUserLiquidity::::set(NetUid::from(netuid), true); - } - EnabledUserLiquidity::::set(NetUid::from(WRAPPING_FEES_NETUID), true); }); ext } diff --git a/pallets/swap/src/pallet/balancer.rs b/pallets/swap/src/pallet/balancer.rs new file mode 100644 index 0000000000..1e1386bd41 --- /dev/null +++ b/pallets/swap/src/pallet/balancer.rs @@ -0,0 +1,1095 @@ +// Balancer swap +// +// Unlike uniswap v2 or v3, it allows adding liquidity disproportionally to price. This is +// achieved by introducing the weights w1 and w2 so that w1 + w2 = 1. In these formulas x +// means base currency (alpha) and y means quote currency (tao). The w1 weight in the code +// below is referred as weight_base, and w2 as weight_quote. Because of the w1 + w2 = 1 +// constraint, only weight_quote is stored, and weight_base is always calculated. +// +// The formulas used for pool operation are following: +// +// Price: p = (w1*y) / (w2*x) +// +// Reserve deltas / (or -1 * payouts) in swaps are computed by: +// +// if ∆x is given (sell) ∆y = y * ((x / (x+∆x))^(w1/w2) - 1) +// if ∆y is given (buy) ∆x = x * ((y / (y+∆y))^(w2/w1) - 1) +// +// When swaps are executing the orders with slippage control, we need to know what amount +// we can swap before the price reaches the limit value of p': +// +// If p' < p (sell): ∆x = x * ((p / p')^w2 - 1) +// If p' < p (buy): ∆y = y * ((p' / p)^w1 - 1) +// +// In order to initialize weights with existing reserve values and price: +// +// w1 = px / (px + y) +// w2 = y / (px + y) +// +// Weights are adjusted when some amounts are added to the reserves. This prevents price +// from changing. +// +// new_w1 = p * (x + ∆x) / (p * (x + ∆x) + y + ∆y) +// new_w2 = (y + ∆y) / (p * (x + ∆x) + y + ∆y) +// +// Weights are limited to stay within [0.1, 0.9] range to avoid precision issues in exponentiation. +// Practically, these limitations will not be achieved, but if they are, the swap will not allow injection +// that will push the weights out of this interval because we prefer chain and swap stability over success +// of a single injection. Currently, we only allow the protocol to inject disproportionally to price, and +// the amount of disproportion will not cause weigths to get far from 0.5. +// + +use codec::{Decode, Encode, MaxEncodedLen}; +use frame_support::pallet_prelude::*; +use safe_bigmath::*; +use safe_math::*; +use sp_arithmetic::Perquintill; +use sp_core::U256; +use sp_runtime::Saturating; +use sp_std::ops::Neg; +use substrate_fixed::types::U64F64; +use subtensor_macros::freeze_struct; + +/// Balancer implements all high complexity math for swap operations such as: +/// - Swapping x for y, which includes limit orders +/// - Adding and removing liquidity (including unbalanced) +/// +/// Notation used in this file: +/// - x: Base reserve (alplha reserve) +/// - y: Quote reserve (tao reserve) +/// - ∆x: Alpha paid in/out +/// - ∆y: Tao paid in/out +/// - w1: Base weight (a.k.a weight_base) +/// - w2: Quote weight (a.k.a weight_quote) +#[freeze_struct("33a4fb0774da77c7")] +#[derive(Clone, Encode, Decode, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)] +pub struct Balancer { + quote: Perquintill, +} + +/// Accuracy matches to 18 decimal digits used to represent weights +pub const ACCURACY: u64 = 1_000_000_000_000_000_000_u64; +/// Lower imit of weights is 0.01 +pub const MIN_WEIGHT: Perquintill = Perquintill::from_parts(ACCURACY / 100); +/// 1.0 in Perquintill +pub const ONE: Perquintill = Perquintill::from_parts(ACCURACY); + +#[derive(Debug)] +pub enum BalancerError { + /// The provided weight value is out of range + InvalidValue, +} + +impl Default for Balancer { + /// The default value of weights is 0.5 for pool initialization + fn default() -> Self { + Self { + quote: Perquintill::from_rational(1u128, 2u128), + } + } +} + +impl Balancer { + /// Creates a new instance of balancer with a given quote weight + pub fn new(quote: Perquintill) -> Result { + if Self::check_constraints(quote) { + Ok(Balancer { quote }) + } else { + Err(BalancerError::InvalidValue) + } + } + + /// Constraints limit balancer weights within certain range of values: + /// - Both weights are above minimum + /// - Sum of weights is equal to 1.0 + fn check_constraints(quote: Perquintill) -> bool { + let base = ONE.saturating_sub(quote); + (base >= MIN_WEIGHT) && (quote >= MIN_WEIGHT) + } + + /// We store quote weight as Perquintill + pub fn get_quote_weight(&self) -> Perquintill { + self.quote + } + + /// Base weight is calculated as 1.0 - quote_weight + pub fn get_base_weight(&self) -> Perquintill { + ONE.saturating_sub(self.quote) + } + + /// Sets quote currency weight in the balancer. + /// Because sum of weights is always 1.0, there is no need to + /// store base currency weight + pub fn set_quote_weight(&mut self, new_value: Perquintill) -> Result<(), BalancerError> { + if Self::check_constraints(new_value) { + self.quote = new_value; + Ok(()) + } else { + Err(BalancerError::InvalidValue) + } + } + + /// If base_quote is true, calculate (x / (x + ∆x))^(weight_base / weight_quote), + /// otherwise, calculate (x / (x + ∆x))^(weight_quote / weight_base) + /// + /// Here we use SafeInt from bigmath crate for high-precision exponentiation, + /// which exposes the function pow_ratio_scaled. + /// + /// Note: ∆x may be negative + fn exp_scaled(&self, x: u64, dx: i128, base_quote: bool) -> U64F64 { + let x_plus_dx = if dx >= 0 { + x.saturating_add(dx as u64) + } else { + x.saturating_sub(dx.neg() as u64) + }; + + if x_plus_dx == 0 { + return U64F64::saturating_from_num(0); + } + let w1: u128 = self.get_base_weight().deconstruct() as u128; + let w2: u128 = self.get_quote_weight().deconstruct() as u128; + + let precision = 1024; + let x_safe = SafeInt::from(x); + let w1_safe = SafeInt::from(w1); + let w2_safe = SafeInt::from(w2); + let perquintill_scale = SafeInt::from(ACCURACY as u128); + let denominator = SafeInt::from(x_plus_dx); + log::debug!("x = {:?}", x); + log::debug!("dx = {:?}", dx); + log::debug!("x_safe = {:?}", x_safe); + log::debug!("denominator = {:?}", denominator); + log::debug!("w1_safe = {:?}", w1_safe); + log::debug!("w2_safe = {:?}", w2_safe); + log::debug!("precision = {:?}", precision); + log::debug!("perquintill_scale = {:?}", perquintill_scale); + + let maybe_result_safe_int = if base_quote { + SafeInt::pow_ratio_scaled( + &x_safe, + &denominator, + &w1_safe, + &w2_safe, + precision, + &perquintill_scale, + ) + } else { + SafeInt::pow_ratio_scaled( + &x_safe, + &denominator, + &w2_safe, + &w1_safe, + precision, + &perquintill_scale, + ) + }; + + if let Some(result_safe_int) = maybe_result_safe_int + && let Some(result_u64) = result_safe_int.to_u64() + { + return U64F64::saturating_from_num(result_u64) + .safe_div(U64F64::saturating_from_num(ACCURACY)); + } + U64F64::saturating_from_num(0) + } + + /// Calculates exponent of (x / (x + ∆x)) ^ (w_base/w_quote) + /// This method is used in sell swaps + /// (∆x is given by user, ∆y is paid out by the pool) + pub fn exp_base_quote(&self, x: u64, dx: u64) -> U64F64 { + self.exp_scaled(x, dx as i128, true) + } + + /// Calculates exponent of (y / (y + ∆y)) ^ (w_quote/w_base) + /// This method is used in buy swaps + /// (∆y is given by user, ∆x is paid out by the pool) + pub fn exp_quote_base(&self, y: u64, dy: u64) -> U64F64 { + self.exp_scaled(y, dy as i128, false) + } + + /// Calculates price as (w1/w2) * (y/x), where + /// - w1 is base weight + /// - w2 is quote weight + /// - x is base reserve + /// - y is quote reserve + pub fn calculate_price(&self, x: u64, y: u64) -> U64F64 { + let w2_fixed = U64F64::saturating_from_num(self.get_quote_weight().deconstruct()); + let w1_fixed = U64F64::saturating_from_num(self.get_base_weight().deconstruct()); + let x_fixed = U64F64::saturating_from_num(x); + let y_fixed = U64F64::saturating_from_num(y); + w1_fixed + .safe_div(w2_fixed) + .saturating_mul(y_fixed.safe_div(x_fixed)) + } + + /// Multiply a u128 value by a Perquintill with u128 result rounded to the + /// nearest integer + fn mul_perquintill_round(p: Perquintill, value: u128) -> u128 { + let parts = p.deconstruct() as u128; + let acc = ACCURACY as u128; + + let num = U256::from(value).saturating_mul(U256::from(parts)); + let den = U256::from(acc); + + // Add 0.5 before integer division to achieve rounding to the nearest + // integer + let zero = U256::from(0); + let res = num + .saturating_add(den.checked_div(U256::from(2u8)).unwrap_or(zero)) + .checked_div(den) + .unwrap_or(zero); + res.min(U256::from(u128::MAX)) + .try_into() + .unwrap_or_default() + } + + /// When liquidity is added to balancer swap, it may be added with arbitrary proportion, + /// not necessarily in the proportion of price, like with uniswap v2 or v3. In order to + /// stay within balancer pool invariant, the weights need to be updated. Invariant: + /// + /// L = x ^ weight_base * y ^ weight_quote + /// + /// Note that weights must remain within the proper range (both be above MIN_WEIGHT), + /// so only reasonably small disproportions of updates are appropriate. + pub fn update_weights_for_added_liquidity( + &mut self, + tao_reserve: u64, + alpha_reserve: u64, + tao_delta: u64, + alpha_delta: u64, + ) -> Result<(), BalancerError> { + // Calculate new to-be reserves (do not update here) + let tao_reserve_u128 = u64::from(tao_reserve) as u128; + let alpha_reserve_u128 = u64::from(alpha_reserve) as u128; + let tao_delta_u128 = u64::from(tao_delta) as u128; + let alpha_delta_u128 = u64::from(alpha_delta) as u128; + let new_tao_reserve_u128 = tao_reserve_u128.saturating_add(tao_delta_u128); + let new_alpha_reserve_u128 = alpha_reserve_u128.saturating_add(alpha_delta_u128); + + // Calculate new weights + let quantity_1: u128 = Self::mul_perquintill_round( + self.get_base_weight(), + tao_reserve_u128.saturating_mul(new_alpha_reserve_u128), + ); + let quantity_2: u128 = Self::mul_perquintill_round( + self.get_quote_weight(), + alpha_reserve_u128.saturating_mul(new_tao_reserve_u128), + ); + let q_sum = quantity_1.saturating_add(quantity_2); + + // Calculate new reserve weights + let new_reserve_weight = if q_sum != 0 { + // Both TAO and Alpha are non-zero, normal case + Perquintill::from_rational(quantity_2, q_sum) + } else { + // Either TAO or Alpha reserve were and/or remain zero => Initialize weights to 0.5 + Perquintill::from_rational(1u128, 2u128) + }; + + self.set_quote_weight(new_reserve_weight) + } + + /// Calculates quote delta needed to reach the price up when byuing + /// This method is needed for limit orders. + /// + /// Formula is: + /// ∆y = y * ((price_new / price)^weight_base - 1) + /// price_new >= price + pub fn calculate_quote_delta_in( + &self, + current_price: U64F64, + target_price: U64F64, + reserve: u64, + ) -> u64 { + let base_numerator: u128 = target_price.to_bits(); + let base_denominator: u128 = current_price.to_bits(); + let w1_fixed: u128 = self.get_base_weight().deconstruct() as u128; + let scale: u128 = 10u128.pow(18); + + let maybe_exp_result = SafeInt::pow_ratio_scaled( + &SafeInt::from(base_numerator), + &SafeInt::from(base_denominator), + &SafeInt::from(w1_fixed), + &SafeInt::from(ACCURACY), + 1024, + &SafeInt::from(scale), + ); + + if let Some(exp_result_safe_int) = maybe_exp_result { + let reserve_fixed = U64F64::saturating_from_num(reserve); + let one = U64F64::saturating_from_num(1); + let scale_fixed = U64F64::saturating_from_num(scale); + let exp_result_fixed = if let Some(exp_result_u64) = exp_result_safe_int.to_u64() { + U64F64::saturating_from_num(exp_result_u64) + } else if u64::MAX < exp_result_safe_int { + U64F64::saturating_from_num(u64::MAX) + } else { + U64F64::saturating_from_num(0) + }; + reserve_fixed + .saturating_mul(exp_result_fixed.safe_div(scale_fixed).saturating_sub(one)) + .saturating_to_num::() + } else { + 0u64 + } + } + + /// Calculates base delta needed to reach the price down when selling + /// This method is needed for limit orders. + /// + /// Formula is: + /// ∆x = x * ((price / price_new)^weight_quote - 1) + /// price_new <= price + pub fn calculate_base_delta_in( + &self, + current_price: U64F64, + target_price: U64F64, + reserve: u64, + ) -> u64 { + let base_numerator: u128 = current_price.to_bits(); + let base_denominator: u128 = target_price.to_bits(); + let w2_fixed: u128 = self.get_quote_weight().deconstruct() as u128; + let scale: u128 = 10u128.pow(18); + + let maybe_exp_result = SafeInt::pow_ratio_scaled( + &SafeInt::from(base_numerator), + &SafeInt::from(base_denominator), + &SafeInt::from(w2_fixed), + &SafeInt::from(ACCURACY), + 1024, + &SafeInt::from(scale), + ); + + if let Some(exp_result_safe_int) = maybe_exp_result { + let one = U64F64::saturating_from_num(1); + let scale_fixed = U64F64::saturating_from_num(scale); + let reserve_fixed = U64F64::saturating_from_num(reserve); + let exp_result_fixed = if let Some(exp_result_u64) = exp_result_safe_int.to_u64() { + U64F64::saturating_from_num(exp_result_u64) + } else if u64::MAX < exp_result_safe_int { + U64F64::saturating_from_num(u64::MAX) + } else { + U64F64::saturating_from_num(0) + }; + reserve_fixed + .saturating_mul(exp_result_fixed.safe_div(scale_fixed).saturating_sub(one)) + .saturating_to_num::() + } else { + 0u64 + } + } + + /// Calculates amount of Alpha that needs to be sold to get a given amount of TAO + pub fn get_base_needed_for_quote( + &self, + tao_reserve: u64, + alpha_reserve: u64, + delta_tao: u64, + ) -> u64 { + let e = self.exp_scaled(tao_reserve, (delta_tao as i128).neg(), false); + let one = U64F64::from_num(1); + let alpha_reserve_fixed = U64F64::from_num(alpha_reserve); + // e > 1 in this case + alpha_reserve_fixed + .saturating_mul(e.saturating_sub(one)) + .saturating_to_num::() + } +} + +// cargo test --package pallet-subtensor-swap --lib -- pallet::balancer::tests --nocapture +#[cfg(test)] +#[allow(clippy::expect_used, clippy::unwrap_used)] +#[cfg(feature = "std")] +mod tests { + use crate::pallet::Balancer; + use crate::pallet::balancer::*; + use approx::assert_abs_diff_eq; + use sp_arithmetic::Perquintill; + + // Helper: convert Perquintill to f64 for comparison + fn perquintill_to_f64(p: Perquintill) -> f64 { + let parts = p.deconstruct() as f64; + parts / ACCURACY as f64 + } + + // Helper: convert U64F64 to f64 for comparison + fn f(v: U64F64) -> f64 { + v.to_num::() + } + + #[test] + fn test_perquintill_power() { + const PRECISION: u32 = 4096; + const PERQUINTILL: u128 = ACCURACY as u128; + + let x = SafeInt::from(21_000_000_000_000_000u64); + let delta = SafeInt::from(7_000_000_000_000_000u64); + let w1 = SafeInt::from(600_000_000_000_000_000u128); + let w2 = SafeInt::from(400_000_000_000_000_000u128); + let denominator = &x + δ + assert_eq!(w1.clone() + w2.clone(), SafeInt::from(PERQUINTILL)); + + let perquintill_result = SafeInt::pow_ratio_scaled( + &x, + &denominator, + &w1, + &w2, + PRECISION, + &SafeInt::from(PERQUINTILL), + ) + .expect("perquintill integer result"); + + assert_eq!( + perquintill_result, + SafeInt::from(649_519_052_838_328_985u128) + ); + let readable = safe_bigmath::SafeDec::<18>::from_raw(perquintill_result); + assert_eq!(format!("{}", readable), "0.649519052838328985"); + } + + /// Validate realistic values that can be calculated with f64 precision + #[test] + fn test_exp_base_quote_happy_path() { + // Outer test cases: w_quote + [ + Perquintill::from_rational(500_000_000_000_u128, 1_000_000_000_000_u128), + Perquintill::from_rational(500_000_000_001_u128, 1_000_000_000_000_u128), + Perquintill::from_rational(499_999_999_999_u128, 1_000_000_000_000_u128), + Perquintill::from_rational(500_000_000_100_u128, 1_000_000_000_000_u128), + Perquintill::from_rational(500_000_001_000_u128, 1_000_000_000_000_u128), + Perquintill::from_rational(500_000_010_000_u128, 1_000_000_000_000_u128), + Perquintill::from_rational(500_000_100_000_u128, 1_000_000_000_000_u128), + Perquintill::from_rational(500_001_000_000_u128, 1_000_000_000_000_u128), + Perquintill::from_rational(500_010_000_000_u128, 1_000_000_000_000_u128), + Perquintill::from_rational(500_100_000_000_u128, 1_000_000_000_000_u128), + Perquintill::from_rational(501_000_000_000_u128, 1_000_000_000_000_u128), + Perquintill::from_rational(510_000_000_000_u128, 1_000_000_000_000_u128), + Perquintill::from_rational(100_000_000_000_u128, 1_000_000_000_000_u128), + Perquintill::from_rational(100_000_000_001_u128, 1_000_000_000_000_u128), + Perquintill::from_rational(200_000_000_000_u128, 1_000_000_000_000_u128), + Perquintill::from_rational(300_000_000_000_u128, 1_000_000_000_000_u128), + Perquintill::from_rational(400_000_000_000_u128, 1_000_000_000_000_u128), + Perquintill::from_rational(600_000_000_000_u128, 1_000_000_000_000_u128), + Perquintill::from_rational(700_000_000_000_u128, 1_000_000_000_000_u128), + Perquintill::from_rational(800_000_000_000_u128, 1_000_000_000_000_u128), + Perquintill::from_rational(899_999_999_999_u128, 1_000_000_000_000_u128), + Perquintill::from_rational(900_000_000_000_u128, 1_000_000_000_000_u128), + Perquintill::from_rational( + 102_337_248_363_782_924_u128, + 1_000_000_000_000_000_000_u128, + ), + ] + .into_iter() + .for_each(|w_quote| { + // Inner test cases: y, x, ∆x + [ + (1_000_u64, 1_000_u64, 0_u64), + (1_000_u64, 1_000_u64, 1_u64), + (1_500_u64, 1_000_u64, 1_u64), + ( + 1_000_000_000_000_u64, + 100_000_000_000_000_u64, + 100_000_000_u64, + ), + ( + 1_000_000_000_000_u64, + 100_000_000_000_000_u64, + 100_000_000_u64, + ), + ( + 100_000_000_000_u64, + 100_000_000_000_000_u64, + 100_000_000_u64, + ), + (100_000_000_000_u64, 100_000_000_000_000_u64, 1_000_000_u64), + ( + 100_000_000_000_u64, + 100_000_000_000_000_u64, + 1_000_000_000_000_u64, + ), + ( + 1_000_000_000_u64, + 100_000_000_000_000_u64, + 1_000_000_000_000_u64, + ), + ( + 1_000_000_u64, + 100_000_000_000_000_u64, + 1_000_000_000_000_u64, + ), + (1_000_u64, 100_000_000_000_000_u64, 1_000_000_000_000_u64), + (1_000_u64, 100_000_000_000_000_u64, 1_000_000_000_u64), + (1_000_u64, 100_000_000_000_000_u64, 1_000_000_u64), + (1_000_u64, 100_000_000_000_000_u64, 1_000_u64), + (1_000_u64, 100_000_000_000_000_u64, 100_000_000_000_000_u64), + (10_u64, 100_000_000_000_000_u64, 100_000_000_000_000_u64), + // Extreme values of ∆x for small x + (1_000_000_000_u64, 4_000_000_000_u64, 1_000_000_000_000_u64), + (1_000_000_000_000_u64, 1_000_u64, 1_000_000_000_000_u64), + ( + 5_628_038_062_729_553_u64, + 400_775_553_u64, + 14_446_633_907_665_582_u64, + ), + ( + 5_600_000_000_000_000_u64, + 400_000_000_u64, + 14_000_000_000_000_000_u64, + ), + ] + .into_iter() + .for_each(|(y, x, dx)| { + let bal = Balancer::new(w_quote).unwrap(); + let e1 = bal.exp_base_quote(x, dx); + let e2 = bal.exp_quote_base(x, dx); + let one = U64F64::from_num(1); + let y_fixed = U64F64::from_num(y); + let dy1 = y_fixed * (one - e1); + let dy2 = y_fixed * (one - e2); + + let w1 = perquintill_to_f64(bal.get_base_weight()); + let w2 = perquintill_to_f64(bal.get_quote_weight()); + let e1_expected = (x as f64 / (x as f64 + dx as f64)).powf(w1 / w2); + let dy1_expected = y as f64 * (1. - e1_expected); + let e2_expected = (x as f64 / (x as f64 + dx as f64)).powf(w2 / w1); + let dy2_expected = y as f64 * (1. - e2_expected); + + // Start tolerance with 0.001 rao + let mut eps1 = 0.001; + let mut eps2 = 0.001; + + // If swapping more than 100k tao/alpha, relax tolerance to 1.0 rao + if dy1_expected > 100_000_000_000_000_f64 { + eps1 = 1.0; + } + if dy2_expected > 100_000_000_000_000_f64 { + eps2 = 1.0; + } + assert_abs_diff_eq!(f(dy1), dy1_expected, epsilon = eps1); + assert_abs_diff_eq!(f(dy2), dy2_expected, epsilon = eps2); + }) + }); + } + + /// This test exercises practical application edge cases of exp_base_quote + /// The practical formula where this function is used: + /// ∆y = y * (exp_base_quote(x, ∆x) - 1) + /// + /// The test validates that two different sets of parameters produce (sensibly) + /// different results + /// + #[test] + fn test_exp_base_quote_dy_precision() { + // Test cases: y, x1, ∆x1, w_quote1, x2, ∆x2, w_quote2 + // Realized dy1 should be greater than dy2 + [ + ( + 1_000_000_000_u64, + 21_000_000_000_000_000_u64, + 21_000_000_000_u64, + Perquintill::from_rational(1_000_000_000_000_u128, 2_000_000_000_000_u128), + 21_000_000_000_000_000_u64, + 21_000_000_000_u64, + Perquintill::from_rational(1_000_000_000_001_u128, 2_000_000_000_000_u128), + ), + ( + 1_000_000_000_u64, + 21_000_000_000_000_000_u64, + 21_000_000_000_u64, + Perquintill::from_rational(1_000_000_000_000_u128, 2_000_000_000_001_u128), + 21_000_000_000_000_000_u64, + 21_000_000_000_u64, + Perquintill::from_rational(1_000_000_000_000_u128, 2_000_000_000_000_u128), + ), + ( + 1_000_000_000_u64, + 21_000_000_000_000_000_u64, + 2_u64, + Perquintill::from_rational(1_000_000_000_000_u128, 2_000_000_000_000_u128), + 21_000_000_000_000_000_u64, + 1_u64, + Perquintill::from_rational(1_000_000_000_000_u128, 2_000_000_000_000_u128), + ), + ( + 1_000_000_000_u64, + 21_000_000_000_000_000_u64, + 1_u64, + Perquintill::from_rational(1_000_000_000_000_u128, 2_000_000_000_000_u128), + 21_000_000_000_000_000_u64, + 1_u64, + Perquintill::from_rational(1_010_000_000_000_u128, 2_000_000_000_000_u128), + ), + ( + 1_000_000_000_u64, + 21_000_000_000_000_000_u64, + 1_u64, + Perquintill::from_rational(1_000_000_000_000_u128, 2_010_000_000_000_u128), + 21_000_000_000_000_000_u64, + 1_u64, + Perquintill::from_rational(1_000_000_000_000_u128, 2_000_000_000_000_u128), + ), + ] + .into_iter() + .for_each(|(y, x1, dx1, w_quote1, x2, dx2, w_quote2)| { + let bal1 = Balancer::new(w_quote1).unwrap(); + let bal2 = Balancer::new(w_quote2).unwrap(); + + let exp1 = bal1.exp_base_quote(x1, dx1); + let exp2 = bal2.exp_base_quote(x2, dx2); + + let one = U64F64::from_num(1); + let y_fixed = U64F64::from_num(y); + let dy1 = y_fixed * (one - exp1); + let dy2 = y_fixed * (one - exp2); + + assert!(dy1 > dy2); + + let zero = U64F64::from_num(0); + assert!(dy1 != zero); + assert!(dy2 != zero); + }) + } + + /// Test the broad range of w_quote values, usually should be ignored + #[ignore] + #[test] + fn test_exp_quote_broad_range() { + let y = 1_000_000_000_000_u64; + let x = 100_000_000_000_000_u64; + let dx = 10_000_000_u64; + + let mut prev = U64F64::from_num(1_000_000_000); + let mut last_progress = 0.; + let start = 100_000_000_000_u128; + let stop = 900_000_000_000_u128; + for num in (start..=stop).step_by(1000_usize) { + let w_quote = Perquintill::from_rational(num, 1_000_000_000_000_u128); + let bal = Balancer::new(w_quote).unwrap(); + let e = bal.exp_base_quote(x, dx); + + let one = U64F64::from_num(1); + let dy = U64F64::from_num(y) * (one - e); + + let progress = (num as f64 - start as f64) / (stop as f64 - start as f64); + if progress - last_progress >= 0.0001 { + // Replace with println for real-time progress + log::debug!("progress = {:?}%", progress * 100.); + log::debug!("dy = {:?}", dy); + last_progress = progress; + } + + assert!(dy != U64F64::from_num(0)); + assert!(dy <= prev); + prev = dy; + } + } + + #[ignore] + #[test] + fn test_exp_quote_fuzzy() { + use rand::rngs::StdRng; + use rand::{Rng, SeedableRng}; + use rayon::prelude::*; + use std::sync::Arc; + use std::sync::atomic::{AtomicUsize, Ordering}; + + const ITERATIONS: usize = 1_000_000_000; + let counter = Arc::new(AtomicUsize::new(0)); + + (0..ITERATIONS) + .into_par_iter() + .for_each(|i| { + // Each iteration gets its own deterministic RNG. + // Seed depends on i, so runs are reproducible. + let mut rng = StdRng::seed_from_u64(42 + i as u64); + let max_supply: u64 = 21_000_000_000_000_000; + let full_range = true; + + let x: u64 = rng.gen_range(1_000..=max_supply); // Alpha reserve + let y: u64 = if full_range { + // TAO reserve (allow huge prices) + rng.gen_range(1_000..=max_supply) + } else { + // TAO reserve (limit prices with 0-1000) + rng.gen_range(1_000..x.saturating_mul(1000).min(max_supply)) + }; + let dx: u64 = if full_range { + // Alhpa sold (allow huge values) + rng.gen_range(1_000..=21_000_000_000_000_000) + } else { + // Alhpa sold (do not sell more than 100% of what's in alpha reserve) + rng.gen_range(1_000..=x) + }; + let w_numerator: u64 = rng.gen_range(ACCURACY / 10..=ACCURACY / 10 * 9); + let w_quote = Perquintill::from_rational(w_numerator, ACCURACY); + + let bal = Balancer::new(w_quote).unwrap(); + let e = bal.exp_base_quote(x, dx); + + let one = U64F64::from_num(1); + let dy = U64F64::from_num(y) * (one - e); + + // Calculate expected in f64 and approx-assert + let w1 = perquintill_to_f64(bal.get_base_weight()); + let w2 = perquintill_to_f64(bal.get_quote_weight()); + let e_expected = (x as f64 / (x as f64 + dx as f64)).powf(w1 / w2); + let dy_expected = y as f64 * (1. - e_expected); + + let actual = dy.to_num::(); + let eps = (dy_expected / 1_000_000.).clamp(1.0, 1000.0); + + assert!( + (actual - dy_expected).abs() <= eps, + "dy mismatch:\n actual: {}\n expected: {}\n eps: {}\nParameters:\n x: {}\n y: {}\n dx: {}\n w_numerator: {}\n", + actual, dy_expected, eps, x, y, dx, w_numerator, + ); + + // Assert that we aren't giving out more than reserve y + assert!(dy <= y, "dy = {},\ny = {}", dy, y,); + + // Print progress + let done = counter.fetch_add(1, Ordering::Relaxed) + 1; + if done % 100_000_000 == 0 { + let progress = done as f64 / ITERATIONS as f64 * 100.0; + // Replace with println for real-time progress + log::debug!("progress = {progress:.4}%"); + } + }); + } + + #[test] + fn test_calculate_quote_delta_in() { + let num = 250_000_000_000_u128; // w1 = 0.75 + let w_quote = Perquintill::from_rational(num, 1_000_000_000_000_u128); + let bal = Balancer::new(w_quote).unwrap(); + + let current_price: U64F64 = U64F64::from_num(0.1); + let target_price: U64F64 = U64F64::from_num(0.2); + let tao_reserve: u64 = 1_000_000_000; + + let dy = bal.calculate_quote_delta_in(current_price, target_price, tao_reserve); + + // ∆y = y•[(p'/p)^w1 - 1] + let dy_expected = tao_reserve as f64 + * ((target_price.to_num::() / current_price.to_num::()).powf(0.75) - 1.0); + + assert_eq!(dy, dy_expected as u64,); + } + + #[test] + fn test_calculate_base_delta_in() { + let num = 250_000_000_000_u128; // w2 = 0.25 + let w_quote = Perquintill::from_rational(num, 1_000_000_000_000_u128); + let bal = Balancer::new(w_quote).unwrap(); + + let current_price: U64F64 = U64F64::from_num(0.2); + let target_price: U64F64 = U64F64::from_num(0.1); + let alpha_reserve: u64 = 1_000_000_000; + + let dx = bal.calculate_base_delta_in(current_price, target_price, alpha_reserve); + + // ∆x = x•[(p/p')^w2 - 1] + let dx_expected = alpha_reserve as f64 + * ((current_price.to_num::() / target_price.to_num::()).powf(0.25) - 1.0); + + assert_eq!(dx, dx_expected as u64,); + } + + #[test] + fn test_calculate_quote_delta_in_impossible() { + let num = 250_000_000_000_u128; // w1 = 0.75 + let w_quote = Perquintill::from_rational(num, 1_000_000_000_000_u128); + let bal = Balancer::new(w_quote).unwrap(); + + // Impossible price (lower) + let current_price: U64F64 = U64F64::from_num(0.1); + let target_price: U64F64 = U64F64::from_num(0.05); + let tao_reserve: u64 = 1_000_000_000; + + let dy = bal.calculate_quote_delta_in(current_price, target_price, tao_reserve); + let dy_expected = 0u64; + + assert_eq!(dy, dy_expected); + } + + #[test] + fn test_calculate_base_delta_in_impossible() { + let num = 250_000_000_000_u128; // w2 = 0.25 + let w_quote = Perquintill::from_rational(num, 1_000_000_000_000_u128); + let bal = Balancer::new(w_quote).unwrap(); + + // Impossible price (higher) + let current_price: U64F64 = U64F64::from_num(0.1); + let target_price: U64F64 = U64F64::from_num(0.2); + let alpha_reserve: u64 = 1_000_000_000; + + let dx = bal.calculate_base_delta_in(current_price, target_price, alpha_reserve); + let dx_expected = 0u64; + + assert_eq!(dx, dx_expected); + } + + #[test] + fn test_calculate_delta_in_reverse_swap() { + let num = 500_000_000_000_u128; + let w_quote = Perquintill::from_rational(num, 1_000_000_000_000_u128); + let bal = Balancer::new(w_quote).unwrap(); + + let current_price: U64F64 = U64F64::from_num(0.1); + let target_price: U64F64 = U64F64::from_num(0.2); + let tao_reserve: u64 = 1_000_000_000; + + // Here is the simple case of w1 = w2 = 0.5, so alpha = tao / price + let alpha_reserve: u64 = (tao_reserve as f64 / current_price.to_num::()) as u64; + + let dy = bal.calculate_quote_delta_in(current_price, target_price, tao_reserve); + let dx = alpha_reserve as f64 + * (1.0 + - (tao_reserve as f64 / (tao_reserve as f64 + dy as f64)) + .powf(num as f64 / (1_000_000_000_000 - num) as f64)); + + // Verify that buying with dy will in fact bring the price to target_price + let actual_price = bal.calculate_price(alpha_reserve - dx as u64, tao_reserve + dy); + assert_abs_diff_eq!( + actual_price.to_num::(), + target_price.to_num::(), + epsilon = target_price.to_num::() / 1_000_000_000. + ); + } + + #[test] + fn test_mul_round_zero_and_one() { + let v = 1_000_000u128; + + // p = 0 -> always 0 + assert_eq!(Balancer::mul_perquintill_round(Perquintill::zero(), v), 0); + + // p = 1 -> identity + assert_eq!(Balancer::mul_perquintill_round(Perquintill::one(), v), v); + } + + #[test] + fn test_mul_round_half_behaviour() { + // p = 1/2 + let p = Perquintill::from_rational(1u128, 2u128); + + // Check rounding around .5 boundaries + // value * 1/2, rounded to nearest + assert_eq!(Balancer::mul_perquintill_round(p, 0), 0); // 0.0 -> 0 + assert_eq!(Balancer::mul_perquintill_round(p, 1), 1); // 0.5 -> 1 (round up) + assert_eq!(Balancer::mul_perquintill_round(p, 2), 1); // 1.0 -> 1 + assert_eq!(Balancer::mul_perquintill_round(p, 3), 2); // 1.5 -> 2 + assert_eq!(Balancer::mul_perquintill_round(p, 4), 2); // 2.0 -> 2 + assert_eq!(Balancer::mul_perquintill_round(p, 5), 3); // 2.5 -> 3 + assert_eq!(Balancer::mul_perquintill_round(p, 1023), 512); // 511.5 -> 512 + assert_eq!(Balancer::mul_perquintill_round(p, 1025), 513); // 512.5 -> 513 + } + + #[test] + fn test_mul_round_third_behaviour() { + // p = 1/3 + let p = Perquintill::from_rational(1u128, 3u128); + + // value * 1/3, rounded to nearest + assert_eq!(Balancer::mul_perquintill_round(p, 3), 1); // 1.0 -> 1 + assert_eq!(Balancer::mul_perquintill_round(p, 4), 1); // 1.333... -> 1 + assert_eq!(Balancer::mul_perquintill_round(p, 5), 2); // 1.666... -> 2 + assert_eq!(Balancer::mul_perquintill_round(p, 6), 2); // 2.0 -> 2 + } + + #[test] + fn test_mul_round_large_values_simple_rational() { + // p = 7/10 (exact in perquintill: 0.7) + let p = Perquintill::from_rational(7u128, 10u128); + let v: u128 = 1_000_000_000_000_000_000; + + let res = Balancer::mul_perquintill_round(p, v); + + // Expected = round(0.7 * v) with pure integer math: + // round(v * 7 / 10) = (v*7 + 10/2) / 10 + let expected = (v.saturating_mul(7) + 10 / 2) / 10; + + assert_eq!(res, expected); + } + + #[test] + fn test_mul_round_max_value_with_one() { + let v = u128::MAX; + let p = ONE; + + // For p = 1, result must be exactly value, and must not overflow + let res = Balancer::mul_perquintill_round(p, v); + assert_eq!(res, v); + } + + #[test] + fn test_price_with_equal_weights_is_y_over_x() { + // quote = 0.5, base = 0.5 -> w1 / w2 = 1, so price = y/x + let quote = Perquintill::from_rational(1u128, 2u128); + let bal = Balancer::new(quote).unwrap(); + + let x = 2u64; + let y = 5u64; + + let price = bal.calculate_price(x, y); + let price_f = f(price); + + let expected_f = (y as f64) / (x as f64); + assert_abs_diff_eq!(price_f, expected_f, epsilon = 1e-12); + } + + #[test] + fn test_price_scales_with_weight_ratio_two_to_one() { + // Assume base = 1 - quote. + // quote = 1/3 -> base = 2/3, so w1 / w2 = 2. + // Then price = 2 * (y/x). + let quote = Perquintill::from_rational(1u128, 3u128); + let bal = Balancer::new(quote).unwrap(); + + let x = 4u64; + let y = 10u64; + + let price_f = f(bal.calculate_price(x, y)); + let expected_f = 2.0 * (y as f64 / x as f64); + + assert_abs_diff_eq!(price_f, expected_f, epsilon = 1e-10); + } + + #[test] + fn test_price_is_zero_when_y_is_zero() { + // If y = 0, y/x = 0 so price must be 0 regardless of weights (for x > 0). + let quote = Perquintill::from_rational(3u128, 10u128); // 0.3 + let bal = Balancer::new(quote).unwrap(); + + let x = 10u64; + let y = 0u64; + + let price_f = f(bal.calculate_price(x, y)); + assert_abs_diff_eq!(price_f, 0.0, epsilon = 0.0); + } + + #[test] + fn test_price_invariant_when_scaling_x_and_y_with_equal_weights() { + // For equal weights, price(x, y) == price(kx, ky). + let quote = Perquintill::from_rational(1u128, 2u128); // 0.5 + let bal = Balancer::new(quote).unwrap(); + + let x1 = 3u64; + let y1 = 7u64; + let k = 10u64; + let x2 = x1 * k; + let y2 = y1 * k; + + let p1 = f(bal.calculate_price(x1, y1)); + let p2 = f(bal.calculate_price(x2, y2)); + + assert_abs_diff_eq!(p1, p2, epsilon = 1e-12); + } + + #[test] + fn test_price_matches_formula_for_general_quote() { + // General check: price = (w1 / w2) * (y/x), + // where w1 = base_weight, w2 = quote_weight. + // Here we assume get_base_weight = 1 - quote. + let quote = Perquintill::from_rational(2u128, 5u128); // 0.4 + let bal = Balancer::new(quote).unwrap(); + + let x = 9u64; + let y = 25u64; + + let price_f = f(bal.calculate_price(x, y)); + + let base = Perquintill::one() - quote; + let w1 = base.deconstruct() as f64; + let w2 = quote.deconstruct() as f64; + + let expected_f = (w1 / w2) * (y as f64 / x as f64); + assert_abs_diff_eq!(price_f, expected_f, epsilon = 1e-9); + } + + #[test] + fn test_price_high_values_non_equal_weights() { + // Non-equal weights, high x and y (up to 21e15) + let quote = Perquintill::from_rational(3u128, 10u128); // 0.3 + let bal = Balancer::new(quote).unwrap(); + + let x: u64 = 21_000_000_000_000_000; + let y: u64 = 15_000_000_000_000_000; + + let price = bal.calculate_price(x, y); + let price_f = f(price); + + // Expected: (w1 / w2) * (y / x), using Balancer's actual weights + let w1 = bal.get_base_weight().deconstruct() as f64; + let w2 = bal.get_quote_weight().deconstruct() as f64; + let expected_f = (w1 / w2) * (y as f64 / x as f64); + + assert_abs_diff_eq!(price_f, expected_f, epsilon = 1e-9); + } + + // cargo test --package pallet-subtensor-swap --lib -- pallet::balancer::tests::test_exp_scaled --exact --nocapture + #[test] + fn test_exp_scaled() { + [ + // base_weight_numerator, base_weight_denominator, reserve, d_reserve, base_quote + (5_u64, 10_u64, 100000_u64, 100_u64, true, 0.999000999000999), + (1_u64, 4_u64, 500000_u64, 5000_u64, true, 0.970590147927644), + (3_u64, 4_u64, 200000_u64, 2000_u64, false, 0.970590147927644), + ( + 9_u64, + 10_u64, + 13513642_u64, + 1673_u64, + false, + 0.998886481979889, + ), + ( + 773_u64, + 1000_u64, + 7_000_000_000_u64, + 10_000_u64, + true, + 0.999999580484586, + ), + ] + .into_iter() + .map(|v| { + ( + Perquintill::from_rational(v.0, v.1), + v.2, + v.3, + v.4, + U64F64::from_num(v.5), + ) + }) + .for_each(|(quote_weight, reserve, d_reserve, base_quote, expected)| { + let balancer = Balancer::new(quote_weight).unwrap(); + let result = balancer.exp_scaled(reserve, d_reserve as i128, base_quote); + assert_abs_diff_eq!( + result.to_num::(), + expected.to_num::(), + epsilon = 0.000000001 + ); + }); + } + + // cargo test --package pallet-subtensor-swap --lib -- pallet::balancer::tests::test_base_needed_for_quote --exact --nocapture + #[test] + fn test_base_needed_for_quote() { + let num = 250_000_000_000_u128; // w1 = 0.75 + let w_quote = Perquintill::from_rational(num, 1_000_000_000_000_u128); + let bal = Balancer::new(w_quote).unwrap(); + + let tao_reserve: u64 = 1_000_000_000; + let alpha_reserve: u64 = 1_000_000_000; + let tao_delta: u64 = 1_123_432; // typical fee range + + let dx = bal.get_base_needed_for_quote(tao_reserve, alpha_reserve, tao_delta); + + // ∆x = x•[(y/(y+∆y))^(w2/w1) - 1] + let dx_expected = tao_reserve as f64 + * ((tao_reserve as f64 / ((tao_reserve - tao_delta) as f64)).powf(0.25 / 0.75) - 1.0); + + assert_eq!(dx, dx_expected as u64,); + } +} diff --git a/pallets/swap/src/pallet/hooks.rs b/pallets/swap/src/pallet/hooks.rs new file mode 100644 index 0000000000..90989d5f52 --- /dev/null +++ b/pallets/swap/src/pallet/hooks.rs @@ -0,0 +1,30 @@ +use frame_support::pallet_macros::pallet_section; + +#[pallet_section] +mod hooks { + #[pallet::hooks] + impl Hooks> for Pallet { + fn on_initialize(block_number: BlockNumberFor) -> Weight { + Weight::from_parts(0, 0) + } + + fn on_finalize(_block_number: BlockNumberFor) {} + + fn on_runtime_upgrade() -> Weight { + // --- Migrate storage + let mut weight = Weight::from_parts(0, 0); + + weight = weight + // Cleanup uniswap v3 and migrate to balancer + .saturating_add( + migrations::migrate_swapv3_to_balancer::migrate_swapv3_to_balancer::(), + ); + weight + } + + #[cfg(feature = "try-runtime")] + fn try_state(_n: BlockNumberFor) -> Result<(), sp_runtime::TryRuntimeError> { + Ok(()) + } + } +} diff --git a/pallets/swap/src/pallet/impls.rs b/pallets/swap/src/pallet/impls.rs index 6ec02879bf..54efabb19a 100644 --- a/pallets/swap/src/pallet/impls.rs +++ b/pallets/swap/src/pallet/impls.rs @@ -1,215 +1,136 @@ -use core::ops::Neg; - use frame_support::storage::{TransactionOutcome, transactional}; use frame_support::{ensure, pallet_prelude::DispatchError, traits::Get}; use safe_math::*; -use sp_arithmetic::helpers_128bit; -use sp_runtime::{DispatchResult, Vec, traits::AccountIdConversion}; -use substrate_fixed::types::{I64F64, U64F64, U96F32}; +use sp_arithmetic::{ + //helpers_128bit, + Perquintill, +}; +use sp_runtime::{DispatchResult, traits::AccountIdConversion}; +use substrate_fixed::types::U64F64; use subtensor_runtime_common::{ - AlphaCurrency, BalanceOps, Currency, CurrencyReserve, NetUid, SubnetInfo, TaoCurrency, + AlphaCurrency, + // BalanceOps, + Currency, + CurrencyReserve, + NetUid, + SubnetInfo, + TaoCurrency, }; use subtensor_swap_interface::{ DefaultPriceLimit, Order as OrderT, SwapEngine, SwapHandler, SwapResult, }; use super::pallet::*; -use super::swap_step::{BasicSwapStep, SwapStep, SwapStepAction}; -use crate::{ - SqrtPrice, - position::{Position, PositionId}, - tick::{ActiveTickIndexManager, Tick, TickIndex}, -}; - -const MAX_SWAP_ITERATIONS: u16 = 1000; - -#[derive(Debug, PartialEq)] -pub struct UpdateLiquidityResult { - pub tao: TaoCurrency, - pub alpha: AlphaCurrency, - pub fee_tao: TaoCurrency, - pub fee_alpha: AlphaCurrency, - pub removed: bool, - pub tick_low: TickIndex, - pub tick_high: TickIndex, -} - -#[derive(Debug, PartialEq)] -pub struct RemoveLiquidityResult { - pub tao: TaoCurrency, - pub alpha: AlphaCurrency, - pub fee_tao: TaoCurrency, - pub fee_alpha: AlphaCurrency, - pub tick_low: TickIndex, - pub tick_high: TickIndex, - pub liquidity: u64, -} +use super::swap_step::{BasicSwapStep, SwapStep}; +use crate::{pallet::Balancer, pallet::balancer::BalancerError}; impl Pallet { - pub fn current_price(netuid: NetUid) -> U96F32 { + pub fn current_price(netuid: NetUid) -> U64F64 { match T::SubnetInfo::mechanism(netuid.into()) { 1 => { - if SwapV3Initialized::::get(netuid) { - let sqrt_price = AlphaSqrtPrice::::get(netuid); - U96F32::saturating_from_num(sqrt_price.saturating_mul(sqrt_price)) - } else { + let alpha_reserve = T::AlphaReserve::reserve(netuid.into()); + if !alpha_reserve.is_zero() { let tao_reserve = T::TaoReserve::reserve(netuid.into()); - let alpha_reserve = T::AlphaReserve::reserve(netuid.into()); - if !alpha_reserve.is_zero() { - U96F32::saturating_from_num(tao_reserve) - .saturating_div(U96F32::saturating_from_num(alpha_reserve)) - } else { - U96F32::saturating_from_num(0) - } + let balancer = SwapBalancer::::get(netuid); + balancer.calculate_price(alpha_reserve.into(), tao_reserve.into()) + } else { + U64F64::saturating_from_num(0) } } - _ => U96F32::saturating_from_num(1), + _ => U64F64::saturating_from_num(1), } } - // initializes V3 swap for a subnet if needed - pub fn maybe_initialize_v3(netuid: NetUid) -> Result<(), Error> { - if SwapV3Initialized::::get(netuid) { + // initializes pal-swap (balancer) for a subnet if needed + pub fn maybe_initialize_palswap( + netuid: NetUid, + maybe_price: Option, + ) -> Result<(), Error> { + if PalSwapInitialized::::get(netuid) { return Ok(()); } - // Initialize the v3: - // Reserves are re-purposed, nothing to set, just query values for liquidity and price - // calculation + // Query reserves let tao_reserve = T::TaoReserve::reserve(netuid.into()); let alpha_reserve = T::AlphaReserve::reserve(netuid.into()); - // Set price - let price = U64F64::saturating_from_num(tao_reserve) - .safe_div(U64F64::saturating_from_num(alpha_reserve)); - - let epsilon = U64F64::saturating_from_num(0.000000000001); - - let current_sqrt_price = price.checked_sqrt(epsilon).unwrap_or(U64F64::from_num(0)); - AlphaSqrtPrice::::set(netuid, current_sqrt_price); - - // Set current tick - let current_tick = TickIndex::from_sqrt_price_bounded(current_sqrt_price); - CurrentTick::::set(netuid, current_tick); - - // Set initial (protocol owned) liquidity and positions - // Protocol liquidity makes one position from TickIndex::MIN to TickIndex::MAX - // We are using the sp_arithmetic sqrt here, which works for u128 - let liquidity = helpers_128bit::sqrt( - (tao_reserve.to_u64() as u128).saturating_mul(alpha_reserve.to_u64() as u128), - ) as u64; - let protocol_account_id = Self::protocol_account_id(); - - let (position, _, _) = Self::add_liquidity_not_insert( - netuid, - &protocol_account_id, - TickIndex::MIN, - TickIndex::MAX, - liquidity, - )?; + // Create balancer based on price + let balancer = Balancer::new(if let Some(price) = maybe_price { + // Price is given, calculate weights: + // w_quote = y / (px + y) + let px_high = (price.saturating_to_num::() as u128) + .saturating_mul(u64::from(alpha_reserve) as u128); + let px_low = U64F64::saturating_from_num(alpha_reserve) + .saturating_mul(price.frac()) + .saturating_to_num::(); + let px_plus_y = px_high + .saturating_add(px_low) + .saturating_add(u64::from(tao_reserve) as u128); + + // If price is given and both reserves are zero, the swap doesn't initialize + if px_plus_y == 0u128 { + return Err(Error::::ReservesOutOfBalance); + } + Perquintill::from_rational(u64::from(tao_reserve) as u128, px_plus_y) + } else { + // No price = insert 0.5 into SwapBalancer + Perquintill::from_rational(1_u64, 2_u64) + }) + .map_err(|err| match err { + BalancerError::InvalidValue => Error::::ReservesOutOfBalance, + })?; + SwapBalancer::::insert(netuid, balancer.clone()); - Positions::::insert(&(netuid, protocol_account_id, position.id), position); + PalSwapInitialized::::insert(netuid, true); Ok(()) } - pub(crate) fn get_proportional_alpha_tao_and_remainders( - sqrt_alpha_price: U64F64, - amount_tao: TaoCurrency, - amount_alpha: AlphaCurrency, - ) -> (TaoCurrency, AlphaCurrency, TaoCurrency, AlphaCurrency) { - let price = sqrt_alpha_price.saturating_mul(sqrt_alpha_price); - let tao_equivalent: u64 = U64F64::saturating_from_num(u64::from(amount_alpha)) - .saturating_mul(price) - .saturating_to_num(); - let amount_tao_u64 = u64::from(amount_tao); - - if tao_equivalent <= amount_tao_u64 { - // Too much or just enough TAO - ( - tao_equivalent.into(), - amount_alpha, - amount_tao.saturating_sub(TaoCurrency::from(tao_equivalent)), - 0.into(), - ) - } else { - // Too much Alpha - let alpha_equivalent: u64 = U64F64::saturating_from_num(u64::from(amount_tao)) - .safe_div(price) - .saturating_to_num(); - ( - amount_tao, - alpha_equivalent.into(), - 0.into(), - u64::from(amount_alpha) - .saturating_sub(alpha_equivalent) - .into(), - ) - } - } - /// Adjusts protocol liquidity with new values of TAO and Alpha reserve + /// Returns actually added Tao and Alpha, which includes fees pub(super) fn adjust_protocol_liquidity( netuid: NetUid, tao_delta: TaoCurrency, alpha_delta: AlphaCurrency, - ) { - // Update protocol position with new liquidity - let protocol_account_id = Self::protocol_account_id(); - let mut positions = - Positions::::iter_prefix_values((netuid, protocol_account_id.clone())) - .collect::>(); - - if let Some(position) = positions.get_mut(0) { - // Claim protocol fees and add them to liquidity - let (tao_fees, alpha_fees) = position.collect_fees(); - - // Add fee reservoirs and get proportional amounts - let current_sqrt_price = AlphaSqrtPrice::::get(netuid); - let tao_reservoir = ScrapReservoirTao::::get(netuid); - let alpha_reservoir = ScrapReservoirAlpha::::get(netuid); - let (corrected_tao_delta, corrected_alpha_delta, tao_scrap, alpha_scrap) = - Self::get_proportional_alpha_tao_and_remainders( - current_sqrt_price, - tao_delta - .saturating_add(TaoCurrency::from(tao_fees)) - .saturating_add(tao_reservoir), - alpha_delta - .saturating_add(AlphaCurrency::from(alpha_fees)) - .saturating_add(alpha_reservoir), - ); - - // Update scrap reservoirs - ScrapReservoirTao::::insert(netuid, tao_scrap); - ScrapReservoirAlpha::::insert(netuid, alpha_scrap); - - // Adjust liquidity - let maybe_token_amounts = position.to_token_amounts(current_sqrt_price); - if let Ok((tao, alpha)) = maybe_token_amounts { - // Get updated reserves, calculate liquidity - let new_tao_reserve = tao.saturating_add(corrected_tao_delta.to_u64()); - let new_alpha_reserve = alpha.saturating_add(corrected_alpha_delta.to_u64()); - let new_liquidity = helpers_128bit::sqrt( - (new_tao_reserve as u128).saturating_mul(new_alpha_reserve as u128), - ) as u64; - let liquidity_delta = new_liquidity.saturating_sub(position.liquidity); - - // Update current liquidity - CurrentLiquidity::::mutate(netuid, |current_liquidity| { - *current_liquidity = current_liquidity.saturating_add(liquidity_delta); - }); - - // Update protocol position - position.liquidity = new_liquidity; - Positions::::insert( - (netuid, protocol_account_id, position.id), - position.clone(), - ); - - // Update position ticks - Self::add_liquidity_at_index(netuid, position.tick_low, liquidity_delta, false); - Self::add_liquidity_at_index(netuid, position.tick_high, liquidity_delta, true); - } + ) -> (TaoCurrency, AlphaCurrency) { + // Collect fees + let tao_fees = FeesTao::::get(netuid); + let alpha_fees = FeesAlpha::::get(netuid); + FeesTao::::insert(netuid, TaoCurrency::ZERO); + FeesAlpha::::insert(netuid, AlphaCurrency::ZERO); + let actual_tao_delta = tao_delta.saturating_add(tao_fees); + let actual_alpha_delta = alpha_delta.saturating_add(alpha_fees); + + // Get reserves + let alpha_reserve = T::AlphaReserve::reserve(netuid.into()); + let tao_reserve = T::TaoReserve::reserve(netuid.into()); + let mut balancer = SwapBalancer::::get(netuid); + + // Update weights and log errors if they go out of range + if balancer + .update_weights_for_added_liquidity( + u64::from(tao_reserve), + u64::from(alpha_reserve), + u64::from(actual_tao_delta), + u64::from(actual_alpha_delta), + ) + .is_err() + { + log::error!( + "Reserves are out of range for emission: netuid = {}, tao = {}, alpha = {}, tao_delta = {}, alpha_delta = {}", + netuid, + tao_reserve, + alpha_reserve, + actual_tao_delta, + actual_alpha_delta + ); + // Return fees back into fee storage and return zeroes + FeesTao::::insert(netuid, tao_fees); + FeesAlpha::::insert(netuid, alpha_fees); + (TaoCurrency::ZERO, AlphaCurrency::ZERO) + } else { + SwapBalancer::::insert(netuid, balancer); + (actual_tao_delta, actual_alpha_delta) } } @@ -240,7 +161,7 @@ impl Pallet { pub(crate) fn do_swap( netuid: NetUid, order: Order, - limit_sqrt_price: SqrtPrice, + limit_price: U64F64, drop_fees: bool, simulate: bool, ) -> Result, DispatchError> @@ -251,7 +172,7 @@ impl Pallet { transactional::with_transaction(|| { let reserve = Order::ReserveOut::reserve(netuid.into()); - let result = Self::swap_inner::(netuid, order, limit_sqrt_price, drop_fees) + let result = Self::swap_inner::(netuid, order, limit_price, drop_fees) .map_err(Into::into); if simulate || result.is_err() { @@ -277,7 +198,7 @@ impl Pallet { fn swap_inner( netuid: NetUid, order: Order, - limit_sqrt_price: SqrtPrice, + limit_price: U64F64, drop_fees: bool, ) -> Result, Error> where @@ -289,70 +210,38 @@ impl Pallet { Error::::ReservesTooLow ); - Self::maybe_initialize_v3(netuid)?; + Self::maybe_initialize_palswap(netuid, None)?; // Because user specifies the limit price, check that it is in fact beoynd the current one ensure!( - order.is_beyond_price_limit(AlphaSqrtPrice::::get(netuid), limit_sqrt_price), + order.is_beyond_price_limit(Self::current_price(netuid), limit_price), Error::::PriceLimitExceeded ); - let mut amount_remaining = order.amount(); - let mut amount_paid_out = Order::PaidOut::ZERO; - let mut iteration_counter: u16 = 0; - let mut in_acc = Order::PaidIn::ZERO; - let mut fee_acc = Order::PaidIn::ZERO; - log::trace!("======== Start Swap ========"); - log::trace!("Amount Remaining: {amount_remaining}"); - - // Swap one tick at a time until we reach one of the stop conditions - while !amount_remaining.is_zero() { - log::trace!("\nIteration: {iteration_counter}"); - log::trace!( - "\tCurrent Liquidity: {}", - CurrentLiquidity::::get(netuid) - ); - - // Create and execute a swap step - let mut swap_step = BasicSwapStep::::new( - netuid, - amount_remaining, - limit_sqrt_price, - drop_fees, - ); - - let swap_result = swap_step.execute()?; - - in_acc = in_acc.saturating_add(swap_result.delta_in); - fee_acc = fee_acc.saturating_add(swap_result.fee_paid); - amount_remaining = amount_remaining.saturating_sub(swap_result.amount_to_take); - amount_paid_out = amount_paid_out.saturating_add(swap_result.delta_out); - - if swap_step.action() == SwapStepAction::Stop { - amount_remaining = Order::PaidIn::ZERO; - } + let amount_to_swap = order.amount(); + log::trace!("Amount to swap: {amount_to_swap}"); - // The swap step didn't exchange anything - if swap_result.amount_to_take.is_zero() { - amount_remaining = Order::PaidIn::ZERO; - } - - iteration_counter = iteration_counter.saturating_add(1); + // Create and execute a swap step + let mut swap_step = BasicSwapStep::::new( + netuid, + amount_to_swap, + limit_price, + drop_fees, + ); - ensure!( - iteration_counter <= MAX_SWAP_ITERATIONS, - Error::::TooManySwapSteps - ); - } + let swap_result = swap_step.execute()?; - log::trace!("\nAmount Paid Out: {amount_paid_out}"); + log::trace!("Delta out: {}", swap_result.delta_out); + log::trace!("Fees: {}", swap_result.fee_paid); + log::trace!("Fees for block author: {}", swap_result.fee_to_block_author); log::trace!("======== End Swap ========"); Ok(SwapResult { - amount_paid_in: in_acc, - amount_paid_out, - fee_paid: fee_acc, + amount_paid_in: swap_result.delta_in, + amount_paid_out: swap_result.delta_out, + fee_paid: swap_result.fee_paid, + fee_to_block_author: swap_result.fee_to_block_author, }) } @@ -381,425 +270,12 @@ impl Pallet { } } - pub fn find_closest_lower_active_tick(netuid: NetUid, index: TickIndex) -> Option { - ActiveTickIndexManager::::find_closest_lower(netuid, index) - .and_then(|ti| Ticks::::get(netuid, ti)) - } - - pub fn find_closest_higher_active_tick(netuid: NetUid, index: TickIndex) -> Option { - ActiveTickIndexManager::::find_closest_higher(netuid, index) - .and_then(|ti| Ticks::::get(netuid, ti)) - } - - /// Here we subtract minimum safe liquidity from current liquidity to stay in the safe range - pub(crate) fn current_liquidity_safe(netuid: NetUid) -> U64F64 { - U64F64::saturating_from_num( - CurrentLiquidity::::get(netuid).saturating_sub(T::MinimumLiquidity::get()), - ) - } - - /// Adds liquidity to the specified price range. - /// - /// This function allows an account to provide liquidity to a given range of price ticks. The - /// amount of liquidity to be added can be determined using - /// [`get_tao_based_liquidity`] and [`get_alpha_based_liquidity`], which compute the required - /// liquidity based on TAO and Alpha balances for the current price tick. - /// - /// ### Behavior: - /// - If the `protocol` flag is **not set** (`false`), the function will attempt to - /// **withdraw balances** from the account using `state_ops.withdraw_balances()`. - /// - If the `protocol` flag is **set** (`true`), the liquidity is added without modifying balances. - /// - If swap V3 was not initialized before, updates the value in storage. - /// - /// ### Parameters: - /// - `coldkey_account_id`: A reference to the account coldkey that is providing liquidity. - /// - `hotkey_account_id`: A reference to the account hotkey that is providing liquidity. - /// - `tick_low`: The lower bound of the price tick range. - /// - `tick_high`: The upper bound of the price tick range. - /// - `liquidity`: The amount of liquidity to be added. - /// - /// ### Returns: - /// - `Ok((u64, u64))`: (tao, alpha) amounts at new position - /// - `Err(SwapError)`: If the operation fails due to insufficient balance, invalid tick range, - /// or other swap-related errors. - /// - /// ### Errors: - /// - [`SwapError::InsufficientBalance`] if the account does not have enough balance. - /// - [`SwapError::InvalidTickRange`] if `tick_low` is greater than or equal to `tick_high`. - /// - Other [`SwapError`] variants as applicable. - pub fn do_add_liquidity( - netuid: NetUid, - coldkey_account_id: &T::AccountId, - hotkey_account_id: &T::AccountId, - tick_low: TickIndex, - tick_high: TickIndex, - liquidity: u64, - ) -> Result<(PositionId, u64, u64), Error> { - ensure!( - EnabledUserLiquidity::::get(netuid), - Error::::UserLiquidityDisabled - ); - - let (position, tao, alpha) = Self::add_liquidity_not_insert( - netuid, - coldkey_account_id, - tick_low, - tick_high, - liquidity, - )?; - let position_id = position.id; - - ensure!( - T::BalanceOps::tao_balance(coldkey_account_id) >= TaoCurrency::from(tao) - && T::BalanceOps::alpha_balance( - netuid.into(), - coldkey_account_id, - hotkey_account_id - ) >= AlphaCurrency::from(alpha), - Error::::InsufficientBalance - ); - - // Small delta is not allowed - ensure!( - liquidity >= T::MinimumLiquidity::get(), - Error::::InvalidLiquidityValue - ); - - Positions::::insert(&(netuid, coldkey_account_id, position.id), position); - - Ok((position_id, tao, alpha)) - } - - // add liquidity without inserting position into storage (used privately for v3 intiialization). - // unlike Self::add_liquidity it also doesn't perform account's balance check. - // - // the public interface is [`Self::add_liquidity`] - fn add_liquidity_not_insert( - netuid: NetUid, - coldkey_account_id: &T::AccountId, - tick_low: TickIndex, - tick_high: TickIndex, - liquidity: u64, - ) -> Result<(Position, u64, u64), Error> { - ensure!( - Self::count_positions(netuid, coldkey_account_id) < T::MaxPositions::get() as usize, - Error::::MaxPositionsExceeded - ); - - // Ensure that tick_high is actually higher than tick_low - ensure!(tick_high > tick_low, Error::::InvalidTickRange); - - // Add liquidity at tick - Self::add_liquidity_at_index(netuid, tick_low, liquidity, false); - Self::add_liquidity_at_index(netuid, tick_high, liquidity, true); - - // Update current tick liquidity - let current_tick_index = TickIndex::current_bounded::(netuid); - Self::clamp_sqrt_price(netuid, current_tick_index); - - Self::update_liquidity_if_needed(netuid, tick_low, tick_high, liquidity as i128); - - // New position - let position_id = PositionId::new::(); - let position = Position::new(position_id, netuid, tick_low, tick_high, liquidity); - - let current_price_sqrt = AlphaSqrtPrice::::get(netuid); - let (tao, alpha) = position.to_token_amounts(current_price_sqrt)?; - - SwapV3Initialized::::set(netuid, true); - - Ok((position, tao, alpha)) - } - - /// Remove liquidity and credit balances back to (coldkey_account_id, hotkey_account_id) stake. - /// Removing is allowed even when user liquidity is enabled. - /// - /// Account ID and Position ID identify position in the storage map - pub fn do_remove_liquidity( - netuid: NetUid, - coldkey_account_id: &T::AccountId, - position_id: PositionId, - ) -> Result> { - let Some(mut position) = Positions::::get((netuid, coldkey_account_id, position_id)) - else { - return Err(Error::::LiquidityNotFound); - }; - - // Collect fees and get tao and alpha amounts - let (fee_tao, fee_alpha) = position.collect_fees(); - let current_price = AlphaSqrtPrice::::get(netuid); - let (tao, alpha) = position.to_token_amounts(current_price)?; - - // Update liquidity at position ticks - Self::remove_liquidity_at_index(netuid, position.tick_low, position.liquidity, false); - Self::remove_liquidity_at_index(netuid, position.tick_high, position.liquidity, true); - - // Update current tick liquidity - Self::update_liquidity_if_needed( - netuid, - position.tick_low, - position.tick_high, - (position.liquidity as i128).neg(), - ); - - // Remove user position - Positions::::remove((netuid, coldkey_account_id, position_id)); - - Ok(RemoveLiquidityResult { - tao: tao.into(), - alpha: alpha.into(), - fee_tao: fee_tao.into(), - fee_alpha: fee_alpha.into(), - tick_low: position.tick_low, - tick_high: position.tick_high, - liquidity: position.liquidity, - }) - } - - pub fn do_modify_position( - netuid: NetUid, - coldkey_account_id: &T::AccountId, - hotkey_account_id: &T::AccountId, - position_id: PositionId, - liquidity_delta: i64, - ) -> Result> { - ensure!( - EnabledUserLiquidity::::get(netuid), - Error::::UserLiquidityDisabled - ); - - // Find the position - let Some(mut position) = Positions::::get((netuid, coldkey_account_id, position_id)) - else { - return Err(Error::::LiquidityNotFound); - }; - - // Small delta is not allowed - ensure!( - liquidity_delta.abs() >= T::MinimumLiquidity::get() as i64, - Error::::InvalidLiquidityValue - ); - let mut delta_liquidity_abs = liquidity_delta.unsigned_abs(); - - // Determine the effective price for token calculations - let current_price_sqrt = AlphaSqrtPrice::::get(netuid); - let sqrt_pa: SqrtPrice = position - .tick_low - .try_to_sqrt_price() - .map_err(|_| Error::::InvalidTickRange)?; - let sqrt_pb: SqrtPrice = position - .tick_high - .try_to_sqrt_price() - .map_err(|_| Error::::InvalidTickRange)?; - let sqrt_price_box = if current_price_sqrt < sqrt_pa { - sqrt_pa - } else if current_price_sqrt > sqrt_pb { - sqrt_pb - } else { - // Update current liquidity if price is in range - let new_liquidity_curr = if liquidity_delta > 0 { - CurrentLiquidity::::get(netuid).saturating_add(delta_liquidity_abs) - } else { - CurrentLiquidity::::get(netuid).saturating_sub(delta_liquidity_abs) - }; - CurrentLiquidity::::set(netuid, new_liquidity_curr); - current_price_sqrt - }; - - // Calculate token amounts for the liquidity change - let mul = SqrtPrice::from_num(1) - .safe_div(sqrt_price_box) - .saturating_sub(SqrtPrice::from_num(1).safe_div(sqrt_pb)); - let alpha = SqrtPrice::saturating_from_num(delta_liquidity_abs).saturating_mul(mul); - let tao = SqrtPrice::saturating_from_num(delta_liquidity_abs) - .saturating_mul(sqrt_price_box.saturating_sub(sqrt_pa)); - - // Validate delta - if liquidity_delta > 0 { - // Check that user has enough balances - ensure!( - T::BalanceOps::tao_balance(coldkey_account_id) - >= TaoCurrency::from(tao.saturating_to_num::()) - && T::BalanceOps::alpha_balance(netuid, coldkey_account_id, hotkey_account_id) - >= AlphaCurrency::from(alpha.saturating_to_num::()), - Error::::InsufficientBalance - ); - } else { - // Check that position has enough liquidity - ensure!( - position.liquidity >= delta_liquidity_abs, - Error::::InsufficientLiquidity - ); - } - - // Collect fees - let (fee_tao, fee_alpha) = position.collect_fees(); - - // If delta brings the position liquidity below MinimumLiquidity, eliminate position and - // withdraw full amounts - let mut remove = false; - if (liquidity_delta < 0) - && (position.liquidity.saturating_sub(delta_liquidity_abs) < T::MinimumLiquidity::get()) - { - delta_liquidity_abs = position.liquidity; - remove = true; - } - - // Adjust liquidity at the ticks based on the delta sign - if liquidity_delta > 0 { - // Add liquidity at tick - Self::add_liquidity_at_index(netuid, position.tick_low, delta_liquidity_abs, false); - Self::add_liquidity_at_index(netuid, position.tick_high, delta_liquidity_abs, true); - - // Add liquidity to user position - position.liquidity = position.liquidity.saturating_add(delta_liquidity_abs); - } else { - // Remove liquidity at tick - Self::remove_liquidity_at_index(netuid, position.tick_low, delta_liquidity_abs, false); - Self::remove_liquidity_at_index(netuid, position.tick_high, delta_liquidity_abs, true); - - // Remove liquidity from user position - position.liquidity = position.liquidity.saturating_sub(delta_liquidity_abs); - } - - // Update or, in case if full liquidity is removed, remove the position - if remove { - Positions::::remove((netuid, coldkey_account_id, position_id)); - } else { - Positions::::insert(&(netuid, coldkey_account_id, position.id), position.clone()); - } - - Ok(UpdateLiquidityResult { - tao: tao.saturating_to_num::().into(), - alpha: alpha.saturating_to_num::().into(), - fee_tao: fee_tao.into(), - fee_alpha: fee_alpha.into(), - removed: remove, - tick_low: position.tick_low, - tick_high: position.tick_high, - }) - } - - /// Adds or updates liquidity at a specific tick index for a subnet - /// - /// # Arguments - /// * `netuid` - The subnet ID - /// * `tick_index` - The tick index to add liquidity to - /// * `liquidity` - The amount of liquidity to add - fn add_liquidity_at_index(netuid: NetUid, tick_index: TickIndex, liquidity: u64, upper: bool) { - // Convert liquidity to signed value, negating it for upper bounds - let net_liquidity_change = if upper { - (liquidity as i128).neg() - } else { - liquidity as i128 - }; - - Ticks::::mutate(netuid, tick_index, |maybe_tick| match maybe_tick { - Some(tick) => { - tick.liquidity_net = tick.liquidity_net.saturating_add(net_liquidity_change); - tick.liquidity_gross = tick.liquidity_gross.saturating_add(liquidity); - } - None => { - let current_tick = TickIndex::current_bounded::(netuid); - - let (fees_out_tao, fees_out_alpha) = if tick_index > current_tick { - ( - I64F64::saturating_from_num(FeeGlobalTao::::get(netuid)), - I64F64::saturating_from_num(FeeGlobalAlpha::::get(netuid)), - ) - } else { - ( - I64F64::saturating_from_num(0), - I64F64::saturating_from_num(0), - ) - }; - *maybe_tick = Some(Tick { - liquidity_net: net_liquidity_change, - liquidity_gross: liquidity, - fees_out_tao, - fees_out_alpha, - }); - } - }); - - // Update active ticks - ActiveTickIndexManager::::insert(netuid, tick_index); - } - - /// Remove liquidity at tick index. - fn remove_liquidity_at_index( - netuid: NetUid, - tick_index: TickIndex, - liquidity: u64, - upper: bool, - ) { - // Calculate net liquidity addition - let net_reduction = if upper { - (liquidity as i128).neg() - } else { - liquidity as i128 - }; - - Ticks::::mutate_exists(netuid, tick_index, |maybe_tick| { - if let Some(tick) = maybe_tick { - tick.liquidity_net = tick.liquidity_net.saturating_sub(net_reduction); - tick.liquidity_gross = tick.liquidity_gross.saturating_sub(liquidity); - - // If no liquidity is left at the tick, remove it - if tick.liquidity_gross == 0 { - *maybe_tick = None; - - // Update active ticks: Final liquidity is zero, remove this tick from active. - ActiveTickIndexManager::::remove(netuid, tick_index); - } - } - }); - } - - /// Updates the current liquidity for a subnet if the current tick index is within the specified - /// range - /// - /// This function handles both increasing and decreasing liquidity based on the sign of the - /// liquidity parameter. It uses i128 to safely handle values up to u64::MAX in both positive - /// and negative directions. - fn update_liquidity_if_needed( - netuid: NetUid, - tick_low: TickIndex, - tick_high: TickIndex, - liquidity: i128, - ) { - let current_tick_index = TickIndex::current_bounded::(netuid); - if (tick_low <= current_tick_index) && (current_tick_index < tick_high) { - CurrentLiquidity::::mutate(netuid, |current_liquidity| { - let is_neg = liquidity.is_negative(); - let liquidity = liquidity.abs().min(u64::MAX as i128) as u64; - if is_neg { - *current_liquidity = current_liquidity.saturating_sub(liquidity); - } else { - *current_liquidity = current_liquidity.saturating_add(liquidity); - } - }); - } - } - - /// Clamps the subnet's sqrt price when tick index is outside of valid bounds - fn clamp_sqrt_price(netuid: NetUid, tick_index: TickIndex) { - if tick_index >= TickIndex::MAX || tick_index <= TickIndex::MIN { - let corrected_price = tick_index.as_sqrt_price_bounded(); - AlphaSqrtPrice::::set(netuid, corrected_price); - } + pub(crate) fn min_price_inner() -> C { + u64::from(1_000_u64).into() } - /// Returns the number of positions for an account in a specific subnet - /// - /// # Arguments - /// * `netuid` - The subnet ID - /// * `account_id` - The account ID - /// - /// # Returns - /// The number of positions that the account has in the specified subnet - pub(super) fn count_positions(netuid: NetUid, account_id: &T::AccountId) -> usize { - Positions::::iter_prefix_values((netuid, account_id.clone())).count() + pub(crate) fn max_price_inner() -> C { + u64::from(1_000_000_000_000_000_u64).into() } /// Returns the protocol account ID @@ -810,207 +286,22 @@ impl Pallet { T::ProtocolId::get().into_account_truncating() } - pub(crate) fn min_price_inner() -> C { - TickIndex::min_sqrt_price() - .saturating_mul(TickIndex::min_sqrt_price()) - .saturating_mul(SqrtPrice::saturating_from_num(1_000_000_000)) - .saturating_to_num::() - .into() - } - - pub(crate) fn max_price_inner() -> C { - TickIndex::max_sqrt_price() - .saturating_mul(TickIndex::max_sqrt_price()) - .saturating_mul(SqrtPrice::saturating_from_num(1_000_000_000)) - .saturating_round() - .saturating_to_num::() - .into() - } - - /// Dissolve all LPs and clean state. - pub fn do_dissolve_all_liquidity_providers(netuid: NetUid) -> DispatchResult { - if SwapV3Initialized::::get(netuid) { - // 1) Snapshot only *non‑protocol* positions: (owner, position_id). - struct CloseItem { - owner: A, - pos_id: PositionId, - } - let protocol_account = Self::protocol_account_id(); - - let mut to_close: sp_std::vec::Vec> = sp_std::vec::Vec::new(); - for ((owner, pos_id), _pos) in Positions::::iter_prefix((netuid,)) { - if owner != protocol_account { - to_close.push(CloseItem { owner, pos_id }); - } - } - - if to_close.is_empty() { - log::debug!( - "dissolve_all_lp: no user positions; netuid={netuid:?}, protocol liquidity untouched" - ); - return Ok(()); - } - - let mut user_refunded_tao = TaoCurrency::ZERO; - let mut user_staked_alpha = AlphaCurrency::ZERO; - - let trust: Vec = T::SubnetInfo::get_validator_trust(netuid.into()); - let permit: Vec = T::SubnetInfo::get_validator_permit(netuid.into()); - - // Helper: pick target validator uid, only among permitted validators, by highest trust. - let pick_target_uid = |trust: &Vec, permit: &Vec| -> Option { - let mut best_uid: Option = None; - let mut best_trust: u16 = 0; - for (i, (&t, &p)) in trust.iter().zip(permit.iter()).enumerate() { - if p && (best_uid.is_none() || t > best_trust) { - best_uid = Some(i); - best_trust = t; - } - } - best_uid.map(|i| i as u16) - }; - - for CloseItem { owner, pos_id } in to_close.into_iter() { - match Self::do_remove_liquidity(netuid, &owner, pos_id) { - Ok(rm) => { - // α withdrawn from the pool = principal + accrued fees - let alpha_total_from_pool: AlphaCurrency = - rm.alpha.saturating_add(rm.fee_alpha); - - // ---------------- USER: refund τ and convert α → stake ---------------- - - // 1) Refund τ principal directly. - let tao_total_from_pool: TaoCurrency = rm.tao.saturating_add(rm.fee_tao); - if tao_total_from_pool > TaoCurrency::ZERO { - T::BalanceOps::increase_balance(&owner, tao_total_from_pool); - user_refunded_tao = - user_refunded_tao.saturating_add(tao_total_from_pool); - T::TaoReserve::decrease_provided(netuid, tao_total_from_pool); - } - - // 2) Stake ALL withdrawn α (principal + fees) to the best permitted validator. - if alpha_total_from_pool > AlphaCurrency::ZERO { - if let Some(target_uid) = pick_target_uid(&trust, &permit) { - let validator_hotkey: T::AccountId = - T::SubnetInfo::hotkey_of_uid(netuid.into(), target_uid).ok_or( - sp_runtime::DispatchError::Other( - "validator_hotkey_missing", - ), - )?; - - // Stake α from LP owner (coldkey) to chosen validator (hotkey). - T::BalanceOps::increase_stake( - &owner, - &validator_hotkey, - netuid, - alpha_total_from_pool, - )?; - - user_staked_alpha = - user_staked_alpha.saturating_add(alpha_total_from_pool); - - log::debug!( - "dissolve_all_lp: user dissolved & staked α: netuid={netuid:?}, owner={owner:?}, pos_id={pos_id:?}, α_staked={alpha_total_from_pool:?}, target_uid={target_uid}" - ); - } else { - // No permitted validators; burn to avoid balance drift. - log::debug!( - "dissolve_all_lp: no permitted validators; α burned: netuid={netuid:?}, owner={owner:?}, pos_id={pos_id:?}, α_total={alpha_total_from_pool:?}" - ); - } - - T::AlphaReserve::decrease_provided(netuid, alpha_total_from_pool); - } - } - Err(e) => { - log::debug!( - "dissolve_all_lp: force-close failed: netuid={netuid:?}, owner={owner:?}, pos_id={pos_id:?}, err={e:?}" - ); - continue; - } - } - } - - log::debug!( - "dissolve_all_liquidity_providers (users-only): netuid={netuid:?}, users_refunded_total_τ={user_refunded_tao:?}, users_staked_total_α={user_staked_alpha:?}; protocol liquidity untouched" - ); - - return Ok(()); - } - - log::debug!( - "dissolve_all_liquidity_providers: netuid={netuid:?}, mode=V2-or-nonV3, leaving all liquidity/state intact" - ); - - Ok(()) - } - /// Clear **protocol-owned** liquidity and wipe all swap state for `netuid`. pub fn do_clear_protocol_liquidity(netuid: NetUid) -> DispatchResult { - let protocol_account = Self::protocol_account_id(); + // let protocol_account = Self::protocol_account_id(); - // 1) Force-close only protocol positions, burning proceeds. - let mut burned_tao = TaoCurrency::ZERO; - let mut burned_alpha = AlphaCurrency::ZERO; + // 1) Force-close protocol liquidity, burning proceeds. + let burned_tao = T::TaoReserve::reserve(netuid.into()); + let burned_alpha = T::AlphaReserve::reserve(netuid.into()); - // Collect protocol position IDs first to avoid mutating while iterating. - let protocol_pos_ids: sp_std::vec::Vec = Positions::::iter_prefix((netuid,)) - .filter_map(|((owner, pos_id), _)| { - if owner == protocol_account { - Some(pos_id) - } else { - None - } - }) - .collect(); - - for pos_id in protocol_pos_ids { - match Self::do_remove_liquidity(netuid, &protocol_account, pos_id) { - Ok(rm) => { - let alpha_total_from_pool: AlphaCurrency = - rm.alpha.saturating_add(rm.fee_alpha); - let tao_total_from_pool: TaoCurrency = rm.tao.saturating_add(rm.fee_tao); - - if tao_total_from_pool > TaoCurrency::ZERO { - burned_tao = burned_tao.saturating_add(tao_total_from_pool); - } - if alpha_total_from_pool > AlphaCurrency::ZERO { - burned_alpha = burned_alpha.saturating_add(alpha_total_from_pool); - } - - log::debug!( - "clear_protocol_liquidity: burned protocol pos: netuid={netuid:?}, pos_id={pos_id:?}, τ={tao_total_from_pool:?}, α_total={alpha_total_from_pool:?}" - ); - } - Err(e) => { - log::debug!( - "clear_protocol_liquidity: force-close failed: netuid={netuid:?}, pos_id={pos_id:?}, err={e:?}" - ); - continue; - } - } - } + T::TaoReserve::decrease_provided(netuid.into(), burned_tao); + T::AlphaReserve::decrease_provided(netuid.into(), burned_alpha); - // 2) Clear active tick index entries, then all swap state (idempotent even if empty/non‑V3). - let active_ticks: sp_std::vec::Vec = - Ticks::::iter_prefix(netuid).map(|(ti, _)| ti).collect(); - for ti in active_ticks { - ActiveTickIndexManager::::remove(netuid, ti); - } - - let _ = Positions::::clear_prefix((netuid,), u32::MAX, None); - let _ = Ticks::::clear_prefix(netuid, u32::MAX, None); - - FeeGlobalTao::::remove(netuid); - FeeGlobalAlpha::::remove(netuid); - CurrentLiquidity::::remove(netuid); - CurrentTick::::remove(netuid); - AlphaSqrtPrice::::remove(netuid); - SwapV3Initialized::::remove(netuid); - - let _ = TickIndexBitmapWords::::clear_prefix((netuid,), u32::MAX, None); + FeesTao::::remove(netuid); + FeesAlpha::::remove(netuid); + PalSwapInitialized::::remove(netuid); FeeRate::::remove(netuid); - EnabledUserLiquidity::::remove(netuid); + SwapBalancer::::remove(netuid); log::debug!( "clear_protocol_liquidity: netuid={netuid:?}, protocol_burned: τ={burned_tao:?}, α={burned_alpha:?}; state cleared" @@ -1046,15 +337,13 @@ where drop_fees: bool, should_rollback: bool, ) -> Result, DispatchError> { - let limit_sqrt_price = SqrtPrice::saturating_from_num(price_limit.to_u64()) - .safe_div(SqrtPrice::saturating_from_num(1_000_000_000)) - .checked_sqrt(SqrtPrice::saturating_from_num(0.0000000001)) - .ok_or(Error::::PriceLimitExceeded)?; + let limit_price = U64F64::saturating_from_num(price_limit.to_u64()) + .safe_div(U64F64::saturating_from_num(1_000_000_000_u64)); Self::do_swap::( NetUid::from(netuid), order, - limit_sqrt_price, + limit_price, drop_fees, should_rollback, ) @@ -1101,6 +390,7 @@ impl SwapHandler for Pallet { amount_paid_in: actual_amount, amount_paid_out: actual_amount.to_u64().into(), fee_paid: 0.into(), + fee_to_block_author: 0.into(), }) } } @@ -1110,28 +400,10 @@ impl SwapHandler for Pallet { Self::calculate_fee_amount(netuid, amount, false) } - fn current_alpha_price(netuid: NetUid) -> U96F32 { + fn current_alpha_price(netuid: NetUid) -> U64F64 { Self::current_price(netuid.into()) } - fn get_protocol_tao(netuid: NetUid) -> TaoCurrency { - let protocol_account_id = Self::protocol_account_id(); - let mut positions = - Positions::::iter_prefix_values((netuid, protocol_account_id.clone())) - .collect::>(); - - if let Some(position) = positions.get_mut(0) { - let current_sqrt_price = AlphaSqrtPrice::::get(netuid); - // Adjust liquidity - let maybe_token_amounts = position.to_token_amounts(current_sqrt_price); - if let Ok((tao, _)) = maybe_token_amounts { - return tao.into(); - } - } - - TaoCurrency::ZERO - } - fn min_price() -> C { Self::min_price_inner() } @@ -1144,20 +416,14 @@ impl SwapHandler for Pallet { netuid: NetUid, tao_delta: TaoCurrency, alpha_delta: AlphaCurrency, - ) { - Self::adjust_protocol_liquidity(netuid, tao_delta, alpha_delta); + ) -> (TaoCurrency, AlphaCurrency) { + Self::adjust_protocol_liquidity(netuid, tao_delta, alpha_delta) } - fn is_user_liquidity_enabled(netuid: NetUid) -> bool { - EnabledUserLiquidity::::get(netuid) - } - fn dissolve_all_liquidity_providers(netuid: NetUid) -> DispatchResult { - Self::do_dissolve_all_liquidity_providers(netuid) - } - fn toggle_user_liquidity(netuid: NetUid, enabled: bool) { - EnabledUserLiquidity::::insert(netuid, enabled) - } fn clear_protocol_liquidity(netuid: NetUid) -> DispatchResult { Self::do_clear_protocol_liquidity(netuid) } + fn init_swap(netuid: NetUid, maybe_price: Option) { + Self::maybe_initialize_palswap(netuid, maybe_price).unwrap_or_default(); + } } diff --git a/pallets/swap/src/pallet/migrations/migrate_swapv3_to_balancer.rs b/pallets/swap/src/pallet/migrations/migrate_swapv3_to_balancer.rs new file mode 100644 index 0000000000..147849e5a6 --- /dev/null +++ b/pallets/swap/src/pallet/migrations/migrate_swapv3_to_balancer.rs @@ -0,0 +1,81 @@ +use super::*; +use crate::HasMigrationRun; +use frame_support::{storage_alias, traits::Get, weights::Weight}; +use scale_info::prelude::string::String; +use substrate_fixed::types::U64F64; + +pub mod deprecated_swap_maps { + use super::*; + + #[storage_alias] + pub type AlphaSqrtPrice = + StorageMap, Twox64Concat, NetUid, U64F64, ValueQuery>; + + /// TAO reservoir for scraps of protocol claimed fees. + #[storage_alias] + pub type ScrapReservoirTao = + StorageMap, Twox64Concat, NetUid, TaoCurrency, ValueQuery>; + + /// Alpha reservoir for scraps of protocol claimed fees. + #[storage_alias] + pub type ScrapReservoirAlpha = + StorageMap, Twox64Concat, NetUid, AlphaCurrency, ValueQuery>; +} + +pub fn migrate_swapv3_to_balancer() -> Weight { + let migration_name = BoundedVec::truncate_from(b"migrate_swapv3_to_balancer".to_vec()); + let mut weight = T::DbWeight::get().reads(1); + + if HasMigrationRun::::get(&migration_name) { + log::info!( + "Migration '{:?}' has already run. Skipping.", + String::from_utf8_lossy(&migration_name) + ); + return weight; + } + + log::info!( + "Running migration '{}'", + String::from_utf8_lossy(&migration_name), + ); + + // ------------------------------ + // Step 1: Initialize swaps with price before price removal + // ------------------------------ + for (netuid, price_sqrt) in deprecated_swap_maps::AlphaSqrtPrice::::iter() { + let price = price_sqrt.saturating_mul(price_sqrt); + crate::Pallet::::maybe_initialize_palswap(netuid, Some(price)).unwrap_or_default(); + } + + // ------------------------------ + // Step 2: Clear Map entries + // ------------------------------ + remove_prefix::("Swap", "AlphaSqrtPrice", &mut weight); + remove_prefix::("Swap", "CurrentTick", &mut weight); + remove_prefix::("Swap", "EnabledUserLiquidity", &mut weight); + remove_prefix::("Swap", "FeeGlobalTao", &mut weight); + remove_prefix::("Swap", "FeeGlobalAlpha", &mut weight); + remove_prefix::("Swap", "LastPositionId", &mut weight); + // Scrap reservoirs can be just cleaned because they are already included in reserves + remove_prefix::("Swap", "ScrapReservoirTao", &mut weight); + remove_prefix::("Swap", "ScrapReservoirAlpha", &mut weight); + remove_prefix::("Swap", "Ticks", &mut weight); + remove_prefix::("Swap", "TickIndexBitmapWords", &mut weight); + remove_prefix::("Swap", "SwapV3Initialized", &mut weight); + remove_prefix::("Swap", "CurrentLiquidity", &mut weight); + remove_prefix::("Swap", "Positions", &mut weight); + + // ------------------------------ + // Step 3: Mark Migration as Completed + // ------------------------------ + + HasMigrationRun::::insert(&migration_name, true); + weight = weight.saturating_add(T::DbWeight::get().writes(1)); + + log::info!( + "Migration '{:?}' completed successfully.", + String::from_utf8_lossy(&migration_name) + ); + + weight +} diff --git a/pallets/swap/src/pallet/migrations/mod.rs b/pallets/swap/src/pallet/migrations/mod.rs new file mode 100644 index 0000000000..d34626f05e --- /dev/null +++ b/pallets/swap/src/pallet/migrations/mod.rs @@ -0,0 +1,25 @@ +use super::*; +use frame_support::pallet_prelude::Weight; +use sp_io::KillStorageResult; +use sp_io::hashing::twox_128; +use sp_io::storage::clear_prefix; +use sp_std::vec::Vec; + +pub mod migrate_swapv3_to_balancer; + +pub(crate) fn remove_prefix(module: &str, old_map: &str, weight: &mut Weight) { + let mut prefix = Vec::new(); + prefix.extend_from_slice(&twox_128(module.as_bytes())); + prefix.extend_from_slice(&twox_128(old_map.as_bytes())); + + let removal_results = clear_prefix(&prefix, Some(u32::MAX)); + let removed_entries_count = match removal_results { + KillStorageResult::AllRemoved(removed) => removed as u64, + KillStorageResult::SomeRemaining(removed) => { + log::info!("Failed To Remove Some Items During migration"); + removed as u64 + } + }; + + *weight = (*weight).saturating_add(T::DbWeight::get().writes(removed_entries_count)); +} diff --git a/pallets/swap/src/pallet/mod.rs b/pallets/swap/src/pallet/mod.rs index b55df77fee..9fc4aca769 100644 --- a/pallets/swap/src/pallet/mod.rs +++ b/pallets/swap/src/pallet/mod.rs @@ -1,32 +1,33 @@ use core::num::NonZeroU64; -use core::ops::Neg; use frame_support::{PalletId, pallet_prelude::*, traits::Get}; use frame_system::pallet_prelude::*; -use substrate_fixed::types::U64F64; +use sp_arithmetic::Perbill; use subtensor_runtime_common::{ - AlphaCurrency, BalanceOps, Currency, CurrencyReserve, NetUid, SubnetInfo, TaoCurrency, -}; - -use crate::{ - position::{Position, PositionId}, - tick::{LayerLevel, Tick, TickIndex}, - weights::WeightInfo, + AlphaCurrency, BalanceOps, CurrencyReserve, NetUid, SubnetInfo, TaoCurrency, }; +use crate::{pallet::balancer::Balancer, weights::WeightInfo}; pub use pallet::*; +use subtensor_macros::freeze_struct; +mod balancer; +mod hooks; mod impls; +pub mod migrations; mod swap_step; #[cfg(test)] mod tests; +// Define a maximum length for the migration key +type MigrationKeyMaxLen = ConstU32<128>; + #[allow(clippy::module_inception)] #[frame_support::pallet] #[allow(clippy::expect_used)] mod pallet { use super::*; - use frame_system::{ensure_root, ensure_signed}; + use frame_system::ensure_root; #[pallet::pallet] pub struct Pallet(_); @@ -56,10 +57,6 @@ mod pallet { #[pallet::constant] type MaxFeeRate: Get; - /// The maximum number of positions a user can have - #[pallet::constant] - type MaxPositions: Get; - /// Minimum liquidity that is safe for rounding and integer math. #[pallet::constant] type MinimumLiquidity: Get; @@ -78,168 +75,52 @@ mod pallet { 33 // ~0.05 % } + /// Fee split between pool and block builder. + /// Pool receives the portion returned by this function + #[pallet::type_value] + pub fn DefaultFeeSplit() -> Perbill { + Perbill::zero() + } + /// The fee rate applied to swaps per subnet, normalized value between 0 and u16::MAX #[pallet::storage] pub type FeeRate = StorageMap<_, Twox64Concat, NetUid, u16, ValueQuery, DefaultFeeRate>; - // Global accrued fees in tao per subnet - #[pallet::storage] - pub type FeeGlobalTao = StorageMap<_, Twox64Concat, NetUid, U64F64, ValueQuery>; - - // Global accrued fees in alpha per subnet - #[pallet::storage] - pub type FeeGlobalAlpha = StorageMap<_, Twox64Concat, NetUid, U64F64, ValueQuery>; - - /// Storage for all ticks, using subnet ID as the primary key and tick index as the secondary key - #[pallet::storage] - pub type Ticks = StorageDoubleMap<_, Twox64Concat, NetUid, Twox64Concat, TickIndex, Tick>; - - /// Storage to determine whether swap V3 was initialized for a specific subnet. - #[pallet::storage] - pub type SwapV3Initialized = StorageMap<_, Twox64Concat, NetUid, bool, ValueQuery>; + //////////////////////////////////////////////////// + // Balancer (PalSwap) maps and variables - /// Storage for the square root price of Alpha token for each subnet. - #[pallet::storage] - pub type AlphaSqrtPrice = StorageMap<_, Twox64Concat, NetUid, U64F64, ValueQuery>; - - /// Storage for the current price tick. - #[pallet::storage] - pub type CurrentTick = StorageMap<_, Twox64Concat, NetUid, TickIndex, ValueQuery>; - - /// Storage for the current liquidity amount for each subnet. + /// Default reserve weight + #[pallet::type_value] + pub fn DefaultBalancer() -> Balancer { + Balancer::default() + } + /// u64-normalized reserve weight #[pallet::storage] - pub type CurrentLiquidity = StorageMap<_, Twox64Concat, NetUid, u64, ValueQuery>; + pub type SwapBalancer = + StorageMap<_, Twox64Concat, NetUid, Balancer, ValueQuery, DefaultBalancer>; - /// Indicates whether a subnet has been switched to V3 swap from V2. - /// If `true`, the subnet is permanently on V3 swap mode allowing add/remove liquidity - /// operations. Once set to `true` for a subnet, it cannot be changed back to `false`. + /// Storage to determine whether balancer swap was initialized for a specific subnet. #[pallet::storage] - pub type EnabledUserLiquidity = StorageMap<_, Twox64Concat, NetUid, bool, ValueQuery>; + pub type PalSwapInitialized = StorageMap<_, Twox64Concat, NetUid, bool, ValueQuery>; - /// Storage for user positions, using subnet ID and account ID as keys - /// The value is a bounded vector of Position structs with details about the liquidity positions - #[pallet::storage] - pub type Positions = StorageNMap< - _, - ( - NMapKey, // Subnet ID - NMapKey, // Account ID - NMapKey, // Position ID - ), - Position, - OptionQuery, - >; - - /// Position ID counter. + /// Total fees in TAO per subnet due to be paid to users / protocol #[pallet::storage] - pub type LastPositionId = StorageValue<_, u128, ValueQuery>; + pub type FeesTao = StorageMap<_, Twox64Concat, NetUid, TaoCurrency, ValueQuery>; - /// Tick index bitmap words storage - #[pallet::storage] - pub type TickIndexBitmapWords = StorageNMap< - _, - ( - NMapKey, // Subnet ID - NMapKey, // Layer level - NMapKey, // word index - ), - u128, - ValueQuery, - >; - - /// TAO reservoir for scraps of protocol claimed fees. + /// Total fees in Alpha per subnet due to be paid to users / protocol #[pallet::storage] - pub type ScrapReservoirTao = StorageMap<_, Twox64Concat, NetUid, TaoCurrency, ValueQuery>; + pub type FeesAlpha = StorageMap<_, Twox64Concat, NetUid, AlphaCurrency, ValueQuery>; - /// Alpha reservoir for scraps of protocol claimed fees. + /// --- Storage for migration run status #[pallet::storage] - pub type ScrapReservoirAlpha = - StorageMap<_, Twox64Concat, NetUid, AlphaCurrency, ValueQuery>; + pub type HasMigrationRun = + StorageMap<_, Identity, BoundedVec, bool, ValueQuery>; #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event { /// Event emitted when the fee rate has been updated for a subnet FeeRateSet { netuid: NetUid, rate: u16 }, - - /// Event emitted when user liquidity operations are enabled for a subnet. - /// First enable even indicates a switch from V2 to V3 swap. - UserLiquidityToggled { netuid: NetUid, enable: bool }, - - /// Event emitted when a liquidity position is added to a subnet's liquidity pool. - LiquidityAdded { - /// The coldkey account that owns the position - coldkey: T::AccountId, - /// The hotkey account where Alpha comes from - hotkey: T::AccountId, - /// The subnet identifier - netuid: NetUid, - /// Unique identifier for the liquidity position - position_id: PositionId, - /// The amount of liquidity added to the position - liquidity: u64, - /// The amount of TAO tokens committed to the position - tao: TaoCurrency, - /// The amount of Alpha tokens committed to the position - alpha: AlphaCurrency, - /// the lower tick - tick_low: TickIndex, - /// the upper tick - tick_high: TickIndex, - }, - - /// Event emitted when a liquidity position is removed from a subnet's liquidity pool. - LiquidityRemoved { - /// The coldkey account that owns the position - coldkey: T::AccountId, - /// The hotkey account where Alpha goes to - hotkey: T::AccountId, - /// The subnet identifier - netuid: NetUid, - /// Unique identifier for the liquidity position - position_id: PositionId, - /// The amount of liquidity removed from the position - liquidity: u64, - /// The amount of TAO tokens returned to the user - tao: TaoCurrency, - /// The amount of Alpha tokens returned to the user - alpha: AlphaCurrency, - /// The amount of TAO fees earned from the position - fee_tao: TaoCurrency, - /// The amount of Alpha fees earned from the position - fee_alpha: AlphaCurrency, - /// the lower tick - tick_low: TickIndex, - /// the upper tick - tick_high: TickIndex, - }, - - /// Event emitted when a liquidity position is modified in a subnet's liquidity pool. - /// Modifying causes the fees to be claimed. - LiquidityModified { - /// The coldkey account that owns the position - coldkey: T::AccountId, - /// The hotkey account where Alpha comes from or goes to - hotkey: T::AccountId, - /// The subnet identifier - netuid: NetUid, - /// Unique identifier for the liquidity position - position_id: PositionId, - /// The amount of liquidity added to or removed from the position - liquidity: i64, - /// The amount of TAO tokens returned to the user - tao: i64, - /// The amount of Alpha tokens returned to the user - alpha: i64, - /// The amount of TAO fees earned from the position - fee_tao: TaoCurrency, - /// The amount of Alpha fees earned from the position - fee_alpha: AlphaCurrency, - /// the lower tick - tick_low: TickIndex, - /// the upper tick - tick_high: TickIndex, - }, } #[pallet::error] @@ -260,18 +141,9 @@ mod pallet { /// The caller does not have enough balance for the operation. InsufficientBalance, - /// Attempted to remove liquidity that does not exist. - LiquidityNotFound, - /// The provided tick range is invalid. InvalidTickRange, - /// Maximum user positions exceeded - MaxPositionsExceeded, - - /// Too many swap steps - TooManySwapSteps, - /// Provided liquidity parameter is invalid (likely too small) InvalidLiquidityValue, @@ -281,11 +153,14 @@ mod pallet { /// The subnet does not exist. MechanismDoesNotExist, - /// User liquidity operations are disabled for this subnet - UserLiquidityDisabled, - /// The subnet does not have subtoken enabled SubtokenDisabled, + + /// Swap reserves are too imbalanced + ReservesOutOfBalance, + + /// The extrinsic is deprecated + Deprecated, } #[pallet::call] @@ -316,315 +191,103 @@ mod pallet { Ok(()) } - /// Enable user liquidity operations for a specific subnet. This switches the - /// subnet from V2 to V3 swap mode. Thereafter, adding new user liquidity can be disabled - /// by toggling this flag to false, but the swap mode will remain V3 because of existing - /// user liquidity until all users withdraw their liquidity. - /// - /// Only sudo or subnet owner can enable user liquidity. - /// Only sudo can disable user liquidity. + /// DEPRECATED #[pallet::call_index(4)] - #[pallet::weight(::WeightInfo::toggle_user_liquidity())] + #[pallet::weight(Weight::from_parts(15_000_000, 0))] + #[deprecated(note = "Deprecated, user liquidity is permanently disabled")] pub fn toggle_user_liquidity( - origin: OriginFor, - netuid: NetUid, - enable: bool, + _origin: OriginFor, + _netuid: NetUid, + _enable: bool, ) -> DispatchResult { - if ensure_root(origin.clone()).is_err() { - let account_id: T::AccountId = ensure_signed(origin)?; - // Only enabling is allowed to subnet owner - ensure!( - T::SubnetInfo::is_owner(&account_id, netuid.into()) && enable, - DispatchError::BadOrigin - ); - } - - ensure!( - T::SubnetInfo::exists(netuid.into()), - Error::::MechanismDoesNotExist - ); - - // EnabledUserLiquidity::::insert(netuid, enable); - - // Self::deposit_event(Event::UserLiquidityToggled { netuid, enable }); - - Ok(()) + Err(Error::::Deprecated.into()) } - /// Add liquidity to a specific price range for a subnet. - /// - /// Parameters: - /// - origin: The origin of the transaction - /// - netuid: Subnet ID - /// - tick_low: Lower bound of the price range - /// - tick_high: Upper bound of the price range - /// - liquidity: Amount of liquidity to add - /// - /// Emits `Event::LiquidityAdded` on success + /// DEPRECATED #[pallet::call_index(1)] - #[pallet::weight(::WeightInfo::add_liquidity())] + #[pallet::weight(Weight::from_parts(15_000_000, 0))] + #[deprecated(note = "Deprecated, user liquidity is permanently disabled")] pub fn add_liquidity( - origin: OriginFor, + _origin: OriginFor, _hotkey: T::AccountId, _netuid: NetUid, _tick_low: TickIndex, _tick_high: TickIndex, _liquidity: u64, ) -> DispatchResult { - ensure_signed(origin)?; - - // Extrinsic should have no effect. This fix may have to be reverted later, - // so leaving the code in for now. - - // // Ensure that the subnet exists. - // ensure!( - // T::SubnetInfo::exists(netuid.into()), - // Error::::MechanismDoesNotExist - // ); - - // ensure!( - // T::SubnetInfo::is_subtoken_enabled(netuid.into()), - // Error::::SubtokenDisabled - // ); - - // let (position_id, tao, alpha) = Self::do_add_liquidity( - // netuid.into(), - // &coldkey, - // &hotkey, - // tick_low, - // tick_high, - // liquidity, - // )?; - // let alpha = AlphaCurrency::from(alpha); - // let tao = TaoCurrency::from(tao); - - // // Remove TAO and Alpha balances or fail transaction if they can't be removed exactly - // let tao_provided = T::BalanceOps::decrease_balance(&coldkey, tao)?; - // ensure!(tao_provided == tao, Error::::InsufficientBalance); - - // let alpha_provided = - // T::BalanceOps::decrease_stake(&coldkey, &hotkey, netuid.into(), alpha)?; - // ensure!(alpha_provided == alpha, Error::::InsufficientBalance); - - // // Add provided liquidity to user-provided reserves - // T::TaoReserve::increase_provided(netuid.into(), tao_provided); - // T::AlphaReserve::increase_provided(netuid.into(), alpha_provided); - - // // Emit an event - // Self::deposit_event(Event::LiquidityAdded { - // coldkey, - // hotkey, - // netuid, - // position_id, - // liquidity, - // tao, - // alpha, - // tick_low, - // tick_high, - // }); - - // Ok(()) - - Err(Error::::UserLiquidityDisabled.into()) + Err(Error::::Deprecated.into()) } - /// Remove liquidity from a specific position. - /// - /// Parameters: - /// - origin: The origin of the transaction - /// - netuid: Subnet ID - /// - position_id: ID of the position to remove - /// - /// Emits `Event::LiquidityRemoved` on success + /// DEPRECATED #[pallet::call_index(2)] - #[pallet::weight(::WeightInfo::remove_liquidity())] + #[pallet::weight(Weight::from_parts(15_000_000, 0))] + #[deprecated(note = "Deprecated, user liquidity is permanently disabled")] pub fn remove_liquidity( - origin: OriginFor, - hotkey: T::AccountId, - netuid: NetUid, - position_id: PositionId, + _origin: OriginFor, + _hotkey: T::AccountId, + _netuid: NetUid, + _position_id: PositionId, ) -> DispatchResult { - let coldkey = ensure_signed(origin)?; - - // Ensure that the subnet exists. - ensure!( - T::SubnetInfo::exists(netuid.into()), - Error::::MechanismDoesNotExist - ); - - // Remove liquidity - let result = Self::do_remove_liquidity(netuid, &coldkey, position_id)?; - - // Credit the returned tao and alpha to the account - T::BalanceOps::increase_balance(&coldkey, result.tao.saturating_add(result.fee_tao)); - T::BalanceOps::increase_stake( - &coldkey, - &hotkey, - netuid.into(), - result.alpha.saturating_add(result.fee_alpha), - )?; - - // Remove withdrawn liquidity from user-provided reserves - T::TaoReserve::decrease_provided(netuid.into(), result.tao); - T::AlphaReserve::decrease_provided(netuid.into(), result.alpha); - - // Emit an event - Self::deposit_event(Event::LiquidityRemoved { - coldkey, - hotkey, - netuid: netuid.into(), - position_id, - liquidity: result.liquidity, - tao: result.tao, - alpha: result.alpha, - fee_tao: result.fee_tao, - fee_alpha: result.fee_alpha, - tick_low: result.tick_low.into(), - tick_high: result.tick_high.into(), - }); - - Ok(()) + Err(Error::::Deprecated.into()) } - /// Modify a liquidity position. - /// - /// Parameters: - /// - origin: The origin of the transaction - /// - netuid: Subnet ID - /// - position_id: ID of the position to remove - /// - liquidity_delta: Liquidity to add (if positive) or remove (if negative) - /// - /// Emits `Event::LiquidityRemoved` on success + /// DEPRECATED #[pallet::call_index(3)] - #[pallet::weight(::WeightInfo::modify_position())] + #[pallet::weight(Weight::from_parts(15_000_000, 0))] + #[deprecated(note = "Deprecated, user liquidity is permanently disabled")] pub fn modify_position( - origin: OriginFor, - hotkey: T::AccountId, - netuid: NetUid, - position_id: PositionId, - liquidity_delta: i64, + _origin: OriginFor, + _hotkey: T::AccountId, + _netuid: NetUid, + _position_id: PositionId, + _liquidity_delta: i64, ) -> DispatchResult { - let coldkey = ensure_signed(origin)?; - - // Ensure that the subnet exists. - ensure!( - T::SubnetInfo::exists(netuid.into()), - Error::::MechanismDoesNotExist - ); - - ensure!( - T::SubnetInfo::is_subtoken_enabled(netuid.into()), - Error::::SubtokenDisabled - ); - - // Add or remove liquidity - let result = - Self::do_modify_position(netuid, &coldkey, &hotkey, position_id, liquidity_delta)?; - - if liquidity_delta > 0 { - // Remove TAO and Alpha balances or fail transaction if they can't be removed exactly - let tao_provided = T::BalanceOps::decrease_balance(&coldkey, result.tao)?; - ensure!(tao_provided == result.tao, Error::::InsufficientBalance); - - let alpha_provided = - T::BalanceOps::decrease_stake(&coldkey, &hotkey, netuid.into(), result.alpha)?; - ensure!( - alpha_provided == result.alpha, - Error::::InsufficientBalance - ); - - // Emit an event - Self::deposit_event(Event::LiquidityModified { - coldkey: coldkey.clone(), - hotkey: hotkey.clone(), - netuid, - position_id, - liquidity: liquidity_delta, - tao: result.tao.to_u64() as i64, - alpha: result.alpha.to_u64() as i64, - fee_tao: result.fee_tao, - fee_alpha: result.fee_alpha, - tick_low: result.tick_low, - tick_high: result.tick_high, - }); - } else { - // Credit the returned tao and alpha to the account - T::BalanceOps::increase_balance(&coldkey, result.tao); - T::BalanceOps::increase_stake(&coldkey, &hotkey, netuid.into(), result.alpha)?; - - // Emit an event - if result.removed { - Self::deposit_event(Event::LiquidityRemoved { - coldkey: coldkey.clone(), - hotkey: hotkey.clone(), - netuid, - position_id, - liquidity: liquidity_delta.unsigned_abs(), - tao: result.tao, - alpha: result.alpha, - fee_tao: result.fee_tao, - fee_alpha: result.fee_alpha, - tick_low: result.tick_low, - tick_high: result.tick_high, - }); - } else { - Self::deposit_event(Event::LiquidityModified { - coldkey: coldkey.clone(), - hotkey: hotkey.clone(), - netuid, - position_id, - liquidity: liquidity_delta, - tao: (result.tao.to_u64() as i64).neg(), - alpha: (result.alpha.to_u64() as i64).neg(), - fee_tao: result.fee_tao, - fee_alpha: result.fee_alpha, - tick_low: result.tick_low, - tick_high: result.tick_high, - }); - } - } - - // Credit accrued fees to user account (no matter if liquidity is added or removed) - if result.fee_tao > TaoCurrency::ZERO { - T::BalanceOps::increase_balance(&coldkey, result.fee_tao); - } - if !result.fee_alpha.is_zero() { - T::BalanceOps::increase_stake( - &coldkey, - &hotkey.clone(), - netuid.into(), - result.fee_alpha, - )?; - } - - Ok(()) + Err(Error::::Deprecated.into()) } - /// Disable user liquidity in all subnets. - /// - /// Emits `Event::UserLiquidityToggled` on success + /// DEPRECATED #[pallet::call_index(5)] - #[pallet::weight(::WeightInfo::modify_position())] - pub fn disable_lp(origin: OriginFor) -> DispatchResult { - ensure_root(origin)?; - - for netuid in 1..=128 { - let netuid = NetUid::from(netuid as u16); - if EnabledUserLiquidity::::get(netuid) { - EnabledUserLiquidity::::insert(netuid, false); - Self::deposit_event(Event::UserLiquidityToggled { - netuid, - enable: false, - }); - } - - // Remove provided liquidity unconditionally because the network may have - // user liquidity previously disabled - // Ignore result to avoid early stopping - let _ = Self::do_dissolve_all_liquidity_providers(netuid); - } - - Ok(()) + #[pallet::weight(Weight::from_parts(15_000_000, 0))] + #[deprecated(note = "Deprecated, user liquidity is permanently disabled")] + pub fn disable_lp(_origin: OriginFor) -> DispatchResult { + Err(Error::::Deprecated.into()) } } } + +/// Struct representing a tick index, DEPRECATED +#[freeze_struct("7c280c2b3bbbb33e")] +#[derive( + Debug, + Default, + Clone, + Copy, + Decode, + Encode, + DecodeWithMemTracking, + TypeInfo, + MaxEncodedLen, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, +)] +pub struct TickIndex(i32); + +/// Struct representing a liquidity position ID, DEPRECATED +#[freeze_struct("e695cd6455c3f0cb")] +#[derive( + Clone, + Copy, + Decode, + DecodeWithMemTracking, + Default, + Encode, + Eq, + MaxEncodedLen, + PartialEq, + RuntimeDebug, + TypeInfo, +)] +pub struct PositionId(u128); diff --git a/pallets/swap/src/pallet/swap_step.rs b/pallets/swap/src/pallet/swap_step.rs index 6791835b1a..a161238300 100644 --- a/pallets/swap/src/pallet/swap_step.rs +++ b/pallets/swap/src/pallet/swap_step.rs @@ -1,14 +1,12 @@ use core::marker::PhantomData; +use frame_support::ensure; use safe_math::*; -use substrate_fixed::types::{I64F64, U64F64}; -use subtensor_runtime_common::{AlphaCurrency, Currency, NetUid, TaoCurrency}; +use sp_core::Get; +use substrate_fixed::types::U64F64; +use subtensor_runtime_common::{AlphaCurrency, Currency, CurrencyReserve, NetUid, TaoCurrency}; use super::pallet::*; -use crate::{ - SqrtPrice, - tick::{ActiveTickIndexManager, TickIndex}, -}; /// A struct representing a single swap step with all its parameters and state pub(crate) struct BasicSwapStep @@ -20,22 +18,16 @@ where // Input parameters netuid: NetUid, drop_fees: bool, + requested_delta_in: PaidIn, + limit_price: U64F64, - // Computed values - current_liquidity: U64F64, - possible_delta_in: PaidIn, - - // Ticks and prices (current, limit, edge, target) - target_sqrt_price: SqrtPrice, - limit_sqrt_price: SqrtPrice, - current_sqrt_price: SqrtPrice, - edge_sqrt_price: SqrtPrice, - edge_tick: TickIndex, + // Intermediate calculations + target_price: U64F64, + current_price: U64F64, // Result values - action: SwapStepAction, delta_in: PaidIn, - final_price: SqrtPrice, + final_price: U64F64, fee: PaidIn, _phantom: PhantomData<(T, PaidIn, PaidOut)>, @@ -52,36 +44,25 @@ where pub(crate) fn new( netuid: NetUid, amount_remaining: PaidIn, - limit_sqrt_price: SqrtPrice, + limit_price: U64F64, drop_fees: bool, ) -> Self { - // Calculate prices and ticks - let current_tick = CurrentTick::::get(netuid); - let current_sqrt_price = AlphaSqrtPrice::::get(netuid); - let edge_tick = Self::tick_edge(netuid, current_tick); - let edge_sqrt_price = edge_tick.as_sqrt_price_bounded(); - let fee = Pallet::::calculate_fee_amount(netuid, amount_remaining, drop_fees); - let possible_delta_in = amount_remaining.saturating_sub(fee); + let requested_delta_in = amount_remaining.saturating_sub(fee); - // Target price and quantities - let current_liquidity = U64F64::saturating_from_num(CurrentLiquidity::::get(netuid)); - let target_sqrt_price = - Self::sqrt_price_target(current_liquidity, current_sqrt_price, possible_delta_in); + // Target and current prices + let target_price = Self::price_target(netuid, requested_delta_in); + let current_price = Pallet::::current_price(netuid); Self { netuid, drop_fees, - target_sqrt_price, - limit_sqrt_price, - current_sqrt_price, - edge_sqrt_price, - edge_tick, - possible_delta_in, - current_liquidity, - action: SwapStepAction::Stop, + requested_delta_in, + limit_price, + target_price, + current_price, delta_in: PaidIn::ZERO, - final_price: target_sqrt_price, + final_price: target_price, fee, _phantom: PhantomData, } @@ -98,64 +79,25 @@ where let mut recalculate_fee = false; // Calculate the stopping price: The price at which we either reach the limit price, - // exchange the full amount, or reach the edge price. - if Self::price_is_closer(&self.target_sqrt_price, &self.limit_sqrt_price) - && Self::price_is_closer(&self.target_sqrt_price, &self.edge_sqrt_price) - { - // Case 1. target_quantity is the lowest - // The trade completely happens within one tick, no tick crossing happens. - self.action = SwapStepAction::Stop; - self.final_price = self.target_sqrt_price; - self.delta_in = self.possible_delta_in; - } else if Self::price_is_closer(&self.limit_sqrt_price, &self.target_sqrt_price) - && Self::price_is_closer(&self.limit_sqrt_price, &self.edge_sqrt_price) - { - // Case 2. lim_quantity is the lowest - // The trade also completely happens within one tick, no tick crossing happens. - self.action = SwapStepAction::Stop; - self.final_price = self.limit_sqrt_price; - self.delta_in = Self::delta_in( - self.current_liquidity, - self.current_sqrt_price, - self.limit_sqrt_price, - ); - recalculate_fee = true; + // or exchange the full amount. + if Self::price_is_closer(&self.target_price, &self.limit_price) { + // Case 1. target_quantity is the lowest, execute in full + self.final_price = self.target_price; + self.delta_in = self.requested_delta_in; } else { - // Case 3. edge_quantity is the lowest - // Tick crossing is likely - self.action = SwapStepAction::Crossing; - self.delta_in = Self::delta_in( - self.current_liquidity, - self.current_sqrt_price, - self.edge_sqrt_price, - ); - self.final_price = self.edge_sqrt_price; + // Case 2. lim_quantity is the lowest + self.final_price = self.limit_price; + self.delta_in = Self::delta_in(self.netuid, self.current_price, self.limit_price); recalculate_fee = true; } - log::trace!("\tAction : {:?}", self.action); - log::trace!( - "\tCurrent Price : {}", - self.current_sqrt_price - .saturating_mul(self.current_sqrt_price) - ); - log::trace!( - "\tTarget Price : {}", - self.target_sqrt_price - .saturating_mul(self.target_sqrt_price) - ); - log::trace!( - "\tLimit Price : {}", - self.limit_sqrt_price.saturating_mul(self.limit_sqrt_price) - ); - log::trace!( - "\tEdge Price : {}", - self.edge_sqrt_price.saturating_mul(self.edge_sqrt_price) - ); + log::trace!("\tCurrent Price : {}", self.current_price); + log::trace!("\tTarget Price : {}", self.target_price); + log::trace!("\tLimit Price : {}", self.limit_price); log::trace!("\tDelta In : {}", self.delta_in); // Because on step creation we calculate fee off the total amount, we might need to - // recalculate it in case if we hit the limit price or the edge price. + // recalculate it in case if we hit the limit price. if recalculate_fee { let u16_max = U64F64::saturating_from_num(u16::MAX); let fee_rate = if self.drop_fees { @@ -169,325 +111,131 @@ where .saturating_to_num::() .into(); } - - // Now correct the action if we stopped exactly at the edge no matter what was the case - // above. Because order type buy moves the price up and tick semi-open interval doesn't - // include its right point, we cross on buys and stop on sells. - let natural_reason_stop_price = - if Self::price_is_closer(&self.limit_sqrt_price, &self.target_sqrt_price) { - self.limit_sqrt_price - } else { - self.target_sqrt_price - }; - if natural_reason_stop_price == self.edge_sqrt_price { - self.action = Self::action_on_edge_sqrt_price(); - } } /// Process a single step of a swap fn process_swap(&self) -> Result, Error> { - // Hold the fees - Self::add_fees( - self.netuid, - Pallet::::current_liquidity_safe(self.netuid), - self.fee, - ); + // Convert amounts, actual swap happens here let delta_out = Self::convert_deltas(self.netuid, self.delta_in); - // log::trace!("\tDelta Out : {delta_out:?}"); - - if self.action == SwapStepAction::Crossing { - let mut tick = Ticks::::get(self.netuid, self.edge_tick).unwrap_or_default(); - tick.fees_out_tao = I64F64::saturating_from_num(FeeGlobalTao::::get(self.netuid)) - .saturating_sub(tick.fees_out_tao); - tick.fees_out_alpha = - I64F64::saturating_from_num(FeeGlobalAlpha::::get(self.netuid)) - .saturating_sub(tick.fees_out_alpha); - Self::update_liquidity_at_crossing(self.netuid)?; - Ticks::::insert(self.netuid, self.edge_tick, tick); + log::trace!("\tDelta Out : {delta_out}"); + let mut fee_to_block_author = 0.into(); + if self.delta_in > 0.into() { + ensure!(delta_out > 0.into(), Error::::ReservesTooLow); + + // Split fees according to DefaultFeeSplit between liquidity pool and + // validators. In case we want just to forward 100% of fees to the block + // author, it can be done this way: + // ``` + // fee_to_block_author = self.fee; + // ``` + let fee_split = DefaultFeeSplit::get(); + let lp_fee = fee_split.mul_floor(self.fee.to_u64()).into(); + Self::add_fees(self.netuid, lp_fee); + fee_to_block_author = self.fee.saturating_sub(lp_fee); } - // Update current price - AlphaSqrtPrice::::set(self.netuid, self.final_price); - - // Update current tick - let new_current_tick = TickIndex::from_sqrt_price_bounded(self.final_price); - CurrentTick::::set(self.netuid, new_current_tick); - Ok(SwapStepResult { - amount_to_take: self.delta_in.saturating_add(self.fee), fee_paid: self.fee, delta_in: self.delta_in, delta_out, + fee_to_block_author, }) } - - pub(crate) fn action(&self) -> SwapStepAction { - self.action - } } impl SwapStep for BasicSwapStep { - fn delta_in( - liquidity_curr: U64F64, - sqrt_price_curr: SqrtPrice, - sqrt_price_target: SqrtPrice, - ) -> TaoCurrency { - liquidity_curr - .saturating_mul(sqrt_price_target.saturating_sub(sqrt_price_curr)) - .saturating_to_num::() - .into() + fn delta_in(netuid: NetUid, price_curr: U64F64, price_target: U64F64) -> TaoCurrency { + let tao_reserve = T::TaoReserve::reserve(netuid.into()); + let balancer = SwapBalancer::::get(netuid); + TaoCurrency::from(balancer.calculate_quote_delta_in( + price_curr, + price_target, + tao_reserve.into(), + )) } - fn tick_edge(netuid: NetUid, current_tick: TickIndex) -> TickIndex { - ActiveTickIndexManager::::find_closest_higher( - netuid, - current_tick.next().unwrap_or(TickIndex::MAX), + fn price_target(netuid: NetUid, delta_in: TaoCurrency) -> U64F64 { + let tao_reserve = T::TaoReserve::reserve(netuid.into()); + let alpha_reserve = T::AlphaReserve::reserve(netuid.into()); + let balancer = SwapBalancer::::get(netuid); + let dy = delta_in; + let dx = Self::convert_deltas(netuid, dy); + balancer.calculate_price( + u64::from(alpha_reserve.saturating_sub(dx)), + u64::from(tao_reserve.saturating_add(dy)), ) - .unwrap_or(TickIndex::MAX) - } - - fn sqrt_price_target( - liquidity_curr: U64F64, - sqrt_price_curr: SqrtPrice, - delta_in: TaoCurrency, - ) -> SqrtPrice { - let delta_fixed = U64F64::saturating_from_num(delta_in); - - // No liquidity means that price should go to the limit - if liquidity_curr == 0 { - return SqrtPrice::saturating_from_num( - Pallet::::max_price_inner::().to_u64(), - ); - } - - delta_fixed - .safe_div(liquidity_curr) - .saturating_add(sqrt_price_curr) } - fn price_is_closer(sq_price1: &SqrtPrice, sq_price2: &SqrtPrice) -> bool { - sq_price1 <= sq_price2 + fn price_is_closer(price1: &U64F64, price2: &U64F64) -> bool { + price1 <= price2 } - fn action_on_edge_sqrt_price() -> SwapStepAction { - SwapStepAction::Crossing - } - - fn add_fees(netuid: NetUid, current_liquidity: U64F64, fee: TaoCurrency) { - if current_liquidity == 0 { - return; - } - - let fee_fixed = U64F64::saturating_from_num(fee.to_u64()); - - FeeGlobalTao::::mutate(netuid, |value| { - *value = value.saturating_add(fee_fixed.safe_div(current_liquidity)) - }); + fn add_fees(netuid: NetUid, fee: TaoCurrency) { + FeesTao::::mutate(netuid, |total| *total = total.saturating_add(fee)) } fn convert_deltas(netuid: NetUid, delta_in: TaoCurrency) -> AlphaCurrency { - // Skip conversion if delta_in is zero - if delta_in.is_zero() { - return AlphaCurrency::ZERO; - } - - let liquidity_curr = SqrtPrice::saturating_from_num(CurrentLiquidity::::get(netuid)); - let sqrt_price_curr = AlphaSqrtPrice::::get(netuid); - let delta_fixed = SqrtPrice::saturating_from_num(delta_in.to_u64()); - - // Calculate result based on order type with proper fixed-point math - // Using safe math operations throughout to prevent overflows - let result = { - // (liquidity_curr * sqrt_price_curr + delta_fixed) * sqrt_price_curr; - let a = liquidity_curr - .saturating_mul(sqrt_price_curr) - .saturating_add(delta_fixed) - .saturating_mul(sqrt_price_curr); - // liquidity_curr / a; - let b = liquidity_curr.safe_div(a); - // b * delta_fixed; - b.saturating_mul(delta_fixed) - }; - - result.saturating_to_num::().into() - } - - fn update_liquidity_at_crossing(netuid: NetUid) -> Result<(), Error> { - let mut liquidity_curr = CurrentLiquidity::::get(netuid); - let current_tick_index = TickIndex::current_bounded::(netuid); - - // Find the appropriate tick based on order type - let tick = { - // Self::find_closest_higher_active_tick(netuid, current_tick_index), - let upper_tick = ActiveTickIndexManager::::find_closest_higher( - netuid, - current_tick_index.next().unwrap_or(TickIndex::MAX), - ) - .unwrap_or(TickIndex::MAX); - Ticks::::get(netuid, upper_tick) - } - .ok_or(Error::::InsufficientLiquidity)?; - - let liquidity_update_abs_u64 = tick.liquidity_net_as_u64(); - - // Update liquidity based on the sign of liquidity_net and the order type - liquidity_curr = if tick.liquidity_net >= 0 { - liquidity_curr.saturating_add(liquidity_update_abs_u64) - } else { - liquidity_curr.saturating_sub(liquidity_update_abs_u64) - }; - - CurrentLiquidity::::set(netuid, liquidity_curr); - - Ok(()) + let alpha_reserve = T::AlphaReserve::reserve(netuid.into()); + let tao_reserve = T::TaoReserve::reserve(netuid.into()); + let balancer = SwapBalancer::::get(netuid); + let e = balancer.exp_quote_base(tao_reserve.into(), delta_in.into()); + let one = U64F64::from_num(1); + let alpha_reserve_fixed = U64F64::from_num(alpha_reserve); + AlphaCurrency::from( + alpha_reserve_fixed + .saturating_mul(one.saturating_sub(e)) + .saturating_to_num::(), + ) } } impl SwapStep for BasicSwapStep { - fn delta_in( - liquidity_curr: U64F64, - sqrt_price_curr: SqrtPrice, - sqrt_price_target: SqrtPrice, - ) -> AlphaCurrency { - let one = U64F64::saturating_from_num(1); - - liquidity_curr - .saturating_mul( - one.safe_div(sqrt_price_target.into()) - .saturating_sub(one.safe_div(sqrt_price_curr)), - ) - .saturating_to_num::() - .into() + fn delta_in(netuid: NetUid, price_curr: U64F64, price_target: U64F64) -> AlphaCurrency { + let alpha_reserve = T::AlphaReserve::reserve(netuid); + let balancer = SwapBalancer::::get(netuid); + AlphaCurrency::from(balancer.calculate_base_delta_in( + price_curr, + price_target, + alpha_reserve.into(), + )) } - fn tick_edge(netuid: NetUid, current_tick: TickIndex) -> TickIndex { - let current_price: SqrtPrice = AlphaSqrtPrice::::get(netuid); - let current_tick_price = current_tick.as_sqrt_price_bounded(); - let is_active = ActiveTickIndexManager::::tick_is_active(netuid, current_tick); - - if is_active && current_price > current_tick_price { - return ActiveTickIndexManager::::find_closest_lower(netuid, current_tick) - .unwrap_or(TickIndex::MIN); - } - - ActiveTickIndexManager::::find_closest_lower( - netuid, - current_tick.prev().unwrap_or(TickIndex::MIN), + fn price_target(netuid: NetUid, delta_in: AlphaCurrency) -> U64F64 { + let tao_reserve = T::TaoReserve::reserve(netuid.into()); + let alpha_reserve = T::AlphaReserve::reserve(netuid.into()); + let balancer = SwapBalancer::::get(netuid); + let dx = delta_in; + let dy = Self::convert_deltas(netuid, dx); + balancer.calculate_price( + u64::from(alpha_reserve.saturating_add(dx)), + u64::from(tao_reserve.saturating_sub(dy)), ) - .unwrap_or(TickIndex::MIN) } - fn sqrt_price_target( - liquidity_curr: U64F64, - sqrt_price_curr: SqrtPrice, - delta_in: AlphaCurrency, - ) -> SqrtPrice { - let delta_fixed = U64F64::saturating_from_num(delta_in); - let one = U64F64::saturating_from_num(1); - - // No liquidity means that price should go to the limit - if liquidity_curr == 0 { - return SqrtPrice::saturating_from_num( - Pallet::::min_price_inner::().to_u64(), - ); - } - - one.safe_div( - delta_fixed - .safe_div(liquidity_curr) - .saturating_add(one.safe_div(sqrt_price_curr)), - ) - } - - fn price_is_closer(sq_price1: &SqrtPrice, sq_price2: &SqrtPrice) -> bool { - sq_price1 >= sq_price2 + fn price_is_closer(price1: &U64F64, price2: &U64F64) -> bool { + price1 >= price2 } - fn action_on_edge_sqrt_price() -> SwapStepAction { - SwapStepAction::Stop - } - - fn add_fees(netuid: NetUid, current_liquidity: U64F64, fee: AlphaCurrency) { - if current_liquidity == 0 { - return; - } - - let fee_fixed = U64F64::saturating_from_num(fee.to_u64()); - - FeeGlobalAlpha::::mutate(netuid, |value| { - *value = value.saturating_add(fee_fixed.safe_div(current_liquidity)) - }); + fn add_fees(netuid: NetUid, fee: AlphaCurrency) { + FeesAlpha::::mutate(netuid, |total| *total = total.saturating_add(fee)) } fn convert_deltas(netuid: NetUid, delta_in: AlphaCurrency) -> TaoCurrency { - // Skip conversion if delta_in is zero - if delta_in.is_zero() { - return TaoCurrency::ZERO; - } - - let liquidity_curr = SqrtPrice::saturating_from_num(CurrentLiquidity::::get(netuid)); - let sqrt_price_curr = AlphaSqrtPrice::::get(netuid); - let delta_fixed = SqrtPrice::saturating_from_num(delta_in.to_u64()); - - // Calculate result based on order type with proper fixed-point math - // Using safe math operations throughout to prevent overflows - let result = { - // liquidity_curr / (liquidity_curr / sqrt_price_curr + delta_fixed); - let denom = liquidity_curr - .safe_div(sqrt_price_curr) - .saturating_add(delta_fixed); - let a = liquidity_curr.safe_div(denom); - // a * sqrt_price_curr; - let b = a.saturating_mul(sqrt_price_curr); - - // delta_fixed * b; - delta_fixed.saturating_mul(b) - }; - - result.saturating_to_num::().into() - } - - fn update_liquidity_at_crossing(netuid: NetUid) -> Result<(), Error> { - let mut liquidity_curr = CurrentLiquidity::::get(netuid); - let current_tick_index = TickIndex::current_bounded::(netuid); - - // Find the appropriate tick based on order type - let tick = { - // Self::find_closest_lower_active_tick(netuid, current_tick_index) - let current_price = AlphaSqrtPrice::::get(netuid); - let current_tick_price = current_tick_index.as_sqrt_price_bounded(); - let is_active = ActiveTickIndexManager::::tick_is_active(netuid, current_tick_index); - - let lower_tick = if is_active && current_price > current_tick_price { - ActiveTickIndexManager::::find_closest_lower(netuid, current_tick_index) - .unwrap_or(TickIndex::MIN) - } else { - ActiveTickIndexManager::::find_closest_lower( - netuid, - current_tick_index.prev().unwrap_or(TickIndex::MIN), - ) - .unwrap_or(TickIndex::MIN) - }; - Ticks::::get(netuid, lower_tick) - } - .ok_or(Error::::InsufficientLiquidity)?; - - let liquidity_update_abs_u64 = tick.liquidity_net_as_u64(); - - // Update liquidity based on the sign of liquidity_net and the order type - liquidity_curr = if tick.liquidity_net >= 0 { - liquidity_curr.saturating_sub(liquidity_update_abs_u64) - } else { - liquidity_curr.saturating_add(liquidity_update_abs_u64) - }; - - CurrentLiquidity::::set(netuid, liquidity_curr); - - Ok(()) + let alpha_reserve = T::AlphaReserve::reserve(netuid.into()); + let tao_reserve = T::TaoReserve::reserve(netuid.into()); + let balancer = SwapBalancer::::get(netuid); + let e = balancer.exp_base_quote(alpha_reserve.into(), delta_in.into()); + let one = U64F64::from_num(1); + let tao_reserve_fixed = U64F64::from_num(u64::from(tao_reserve)); + TaoCurrency::from( + tao_reserve_fixed + .saturating_mul(one.saturating_sub(e)) + .saturating_to_num::(), + ) } } @@ -498,49 +246,25 @@ where PaidOut: Currency, { /// Get the input amount needed to reach the target price - fn delta_in( - liquidity_curr: U64F64, - sqrt_price_curr: SqrtPrice, - sqrt_price_target: SqrtPrice, - ) -> PaidIn; + fn delta_in(netuid: NetUid, price_curr: U64F64, price_target: U64F64) -> PaidIn; - /// Get the tick at the current tick edge. - /// - /// If anything is wrong with tick math and it returns Err, we just abort the deal, i.e. return - /// the edge that is impossible to execute - fn tick_edge(netuid: NetUid, current_tick: TickIndex) -> TickIndex; + /// Get the target price based on the input amount + fn price_target(netuid: NetUid, delta_in: PaidIn) -> U64F64; - /// Get the target square root price based on the input amount - /// - /// This is the price that would be reached if - /// - There are no liquidity positions other than protocol liquidity - /// - Full delta_in amount is executed - fn sqrt_price_target( - liquidity_curr: U64F64, - sqrt_price_curr: SqrtPrice, - delta_in: PaidIn, - ) -> SqrtPrice; - - /// Returns True if sq_price1 is closer to the current price than sq_price2 + /// Returns True if price1 is closer to the current price than price2 /// in terms of order direction. - /// For buying: sq_price1 <= sq_price2 - /// For selling: sq_price1 >= sq_price2 - fn price_is_closer(sq_price1: &SqrtPrice, sq_price2: &SqrtPrice) -> bool; - - /// Get swap step action on the edge sqrt price. - fn action_on_edge_sqrt_price() -> SwapStepAction; + /// For buying: price1 <= price2 + /// For selling: price1 >= price2 + fn price_is_closer(price1: &U64F64, price2: &U64F64) -> bool; /// Add fees to the global fee counters - fn add_fees(netuid: NetUid, current_liquidity: U64F64, fee: PaidIn); + fn add_fees(netuid: NetUid, fee: PaidIn); /// Convert input amount (delta_in) to output amount (delta_out) /// - /// This is the core method of uniswap V3 that tells how much output token is given for an - /// amount of input token within one price tick. + /// This is the core method of the swap that tells how much output token is given for an + /// amount of input token fn convert_deltas(netuid: NetUid, delta_in: PaidIn) -> PaidOut; - - /// Update liquidity when crossing a tick - fn update_liquidity_at_crossing(netuid: NetUid) -> Result<(), Error>; } #[derive(Debug, PartialEq)] @@ -549,14 +273,8 @@ where PaidIn: Currency, PaidOut: Currency, { - pub(crate) amount_to_take: PaidIn, pub(crate) fee_paid: PaidIn, pub(crate) delta_in: PaidIn, pub(crate) delta_out: PaidOut, -} - -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum SwapStepAction { - Crossing, - Stop, + pub(crate) fee_to_block_author: PaidIn, } diff --git a/pallets/swap/src/pallet/tests.rs b/pallets/swap/src/pallet/tests.rs index cd18e665cd..912c89bbe8 100644 --- a/pallets/swap/src/pallet/tests.rs +++ b/pallets/swap/src/pallet/tests.rs @@ -6,76 +6,30 @@ )] use approx::assert_abs_diff_eq; -use frame_support::{assert_err, assert_noop, assert_ok}; -use sp_arithmetic::helpers_128bit; +use frame_support::{assert_noop, assert_ok}; +use sp_arithmetic::Perquintill; use sp_runtime::DispatchError; -use substrate_fixed::types::U96F32; -use subtensor_runtime_common::NetUid; +use substrate_fixed::types::U64F64; +use subtensor_runtime_common::{Currency, NetUid}; use subtensor_swap_interface::Order as OrderT; use super::*; +use crate::mock::*; use crate::pallet::swap_step::*; -use crate::{SqrtPrice, mock::*}; - -// this function is used to convert price (NON-SQRT price!) to TickIndex. it's only utility for -// testing, all the implementation logic is based on sqrt prices -fn price_to_tick(price: f64) -> TickIndex { - let price_sqrt: SqrtPrice = SqrtPrice::from_num(price.sqrt()); - // Handle potential errors in the conversion - match TickIndex::try_from_sqrt_price(price_sqrt) { - Ok(mut tick) => { - // Ensure the tick is within bounds - if tick > TickIndex::MAX { - tick = TickIndex::MAX; - } else if tick < TickIndex::MIN { - tick = TickIndex::MIN; - } - tick - } - // Default to a reasonable value when conversion fails - Err(_) => { - if price > 1.0 { - TickIndex::MAX - } else { - TickIndex::MIN - } - } - } -} - -fn get_ticked_prices_around_current_price() -> (f64, f64) { - // Get current price, ticks around it, and prices on the tick edges for test cases - let netuid = NetUid::from(1); - assert_ok!(Pallet::::maybe_initialize_v3(netuid)); - let current_tick = CurrentTick::::get(netuid); - // Low and high prices that match to a lower and higher tick that doesn't contain the current price - let current_price_low_sqrt = current_tick.as_sqrt_price_bounded(); - let current_price_high_sqrt = current_tick.next().unwrap().as_sqrt_price_bounded(); - let current_price_low = U96F32::from_num(current_price_low_sqrt * current_price_low_sqrt); - let current_price_high = U96F32::from_num(current_price_high_sqrt * current_price_high_sqrt); +// Run all tests: +// cargo test --package pallet-subtensor-swap --lib -- pallet::tests --nocapture - ( - current_price_low.to_num::(), - current_price_high.to_num::() + 0.000000001, - ) +#[allow(dead_code)] +fn get_min_price() -> U64F64 { + U64F64::from_num(Pallet::::min_price_inner::()) + / U64F64::from_num(1_000_000_000) } -// this function is used to convert tick index NON-SQRT (!) price. it's only utility for -// testing, all the implementation logic is based on sqrt prices -fn tick_to_price(tick: TickIndex) -> f64 { - // Handle errors gracefully - match tick.try_to_sqrt_price() { - Ok(price_sqrt) => (price_sqrt * price_sqrt).to_num::(), - Err(_) => { - // Return a sensible default based on whether the tick is above or below the valid range - if tick > TickIndex::MAX { - tick_to_price(TickIndex::MAX) // Use the max valid tick price - } else { - tick_to_price(TickIndex::MIN) // Use the min valid tick price - } - } - } +#[allow(dead_code)] +fn get_max_price() -> U64F64 { + U64F64::from_num(Pallet::::max_price_inner::()) + / U64F64::from_num(1_000_000_000) } mod dispatchables { @@ -106,641 +60,405 @@ mod dispatchables { }); } - // #[test] - // fn test_toggle_user_liquidity() { - // new_test_ext().execute_with(|| { - // let netuid = NetUid::from(101); - - // assert!(!EnabledUserLiquidity::::get(netuid)); - - // assert_ok!(Swap::toggle_user_liquidity( - // RuntimeOrigin::root(), - // netuid.into(), - // true - // )); - - // assert!(EnabledUserLiquidity::::get(netuid)); - - // assert_noop!( - // Swap::toggle_user_liquidity(RuntimeOrigin::signed(666), netuid.into(), true), - // DispatchError::BadOrigin - // ); - - // assert_ok!(Swap::toggle_user_liquidity( - // RuntimeOrigin::signed(1), - // netuid.into(), - // true - // )); - - // assert_noop!( - // Swap::toggle_user_liquidity( - // RuntimeOrigin::root(), - // NON_EXISTENT_NETUID.into(), - // true - // ), - // Error::::MechanismDoesNotExist - // ); - // }); - // } -} + fn perquintill_to_f64(p: Perquintill) -> f64 { + let parts = p.deconstruct() as f64; + parts / 1_000_000_000_000_000_000_f64 + } -#[test] -fn test_swap_initialization() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); + /// cargo test --package pallet-subtensor-swap --lib -- pallet::tests::dispatchables::test_adjust_protocol_liquidity_happy --exact --nocapture + #[test] + fn test_adjust_protocol_liquidity_happy() { + // test case: tao_delta, alpha_delta + [ + (0_u64, 0_u64), + (0_u64, 1_u64), + (1_u64, 0_u64), + (1_u64, 1_u64), + (0_u64, 10_u64), + (10_u64, 0_u64), + (10_u64, 10_u64), + (0_u64, 100_u64), + (100_u64, 0_u64), + (100_u64, 100_u64), + (0_u64, 1_000_u64), + (1_000_u64, 0_u64), + (1_000_u64, 1_000_u64), + (1_000_000_u64, 0_u64), + (0_u64, 1_000_000_u64), + (1_000_000_u64, 1_000_000_u64), + (1_000_000_000_u64, 0_u64), + (0_u64, 1_000_000_000_u64), + (1_000_000_000_u64, 1_000_000_000_u64), + (1_000_000_000_000_u64, 0_u64), + (0_u64, 1_000_000_000_000_u64), + (1_000_000_000_000_u64, 1_000_000_000_000_u64), + (1_u64, 2_u64), + (2_u64, 1_u64), + (10_u64, 20_u64), + (20_u64, 10_u64), + (100_u64, 200_u64), + (200_u64, 100_u64), + (1_000_u64, 2_000_u64), + (2_000_u64, 1_000_u64), + (1_000_000_u64, 2_000_000_u64), + (2_000_000_u64, 1_000_000_u64), + (1_000_000_000_u64, 2_000_000_000_u64), + (2_000_000_000_u64, 1_000_000_000_u64), + (1_000_000_000_000_u64, 2_000_000_000_000_u64), + (2_000_000_000_000_u64, 1_000_000_000_000_u64), + (1_234_567_u64, 2_432_765_u64), + (1_234_567_u64, 2_432_765_890_u64), + ] + .into_iter() + .for_each(|(tao_delta, alpha_delta)| { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); - // Get reserves from the mock provider - let tao = TaoReserve::reserve(netuid.into()); - let alpha = AlphaReserve::reserve(netuid.into()); + let tao_delta = TaoCurrency::from(tao_delta); + let alpha_delta = AlphaCurrency::from(alpha_delta); - assert_ok!(Pallet::::maybe_initialize_v3(netuid)); + // Initialize reserves and price + let tao = TaoCurrency::from(1_000_000_000_000_u64); + let alpha = AlphaCurrency::from(4_000_000_000_000_u64); + TaoReserve::set_mock_reserve(netuid, tao); + AlphaReserve::set_mock_reserve(netuid, alpha); + let price_before = Swap::current_price(netuid); - assert!(SwapV3Initialized::::get(netuid)); + // Adjust reserves + Swap::adjust_protocol_liquidity(netuid, tao_delta, alpha_delta); + TaoReserve::set_mock_reserve(netuid, tao + tao_delta); + AlphaReserve::set_mock_reserve(netuid, alpha + alpha_delta); - // Verify current price is set - let sqrt_price = AlphaSqrtPrice::::get(netuid); - let expected_sqrt_price = U64F64::from_num(0.5_f64); - assert_abs_diff_eq!( - sqrt_price.to_num::(), - expected_sqrt_price.to_num::(), - epsilon = 0.000000001 - ); + // Check that price didn't change + let price_after = Swap::current_price(netuid); + assert_abs_diff_eq!( + price_before.to_num::(), + price_after.to_num::(), + epsilon = price_before.to_num::() / 1_000_000_000_000. + ); - // Verify that current tick is set - let current_tick = CurrentTick::::get(netuid); - let expected_current_tick = TickIndex::from_sqrt_price_bounded(expected_sqrt_price); - assert_eq!(current_tick, expected_current_tick); - - // Calculate expected liquidity - let expected_liquidity = - helpers_128bit::sqrt((tao.to_u64() as u128).saturating_mul(alpha.to_u64() as u128)) - as u64; - - // Get the protocol account - let protocol_account_id = Pallet::::protocol_account_id(); - - // Verify position created for protocol account - let positions = Positions::::iter_prefix_values((netuid, protocol_account_id)) - .collect::>(); - assert_eq!(positions.len(), 1); - - let position = &positions[0]; - assert_eq!(position.liquidity, expected_liquidity); - assert_eq!(position.tick_low, TickIndex::MIN); - assert_eq!(position.tick_high, TickIndex::MAX); - assert_eq!(position.fees_tao, 0); - assert_eq!(position.fees_alpha, 0); - - // Verify ticks were created - let tick_low = Ticks::::get(netuid, TickIndex::MIN).unwrap(); - let tick_high = Ticks::::get(netuid, TickIndex::MAX).unwrap(); - - // Check liquidity values - assert_eq!(tick_low.liquidity_net, expected_liquidity as i128); - assert_eq!(tick_low.liquidity_gross, expected_liquidity); - assert_eq!(tick_high.liquidity_net, -(expected_liquidity as i128)); - assert_eq!(tick_high.liquidity_gross, expected_liquidity); - - // Verify current liquidity is set - assert_eq!(CurrentLiquidity::::get(netuid), expected_liquidity); - }); -} + // Check that reserve weight was properly updated + let new_tao = u64::from(tao + tao_delta) as f64; + let new_alpha = u64::from(alpha + alpha_delta) as f64; + let expected_quote_weight = + new_tao / (new_alpha * price_before.to_num::() + new_tao); + let expected_quote_weight_delta = expected_quote_weight - 0.5; + let res_weights = SwapBalancer::::get(netuid); + let actual_quote_weight_delta = + perquintill_to_f64(res_weights.get_quote_weight()) - 0.5; + let eps = expected_quote_weight / 1_000_000_000_000.; + assert_abs_diff_eq!( + expected_quote_weight_delta, + actual_quote_weight_delta, + epsilon = eps + ); + }); + }); + } -// Test adding liquidity on top of the existing protocol liquidity -#[test] -fn test_add_liquidity_basic() { - new_test_ext().execute_with(|| { - let min_price = tick_to_price(TickIndex::MIN); - let max_price = tick_to_price(TickIndex::MAX); - let max_tick = price_to_tick(max_price); - assert_eq!(max_tick, TickIndex::MAX); - - assert_ok!(Pallet::::maybe_initialize_v3(NetUid::from(1))); - let current_price = Pallet::::current_price(NetUid::from(1)).to_num::(); - let (current_price_low, current_price_high) = get_ticked_prices_around_current_price(); - - // As a user add liquidity with all possible corner cases - // - Initial price is 0.25 - // - liquidity is expressed in RAO units - // Test case is (price_low, price_high, liquidity, tao, alpha) + /// This test case verifies that small gradual injections (like emissions in every block) + /// in the worst case + /// - Do not cause price to change + /// - Result in the same weight change as one large injection + /// + /// This is a long test that only tests validity of weights math. Run again if changing + /// Balancer::update_weights_for_added_liquidity + /// + /// cargo test --package pallet-subtensor-swap --lib -- pallet::tests::dispatchables::test_adjust_protocol_liquidity_deltas --exact --nocapture + #[ignore] + #[test] + fn test_adjust_protocol_liquidity_deltas() { + // The number of times (blocks) over which gradual injections will be made + // One year price drift due to precision is under 1e-6 + const ITERATIONS: u64 = 2_700_000; + const PRICE_PRECISION: f64 = 0.000_001; + const PREC_LARGE_DELTA: f64 = 0.001; + const WEIGHT_PRECISION: f64 = 0.000_000_000_000_000_001; + + let initial_tao_reserve = TaoCurrency::from(1_000_000_000_000_000_u64); + let initial_alpha_reserve = AlphaCurrency::from(10_000_000_000_000_000_u64); + + // test case: tao_delta, alpha_delta, price_precision [ - // Repeat the protocol liquidity at maximum range: Expect all the same values - ( - min_price, - max_price, - 2_000_000_000_u64, - 1_000_000_000_u64, - 4_000_000_000_u64, - ), - // Repeat the protocol liquidity at current to max range: Expect the same alpha - ( - current_price_high, - max_price, - 2_000_000_000_u64, - 0, - 4_000_000_000, - ), - // Repeat the protocol liquidity at min to current range: Expect all the same tao - ( - min_price, - current_price_low, - 2_000_000_000_u64, - 1_000_000_000, - 0, - ), - // Half to double price - just some sane wothdraw amounts - (0.125, 0.5, 2_000_000_000_u64, 293_000_000, 1_171_000_000), - // Both below price - tao is non-zero, alpha is zero - (0.12, 0.13, 2_000_000_000_u64, 28_270_000, 0), - // Both above price - tao is zero, alpha is non-zero - (0.3, 0.4, 2_000_000_000_u64, 0, 489_200_000), + (0_u64, 0_u64, PRICE_PRECISION), + (0_u64, 1_u64, PRICE_PRECISION), + (1_u64, 0_u64, PRICE_PRECISION), + (1_u64, 1_u64, PRICE_PRECISION), + (0_u64, 10_u64, PRICE_PRECISION), + (10_u64, 0_u64, PRICE_PRECISION), + (10_u64, 10_u64, PRICE_PRECISION), + (0_u64, 100_u64, PRICE_PRECISION), + (100_u64, 0_u64, PRICE_PRECISION), + (100_u64, 100_u64, PRICE_PRECISION), + (0_u64, 987_u64, PRICE_PRECISION), + (987_u64, 0_u64, PRICE_PRECISION), + (876_u64, 987_u64, PRICE_PRECISION), + (0_u64, 1_000_u64, PRICE_PRECISION), + (1_000_u64, 0_u64, PRICE_PRECISION), + (1_000_u64, 1_000_u64, PRICE_PRECISION), + (0_u64, 1_234_u64, PRICE_PRECISION), + (1_234_u64, 0_u64, PRICE_PRECISION), + (1_234_u64, 4_321_u64, PRICE_PRECISION), + (1_234_000_u64, 4_321_000_u64, PREC_LARGE_DELTA), + (1_234_u64, 4_321_000_u64, PREC_LARGE_DELTA), ] .into_iter() - .enumerate() - .map(|(n, v)| (NetUid::from(n as u16 + 1), v.0, v.1, v.2, v.3, v.4)) - .for_each( - |(netuid, price_low, price_high, liquidity, expected_tao, expected_alpha)| { - assert_ok!(Pallet::::maybe_initialize_v3(netuid)); - - // Calculate ticks (assuming tick math is tested separately) - let tick_low = price_to_tick(price_low); - let tick_high = price_to_tick(price_high); - - // Get tick infos and liquidity before adding (to account for protocol liquidity) - let tick_low_info_before = Ticks::::get(netuid, tick_low).unwrap_or_default(); - let tick_high_info_before = - Ticks::::get(netuid, tick_high).unwrap_or_default(); - let liquidity_before = CurrentLiquidity::::get(netuid); - - // Add liquidity - let (position_id, tao, alpha) = Pallet::::do_add_liquidity( - netuid, - &OK_COLDKEY_ACCOUNT_ID, - &OK_HOTKEY_ACCOUNT_ID, - tick_low, - tick_high, - liquidity, - ) - .unwrap(); - - assert_abs_diff_eq!(tao, expected_tao, epsilon = tao / 1000); - assert_abs_diff_eq!(alpha, expected_alpha, epsilon = alpha / 1000); - - // Check that low and high ticks appear in the state and are properly updated - let tick_low_info = Ticks::::get(netuid, tick_low).unwrap(); - let tick_high_info = Ticks::::get(netuid, tick_high).unwrap(); - let expected_liquidity_net_low = liquidity as i128; - let expected_liquidity_gross_low = liquidity; - let expected_liquidity_net_high = -(liquidity as i128); - let expected_liquidity_gross_high = liquidity; - - assert_eq!( - tick_low_info.liquidity_net - tick_low_info_before.liquidity_net, - expected_liquidity_net_low, - ); - assert_eq!( - tick_low_info.liquidity_gross - tick_low_info_before.liquidity_gross, - expected_liquidity_gross_low, - ); - assert_eq!( - tick_high_info.liquidity_net - tick_high_info_before.liquidity_net, - expected_liquidity_net_high, - ); - assert_eq!( - tick_high_info.liquidity_gross - tick_high_info_before.liquidity_gross, - expected_liquidity_gross_high, - ); + .for_each(|(tao_delta, alpha_delta, price_precision)| { + new_test_ext().execute_with(|| { + let netuid1 = NetUid::from(1); + + let tao_delta = TaoCurrency::from(tao_delta); + let alpha_delta = AlphaCurrency::from(alpha_delta); + + // Initialize realistically large reserves + let mut tao = initial_tao_reserve; + let mut alpha = initial_alpha_reserve; + TaoReserve::set_mock_reserve(netuid1, tao); + AlphaReserve::set_mock_reserve(netuid1, alpha); + let price_before = Swap::current_price(netuid1); + + // Adjust reserves gradually + for _ in 0..ITERATIONS { + Swap::adjust_protocol_liquidity(netuid1, tao_delta, alpha_delta); + tao += tao_delta; + alpha += alpha_delta; + TaoReserve::set_mock_reserve(netuid1, tao); + AlphaReserve::set_mock_reserve(netuid1, alpha); + } - // Liquidity position at correct ticks - assert_eq!( - Pallet::::count_positions(netuid, &OK_COLDKEY_ACCOUNT_ID), - 1 + // Check that price didn't change + let price_after = Swap::current_price(netuid1); + assert_abs_diff_eq!( + price_before.to_num::(), + price_after.to_num::(), + epsilon = price_precision ); - let position = - Positions::::get((netuid, OK_COLDKEY_ACCOUNT_ID, position_id)).unwrap(); - assert_eq!(position.liquidity, liquidity); - assert_eq!(position.tick_low, tick_low); - assert_eq!(position.tick_high, tick_high); - assert_eq!(position.fees_alpha, 0); - assert_eq!(position.fees_tao, 0); - - // Current liquidity is updated only when price range includes the current price - let expected_liquidity = - if (price_high > current_price) && (price_low <= current_price) { - liquidity_before + liquidity - } else { - liquidity_before - }; - - assert_eq!(CurrentLiquidity::::get(netuid), expected_liquidity) - }, - ); - }); -} + ///////////////////////// -#[test] -fn test_add_liquidity_max_limit_enforced() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - let liquidity = 2_000_000_000_u64; - assert_ok!(Pallet::::maybe_initialize_v3(netuid)); + // Now do one-time big injection with another netuid and compare weights - let limit = MaxPositions::get() as usize; + let netuid2 = NetUid::from(2); - for _ in 0..limit { - Pallet::::do_add_liquidity( - netuid, - &OK_COLDKEY_ACCOUNT_ID, - &OK_HOTKEY_ACCOUNT_ID, - TickIndex::MIN, - TickIndex::MAX, - liquidity, - ) - .unwrap(); - } + // Initialize same large reserves + TaoReserve::set_mock_reserve(netuid2, initial_tao_reserve); + AlphaReserve::set_mock_reserve(netuid2, initial_alpha_reserve); - let test_result = Pallet::::do_add_liquidity( - netuid, - &OK_COLDKEY_ACCOUNT_ID, - &OK_HOTKEY_ACCOUNT_ID, - TickIndex::MIN, - TickIndex::MAX, - liquidity, - ); + // Adjust reserves by one large amount at once + let tao_delta_once = TaoCurrency::from(ITERATIONS * u64::from(tao_delta)); + let alpha_delta_once = AlphaCurrency::from(ITERATIONS * u64::from(alpha_delta)); + Swap::adjust_protocol_liquidity(netuid2, tao_delta_once, alpha_delta_once); + TaoReserve::set_mock_reserve(netuid2, initial_tao_reserve + tao_delta_once); + AlphaReserve::set_mock_reserve(netuid2, initial_alpha_reserve + alpha_delta_once); - assert_err!(test_result, Error::::MaxPositionsExceeded); - }); -} + // Compare reserve weights for netuid 1 and 2 + let res_weights1 = SwapBalancer::::get(netuid1); + let res_weights2 = SwapBalancer::::get(netuid2); + let actual_quote_weight1 = perquintill_to_f64(res_weights1.get_quote_weight()); + let actual_quote_weight2 = perquintill_to_f64(res_weights2.get_quote_weight()); + assert_abs_diff_eq!( + actual_quote_weight1, + actual_quote_weight2, + epsilon = WEIGHT_PRECISION + ); + }); + }); + } -#[test] -fn test_add_liquidity_out_of_bounds() { - new_test_ext().execute_with(|| { + /// Should work ok when initial alpha is zero + /// cargo test --package pallet-subtensor-swap --lib -- pallet::tests::dispatchables::test_adjust_protocol_liquidity_zero_alpha --exact --nocapture + #[test] + fn test_adjust_protocol_liquidity_zero_alpha() { + // test case: tao_delta, alpha_delta [ - // For our tests, we'll construct TickIndex values that are intentionally - // outside the valid range for testing purposes only - ( - TickIndex::new_unchecked(TickIndex::MIN.get() - 1), - TickIndex::MAX, - 1_000_000_000_u64, - ), - ( - TickIndex::MIN, - TickIndex::new_unchecked(TickIndex::MAX.get() + 1), - 1_000_000_000_u64, - ), - ( - TickIndex::new_unchecked(TickIndex::MIN.get() - 1), - TickIndex::new_unchecked(TickIndex::MAX.get() + 1), - 1_000_000_000_u64, - ), - ( - TickIndex::new_unchecked(TickIndex::MIN.get() - 100), - TickIndex::new_unchecked(TickIndex::MAX.get() + 100), - 1_000_000_000_u64, - ), - // Inverted ticks: high < low - ( - TickIndex::new_unchecked(-900), - TickIndex::new_unchecked(-1000), - 1_000_000_000_u64, - ), - // Equal ticks: high == low - ( - TickIndex::new_unchecked(-10_000), - TickIndex::new_unchecked(-10_000), - 1_000_000_000_u64, - ), + (0_u64, 0_u64), + (0_u64, 1_u64), + (1_u64, 0_u64), + (1_u64, 1_u64), + (0_u64, 10_u64), + (10_u64, 0_u64), + (10_u64, 10_u64), + (0_u64, 100_u64), + (100_u64, 0_u64), + (100_u64, 100_u64), + (0_u64, 1_000_u64), + (1_000_u64, 0_u64), + (1_000_u64, 1_000_u64), + (1_000_000_u64, 0_u64), + (0_u64, 1_000_000_u64), + (1_000_000_u64, 1_000_000_u64), + (1_000_000_000_u64, 0_u64), + (0_u64, 1_000_000_000_u64), + (1_000_000_000_u64, 1_000_000_000_u64), + (1_000_000_000_000_u64, 0_u64), + (0_u64, 1_000_000_000_000_u64), + (1_000_000_000_000_u64, 1_000_000_000_000_u64), + (1_u64, 2_u64), + (2_u64, 1_u64), + (10_u64, 20_u64), + (20_u64, 10_u64), + (100_u64, 200_u64), + (200_u64, 100_u64), + (1_000_u64, 2_000_u64), + (2_000_u64, 1_000_u64), + (1_000_000_u64, 2_000_000_u64), + (2_000_000_u64, 1_000_000_u64), + (1_000_000_000_u64, 2_000_000_000_u64), + (2_000_000_000_u64, 1_000_000_000_u64), + (1_000_000_000_000_u64, 2_000_000_000_000_u64), + (2_000_000_000_000_u64, 1_000_000_000_000_u64), + (1_234_567_u64, 2_432_765_u64), + (1_234_567_u64, 2_432_765_890_u64), ] .into_iter() - .enumerate() - .map(|(n, v)| (NetUid::from(n as u16 + 1), v.0, v.1, v.2)) - .for_each(|(netuid, tick_low, tick_high, liquidity)| { - assert_ok!(Pallet::::maybe_initialize_v3(netuid)); - - // Add liquidity - assert_err!( - Swap::do_add_liquidity( - netuid, - &OK_COLDKEY_ACCOUNT_ID, - &OK_HOTKEY_ACCOUNT_ID, - tick_low, - tick_high, - liquidity - ), - Error::::InvalidTickRange, - ); + .for_each(|(tao_delta, alpha_delta)| { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + + let tao_delta = TaoCurrency::from(tao_delta); + let alpha_delta = AlphaCurrency::from(alpha_delta); + + // Initialize reserves and price + // broken state: Zero price because of zero alpha reserve + let tao = TaoCurrency::from(1_000_000_000_000_u64); + let alpha = AlphaCurrency::from(0_u64); + TaoReserve::set_mock_reserve(netuid, tao); + AlphaReserve::set_mock_reserve(netuid, alpha); + let price_before = Swap::current_price(netuid); + assert_eq!(price_before, U64F64::from_num(0)); + let new_tao = u64::from(tao + tao_delta) as f64; + let new_alpha = u64::from(alpha + alpha_delta) as f64; + + // Adjust reserves + Swap::adjust_protocol_liquidity(netuid, tao_delta, alpha_delta); + TaoReserve::set_mock_reserve(netuid, tao + tao_delta); + AlphaReserve::set_mock_reserve(netuid, alpha + alpha_delta); + + let res_weights = SwapBalancer::::get(netuid); + let actual_quote_weight = perquintill_to_f64(res_weights.get_quote_weight()); + + // Check that price didn't change + let price_after = Swap::current_price(netuid); + if new_alpha == 0. { + // If the pool state is still broken (∆x = 0), no change + assert_eq!(actual_quote_weight, 0.5); + assert_eq!(price_after, U64F64::from_num(0)); + } else { + // Price got fixed + let expected_price = new_tao / new_alpha; + assert_abs_diff_eq!( + expected_price, + price_after.to_num::(), + epsilon = price_before.to_num::() / 1_000_000_000_000. + ); + assert_eq!(actual_quote_weight, 0.5); + } + }); }); - }); -} + } -#[test] -fn test_add_liquidity_over_balance() { - new_test_ext().execute_with(|| { - let coldkey_account_id = 3; - let hotkey_account_id = 1002; + /// Collects the fees and adds them to protocol liquidity + /// cargo test --package pallet-subtensor-swap --lib -- pallet::tests::dispatchables::test_adjust_protocol_liquidity_collects_fees --exact --nocapture + #[test] + fn test_adjust_protocol_liquidity_collects_fees() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); - [ - // Lower than price (not enough tao) - (0.1, 0.2, 100_000_000_000_u64), - // Higher than price (not enough alpha) - (0.3, 0.4, 100_000_000_000_u64), - // Around the price (not enough both) - (0.1, 0.4, 100_000_000_000_u64), - ] - .into_iter() - .enumerate() - .map(|(n, v)| (NetUid::from(n as u16 + 1), v.0, v.1, v.2)) - .for_each(|(netuid, price_low, price_high, liquidity)| { - // Calculate ticks - let tick_low = price_to_tick(price_low); - let tick_high = price_to_tick(price_high); - - assert_ok!(Pallet::::maybe_initialize_v3(netuid)); - - // Add liquidity - assert_err!( - Pallet::::do_add_liquidity( - netuid, - &coldkey_account_id, - &hotkey_account_id, - tick_low, - tick_high, - liquidity - ), - Error::::InsufficientBalance, - ); + let tao_delta = TaoCurrency::ZERO; + let alpha_delta = AlphaCurrency::ZERO; + + // Initialize reserves and price + // 0.1 price + let tao = TaoCurrency::from(1_000_000_000_u64); + let alpha = AlphaCurrency::from(10_000_000_000_u64); + TaoReserve::set_mock_reserve(netuid, tao); + AlphaReserve::set_mock_reserve(netuid, alpha); + + // Insert fees + let tao_fees = TaoCurrency::from(1_000); + let alpha_fees = AlphaCurrency::from(1_000); + FeesTao::::insert(netuid, tao_fees); + FeesAlpha::::insert(netuid, alpha_fees); + + // Adjust reserves + let (actual_tao_delta, actual_alpha_delta) = + Swap::adjust_protocol_liquidity(netuid, tao_delta, alpha_delta); + TaoReserve::set_mock_reserve(netuid, tao + tao_delta); + AlphaReserve::set_mock_reserve(netuid, alpha + alpha_delta); + + // Check that returned reserve deltas are correct (include fees) + assert_eq!(actual_tao_delta, tao_fees); + assert_eq!(actual_alpha_delta, alpha_fees); + + // Check that fees got reset + assert_eq!(FeesTao::::get(netuid), TaoCurrency::ZERO); + assert_eq!(FeesAlpha::::get(netuid), AlphaCurrency::ZERO); }); - }); + } } -// cargo test --package pallet-subtensor-swap --lib -- pallet::impls::tests::test_remove_liquidity_basic --exact --show-output #[test] -fn test_remove_liquidity_basic() { +fn test_swap_initialization() { new_test_ext().execute_with(|| { - let min_price = tick_to_price(TickIndex::MIN); - let max_price = tick_to_price(TickIndex::MAX); - let max_tick = price_to_tick(max_price); - assert_eq!(max_tick, TickIndex::MAX); - - let (current_price_low, current_price_high) = get_ticked_prices_around_current_price(); + let netuid = NetUid::from(1); - // As a user add liquidity with all possible corner cases - // - Initial price is 0.25 - // - liquidity is expressed in RAO units - // Test case is (price_low, price_high, liquidity, tao, alpha) - [ - // Repeat the protocol liquidity at maximum range: Expect all the same values - ( - min_price, - max_price, - 2_000_000_000_u64, - 1_000_000_000_u64, - 4_000_000_000_u64, - ), - // Repeat the protocol liquidity at current to max range: Expect the same alpha - ( - current_price_high, - max_price, - 2_000_000_000_u64, - 0, - 4_000_000_000, - ), - // Repeat the protocol liquidity at min to current range: Expect all the same tao - ( - min_price, - current_price_low, - 2_000_000_000_u64, - 1_000_000_000, - 0, - ), - // Half to double price - just some sane wothdraw amounts - (0.125, 0.5, 2_000_000_000_u64, 293_000_000, 1_171_000_000), - // Both below price - tao is non-zero, alpha is zero - (0.12, 0.13, 2_000_000_000_u64, 28_270_000, 0), - // Both above price - tao is zero, alpha is non-zero - (0.3, 0.4, 2_000_000_000_u64, 0, 489_200_000), - ] - .into_iter() - .enumerate() - .map(|(n, v)| (NetUid::from(n as u16 + 1), v.0, v.1, v.2, v.3, v.4)) - .for_each(|(netuid, price_low, price_high, liquidity, tao, alpha)| { - // Calculate ticks (assuming tick math is tested separately) - let tick_low = price_to_tick(price_low); - let tick_high = price_to_tick(price_high); - - assert_ok!(Pallet::::maybe_initialize_v3(netuid)); - let liquidity_before = CurrentLiquidity::::get(netuid); - - // Add liquidity - let (position_id, _, _) = Pallet::::do_add_liquidity( - netuid, - &OK_COLDKEY_ACCOUNT_ID, - &OK_HOTKEY_ACCOUNT_ID, - tick_low, - tick_high, - liquidity, - ) - .unwrap(); + // Setup reserves + let tao = TaoCurrency::from(1_000_000_000u64); + let alpha = AlphaCurrency::from(4_000_000_000u64); + TaoReserve::set_mock_reserve(netuid, tao); + AlphaReserve::set_mock_reserve(netuid, alpha); - // Remove liquidity - let remove_result = - Pallet::::do_remove_liquidity(netuid, &OK_COLDKEY_ACCOUNT_ID, position_id) - .unwrap(); - assert_abs_diff_eq!(remove_result.tao.to_u64(), tao, epsilon = tao / 1000); - assert_abs_diff_eq!( - u64::from(remove_result.alpha), - alpha, - epsilon = alpha / 1000 - ); - assert_eq!(remove_result.fee_tao, TaoCurrency::ZERO); - assert_eq!(remove_result.fee_alpha, AlphaCurrency::ZERO); + assert_ok!(Pallet::::maybe_initialize_palswap(netuid, None)); + assert!(PalSwapInitialized::::get(netuid)); - // Liquidity position is removed - assert_eq!( - Pallet::::count_positions(netuid, &OK_COLDKEY_ACCOUNT_ID), - 0 - ); - assert!(Positions::::get((netuid, OK_COLDKEY_ACCOUNT_ID, position_id)).is_none()); + // Verify current price is set + let price = Pallet::::current_price(netuid); + let expected_price = U64F64::from_num(0.25_f64); + assert_abs_diff_eq!( + price.to_num::(), + expected_price.to_num::(), + epsilon = 0.000000001 + ); - // Current liquidity is updated (back where it was) - assert_eq!(CurrentLiquidity::::get(netuid), liquidity_before); - }); + // Verify that swap reserve weight is initialized + let reserve_weight = SwapBalancer::::get(netuid); + assert_eq!( + reserve_weight.get_quote_weight(), + Perquintill::from_rational(1_u64, 2_u64), + ); }); } #[test] -fn test_remove_liquidity_nonexisting_position() { +fn test_swap_initialization_with_price() { new_test_ext().execute_with(|| { - let min_price = tick_to_price(TickIndex::MIN); - let max_price = tick_to_price(TickIndex::MAX); - let max_tick = price_to_tick(max_price); - assert_eq!(max_tick.get(), TickIndex::MAX.get()); - - let liquidity = 2_000_000_000_u64; let netuid = NetUid::from(1); - // Calculate ticks (assuming tick math is tested separately) - let tick_low = price_to_tick(min_price); - let tick_high = price_to_tick(max_price); + // Setup reserves, tao / alpha = 0.25 + let tao = TaoCurrency::from(1_000_000_000u64); + let alpha = AlphaCurrency::from(4_000_000_000u64); + TaoReserve::set_mock_reserve(netuid, tao); + AlphaReserve::set_mock_reserve(netuid, alpha); - // Setup swap - assert_ok!(Pallet::::maybe_initialize_v3(netuid)); - - // Add liquidity - assert_ok!(Pallet::::do_add_liquidity( + // Initialize with 0.2 price + assert_ok!(Pallet::::maybe_initialize_palswap( netuid, - &OK_COLDKEY_ACCOUNT_ID, - &OK_HOTKEY_ACCOUNT_ID, - tick_low, - tick_high, - liquidity, + Some(U64F64::from(1u16) / U64F64::from(5u16)) )); + assert!(PalSwapInitialized::::get(netuid)); - assert!(Pallet::::count_positions(netuid, &OK_COLDKEY_ACCOUNT_ID) > 0); - - // Remove liquidity - assert_err!( - Pallet::::do_remove_liquidity( - netuid, - &OK_COLDKEY_ACCOUNT_ID, - PositionId::new::() - ), - Error::::LiquidityNotFound, + // Verify current price is set to 0.2 + let price = Pallet::::current_price(netuid); + let expected_price = U64F64::from_num(0.2_f64); + assert_abs_diff_eq!( + price.to_num::(), + expected_price.to_num::(), + epsilon = 0.000000001 ); }); } -// cargo test --package pallet-subtensor-swap --lib -- pallet::tests::test_modify_position_basic --exact --show-output -#[test] -fn test_modify_position_basic() { - new_test_ext().execute_with(|| { - let max_price = tick_to_price(TickIndex::MAX); - let max_tick = price_to_tick(max_price); - let limit_price = 1000.0_f64; - assert_eq!(max_tick, TickIndex::MAX); - let (current_price_low, _current_price_high) = get_ticked_prices_around_current_price(); - - // As a user add liquidity with all possible corner cases - // - Initial price is 0.25 - // - liquidity is expressed in RAO units - // Test case is (price_low, price_high, liquidity, tao, alpha) - [ - // Repeat the protocol liquidity at current to max range: Expect the same alpha - ( - current_price_low, - max_price, - 2_000_000_000_u64, - 4_000_000_000, - ), - ] - .into_iter() - .enumerate() - .map(|(n, v)| (NetUid::from(n as u16 + 1), v.0, v.1, v.2, v.3)) - .for_each(|(netuid, price_low, price_high, liquidity, alpha)| { - // Calculate ticks (assuming tick math is tested separately) - let tick_low = price_to_tick(price_low); - let tick_high = price_to_tick(price_high); - - assert_ok!(Pallet::::maybe_initialize_v3(netuid)); - - // Add liquidity - let (position_id, _, _) = Pallet::::do_add_liquidity( - netuid, - &OK_COLDKEY_ACCOUNT_ID, - &OK_HOTKEY_ACCOUNT_ID, - tick_low, - tick_high, - liquidity, - ) - .unwrap(); - - // Get tick infos before the swap/update - let tick_low_info_before = Ticks::::get(netuid, tick_low).unwrap(); - let tick_high_info_before = Ticks::::get(netuid, tick_high).unwrap(); - - // Swap to create fees on the position - let sqrt_limit_price = SqrtPrice::from_num((limit_price).sqrt()); - let order = GetAlphaForTao::with_amount(liquidity / 10); - Pallet::::do_swap(netuid, order, sqrt_limit_price, false, false).unwrap(); - - // Modify liquidity (also causes claiming of fees) - let liquidity_before = CurrentLiquidity::::get(netuid); - let modify_result = Pallet::::do_modify_position( - netuid, - &OK_COLDKEY_ACCOUNT_ID, - &OK_HOTKEY_ACCOUNT_ID, - position_id, - -((liquidity / 10) as i64), - ) - .unwrap(); - assert_abs_diff_eq!( - u64::from(modify_result.alpha), - alpha / 10, - epsilon = alpha / 1000 - ); - assert!(modify_result.fee_tao > TaoCurrency::ZERO); - assert_eq!(modify_result.fee_alpha, AlphaCurrency::ZERO); - - // Liquidity position is reduced - assert_eq!( - Pallet::::count_positions(netuid, &OK_COLDKEY_ACCOUNT_ID), - 1 - ); - - // Current liquidity is reduced with modify_position - assert!(CurrentLiquidity::::get(netuid) < liquidity_before); - - // Position liquidity is reduced - let position = - Positions::::get((netuid, OK_COLDKEY_ACCOUNT_ID, position_id)).unwrap(); - assert_eq!(position.liquidity, liquidity * 9 / 10); - assert_eq!(position.tick_low, tick_low); - assert_eq!(position.tick_high, tick_high); - - // Tick liquidity is updated properly for low and high position ticks - let tick_low_info_after = Ticks::::get(netuid, tick_low).unwrap(); - let tick_high_info_after = Ticks::::get(netuid, tick_high).unwrap(); - - assert_eq!( - tick_low_info_before.liquidity_net - (liquidity / 10) as i128, - tick_low_info_after.liquidity_net, - ); - assert_eq!( - tick_low_info_before.liquidity_gross - (liquidity / 10), - tick_low_info_after.liquidity_gross, - ); - assert_eq!( - tick_high_info_before.liquidity_net + (liquidity / 10) as i128, - tick_high_info_after.liquidity_net, - ); - assert_eq!( - tick_high_info_before.liquidity_gross - (liquidity / 10), - tick_high_info_after.liquidity_gross, - ); - - // Modify liquidity again (ensure fees aren't double-collected) - let modify_result = Pallet::::do_modify_position( - netuid, - &OK_COLDKEY_ACCOUNT_ID, - &OK_HOTKEY_ACCOUNT_ID, - position_id, - -((liquidity / 100) as i64), - ) - .unwrap(); - - assert_abs_diff_eq!( - u64::from(modify_result.alpha), - alpha / 100, - epsilon = alpha / 1000 - ); - assert_eq!(modify_result.fee_tao, TaoCurrency::ZERO); - assert_eq!(modify_result.fee_alpha, AlphaCurrency::ZERO); - }); - }); -} - -// cargo test --package pallet-subtensor-swap --lib -- pallet::impls::tests::test_swap_basic --exact --show-output +// cargo test --package pallet-subtensor-swap --lib -- pallet::tests::test_swap_basic --exact --nocapture #[test] fn test_swap_basic() { new_test_ext().execute_with(|| { @@ -748,851 +466,277 @@ fn test_swap_basic() { netuid: NetUid, order: Order, limit_price: f64, - output_amount: u64, price_should_grow: bool, ) where Order: OrderT, - Order::PaidIn: GlobalFeeInfo, BasicSwapStep: SwapStep, { - // Consumed liquidity ticks - let tick_low = TickIndex::MIN; - let tick_high = TickIndex::MAX; - let liquidity = order.amount().to_u64(); + let swap_amount = order.amount().to_u64(); // Setup swap - assert_ok!(Pallet::::maybe_initialize_v3(netuid)); - - // Get tick infos before the swap - let tick_low_info_before = Ticks::::get(netuid, tick_low).unwrap_or_default(); - let tick_high_info_before = Ticks::::get(netuid, tick_high).unwrap_or_default(); - let liquidity_before = CurrentLiquidity::::get(netuid); + // Price is 0.25 + let initial_tao_reserve = TaoCurrency::from(1_000_000_000_u64); + let initial_alpha_reserve = AlphaCurrency::from(4_000_000_000_u64); + TaoReserve::set_mock_reserve(netuid, initial_tao_reserve); + AlphaReserve::set_mock_reserve(netuid, initial_alpha_reserve); + assert_ok!(Pallet::::maybe_initialize_palswap(netuid, None)); // Get current price - let current_price = Pallet::::current_price(netuid); + let current_price_before = Pallet::::current_price(netuid); + + // Get reserves + let tao_reserve = TaoReserve::reserve(netuid.into()).to_u64(); + let alpha_reserve = AlphaReserve::reserve(netuid.into()).to_u64(); + + // Expected fee amount + let fee_rate = FeeRate::::get(netuid) as f64 / u16::MAX as f64; + let expected_fee = (swap_amount as f64 * fee_rate) as u64; + + // Calculate expected output amount using f64 math + // This is a simple case when w1 = w2 = 0.5, so there's no + // exponentiation needed + let x = alpha_reserve as f64; + let y = tao_reserve as f64; + let expected_output_amount = if price_should_grow { + x * (1.0 - y / (y + (swap_amount - expected_fee) as f64)) + } else { + y * (1.0 - x / (x + (swap_amount - expected_fee) as f64)) + }; // Swap - let sqrt_limit_price = SqrtPrice::from_num((limit_price).sqrt()); + let limit_price_fixed = U64F64::from_num(limit_price); let swap_result = - Pallet::::do_swap(netuid, order.clone(), sqrt_limit_price, false, false) + Pallet::::do_swap(netuid, order.clone(), limit_price_fixed, false, false) .unwrap(); assert_abs_diff_eq!( swap_result.amount_paid_out.to_u64(), - output_amount, - epsilon = output_amount / 100 + expected_output_amount as u64, + epsilon = 1 ); assert_abs_diff_eq!( swap_result.paid_in_reserve_delta() as u64, - liquidity, - epsilon = liquidity / 10 + (swap_amount - expected_fee), + epsilon = 1 ); assert_abs_diff_eq!( swap_result.paid_out_reserve_delta() as i64, - -(output_amount as i64), - epsilon = output_amount as i64 / 10 - ); - - // Check that low and high ticks' fees were updated properly, and liquidity values were not updated - let tick_low_info = Ticks::::get(netuid, tick_low).unwrap(); - let tick_high_info = Ticks::::get(netuid, tick_high).unwrap(); - let expected_liquidity_net_low = tick_low_info_before.liquidity_net; - let expected_liquidity_gross_low = tick_low_info_before.liquidity_gross; - let expected_liquidity_net_high = tick_high_info_before.liquidity_net; - let expected_liquidity_gross_high = tick_high_info_before.liquidity_gross; - assert_eq!(tick_low_info.liquidity_net, expected_liquidity_net_low,); - assert_eq!(tick_low_info.liquidity_gross, expected_liquidity_gross_low,); - assert_eq!(tick_high_info.liquidity_net, expected_liquidity_net_high,); - assert_eq!( - tick_high_info.liquidity_gross, - expected_liquidity_gross_high, - ); - - // Expected fee amount - let fee_rate = FeeRate::::get(netuid) as f64 / u16::MAX as f64; - let expected_fee = (liquidity as f64 * fee_rate) as u64; - - // Global fees should be updated - let actual_global_fee = (order.amount().global_fee(netuid).to_num::() - * (liquidity_before as f64)) as u64; - - assert!((swap_result.fee_paid.to_u64() as i64 - expected_fee as i64).abs() <= 1); - assert!((actual_global_fee as i64 - expected_fee as i64).abs() <= 1); - - // Tick fees should be updated - - // Liquidity position should not be updated - let protocol_id = Pallet::::protocol_account_id(); - let positions = - Positions::::iter_prefix_values((netuid, protocol_id)).collect::>(); - let position = positions.first().unwrap(); - - assert_eq!( - position.liquidity, - helpers_128bit::sqrt( - TaoReserve::reserve(netuid.into()).to_u64() as u128 - * AlphaReserve::reserve(netuid.into()).to_u64() as u128 - ) as u64 + -(expected_output_amount as i64), + epsilon = 1 ); - assert_eq!(position.tick_low, tick_low); - assert_eq!(position.tick_high, tick_high); - assert_eq!(position.fees_alpha, 0); - assert_eq!(position.fees_tao, 0); - // Current liquidity is not updated - assert_eq!(CurrentLiquidity::::get(netuid), liquidity_before); + // Update reserves (because it happens outside of do_swap in stake_utils) + if price_should_grow { + TaoReserve::set_mock_reserve( + netuid, + TaoCurrency::from( + (u64::from(initial_tao_reserve) as i128 + + swap_result.paid_in_reserve_delta()) as u64, + ), + ); + AlphaReserve::set_mock_reserve( + netuid, + AlphaCurrency::from( + (u64::from(initial_alpha_reserve) as i128 + + swap_result.paid_out_reserve_delta()) as u64, + ), + ); + } else { + TaoReserve::set_mock_reserve( + netuid, + TaoCurrency::from( + (u64::from(initial_tao_reserve) as i128 + + swap_result.paid_out_reserve_delta()) as u64, + ), + ); + AlphaReserve::set_mock_reserve( + netuid, + AlphaCurrency::from( + (u64::from(initial_alpha_reserve) as i128 + + swap_result.paid_in_reserve_delta()) as u64, + ), + ); + } // Assert that price movement is in correct direction - let sqrt_current_price_after = AlphaSqrtPrice::::get(netuid); let current_price_after = Pallet::::current_price(netuid); - assert_eq!(current_price_after >= current_price, price_should_grow); - - // Assert that current tick is updated - let current_tick = CurrentTick::::get(netuid); - let expected_current_tick = - TickIndex::from_sqrt_price_bounded(sqrt_current_price_after); - assert_eq!(current_tick, expected_current_tick); + assert_eq!( + current_price_after >= current_price_before, + price_should_grow + ); } // Current price is 0.25 // Test case is (order_type, liquidity, limit_price, output_amount) - perform_test( - 1.into(), - GetAlphaForTao::with_amount(1_000), - 1000.0, - 3990, - true, - ); + perform_test(1.into(), GetAlphaForTao::with_amount(1_000), 1000.0, true); + perform_test(1.into(), GetAlphaForTao::with_amount(2_000), 1000.0, true); + perform_test(1.into(), GetAlphaForTao::with_amount(123_456), 1000.0, true); + perform_test(2.into(), GetTaoForAlpha::with_amount(1_000), 0.0001, false); + perform_test(2.into(), GetTaoForAlpha::with_amount(2_000), 0.0001, false); perform_test( 2.into(), - GetTaoForAlpha::with_amount(1_000), + GetTaoForAlpha::with_amount(123_456), 0.0001, - 250, false, ); perform_test( 3.into(), - GetAlphaForTao::with_amount(500_000_000), + GetAlphaForTao::with_amount(1_000_000_000), + 1000.0, + true, + ); + perform_test( + 3.into(), + GetAlphaForTao::with_amount(10_000_000_000), 1000.0, - 2_000_000_000, true, ); }); } -// In this test the swap starts and ends within one (large liquidity) position -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor-swap --lib -- pallet::tests::test_swap_single_position --exact --show-output +// cargo test --package pallet-subtensor-swap --lib -- pallet::impls::tests::test_swap_precision_edge_case --exact --show-output #[test] -fn test_swap_single_position() { - let min_price = tick_to_price(TickIndex::MIN); - let max_price = tick_to_price(TickIndex::MAX); - let max_tick = price_to_tick(max_price); - let netuid = NetUid::from(1); - assert_eq!(max_tick, TickIndex::MAX); - - let mut current_price_low = 0_f64; - let mut current_price_high = 0_f64; - let mut current_price = 0_f64; - new_test_ext().execute_with(|| { - let (low, high) = get_ticked_prices_around_current_price(); - current_price_low = low; - current_price_high = high; - current_price = Pallet::::current_price(netuid).to_num::(); - }); - - macro_rules! perform_test { - ($order_t:ident, - $price_low_offset:expr, - $price_high_offset:expr, - $position_liquidity:expr, - $liquidity_fraction:expr, - $limit_price:expr, - $price_should_grow:expr - ) => { - new_test_ext().execute_with(|| { - let price_low_offset = $price_low_offset; - let price_high_offset = $price_high_offset; - let position_liquidity = $position_liquidity; - let order_liquidity_fraction = $liquidity_fraction; - let limit_price = $limit_price; - let price_should_grow = $price_should_grow; - - ////////////////////////////////////////////// - // Initialize pool and add the user position - assert_ok!(Pallet::::maybe_initialize_v3(netuid)); - let tao_reserve = TaoReserve::reserve(netuid.into()).to_u64(); - let alpha_reserve = AlphaReserve::reserve(netuid.into()).to_u64(); - let protocol_liquidity = (tao_reserve as f64 * alpha_reserve as f64).sqrt(); - - // Add liquidity - let current_price = Pallet::::current_price(netuid).to_num::(); - let sqrt_current_price = AlphaSqrtPrice::::get(netuid).to_num::(); - - let price_low = price_low_offset + current_price; - let price_high = price_high_offset + current_price; - let tick_low = price_to_tick(price_low); - let tick_high = price_to_tick(price_high); - let (_position_id, _tao, _alpha) = Pallet::::do_add_liquidity( - netuid, - &OK_COLDKEY_ACCOUNT_ID, - &OK_HOTKEY_ACCOUNT_ID, - tick_low, - tick_high, - position_liquidity, - ) - .unwrap(); - - // Liquidity position at correct ticks - assert_eq!( - Pallet::::count_positions(netuid, &OK_COLDKEY_ACCOUNT_ID), - 1 - ); +fn test_swap_precision_edge_case() { + // Test case: tao_reserve, alpha_reserve, swap_amount + [ + (1_000_u64, 1_000_u64, 999_500_u64), + (1_000_000_u64, 1_000_000_u64, 999_500_000_u64), + ] + .into_iter() + .for_each(|(tao_reserve, alpha_reserve, swap_amount)| { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + let order = GetTaoForAlpha::with_amount(swap_amount); - // Get tick infos before the swap - let tick_low_info_before = Ticks::::get(netuid, tick_low).unwrap_or_default(); - let tick_high_info_before = - Ticks::::get(netuid, tick_high).unwrap_or_default(); - let liquidity_before = CurrentLiquidity::::get(netuid); - assert_abs_diff_eq!( - liquidity_before as f64, - protocol_liquidity + position_liquidity as f64, - epsilon = liquidity_before as f64 / 1000. - ); + // Very low reserves + TaoReserve::set_mock_reserve(netuid, TaoCurrency::from(tao_reserve)); + AlphaReserve::set_mock_reserve(netuid, AlphaCurrency::from(alpha_reserve)); - ////////////////////////////////////////////// - // Swap + // Minimum possible limit price + let limit_price: U64F64 = get_min_price(); + println!("limit_price = {:?}", limit_price); - // Calculate the expected output amount for the cornercase of one step - let order_liquidity = order_liquidity_fraction * position_liquidity as f64; + // Swap + let swap_result = + Pallet::::do_swap(netuid, order, limit_price, false, true).unwrap(); - let output_amount = >::approx_expected_swap_output( - sqrt_current_price, - liquidity_before as f64, - order_liquidity, - ); + assert!(swap_result.amount_paid_out > TaoCurrency::ZERO); + }); + }); +} - // Do the swap - let sqrt_limit_price = SqrtPrice::from_num((limit_price).sqrt()); - let order = $order_t::with_amount(order_liquidity as u64); - let swap_result = - Pallet::::do_swap(netuid, order, sqrt_limit_price, false, false).unwrap(); - assert_abs_diff_eq!( - swap_result.amount_paid_out.to_u64() as f64, - output_amount, - epsilon = output_amount / 10. - ); +#[test] +fn test_convert_deltas() { + new_test_ext().execute_with(|| { + for (tao, alpha, w_quote, delta_in) in [ + (1500, 1000, 0.5, 1), + (1500, 1000, 0.5, 10000), + (1500, 1000, 0.5, 1000000), + (1500, 1000, 0.5, u64::MAX), + (1, 1000000, 0.5, 1), + (1, 1000000, 0.5, 10000), + (1, 1000000, 0.5, 1000000), + (1, 1000000, 0.5, u64::MAX), + (1000000, 1, 0.5, 1), + (1000000, 1, 0.5, 10000), + (1000000, 1, 0.5, 1000000), + (1000000, 1, 0.5, u64::MAX), + (1500, 1000, 0.50000001, 1), + (1500, 1000, 0.50000001, 10000), + (1500, 1000, 0.50000001, 1000000), + (1500, 1000, 0.50000001, u64::MAX), + (1, 1000000, 0.50000001, 1), + (1, 1000000, 0.50000001, 10000), + (1, 1000000, 0.50000001, 1000000), + (1, 1000000, 0.50000001, u64::MAX), + (1000000, 1, 0.50000001, 1), + (1000000, 1, 0.50000001, 10000), + (1000000, 1, 0.50000001, 1000000), + (1000000, 1, 0.50000001, u64::MAX), + (1500, 1000, 0.49999999, 1), + (1500, 1000, 0.49999999, 10000), + (1500, 1000, 0.49999999, 1000000), + (1500, 1000, 0.49999999, u64::MAX), + (1, 1000000, 0.49999999, 1), + (1, 1000000, 0.49999999, 10000), + (1, 1000000, 0.49999999, 1000000), + (1, 1000000, 0.49999999, u64::MAX), + (1000000, 1, 0.49999999, 1), + (1000000, 1, 0.49999999, 10000), + (1000000, 1, 0.49999999, 1000000), + (1000000, 1, 0.49999999, u64::MAX), + // Low quote weight + (1500, 1000, 0.1, 1), + (1500, 1000, 0.1, 10000), + (1500, 1000, 0.1, 1000000), + (1500, 1000, 0.1, u64::MAX), + (1, 1000000, 0.1, 1), + (1, 1000000, 0.1, 10000), + (1, 1000000, 0.1, 1000000), + (1, 1000000, 0.1, u64::MAX), + (1000000, 1, 0.1, 1), + (1000000, 1, 0.1, 10000), + (1000000, 1, 0.1, 1000000), + (1000000, 1, 0.1, u64::MAX), + // High quote weight + (1500, 1000, 0.9, 1), + (1500, 1000, 0.9, 10000), + (1500, 1000, 0.9, 1000000), + (1500, 1000, 0.9, u64::MAX), + (1, 1000000, 0.9, 1), + (1, 1000000, 0.9, 10000), + (1, 1000000, 0.9, 1000000), + (1, 1000000, 0.9, u64::MAX), + (1000000, 1, 0.9, 1), + (1000000, 1, 0.9, 10000), + (1000000, 1, 0.9, 1000000), + (1000000, 1, 0.9, u64::MAX), + ] { + // Initialize reserves and weights + let netuid = NetUid::from(1); + TaoReserve::set_mock_reserve(netuid, TaoCurrency::from(tao)); + AlphaReserve::set_mock_reserve(netuid, AlphaCurrency::from(alpha)); + assert_ok!(Pallet::::maybe_initialize_palswap(netuid, None)); + + let w_accuracy = 1_000_000_000_f64; + let w_quote_pt = + Perquintill::from_rational((w_quote * w_accuracy) as u128, w_accuracy as u128); + let bal = Balancer::new(w_quote_pt).unwrap(); + SwapBalancer::::insert(netuid, bal); + + // Calculate expected swap results (buy and sell) using f64 math + let y = tao as f64; + let x = alpha as f64; + let d = delta_in as f64; + let w1_div_w2 = (1. - w_quote) / w_quote; + let w2_div_w1 = w_quote / (1. - w_quote); + let expected_sell = y * (1. - (x / (x + d)).powf(w1_div_w2)); + let expected_buy = x * (1. - (y / (y + d)).powf(w2_div_w1)); - if order_liquidity_fraction <= 0.001 { - assert_abs_diff_eq!( - swap_result.paid_in_reserve_delta() as i64, - order_liquidity as i64, - epsilon = order_liquidity as i64 / 10 - ); - assert_abs_diff_eq!( - swap_result.paid_out_reserve_delta() as i64, - -(output_amount as i64), - epsilon = output_amount as i64 / 10 - ); - } - - // Assert that price movement is in correct direction - let current_price_after = Pallet::::current_price(netuid); - assert_eq!(price_should_grow, current_price_after > current_price); - - // Assert that for small amounts price stays within the user position - if (order_liquidity_fraction <= 0.001) - && (price_low_offset > 0.0001) - && (price_high_offset > 0.0001) - { - assert!(current_price_after <= price_high); - assert!(current_price_after >= price_low); - } - - // Check that low and high ticks' fees were updated properly - let tick_low_info = Ticks::::get(netuid, tick_low).unwrap(); - let tick_high_info = Ticks::::get(netuid, tick_high).unwrap(); - let expected_liquidity_net_low = tick_low_info_before.liquidity_net; - let expected_liquidity_gross_low = tick_low_info_before.liquidity_gross; - let expected_liquidity_net_high = tick_high_info_before.liquidity_net; - let expected_liquidity_gross_high = tick_high_info_before.liquidity_gross; - assert_eq!(tick_low_info.liquidity_net, expected_liquidity_net_low,); - assert_eq!(tick_low_info.liquidity_gross, expected_liquidity_gross_low,); - assert_eq!(tick_high_info.liquidity_net, expected_liquidity_net_high,); - assert_eq!( - tick_high_info.liquidity_gross, - expected_liquidity_gross_high, - ); - - // Expected fee amount - let fee_rate = FeeRate::::get(netuid) as f64 / u16::MAX as f64; - let expected_fee = (order_liquidity - order_liquidity / (1.0 + fee_rate)) as u64; - - // // Global fees should be updated - let actual_global_fee = ($order_t::with_amount(0) - .amount() - .global_fee(netuid) - .to_num::() - * (liquidity_before as f64)) as u64; - - assert_abs_diff_eq!( - swap_result.fee_paid.to_u64(), - expected_fee, - epsilon = expected_fee / 10 - ); - assert_abs_diff_eq!(actual_global_fee, expected_fee, epsilon = expected_fee / 10); - - // Tick fees should be updated - - // Liquidity position should not be updated - let positions = - Positions::::iter_prefix_values((netuid, OK_COLDKEY_ACCOUNT_ID)) - .collect::>(); - let position = positions.first().unwrap(); - - assert_eq!(position.liquidity, position_liquidity,); - assert_eq!(position.tick_low, tick_low); - assert_eq!(position.tick_high, tick_high); - assert_eq!(position.fees_alpha, 0); - assert_eq!(position.fees_tao, 0); - }); - }; - } - - // Current price is 0.25 - // The test case is based on the current price and position prices are defined as a price - // offset from the current price - // Outer part of test case is Position: (price_low_offset, price_high_offset, liquidity) - [ - // Very localized position at the current price - (-0.1, 0.1, 500_000_000_000_u64), - // Repeat the protocol liquidity at maximum range - ( - min_price - current_price, - max_price - current_price, - 2_000_000_000_u64, - ), - // Repeat the protocol liquidity at current to max range - ( - current_price_high - current_price, - max_price - current_price, - 2_000_000_000_u64, - ), - // Repeat the protocol liquidity at min to current range - ( - min_price - current_price, - current_price_low - current_price, - 2_000_000_000_u64, - ), - // Half to double price - (-0.125, 0.25, 2_000_000_000_u64), - // A few other price ranges and liquidity volumes - (-0.1, 0.1, 2_000_000_000_u64), - (-0.1, 0.1, 10_000_000_000_u64), - (-0.1, 0.1, 100_000_000_000_u64), - (-0.01, 0.01, 100_000_000_000_u64), - (-0.001, 0.001, 100_000_000_000_u64), - ] - .into_iter() - .for_each( - |(price_low_offset, price_high_offset, position_liquidity)| { - // Inner part of test case is Order: (order_type, order_liquidity, limit_price) - // order_liquidity is represented as a fraction of position_liquidity - for liquidity_fraction in [0.0001, 0.001, 0.01, 0.1, 0.2, 0.5] { - perform_test!( - GetAlphaForTao, - price_low_offset, - price_high_offset, - position_liquidity, - liquidity_fraction, - 1000.0_f64, - true - ); - perform_test!( - GetTaoForAlpha, - price_low_offset, - price_high_offset, - position_liquidity, - liquidity_fraction, - 0.0001_f64, - false - ); - } - }, - ); -} - -// This test is a sanity check for swap and multiple positions -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor-swap --lib -- pallet::impls::tests::test_swap_multiple_positions --exact --show-output --nocapture -#[test] -fn test_swap_multiple_positions() { - new_test_ext().execute_with(|| { - let min_price = tick_to_price(TickIndex::MIN); - let max_price = tick_to_price(TickIndex::MAX); - let max_tick = price_to_tick(max_price); - let netuid = NetUid::from(1); - assert_eq!(max_tick, TickIndex::MAX); - - ////////////////////////////////////////////// - // Initialize pool and add the user position - assert_ok!(Pallet::::maybe_initialize_v3(netuid)); - - // Add liquidity - let current_price = Pallet::::current_price(netuid).to_num::(); - - // Current price is 0.25 - // All positions below are placed at once - [ - // Very localized position at the current price - (-0.1, 0.1, 500_000_000_000_u64), - // Repeat the protocol liquidity at maximum range - ( - min_price - current_price, - max_price - current_price, - 2_000_000_000_u64, - ), - // Repeat the protocol liquidity at current to max range - (0.0, max_price - current_price, 2_000_000_000_u64), - // Repeat the protocol liquidity at min to current range - (min_price - current_price, 0.0, 2_000_000_000_u64), - // Half to double price - (-0.125, 0.25, 2_000_000_000_u64), - // A few other price ranges and liquidity volumes - (-0.1, 0.1, 2_000_000_000_u64), - (-0.1, 0.1, 10_000_000_000_u64), - (-0.1, 0.1, 100_000_000_000_u64), - (-0.01, 0.01, 100_000_000_000_u64), - (-0.001, 0.001, 100_000_000_000_u64), - // A few (overlapping) positions up the range - (0.01, 0.02, 100_000_000_000_u64), - (0.02, 0.03, 100_000_000_000_u64), - (0.03, 0.04, 100_000_000_000_u64), - (0.03, 0.05, 100_000_000_000_u64), - // A few (overlapping) positions down the range - (-0.02, -0.01, 100_000_000_000_u64), - (-0.03, -0.02, 100_000_000_000_u64), - (-0.04, -0.03, 100_000_000_000_u64), - (-0.05, -0.03, 100_000_000_000_u64), - ] - .into_iter() - .for_each( - |(price_low_offset, price_high_offset, position_liquidity)| { - let price_low = price_low_offset + current_price; - let price_high = price_high_offset + current_price; - let tick_low = price_to_tick(price_low); - let tick_high = price_to_tick(price_high); - let (_position_id, _tao, _alpha) = Pallet::::do_add_liquidity( - netuid, - &OK_COLDKEY_ACCOUNT_ID, - &OK_HOTKEY_ACCOUNT_ID, - tick_low, - tick_high, - position_liquidity, - ) - .unwrap(); - }, - ); - - macro_rules! perform_test { - ($order_t:ident, $order_liquidity:expr, $limit_price:expr, $should_price_grow:expr) => { - ////////////////////////////////////////////// - // Swap - let order_liquidity = $order_liquidity; - let limit_price = $limit_price; - let should_price_grow = $should_price_grow; - - let sqrt_current_price = AlphaSqrtPrice::::get(netuid); - let current_price = (sqrt_current_price * sqrt_current_price).to_num::(); - let liquidity_before = CurrentLiquidity::::get(netuid); - let output_amount = >::approx_expected_swap_output( - sqrt_current_price.to_num(), - liquidity_before as f64, - order_liquidity as f64, - ); - - // Do the swap - let sqrt_limit_price = SqrtPrice::from_num((limit_price).sqrt()); - let order = $order_t::with_amount(order_liquidity); - let swap_result = - Pallet::::do_swap(netuid, order, sqrt_limit_price, false, false).unwrap(); - assert_abs_diff_eq!( - swap_result.amount_paid_out.to_u64() as f64, - output_amount, - epsilon = output_amount / 10. - ); - - let tao_reserve = TaoReserve::reserve(netuid.into()).to_u64(); - let alpha_reserve = AlphaReserve::reserve(netuid.into()).to_u64(); - let output_amount = output_amount as u64; - - assert!(output_amount > 0); - - if alpha_reserve > order_liquidity && tao_reserve > order_liquidity { - assert_abs_diff_eq!( - swap_result.paid_in_reserve_delta() as i64, - order_liquidity as i64, - epsilon = order_liquidity as i64 / 100 - ); - assert_abs_diff_eq!( - swap_result.paid_out_reserve_delta() as i64, - -(output_amount as i64), - epsilon = output_amount as i64 / 100 - ); - } - - // Assert that price movement is in correct direction - let sqrt_current_price_after = AlphaSqrtPrice::::get(netuid); - let current_price_after = - (sqrt_current_price_after * sqrt_current_price_after).to_num::(); - assert_eq!(should_price_grow, current_price_after > current_price); - }; - } - - // All these orders are executed without swap reset - for order_liquidity in [ - (100_000_u64), - (1_000_000), - (10_000_000), - (100_000_000), - (200_000_000), - (500_000_000), - (1_000_000_000), - (10_000_000_000), - ] { - perform_test!(GetAlphaForTao, order_liquidity, 1000.0_f64, true); - perform_test!(GetTaoForAlpha, order_liquidity, 0.0001_f64, false); - } - - // Current price shouldn't be much different from the original - let sqrt_current_price_after = AlphaSqrtPrice::::get(netuid); - let current_price_after = - (sqrt_current_price_after * sqrt_current_price_after).to_num::(); - assert_abs_diff_eq!( - current_price, - current_price_after, - epsilon = current_price / 10. - ) - }); -} - -// cargo test --package pallet-subtensor-swap --lib -- pallet::impls::tests::test_swap_precision_edge_case --exact --show-output -#[test] -fn test_swap_precision_edge_case() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(123); // 123 is netuid with low edge case liquidity - let order = GetTaoForAlpha::with_amount(1_000_000_000_000_000_000); - let tick_low = TickIndex::MIN; - - let sqrt_limit_price: SqrtPrice = tick_low.try_to_sqrt_price().unwrap(); - - // Setup swap - assert_ok!(Pallet::::maybe_initialize_v3(netuid)); - - // Swap - let swap_result = - Pallet::::do_swap(netuid, order, sqrt_limit_price, false, true).unwrap(); - - assert!(swap_result.amount_paid_out > TaoCurrency::ZERO); - }); -} - -// cargo test --package pallet-subtensor-swap --lib -- pallet::impls::tests::test_price_tick_price_roundtrip --exact --show-output -#[test] -fn test_price_tick_price_roundtrip() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - - // Setup swap - assert_ok!(Pallet::::maybe_initialize_v3(netuid)); - - let current_price = SqrtPrice::from_num(0.500_000_512_192_122_7); - let tick = TickIndex::try_from_sqrt_price(current_price).unwrap(); - - let round_trip_price = TickIndex::try_to_sqrt_price(&tick).unwrap(); - assert!(round_trip_price <= current_price); - - let roundtrip_tick = TickIndex::try_from_sqrt_price(round_trip_price).unwrap(); - assert!(tick == roundtrip_tick); - }); -} - -#[test] -fn test_convert_deltas() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - assert_ok!(Pallet::::maybe_initialize_v3(netuid)); - - for (sqrt_price, delta_in, expected_buy, expected_sell) in [ - (SqrtPrice::from_num(1.5), 1, 0, 2), - (SqrtPrice::from_num(1.5), 10000, 4444, 22500), - (SqrtPrice::from_num(1.5), 1000000, 444444, 2250000), - ( - SqrtPrice::from_num(1.5), - u64::MAX, - 2000000000000, - 3000000000000, - ), - ( - TickIndex::MIN.as_sqrt_price_bounded(), - 1, - 18406523739291577836, - 465, - ), - (TickIndex::MIN.as_sqrt_price_bounded(), 10000, u64::MAX, 465), - ( - TickIndex::MIN.as_sqrt_price_bounded(), - 1000000, - u64::MAX, - 465, - ), - ( - TickIndex::MIN.as_sqrt_price_bounded(), - u64::MAX, - u64::MAX, - 464, - ), - ( - TickIndex::MAX.as_sqrt_price_bounded(), - 1, - 0, - 18406523745214495085, - ), - (TickIndex::MAX.as_sqrt_price_bounded(), 10000, 0, u64::MAX), - (TickIndex::MAX.as_sqrt_price_bounded(), 1000000, 0, u64::MAX), - ( - TickIndex::MAX.as_sqrt_price_bounded(), - u64::MAX, - 2000000000000, - u64::MAX, - ), - ] { - { - AlphaSqrtPrice::::insert(netuid, sqrt_price); - - assert_abs_diff_eq!( - BasicSwapStep::::convert_deltas( - netuid, - delta_in.into() - ), - expected_sell.into(), - epsilon = 2.into() - ); - assert_abs_diff_eq!( - BasicSwapStep::::convert_deltas( - netuid, - delta_in.into() - ), - expected_buy.into(), - epsilon = 2.into() - ); - } - } - }); -} - -// #[test] -// fn test_user_liquidity_disabled() { -// new_test_ext().execute_with(|| { -// // Use a netuid above 100 since our mock enables liquidity for 0-100 -// let netuid = NetUid::from(101); -// let tick_low = TickIndex::new_unchecked(-1000); -// let tick_high = TickIndex::new_unchecked(1000); -// let position_id = PositionId::from(1); -// let liquidity = 1_000_000_000; -// let liquidity_delta = 500_000_000; - -// assert!(!EnabledUserLiquidity::::get(netuid)); - -// assert_noop!( -// Swap::do_add_liquidity( -// netuid, -// &OK_COLDKEY_ACCOUNT_ID, -// &OK_HOTKEY_ACCOUNT_ID, -// tick_low, -// tick_high, -// liquidity -// ), -// Error::::UserLiquidityDisabled -// ); - -// assert_noop!( -// Swap::do_remove_liquidity(netuid, &OK_COLDKEY_ACCOUNT_ID, position_id), -// Error::::LiquidityNotFound -// ); - -// assert_noop!( -// Swap::modify_position( -// RuntimeOrigin::signed(OK_COLDKEY_ACCOUNT_ID), -// OK_HOTKEY_ACCOUNT_ID, -// netuid, -// position_id, -// liquidity_delta -// ), -// Error::::UserLiquidityDisabled -// ); - -// assert_ok!(Swap::toggle_user_liquidity( -// RuntimeOrigin::root(), -// netuid, -// true -// )); - -// let position_id = Swap::do_add_liquidity( -// netuid, -// &OK_COLDKEY_ACCOUNT_ID, -// &OK_HOTKEY_ACCOUNT_ID, -// tick_low, -// tick_high, -// liquidity, -// ) -// .unwrap() -// .0; - -// assert_ok!(Swap::do_modify_position( -// netuid.into(), -// &OK_COLDKEY_ACCOUNT_ID, -// &OK_HOTKEY_ACCOUNT_ID, -// position_id, -// liquidity_delta, -// )); - -// assert_ok!(Swap::do_remove_liquidity( -// netuid, -// &OK_COLDKEY_ACCOUNT_ID, -// position_id, -// )); -// }); -// } - -/// Test correctness of swap fees: -/// - Fees are distribued to (concentrated) liquidity providers -/// -#[test] -fn test_swap_fee_correctness() { - new_test_ext().execute_with(|| { - let min_price = tick_to_price(TickIndex::MIN); - let max_price = tick_to_price(TickIndex::MAX); - let netuid = NetUid::from(1); - - // Provide very spread liquidity at the range from min to max that matches protocol liquidity - let liquidity = 2_000_000_000_000_u64; // 1x of protocol liquidity - - assert_ok!(Pallet::::maybe_initialize_v3(netuid)); - - // Calculate ticks - let tick_low = price_to_tick(min_price); - let tick_high = price_to_tick(max_price); - - // Add user liquidity - let (position_id, _tao, _alpha) = Pallet::::do_add_liquidity( - netuid, - &OK_COLDKEY_ACCOUNT_ID, - &OK_HOTKEY_ACCOUNT_ID, - tick_low, - tick_high, - liquidity, - ) - .unwrap(); - - // Swap buy and swap sell - Pallet::::do_swap( - netuid, - GetAlphaForTao::with_amount(liquidity / 10), - u64::MAX.into(), - false, - false, - ) - .unwrap(); - Pallet::::do_swap( - netuid, - GetTaoForAlpha::with_amount(liquidity / 10), - 0_u64.into(), - false, - false, - ) - .unwrap(); - - // Get user position - let mut position = - Positions::::get((netuid, OK_COLDKEY_ACCOUNT_ID, position_id)).unwrap(); - assert_eq!(position.liquidity, liquidity); - assert_eq!(position.tick_low, tick_low); - assert_eq!(position.tick_high, tick_high); - - // Check that 50% of fees were credited to the position - let fee_rate = FeeRate::::get(NetUid::from(netuid)) as f64 / u16::MAX as f64; - let (actual_fee_tao, actual_fee_alpha) = position.collect_fees(); - let expected_fee = (fee_rate * (liquidity / 10) as f64 * 0.5) as u64; - - assert_abs_diff_eq!(actual_fee_tao, expected_fee, epsilon = 1,); - assert_abs_diff_eq!(actual_fee_alpha, expected_fee, epsilon = 1,); - }); -} - -#[test] -fn test_current_liquidity_updates() { - let netuid = NetUid::from(1); - let liquidity = 1_000_000_000; - - // Get current price - let (current_price, current_price_low, current_price_high) = - new_test_ext().execute_with(|| { - assert_ok!(Pallet::::maybe_initialize_v3(netuid)); - let sqrt_current_price = AlphaSqrtPrice::::get(netuid); - let current_price = (sqrt_current_price * sqrt_current_price).to_num::(); - let (current_price_low, current_price_high) = get_ticked_prices_around_current_price(); - (current_price, current_price_low, current_price_high) - }); - - // Test case: (price_low, price_high, expect_to_update) - [ - // Current price is out of position range (lower), no current lq update - (current_price * 2., current_price * 3., false), - // Current price is out of position range (higher), no current lq update - (current_price / 3., current_price / 2., false), - // Current price is just below position range, no current lq update - (current_price_high, current_price * 3., false), - // Position lower edge is just below the current price, current lq updates - (current_price_low, current_price * 3., true), - // Current price is exactly at lower edge of position range, current lq updates - (current_price, current_price * 3., true), - // Current price is exactly at higher edge of position range, no current lq update - (current_price / 2., current_price, false), - ] - .into_iter() - .for_each(|(price_low, price_high, expect_to_update)| { - new_test_ext().execute_with(|| { - assert_ok!(Pallet::::maybe_initialize_v3(netuid)); - - // Calculate ticks (assuming tick math is tested separately) - let tick_low = price_to_tick(price_low); - let tick_high = price_to_tick(price_high); - let liquidity_before = CurrentLiquidity::::get(netuid); - - // Add liquidity - assert_ok!(Pallet::::do_add_liquidity( - netuid, - &OK_COLDKEY_ACCOUNT_ID, - &OK_HOTKEY_ACCOUNT_ID, - tick_low, - tick_high, - liquidity, - )); - - // Current liquidity is updated only when price range includes the current price - let expected_liquidity = if (price_high > current_price) && (price_low <= current_price) - { - assert!(expect_to_update); - liquidity_before + liquidity - } else { - assert!(!expect_to_update); - liquidity_before - }; - - assert_eq!(CurrentLiquidity::::get(netuid), expected_liquidity) - }); - }); -} + assert_abs_diff_eq!( + u64::from( + BasicSwapStep::::convert_deltas( + netuid, + delta_in.into() + ) + ), + expected_sell as u64, + epsilon = 2u64 + ); + assert_abs_diff_eq!( + u64::from( + BasicSwapStep::::convert_deltas( + netuid, + delta_in.into() + ) + ), + expected_buy as u64, + epsilon = 2u64 + ); + } + }); +} #[test] fn test_rollback_works() { @@ -1620,80 +764,7 @@ fn test_rollback_works() { }) } -/// Test correctness of swap fees: -/// - New LP is not eligible to previously accrued fees -/// -/// cargo test --package pallet-subtensor-swap --lib -- pallet::tests::test_new_lp_doesnt_get_old_fees --exact --show-output -#[test] -fn test_new_lp_doesnt_get_old_fees() { - new_test_ext().execute_with(|| { - let min_price = tick_to_price(TickIndex::MIN); - let max_price = tick_to_price(TickIndex::MAX); - let netuid = NetUid::from(1); - - // Provide very spread liquidity at the range from min to max that matches protocol liquidity - let liquidity = 2_000_000_000_000_u64; // 1x of protocol liquidity - - assert_ok!(Pallet::::maybe_initialize_v3(netuid)); - - // Calculate ticks - let tick_low = price_to_tick(min_price); - let tick_high = price_to_tick(max_price); - - // Add user liquidity - Pallet::::do_add_liquidity( - netuid, - &OK_COLDKEY_ACCOUNT_ID, - &OK_HOTKEY_ACCOUNT_ID, - tick_low, - tick_high, - liquidity, - ) - .unwrap(); - - // Swap buy and swap sell - Pallet::::do_swap( - netuid, - GetAlphaForTao::with_amount(liquidity / 10), - u64::MAX.into(), - false, - false, - ) - .unwrap(); - Pallet::::do_swap( - netuid, - GetTaoForAlpha::with_amount(liquidity / 10), - 0_u64.into(), - false, - false, - ) - .unwrap(); - - // Add liquidity from a different user to a new tick - let (position_id_2, _tao, _alpha) = Pallet::::do_add_liquidity( - netuid, - &OK_COLDKEY_ACCOUNT_ID_2, - &OK_HOTKEY_ACCOUNT_ID_2, - tick_low.next().unwrap(), - tick_high.prev().unwrap(), - liquidity, - ) - .unwrap(); - - // Get user position - let mut position = - Positions::::get((netuid, OK_COLDKEY_ACCOUNT_ID_2, position_id_2)).unwrap(); - assert_eq!(position.liquidity, liquidity); - assert_eq!(position.tick_low, tick_low.next().unwrap()); - assert_eq!(position.tick_high, tick_high.prev().unwrap()); - - // Check that collected fees are 0 - let (actual_fee_tao, actual_fee_alpha) = position.collect_fees(); - assert_abs_diff_eq!(actual_fee_tao, 0, epsilon = 1); - assert_abs_diff_eq!(actual_fee_alpha, 0, epsilon = 1); - }); -} - +#[allow(dead_code)] fn bbox(t: U64F64, a: U64F64, b: U64F64) -> U64F64 { if t < a { a @@ -1704,955 +775,40 @@ fn bbox(t: U64F64, a: U64F64, b: U64F64) -> U64F64 { } } +#[allow(dead_code)] fn print_current_price(netuid: NetUid) { - let current_sqrt_price = AlphaSqrtPrice::::get(netuid).to_num::(); - let current_price = current_sqrt_price * current_sqrt_price; + let current_price = Pallet::::current_price(netuid); log::trace!("Current price: {current_price:.6}"); } -/// RUST_LOG=pallet_subtensor_swap=trace cargo test --package pallet-subtensor-swap --lib -- pallet::tests::test_wrapping_fees --exact --show-output --nocapture +/// Simple palswap path: PalSwap is initialized. +/// Function must still clear any residual storages and succeed. #[test] -fn test_wrapping_fees() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(WRAPPING_FEES_NETUID); - let position_1_low_price = 0.20; - let position_1_high_price = 0.255; - let position_2_low_price = 0.255; - let position_2_high_price = 0.257; - assert_ok!(Pallet::::maybe_initialize_v3(netuid)); - - Pallet::::do_add_liquidity( - netuid, - &OK_COLDKEY_ACCOUNT_ID_RICH, - &OK_COLDKEY_ACCOUNT_ID_RICH, - price_to_tick(position_1_low_price), - price_to_tick(position_1_high_price), - 1_000_000_000_u64, - ) - .unwrap(); - - print_current_price(netuid); - - let order = GetTaoForAlpha::with_amount(800_000_000); - let sqrt_limit_price = SqrtPrice::from_num(0.000001); - Pallet::::do_swap(netuid, order, sqrt_limit_price, false, false).unwrap(); - - let order = GetAlphaForTao::with_amount(1_850_000_000); - let sqrt_limit_price = SqrtPrice::from_num(1_000_000.0); - - print_current_price(netuid); - - Pallet::::do_swap(netuid, order, sqrt_limit_price, false, false).unwrap(); - - print_current_price(netuid); - - let add_liquidity_result = Pallet::::do_add_liquidity( - netuid, - &OK_COLDKEY_ACCOUNT_ID_RICH, - &OK_COLDKEY_ACCOUNT_ID_RICH, - price_to_tick(position_2_low_price), - price_to_tick(position_2_high_price), - 1_000_000_000_u64, - ) - .unwrap(); - - let order = GetTaoForAlpha::with_amount(1_800_000_000); - let sqrt_limit_price = SqrtPrice::from_num(0.000001); - - let initial_sqrt_price = AlphaSqrtPrice::::get(netuid); - Pallet::::do_swap(netuid, order, sqrt_limit_price, false, false).unwrap(); - let final_sqrt_price = AlphaSqrtPrice::::get(netuid); - - print_current_price(netuid); - - let mut position = - Positions::::get((netuid, &OK_COLDKEY_ACCOUNT_ID_RICH, add_liquidity_result.0)) - .unwrap(); - - let initial_box_price = bbox( - initial_sqrt_price, - position.tick_low.try_to_sqrt_price().unwrap(), - position.tick_high.try_to_sqrt_price().unwrap(), - ); - - let final_box_price = bbox( - final_sqrt_price, - position.tick_low.try_to_sqrt_price().unwrap(), - position.tick_high.try_to_sqrt_price().unwrap(), - ); - - let fee_rate = FeeRate::::get(netuid) as f64 / u16::MAX as f64; - - log::trace!("fee_rate: {fee_rate:.6}"); - log::trace!("position.liquidity: {}", position.liquidity); - log::trace!( - "initial_box_price: {:.6}", - initial_box_price.to_num::() - ); - log::trace!("final_box_price: {:.6}", final_box_price.to_num::()); - - let expected_fee_tao = ((fee_rate / (1.0 - fee_rate)) - * (position.liquidity as f64) - * (final_box_price.to_num::() - initial_box_price.to_num::())) - as u64; - - let expected_fee_alpha = ((fee_rate / (1.0 - fee_rate)) - * (position.liquidity as f64) - * ((1.0 / final_box_price.to_num::()) - (1.0 / initial_box_price.to_num::()))) - as u64; - - log::trace!("Expected ALPHA fee: {:.6}", expected_fee_alpha as f64); - - let (fee_tao, fee_alpha) = position.collect_fees(); - - log::trace!("Collected fees: TAO: {fee_tao}, ALPHA: {fee_alpha}"); - - assert_abs_diff_eq!(fee_tao, expected_fee_tao, epsilon = 1); - assert_abs_diff_eq!(fee_alpha, expected_fee_alpha, epsilon = 1); - }); -} - -/// Test that price moves less with provided liquidity -/// cargo test --package pallet-subtensor-swap --lib -- pallet::tests::test_less_price_movement --exact --show-output -#[test] -fn test_less_price_movement() { - let netuid = NetUid::from(1); - let mut last_end_price = U96F32::from_num(0); - let initial_stake_liquidity = 1_000_000_000; - let swapped_liquidity = 1_000_000; - - // Test case is (order_type, provided_liquidity) - // Testing algorithm: - // - Stake initial_stake_liquidity - // - Provide liquidity if iteration provides lq - // - Buy or sell - // - Save end price if iteration doesn't provide lq - macro_rules! perform_test { - ($order_t:ident, $provided_liquidity:expr, $limit_price:expr, $should_price_shrink:expr) => { - let provided_liquidity = $provided_liquidity; - let should_price_shrink = $should_price_shrink; - let limit_price = $limit_price; - new_test_ext().execute_with(|| { - // Setup swap - assert_ok!(Pallet::::maybe_initialize_v3(netuid)); - - // Buy Alpha - assert_ok!(Pallet::::do_swap( - netuid, - GetAlphaForTao::with_amount(initial_stake_liquidity), - SqrtPrice::from_num(10_000_000_000_u64), - false, - false - )); - - // Get current price - let start_price = Pallet::::current_price(netuid); - - // Add liquidity if this test iteration provides - if provided_liquidity > 0 { - let tick_low = price_to_tick(start_price.to_num::() * 0.5); - let tick_high = price_to_tick(start_price.to_num::() * 1.5); - assert_ok!(Pallet::::do_add_liquidity( - netuid, - &OK_COLDKEY_ACCOUNT_ID, - &OK_HOTKEY_ACCOUNT_ID, - tick_low, - tick_high, - provided_liquidity, - )); - } - - // Swap - let sqrt_limit_price = SqrtPrice::from_num(limit_price); - assert_ok!(Pallet::::do_swap( - netuid, - $order_t::with_amount(swapped_liquidity), - sqrt_limit_price, - false, - false - )); - - let end_price = Pallet::::current_price(netuid); - - // Save end price if iteration doesn't provide or compare with previous end price if - // it does - if provided_liquidity > 0 { - assert_eq!(should_price_shrink, end_price < last_end_price); - } else { - last_end_price = end_price; - } - }); - }; - } - - for provided_liquidity in [0, 1_000_000_000_000_u64] { - perform_test!(GetAlphaForTao, provided_liquidity, 1000.0_f64, true); - } - for provided_liquidity in [0, 1_000_000_000_000_u64] { - perform_test!(GetTaoForAlpha, provided_liquidity, 0.001_f64, false); - } -} - -// TODO: Revise when user liquidity is available -// #[test] -// fn test_swap_subtoken_disabled() { -// new_test_ext().execute_with(|| { -// let netuid = NetUid::from(SUBTOKEN_DISABLED_NETUID); // Use a netuid not used elsewhere -// let price_low = 0.1; -// let price_high = 0.2; -// let tick_low = price_to_tick(price_low); -// let tick_high = price_to_tick(price_high); -// let liquidity = 1_000_000_u64; - -// assert_ok!(Pallet::::maybe_initialize_v3(netuid)); - -// assert_noop!( -// Pallet::::add_liquidity( -// RuntimeOrigin::signed(OK_COLDKEY_ACCOUNT_ID), -// OK_HOTKEY_ACCOUNT_ID, -// netuid, -// tick_low, -// tick_high, -// liquidity, -// ), -// Error::::SubtokenDisabled -// ); - -// assert_noop!( -// Pallet::::modify_position( -// RuntimeOrigin::signed(OK_COLDKEY_ACCOUNT_ID), -// OK_HOTKEY_ACCOUNT_ID, -// netuid, -// PositionId::from(0), -// liquidity as i64, -// ), -// Error::::SubtokenDisabled -// ); -// }); -// } - -#[test] -fn test_liquidate_v3_removes_positions_ticks_and_state() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - - // Initialize V3 (creates protocol position, ticks, price, liquidity) - assert_ok!(Pallet::::maybe_initialize_v3(netuid)); - assert!(SwapV3Initialized::::get(netuid)); - - // Enable user LP - assert_ok!(Swap::toggle_user_liquidity( - RuntimeOrigin::root(), - netuid.into(), - true - )); - - // Add a user position across the full range to ensure ticks/bitmap are populated. - let min_price = tick_to_price(TickIndex::MIN); - let max_price = tick_to_price(TickIndex::MAX); - let tick_low = price_to_tick(min_price); - let tick_high = price_to_tick(max_price); - let liquidity = 2_000_000_000_u64; - - let (_pos_id, _tao, _alpha) = Pallet::::do_add_liquidity( - netuid, - &OK_COLDKEY_ACCOUNT_ID, - &OK_HOTKEY_ACCOUNT_ID, - tick_low, - tick_high, - liquidity, - ) - .expect("add liquidity"); - - // Accrue some global fees so we can verify fee storage is cleared later. - let sqrt_limit_price = SqrtPrice::from_num(1_000_000.0); - assert_ok!(Pallet::::do_swap( - netuid, - GetAlphaForTao::with_amount(1_000_000), - sqrt_limit_price, - false, - false - )); - - // Sanity: protocol & user positions exist, ticks exist, liquidity > 0 - let protocol_id = Pallet::::protocol_account_id(); - let prot_positions = - Positions::::iter_prefix_values((netuid, protocol_id)).collect::>(); - assert!(!prot_positions.is_empty()); - - let user_positions = Positions::::iter_prefix_values((netuid, OK_COLDKEY_ACCOUNT_ID)) - .collect::>(); - assert_eq!(user_positions.len(), 1); - - assert!(Ticks::::get(netuid, TickIndex::MIN).is_some()); - assert!(Ticks::::get(netuid, TickIndex::MAX).is_some()); - assert!(CurrentLiquidity::::get(netuid) > 0); - - let had_bitmap_words = TickIndexBitmapWords::::iter_prefix((netuid,)) - .next() - .is_some(); - assert!(had_bitmap_words); - - // ACT: users-only liquidation then protocol clear - assert_ok!(Pallet::::do_dissolve_all_liquidity_providers(netuid)); - assert_ok!(Pallet::::do_clear_protocol_liquidity(netuid)); - - // ASSERT: positions cleared (both user and protocol) - assert_eq!( - Pallet::::count_positions(netuid, &OK_COLDKEY_ACCOUNT_ID), - 0 - ); - let prot_positions_after = - Positions::::iter_prefix_values((netuid, protocol_id)).collect::>(); - assert!(prot_positions_after.is_empty()); - let user_positions_after = - Positions::::iter_prefix_values((netuid, OK_COLDKEY_ACCOUNT_ID)) - .collect::>(); - assert!(user_positions_after.is_empty()); - - // ASSERT: ticks cleared - assert!(Ticks::::iter_prefix(netuid).next().is_none()); - assert!(Ticks::::get(netuid, TickIndex::MIN).is_none()); - assert!(Ticks::::get(netuid, TickIndex::MAX).is_none()); - - // ASSERT: fee globals cleared - assert!(!FeeGlobalTao::::contains_key(netuid)); - assert!(!FeeGlobalAlpha::::contains_key(netuid)); - - // ASSERT: price/tick/liquidity flags cleared - assert!(!AlphaSqrtPrice::::contains_key(netuid)); - assert!(!CurrentTick::::contains_key(netuid)); - assert!(!CurrentLiquidity::::contains_key(netuid)); - assert!(!SwapV3Initialized::::contains_key(netuid)); - - // ASSERT: active tick bitmap cleared - assert!( - TickIndexBitmapWords::::iter_prefix((netuid,)) - .next() - .is_none() - ); - - // ASSERT: knobs removed on dereg - assert!(!FeeRate::::contains_key(netuid)); - assert!(!EnabledUserLiquidity::::contains_key(netuid)); - }); -} - -// V3 path with user liquidity disabled at teardown: -// must still remove positions and clear state (after protocol clear). -// #[test] -// fn test_liquidate_v3_with_user_liquidity_disabled() { -// new_test_ext().execute_with(|| { -// let netuid = NetUid::from(101); - -// assert_ok!(Pallet::::maybe_initialize_v3(netuid)); -// assert!(SwapV3Initialized::::get(netuid)); - -// // Enable temporarily to add a user position -// assert_ok!(Swap::toggle_user_liquidity( -// RuntimeOrigin::root(), -// netuid.into(), -// true -// )); - -// let min_price = tick_to_price(TickIndex::MIN); -// let max_price = tick_to_price(TickIndex::MAX); -// let tick_low = price_to_tick(min_price); -// let tick_high = price_to_tick(max_price); -// let liquidity = 1_000_000_000_u64; - -// let (_pos_id, _tao, _alpha) = Pallet::::do_add_liquidity( -// netuid, -// &OK_COLDKEY_ACCOUNT_ID, -// &OK_HOTKEY_ACCOUNT_ID, -// tick_low, -// tick_high, -// liquidity, -// ) -// .expect("add liquidity"); - -// // Disable user LP *before* liquidation; removal must ignore this flag. -// assert_ok!(Swap::toggle_user_liquidity( -// RuntimeOrigin::root(), -// netuid.into(), -// false -// )); - -// // Users-only dissolve, then clear protocol liquidity/state. -// assert_ok!(Pallet::::do_dissolve_all_liquidity_providers(netuid)); -// assert_ok!(Pallet::::do_clear_protocol_liquidity(netuid)); - -// // ASSERT: positions & ticks gone, state reset -// assert_eq!( -// Pallet::::count_positions(netuid, &OK_COLDKEY_ACCOUNT_ID), -// 0 -// ); -// assert!( -// Positions::::iter_prefix_values((netuid, OK_COLDKEY_ACCOUNT_ID)) -// .next() -// .is_none() -// ); -// assert!(Ticks::::iter_prefix(netuid).next().is_none()); -// assert!( -// TickIndexBitmapWords::::iter_prefix((netuid,)) -// .next() -// .is_none() -// ); -// assert!(!SwapV3Initialized::::contains_key(netuid)); -// assert!(!AlphaSqrtPrice::::contains_key(netuid)); -// assert!(!CurrentTick::::contains_key(netuid)); -// assert!(!CurrentLiquidity::::contains_key(netuid)); -// assert!(!FeeGlobalTao::::contains_key(netuid)); -// assert!(!FeeGlobalAlpha::::contains_key(netuid)); - -// // `EnabledUserLiquidity` is removed by protocol clear stage. -// assert!(!EnabledUserLiquidity::::contains_key(netuid)); -// }); -// } - -/// Non‑V3 path: V3 not initialized (no positions); function must still clear any residual storages and succeed. -#[test] -fn test_liquidate_non_v3_uninitialized_ok_and_clears() { +fn test_liquidate_pal_simple_ok_and_clears() { new_test_ext().execute_with(|| { let netuid = NetUid::from(202); - // Sanity: V3 is not initialized - assert!(!SwapV3Initialized::::get(netuid)); - assert!( - Positions::::iter_prefix_values((netuid, OK_COLDKEY_ACCOUNT_ID)) - .next() - .is_none() - ); - - // ACT - assert_ok!(Pallet::::do_dissolve_all_liquidity_providers(netuid)); - - // ASSERT: Defensive clears leave no residues and do not panic - assert!( - Positions::::iter_prefix_values((netuid, OK_COLDKEY_ACCOUNT_ID)) - .next() - .is_none() - ); - assert!(Ticks::::iter_prefix(netuid).next().is_none()); - assert!( - TickIndexBitmapWords::::iter_prefix((netuid,)) - .next() - .is_none() - ); - - // All single-key maps should not have the key after liquidation - assert!(!FeeGlobalTao::::contains_key(netuid)); - assert!(!FeeGlobalAlpha::::contains_key(netuid)); - assert!(!CurrentLiquidity::::contains_key(netuid)); - assert!(!CurrentTick::::contains_key(netuid)); - assert!(!AlphaSqrtPrice::::contains_key(netuid)); - assert!(!SwapV3Initialized::::contains_key(netuid)); - assert!(!FeeRate::::contains_key(netuid)); - assert!(!EnabledUserLiquidity::::contains_key(netuid)); - }); -} - -#[test] -fn test_liquidate_idempotent() { - // V3 flavor - new_test_ext().execute_with(|| { - let netuid = NetUid::from(7); - assert_ok!(Pallet::::maybe_initialize_v3(netuid)); - - // Add a small user position - assert_ok!(Swap::toggle_user_liquidity( - RuntimeOrigin::root(), - netuid.into(), - true - )); - let tick_low = price_to_tick(0.2); - let tick_high = price_to_tick(0.3); - assert_ok!(Pallet::::do_add_liquidity( - netuid, - &OK_COLDKEY_ACCOUNT_ID, - &OK_HOTKEY_ACCOUNT_ID, - tick_low, - tick_high, - 123_456_789 - )); - - // Users-only liquidations are idempotent. - assert_ok!(Pallet::::do_dissolve_all_liquidity_providers(netuid)); - assert_ok!(Pallet::::do_dissolve_all_liquidity_providers(netuid)); - - // Now clear protocol liquidity/state—also idempotent. - assert_ok!(Pallet::::do_clear_protocol_liquidity(netuid)); - assert_ok!(Pallet::::do_clear_protocol_liquidity(netuid)); - - // State remains empty - assert!( - Positions::::iter_prefix_values((netuid, OK_COLDKEY_ACCOUNT_ID)) - .next() - .is_none() - ); - assert!(Ticks::::iter_prefix(netuid).next().is_none()); - assert!( - TickIndexBitmapWords::::iter_prefix((netuid,)) - .next() - .is_none() - ); - assert!(!SwapV3Initialized::::contains_key(netuid)); - }); - - // Non‑V3 flavor - new_test_ext().execute_with(|| { - let netuid = NetUid::from(8); - - // Never initialize V3; both calls no-op and succeed. - assert_ok!(Pallet::::do_dissolve_all_liquidity_providers(netuid)); - assert_ok!(Pallet::::do_dissolve_all_liquidity_providers(netuid)); - - assert!( - Positions::::iter_prefix_values((netuid, OK_COLDKEY_ACCOUNT_ID)) - .next() - .is_none() - ); - assert!(Ticks::::iter_prefix(netuid).next().is_none()); - assert!( - TickIndexBitmapWords::::iter_prefix((netuid,)) - .next() - .is_none() - ); - assert!(!SwapV3Initialized::::contains_key(netuid)); - }); -} - -#[test] -fn liquidate_v3_refunds_user_funds_and_clears_state() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - - // Enable V3 path & initialize price/ticks (also creates a protocol position). - assert_ok!(Pallet::::toggle_user_liquidity( - RuntimeOrigin::root(), - netuid, - true - )); - assert_ok!(Pallet::::maybe_initialize_v3(netuid)); - - // Use distinct cold/hot to demonstrate alpha refund/stake accounting. - let cold = OK_COLDKEY_ACCOUNT_ID; - let hot = OK_HOTKEY_ACCOUNT_ID; - - // Tight in‑range band around current tick. - let ct = CurrentTick::::get(netuid); - let tick_low = ct.saturating_sub(10); - let tick_high = ct.saturating_add(10); - let liquidity: u64 = 1_000_000; - - // Snapshot balances BEFORE. - let tao_before = ::BalanceOps::tao_balance(&cold); - let alpha_before_hot = - ::BalanceOps::alpha_balance(netuid.into(), &cold, &hot); - let alpha_before_owner = - ::BalanceOps::alpha_balance(netuid.into(), &cold, &cold); - let alpha_before_total = alpha_before_hot + alpha_before_owner; - - // Create the user position (storage & v3 state only; no balances moved yet). - let (_pos_id, need_tao, need_alpha) = - Pallet::::do_add_liquidity(netuid, &cold, &hot, tick_low, tick_high, liquidity) - .expect("add liquidity"); - - // Mirror extrinsic bookkeeping: withdraw funds & bump provided‑reserve counters. - let tao_taken = ::BalanceOps::decrease_balance(&cold, need_tao.into()) - .expect("decrease TAO"); - let alpha_taken = ::BalanceOps::decrease_stake( - &cold, - &hot, - netuid.into(), - need_alpha.into(), - ) - .expect("decrease ALPHA"); - TaoReserve::increase_provided(netuid.into(), tao_taken); - AlphaReserve::increase_provided(netuid.into(), alpha_taken); - - // Users‑only liquidation. - assert_ok!(Pallet::::do_dissolve_all_liquidity_providers(netuid)); - - // Expect balances restored to BEFORE snapshots (no swaps ran -> zero fees). - let tao_after = ::BalanceOps::tao_balance(&cold); - assert_eq!(tao_after, tao_before, "TAO principal must be refunded"); - - // ALPHA totals conserved to owner (distribution may differ). - let alpha_after_hot = - ::BalanceOps::alpha_balance(netuid.into(), &cold, &hot); - let alpha_after_owner = - ::BalanceOps::alpha_balance(netuid.into(), &cold, &cold); - let alpha_after_total = alpha_after_hot + alpha_after_owner; - assert_eq!( - alpha_after_total, alpha_before_total, - "ALPHA principal must be refunded/staked for the account (check totals)" - ); - - // Clear protocol liquidity and V3 state now. - assert_ok!(Pallet::::do_clear_protocol_liquidity(netuid)); - - // User position(s) are gone and all V3 state cleared. - assert_eq!(Pallet::::count_positions(netuid, &cold), 0); - assert!(Ticks::::iter_prefix(netuid).next().is_none()); - assert!(!SwapV3Initialized::::contains_key(netuid)); - }); -} - -#[test] -fn refund_alpha_single_provider_exact() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(11); - let cold = OK_COLDKEY_ACCOUNT_ID; - let hot = OK_HOTKEY_ACCOUNT_ID; - - assert_ok!(Pallet::::maybe_initialize_v3(netuid)); - - // --- Create an alpha‑only position (range entirely above current tick → TAO = 0, ALPHA > 0). - let ct = CurrentTick::::get(netuid); - let tick_low = ct.next().expect("current tick should not be MAX in tests"); - let tick_high = TickIndex::MAX; - - let liquidity = 1_000_000_u64; - let (_pos_id, tao_needed, alpha_needed) = - Pallet::::do_add_liquidity(netuid, &cold, &hot, tick_low, tick_high, liquidity) - .expect("add alpha-only liquidity"); - assert_eq!(tao_needed, 0, "alpha-only position must not require TAO"); - assert!(alpha_needed > 0, "alpha-only position must require ALPHA"); - - // --- Snapshot BEFORE we withdraw funds (baseline for conservation). - let alpha_before_hot = - ::BalanceOps::alpha_balance(netuid.into(), &cold, &hot); - let alpha_before_owner = - ::BalanceOps::alpha_balance(netuid.into(), &cold, &cold); - let alpha_before_total = alpha_before_hot + alpha_before_owner; - - // --- Mimic extrinsic bookkeeping: withdraw α and record provided reserve. - let alpha_taken = ::BalanceOps::decrease_stake( - &cold, - &hot, - netuid.into(), - alpha_needed.into(), - ) - .expect("decrease ALPHA"); - AlphaReserve::increase_provided(netuid.into(), alpha_taken); - - // --- Act: users‑only dissolve. - assert_ok!(Pallet::::do_dissolve_all_liquidity_providers(netuid)); - - // --- Assert: total α conserved to owner (may be staked to validator). - let alpha_after_hot = - ::BalanceOps::alpha_balance(netuid.into(), &cold, &hot); - let alpha_after_owner = - ::BalanceOps::alpha_balance(netuid.into(), &cold, &cold); - let alpha_after_total = alpha_after_hot + alpha_after_owner; - assert_eq!( - alpha_after_total, alpha_before_total, - "ALPHA principal must be conserved to the account" - ); - - // Clear protocol liquidity and V3 state now. - assert_ok!(Pallet::::do_clear_protocol_liquidity(netuid)); - - // --- State is cleared. - assert!(Ticks::::iter_prefix(netuid).next().is_none()); - assert_eq!(Pallet::::count_positions(netuid, &cold), 0); - assert!(!SwapV3Initialized::::contains_key(netuid)); - }); -} - -#[test] -fn refund_alpha_multiple_providers_proportional_to_principal() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(12); - let c1 = OK_COLDKEY_ACCOUNT_ID; - let h1 = OK_HOTKEY_ACCOUNT_ID; - let c2 = OK_COLDKEY_ACCOUNT_ID_2; - let h2 = OK_HOTKEY_ACCOUNT_ID_2; - - assert_ok!(Pallet::::maybe_initialize_v3(netuid)); - - // Use the same "above current tick" trick for alpha‑only positions. - let ct = CurrentTick::::get(netuid); - let tick_low = ct.next().expect("current tick should not be MAX in tests"); - let tick_high = TickIndex::MAX; - - // Provider #1 (smaller α) - let liq1 = 700_000_u64; - let (_p1, t1, a1) = - Pallet::::do_add_liquidity(netuid, &c1, &h1, tick_low, tick_high, liq1) - .expect("add alpha-only liquidity #1"); - assert_eq!(t1, 0); - assert!(a1 > 0); - - // Provider #2 (larger α) - let liq2 = 2_100_000_u64; - let (_p2, t2, a2) = - Pallet::::do_add_liquidity(netuid, &c2, &h2, tick_low, tick_high, liq2) - .expect("add alpha-only liquidity #2"); - assert_eq!(t2, 0); - assert!(a2 > 0); - - // Baselines BEFORE withdrawing - let a1_before_hot = ::BalanceOps::alpha_balance(netuid.into(), &c1, &h1); - let a1_before_owner = ::BalanceOps::alpha_balance(netuid.into(), &c1, &c1); - let a1_before = a1_before_hot + a1_before_owner; - - let a2_before_hot = ::BalanceOps::alpha_balance(netuid.into(), &c2, &h2); - let a2_before_owner = ::BalanceOps::alpha_balance(netuid.into(), &c2, &c2); - let a2_before = a2_before_hot + a2_before_owner; - - // Withdraw α and account reserves for each provider. - let a1_taken = - ::BalanceOps::decrease_stake(&c1, &h1, netuid.into(), a1.into()) - .expect("decrease α #1"); - AlphaReserve::increase_provided(netuid.into(), a1_taken); - - let a2_taken = - ::BalanceOps::decrease_stake(&c2, &h2, netuid.into(), a2.into()) - .expect("decrease α #2"); - AlphaReserve::increase_provided(netuid.into(), a2_taken); - - // Act - assert_ok!(Pallet::::do_dissolve_all_liquidity_providers(netuid)); - - // Each owner is restored to their exact baseline. - let a1_after_hot = ::BalanceOps::alpha_balance(netuid.into(), &c1, &h1); - let a1_after_owner = ::BalanceOps::alpha_balance(netuid.into(), &c1, &c1); - let a1_after = a1_after_hot + a1_after_owner; - assert_eq!( - a1_after, a1_before, - "owner #1 must receive their α principal back" - ); + // Insert map values + FeeRate::::insert(netuid, 1_000); + FeesTao::::insert(netuid, TaoCurrency::from(1_000)); + FeesAlpha::::insert(netuid, AlphaCurrency::from(1_000)); + PalSwapInitialized::::insert(netuid, true); + let w_quote_pt = Perquintill::from_rational(1u128, 2u128); + let bal = Balancer::new(w_quote_pt).unwrap(); + SwapBalancer::::insert(netuid, bal); - let a2_after_hot = ::BalanceOps::alpha_balance(netuid.into(), &c2, &h2); - let a2_after_owner = ::BalanceOps::alpha_balance(netuid.into(), &c2, &c2); - let a2_after = a2_after_hot + a2_after_owner; - assert_eq!( - a2_after, a2_before, - "owner #2 must receive their α principal back" - ); - }); -} - -#[test] -fn refund_alpha_same_cold_multiple_hotkeys_conserved_to_owner() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(13); - let cold = OK_COLDKEY_ACCOUNT_ID; - let hot1 = OK_HOTKEY_ACCOUNT_ID; - let hot2 = OK_HOTKEY_ACCOUNT_ID_2; - - assert_ok!(Pallet::::maybe_initialize_v3(netuid)); - - // Two alpha‑only positions on different hotkeys of the same owner. - let ct = CurrentTick::::get(netuid); - let tick_low = ct.next().expect("current tick should not be MAX in tests"); - let tick_high = TickIndex::MAX; - - let (_p1, _t1, a1) = - Pallet::::do_add_liquidity(netuid, &cold, &hot1, tick_low, tick_high, 900_000) - .expect("add alpha-only pos (hot1)"); - let (_p2, _t2, a2) = - Pallet::::do_add_liquidity(netuid, &cold, &hot2, tick_low, tick_high, 1_500_000) - .expect("add alpha-only pos (hot2)"); - assert!(a1 > 0 && a2 > 0); - - // Baseline BEFORE: sum over (cold,hot1) + (cold,hot2) + (cold,cold). - let before_hot1 = ::BalanceOps::alpha_balance(netuid.into(), &cold, &hot1); - let before_hot2 = ::BalanceOps::alpha_balance(netuid.into(), &cold, &hot2); - let before_owner = ::BalanceOps::alpha_balance(netuid.into(), &cold, &cold); - let before_total = before_hot1 + before_hot2 + before_owner; - - // Withdraw α from both hotkeys; track provided‑reserve. - let t1 = - ::BalanceOps::decrease_stake(&cold, &hot1, netuid.into(), a1.into()) - .expect("decr α #hot1"); - AlphaReserve::increase_provided(netuid.into(), t1); - - let t2 = - ::BalanceOps::decrease_stake(&cold, &hot2, netuid.into(), a2.into()) - .expect("decr α #hot2"); - AlphaReserve::increase_provided(netuid.into(), t2); - - // Act - assert_ok!(Pallet::::do_dissolve_all_liquidity_providers(netuid)); - - // The total α "owned" by the coldkey is conserved (credit may land on (cold,cold)). - let after_hot1 = ::BalanceOps::alpha_balance(netuid.into(), &cold, &hot1); - let after_hot2 = ::BalanceOps::alpha_balance(netuid.into(), &cold, &hot2); - let after_owner = ::BalanceOps::alpha_balance(netuid.into(), &cold, &cold); - let after_total = after_hot1 + after_hot2 + after_owner; + // Sanity: PalSwap is not initialized + assert!(PalSwapInitialized::::get(netuid)); - assert_eq!( - after_total, before_total, - "owner’s α must be conserved across hot ledgers + (owner,owner)" - ); - }); -} - -#[test] -fn test_dissolve_v3_green_path_refund_tao_stake_alpha_and_clear_state() { - new_test_ext().execute_with(|| { - // --- Setup --- - let netuid = NetUid::from(42); - let cold = OK_COLDKEY_ACCOUNT_ID; - let hot = OK_HOTKEY_ACCOUNT_ID; - - assert_ok!(Swap::toggle_user_liquidity( - RuntimeOrigin::root(), - netuid.into(), - true - )); - assert_ok!(Pallet::::maybe_initialize_v3(netuid)); - assert!(SwapV3Initialized::::get(netuid)); - - // Tight in‑range band so BOTH τ and α are required. - let ct = CurrentTick::::get(netuid); - let tick_low = ct.saturating_sub(10); - let tick_high = ct.saturating_add(10); - let liquidity: u64 = 1_250_000; - - // Add liquidity and capture required τ/α. - let (_pos_id, tao_needed, alpha_needed) = - Pallet::::do_add_liquidity(netuid, &cold, &hot, tick_low, tick_high, liquidity) - .expect("add in-range liquidity"); - assert!(tao_needed > 0, "in-range pos must require TAO"); - assert!(alpha_needed > 0, "in-range pos must require ALPHA"); - - // Determine the permitted validator with the highest trust (green path). - let trust = ::SubnetInfo::get_validator_trust(netuid.into()); - let permit = ::SubnetInfo::get_validator_permit(netuid.into()); - assert_eq!(trust.len(), permit.len(), "trust/permit must align"); - let target_uid: u16 = trust - .iter() - .zip(permit.iter()) - .enumerate() - .filter(|(_, (_t, p))| **p) - .max_by_key(|(_, (t, _))| *t) - .map(|(i, _)| i as u16) - .expect("at least one permitted validator"); - let validator_hotkey: ::AccountId = - ::SubnetInfo::hotkey_of_uid(netuid.into(), target_uid) - .expect("uid -> hotkey mapping must exist"); - - // --- Snapshot BEFORE we withdraw τ/α to fund the position --- - let tao_before = ::BalanceOps::tao_balance(&cold); - - let alpha_before_hot = - ::BalanceOps::alpha_balance(netuid.into(), &cold, &hot); - let alpha_before_owner = - ::BalanceOps::alpha_balance(netuid.into(), &cold, &cold); - let alpha_before_val = - ::BalanceOps::alpha_balance(netuid.into(), &cold, &validator_hotkey); - - let alpha_before_total = if validator_hotkey == hot { - alpha_before_hot + alpha_before_owner - } else { - alpha_before_hot + alpha_before_owner + alpha_before_val - }; - - // --- Mirror extrinsic bookkeeping: withdraw τ & α; bump provided reserves --- - let tao_taken = ::BalanceOps::decrease_balance(&cold, tao_needed.into()) - .expect("decrease TAO"); - let alpha_taken = ::BalanceOps::decrease_stake( - &cold, - &hot, - netuid.into(), - alpha_needed.into(), - ) - .expect("decrease ALPHA"); - - TaoReserve::increase_provided(netuid.into(), tao_taken); - AlphaReserve::increase_provided(netuid.into(), alpha_taken); - - // --- Act: dissolve (GREEN PATH: permitted validators exist) --- - assert_ok!(Pallet::::do_dissolve_all_liquidity_providers(netuid)); - - // --- Assert: τ principal refunded to user --- - let tao_after = ::BalanceOps::tao_balance(&cold); - assert_eq!(tao_after, tao_before, "TAO principal must be refunded"); - - // --- α ledger assertions --- - let alpha_after_hot = - ::BalanceOps::alpha_balance(netuid.into(), &cold, &hot); - let alpha_after_owner = - ::BalanceOps::alpha_balance(netuid.into(), &cold, &cold); - let alpha_after_val = - ::BalanceOps::alpha_balance(netuid.into(), &cold, &validator_hotkey); - - // Owner ledger must be unchanged in the green path. - assert_eq!( - alpha_after_owner, alpha_before_owner, - "Owner α ledger must be unchanged (staked to validator, not refunded)" - ); - - if validator_hotkey == hot { - assert_eq!( - alpha_after_hot, alpha_before_hot, - "When validator == hotkey, user's hot ledger must net back to its original balance" - ); - let alpha_after_total = alpha_after_hot + alpha_after_owner; - assert_eq!( - alpha_after_total, alpha_before_total, - "Total α for the coldkey must be conserved (validator==hotkey)" - ); - } else { - assert!( - alpha_before_hot >= alpha_after_hot, - "hot ledger should not increase" - ); - assert!( - alpha_after_val >= alpha_before_val, - "validator ledger should not decrease" - ); - - let hot_loss = alpha_before_hot - alpha_after_hot; - let val_gain = alpha_after_val - alpha_before_val; - assert_eq!( - val_gain, hot_loss, - "α that left the user's hot ledger must equal α credited to the validator ledger" - ); - - let alpha_after_total = alpha_after_hot + alpha_after_owner + alpha_after_val; - assert_eq!( - alpha_after_total, alpha_before_total, - "Total α for the coldkey must be conserved" - ); - } - - // Now clear protocol liquidity & state and assert full reset. + // ACT assert_ok!(Pallet::::do_clear_protocol_liquidity(netuid)); - let protocol_id = Pallet::::protocol_account_id(); - assert_eq!(Pallet::::count_positions(netuid, &cold), 0); - let prot_positions_after = - Positions::::iter_prefix_values((netuid, protocol_id)).collect::>(); - assert!( - prot_positions_after.is_empty(), - "protocol positions must be removed" - ); - - assert!(Ticks::::iter_prefix(netuid).next().is_none()); - assert!(Ticks::::get(netuid, TickIndex::MIN).is_none()); - assert!(Ticks::::get(netuid, TickIndex::MAX).is_none()); - assert!(!CurrentLiquidity::::contains_key(netuid)); - assert!(!CurrentTick::::contains_key(netuid)); - assert!(!AlphaSqrtPrice::::contains_key(netuid)); - assert!(!SwapV3Initialized::::contains_key(netuid)); - - assert!(!FeeGlobalTao::::contains_key(netuid)); - assert!(!FeeGlobalAlpha::::contains_key(netuid)); - - assert!( - TickIndexBitmapWords::::iter_prefix((netuid,)) - .next() - .is_none(), - "active tick bitmap words must be cleared" - ); - + // All single-key maps should not have the key after liquidation assert!(!FeeRate::::contains_key(netuid)); - assert!(!EnabledUserLiquidity::::contains_key(netuid)); + assert!(!FeesTao::::contains_key(netuid)); + assert!(!FeesAlpha::::contains_key(netuid)); + assert!(!PalSwapInitialized::::contains_key(netuid)); + assert!(!SwapBalancer::::contains_key(netuid)); }); } @@ -2660,84 +816,36 @@ fn test_dissolve_v3_green_path_refund_tao_stake_alpha_and_clear_state() { fn test_clear_protocol_liquidity_green_path() { new_test_ext().execute_with(|| { // --- Arrange --- - let netuid = NetUid::from(55); - - // Ensure the "user liquidity enabled" flag exists so we can verify it's removed later. - assert_ok!(Pallet::::toggle_user_liquidity( - RuntimeOrigin::root(), - netuid, - true - )); - - // Initialize V3 state; this should set price/tick flags and create a protocol position. - assert_ok!(Pallet::::maybe_initialize_v3(netuid)); - assert!( - SwapV3Initialized::::get(netuid), - "V3 must be initialized" - ); + let netuid = NetUid::from(1); - // Sanity: protocol positions exist before clearing. - let protocol_id = Pallet::::protocol_account_id(); - let prot_positions_before = - Positions::::iter_prefix_values((netuid, protocol_id)).collect::>(); + // Initialize swap state + assert_ok!(Pallet::::maybe_initialize_palswap(netuid, None)); assert!( - !prot_positions_before.is_empty(), - "protocol positions should exist after V3 init" + PalSwapInitialized::::get(netuid), + "Swap must be initialized" ); // --- Act --- // Green path: just clear protocol liquidity and wipe all V3 state. assert_ok!(Pallet::::do_clear_protocol_liquidity(netuid)); - // --- Assert: all protocol positions removed --- - let prot_positions_after = - Positions::::iter_prefix_values((netuid, protocol_id)).collect::>(); - assert!( - prot_positions_after.is_empty(), - "protocol positions must be removed by do_clear_protocol_liquidity" - ); - - // --- Assert: V3 data wiped (idempotent even if some maps were empty) --- - // Ticks / active tick bitmap - assert!(Ticks::::iter_prefix(netuid).next().is_none()); - assert!( - TickIndexBitmapWords::::iter_prefix((netuid,)) - .next() - .is_none(), - "active tick bitmap words must be cleared" - ); - // Fee globals - assert!(!FeeGlobalTao::::contains_key(netuid)); - assert!(!FeeGlobalAlpha::::contains_key(netuid)); + assert!(!FeesTao::::contains_key(netuid)); + assert!(!FeesAlpha::::contains_key(netuid)); - // Price / tick / liquidity / flags - assert!(!AlphaSqrtPrice::::contains_key(netuid)); - assert!(!CurrentTick::::contains_key(netuid)); - assert!(!CurrentLiquidity::::contains_key(netuid)); - assert!(!SwapV3Initialized::::contains_key(netuid)); + // Flags + assert!(!PalSwapInitialized::::contains_key(netuid)); // Knobs removed assert!(!FeeRate::::contains_key(netuid)); - assert!(!EnabledUserLiquidity::::contains_key(netuid)); // --- And it's idempotent --- assert_ok!(Pallet::::do_clear_protocol_liquidity(netuid)); - assert!( - Positions::::iter_prefix_values((netuid, protocol_id)) - .next() - .is_none() - ); - assert!(Ticks::::iter_prefix(netuid).next().is_none()); - assert!( - TickIndexBitmapWords::::iter_prefix((netuid,)) - .next() - .is_none() - ); - assert!(!SwapV3Initialized::::contains_key(netuid)); + assert!(!PalSwapInitialized::::contains_key(netuid)); }); } +#[allow(dead_code)] fn as_tuple( (t_used, a_used, t_rem, a_rem): (TaoCurrency, AlphaCurrency, TaoCurrency, AlphaCurrency), ) -> (u64, u64, u64, u64) { @@ -2749,160 +857,43 @@ fn as_tuple( ) } +// cargo test --package pallet-subtensor-swap --lib -- pallet::tests::test_migrate_swapv3_to_balancer --exact --nocapture #[test] -fn proportional_when_price_is_one_and_tao_is_plenty() { - // sqrt_price = 1.0 => price = 1.0 - let sqrt = U64F64::from_num(1u64); - let amount_tao: TaoCurrency = 10u64.into(); - let amount_alpha: AlphaCurrency = 3u64.into(); - - // alpha * price = 3 * 1 = 3 <= amount_tao(10) - let out = - Pallet::::get_proportional_alpha_tao_and_remainders(sqrt, amount_tao, amount_alpha); - assert_eq!(as_tuple(out), (3, 3, 7, 0)); -} - -#[test] -fn proportional_when_price_is_one_and_alpha_is_excess() { - // sqrt_price = 1.0 => price = 1.0 - let sqrt = U64F64::from_num(1u64); - let amount_tao: TaoCurrency = 5u64.into(); - let amount_alpha: AlphaCurrency = 10u64.into(); - - // tao is limiting: alpha_equiv = floor(5 / 1) = 5 - let out = - Pallet::::get_proportional_alpha_tao_and_remainders(sqrt, amount_tao, amount_alpha); - assert_eq!(as_tuple(out), (5, 5, 0, 5)); -} - -#[test] -fn proportional_with_higher_price_and_alpha_limiting() { - // Choose sqrt_price = 2.0 => price = 4.0 (since implementation squares it) - let sqrt = U64F64::from_num(2u64); - let amount_tao: TaoCurrency = 85u64.into(); - let amount_alpha: AlphaCurrency = 20u64.into(); - - // tao_equivalent = alpha * price = 20 * 4 = 80 < 85 => alpha limits tao - // remainders: tao 5, alpha 0 - let out = - Pallet::::get_proportional_alpha_tao_and_remainders(sqrt, amount_tao, amount_alpha); - assert_eq!(as_tuple(out), (80, 20, 5, 0)); -} - -#[test] -fn proportional_with_higher_price_and_tao_limiting() { - // Choose sqrt_price = 2.0 => price = 4.0 (since implementation squares it) - let sqrt = U64F64::from_num(2u64); - let amount_tao: TaoCurrency = 50u64.into(); - let amount_alpha: AlphaCurrency = 20u64.into(); - - // tao_equivalent = alpha * price = 20 * 4 = 80 > 50 => tao limits alpha - // alpha_equivalent = floor(50 / 4) = 12 - // remainders: tao 0, alpha 20 - 12 = 8 - let out = - Pallet::::get_proportional_alpha_tao_and_remainders(sqrt, amount_tao, amount_alpha); - assert_eq!(as_tuple(out), (50, 12, 0, 8)); -} - -#[test] -fn zero_price_uses_no_tao_and_all_alpha() { - // sqrt_price = 0 => price = 0 - let sqrt = U64F64::from_num(0u64); - let amount_tao: TaoCurrency = 42u64.into(); - let amount_alpha: AlphaCurrency = 17u64.into(); - - // tao_equivalent = 17 * 0 = 0 <= 42 - let out = - Pallet::::get_proportional_alpha_tao_and_remainders(sqrt, amount_tao, amount_alpha); - assert_eq!(as_tuple(out), (0, 17, 42, 0)); -} - -#[test] -fn rounding_down_behavior_when_dividing_by_price() { - // sqrt_price = 2.0 => price = 4.0 - let sqrt = U64F64::from_num(2u64); - let amount_tao: TaoCurrency = 13u64.into(); - let amount_alpha: AlphaCurrency = 100u64.into(); - - // tao is limiting; alpha_equiv = floor(13 / 4) = 3 - // remainders: tao 0, alpha 100 - 3 = 97 - let out = - Pallet::::get_proportional_alpha_tao_and_remainders(sqrt, amount_tao, amount_alpha); - assert_eq!(as_tuple(out), (13, 3, 0, 97)); -} - -#[test] -fn exact_fit_when_tao_matches_alpha_times_price() { - // sqrt_price = 1.0 => price = 1.0 - let sqrt = U64F64::from_num(1u64); - let amount_tao: TaoCurrency = 9u64.into(); - let amount_alpha: AlphaCurrency = 9u64.into(); - - let out = - Pallet::::get_proportional_alpha_tao_and_remainders(sqrt, amount_tao, amount_alpha); - assert_eq!(as_tuple(out), (9, 9, 0, 0)); -} - -#[test] -fn handles_zero_balances() { - let sqrt = U64F64::from_num(1u64); - - // Zero TAO, some alpha - let out = - Pallet::::get_proportional_alpha_tao_and_remainders(sqrt, 0u64.into(), 7u64.into()); - // tao limits; alpha_equiv = floor(0 / 1) = 0 - assert_eq!(as_tuple(out), (0, 0, 0, 7)); - - // Some TAO, zero alpha - let out = - Pallet::::get_proportional_alpha_tao_and_remainders(sqrt, 7u64.into(), 0u64.into()); - // tao_equiv = 0 * 1 = 0 <= 7 - assert_eq!(as_tuple(out), (0, 0, 7, 0)); - - // Both zero - let out = - Pallet::::get_proportional_alpha_tao_and_remainders(sqrt, 0u64.into(), 0u64.into()); - assert_eq!(as_tuple(out), (0, 0, 0, 0)); -} +fn test_migrate_swapv3_to_balancer() { + use crate::migrations::migrate_swapv3_to_balancer::deprecated_swap_maps; + use substrate_fixed::types::U64F64; -#[test] -fn adjust_protocol_liquidity_uses_and_sets_scrap_reservoirs() { new_test_ext().execute_with(|| { - // --- Arrange - let netuid: NetUid = 1u16.into(); - // Price = 1.0 (since sqrt_price^2 = 1), so proportional match is 1:1 - AlphaSqrtPrice::::insert(netuid, U64F64::saturating_from_num(1u64)); - - // Start with some non-zero scrap reservoirs - ScrapReservoirTao::::insert(netuid, TaoCurrency::from(7u64)); - ScrapReservoirAlpha::::insert(netuid, AlphaCurrency::from(5u64)); - - // Create a minimal protocol position so the function’s body executes. - let protocol = Pallet::::protocol_account_id(); - let position = Position::new( - PositionId::from(0), + let migration = + crate::migrations::migrate_swapv3_to_balancer::migrate_swapv3_to_balancer::; + let netuid = NetUid::from(1); + + // Insert deprecated maps values + deprecated_swap_maps::AlphaSqrtPrice::::insert(netuid, U64F64::from_num(1.23)); + deprecated_swap_maps::ScrapReservoirTao::::insert(netuid, TaoCurrency::from(9876)); + deprecated_swap_maps::ScrapReservoirAlpha::::insert( netuid, - TickIndex::MIN, - TickIndex::MAX, - 0, + AlphaCurrency::from(9876), ); - // Ensure collect_fees() returns (0,0) via zeroed fees in `position` (default). - Positions::::insert((netuid, protocol, position.id), position.clone()); - // --- Act - // No external deltas or fees; only reservoirs should be considered. - // With price=1, the exact proportional pair uses 5 alpha and 5 tao, - // leaving tao scrap = 7 - 5 = 2, alpha scrap = 5 - 5 = 0. - Pallet::::adjust_protocol_liquidity(netuid, 0u64.into(), 0u64.into()); + // Insert reserves that do not match the 1.23 price + TaoReserve::set_mock_reserve(netuid, TaoCurrency::from(1_000_000_000)); + AlphaReserve::set_mock_reserve(netuid, AlphaCurrency::from(4_000_000_000)); - // --- Assert: reservoirs were READ (used in proportional calc) and then SET (updated) - assert_eq!( - ScrapReservoirTao::::get(netuid), - TaoCurrency::from(2u64) - ); - assert_eq!( - ScrapReservoirAlpha::::get(netuid), - AlphaCurrency::from(0u64) + // Run migration + migration(); + + // Test that values are removed from state + assert!(!deprecated_swap_maps::AlphaSqrtPrice::::contains_key( + netuid + )); + assert!(!deprecated_swap_maps::ScrapReservoirAlpha::::contains_key(netuid)); + + // Test that subnet price is still 1.23^2 + assert_abs_diff_eq!( + Swap::current_price(netuid).to_num::(), + 1.23 * 1.23, + epsilon = 0.1 ); }); } diff --git a/pallets/swap/src/position.rs b/pallets/swap/src/position.rs deleted file mode 100644 index 5a57928a93..0000000000 --- a/pallets/swap/src/position.rs +++ /dev/null @@ -1,198 +0,0 @@ -use core::marker::PhantomData; - -use codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen}; -use frame_support::pallet_prelude::*; -use safe_math::*; -use substrate_fixed::types::{I64F64, U64F64}; -use subtensor_macros::freeze_struct; -use subtensor_runtime_common::NetUid; - -use crate::SqrtPrice; -use crate::pallet::{Config, Error, FeeGlobalAlpha, FeeGlobalTao, LastPositionId}; -use crate::tick::TickIndex; - -/// Position designates one liquidity position. -/// -/// Alpha price is expressed in rao units per one 10^9 unit. For example, -/// price 1_000_000 is equal to 0.001 TAO per Alpha. -#[freeze_struct("27a1bf8c59480f0")] -#[derive(Clone, Encode, Decode, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen, Default)] -#[scale_info(skip_type_params(T))] -pub struct Position { - /// Unique ID of the position - pub id: PositionId, - /// Network identifier - pub netuid: NetUid, - /// Tick index for lower boundary of price - pub tick_low: TickIndex, - /// Tick index for higher boundary of price - pub tick_high: TickIndex, - /// Position liquidity - pub liquidity: u64, - /// Fees accrued by the position in quote currency (TAO) relative to global fees - pub fees_tao: I64F64, - /// Fees accrued by the position in base currency (Alpha) relative to global fees - pub fees_alpha: I64F64, - /// Phantom marker for generic Config type - pub _phantom: PhantomData, -} - -impl Position { - pub fn new( - id: PositionId, - netuid: NetUid, - tick_low: TickIndex, - tick_high: TickIndex, - liquidity: u64, - ) -> Self { - let mut position = Position { - id, - netuid, - tick_low, - tick_high, - liquidity, - fees_tao: I64F64::saturating_from_num(0), - fees_alpha: I64F64::saturating_from_num(0), - _phantom: PhantomData, - }; - - position.fees_tao = position.fees_in_range(true); - position.fees_alpha = position.fees_in_range(false); - - position - } - - /// Converts position to token amounts - /// - /// returns tuple of (TAO, Alpha) - /// - /// Pseudocode: - /// if self.sqrt_price_curr < sqrt_pa: - /// tao = 0 - /// alpha = L * (1 / sqrt_pa - 1 / sqrt_pb) - /// elif self.sqrt_price_curr > sqrt_pb: - /// tao = L * (sqrt_pb - sqrt_pa) - /// alpha = 0 - /// else: - /// tao = L * (self.sqrt_price_curr - sqrt_pa) - /// alpha = L * (1 / self.sqrt_price_curr - 1 / sqrt_pb) - /// - pub fn to_token_amounts(&self, sqrt_price_curr: SqrtPrice) -> Result<(u64, u64), Error> { - let one = U64F64::saturating_from_num(1); - - let sqrt_price_low = self - .tick_low - .try_to_sqrt_price() - .map_err(|_| Error::::InvalidTickRange)?; - let sqrt_price_high = self - .tick_high - .try_to_sqrt_price() - .map_err(|_| Error::::InvalidTickRange)?; - let liquidity_fixed = U64F64::saturating_from_num(self.liquidity); - - Ok(if sqrt_price_curr < sqrt_price_low { - ( - 0, - liquidity_fixed - .saturating_mul( - one.safe_div(sqrt_price_low) - .saturating_sub(one.safe_div(sqrt_price_high)), - ) - .saturating_to_num::(), - ) - } else if sqrt_price_curr > sqrt_price_high { - ( - liquidity_fixed - .saturating_mul(sqrt_price_high.saturating_sub(sqrt_price_low)) - .saturating_to_num::(), - 0, - ) - } else { - ( - liquidity_fixed - .saturating_mul(sqrt_price_curr.saturating_sub(sqrt_price_low)) - .saturating_to_num::(), - liquidity_fixed - .saturating_mul( - one.safe_div(sqrt_price_curr) - .saturating_sub(one.safe_div(sqrt_price_high)), - ) - .saturating_to_num::(), - ) - }) - } - - /// Collect fees for a position - /// Updates the position - pub fn collect_fees(&mut self) -> (u64, u64) { - let fee_tao_agg = self.fees_in_range(true); - let fee_alpha_agg = self.fees_in_range(false); - - let mut fee_tao = fee_tao_agg.saturating_sub(self.fees_tao); - let mut fee_alpha = fee_alpha_agg.saturating_sub(self.fees_alpha); - - self.fees_tao = fee_tao_agg; - self.fees_alpha = fee_alpha_agg; - - let liquidity_frac = I64F64::saturating_from_num(self.liquidity); - - fee_tao = liquidity_frac.saturating_mul(fee_tao); - fee_alpha = liquidity_frac.saturating_mul(fee_alpha); - - ( - fee_tao.saturating_to_num::(), - fee_alpha.saturating_to_num::(), - ) - } - - /// Get fees in a position's range - /// - /// If quote flag is true, Tao is returned, otherwise alpha. - fn fees_in_range(&self, quote: bool) -> I64F64 { - if quote { - I64F64::saturating_from_num(FeeGlobalTao::::get(self.netuid)) - } else { - I64F64::saturating_from_num(FeeGlobalAlpha::::get(self.netuid)) - } - .saturating_sub(self.tick_low.fees_below::(self.netuid, quote)) - .saturating_sub(self.tick_high.fees_above::(self.netuid, quote)) - } -} - -#[freeze_struct("8501fa251c9d74c")] -#[derive( - Clone, - Copy, - Decode, - DecodeWithMemTracking, - Default, - Encode, - Eq, - MaxEncodedLen, - PartialEq, - RuntimeDebug, - TypeInfo, -)] -pub struct PositionId(u128); - -impl PositionId { - /// Create a new position ID - pub fn new() -> Self { - let new = LastPositionId::::get().saturating_add(1); - LastPositionId::::put(new); - - Self(new) - } -} - -impl From for PositionId { - fn from(value: u128) -> Self { - Self(value) - } -} - -impl From for u128 { - fn from(value: PositionId) -> Self { - value.0 - } -} diff --git a/pallets/swap/src/tick.rs b/pallets/swap/src/tick.rs deleted file mode 100644 index d3493fde45..0000000000 --- a/pallets/swap/src/tick.rs +++ /dev/null @@ -1,2198 +0,0 @@ -//! The math is adapted from github.com/0xKitsune/uniswap-v3-math -use core::cmp::Ordering; -use core::convert::TryFrom; -use core::error::Error; -use core::fmt; -use core::hash::Hash; -use core::ops::{Add, AddAssign, BitOr, Deref, Neg, Shl, Shr, Sub, SubAssign}; - -use alloy_primitives::{I256, U256}; -use codec::{Decode, DecodeWithMemTracking, Encode, Error as CodecError, Input, MaxEncodedLen}; -use frame_support::pallet_prelude::*; -use safe_math::*; -use sp_std::vec; -use sp_std::vec::Vec; -use substrate_fixed::types::{I64F64, U64F64}; -use subtensor_macros::freeze_struct; -use subtensor_runtime_common::NetUid; - -use crate::SqrtPrice; -use crate::pallet::{ - Config, CurrentTick, FeeGlobalAlpha, FeeGlobalTao, TickIndexBitmapWords, Ticks, -}; - -const U256_1: U256 = U256::from_limbs([1, 0, 0, 0]); -const U256_2: U256 = U256::from_limbs([2, 0, 0, 0]); -const U256_3: U256 = U256::from_limbs([3, 0, 0, 0]); -const U256_4: U256 = U256::from_limbs([4, 0, 0, 0]); -const U256_5: U256 = U256::from_limbs([5, 0, 0, 0]); -const U256_6: U256 = U256::from_limbs([6, 0, 0, 0]); -const U256_7: U256 = U256::from_limbs([7, 0, 0, 0]); -const U256_8: U256 = U256::from_limbs([8, 0, 0, 0]); -const U256_15: U256 = U256::from_limbs([15, 0, 0, 0]); -const U256_16: U256 = U256::from_limbs([16, 0, 0, 0]); -const U256_32: U256 = U256::from_limbs([32, 0, 0, 0]); -const U256_64: U256 = U256::from_limbs([64, 0, 0, 0]); -const U256_127: U256 = U256::from_limbs([127, 0, 0, 0]); -const U256_128: U256 = U256::from_limbs([128, 0, 0, 0]); -const U256_255: U256 = U256::from_limbs([255, 0, 0, 0]); - -const U256_256: U256 = U256::from_limbs([256, 0, 0, 0]); -const U256_512: U256 = U256::from_limbs([512, 0, 0, 0]); -const U256_1024: U256 = U256::from_limbs([1024, 0, 0, 0]); -const U256_2048: U256 = U256::from_limbs([2048, 0, 0, 0]); -const U256_4096: U256 = U256::from_limbs([4096, 0, 0, 0]); -const U256_8192: U256 = U256::from_limbs([8192, 0, 0, 0]); -const U256_16384: U256 = U256::from_limbs([16384, 0, 0, 0]); -const U256_32768: U256 = U256::from_limbs([32768, 0, 0, 0]); -const U256_65536: U256 = U256::from_limbs([65536, 0, 0, 0]); -const U256_131072: U256 = U256::from_limbs([131072, 0, 0, 0]); -const U256_262144: U256 = U256::from_limbs([262144, 0, 0, 0]); -const U256_524288: U256 = U256::from_limbs([524288, 0, 0, 0]); - -const U256_MAX_TICK: U256 = U256::from_limbs([887272, 0, 0, 0]); - -const MIN_TICK: i32 = -887272; -const MAX_TICK: i32 = -MIN_TICK; - -const MIN_SQRT_RATIO: U256 = U256::from_limbs([4295128739, 0, 0, 0]); -const MAX_SQRT_RATIO: U256 = - U256::from_limbs([6743328256752651558, 17280870778742802505, 4294805859, 0]); - -const SQRT_10001: I256 = I256::from_raw(U256::from_limbs([11745905768312294533, 13863, 0, 0])); -const TICK_LOW: I256 = I256::from_raw(U256::from_limbs([ - 6552757943157144234, - 184476617836266586, - 0, - 0, -])); -const TICK_HIGH: I256 = I256::from_raw(U256::from_limbs([ - 4998474450511881007, - 15793544031827761793, - 0, - 0, -])); - -/// Tick is the price range determined by tick index (not part of this struct, but is the key at -/// which the Tick is stored in state hash maps). Tick struct stores liquidity and fee information. -/// -/// - Net liquidity -/// - Gross liquidity -/// - Fees (above global) in both currencies -#[freeze_struct("ff1bce826e64c4aa")] -#[derive(Debug, Default, Clone, Encode, Decode, TypeInfo, MaxEncodedLen, PartialEq, Eq)] -pub struct Tick { - pub liquidity_net: i128, - pub liquidity_gross: u64, - pub fees_out_tao: I64F64, - pub fees_out_alpha: I64F64, -} - -impl Tick { - pub fn liquidity_net_as_u64(&self) -> u64 { - self.liquidity_net.abs().min(u64::MAX as i128) as u64 - } -} - -/// Struct representing a tick index -#[freeze_struct("13c1f887258657f2")] -#[derive( - Debug, - Default, - Clone, - Copy, - Encode, - DecodeWithMemTracking, - TypeInfo, - MaxEncodedLen, - PartialEq, - Eq, - PartialOrd, - Ord, - Hash, -)] -pub struct TickIndex(i32); - -impl Decode for TickIndex { - fn decode(input: &mut I) -> Result { - let raw = i32::decode(input)?; - TickIndex::new(raw).map_err(|_| "TickIndex out of bounds".into()) - } -} - -impl Add for TickIndex { - type Output = Self; - - #[allow(clippy::arithmetic_side_effects)] - fn add(self, rhs: Self) -> Self::Output { - // Note: This assumes the result is within bounds. - // For a safer implementation, consider using checked_add. - Self::new_unchecked(self.get() + rhs.get()) - } -} - -impl Sub for TickIndex { - type Output = Self; - - #[allow(clippy::arithmetic_side_effects)] - fn sub(self, rhs: Self) -> Self::Output { - // Note: This assumes the result is within bounds. - // For a safer implementation, consider using checked_sub. - Self::new_unchecked(self.get() - rhs.get()) - } -} - -impl AddAssign for TickIndex { - #[allow(clippy::arithmetic_side_effects)] - fn add_assign(&mut self, rhs: Self) { - *self = Self::new_unchecked(self.get() + rhs.get()); - } -} - -impl SubAssign for TickIndex { - #[allow(clippy::arithmetic_side_effects)] - fn sub_assign(&mut self, rhs: Self) { - *self = Self::new_unchecked(self.get() - rhs.get()); - } -} - -impl TryFrom for TickIndex { - type Error = TickMathError; - - fn try_from(value: i32) -> Result { - Self::new(value) - } -} - -impl Deref for TickIndex { - type Target = i32; - - fn deref(&self) -> &Self::Target { - // Using get() would create an infinite recursion, so this is one place where we need direct - // field access. This is safe because Self::Target is i32, which is exactly what we're - // storing - &self.0 - } -} - -/// Extension trait to make working with TryFrom more ergonomic -pub trait TryIntoTickIndex { - /// Convert an i32 into a TickIndex, with bounds checking - fn into_tick_index(self) -> Result; -} - -impl TryIntoTickIndex for i32 { - fn into_tick_index(self) -> Result { - TickIndex::try_from(self) - } -} - -impl TickIndex { - /// Minimum value of the tick index - /// The tick_math library uses different bitness, so we have to divide by 2. - /// It's unsafe to change this value to something else. - pub const MIN: Self = Self(MIN_TICK.saturating_div(2)); - - /// Maximum value of the tick index - /// The tick_math library uses different bitness, so we have to divide by 2. - /// It's unsafe to change this value to something else. - pub const MAX: Self = Self(MAX_TICK.saturating_div(2)); - - /// All tick indexes are offset by this value for storage needs - /// so that tick indexes are positive, which simplifies bit logic - const OFFSET: Self = Self(MAX_TICK); - - /// The MIN sqrt price, which is caclculated at Self::MIN - pub fn min_sqrt_price() -> SqrtPrice { - SqrtPrice::saturating_from_num(0.0000000002328350195) - } - - /// The MAX sqrt price, which is calculated at Self::MAX - #[allow(clippy::excessive_precision)] - pub fn max_sqrt_price() -> SqrtPrice { - SqrtPrice::saturating_from_num(4294886577.20989222513899790805) - } - - /// Get fees above a tick - pub fn fees_above(&self, netuid: NetUid, quote: bool) -> I64F64 { - let current_tick = Self::current_bounded::(netuid); - - let tick = Ticks::::get(netuid, *self).unwrap_or_default(); - if *self <= current_tick { - if quote { - I64F64::saturating_from_num(FeeGlobalTao::::get(netuid)) - .saturating_sub(tick.fees_out_tao) - } else { - I64F64::saturating_from_num(FeeGlobalAlpha::::get(netuid)) - .saturating_sub(tick.fees_out_alpha) - } - } else if quote { - tick.fees_out_tao - } else { - tick.fees_out_alpha - } - } - - /// Get fees below a tick - pub fn fees_below(&self, netuid: NetUid, quote: bool) -> I64F64 { - let current_tick = Self::current_bounded::(netuid); - - let tick = Ticks::::get(netuid, *self).unwrap_or_default(); - if *self <= current_tick { - if quote { - tick.fees_out_tao - } else { - tick.fees_out_alpha - } - } else if quote { - I64F64::saturating_from_num(FeeGlobalTao::::get(netuid)) - .saturating_sub(tick.fees_out_tao) - } else { - I64F64::saturating_from_num(FeeGlobalAlpha::::get(netuid)) - .saturating_sub(tick.fees_out_alpha) - } - } - - /// Get the current tick index for a subnet, ensuring it's within valid bounds - pub fn current_bounded(netuid: NetUid) -> Self { - let current_tick = CurrentTick::::get(netuid); - if current_tick > Self::MAX { - Self::MAX - } else if current_tick < Self::MIN { - Self::MIN - } else { - current_tick - } - } - - /// Converts a sqrt price to a tick index, ensuring it's within valid bounds - /// - /// If the price is outside the valid range, this function will return the appropriate boundary - /// tick index (MIN or MAX) instead of an error. - /// - /// # Arguments - /// * `sqrt_price` - The square root price to convert to a tick index - /// - /// # Returns - /// * `TickIndex` - A tick index that is guaranteed to be within valid bounds - pub fn from_sqrt_price_bounded(sqrt_price: SqrtPrice) -> Self { - match Self::try_from_sqrt_price(sqrt_price) { - Ok(index) => index, - Err(_) => { - let max_price = Self::MAX.as_sqrt_price_bounded(); - - if sqrt_price > max_price { - Self::MAX - } else { - Self::MIN - } - } - } - } - - /// Converts a tick index to a sqrt price, ensuring it's within valid bounds - /// - /// Unlike try_to_sqrt_price which returns an error for boundary indices, this function - /// guarantees a valid sqrt price by using fallback values if conversion fails. - /// - /// # Returns - /// * `SqrtPrice` - A sqrt price that is guaranteed to be a valid value - pub fn as_sqrt_price_bounded(&self) -> SqrtPrice { - self.try_to_sqrt_price().unwrap_or_else(|_| { - if *self >= Self::MAX { - Self::max_sqrt_price() - } else { - Self::min_sqrt_price() - } - }) - } - - /// Creates a new TickIndex instance with bounds checking - pub fn new(value: i32) -> Result { - if !(Self::MIN.0..=Self::MAX.0).contains(&value) { - Err(TickMathError::TickOutOfBounds) - } else { - Ok(Self(value)) - } - } - - /// Creates a new TickIndex without bounds checking - /// Use this function with caution, only when you're certain the value is valid - pub fn new_unchecked(value: i32) -> Self { - Self(value) - } - - /// Get the inner value - pub fn get(&self) -> i32 { - self.0 - } - - /// Creates a TickIndex from an offset representation (u32) - /// - /// # Arguments - /// * `offset_index` - An offset index (u32 value) representing a tick index - /// - /// # Returns - /// * `Result` - The corresponding TickIndex if within valid bounds - pub fn from_offset_index(offset_index: u32) -> Result { - // while it's safe, we use saturating math to mute the linter and just in case - let signed_index = ((offset_index as i64).saturating_sub(Self::OFFSET.get() as i64)) as i32; - Self::new(signed_index) - } - - /// Get the next tick index (incrementing by 1) - pub fn next(&self) -> Result { - Self::new(self.0.saturating_add(1)) - } - - /// Get the previous tick index (decrementing by 1) - pub fn prev(&self) -> Result { - Self::new(self.0.saturating_sub(1)) - } - - /// Add a value to this tick index with bounds checking - pub fn checked_add(&self, value: i32) -> Result { - Self::new(self.0.saturating_add(value)) - } - - /// Subtract a value from this tick index with bounds checking - pub fn checked_sub(&self, value: i32) -> Result { - Self::new(self.0.saturating_sub(value)) - } - - /// Add a value to this tick index, saturating at the bounds instead of overflowing - pub fn saturating_add(&self, value: i32) -> Self { - match self.checked_add(value) { - Ok(result) => result, - Err(_) => { - if value > 0 { - Self::MAX - } else { - Self::MIN - } - } - } - } - - /// Subtract a value from this tick index, saturating at the bounds instead of overflowing - pub fn saturating_sub(&self, value: i32) -> Self { - match self.checked_sub(value) { - Ok(result) => result, - Err(_) => { - if value > 0 { - Self::MIN - } else { - Self::MAX - } - } - } - } - - /// Divide the tick index by a value with bounds checking - #[allow(clippy::arithmetic_side_effects)] - pub fn checked_div(&self, value: i32) -> Result { - if value == 0 { - return Err(TickMathError::DivisionByZero); - } - Self::new(self.0.saturating_div(value)) - } - - /// Divide the tick index by a value, saturating at the bounds - pub fn saturating_div(&self, value: i32) -> Self { - if value == 0 { - return Self::MAX; // Return MAX for division by zero - } - match self.checked_div(value) { - Ok(result) => result, - Err(_) => { - if (self.0 < 0 && value > 0) || (self.0 > 0 && value < 0) { - Self::MIN - } else { - Self::MAX - } - } - } - } - - /// Multiply the tick index by a value with bounds checking - pub fn checked_mul(&self, value: i32) -> Result { - // Check for potential overflow - match self.0.checked_mul(value) { - Some(result) => Self::new(result), - None => Err(TickMathError::Overflow), - } - } - - /// Multiply the tick index by a value, saturating at the bounds - pub fn saturating_mul(&self, value: i32) -> Self { - match self.checked_mul(value) { - Ok(result) => result, - Err(_) => { - if (self.0 < 0 && value > 0) || (self.0 > 0 && value < 0) { - Self::MIN - } else { - Self::MAX - } - } - } - } - - /// Converts tick index into SQRT of lower price of this tick In order to find the higher price - /// of this tick, call tick_index_to_sqrt_price(tick_idx + 1) - pub fn try_to_sqrt_price(&self) -> Result { - // because of u256->u128 conversion we have twice less values for min/max ticks - if !(Self::MIN..=Self::MAX).contains(self) { - return Err(TickMathError::TickOutOfBounds); - } - get_sqrt_ratio_at_tick(self.0).and_then(u256_q64_96_to_u64f64) - } - - /// Converts SQRT price to tick index - /// Because the tick is the range of prices [sqrt_lower_price, sqrt_higher_price), the resulting - /// tick index matches the price by the following inequality: - /// sqrt_lower_price <= sqrt_price < sqrt_higher_price - pub fn try_from_sqrt_price(sqrt_price: SqrtPrice) -> Result { - // price in the native Q64.96 integer format - let price_x96 = u64f64_to_u256_q64_96(sqrt_price); - - // first‑pass estimate from the log calculation - let mut tick = get_tick_at_sqrt_ratio(price_x96)?; - - // post‑verification, *both* directions - let price_at_tick = get_sqrt_ratio_at_tick(tick)?; - if price_at_tick > price_x96 { - tick = tick.saturating_sub(1); // estimate was too high - } else { - // it may still be one too low - let price_at_tick_plus = get_sqrt_ratio_at_tick(tick.saturating_add(1))?; - if price_at_tick_plus <= price_x96 { - tick = tick.saturating_add(1); // step up when required - } - } - - tick.into_tick_index() - } -} - -pub struct ActiveTickIndexManager(PhantomData); - -impl ActiveTickIndexManager { - pub fn insert(netuid: NetUid, index: TickIndex) { - // Check the range - if (index < TickIndex::MIN) || (index > TickIndex::MAX) { - return; - } - - // Convert to bitmap representation - let bitmap = TickIndexBitmap::from(index); - - // Update layer words - let mut word0_value = TickIndexBitmapWords::::get(( - netuid, - LayerLevel::Top, - bitmap.word_at(LayerLevel::Top), - )); - let mut word1_value = TickIndexBitmapWords::::get(( - netuid, - LayerLevel::Middle, - bitmap.word_at(LayerLevel::Middle), - )); - let mut word2_value = TickIndexBitmapWords::::get(( - netuid, - LayerLevel::Bottom, - bitmap.word_at(LayerLevel::Bottom), - )); - - // Set bits in each layer - word0_value |= bitmap.bit_mask(LayerLevel::Top); - word1_value |= bitmap.bit_mask(LayerLevel::Middle); - word2_value |= bitmap.bit_mask(LayerLevel::Bottom); - - // Update the storage - TickIndexBitmapWords::::set( - (netuid, LayerLevel::Top, bitmap.word_at(LayerLevel::Top)), - word0_value, - ); - TickIndexBitmapWords::::set( - ( - netuid, - LayerLevel::Middle, - bitmap.word_at(LayerLevel::Middle), - ), - word1_value, - ); - TickIndexBitmapWords::::set( - ( - netuid, - LayerLevel::Bottom, - bitmap.word_at(LayerLevel::Bottom), - ), - word2_value, - ); - } - - pub fn remove(netuid: NetUid, index: TickIndex) { - // Check the range - if (index < TickIndex::MIN) || (index > TickIndex::MAX) { - return; - } - - // Convert to bitmap representation - let bitmap = TickIndexBitmap::from(index); - - // Update layer words - let mut word0_value = TickIndexBitmapWords::::get(( - netuid, - LayerLevel::Top, - bitmap.word_at(LayerLevel::Top), - )); - let mut word1_value = TickIndexBitmapWords::::get(( - netuid, - LayerLevel::Middle, - bitmap.word_at(LayerLevel::Middle), - )); - let mut word2_value = TickIndexBitmapWords::::get(( - netuid, - LayerLevel::Bottom, - bitmap.word_at(LayerLevel::Bottom), - )); - - // Turn the bit off (& !bit) and save as needed - word2_value &= !bitmap.bit_mask(LayerLevel::Bottom); - TickIndexBitmapWords::::set( - ( - netuid, - LayerLevel::Bottom, - bitmap.word_at(LayerLevel::Bottom), - ), - word2_value, - ); - - if word2_value == 0 { - word1_value &= !bitmap.bit_mask(LayerLevel::Middle); - TickIndexBitmapWords::::set( - ( - netuid, - LayerLevel::Middle, - bitmap.word_at(LayerLevel::Middle), - ), - word1_value, - ); - } - - if word1_value == 0 { - word0_value &= !bitmap.bit_mask(LayerLevel::Top); - TickIndexBitmapWords::::set( - (netuid, LayerLevel::Top, bitmap.word_at(LayerLevel::Top)), - word0_value, - ); - } - } - - pub fn find_closest_lower(netuid: NetUid, index: TickIndex) -> Option { - Self::find_closest(netuid, index, true) - } - - pub fn find_closest_higher(netuid: NetUid, index: TickIndex) -> Option { - Self::find_closest(netuid, index, false) - } - - fn find_closest(netuid: NetUid, index: TickIndex, lower: bool) -> Option { - // Check the range - if (index < TickIndex::MIN) || (index > TickIndex::MAX) { - return None; - } - - // Convert to bitmap representation - let bitmap = TickIndexBitmap::from(index); - let mut found = false; - let mut result: u32 = 0; - - // Layer positions from bitmap - let layer0_word = bitmap.word_at(LayerLevel::Top); - let layer0_bit = bitmap.bit_at(LayerLevel::Top); - let layer1_word = bitmap.word_at(LayerLevel::Middle); - let layer1_bit = bitmap.bit_at(LayerLevel::Middle); - let layer2_word = bitmap.word_at(LayerLevel::Bottom); - let layer2_bit = bitmap.bit_at(LayerLevel::Bottom); - - // Find the closest active bits in layer 0, then 1, then 2 - - /////////////// - // Level 0 - let word0 = TickIndexBitmapWords::::get((netuid, LayerLevel::Top, layer0_word)); - let closest_bits_l0 = - TickIndexBitmap::find_closest_active_bit_candidates(word0, layer0_bit, lower); - - for closest_bit_l0 in closest_bits_l0.iter() { - /////////////// - // Level 1 - let word1_index = TickIndexBitmap::layer_to_index(BitmapLayer::new(0, *closest_bit_l0)); - - // Layer 1 words are different, shift the bit to the word edge - let start_from_l1_bit = match word1_index.cmp(&layer1_word) { - Ordering::Less => 127, - Ordering::Greater => 0, - _ => layer1_bit, - }; - let word1_value = - TickIndexBitmapWords::::get((netuid, LayerLevel::Middle, word1_index)); - let closest_bits_l1 = TickIndexBitmap::find_closest_active_bit_candidates( - word1_value, - start_from_l1_bit, - lower, - ); - - for closest_bit_l1 in closest_bits_l1.iter() { - /////////////// - // Level 2 - let word2_index = - TickIndexBitmap::layer_to_index(BitmapLayer::new(word1_index, *closest_bit_l1)); - - // Layer 2 words are different, shift the bit to the word edge - let start_from_l2_bit = match word2_index.cmp(&layer2_word) { - Ordering::Less => 127, - Ordering::Greater => 0, - _ => layer2_bit, - }; - - let word2_value = - TickIndexBitmapWords::::get((netuid, LayerLevel::Bottom, word2_index)); - - let closest_bits_l2 = TickIndexBitmap::find_closest_active_bit_candidates( - word2_value, - start_from_l2_bit, - lower, - ); - - if !closest_bits_l2.is_empty() { - // The active tick is found, restore its full index and return - let offset_found_index = TickIndexBitmap::layer_to_index(BitmapLayer::new( - word2_index, - // it's safe to unwrap, because the len is > 0, but to prevent errors in - // refactoring, we use default fallback here for extra safety - closest_bits_l2.first().copied().unwrap_or_default(), - )); - - if lower { - if (offset_found_index > result) || (!found) { - result = offset_found_index; - found = true; - } - } else if (offset_found_index < result) || (!found) { - result = offset_found_index; - found = true; - } - } - } - } - - if !found { - return None; - } - - // Convert the result offset_index back to a tick index - TickIndex::from_offset_index(result).ok() - } - - pub fn tick_is_active(netuid: NetUid, tick: TickIndex) -> bool { - Self::find_closest_lower(netuid, tick).unwrap_or(TickIndex::MAX) == tick - } -} - -/// Represents the three layers in the Uniswap V3 bitmap structure -#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode, TypeInfo, MaxEncodedLen)] -pub enum LayerLevel { - /// Top layer (highest level of the hierarchy) - Top = 0, - /// Middle layer - Middle = 1, - /// Bottom layer (contains the actual ticks) - Bottom = 2, -} - -#[freeze_struct("4015a04919eb5e2e")] -#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode, TypeInfo, MaxEncodedLen)] -pub(crate) struct BitmapLayer { - word: u32, - bit: u32, -} - -impl BitmapLayer { - pub fn new(word: u32, bit: u32) -> Self { - Self { word, bit } - } -} - -/// A bitmap representation of a tick index position across the three-layer structure -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) struct TickIndexBitmap { - /// The position in layer 0 (top layer) - layer0: BitmapLayer, - /// The position in layer 1 (middle layer) - layer1: BitmapLayer, - /// The position in layer 2 (bottom layer) - layer2: BitmapLayer, -} - -impl TickIndexBitmap { - /// Helper function to convert a bitmap index to a (word, bit) tuple in a bitmap layer using - /// safe methods - /// - /// Note: This function operates on bitmap navigation indices, NOT tick indices. - /// It converts a flat index within the bitmap structure to a (word, bit) position. - fn index_to_layer(index: u32) -> BitmapLayer { - let word = index.safe_div(128); - let bit = index.checked_rem(128).unwrap_or_default(); - BitmapLayer { word, bit } - } - - /// Converts a position (word, bit) within a layer to a word index in the next layer down - /// Note: This returns a bitmap navigation index, NOT a tick index - pub(crate) fn layer_to_index(layer: BitmapLayer) -> u32 { - layer.word.saturating_mul(128).saturating_add(layer.bit) - } - - /// Get the mask for a bit in the specified layer - pub(crate) fn bit_mask(&self, layer: LayerLevel) -> u128 { - match layer { - LayerLevel::Top => 1u128 << self.layer0.bit, - LayerLevel::Middle => 1u128 << self.layer1.bit, - LayerLevel::Bottom => 1u128 << self.layer2.bit, - } - } - - /// Get the word for the specified layer - pub(crate) fn word_at(&self, layer: LayerLevel) -> u32 { - match layer { - LayerLevel::Top => self.layer0.word, - LayerLevel::Middle => self.layer1.word, - LayerLevel::Bottom => self.layer2.word, - } - } - - /// Get the bit for the specified layer - pub(crate) fn bit_at(&self, layer: LayerLevel) -> u32 { - match layer { - LayerLevel::Top => self.layer0.bit, - LayerLevel::Middle => self.layer1.bit, - LayerLevel::Bottom => self.layer2.bit, - } - } - - /// Finds the closest active bit in a bitmap word, and if the active bit exactly matches the - /// requested bit, then it finds the next one as well - /// - /// # Arguments - /// * `word` - The bitmap word to search within - /// * `bit` - The bit position to start searching from - /// * `lower` - If true, search for lower bits (decreasing bit position), if false, search for - /// higher bits (increasing bit position) - /// - /// # Returns - /// * Exact match: Vec with [next_bit, bit] - /// * Non-exact match: Vec with [closest_bit] - /// * No match: Empty Vec - pub(crate) fn find_closest_active_bit_candidates( - word: u128, - bit: u32, - lower: bool, - ) -> Vec { - let mut result = vec![]; - let mut mask: u128 = 1_u128.wrapping_shl(bit); - let mut active_bit: u32 = bit; - - while mask > 0 { - if mask & word != 0 { - result.push(active_bit); - if active_bit != bit { - break; - } - } - - mask = if lower { - active_bit = active_bit.saturating_sub(1); - mask.wrapping_shr(1) - } else { - active_bit = active_bit.saturating_add(1); - mask.wrapping_shl(1) - }; - } - - result - } -} - -impl From for TickIndexBitmap { - fn from(tick_index: TickIndex) -> Self { - // Convert to offset index (internal operation only) - let offset_index = (tick_index.get().saturating_add(TickIndex::OFFSET.get())) as u32; - - // Calculate layer positions - let layer2 = Self::index_to_layer(offset_index); - let layer1 = Self::index_to_layer(layer2.word); - let layer0 = Self::index_to_layer(layer1.word); - - Self { - layer0, - layer1, - layer2, - } - } -} - -#[allow(clippy::arithmetic_side_effects)] -fn get_sqrt_ratio_at_tick(tick: i32) -> Result { - let abs_tick = if tick < 0 { - U256::from(tick.neg()) - } else { - U256::from(tick) - }; - - if abs_tick > U256_MAX_TICK { - return Err(TickMathError::TickOutOfBounds); - } - - let mut ratio = if abs_tick & (U256_1) != U256::ZERO { - U256::from_limbs([12262481743371124737, 18445821805675392311, 0, 0]) - } else { - U256::from_limbs([0, 0, 1, 0]) - }; - - if !(abs_tick & U256_2).is_zero() { - ratio = (ratio.saturating_mul(U256::from_limbs([ - 6459403834229662010, - 18444899583751176498, - 0, - 0, - ]))) >> 128 - } - if !(abs_tick & U256_4).is_zero() { - ratio = (ratio.saturating_mul(U256::from_limbs([ - 17226890335427755468, - 18443055278223354162, - 0, - 0, - ]))) >> 128 - } - if !(abs_tick & U256_8).is_zero() { - ratio = (ratio.saturating_mul(U256::from_limbs([ - 2032852871939366096, - 18439367220385604838, - 0, - 0, - ]))) >> 128 - } - if !(abs_tick & U256_16).is_zero() { - ratio = (ratio.saturating_mul(U256::from_limbs([ - 14545316742740207172, - 18431993317065449817, - 0, - 0, - ]))) >> 128 - } - if !(abs_tick & U256_32).is_zero() { - ratio = (ratio.saturating_mul(U256::from_limbs([ - 5129152022828963008, - 18417254355718160513, - 0, - 0, - ]))) >> 128 - } - if !(abs_tick & U256_64).is_zero() { - ratio = (ratio.saturating_mul(U256::from_limbs([ - 4894419605888772193, - 18387811781193591352, - 0, - 0, - ]))) >> 128 - } - if !(abs_tick & U256_128).is_zero() { - ratio = (ratio.saturating_mul(U256::from_limbs([ - 1280255884321894483, - 18329067761203520168, - 0, - 0, - ]))) >> 128 - } - if !(abs_tick & U256_256).is_zero() { - ratio = (ratio.saturating_mul(U256::from_limbs([ - 15924666964335305636, - 18212142134806087854, - 0, - 0, - ]))) >> 128 - } - if !(abs_tick & U256_512).is_zero() { - ratio = (ratio.saturating_mul(U256::from_limbs([ - 8010504389359918676, - 17980523815641551639, - 0, - 0, - ]))) >> 128 - } - if !(abs_tick & U256_1024).is_zero() { - ratio = (ratio.saturating_mul(U256::from_limbs([ - 10668036004952895731, - 17526086738831147013, - 0, - 0, - ]))) >> 128 - } - if !(abs_tick & U256_2048).is_zero() { - ratio = (ratio.saturating_mul(U256::from_limbs([ - 4878133418470705625, - 16651378430235024244, - 0, - 0, - ]))) >> 128 - } - if !(abs_tick & U256_4096).is_zero() { - ratio = (ratio.saturating_mul(U256::from_limbs([ - 9537173718739605541, - 15030750278693429944, - 0, - 0, - ]))) >> 128 - } - if !(abs_tick & U256_8192).is_zero() { - ratio = (ratio.saturating_mul(U256::from_limbs([ - 9972618978014552549, - 12247334978882834399, - 0, - 0, - ]))) >> 128 - } - if !(abs_tick & U256_16384).is_zero() { - ratio = (ratio.saturating_mul(U256::from_limbs([ - 10428997489610666743, - 8131365268884726200, - 0, - 0, - ]))) >> 128 - } - if !(abs_tick & U256_32768).is_zero() { - ratio = (ratio.saturating_mul(U256::from_limbs([ - 9305304367709015974, - 3584323654723342297, - 0, - 0, - ]))) >> 128 - } - if !(abs_tick & U256_65536).is_zero() { - ratio = (ratio.saturating_mul(U256::from_limbs([ - 14301143598189091785, - 696457651847595233, - 0, - 0, - ]))) >> 128 - } - if !(abs_tick & U256_131072).is_zero() { - ratio = (ratio.saturating_mul(U256::from_limbs([ - 7393154844743099908, - 26294789957452057, - 0, - 0, - ]))) >> 128 - } - if !(abs_tick & U256_262144).is_zero() { - ratio = (ratio.saturating_mul(U256::from_limbs([ - 2209338891292245656, - 37481735321082, - 0, - 0, - ]))) >> 128 - } - if !(abs_tick & U256_524288).is_zero() { - ratio = - (ratio.saturating_mul(U256::from_limbs([10518117631919034274, 76158723, 0, 0]))) >> 128 - } - - if tick > 0 { - ratio = U256::MAX / ratio; - } - - let shifted: U256 = ratio >> 32; - let ceil = if ratio & U256::from((1u128 << 32) - 1) != U256::ZERO { - shifted.saturating_add(U256_1) - } else { - shifted - }; - Ok(ceil) -} - -#[allow(clippy::arithmetic_side_effects)] -fn get_tick_at_sqrt_ratio(sqrt_price_x_96: U256) -> Result { - if !(sqrt_price_x_96 >= MIN_SQRT_RATIO && sqrt_price_x_96 < MAX_SQRT_RATIO) { - return Err(TickMathError::SqrtPriceOutOfBounds); - } - - let ratio: U256 = sqrt_price_x_96.shl(32); - let mut r = ratio; - let mut msb = U256::ZERO; - - let mut f = if r > U256::from_limbs([18446744073709551615, 18446744073709551615, 0, 0]) { - U256_1.shl(U256_7) - } else { - U256::ZERO - }; - msb = msb.bitor(f); - r = r.shr(f); - - f = if r > U256::from_limbs([18446744073709551615, 0, 0, 0]) { - U256_1.shl(U256_6) - } else { - U256::ZERO - }; - msb = msb.bitor(f); - r = r.shr(f); - - f = if r > U256::from_limbs([4294967295, 0, 0, 0]) { - U256_1.shl(U256_5) - } else { - U256::ZERO - }; - msb = msb.bitor(f); - r = r.shr(f); - - f = if r > U256::from_limbs([65535, 0, 0, 0]) { - U256_1.shl(U256_4) - } else { - U256::ZERO - }; - msb = msb.bitor(f); - r = r.shr(f); - - f = if r > U256_255 { - U256_1.shl(U256_3) - } else { - U256::ZERO - }; - msb = msb.bitor(f); - r = r.shr(f); - - f = if r > U256_15 { - U256_1.shl(U256_2) - } else { - U256::ZERO - }; - msb = msb.bitor(f); - r = r.shr(f); - - f = if r > U256_3 { - U256_1.shl(U256_1) - } else { - U256::ZERO - }; - msb = msb.bitor(f); - r = r.shr(f); - - f = if r > U256_1 { U256_1 } else { U256::ZERO }; - - msb = msb.bitor(f); - - r = if msb >= U256_128 { - ratio.shr(msb.saturating_sub(U256_127)) - } else { - ratio.shl(U256_127.saturating_sub(msb)) - }; - - let mut log_2: I256 = - (I256::from_raw(msb).saturating_sub(I256::from_limbs([128, 0, 0, 0]))).shl(64); - - for i in (51..=63).rev() { - r = r.overflowing_mul(r).0.shr(U256_127); - let f: U256 = r.shr(128); - log_2 = log_2.bitor(I256::from_raw(f.shl(i))); - - r = r.shr(f); - } - - r = r.overflowing_mul(r).0.shr(U256_127); - let f: U256 = r.shr(128); - log_2 = log_2.bitor(I256::from_raw(f.shl(50))); - - let log_sqrt10001 = log_2.wrapping_mul(SQRT_10001); - - let tick_low = (log_sqrt10001.saturating_sub(TICK_LOW) >> 128_u8).low_i32(); - - let tick_high = (log_sqrt10001.saturating_add(TICK_HIGH) >> 128_u8).low_i32(); - - let tick = if tick_low == tick_high { - tick_low - } else if get_sqrt_ratio_at_tick(tick_high)? <= sqrt_price_x_96 { - tick_high - } else { - tick_low - }; - - Ok(tick) -} - -// Convert U64F64 to U256 in Q64.96 format (Uniswap's sqrt price format) -fn u64f64_to_u256_q64_96(value: U64F64) -> U256 { - u64f64_to_u256(value, 96) -} - -/// Convert U64F64 to U256 -/// -/// # Arguments -/// * `value` - The U64F64 value to convert -/// * `target_fractional_bits` - Number of fractional bits in the target U256 format -/// -/// # Returns -/// * `U256` - Converted value -#[allow(clippy::arithmetic_side_effects)] -fn u64f64_to_u256(value: U64F64, target_fractional_bits: u32) -> U256 { - let raw = U256::from(value.to_bits()); - - match target_fractional_bits.cmp(&64) { - Ordering::Less => raw >> (64 - target_fractional_bits), - Ordering::Greater => raw.saturating_shl((target_fractional_bits - 64) as usize), - Ordering::Equal => raw, - } -} - -/// Convert U256 in Q64.96 format (Uniswap's sqrt price format) to U64F64 -fn u256_q64_96_to_u64f64(value: U256) -> Result { - q_to_u64f64(value, 96) -} - -#[allow(clippy::arithmetic_side_effects)] -fn q_to_u64f64(x: U256, frac_bits: u32) -> Result { - let diff = frac_bits.saturating_sub(64) as usize; - - // 1. shift right diff bits - let shifted = if diff != 0 { x >> diff } else { x }; - - // 2. **round up** if we threw away any 1‑bits - let mask = if diff != 0 { - (U256_1.saturating_shl(diff)).saturating_sub(U256_1) - } else { - U256::ZERO - }; - let rounded = if diff != 0 && (x & mask) != U256::ZERO { - shifted.saturating_add(U256_1) - } else { - shifted - }; - - // 3. check that it fits in 128 bits and transmute - if (rounded >> 128) != U256::ZERO { - return Err(TickMathError::Overflow); - } - Ok(U64F64::from_bits(rounded.to::())) -} - -#[derive(Debug, PartialEq, Eq)] -pub enum TickMathError { - TickOutOfBounds, - SqrtPriceOutOfBounds, - ConversionError, - Overflow, - DivisionByZero, -} - -impl fmt::Display for TickMathError { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::TickOutOfBounds => f.write_str("The given tick is outside of the minimum/maximum values."), - Self::SqrtPriceOutOfBounds =>f.write_str("Second inequality must be < because the price can never reach the price at the max tick"), - Self::ConversionError => f.write_str("Error converting from one number type into another"), - Self::Overflow => f.write_str("Number overflow in arithmetic operation"), - Self::DivisionByZero => f.write_str("Division by zero is not allowed") - } - } -} - -impl Error for TickMathError {} - -#[allow(clippy::unwrap_used)] -#[cfg(test)] -mod tests { - use safe_math::FixedExt; - use std::{ops::Sub, str::FromStr}; - - use super::*; - use crate::mock::*; - - #[test] - fn test_get_sqrt_ratio_at_tick_bounds() { - // the function should return an error if the tick is out of bounds - if let Err(err) = get_sqrt_ratio_at_tick(MIN_TICK - 1) { - assert!(matches!(err, TickMathError::TickOutOfBounds)); - } else { - panic!("get_qrt_ratio_at_tick did not respect lower tick bound") - } - if let Err(err) = get_sqrt_ratio_at_tick(MAX_TICK + 1) { - assert!(matches!(err, TickMathError::TickOutOfBounds)); - } else { - panic!("get_qrt_ratio_at_tick did not respect upper tick bound") - } - } - - #[test] - fn test_get_sqrt_ratio_at_tick_values() { - // test individual values for correct results - assert_eq!( - get_sqrt_ratio_at_tick(MIN_TICK).unwrap(), - U256::from(4295128739u64), - "sqrt ratio at min incorrect" - ); - assert_eq!( - get_sqrt_ratio_at_tick(MIN_TICK + 1).unwrap(), - U256::from(4295343490u64), - "sqrt ratio at min + 1 incorrect" - ); - assert_eq!( - get_sqrt_ratio_at_tick(MAX_TICK - 1).unwrap(), - U256::from_str("1461373636630004318706518188784493106690254656249").unwrap(), - "sqrt ratio at max - 1 incorrect" - ); - assert_eq!( - get_sqrt_ratio_at_tick(MAX_TICK).unwrap(), - U256::from_str("1461446703485210103287273052203988822378723970342").unwrap(), - "sqrt ratio at max incorrect" - ); - // checking hard coded values against solidity results - assert_eq!( - get_sqrt_ratio_at_tick(50).unwrap(), - U256::from(79426470787362580746886972461u128), - "sqrt ratio at 50 incorrect" - ); - assert_eq!( - get_sqrt_ratio_at_tick(100).unwrap(), - U256::from(79625275426524748796330556128u128), - "sqrt ratio at 100 incorrect" - ); - assert_eq!( - get_sqrt_ratio_at_tick(250).unwrap(), - U256::from(80224679980005306637834519095u128), - "sqrt ratio at 250 incorrect" - ); - assert_eq!( - get_sqrt_ratio_at_tick(500).unwrap(), - U256::from(81233731461783161732293370115u128), - "sqrt ratio at 500 incorrect" - ); - assert_eq!( - get_sqrt_ratio_at_tick(1000).unwrap(), - U256::from(83290069058676223003182343270u128), - "sqrt ratio at 1000 incorrect" - ); - assert_eq!( - get_sqrt_ratio_at_tick(2500).unwrap(), - U256::from(89776708723587163891445672585u128), - "sqrt ratio at 2500 incorrect" - ); - assert_eq!( - get_sqrt_ratio_at_tick(3000).unwrap(), - U256::from(92049301871182272007977902845u128), - "sqrt ratio at 3000 incorrect" - ); - assert_eq!( - get_sqrt_ratio_at_tick(4000).unwrap(), - U256::from(96768528593268422080558758223u128), - "sqrt ratio at 4000 incorrect" - ); - assert_eq!( - get_sqrt_ratio_at_tick(5000).unwrap(), - U256::from(101729702841318637793976746270u128), - "sqrt ratio at 5000 incorrect" - ); - assert_eq!( - get_sqrt_ratio_at_tick(50000).unwrap(), - U256::from(965075977353221155028623082916u128), - "sqrt ratio at 50000 incorrect" - ); - assert_eq!( - get_sqrt_ratio_at_tick(150000).unwrap(), - U256::from(143194173941309278083010301478497u128), - "sqrt ratio at 150000 incorrect" - ); - assert_eq!( - get_sqrt_ratio_at_tick(250000).unwrap(), - U256::from(21246587762933397357449903968194344u128), - "sqrt ratio at 250000 incorrect" - ); - assert_eq!( - get_sqrt_ratio_at_tick(500000).unwrap(), - U256::from_str("5697689776495288729098254600827762987878").unwrap(), - "sqrt ratio at 500000 incorrect" - ); - assert_eq!( - get_sqrt_ratio_at_tick(738203).unwrap(), - U256::from_str("847134979253254120489401328389043031315994541").unwrap(), - "sqrt ratio at 738203 incorrect" - ); - } - - #[test] - fn test_get_tick_at_sqrt_ratio() { - //throws for too low - let result = get_tick_at_sqrt_ratio(MIN_SQRT_RATIO.sub(U256_1)); - assert_eq!( - result.unwrap_err().to_string(), - "Second inequality must be < because the price can never reach the price at the max tick" - ); - - //throws for too high - let result = get_tick_at_sqrt_ratio(MAX_SQRT_RATIO); - assert_eq!( - result.unwrap_err().to_string(), - "Second inequality must be < because the price can never reach the price at the max tick" - ); - - //ratio of min tick - let result = get_tick_at_sqrt_ratio(MIN_SQRT_RATIO).unwrap(); - assert_eq!(result, MIN_TICK); - - //ratio of min tick + 1 - let result = get_tick_at_sqrt_ratio(U256::from_str("4295343490").unwrap()).unwrap(); - assert_eq!(result, MIN_TICK + 1); - } - - #[test] - fn test_roundtrip() { - for tick_index in [ - MIN_TICK + 1, // we can't use extremes because of rounding during roundtrip conversion - -1000, - -100, - -10, - -4, - -2, - 0, - 2, - 4, - 10, - 100, - 1000, - MAX_TICK - 1, - ] - .iter() - { - let sqrt_price = get_sqrt_ratio_at_tick(*tick_index).unwrap(); - let round_trip_tick_index = get_tick_at_sqrt_ratio(sqrt_price).unwrap(); - assert_eq!(round_trip_tick_index, *tick_index); - } - } - - #[test] - fn test_u256_to_u64f64_q64_96() { - // Test tick 0 (sqrt price = 1.0 * 2^96) - let tick0_sqrt_price = U256::from(1u128 << 96); - let fixed_price = u256_q64_96_to_u64f64(tick0_sqrt_price).unwrap(); - - // Should be 1.0 in U64F64 - assert_eq!(fixed_price, U64F64::from_num(1.0)); - - // Round trip back to U256 Q64.96 - let back_to_u256 = u64f64_to_u256_q64_96(fixed_price); - assert_eq!(back_to_u256, tick0_sqrt_price); - } - - #[test] - fn test_tick_index_to_sqrt_price() { - let tick_spacing = SqrtPrice::from_num(1.0001); - - // check tick bounds - assert_eq!( - TickIndex(MIN_TICK).try_to_sqrt_price(), - Err(TickMathError::TickOutOfBounds) - ); - - assert_eq!( - TickIndex(MAX_TICK).try_to_sqrt_price(), - Err(TickMathError::TickOutOfBounds), - ); - - assert!( - TickIndex::MAX.try_to_sqrt_price().unwrap().abs_diff( - TickIndex::new_unchecked(TickIndex::MAX.get() + 1).as_sqrt_price_bounded() - ) < SqrtPrice::from_num(1e-6) - ); - - assert!( - TickIndex::MIN.try_to_sqrt_price().unwrap().abs_diff( - TickIndex::new_unchecked(TickIndex::MIN.get() - 1).as_sqrt_price_bounded() - ) < SqrtPrice::from_num(1e-6) - ); - - // At tick index 0, the sqrt price should be 1.0 - let sqrt_price = TickIndex(0).try_to_sqrt_price().unwrap(); - assert_eq!(sqrt_price, SqrtPrice::from_num(1.0)); - - let sqrt_price = TickIndex(2).try_to_sqrt_price().unwrap(); - assert!(sqrt_price.abs_diff(tick_spacing) < SqrtPrice::from_num(1e-10)); - - let sqrt_price = TickIndex(4).try_to_sqrt_price().unwrap(); - // Calculate the expected value: (1 + TICK_SPACING/1e9 + 1.0)^2 - let expected = tick_spacing * tick_spacing; - assert!(sqrt_price.abs_diff(expected) < SqrtPrice::from_num(1e-10)); - - // Test with tick index 10 - let sqrt_price = TickIndex(10).try_to_sqrt_price().unwrap(); - // Calculate the expected value: (1 + TICK_SPACING/1e9 + 1.0)^5 - let expected = tick_spacing.checked_pow(5).unwrap(); - assert!( - sqrt_price.abs_diff(expected) < SqrtPrice::from_num(1e-10), - "diff: {}", - sqrt_price.abs_diff(expected), - ); - } - - #[test] - fn test_sqrt_price_to_tick_index() { - let tick_spacing = SqrtPrice::from_num(1.0001); - let tick_index = TickIndex::try_from_sqrt_price(SqrtPrice::from_num(1.0)).unwrap(); - assert_eq!(tick_index, TickIndex::new_unchecked(0)); - - // Test with sqrt price equal to tick_spacing_tao (should be tick index 2) - let epsilon = SqrtPrice::from_num(0.0000000000000001); - assert!( - TickIndex::new_unchecked(2) - .as_sqrt_price_bounded() - .abs_diff(tick_spacing) - < epsilon - ); - - // Test with sqrt price equal to tick_spacing_tao^2 (should be tick index 4) - let sqrt_price = tick_spacing * tick_spacing; - assert!( - TickIndex::new_unchecked(4) - .as_sqrt_price_bounded() - .abs_diff(sqrt_price) - < epsilon - ); - - // Test with sqrt price equal to tick_spacing_tao^5 (should be tick index 10) - let sqrt_price = tick_spacing.checked_pow(5).unwrap(); - assert!( - TickIndex::new_unchecked(10) - .as_sqrt_price_bounded() - .abs_diff(sqrt_price) - < epsilon - ); - } - - #[test] - fn test_roundtrip_tick_index_sqrt_price() { - for i32_value in [ - TickIndex::MIN.get(), - -1000, - -100, - -10, - -4, - -2, - 0, - 2, - 4, - 10, - 100, - 1000, - TickIndex::MAX.get(), - ] - .into_iter() - { - let tick_index = TickIndex::new_unchecked(i32_value); - let sqrt_price = tick_index.try_to_sqrt_price().unwrap(); - let round_trip_tick_index = TickIndex::try_from_sqrt_price(sqrt_price).unwrap(); - assert_eq!(round_trip_tick_index, tick_index); - } - } - - #[test] - fn test_from_offset_index() { - // Test various tick indices - for i32_value in [ - TickIndex::MIN.get(), - -1000, - -100, - -10, - 0, - 10, - 100, - 1000, - TickIndex::MAX.get(), - ] { - let original_tick = TickIndex::new_unchecked(i32_value); - - // Calculate the offset index (adding OFFSET) - let offset_index = (i32_value + TickIndex::OFFSET.get()) as u32; - - // Convert back from offset index to tick index - let roundtrip_tick = TickIndex::from_offset_index(offset_index).unwrap(); - - // Check that we get the same tick index back - assert_eq!(original_tick, roundtrip_tick); - } - - // Test out of bounds values - let too_large = (TickIndex::MAX.get() + TickIndex::OFFSET.get() + 1) as u32; - assert!(TickIndex::from_offset_index(too_large).is_err()); - } - - #[test] - fn test_tick_price_sanity_check() { - let min_price = TickIndex::MIN.try_to_sqrt_price().unwrap(); - let max_price = TickIndex::MAX.try_to_sqrt_price().unwrap(); - - assert!(min_price > 0.); - assert!(max_price > 0.); - assert!(max_price > min_price); - assert!(min_price < 0.000001); - assert!(max_price > 10.); - - // Roundtrip conversions - let min_price_sqrt = TickIndex::MIN.try_to_sqrt_price().unwrap(); - let min_tick = TickIndex::try_from_sqrt_price(min_price_sqrt).unwrap(); - assert_eq!(min_tick, TickIndex::MIN); - - let max_price_sqrt: SqrtPrice = TickIndex::MAX.try_to_sqrt_price().unwrap(); - let max_tick = TickIndex::try_from_sqrt_price(max_price_sqrt).unwrap(); - assert_eq!(max_tick, TickIndex::MAX); - } - - #[test] - fn test_to_sqrt_price_bounded() { - assert_eq!( - TickIndex::MAX.as_sqrt_price_bounded(), - TickIndex::MAX.try_to_sqrt_price().unwrap() - ); - - assert_eq!( - TickIndex::MIN.as_sqrt_price_bounded(), - TickIndex::MIN.try_to_sqrt_price().unwrap() - ); - } - - mod active_tick_index_manager { - - use super::*; - - #[test] - fn test_tick_search_basic() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - - ActiveTickIndexManager::::insert(netuid, TickIndex::MIN); - - assert_eq!( - ActiveTickIndexManager::::find_closest_lower(netuid, TickIndex::MIN) - .unwrap(), - TickIndex::MIN - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_lower(netuid, TickIndex::MAX) - .unwrap(), - TickIndex::MIN - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_lower( - netuid, - TickIndex::MAX.saturating_div(2) - ) - .unwrap(), - TickIndex::MIN - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_lower( - netuid, - TickIndex::MAX.prev().unwrap() - ) - .unwrap(), - TickIndex::MIN - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_lower( - netuid, - TickIndex::MIN.next().unwrap() - ) - .unwrap(), - TickIndex::MIN - ); - - assert_eq!( - ActiveTickIndexManager::::find_closest_higher(netuid, TickIndex::MIN) - .unwrap(), - TickIndex::MIN - ); - assert!( - ActiveTickIndexManager::::find_closest_higher(netuid, TickIndex::MAX) - .is_none() - ); - assert!( - ActiveTickIndexManager::::find_closest_higher( - netuid, - TickIndex::MAX.saturating_div(2) - ) - .is_none() - ); - assert!( - ActiveTickIndexManager::::find_closest_higher( - netuid, - TickIndex::MAX.prev().unwrap() - ) - .is_none() - ); - assert!( - ActiveTickIndexManager::::find_closest_higher( - netuid, - TickIndex::MIN.next().unwrap() - ) - .is_none() - ); - - ActiveTickIndexManager::::insert(netuid, TickIndex::MAX); - - assert_eq!( - ActiveTickIndexManager::::find_closest_lower(netuid, TickIndex::MIN) - .unwrap(), - TickIndex::MIN - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_lower(netuid, TickIndex::MAX) - .unwrap(), - TickIndex::MAX - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_lower( - netuid, - TickIndex::MAX.saturating_div(2) - ) - .unwrap(), - TickIndex::MIN - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_lower( - netuid, - TickIndex::MAX.prev().unwrap() - ) - .unwrap(), - TickIndex::MIN - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_lower( - netuid, - TickIndex::MIN.next().unwrap() - ) - .unwrap(), - TickIndex::MIN - ); - }); - } - - #[test] - fn test_tick_search_sparse_queries() { - new_test_ext().execute_with(|| { - let active_index = TickIndex::MIN.saturating_add(10); - let netuid = NetUid::from(1); - - ActiveTickIndexManager::::insert(netuid, active_index); - - assert_eq!( - ActiveTickIndexManager::::find_closest_lower(netuid, active_index) - .unwrap(), - active_index - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_lower( - netuid, - TickIndex::MIN.saturating_add(11) - ) - .unwrap(), - active_index - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_lower( - netuid, - TickIndex::MIN.saturating_add(12) - ) - .unwrap(), - active_index - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_lower(netuid, TickIndex::MIN), - None - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_lower( - netuid, - TickIndex::MIN.saturating_add(9) - ), - None - ); - - assert_eq!( - ActiveTickIndexManager::::find_closest_higher(netuid, active_index) - .unwrap(), - active_index - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_higher( - netuid, - TickIndex::MIN.saturating_add(11) - ), - None - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_higher( - netuid, - TickIndex::MIN.saturating_add(12) - ), - None - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_higher(netuid, TickIndex::MIN) - .unwrap(), - active_index - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_higher( - netuid, - TickIndex::MIN.saturating_add(9) - ) - .unwrap(), - active_index - ); - }); - } - - #[test] - fn test_tick_search_many_lows() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - - (0..1000).for_each(|i| { - ActiveTickIndexManager::::insert( - netuid, - TickIndex::MIN.saturating_add(i), - ); - }); - - for i in 0..1000 { - let test_index = TickIndex::MIN.saturating_add(i); - assert_eq!( - ActiveTickIndexManager::::find_closest_lower(netuid, test_index) - .unwrap(), - test_index - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_higher(netuid, test_index) - .unwrap(), - test_index - ); - } - }); - } - - #[test] - fn test_tick_search_many_sparse() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - let count = 1000; - - for i in 0..=count { - ActiveTickIndexManager::::insert( - netuid, - TickIndex::new_unchecked(i * 10), - ); - } - - for i in 1..count { - let tick = TickIndex::new_unchecked(i * 10); - assert_eq!( - ActiveTickIndexManager::::find_closest_lower(netuid, tick).unwrap(), - tick - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_higher(netuid, tick).unwrap(), - tick - ); - for j in 1..=9 { - let before_tick = TickIndex::new_unchecked(i * 10 - j); - let after_tick = TickIndex::new_unchecked(i * 10 + j); - let prev_tick = TickIndex::new_unchecked((i - 1) * 10); - let next_tick = TickIndex::new_unchecked((i + 1) * 10); - assert_eq!( - ActiveTickIndexManager::::find_closest_lower(netuid, before_tick) - .unwrap(), - prev_tick - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_lower(netuid, after_tick) - .unwrap(), - tick - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_higher( - netuid, - before_tick - ) - .unwrap(), - tick - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_higher(netuid, after_tick) - .unwrap(), - next_tick - ); - } - } - }); - } - - #[test] - fn test_tick_search_many_lows_sparse_reversed() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - let count = 1000; - - for i in (0..=count).rev() { - ActiveTickIndexManager::::insert( - netuid, - TickIndex::new_unchecked(i * 10), - ); - } - - for i in 1..count { - let tick = TickIndex::new_unchecked(i * 10); - assert_eq!( - ActiveTickIndexManager::::find_closest_lower(netuid, tick).unwrap(), - tick - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_higher(netuid, tick).unwrap(), - tick - ); - for j in 1..=9 { - let before_tick = TickIndex::new_unchecked(i * 10 - j); - let after_tick = TickIndex::new_unchecked(i * 10 + j); - let prev_tick = TickIndex::new_unchecked((i - 1) * 10); - let next_tick = TickIndex::new_unchecked((i + 1) * 10); - - assert_eq!( - ActiveTickIndexManager::::find_closest_lower(netuid, before_tick) - .unwrap(), - prev_tick - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_lower(netuid, after_tick) - .unwrap(), - tick - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_higher( - netuid, - before_tick - ) - .unwrap(), - tick - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_higher(netuid, after_tick) - .unwrap(), - next_tick - ); - } - } - }); - } - - #[test] - fn test_tick_search_repeated_insertions() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - let count = 1000; - - for _ in 0..10 { - for i in 0..=count { - let tick = TickIndex::new_unchecked(i * 10); - ActiveTickIndexManager::::insert(netuid, tick); - } - - for i in 1..count { - let tick = TickIndex::new_unchecked(i * 10); - assert_eq!( - ActiveTickIndexManager::::find_closest_lower(netuid, tick) - .unwrap(), - tick - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_higher(netuid, tick) - .unwrap(), - tick - ); - for j in 1..=9 { - let before_tick = TickIndex::new_unchecked(i * 10 - j); - let after_tick = TickIndex::new_unchecked(i * 10 + j); - let prev_tick = TickIndex::new_unchecked((i - 1) * 10); - let next_tick = TickIndex::new_unchecked((i + 1) * 10); - - assert_eq!( - ActiveTickIndexManager::::find_closest_lower( - netuid, - before_tick - ) - .unwrap(), - prev_tick - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_lower( - netuid, after_tick - ) - .unwrap(), - tick - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_higher( - netuid, - before_tick - ) - .unwrap(), - tick - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_higher( - netuid, after_tick - ) - .unwrap(), - next_tick - ); - } - } - } - }); - } - - #[test] - fn test_tick_search_full_range() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - let step = 1019; - // Get the full valid tick range by subtracting MIN from MAX - let count = (TickIndex::MAX.get() - TickIndex::MIN.get()) / step; - - for i in 0..=count { - let index = TickIndex::MIN.saturating_add(i * step); - ActiveTickIndexManager::::insert(netuid, index); - } - for i in 1..count { - let index = TickIndex::MIN.saturating_add(i * step); - - let prev_index = TickIndex::new_unchecked(index.get() - step); - let next_minus_one = TickIndex::new_unchecked(index.get() + step - 1); - - assert_eq!( - ActiveTickIndexManager::::find_closest_lower(netuid, prev_index) - .unwrap(), - prev_index - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_lower(netuid, index).unwrap(), - index - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_lower(netuid, next_minus_one) - .unwrap(), - index - ); - - let mid_next = TickIndex::new_unchecked(index.get() + step / 2); - assert_eq!( - ActiveTickIndexManager::::find_closest_lower(netuid, mid_next) - .unwrap(), - index - ); - - assert_eq!( - ActiveTickIndexManager::::find_closest_higher(netuid, index).unwrap(), - index - ); - - let next_index = TickIndex::new_unchecked(index.get() + step); - assert_eq!( - ActiveTickIndexManager::::find_closest_higher(netuid, next_index) - .unwrap(), - next_index - ); - - let mid_next = TickIndex::new_unchecked(index.get() + step / 2); - assert_eq!( - ActiveTickIndexManager::::find_closest_higher(netuid, mid_next) - .unwrap(), - next_index - ); - - let next_minus_1 = TickIndex::new_unchecked(index.get() + step - 1); - assert_eq!( - ActiveTickIndexManager::::find_closest_higher(netuid, next_minus_1) - .unwrap(), - next_index - ); - for j in 1..=9 { - let before_index = TickIndex::new_unchecked(index.get() - j); - let after_index = TickIndex::new_unchecked(index.get() + j); - - assert_eq!( - ActiveTickIndexManager::::find_closest_lower( - netuid, - before_index - ) - .unwrap(), - prev_index - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_lower(netuid, after_index) - .unwrap(), - index - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_higher( - netuid, - before_index - ) - .unwrap(), - index - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_higher( - netuid, - after_index - ) - .unwrap(), - next_index - ); - } - } - }); - } - - #[test] - fn test_tick_remove_basic() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - - ActiveTickIndexManager::::insert(netuid, TickIndex::MIN); - ActiveTickIndexManager::::insert(netuid, TickIndex::MAX); - ActiveTickIndexManager::::remove(netuid, TickIndex::MAX); - - assert_eq!( - ActiveTickIndexManager::::find_closest_lower(netuid, TickIndex::MIN) - .unwrap(), - TickIndex::MIN - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_lower(netuid, TickIndex::MAX) - .unwrap(), - TickIndex::MIN - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_lower( - netuid, - TickIndex::MAX.saturating_div(2) - ) - .unwrap(), - TickIndex::MIN - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_lower( - netuid, - TickIndex::MAX.prev().unwrap() - ) - .unwrap(), - TickIndex::MIN - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_lower( - netuid, - TickIndex::MIN.next().unwrap() - ) - .unwrap(), - TickIndex::MIN - ); - - assert_eq!( - ActiveTickIndexManager::::find_closest_higher(netuid, TickIndex::MIN) - .unwrap(), - TickIndex::MIN - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_higher(netuid, TickIndex::MAX), - None - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_higher( - netuid, - TickIndex::MAX.saturating_div(2) - ), - None - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_higher( - netuid, - TickIndex::MAX.prev().unwrap() - ), - None - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_higher( - netuid, - TickIndex::MIN.next().unwrap() - ), - None - ); - }); - } - - #[test] - fn test_tick_remove_full_range() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - let step = 1019; - // Get the full valid tick range by subtracting MIN from MAX - let count = (TickIndex::MAX.get() - TickIndex::MIN.get()) / step; - let remove_frequency = 5; // Remove every 5th tick - - // Insert ticks - for i in 0..=count { - let index = TickIndex::MIN.saturating_add(i * step); - ActiveTickIndexManager::::insert(netuid, index); - } - - // Remove some ticks - for i in 1..count { - if i % remove_frequency == 0 { - let index = TickIndex::MIN.saturating_add(i * step); - ActiveTickIndexManager::::remove(netuid, index); - } - } - - // Verify - for i in 1..count { - let index = TickIndex::MIN.saturating_add(i * step); - - if i % remove_frequency == 0 { - let lower = - ActiveTickIndexManager::::find_closest_lower(netuid, index); - let higher = - ActiveTickIndexManager::::find_closest_higher(netuid, index); - assert!(lower != Some(index)); - assert!(higher != Some(index)); - } else { - assert_eq!( - ActiveTickIndexManager::::find_closest_lower(netuid, index) - .unwrap(), - index - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_higher(netuid, index) - .unwrap(), - index - ); - } - } - }); - } - } -} diff --git a/pallets/swap/src/weights.rs b/pallets/swap/src/weights.rs index 2bbbb8dbdf..210bf1dc6d 100644 --- a/pallets/swap/src/weights.rs +++ b/pallets/swap/src/weights.rs @@ -15,10 +15,6 @@ use sp_std::marker::PhantomData; /// Weight functions needed for pallet_subtensor_swap. pub trait WeightInfo { fn set_fee_rate() -> Weight; - fn add_liquidity() -> Weight; - fn remove_liquidity() -> Weight; - fn modify_position() -> Weight; - fn toggle_user_liquidity() -> Weight; } /// Default weights for pallet_subtensor_swap. @@ -30,34 +26,6 @@ impl WeightInfo for DefaultWeight { .saturating_add(T::DbWeight::get().reads(1)) .saturating_add(T::DbWeight::get().writes(1)) } - - fn add_liquidity() -> Weight { - // Conservative weight estimate for add_liquidity - Weight::from_parts(50_000_000, 0) - .saturating_add(T::DbWeight::get().reads(5)) - .saturating_add(T::DbWeight::get().writes(4)) - } - - fn remove_liquidity() -> Weight { - // Conservative weight estimate for remove_liquidity - Weight::from_parts(50_000_000, 0) - .saturating_add(T::DbWeight::get().reads(4)) - .saturating_add(T::DbWeight::get().writes(4)) - } - - fn modify_position() -> Weight { - // Conservative weight estimate for modify_position - Weight::from_parts(50_000_000, 0) - .saturating_add(T::DbWeight::get().reads(4)) - .saturating_add(T::DbWeight::get().writes(4)) - } - - fn toggle_user_liquidity() -> Weight { - // Conservative weight estimate: one read and one write - Weight::from_parts(10_000_000, 0) - .saturating_add(T::DbWeight::get().reads(1)) - .saturating_add(T::DbWeight::get().writes(1)) - } } // For backwards compatibility and tests @@ -67,28 +35,4 @@ impl WeightInfo for () { .saturating_add(RocksDbWeight::get().reads(1)) .saturating_add(RocksDbWeight::get().writes(1)) } - - fn add_liquidity() -> Weight { - Weight::from_parts(50_000_000, 0) - .saturating_add(RocksDbWeight::get().reads(5)) - .saturating_add(RocksDbWeight::get().writes(4)) - } - - fn remove_liquidity() -> Weight { - Weight::from_parts(50_000_000, 0) - .saturating_add(RocksDbWeight::get().reads(4)) - .saturating_add(RocksDbWeight::get().writes(4)) - } - - fn modify_position() -> Weight { - Weight::from_parts(50_000_000, 0) - .saturating_add(RocksDbWeight::get().reads(4)) - .saturating_add(RocksDbWeight::get().writes(4)) - } - - fn toggle_user_liquidity() -> Weight { - Weight::from_parts(10_000_000, 0) - .saturating_add(RocksDbWeight::get().reads(1)) - .saturating_add(RocksDbWeight::get().writes(1)) - } } diff --git a/pallets/transaction-fee/src/lib.rs b/pallets/transaction-fee/src/lib.rs index fc2a16a409..21e72e8853 100644 --- a/pallets/transaction-fee/src/lib.rs +++ b/pallets/transaction-fee/src/lib.rs @@ -30,8 +30,8 @@ use subtensor_swap_interface::SwapHandler; use core::marker::PhantomData; use smallvec::smallvec; use sp_std::vec::Vec; -use substrate_fixed::types::U96F32; -use subtensor_runtime_common::{Balance, Currency, NetUid}; +use substrate_fixed::types::U64F64; +use subtensor_runtime_common::{AuthorshipInfo, Balance, NetUid}; // Tests #[cfg(test)] @@ -47,7 +47,7 @@ impl WeightToFeePolynomial for LinearWeightToFee { fn polynomial() -> WeightToFeeCoefficients { let coefficient = WeightToFeeCoefficient { coeff_integer: 0, - coeff_frac: Perbill::from_parts(50_000), // 0.05 unit per weight + coeff_frac: Perbill::from_parts(500_000), // 0.5 unit per weight negative: false, degree: 1, }; @@ -95,6 +95,7 @@ where T: frame_system::Config, T: pallet_subtensor::Config, T: pallet_balances::Config, + T: AuthorshipInfo>, { fn on_nonzero_unbalanced( imbalance: FungibleImbalance< @@ -103,11 +104,18 @@ where IncreaseIssuance, pallet_balances::Pallet>, >, ) { - let ti_before = pallet_subtensor::TotalIssuance::::get(); - pallet_subtensor::TotalIssuance::::put( - ti_before.saturating_sub(imbalance.peek().into()), - ); - drop(imbalance); + if let Some(author) = T::author() { + // Pay block author instead of burning. + // One of these is the right call depending on your exact fungible API: + // let _ = pallet_balances::Pallet::::resolve(&author, imbalance); + // or: let _ = pallet_balances::Pallet::::deposit(&author, imbalance.peek(), Precision::BestEffort); + // + // Prefer "resolve" (moves the actual imbalance) if available: + let _ = as Balanced<_>>::resolve(&author, imbalance); + } else { + // Fallback: if no author, burn (or just drop). + drop(imbalance); + } } } @@ -145,7 +153,7 @@ where // This is not ideal because it may not pay all fees, but UX is the priority // and this approach still provides spam protection. alpha_vec.iter().any(|(hotkey, netuid)| { - let alpha_balance = U96F32::saturating_from_num( + let alpha_balance = U64F64::saturating_from_num( pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( hotkey, coldkey, *netuid, ), @@ -168,13 +176,13 @@ where alpha_vec.iter().for_each(|(hotkey, netuid)| { // Divide tao_amount evenly among all alpha entries - let alpha_balance = U96F32::saturating_from_num( + let alpha_balance = U64F64::saturating_from_num( pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( hotkey, coldkey, *netuid, ), ); let alpha_price = pallet_subtensor_swap::Pallet::::current_alpha_price(*netuid); - let alpha_fee = U96F32::saturating_from_num(tao_per_entry) + let alpha_fee = U64F64::saturating_from_num(tao_per_entry) .checked_div(alpha_price) .unwrap_or(alpha_balance) .min(alpha_balance) diff --git a/pallets/transaction-fee/src/tests/mock.rs b/pallets/transaction-fee/src/tests/mock.rs index 3bad4a275f..e6452a0bcd 100644 --- a/pallets/transaction-fee/src/tests/mock.rs +++ b/pallets/transaction-fee/src/tests/mock.rs @@ -21,7 +21,7 @@ use sp_runtime::{ }; use sp_std::cmp::Ordering; use sp_weights::Weight; -pub use subtensor_runtime_common::{AlphaCurrency, Currency, NetUid, TaoCurrency}; +pub use subtensor_runtime_common::{AlphaCurrency, AuthorshipInfo, Currency, NetUid, TaoCurrency}; use subtensor_swap_interface::{Order, SwapHandler}; use crate::SubtensorTxFeeHandler; @@ -29,10 +29,7 @@ use pallet_transaction_payment::{ConstFeeMultiplier, Multiplier}; pub const TAO: u64 = 1_000_000_000; -pub type Block = sp_runtime::generic::Block< - sp_runtime::generic::Header, - UncheckedExtrinsic, ->; +type Block = frame_system::mocking::MockBlock; // Configure a mock runtime to test the pallet. frame_support::construct_runtime!( @@ -139,6 +136,22 @@ impl pallet_transaction_payment::Config for Test { type WeightInfo = pallet_transaction_payment::weights::SubstrateWeight; } +pub struct MockAuthorshipProvider; + +pub const MOCK_BLOCK_BUILDER: u64 = 12345u64; + +impl AuthorshipInfo for MockAuthorshipProvider { + fn author() -> Option { + Some(U256::from(MOCK_BLOCK_BUILDER)) + } +} + +impl AuthorshipInfo for Test { + fn author() -> Option { + Some(U256::from(MOCK_BLOCK_BUILDER)) + } +} + parameter_types! { pub const InitialMinAllowedWeights: u16 = 0; pub const InitialEmissionValue: u16 = 0; @@ -196,17 +209,14 @@ parameter_types! { pub const InitialNetworkMinLockCost: u64 = 100_000_000_000; pub const InitialSubnetOwnerCut: u16 = 0; // 0%. 100% of rewards go to validators + miners. pub const InitialNetworkLockReductionInterval: u64 = 2; // 2 blocks. - // pub const InitialSubnetLimit: u16 = 10; // (DEPRECATED) pub const InitialNetworkRateLimit: u64 = 0; pub const InitialKeySwapCost: u64 = 1_000_000_000; pub const InitialAlphaHigh: u16 = 58982; // Represents 0.9 as per the production default pub const InitialAlphaLow: u16 = 45875; // Represents 0.7 as per the production default pub const InitialLiquidAlphaOn: bool = false; // Default value for LiquidAlphaOn pub const InitialYuma3On: bool = false; // Default value for Yuma3On - // pub const InitialHotkeyEmissionTempo: u64 = 1; // (DEPRECATED) - // pub const InitialNetworkMaxStake: u64 = u64::MAX; // (DEPRECATED) - pub const InitialColdkeySwapScheduleDuration: u64 = 5 * 24 * 60 * 60 / 12; // 5 days - pub const InitialColdkeySwapRescheduleDuration: u64 = 24 * 60 * 60 / 12; // 1 day + pub const InitialColdkeySwapAnnouncementDelay: u64 = 50; + pub const InitialColdkeySwapReannouncementDelay: u64 = 10; pub const InitialDissolveNetworkScheduleDuration: u64 = 5 * 24 * 60 * 60 / 12; // 5 days pub const InitialTaoWeight: u64 = u64::MAX/10; // 10% global weight. pub const InitialEmaPriceHalvingPeriod: u64 = 201_600_u64; // 4 weeks @@ -275,8 +285,8 @@ impl pallet_subtensor::Config for Test { type LiquidAlphaOn = InitialLiquidAlphaOn; type Yuma3On = InitialYuma3On; type Preimages = (); - type InitialColdkeySwapScheduleDuration = InitialColdkeySwapScheduleDuration; - type InitialColdkeySwapRescheduleDuration = InitialColdkeySwapRescheduleDuration; + type InitialColdkeySwapAnnouncementDelay = InitialColdkeySwapAnnouncementDelay; + type InitialColdkeySwapReannouncementDelay = InitialColdkeySwapReannouncementDelay; type InitialDissolveNetworkScheduleDuration = InitialDissolveNetworkScheduleDuration; type InitialTaoWeight = InitialTaoWeight; type InitialEmaPriceHalvingPeriod = InitialEmaPriceHalvingPeriod; @@ -290,6 +300,7 @@ impl pallet_subtensor::Config for Test { type MaxImmuneUidsPercentage = MaxImmuneUidsPercentage; type CommitmentsInterface = CommitmentsI; type EvmKeyAssociateRateLimit = EvmKeyAssociateRateLimit; + type AuthorshipProvider = MockAuthorshipProvider; } parameter_types! { @@ -390,7 +401,6 @@ impl pallet_balances::Config for Test { parameter_types! { pub const SwapProtocolId: PalletId = PalletId(*b"ten/swap"); pub const SwapMaxFeeRate: u16 = 10000; // 15.26% - pub const SwapMaxPositions: u32 = 100; pub const SwapMinimumLiquidity: u64 = 1_000; pub const SwapMinimumReserve: NonZeroU64 = NonZeroU64::new(1_000_000).unwrap(); } @@ -402,7 +412,6 @@ impl pallet_subtensor_swap::Config for Test { type TaoReserve = pallet_subtensor::TaoCurrencyReserve; type AlphaReserve = pallet_subtensor::AlphaCurrencyReserve; type MaxFeeRate = SwapMaxFeeRate; - type MaxPositions = SwapMaxPositions; type MinimumLiquidity = SwapMinimumLiquidity; type MinimumReserve = SwapMinimumReserve; type WeightInfo = (); @@ -522,7 +531,12 @@ where pallet_transaction_payment::ChargeTransactionPayment::::from(0), ); - Some(UncheckedExtrinsic::new_signed(call, nonce, (), extra)) + Some(UncheckedExtrinsic::new_signed( + call, + nonce.into(), + (), + extra, + )) } } @@ -629,6 +643,38 @@ pub(crate) fn swap_alpha_to_tao(netuid: NetUid, alpha: AlphaCurrency) -> (u64, u swap_alpha_to_tao_ext(netuid, alpha, false) } +pub(crate) fn swap_tao_to_alpha_ext( + netuid: NetUid, + tao: TaoCurrency, + drop_fees: bool, +) -> (u64, u64) { + if netuid.is_root() { + return (tao.into(), 0); + } + + let order = GetAlphaForTao::::with_amount(tao); + let result = ::SwapInterface::swap( + netuid.into(), + order, + ::SwapInterface::max_price(), + drop_fees, + true, + ); + + assert_ok!(&result); + + let result = result.unwrap(); + + // we don't want to have silent 0 comparisons in tests + assert!(!result.amount_paid_out.is_zero()); + + (result.amount_paid_out.to_u64(), result.fee_paid.to_u64()) +} + +pub(crate) fn swap_tao_to_alpha(netuid: NetUid, tao: TaoCurrency) -> (u64, u64) { + swap_tao_to_alpha_ext(netuid, tao, false) +} + #[allow(dead_code)] pub fn add_network(netuid: NetUid, tempo: u16) { SubtensorModule::init_new_network(netuid, tempo); diff --git a/pallets/transaction-fee/src/tests/mod.rs b/pallets/transaction-fee/src/tests/mod.rs index b6697e87f0..80cfab5fd3 100644 --- a/pallets/transaction-fee/src/tests/mod.rs +++ b/pallets/transaction-fee/src/tests/mod.rs @@ -2,12 +2,11 @@ use crate::TransactionSource; use frame_support::assert_ok; use frame_support::dispatch::GetDispatchInfo; -use pallet_subtensor_swap::AlphaSqrtPrice; use sp_runtime::{ traits::{DispatchTransaction, TransactionExtension, TxBaseImplication}, transaction_validity::{InvalidTransaction, TransactionValidityError}, }; -use substrate_fixed::types::U64F64; +// use substrate_fixed::types::U64F64; use subtensor_runtime_common::AlphaCurrency; use mock::*; @@ -444,7 +443,9 @@ fn test_remove_stake_edge_alpha() { assert_ok!(result); // Lower Alpha price to 0.0001 so that there is not enough alpha to cover tx fees - AlphaSqrtPrice::::insert(sn.subnets[0].netuid, U64F64::from_num(0.01)); + SubnetTAO::::insert(sn.subnets[0].netuid, TaoCurrency::from(1_000_000)); + SubnetAlphaIn::::insert(sn.subnets[0].netuid, AlphaCurrency::from(10_000_000_000)); + let result_low_alpha_price = ext.validate( RuntimeOrigin::signed(sn.coldkey).into(), &call.clone(), @@ -1209,3 +1210,55 @@ fn test_recycle_alpha_fees_alpha() { assert!(actual_alpha_fee > 0.into()); }); } + +// cargo test --package subtensor-transaction-fee --lib -- tests::test_add_stake_fees_go_to_block_builder --exact --show-output +#[test] +fn test_add_stake_fees_go_to_block_builder() { + new_test_ext().execute_with(|| { + // Portion of swap fees that should go to the block builder + let block_builder_fee_portion = 3. / 5.; + + // Get the block builder balance + let block_builder = U256::from(MOCK_BLOCK_BUILDER); + let block_builder_balance_before = Balances::free_balance(block_builder); + + let stake_amount = TAO; + let sn = setup_subnets(1, 1); + + // Simulate add stake to get the expected TAO fee + let (_, swap_fee) = mock::swap_tao_to_alpha(sn.subnets[0].netuid, stake_amount.into()); + + SubtensorModule::add_balance_to_coldkey_account(&sn.coldkey, stake_amount * 10_u64); + remove_stake_rate_limit_for_tests(&sn.hotkeys[0], &sn.coldkey, sn.subnets[0].netuid); + + // Stake + let balance_before = Balances::free_balance(sn.coldkey); + let call = RuntimeCall::SubtensorModule(pallet_subtensor::Call::add_stake { + hotkey: sn.hotkeys[0], + netuid: sn.subnets[0].netuid, + amount_staked: stake_amount.into(), + }); + + // Dispatch the extrinsic with ChargeTransactionPayment extension + let info = call.get_dispatch_info(); + let ext = pallet_transaction_payment::ChargeTransactionPayment::::from(0); + assert_ok!(ext.dispatch_transaction( + RuntimeOrigin::signed(sn.coldkey).into(), + call, + &info, + 0, + 0, + )); + + let final_balance = Balances::free_balance(sn.coldkey); + let actual_tao_fee = balance_before - stake_amount - final_balance; + assert!(actual_tao_fee > 0); + + // Expect that block builder balance has increased by both the swap fee and the transaction fee + let expected_block_builder_swap_reward = swap_fee as f64 * block_builder_fee_portion; + let expected_tx_fee = 0.000136; // Use very low value for less test flakiness + let block_builder_balance_after = Balances::free_balance(block_builder); + let actual_reward = block_builder_balance_after - block_builder_balance_before; + assert!(actual_reward as f64 >= expected_block_builder_swap_reward + expected_tx_fee); + }); +} diff --git a/precompiles/Cargo.toml b/precompiles/Cargo.toml index 400e1caa9f..be1824cf91 100644 --- a/precompiles/Cargo.toml +++ b/precompiles/Cargo.toml @@ -38,6 +38,7 @@ pallet-subtensor-swap.workspace = true pallet-admin-utils.workspace = true subtensor-swap-interface.workspace = true pallet-crowdloan.workspace = true +pallet-shield.workspace = true [lints] workspace = true @@ -63,6 +64,7 @@ std = [ "pallet-subtensor-proxy/std", "pallet-subtensor-swap/std", "pallet-subtensor/std", + "pallet-shield/std", "precompile-utils/std", "scale-info/std", "sp-core/std", diff --git a/precompiles/src/alpha.rs b/precompiles/src/alpha.rs index 8dcea0e829..98737f1269 100644 --- a/precompiles/src/alpha.rs +++ b/precompiles/src/alpha.rs @@ -5,7 +5,7 @@ use pallet_evm::{BalanceConverter, PrecompileHandle, SubstrateBalance}; use precompile_utils::EvmResult; use sp_core::U256; use sp_std::vec::Vec; -use substrate_fixed::types::U96F32; +use substrate_fixed::types::{U64F64, U96F32}; use subtensor_runtime_common::{Currency, NetUid}; use subtensor_swap_interface::{Order, SwapHandler}; @@ -37,7 +37,7 @@ where fn get_alpha_price(_handle: &mut impl PrecompileHandle, netuid: u16) -> EvmResult { let current_alpha_price = as SwapHandler>::current_alpha_price(netuid.into()); - let price = current_alpha_price.saturating_mul(U96F32::from_num(1_000_000_000)); + let price = current_alpha_price.saturating_mul(U64F64::from_num(1_000_000_000)); let price: SubstrateBalance = price.saturating_to_num::().into(); let price_eth = ::BalanceConverter::into_evm_balance(price) .map(|amount| amount.into_u256()) @@ -194,18 +194,18 @@ where .filter(|(netuid, _)| *netuid != NetUid::ROOT) .collect::>(); - let mut sum_alpha_price: U96F32 = U96F32::from_num(0); + let mut sum_alpha_price: U64F64 = U64F64::from_num(0); for (netuid, _) in netuids { let price = as SwapHandler>::current_alpha_price( netuid.into(), ); - if price < U96F32::from_num(1) { + if price < U64F64::from_num(1) { sum_alpha_price = sum_alpha_price.saturating_add(price); } } - let price = sum_alpha_price.saturating_mul(U96F32::from_num(1_000_000_000)); + let price = sum_alpha_price.saturating_mul(U64F64::from_num(1_000_000_000)); let price: SubstrateBalance = price.saturating_to_num::().into(); let price_eth = ::BalanceConverter::into_evm_balance(price) .map(|amount| amount.into_u256()) diff --git a/precompiles/src/balance_transfer.rs b/precompiles/src/balance_transfer.rs index c1cdab6ca5..d8d10970a3 100644 --- a/precompiles/src/balance_transfer.rs +++ b/precompiles/src/balance_transfer.rs @@ -10,7 +10,7 @@ use sp_runtime::traits::{AsSystemOriginSigner, Dispatchable, StaticLookup, Uniqu use crate::{PrecompileExt, PrecompileHandleExt}; -pub(crate) struct BalanceTransferPrecompile(PhantomData); +pub struct BalanceTransferPrecompile(PhantomData); impl PrecompileExt for BalanceTransferPrecompile where @@ -18,6 +18,8 @@ where + pallet_balances::Config + pallet_evm::Config + pallet_subtensor::Config + + pallet_shield::Config + + pallet_subtensor_proxy::Config + Send + Sync + scale_info::TypeInfo, @@ -26,7 +28,9 @@ where ::RuntimeCall: GetDispatchInfo + Dispatchable + IsSubType> - + IsSubType>, + + IsSubType> + + IsSubType> + + IsSubType>, ::RuntimeCall: From> + GetDispatchInfo + Dispatchable, @@ -43,6 +47,8 @@ where + pallet_balances::Config + pallet_evm::Config + pallet_subtensor::Config + + pallet_shield::Config + + pallet_subtensor_proxy::Config + Send + Sync + scale_info::TypeInfo, @@ -51,7 +57,9 @@ where ::RuntimeCall: GetDispatchInfo + Dispatchable + IsSubType> - + IsSubType>, + + IsSubType> + + IsSubType> + + IsSubType>, ::RuntimeCall: From> + GetDispatchInfo + Dispatchable, diff --git a/precompiles/src/crowdloan.rs b/precompiles/src/crowdloan.rs index f0971015d9..c9926dfe3b 100644 --- a/precompiles/src/crowdloan.rs +++ b/precompiles/src/crowdloan.rs @@ -25,6 +25,8 @@ where + pallet_evm::Config + pallet_proxy::Config + pallet_subtensor::Config + + pallet_shield::Config + + pallet_subtensor_proxy::Config + Send + Sync + scale_info::TypeInfo, @@ -34,7 +36,9 @@ where + GetDispatchInfo + Dispatchable + IsSubType> - + IsSubType>, + + IsSubType> + + IsSubType> + + IsSubType>, ::AddressMapping: AddressMapping, { const INDEX: u64 = 2057; @@ -49,6 +53,8 @@ where + pallet_evm::Config + pallet_proxy::Config + pallet_subtensor::Config + + pallet_shield::Config + + pallet_subtensor_proxy::Config + Send + Sync + scale_info::TypeInfo, @@ -58,7 +64,9 @@ where + GetDispatchInfo + Dispatchable + IsSubType> - + IsSubType>, + + IsSubType> + + IsSubType> + + IsSubType>, ::RuntimeCall: From> + GetDispatchInfo + Dispatchable, diff --git a/precompiles/src/ed25519.rs b/precompiles/src/ed25519.rs index 1ea581bd35..dbfe032cdf 100644 --- a/precompiles/src/ed25519.rs +++ b/precompiles/src/ed25519.rs @@ -8,7 +8,7 @@ use fp_evm::{ExitError, ExitSucceed, LinearCostPrecompile, PrecompileFailure}; use crate::{PrecompileExt, parse_slice}; -pub(crate) struct Ed25519Verify(PhantomData); +pub struct Ed25519Verify(PhantomData); impl PrecompileExt for Ed25519Verify where diff --git a/precompiles/src/extensions.rs b/precompiles/src/extensions.rs index 5d73a75f9b..51287a1b88 100644 --- a/precompiles/src/extensions.rs +++ b/precompiles/src/extensions.rs @@ -10,7 +10,7 @@ use pallet_evm::{ AddressMapping, BalanceConverter, EvmBalance, ExitError, GasWeightMapping, Precompile, PrecompileFailure, PrecompileHandle, PrecompileResult, }; -use pallet_subtensor::transaction_extension::SubtensorTransactionExtension; +use pallet_subtensor::SubtensorTransactionExtension; use precompile_utils::EvmResult; use scale_info::TypeInfo; use sp_core::{H160, U256, blake2_256}; @@ -58,6 +58,8 @@ pub(crate) trait PrecompileHandleExt: PrecompileHandle { + pallet_balances::Config + pallet_evm::Config + pallet_subtensor::Config + + pallet_shield::Config + + pallet_subtensor_proxy::Config + Send + Sync + TypeInfo, @@ -65,7 +67,9 @@ pub(crate) trait PrecompileHandleExt: PrecompileHandle { ::RuntimeCall: GetDispatchInfo + Dispatchable + IsSubType> - + IsSubType>, + + IsSubType> + + IsSubType> + + IsSubType>, ::RuntimeOrigin: From> + AsSystemOriginSigner + Clone, { @@ -182,7 +186,7 @@ fn extension_error(err: TransactionValidityError) -> PrecompileFailure { impl PrecompileHandleExt for T where T: PrecompileHandle {} -pub(crate) trait PrecompileExt>: Precompile { +pub trait PrecompileExt>: Precompile { const INDEX: u64; // ss58 public key i.e., the contract sends funds it received to the destination address from diff --git a/precompiles/src/leasing.rs b/precompiles/src/leasing.rs index 01a8db4354..732e687c6f 100644 --- a/precompiles/src/leasing.rs +++ b/precompiles/src/leasing.rs @@ -26,6 +26,8 @@ where + pallet_evm::Config + pallet_subtensor::Config + pallet_crowdloan::Config + + pallet_shield::Config + + pallet_subtensor_proxy::Config + Send + Sync + scale_info::TypeInfo, @@ -36,7 +38,9 @@ where + GetDispatchInfo + Dispatchable + IsSubType> - + IsSubType>, + + IsSubType> + + IsSubType> + + IsSubType>, ::AddressMapping: AddressMapping, { const INDEX: u64 = 2058; @@ -50,6 +54,8 @@ where + pallet_evm::Config + pallet_subtensor::Config + pallet_crowdloan::Config + + pallet_shield::Config + + pallet_subtensor_proxy::Config + Send + Sync + scale_info::TypeInfo, @@ -60,7 +66,9 @@ where + GetDispatchInfo + Dispatchable + IsSubType> - + IsSubType>, + + IsSubType> + + IsSubType> + + IsSubType>, ::AddressMapping: AddressMapping, { #[precompile::public("getLease(uint32)")] diff --git a/precompiles/src/lib.rs b/precompiles/src/lib.rs index 864119d89f..a824ac39d4 100644 --- a/precompiles/src/lib.rs +++ b/precompiles/src/lib.rs @@ -10,6 +10,7 @@ use frame_support::{ dispatch::{DispatchInfo, GetDispatchInfo, PostDispatchInfo}, pallet_prelude::Decode, }; +use pallet_admin_utils::PrecompileEnum; use pallet_evm::{ AddressMapping, IsPrecompileResult, Precompile, PrecompileHandle, PrecompileResult, PrecompileSet, @@ -24,23 +25,24 @@ use sp_core::{H160, U256, crypto::ByteArray}; use sp_runtime::traits::{AsSystemOriginSigner, Dispatchable, StaticLookup}; use subtensor_runtime_common::ProxyType; -use pallet_admin_utils::PrecompileEnum; - -use crate::address_mapping::*; -use crate::alpha::*; -use crate::balance_transfer::*; -use crate::crowdloan::*; -use crate::ed25519::*; use crate::extensions::*; -use crate::leasing::*; -use crate::metagraph::*; -use crate::neuron::*; -use crate::proxy::*; -use crate::sr25519::*; -use crate::staking::*; -use crate::storage_query::*; -use crate::subnet::*; -use crate::uid_lookup::*; + +pub use address_mapping::AddressMappingPrecompile; +pub use alpha::AlphaPrecompile; +pub use balance_transfer::BalanceTransferPrecompile; +pub use crowdloan::CrowdloanPrecompile; +pub use ed25519::Ed25519Verify; +pub use extensions::PrecompileExt; +pub use leasing::LeasingPrecompile; +pub use metagraph::MetagraphPrecompile; +pub use neuron::NeuronPrecompile; +pub use proxy::ProxyPrecompile; +pub use sr25519::Sr25519Verify; +pub use staking::{StakingPrecompile, StakingPrecompileV2}; +pub use storage_query::StorageQueryPrecompile; +pub use subnet::SubnetPrecompile; +pub use uid_lookup::UidLookupPrecompile; +pub use voting_power::VotingPowerPrecompile; mod address_mapping; mod alpha; @@ -57,6 +59,7 @@ mod staking; mod storage_query; mod subnet; mod uid_lookup; +mod voting_power; pub struct Precompiles(PhantomData); @@ -70,6 +73,8 @@ where + pallet_subtensor_swap::Config + pallet_proxy::Config + pallet_crowdloan::Config + + pallet_shield::Config + + pallet_subtensor_proxy::Config + Send + Sync + scale_info::TypeInfo, @@ -83,7 +88,9 @@ where + GetDispatchInfo + Dispatchable + IsSubType> - + IsSubType>, + + IsSubType> + + IsSubType> + + IsSubType>, ::AddressMapping: AddressMapping, ::Balance: TryFrom, <::Lookup as StaticLookup>::Source: From, @@ -103,6 +110,8 @@ where + pallet_subtensor_swap::Config + pallet_proxy::Config + pallet_crowdloan::Config + + pallet_shield::Config + + pallet_subtensor_proxy::Config + Send + Sync + scale_info::TypeInfo, @@ -116,7 +125,9 @@ where + GetDispatchInfo + Dispatchable + IsSubType> - + IsSubType>, + + IsSubType> + + IsSubType> + + IsSubType>, ::AddressMapping: AddressMapping, ::Balance: TryFrom, <::Lookup as StaticLookup>::Source: From, @@ -125,7 +136,7 @@ where Self(Default::default()) } - pub fn used_addresses() -> [H160; 26] { + pub fn used_addresses() -> [H160; 27] { [ hash(1), hash(2), @@ -151,6 +162,7 @@ where hash(AlphaPrecompile::::INDEX), hash(CrowdloanPrecompile::::INDEX), hash(LeasingPrecompile::::INDEX), + hash(VotingPowerPrecompile::::INDEX), hash(ProxyPrecompile::::INDEX), hash(AddressMappingPrecompile::::INDEX), ] @@ -166,6 +178,8 @@ where + pallet_subtensor_swap::Config + pallet_proxy::Config + pallet_crowdloan::Config + + pallet_shield::Config + + pallet_subtensor_proxy::Config + Send + Sync + scale_info::TypeInfo, @@ -180,6 +194,8 @@ where + Dispatchable + IsSubType> + IsSubType> + + IsSubType> + + IsSubType> + Decode, <::RuntimeCall as Dispatchable>::RuntimeOrigin: From>>, @@ -245,6 +261,9 @@ where a if a == hash(LeasingPrecompile::::INDEX) => { LeasingPrecompile::::try_execute::(handle, PrecompileEnum::Leasing) } + a if a == hash(VotingPowerPrecompile::::INDEX) => { + VotingPowerPrecompile::::try_execute::(handle, PrecompileEnum::VotingPower) + } a if a == hash(ProxyPrecompile::::INDEX) => { ProxyPrecompile::::try_execute::(handle, PrecompileEnum::Proxy) } diff --git a/precompiles/src/neuron.rs b/precompiles/src/neuron.rs index 0b998b3c07..e2602c0246 100644 --- a/precompiles/src/neuron.rs +++ b/precompiles/src/neuron.rs @@ -19,6 +19,8 @@ where + pallet_balances::Config + pallet_evm::Config + pallet_subtensor::Config + + pallet_shield::Config + + pallet_subtensor_proxy::Config + Send + Sync + scale_info::TypeInfo, @@ -28,7 +30,9 @@ where + GetDispatchInfo + Dispatchable + IsSubType> - + IsSubType>, + + IsSubType> + + IsSubType> + + IsSubType>, ::AddressMapping: AddressMapping, { const INDEX: u64 = 2052; @@ -41,6 +45,8 @@ where + pallet_balances::Config + pallet_evm::Config + pallet_subtensor::Config + + pallet_shield::Config + + pallet_subtensor_proxy::Config + Send + Sync + scale_info::TypeInfo, @@ -50,7 +56,9 @@ where + GetDispatchInfo + Dispatchable + IsSubType> - + IsSubType>, + + IsSubType> + + IsSubType> + + IsSubType>, ::AddressMapping: AddressMapping, { #[precompile::public("setWeights(uint16,uint16[],uint16[],uint64)")] diff --git a/precompiles/src/proxy.rs b/precompiles/src/proxy.rs index 5139477f00..3312b67194 100644 --- a/precompiles/src/proxy.rs +++ b/precompiles/src/proxy.rs @@ -30,6 +30,8 @@ where + pallet_evm::Config + pallet_subtensor::Config + pallet_proxy::Config + + pallet_shield::Config + + pallet_subtensor_proxy::Config + Send + Sync + scale_info::TypeInfo, @@ -41,7 +43,9 @@ where + GetDispatchInfo + Dispatchable + IsSubType> - + IsSubType>, + + IsSubType> + + IsSubType> + + IsSubType>, ::AddressMapping: AddressMapping, <::Lookup as StaticLookup>::Source: From, { @@ -56,6 +60,8 @@ where + pallet_evm::Config + pallet_subtensor::Config + pallet_proxy::Config + + pallet_shield::Config + + pallet_subtensor_proxy::Config + Send + Sync + scale_info::TypeInfo, @@ -67,7 +73,9 @@ where + GetDispatchInfo + Dispatchable + IsSubType> - + IsSubType>, + + IsSubType> + + IsSubType> + + IsSubType>, <::Lookup as StaticLookup>::Source: From, { #[precompile::public("createPureProxy(uint8,uint32,uint16)")] diff --git a/precompiles/src/sr25519.rs b/precompiles/src/sr25519.rs index 6c6245d8f0..054948d524 100644 --- a/precompiles/src/sr25519.rs +++ b/precompiles/src/sr25519.rs @@ -9,7 +9,7 @@ use fp_evm::{ExitError, ExitSucceed, LinearCostPrecompile, PrecompileFailure}; use crate::{PrecompileExt, parse_slice}; -pub(crate) struct Sr25519Verify(PhantomData); +pub struct Sr25519Verify(PhantomData); impl PrecompileExt for Sr25519Verify where diff --git a/precompiles/src/staking.rs b/precompiles/src/staking.rs index 9cc2a2aaa4..c276c32e60 100644 --- a/precompiles/src/staking.rs +++ b/precompiles/src/staking.rs @@ -48,7 +48,7 @@ use crate::{PrecompileExt, PrecompileHandleExt}; // to stop supporting both precompiles. // // All the future extensions should happen in StakingPrecompileV2. -pub(crate) struct StakingPrecompileV2(PhantomData); +pub struct StakingPrecompileV2(PhantomData); impl PrecompileExt for StakingPrecompileV2 where @@ -57,6 +57,8 @@ where + pallet_evm::Config + pallet_subtensor::Config + pallet_proxy::Config + + pallet_shield::Config + + pallet_subtensor_proxy::Config + Send + Sync + scale_info::TypeInfo, @@ -67,7 +69,9 @@ where + GetDispatchInfo + Dispatchable + IsSubType> - + IsSubType>, + + IsSubType> + + IsSubType> + + IsSubType>, ::AddressMapping: AddressMapping, <::Lookup as StaticLookup>::Source: From, { @@ -82,6 +86,8 @@ where + pallet_evm::Config + pallet_subtensor::Config + pallet_proxy::Config + + pallet_shield::Config + + pallet_subtensor_proxy::Config + Send + Sync + scale_info::TypeInfo, @@ -92,7 +98,9 @@ where + GetDispatchInfo + Dispatchable + IsSubType> - + IsSubType>, + + IsSubType> + + IsSubType> + + IsSubType>, ::AddressMapping: AddressMapping, <::Lookup as StaticLookup>::Source: From, { @@ -442,7 +450,7 @@ where } // Deprecated, exists for backward compatibility. -pub(crate) struct StakingPrecompile(PhantomData); +pub struct StakingPrecompile(PhantomData); impl PrecompileExt for StakingPrecompile where @@ -451,6 +459,8 @@ where + pallet_subtensor::Config + pallet_proxy::Config + pallet_balances::Config + + pallet_shield::Config + + pallet_subtensor_proxy::Config + Send + Sync + scale_info::TypeInfo, @@ -462,7 +472,9 @@ where + GetDispatchInfo + Dispatchable + IsSubType> - + IsSubType>, + + IsSubType> + + IsSubType> + + IsSubType>, ::AddressMapping: AddressMapping, ::Balance: TryFrom, <::Lookup as StaticLookup>::Source: From, @@ -478,6 +490,8 @@ where + pallet_subtensor::Config + pallet_proxy::Config + pallet_balances::Config + + pallet_shield::Config + + pallet_subtensor_proxy::Config + Send + Sync + scale_info::TypeInfo, @@ -489,7 +503,9 @@ where + GetDispatchInfo + Dispatchable + IsSubType> - + IsSubType>, + + IsSubType> + + IsSubType> + + IsSubType>, ::AddressMapping: AddressMapping, ::Balance: TryFrom, <::Lookup as StaticLookup>::Source: From, diff --git a/precompiles/src/storage_query.rs b/precompiles/src/storage_query.rs index 3022fff36b..796d1c8f04 100644 --- a/precompiles/src/storage_query.rs +++ b/precompiles/src/storage_query.rs @@ -62,7 +62,7 @@ const AUTHORIZED_PREFIXES: [[u8; 16]; 10] = [ use crate::PrecompileExt; -pub(crate) struct StorageQueryPrecompile(PhantomData); +pub struct StorageQueryPrecompile(PhantomData); impl PrecompileExt for StorageQueryPrecompile where diff --git a/precompiles/src/subnet.rs b/precompiles/src/subnet.rs index 4df373c116..9d1e24cc1e 100644 --- a/precompiles/src/subnet.rs +++ b/precompiles/src/subnet.rs @@ -22,6 +22,8 @@ where + pallet_evm::Config + pallet_subtensor::Config + pallet_admin_utils::Config + + pallet_shield::Config + + pallet_subtensor_proxy::Config + Send + Sync + scale_info::TypeInfo, @@ -32,7 +34,9 @@ where + GetDispatchInfo + Dispatchable + IsSubType> - + IsSubType>, + + IsSubType> + + IsSubType> + + IsSubType>, ::AddressMapping: AddressMapping, { const INDEX: u64 = 2051; @@ -45,7 +49,9 @@ where + pallet_balances::Config + pallet_evm::Config + pallet_subtensor::Config + + pallet_shield::Config + pallet_admin_utils::Config + + pallet_subtensor_proxy::Config + Send + Sync + scale_info::TypeInfo, @@ -56,7 +62,9 @@ where + GetDispatchInfo + Dispatchable + IsSubType> - + IsSubType>, + + IsSubType> + + IsSubType> + + IsSubType>, ::AddressMapping: AddressMapping, { #[precompile::public("registerNetwork(bytes32)")] diff --git a/precompiles/src/uid_lookup.rs b/precompiles/src/uid_lookup.rs index be8c803b45..b791b96786 100644 --- a/precompiles/src/uid_lookup.rs +++ b/precompiles/src/uid_lookup.rs @@ -8,7 +8,7 @@ use sp_std::vec::Vec; use crate::PrecompileExt; -pub(crate) struct UidLookupPrecompile(PhantomData); +pub struct UidLookupPrecompile(PhantomData); impl PrecompileExt for UidLookupPrecompile where diff --git a/precompiles/src/voting_power.rs b/precompiles/src/voting_power.rs new file mode 100644 index 0000000000..74e1731b6e --- /dev/null +++ b/precompiles/src/voting_power.rs @@ -0,0 +1,131 @@ +use core::marker::PhantomData; + +use fp_evm::PrecompileHandle; +use precompile_utils::EvmResult; +use sp_core::{ByteArray, H256, U256}; +use subtensor_runtime_common::NetUid; + +use crate::PrecompileExt; + +/// VotingPower precompile for smart contract access to validator voting power. +/// +/// This precompile allows smart contracts to query voting power for validators, +/// enabling on-chain governance decisions like slashing and spending. +pub struct VotingPowerPrecompile(PhantomData); + +impl PrecompileExt for VotingPowerPrecompile +where + R: frame_system::Config + pallet_subtensor::Config, + R::AccountId: From<[u8; 32]> + ByteArray, +{ + const INDEX: u64 = 2061; +} + +#[precompile_utils::precompile] +impl VotingPowerPrecompile +where + R: frame_system::Config + pallet_subtensor::Config, + R::AccountId: From<[u8; 32]>, +{ + /// Get voting power for a hotkey on a subnet. + /// + /// Returns the EMA of stake for the hotkey, which represents its voting power. + /// Returns 0 if: + /// - The hotkey has no voting power entry + /// - Voting power tracking is not enabled for the subnet + /// - The hotkey is not registered on the subnet + /// + /// # Arguments + /// * `netuid` - The subnet identifier (u16) + /// * `hotkey` - The hotkey account ID (bytes32) + /// + /// # Returns + /// * `u256` - The voting power value (in RAO, same precision as stake) + #[precompile::public("getVotingPower(uint16,bytes32)")] + #[precompile::view] + fn get_voting_power( + _: &mut impl PrecompileHandle, + netuid: u16, + hotkey: H256, + ) -> EvmResult { + let hotkey = R::AccountId::from(hotkey.0); + let voting_power = pallet_subtensor::VotingPower::::get(NetUid::from(netuid), &hotkey); + Ok(U256::from(voting_power)) + } + + /// Check if voting power tracking is enabled for a subnet. + /// + /// # Arguments + /// * `netuid` - The subnet identifier (u16) + /// + /// # Returns + /// * `bool` - True if voting power tracking is enabled + #[precompile::public("isVotingPowerTrackingEnabled(uint16)")] + #[precompile::view] + fn is_voting_power_tracking_enabled( + _: &mut impl PrecompileHandle, + netuid: u16, + ) -> EvmResult { + Ok(pallet_subtensor::VotingPowerTrackingEnabled::::get( + NetUid::from(netuid), + )) + } + + /// Get the block at which voting power tracking will be disabled. + /// + /// Returns 0 if not scheduled for disabling. + /// When non-zero, tracking continues until this block, then stops. + /// + /// # Arguments + /// * `netuid` - The subnet identifier (u16) + /// + /// # Returns + /// * `u64` - The block number at which tracking will be disabled (0 if not scheduled) + #[precompile::public("getVotingPowerDisableAtBlock(uint16)")] + #[precompile::view] + fn get_voting_power_disable_at_block( + _: &mut impl PrecompileHandle, + netuid: u16, + ) -> EvmResult { + Ok(pallet_subtensor::VotingPowerDisableAtBlock::::get( + NetUid::from(netuid), + )) + } + + /// Get the EMA alpha value for voting power calculation on a subnet. + /// + /// Alpha is stored with 18 decimal precision (1.0 = 10^18). + /// Higher alpha = faster response to stake changes. + /// + /// # Arguments + /// * `netuid` - The subnet identifier (u16) + /// + /// # Returns + /// * `u64` - The alpha value (with 18 decimal precision) + #[precompile::public("getVotingPowerEmaAlpha(uint16)")] + #[precompile::view] + fn get_voting_power_ema_alpha(_: &mut impl PrecompileHandle, netuid: u16) -> EvmResult { + Ok(pallet_subtensor::VotingPowerEmaAlpha::::get( + NetUid::from(netuid), + )) + } + + /// Get total voting power for a subnet. + /// + /// Returns the sum of all voting power for all validators on the subnet. + /// Useful for calculating voting thresholds (e.g., 51% quorum). + /// + /// # Arguments + /// * `netuid` - The subnet identifier (u16) + /// + /// # Returns + /// * `u256` - The total voting power across all validators + #[precompile::public("getTotalVotingPower(uint16)")] + #[precompile::view] + fn get_total_voting_power(_: &mut impl PrecompileHandle, netuid: u16) -> EvmResult { + let total: u64 = pallet_subtensor::VotingPower::::iter_prefix(NetUid::from(netuid)) + .map(|(_, voting_power)| voting_power) + .fold(0u64, |acc, vp| acc.saturating_add(vp)); + Ok(U256::from(total)) + } +} diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index b3aced2160..95564977cf 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -21,6 +21,7 @@ subtensor-custom-rpc-runtime-api.workspace = true smallvec.workspace = true log.workspace = true codec = { workspace = true, features = ["derive"] } +safe-math.workspace = true scale-info = { workspace = true, features = ["derive"] } serde_json = { workspace = true, features = ["alloc"] } pallet-aura = { workspace = true } @@ -158,6 +159,7 @@ ethereum.workspace = true frame-metadata.workspace = true sp-io.workspace = true sp-tracing.workspace = true +precompile-utils = { workspace = true, features = ["testing"] } [build-dependencies] substrate-wasm-builder = { workspace = true, optional = true } @@ -198,6 +200,7 @@ std = [ "pallet-preimage/std", "pallet-commitments/std", "precompile-utils/std", + "safe-math/std", "sp-api/std", "sp-block-builder/std", "sp-core/std", diff --git a/runtime/src/base_call_filter.rs b/runtime/src/base_call_filter.rs new file mode 100644 index 0000000000..8d76c422c1 --- /dev/null +++ b/runtime/src/base_call_filter.rs @@ -0,0 +1,96 @@ +use crate::RuntimeCall; +use crate::Vec; +use crate::pallet_proxy; +use crate::pallet_utility; +use frame_support::traits::Contains; +use sp_std::boxed::Box; +use sp_std::vec; +pub struct NoNestingCallFilter; + +impl Contains for NoNestingCallFilter { + fn contains(call: &RuntimeCall) -> bool { + let calls = match call { + RuntimeCall::Utility(inner) => { + let calls = match inner { + pallet_utility::Call::force_batch { calls } => calls, + pallet_utility::Call::batch { calls } => calls, + pallet_utility::Call::batch_all { calls } => calls, + _ => return true, + }; + + calls + .iter() + .map(|call| Box::new(call.clone())) + .collect::>() + } + RuntimeCall::Proxy(inner) => { + let call = match inner { + pallet_proxy::Call::proxy { call, .. } => call, + pallet_proxy::Call::proxy_announced { call, .. } => call, + _ => return true, + }; + + vec![call.clone()] + } + RuntimeCall::Multisig(inner) => { + let call = match inner { + pallet_multisig::Call::as_multi { call, .. } => call, + pallet_multisig::Call::as_multi_threshold_1 { call, .. } => call, + _ => return true, + }; + + vec![call.clone()] + } + RuntimeCall::Crowdloan(inner) => { + let call = match inner { + pallet_crowdloan::Call::create { + call: Some(call), .. + } => call, + _ => return true, + }; + + vec![call.clone()] + } + RuntimeCall::Scheduler(inner) => { + let call = match inner { + pallet_scheduler::Call::schedule { call, .. } => call, + pallet_scheduler::Call::schedule_after { call, .. } => call, + pallet_scheduler::Call::schedule_named { call, .. } => call, + pallet_scheduler::Call::schedule_named_after { call, .. } => call, + _ => return true, + }; + + vec![call.clone()] + } + _ => return true, + }; + + !calls.iter().any(|call| { + matches!(&**call, RuntimeCall::Utility(inner) if matches!(inner, pallet_utility::Call::force_batch { .. } | pallet_utility::Call::batch_all { .. } | pallet_utility::Call::batch { .. })) || + matches!(&**call, RuntimeCall::Proxy(inner) if matches!(inner, pallet_proxy::Call::proxy { .. } | pallet_proxy::Call::proxy_announced { .. })) || + matches!(&**call, RuntimeCall::Multisig(inner) if matches!(inner, pallet_multisig::Call::as_multi { .. } | pallet_multisig::Call::as_multi_threshold_1 { .. })) || + matches!(&**call, RuntimeCall::Crowdloan(inner) if matches!(inner, pallet_crowdloan::Call::create { .. } )) || + matches!(&**call, RuntimeCall::Scheduler(inner) if matches!(inner, pallet_scheduler::Call::schedule {..} | pallet_scheduler::Call::schedule_after { .. } | pallet_scheduler::Call::schedule_named {.. } | pallet_scheduler::Call::schedule_named_after { .. } )) || + matches!(&**call, RuntimeCall::Sudo(inner) if matches!(inner, pallet_sudo::Call::sudo {..} | pallet_sudo::Call::sudo_as { .. } | pallet_sudo::Call::sudo_unchecked_weight { .. } )) + }) + } +} + +pub struct SafeModeWhitelistedCalls; +impl Contains for SafeModeWhitelistedCalls { + fn contains(call: &RuntimeCall) -> bool { + matches!( + call, + RuntimeCall::Sudo(_) + | RuntimeCall::Multisig(_) + | RuntimeCall::System(_) + | RuntimeCall::SafeMode(_) + | RuntimeCall::Timestamp(_) + | RuntimeCall::SubtensorModule( + pallet_subtensor::Call::set_weights { .. } + | pallet_subtensor::Call::serve_axon { .. } + ) + | RuntimeCall::Commitments(pallet_commitments::Call::set_commitment { .. }) + ) + } +} diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 919468c1ef..460a84e049 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -10,6 +10,7 @@ include!(concat!(env!("OUT_DIR"), "/wasm_binary.rs")); use core::num::NonZeroU64; +mod base_call_filter; pub mod check_nonce; mod migrations; pub mod sudo_wrapper; @@ -42,9 +43,10 @@ use pallet_subtensor::rpc_info::{ }; use pallet_subtensor::{CommitmentsInterface, ProxyInterface}; use pallet_subtensor_proxy as pallet_proxy; -use pallet_subtensor_swap_runtime_api::SimSwapResult; +use pallet_subtensor_swap_runtime_api::{SimSwapResult, SubnetPrice}; use pallet_subtensor_utility as pallet_utility; use runtime_common::prod_or_fast; +use safe_math::FixedExt; use sp_api::impl_runtime_apis; use sp_consensus_aura::sr25519::AuthorityId as AuraId; use sp_consensus_babe::BabeConfiguration; @@ -70,11 +72,15 @@ use sp_std::prelude::*; #[cfg(feature = "std")] use sp_version::NativeVersion; use sp_version::RuntimeVersion; +use substrate_fixed::types::U64F64; use subtensor_precompiles::Precompiles; -use subtensor_runtime_common::{AlphaCurrency, TaoCurrency, time::*, *}; +use subtensor_runtime_common::{AlphaCurrency, AuthorshipInfo, TaoCurrency, time::*, *}; use subtensor_swap_interface::{Order, SwapHandler}; // A few exports that help ease life for downstream crates. +use crate::base_call_filter::NoNestingCallFilter; +use crate::base_call_filter::SafeModeWhitelistedCalls; +use core::marker::PhantomData; pub use frame_support::{ StorageValue, construct_runtime, parameter_types, traits::{ @@ -94,15 +100,12 @@ pub use pallet_balances::Call as BalancesCall; use pallet_commitments::GetCommitments; pub use pallet_timestamp::Call as TimestampCall; use pallet_transaction_payment::{ConstFeeMultiplier, Multiplier}; +use scale_info::TypeInfo; #[cfg(any(feature = "std", test))] pub use sp_runtime::BuildStorage; pub use sp_runtime::{Perbill, Permill}; use subtensor_transaction_fee::{SubtensorTxFeeHandler, TransactionFeeHandler}; -use core::marker::PhantomData; - -use scale_info::TypeInfo; - // Frontier use fp_rpc::TransactionStatus; use pallet_ethereum::{Call::transact, PostLogContent, Transaction as EthereumTransaction}; @@ -177,8 +180,7 @@ impl frame_system::offchain::CreateSignedTransaction pallet_transaction_payment::ChargeTransactionPayment::::from(0), ), SudoTransactionExtension::::new(), - pallet_subtensor::transaction_extension::SubtensorTransactionExtension::::new( - ), + pallet_subtensor::SubtensorTransactionExtension::::new(), pallet_drand::drand_priority::DrandPriority::::new(), frame_metadata_hash_extension::CheckMetadataHash::::new(true), ); @@ -241,7 +243,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { // `spec_version`, and `authoring_version` are the same between Wasm and native. // This value is set to 100 to notify Polkadot-JS App (https://polkadot.js.org/apps) to use // the compatible custom types. - spec_version: 377, + spec_version: 380, impl_version: 1, apis: RUNTIME_API_VERSIONS, transaction_version: 1, @@ -276,28 +278,6 @@ parameter_types! { pub const SS58Prefix: u8 = 42; } -pub struct NoNestingCallFilter; - -impl Contains for NoNestingCallFilter { - fn contains(call: &RuntimeCall) -> bool { - match call { - RuntimeCall::Utility(inner) => { - let calls = match inner { - pallet_utility::Call::force_batch { calls } => calls, - pallet_utility::Call::batch { calls } => calls, - pallet_utility::Call::batch_all { calls } => calls, - _ => &Vec::new(), - }; - - !calls.iter().any(|call| { - matches!(call, RuntimeCall::Utility(inner) if matches!(inner, pallet_utility::Call::force_batch { .. } | pallet_utility::Call::batch_all { .. } | pallet_utility::Call::batch { .. })) - }) - } - _ => true, - } - } -} - // Configure FRAME pallets to include in runtime. impl frame_system::Config for Runtime { @@ -431,25 +411,6 @@ parameter_types! { pub const DisallowPermissionlessRelease: Option = None; } -pub struct SafeModeWhitelistedCalls; -impl Contains for SafeModeWhitelistedCalls { - fn contains(call: &RuntimeCall) -> bool { - matches!( - call, - RuntimeCall::Sudo(_) - | RuntimeCall::Multisig(_) - | RuntimeCall::System(_) - | RuntimeCall::SafeMode(_) - | RuntimeCall::Timestamp(_) - | RuntimeCall::SubtensorModule( - pallet_subtensor::Call::set_weights { .. } - | pallet_subtensor::Call::serve_axon { .. } - ) - | RuntimeCall::Commitments(pallet_commitments::Call::set_commitment { .. }) - ) - } -} - impl pallet_safe_mode::Config for Runtime { type RuntimeEvent = RuntimeEvent; type Currency = Balances; @@ -491,6 +452,34 @@ impl pallet_balances::Config for Runtime { type DoneSlashHandler = (); } +// Implement AuthorshipInfo trait for Runtime to satisfy pallet transaction +// fee OnUnbalanced trait bounds +pub struct BlockAuthorFromAura(core::marker::PhantomData); + +impl> BlockAuthorFromAura { + pub fn get_block_author() -> Option { + let binding = frame_system::Pallet::::digest(); + let digest_logs = binding.logs(); + let author_index = F::find_author(digest_logs.iter().filter_map(|d| d.as_pre_runtime()))?; + let authority_id = pallet_aura::Authorities::::get() + .get(author_index as usize)? + .clone(); + Some(AccountId32::new(authority_id.to_raw_vec().try_into().ok()?)) + } +} + +impl AuthorshipInfo for Runtime { + fn author() -> Option { + BlockAuthorFromAura::::get_block_author() + } +} + +impl> AuthorshipInfo for BlockAuthorFromAura { + fn author() -> Option { + Self::get_block_author() + } +} + parameter_types! { pub const OperationalFeeMultiplier: u8 = 5; pub FeeMultiplier: Multiplier = Multiplier::one(); @@ -1001,7 +990,7 @@ parameter_types! { pub const SubtensorInitialRho: u16 = 10; pub const SubtensorInitialAlphaSigmoidSteepness: i16 = 1000; pub const SubtensorInitialKappa: u16 = 32_767; // 0.5 = 65535/2 - pub const SubtensorInitialMaxAllowedUids: u16 = 4096; + pub const SubtensorInitialMaxAllowedUids: u16 = 256; pub const SubtensorInitialIssuance: u64 = 0; pub const SubtensorInitialMinAllowedWeights: u16 = 1024; pub const SubtensorInitialEmissionValue: u16 = 0; @@ -1043,7 +1032,6 @@ parameter_types! { pub const SubtensorInitialMinAllowedUids: u16 = 64; pub const SubtensorInitialMinLockCost: u64 = 1_000_000_000_000; // 1000 TAO pub const SubtensorInitialSubnetOwnerCut: u16 = 11_796; // 18 percent - // pub const SubtensorInitialSubnetLimit: u16 = 12; // (DEPRECATED) pub const SubtensorInitialNetworkLockReductionInterval: u64 = 14 * 7200; pub const SubtensorInitialNetworkRateLimit: u64 = 7200; pub const SubtensorInitialKeySwapCost: u64 = 100_000_000; // 0.1 TAO @@ -1051,9 +1039,8 @@ parameter_types! { pub const InitialAlphaLow: u16 = 45875; // Represents 0.7 as per the production default pub const InitialLiquidAlphaOn: bool = false; // Default value for LiquidAlphaOn pub const InitialYuma3On: bool = false; // Default value for Yuma3On - // pub const SubtensorInitialNetworkMaxStake: u64 = u64::MAX; // (DEPRECATED) - pub const InitialColdkeySwapScheduleDuration: BlockNumber = 5 * 24 * 60 * 60 / 12; // 5 days - pub const InitialColdkeySwapRescheduleDuration: BlockNumber = 24 * 60 * 60 / 12; // 1 day + pub const InitialColdkeySwapAnnouncementDelay: BlockNumber = prod_or_fast!(5 * 24 * 60 * 60 / 12, 50); // 5 days + pub const InitialColdkeySwapReannouncementDelay: BlockNumber = prod_or_fast!(24 * 60 * 60 / 12, 10); // 1 day pub const InitialDissolveNetworkScheduleDuration: BlockNumber = 5 * 24 * 60 * 60 / 12; // 5 days pub const SubtensorInitialTaoWeight: u64 = 971_718_665_099_567_868; // 0.05267697438728329% tao weight. pub const InitialEmaPriceHalvingPeriod: u64 = 201_600_u64; // 4 weeks @@ -1124,8 +1111,8 @@ impl pallet_subtensor::Config for Runtime { type Yuma3On = InitialYuma3On; type InitialTaoWeight = SubtensorInitialTaoWeight; type Preimages = Preimage; - type InitialColdkeySwapScheduleDuration = InitialColdkeySwapScheduleDuration; - type InitialColdkeySwapRescheduleDuration = InitialColdkeySwapRescheduleDuration; + type InitialColdkeySwapAnnouncementDelay = InitialColdkeySwapAnnouncementDelay; + type InitialColdkeySwapReannouncementDelay = InitialColdkeySwapReannouncementDelay; type InitialDissolveNetworkScheduleDuration = InitialDissolveNetworkScheduleDuration; type InitialEmaPriceHalvingPeriod = InitialEmaPriceHalvingPeriod; type InitialStartCallDelay = InitialStartCallDelay; @@ -1138,12 +1125,12 @@ impl pallet_subtensor::Config for Runtime { type MaxImmuneUidsPercentage = MaxImmuneUidsPercentage; type CommitmentsInterface = CommitmentsI; type EvmKeyAssociateRateLimit = EvmKeyAssociateRateLimit; + type AuthorshipProvider = BlockAuthorFromAura; } parameter_types! { pub const SwapProtocolId: PalletId = PalletId(*b"ten/swap"); pub const SwapMaxFeeRate: u16 = 10000; // 15.26% - pub const SwapMaxPositions: u32 = 100; pub const SwapMinimumLiquidity: u64 = 1_000; pub const SwapMinimumReserve: NonZeroU64 = unsafe { NonZeroU64::new_unchecked(1_000_000) }; } @@ -1155,7 +1142,6 @@ impl pallet_subtensor_swap::Config for Runtime { type TaoReserve = pallet_subtensor::TaoCurrencyReserve; type AlphaReserve = pallet_subtensor::AlphaCurrencyReserve; type MaxFeeRate = SwapMaxFeeRate; - type MaxPositions = SwapMaxPositions; type MinimumLiquidity = SwapMinimumLiquidity; type MinimumReserve = SwapMinimumReserve; // TODO: set measured weights when the pallet been benchmarked and the type is generated @@ -1618,7 +1604,7 @@ pub type TransactionExtensions = ( frame_system::CheckWeight, ChargeTransactionPaymentWrapper, SudoTransactionExtension, - pallet_subtensor::transaction_extension::SubtensorTransactionExtension, + pallet_subtensor::SubtensorTransactionExtension, pallet_drand::drand_priority::DrandPriority, frame_metadata_hash_extension::CheckMetadataHash, ); @@ -2500,15 +2486,28 @@ impl_runtime_apis! { impl pallet_subtensor_swap_runtime_api::SwapRuntimeApi for Runtime { fn current_alpha_price(netuid: NetUid) -> u64 { - use substrate_fixed::types::U96F32; - pallet_subtensor_swap::Pallet::::current_price(netuid.into()) - .saturating_mul(U96F32::from_num(1_000_000_000)) + .saturating_mul(U64F64::from_num(1_000_000_000)) .saturating_to_num() } + fn current_alpha_price_all() -> Vec { + pallet_subtensor::Pallet::::get_all_subnet_netuids() + .into_iter() + .map(|netuid| { + SubnetPrice { + netuid, + price: Self::current_alpha_price(netuid), + } + }) + .collect() + } + fn sim_swap_tao_for_alpha(netuid: NetUid, tao: TaoCurrency) -> SimSwapResult { + let price = pallet_subtensor_swap::Pallet::::current_price(netuid.into()); + let no_slippage_alpha = U64F64::saturating_from_num(u64::from(tao)).safe_div(price).saturating_to_num::(); let order = pallet_subtensor::GetAlphaForTao::::with_amount(tao); + // fee_to_block_author is included in sr.fee_paid, so it is absent in this calculation pallet_subtensor_swap::Pallet::::sim_swap( netuid.into(), order, @@ -2519,18 +2518,25 @@ impl_runtime_apis! { alpha_amount: 0.into(), tao_fee: 0.into(), alpha_fee: 0.into(), + tao_slippage: 0.into(), + alpha_slippage: 0.into(), }, |sr| SimSwapResult { tao_amount: sr.amount_paid_in.into(), alpha_amount: sr.amount_paid_out.into(), tao_fee: sr.fee_paid.into(), alpha_fee: 0.into(), + tao_slippage: 0.into(), + alpha_slippage: no_slippage_alpha.saturating_sub(sr.amount_paid_out.into()).into(), }, ) } fn sim_swap_alpha_for_tao(netuid: NetUid, alpha: AlphaCurrency) -> SimSwapResult { + let price = pallet_subtensor_swap::Pallet::::current_price(netuid.into()); + let no_slippage_tao = U64F64::saturating_from_num(u64::from(alpha)).saturating_mul(price).saturating_to_num::(); let order = pallet_subtensor::GetTaoForAlpha::::with_amount(alpha); + // fee_to_block_author is included in sr.fee_paid, so it is absent in this calculation pallet_subtensor_swap::Pallet::::sim_swap( netuid.into(), order, @@ -2541,12 +2547,16 @@ impl_runtime_apis! { alpha_amount: 0.into(), tao_fee: 0.into(), alpha_fee: 0.into(), + tao_slippage: 0.into(), + alpha_slippage: 0.into(), }, |sr| SimSwapResult { tao_amount: sr.amount_paid_out.into(), alpha_amount: sr.amount_paid_in.into(), tao_fee: 0.into(), alpha_fee: sr.fee_paid.into(), + tao_slippage: no_slippage_tao.saturating_sub(sr.amount_paid_out.into()).into(), + alpha_slippage: 0.into(), }, ) } diff --git a/runtime/tests/precompiles.rs b/runtime/tests/precompiles.rs new file mode 100644 index 0000000000..5565aa8f7f --- /dev/null +++ b/runtime/tests/precompiles.rs @@ -0,0 +1,219 @@ +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use core::iter::IntoIterator; +use std::collections::BTreeSet; + +use fp_evm::{Context, ExitError, PrecompileFailure, PrecompileResult}; +use node_subtensor_runtime::{BuildStorage, Runtime, RuntimeGenesisConfig, System}; +use pallet_evm::{AddressMapping, BalanceConverter, PrecompileSet}; +use precompile_utils::testing::{MockHandle, PrecompileTesterExt}; +use sp_core::{H160, H256, U256}; +use sp_runtime::traits::Hash; +use subtensor_precompiles::{ + AddressMappingPrecompile, BalanceTransferPrecompile, PrecompileExt, Precompiles, +}; + +type AccountId = ::AccountId; + +fn new_test_ext() -> sp_io::TestExternalities { + let mut ext: sp_io::TestExternalities = RuntimeGenesisConfig::default() + .build_storage() + .unwrap() + .into(); + ext.execute_with(|| System::set_block_number(1)); + ext +} + +fn execute_precompile( + precompiles: &Precompiles, + precompile_address: H160, + caller: H160, + input: Vec, + apparent_value: U256, +) -> Option { + let mut handle = MockHandle::new( + precompile_address, + Context { + address: precompile_address, + caller, + apparent_value, + }, + ); + handle.input = input; + precompiles.execute(&mut handle) +} + +fn evm_apparent_value_from_substrate(amount: u64) -> U256 { + ::BalanceConverter::into_evm_balance(amount.into()) + .expect("runtime balance conversion should work for test amount") + .into() +} + +fn addr_from_index(index: u64) -> H160 { + H160::from_low_u64_be(index) +} + +#[test] +fn precompile_registry_addresses_are_unique() { + new_test_ext().execute_with(|| { + let addresses = Precompiles::::used_addresses(); + let unique: BTreeSet<_> = IntoIterator::into_iter(addresses).collect(); + assert_eq!(unique.len(), addresses.len()); + }); +} + +mod address_mapping { + use super::*; + + fn address_mapping_call_data(target: H160) -> Vec { + // Solidity selector for addressMapping(address). + let selector = sp_io::hashing::keccak_256(b"addressMapping(address)"); + let mut input = Vec::with_capacity(4 + 32); + // First 4 bytes of keccak256(function_signature): ABI function selector. + input.extend_from_slice(&selector[..4]); + // Left-pad the 20-byte address argument to a 32-byte ABI word. + input.extend_from_slice(&[0u8; 12]); + // The 20-byte address payload (right-aligned in the 32-byte ABI word). + input.extend_from_slice(target.as_bytes()); + input + } + + #[test] + fn address_mapping_precompile_returns_runtime_address_mapping() { + new_test_ext().execute_with(|| { + let precompiles = Precompiles::::new(); + + let caller = addr_from_index(1); + let target_address = addr_from_index(0x1234); + let input = address_mapping_call_data(target_address); + + let mapped_account = + ::AddressMapping::into_account_id(target_address); + let expected_output: [u8; 32] = mapped_account.into(); + + let precompile_addr = addr_from_index(AddressMappingPrecompile::::INDEX); + precompiles + .prepare_test(caller, precompile_addr, input) + .with_static_call(true) + .execute_returns_raw(expected_output.to_vec()); + }); + } +} + +mod balance_transfer { + use super::*; + + fn balance_transfer_call_data(target: H256) -> Vec { + // Solidity selector for transfer(bytes32). + let selector = sp_io::hashing::keccak_256(b"transfer(bytes32)"); + let mut input = Vec::with_capacity(4 + 32); + input.extend_from_slice(&selector[..4]); + input.extend_from_slice(target.as_bytes()); + input + } + + #[test] + fn balance_transfer_precompile_transfers_balance() { + new_test_ext().execute_with(|| { + let precompiles = Precompiles::::new(); + let precompile_addr = addr_from_index(BalanceTransferPrecompile::::INDEX); + let dispatch_account: AccountId = BalanceTransferPrecompile::::account_id(); + let destination_raw = H256::repeat_byte(7); + let destination_account: AccountId = destination_raw.0.into(); + + let amount = 123_456; + pallet_subtensor::Pallet::::add_balance_to_coldkey_account( + &dispatch_account, + (amount * 2).into(), + ); + + let source_balance_before = + pallet_balances::Pallet::::free_balance(&dispatch_account); + let destination_balance_before = + pallet_balances::Pallet::::free_balance(&destination_account); + + let result = execute_precompile( + &precompiles, + precompile_addr, + addr_from_index(1), + balance_transfer_call_data(destination_raw), + evm_apparent_value_from_substrate(amount), + ); + let precompile_result = + result.expect("expected precompile transfer call to be routed to a precompile"); + precompile_result.expect("expected successful precompile transfer dispatch"); + + let source_balance_after = + pallet_balances::Pallet::::free_balance(&dispatch_account); + let destination_balance_after = + pallet_balances::Pallet::::free_balance(&destination_account); + + assert_eq!(source_balance_after, source_balance_before - amount); + assert_eq!( + destination_balance_after, + destination_balance_before + amount + ); + }); + } + + #[test] + fn balance_transfer_precompile_respects_subtensor_extension_policy() { + new_test_ext().execute_with(|| { + let precompiles = Precompiles::::new(); + let precompile_addr = addr_from_index(BalanceTransferPrecompile::::INDEX); + let dispatch_account: AccountId = BalanceTransferPrecompile::::account_id(); + let destination_raw = H256::repeat_byte(8); + let destination_account: AccountId = destination_raw.0.into(); + + let amount = 100; + pallet_subtensor::Pallet::::add_balance_to_coldkey_account( + &dispatch_account, + 1_000_000_u64.into(), + ); + + // Activate coldkey-swap guard for precompile dispatch account. + let replacement_coldkey = AccountId::from([9u8; 32]); + let replacement_hash = + ::Hashing::hash_of(&replacement_coldkey); + pallet_subtensor::ColdkeySwapAnnouncements::::insert( + &dispatch_account, + (System::block_number(), replacement_hash), + ); + + let source_balance_before = + pallet_balances::Pallet::::free_balance(&dispatch_account); + let destination_balance_before = + pallet_balances::Pallet::::free_balance(&destination_account); + + let result = execute_precompile( + &precompiles, + precompile_addr, + addr_from_index(1), + balance_transfer_call_data(destination_raw), + evm_apparent_value_from_substrate(amount), + ); + let precompile_result = + result.expect("expected precompile transfer call to be routed to a precompile"); + let failure = precompile_result + .expect_err("expected transaction extension rejection on precompile dispatch"); + let message = match failure { + PrecompileFailure::Error { + exit_status: ExitError::Other(message), + } => message, + other => panic!("unexpected precompile failure: {other:?}"), + }; + assert!( + message.contains("transaction extension rejected"), + "unexpected precompile failure: {message}" + ); + + let source_balance_after = + pallet_balances::Pallet::::free_balance(&dispatch_account); + let destination_balance_after = + pallet_balances::Pallet::::free_balance(&destination_account); + assert_eq!(source_balance_after, source_balance_before); + assert_eq!(destination_balance_after, destination_balance_before); + }); + } +} diff --git a/scripts/update-deps-to-path.sh b/scripts/update-deps-to-path.sh new file mode 100755 index 0000000000..a1eab9b99c --- /dev/null +++ b/scripts/update-deps-to-path.sh @@ -0,0 +1,90 @@ +#!/bin/zsh +# update-deps-to-path.sh goal is to replace git dependencies with path dependencies +# in the target Cargo.toml file. +# +# The script will scan the source repos for packages and build a mapping of package name to path. +# It will then process the target Cargo.toml file line by line and replace git dependencies with path dependencies. +# +# The script will output the new Cargo.toml file to stdout. +# +# The script will exit with a non-zero status if the target Cargo.toml file is not found. +# Usage: ./scripts/update-deps-to-path.sh ./Cargo.toml ./polkadot-sdk-otf ../frontier-otf > Cargo.toml.new + +set -e + +TARGET_TOML="${1:?Usage: $0 [source-repo-2] ...}" +shift + +if [[ $# -eq 0 ]]; then + echo "Error: At least one source repo path required" >&2 + exit 1 +fi + +# Build package name -> path mapping from all source repos +typeset -A PKG_PATHS + +for SOURCE_PATH in "$@"; do + SOURCE_PATH="$(cd "$SOURCE_PATH" && pwd)" + echo "Scanning $SOURCE_PATH for packages..." >&2 + + for cargo_toml in $(find "$SOURCE_PATH" -name "Cargo.toml" -type f 2>/dev/null); do + pkg_name=$(yq -p toml -o yaml '.package.name // ""' "$cargo_toml" 2>/dev/null | tr -d '"') + + if [[ -n "$pkg_name" && "$pkg_name" != "null" ]]; then + pkg_dir="$(dirname "$cargo_toml")" + PKG_PATHS[$pkg_name]="$pkg_dir" + echo " Found: $pkg_name" >&2 + fi + done +done + +echo "Found ${#PKG_PATHS[@]} total packages" >&2 +echo "" >&2 + +# Process target Cargo.toml line by line +echo "Updating dependencies in $TARGET_TOML..." >&2 + +while IFS= read -r line; do + # Check if this line has a git dependency + if [[ "$line" =~ ^([a-zA-Z0-9_-]+|\"[^\"]+\")\ *=\ *\{.*git\ *=\ *\" ]]; then + # Extract package name (handle both quoted and unquoted) + dep_name=$(echo "$line" | sed -E 's/^"?([a-zA-Z0-9_-]+)"? *=.*/\1/') + + # Check for package alias + if [[ "$line" =~ package\ *=\ *\"([^\"]+)\" ]]; then + lookup_name="${match[1]}" + else + lookup_name="$dep_name" + fi + + # Check if we have this package + if [[ -n "${PKG_PATHS[$lookup_name]}" ]]; then + pkg_path="${PKG_PATHS[$lookup_name]}" + echo " $dep_name -> $pkg_path" >&2 + + # Extract features/default-features/package if present + extras="" + if [[ "$line" =~ default-features\ *=\ *false ]]; then + extras="$extras, default-features = false" + fi + if [[ "$line" =~ package\ *=\ *\"([^\"]+)\" ]]; then + extras="$extras, package = \"${match[1]}\"" + fi + if [[ "$line" =~ features\ *=\ *\[([^\]]*)\] ]]; then + extras="$extras, features = [${match[1]}]" + fi + + # Output new line with just path + echo "${dep_name} = { path = \"${pkg_path}\"${extras} }" + else + # Package not found in sources, keep original + echo "$line" + fi + else + # Not a git dependency, keep as-is + echo "$line" + fi +done < "$TARGET_TOML" + +echo "" >&2 +echo "Done!" >&2