diff --git a/smart-contracts/repos-v0/Cargo.lock b/smart-contracts/repos-v0/Cargo.lock new file mode 100644 index 000000000..0c45b61b7 --- /dev/null +++ b/smart-contracts/repos-v0/Cargo.lock @@ -0,0 +1,1693 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "Inflector" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" +dependencies = [ + "lazy_static", + "regex", +] + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "array-bytes" +version = "6.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d5dde061bd34119e902bbb2d9b90c5692635cf59fb91d582c2b68043f1b8293" + +[[package]] +name = "array-init" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d62b7694a562cdf5a74227903507c56ab2cc8bdd1f781ed5cb4cf9c9f810bfc" + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitvec" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddcec3d12c579d40898fe0a9a358a803c23e9c52ca3c425707f81c9436211837" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bounded-collections" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ad8a0bed7827f0b07a5d23cec2e58cc02038a99e4ca81616cb2bb2025f804d" +dependencies = [ + "log", + "parity-scale-codec", + "scale-info", + "serde", +] + +[[package]] +name = "byte-slice-cast" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7575182f7272186991736b70173b0ea045398f984bf5ebbb3804736ce1330c9d" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cc" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "common-path" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2382f75942f4b3be3690fe4f86365e9c853c1587d6ee58212cebf6e2a9ccd101" + +[[package]] +name = "const_env" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "103172f14a86f2e45b8eecae496605e58b347839c739e869ab918154ad7c312e" +dependencies = [ + "const_env_impl", +] + +[[package]] +name = "const_env_impl" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ff249495e37c4ae62e9cf56ec642f1a011804500cc1ab6f81268dc5bf907cfe" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "const_format" +version = "0.2.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4481a617ad9a412be3b97c5d403fef8ed023103368908b9c50af598ff467cc1e" +dependencies = [ + "const_format_proc_macros", + "konst", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "rand_core", + "typenum", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "darling" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 1.0.109", +] + +[[package]] +name = "darling_macro" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" +dependencies = [ + "darling_core", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive-syn-parse" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d65d7ce8132b7c0e54497a4d9a55a1c2a0912a0d786cf894472ba818fba45762" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "derive_more" +version = "0.99.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "derive_more" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "unicode-xid", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "docify" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a772b62b1837c8f060432ddcc10b17aae1453ef17617a99bc07789252d2a5896" +dependencies = [ + "docify_macros", +] + +[[package]] +name = "docify_macros" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60e6be249b0a462a14784a99b19bf35a667bb5e09de611738bb7362fa4c95ff7" +dependencies = [ + "common-path", + "derive-syn-parse", + "once_cell", + "proc-macro2", + "quote", + "regex", + "syn 2.0.119", + "termcolor", + "toml", + "walkdir", +] + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "either" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" + +[[package]] +name = "environmental" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e48c92028aaa870e83d51c64e5d4e0b6981b360c522198c23959f219a4e1b15b" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom_or_panic" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea1015b5a70616b688dc230cfe50c8af89d972cb132d5a622814d29773b10b9" +dependencies = [ + "rand", + "rand_core", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "impl-serde" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc88fc67028ae3db0c853baa36269d398d5f45b6982f95549ff5def78c935cd" +dependencies = [ + "serde", +] + +[[package]] +name = "impl-trait-for-tuples" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0eb5a3343abf848c0984fe4604b2b105da9539376e24fc0a3b0007411ae4fd9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "ink" +version = "5.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15d7438a13d38fa8f4eebea8d1e7c2931058eafd0336c79f4141d4ed0162a412" +dependencies = [ + "derive_more 1.0.0", + "ink_env", + "ink_macro", + "ink_metadata", + "ink_prelude", + "ink_primitives", + "ink_storage", + "pallet-contracts-uapi", + "parity-scale-codec", + "scale-info", + "staging-xcm", +] + +[[package]] +name = "ink_allocator" +version = "5.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ec348ce75d284bc2e698187dc01da416a52dfa2d685e2a57d04e9e580447df0" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "ink_codegen" +version = "5.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2238f147295746f1fee4cf7dcdee6378c94e61fbf7b9f9f4bb2a7f918530801b" +dependencies = [ + "blake2", + "derive_more 1.0.0", + "either", + "heck", + "impl-serde", + "ink_ir", + "ink_primitives", + "itertools", + "parity-scale-codec", + "proc-macro2", + "quote", + "serde", + "serde_json", + "syn 2.0.119", +] + +[[package]] +name = "ink_engine" +version = "5.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273f2aa983d04a6476d3c5ac76ddbef07555664b88f923996e7465e261dda48" +dependencies = [ + "blake2", + "derive_more 1.0.0", + "ink_primitives", + "pallet-contracts-uapi", + "parity-scale-codec", + "secp256k1", + "sha2", + "sha3", +] + +[[package]] +name = "ink_env" +version = "5.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9ee6089a1836c2e92d00be97d42308b7fb2c2b51ff6150b1130166a58107092" +dependencies = [ + "blake2", + "cfg-if", + "const_env", + "derive_more 1.0.0", + "ink_allocator", + "ink_engine", + "ink_prelude", + "ink_primitives", + "ink_storage_traits", + "num-traits", + "pallet-contracts-uapi", + "parity-scale-codec", + "paste", + "rlibc", + "scale-decode", + "scale-encode", + "scale-info", + "schnorrkel", + "secp256k1", + "sha2", + "sha3", + "staging-xcm", + "static_assertions", +] + +[[package]] +name = "ink_ir" +version = "5.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e201688fb27ff97496a4231a393dd4befcc5a9c092d6bf231f0f5d409ef44f34" +dependencies = [ + "blake2", + "either", + "impl-serde", + "ink_prelude", + "itertools", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "ink_macro" +version = "5.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce9465553d3066a8e28bd94a94880289084c4ff12f1852312553e902fa1ffdd" +dependencies = [ + "ink_codegen", + "ink_ir", + "ink_primitives", + "parity-scale-codec", + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "ink_metadata" +version = "5.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27135c651274087ba0578d2c07866c31d8dd481ae8de5bb7295fe3931491aa80" +dependencies = [ + "derive_more 1.0.0", + "impl-serde", + "ink_prelude", + "ink_primitives", + "linkme", + "parity-scale-codec", + "scale-info", + "schemars", + "serde", +] + +[[package]] +name = "ink_prelude" +version = "5.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b29c9b7f686f4305f523bca5e2ae6f22a09531ec2bf0a9498cdc877959f70ad0" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "ink_primitives" +version = "5.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a530c1b352a53176ea718f3a65f15003e54e0474ec12353ea0e0e5bb60b25741" +dependencies = [ + "derive_more 1.0.0", + "ink_prelude", + "parity-scale-codec", + "scale-decode", + "scale-encode", + "scale-info", + "xxhash-rust", +] + +[[package]] +name = "ink_storage" +version = "5.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bed602a974481b194084b93957917f27e724c7561fd0de9b6f3c171590c839b" +dependencies = [ + "array-init", + "cfg-if", + "derive_more 1.0.0", + "ink_env", + "ink_metadata", + "ink_prelude", + "ink_primitives", + "ink_storage_traits", + "pallet-contracts-uapi", + "parity-scale-codec", + "scale-info", +] + +[[package]] +name = "ink_storage_traits" +version = "5.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fde9b3f4a1e355682e5d13fd5639e5da4d0a2029537292e05a4255ea1169663e" +dependencies = [ + "ink_metadata", + "ink_prelude", + "ink_primitives", + "parity-scale-codec", + "scale-info", +] + +[[package]] +name = "integer-sqrt" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "276ec31bcb4a9ee45f58bec6f9ec700ae4cf4f4f8f2fa7e06cb406bd5ffdd770" +dependencies = [ + "num-traits", +] + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "keccak" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" +dependencies = [ + "cpufeatures", +] + +[[package]] +name = "konst" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "128133ed7824fcd73d6e7b17957c5eb7bacb885649bd8c69708b2331a10bcefb" +dependencies = [ + "konst_macro_rules", +] + +[[package]] +name = "konst_macro_rules" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4933f3f57a8e9d9da04db23fb153356ecaf00cbd14aee46279c33dc80925c37" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "linkme" +version = "0.3.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3045e122bd98aef8ec3ad58ce84f0791f64e70163d1a02710af4aa11a4d54cc5" +dependencies = [ + "linkme-impl", +] + +[[package]] +name = "linkme-impl" +version = "0.3.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77060ebe535362c3da75682cd17b0431017b6e7c5661e714fc69a7ad017d1301" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "merlin" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58c38e2799fc0978b65dfff8023ec7843e2330bb462f19198840b34b6582397d" +dependencies = [ + "byteorder", + "keccak", + "rand_core", + "zeroize", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "pallet-contracts-uapi" +version = "9.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d7a51646d9ff1d91abd0186d2c074f0dfd3b1a2d55f08a229a2f2e4bc6d1e49" +dependencies = [ + "bitflags", + "paste", + "polkavm-derive", +] + +[[package]] +name = "parity-scale-codec" +version = "3.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799781ae679d79a948e13d4824a40970bfa500058d245760dd857301059810fa" +dependencies = [ + "arrayvec", + "bitvec", + "byte-slice-cast", + "const_format", + "impl-trait-for-tuples", + "parity-scale-codec-derive", + "rustversion", + "serde", +] + +[[package]] +name = "parity-scale-codec-derive" +version = "3.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34b4653168b563151153c9e4c08ebed57fb8262bebfa79711552fa983c623e7a" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "polkavm-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d9428a5cfcc85c5d7b9fc4b6a18c4b802d0173d768182a51cc7751640f08b92" + +[[package]] +name = "polkavm-derive" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae8c4bea6f3e11cd89bb18bcdddac10bd9a24015399bd1c485ad68a985a19606" +dependencies = [ + "polkavm-derive-impl-macro", +] + +[[package]] +name = "polkavm-derive-impl" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c4fdfc49717fb9a196e74a5d28e0bc764eb394a2c803eb11133a31ac996c60c" +dependencies = [ + "polkavm-common", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "polkavm-derive-impl-macro" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ba81f7b5faac81e528eb6158a6f3c9e0bb1008e0ffa19653bc8dea925ecb429" +dependencies = [ + "polkavm-derive-impl", + "syn 2.0.119", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.13+spec-1.1.0", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "repo_registry" +version = "0.1.0" +dependencies = [ + "ink", + "parity-scale-codec", + "scale-info", +] + +[[package]] +name = "rlibc" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc874b127765f014d792f16763a81245ab80500e2ad921ed4ee9e82481ee08fe" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scale-bits" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "662d10dcd57b1c2a3c41c9cf68f71fb09747ada1ea932ad961aca7e2ca28315f" +dependencies = [ + "parity-scale-codec", + "scale-type-resolver", +] + +[[package]] +name = "scale-decode" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afc79ba56a1c742f5aeeed1f1801f3edf51f7e818f0a54582cac6f131364ea7b" +dependencies = [ + "derive_more 0.99.20", + "parity-scale-codec", + "scale-bits", + "scale-decode-derive", + "scale-type-resolver", + "smallvec", +] + +[[package]] +name = "scale-decode-derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5398fdb3c7bea3cb419bac4983aadacae93fe1a7b5f693f4ebd98c3821aad7a5" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "scale-encode" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "628800925a33794fb5387781b883b5e14d130fece9af5a63613867b8de07c5c7" +dependencies = [ + "derive_more 0.99.20", + "parity-scale-codec", + "scale-encode-derive", + "scale-type-resolver", + "smallvec", +] + +[[package]] +name = "scale-encode-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a304e1af7cdfbe7a24e08b012721456cc8cecdedadc14b3d10513eada63233c" +dependencies = [ + "darling", + "proc-macro-crate 1.3.1", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "scale-info" +version = "2.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346a3b32eba2640d17a9cb5927056b08f3de90f65b72fe09402c2ad07d684d0b" +dependencies = [ + "bitvec", + "cfg-if", + "derive_more 1.0.0", + "parity-scale-codec", + "scale-info-derive", + "schemars", + "serde", +] + +[[package]] +name = "scale-info-derive" +version = "2.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6630024bf739e2179b91fb424b28898baf819414262c5d376677dbff1fe7ebf" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "scale-type-resolver" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10b800069bfd43374e0f96f653e0d46882a2cb16d6d961ac43bea80f26c76843" +dependencies = [ + "smallvec", +] + +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.119", +] + +[[package]] +name = "schnorrkel" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9fcb6c2e176e86ec703e22560d99d65a5ee9056ae45a08e13e84ebf796296f" +dependencies = [ + "aead", + "arrayref", + "arrayvec", + "curve25519-dalek", + "getrandom_or_panic", + "merlin", + "rand_core", + "serde_bytes", + "sha2", + "subtle", + "zeroize", +] + +[[package]] +name = "secp256k1" +version = "0.28.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24b59d129cdadea20aea4fb2352fa053712e5d713eee47d700cd4b2bc002f10" +dependencies = [ + "secp256k1-sys", +] + +[[package]] +name = "secp256k1-sys" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d1746aae42c19d583c3c1a8c646bfad910498e2051c551a7f2e3c0c9fbb7eb" +dependencies = [ + "cc", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_bytes" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha3" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77fd7028345d415a4034cf8777cd4f8ab1851274233b45f84e3d955502d93874" +dependencies = [ + "digest", + "keccak", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "sp-arithmetic" +version = "26.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9971b30935cea3858664965039dabd80f67aca74cc6cc6dd42ff1ab14547bc53" +dependencies = [ + "docify", + "integer-sqrt", + "num-traits", + "parity-scale-codec", + "scale-info", + "serde", + "static_assertions", +] + +[[package]] +name = "sp-debug-derive" +version = "14.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48d09fa0a5f7299fb81ee25ae3853d26200f7a348148aed6de76be905c007dbe" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "sp-weights" +version = "31.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "515aa194eabac059041df2dbee75b059b99981213ec680e9de85b45b6988346a" +dependencies = [ + "bounded-collections", + "parity-scale-codec", + "scale-info", + "serde", + "smallvec", + "sp-arithmetic", + "sp-debug-derive", +] + +[[package]] +name = "staging-xcm" +version = "11.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aded0292274ad473250c22ed3deaf2d9ed47d15786d700e9e83ab7c1cad2ad44" +dependencies = [ + "array-bytes", + "bounded-collections", + "derivative", + "environmental", + "impl-trait-for-tuples", + "log", + "parity-scale-codec", + "scale-info", + "serde", + "sp-weights", + "xcm-procedural", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime 0.6.11", + "toml_edit 0.22.27", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap", + "toml_datetime 0.6.11", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime 0.6.11", + "toml_write", + "winnow 0.7.15", +] + +[[package]] +name = "toml_edit" +version = "0.25.13+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" +dependencies = [ + "indexmap", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.4", +] + +[[package]] +name = "toml_parser" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow 1.0.4", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "xcm-procedural" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4717a97970a9cda70d7db53cf50d2615c2f6f6b7c857445325b4a39ea7aa2cd" +dependencies = [ + "Inflector", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "xxhash-rust" +version = "0.8.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aee1b19627c7c60102ab80d3a9cbe18de90bfe03bfa6c3715447681f0e8c8af6" + +[[package]] +name = "zerocopy" +version = "0.8.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a105cd7b140f6eeec8acff2ea38135d3cab283ada58540f629fe51e46696eb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fe976fb70c78cd64cccfe3a6fc142244e8a77b70959b30faf9d0ac37ee228eb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/smart-contracts/repos-v0/Cargo.toml b/smart-contracts/repos-v0/Cargo.toml new file mode 100644 index 000000000..d156f5178 --- /dev/null +++ b/smart-contracts/repos-v0/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "repo_registry" +version = "0.1.0" +authors = ["Gittensor Team"] +edition = "2021" + +[dependencies] +ink = { version = "5", default-features = false } +scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] } +scale-info = { version = "2", default-features = false, features = ["derive"], optional = true } + +[lib] +path = "lib.rs" + +[features] +default = ["std"] +std = [ + "ink/std", + "scale/std", + "scale-info/std", +] +ink-as-dependency = [] diff --git a/smart-contracts/repos-v0/errors.rs b/smart-contracts/repos-v0/errors.rs new file mode 100644 index 000000000..2acf8159e --- /dev/null +++ b/smart-contracts/repos-v0/errors.rs @@ -0,0 +1,79 @@ +use scale::{Decode, Encode}; + +/// Errors that can occur in the RepoRegistry contract +#[derive(Debug, PartialEq, Eq, Encode, Decode)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +pub enum Error { + /// Caller is not the contract owner + NotOwner, + /// Caller is not the repository owner + NotRepoOwner, + /// Caller is not a whitelisted validator hotkey + NotWhitelistedVoter, + /// Contract is paused + Paused, + /// Repository not found + RepoNotFound, + /// Repository exists but is not active + RepoNotActive, + /// Repository is already registered and active + AlreadyRegistered, + /// GitHub id must be non-zero + InvalidGithubId, + /// Full name must be lowercase "owner/repo" + InvalidFullName, + /// Zero address not allowed + ZeroAddress, + /// Registry full and every repo is inside its immunity window + NoPrunableSlot, + /// Per-block registration cap reached + TooManyRegistrationsThisBlock, + /// No bounds registered for this param key + UnknownParamKey, + /// Value outside the allowed bounds + ValueOutOfBounds, + /// Change rate limit not yet elapsed + RateLimited, + /// Label is empty, too long, or contains control characters + InvalidLabel, + /// Label multiplier cap reached + TooManyLabels, + /// Label multiplier not found + LabelNotFound, + /// Branch pattern fails validation + InvalidPattern, + /// Branch pattern cap reached + TooManyPatterns, + /// Duplicate branch pattern + DuplicatePattern, + /// Basket must not be empty + EmptyBasket, + /// Basket exceeds the basket cap + BasketTooLarge, + /// Duplicate repo id in basket + DuplicateBasketEntry, + /// Basket weights must be non-zero + ZeroWeight, + /// Basket weights must sum to 65535 + WeightSumMismatch, + /// Voter already whitelisted + VoterAlreadyAdded, + /// Voter not in whitelist + VoterNotFound, + /// Voter whitelist cap reached + TooManyVoters, + /// Bounds must satisfy min <= max + InvalidBounds, + /// Param bounds key cap reached + TooManyParamKeys, + /// Constants failed validation + InvalidConfig, + /// Fee stake transfer failed + FeeTransferFailed, + /// Fee stake delta shortfall (silent cap detected) + FeeShortfall, + /// Fee recycle failed + RecycleFailed, + /// set_code_hash failed + SetCodeFailed, +} diff --git a/smart-contracts/repos-v0/events.rs b/smart-contracts/repos-v0/events.rs new file mode 100644 index 000000000..8ef64e7ba --- /dev/null +++ b/smart-contracts/repos-v0/events.rs @@ -0,0 +1,137 @@ +use ink::prelude::string::String; +use ink::prelude::vec::Vec; +use ink::primitives::{AccountId, Hash}; + +/// A repository was registered (fee paid and recycled). +/// Consumed by the das-github-mirror reconciler: track + deep-backfill. +#[ink::event] +pub struct RepositoryRegistered { + #[ink(topic)] + pub github_id: u64, + pub full_name: String, + pub owner: AccountId, + pub fee_paid: u64, + pub reg_block: u64, +} + +/// A repository was removed by its owner (forced = team escape hatch). +/// Consumed by the das-github-mirror reconciler: untrack. +#[ink::event] +pub struct RepositoryDeregistered { + #[ink(topic)] + pub github_id: u64, + pub full_name: String, + pub forced: bool, +} + +/// A repository was pruned to free a slot for `replaced_by`. +/// Consumed by the das-github-mirror reconciler: untrack. +#[ink::event] +pub struct RepositoryPruned { + #[ink(topic)] + pub github_id: u64, + pub full_name: String, + pub replaced_by: u64, +} + +/// Repository renamed on GitHub; id is stable, metadata updated. +#[ink::event] +pub struct FullNameUpdated { + #[ink(topic)] + pub github_id: u64, + pub old_full_name: String, + pub new_full_name: String, +} + +#[ink::event] +pub struct RepoOwnershipTransferred { + #[ink(topic)] + pub github_id: u64, + pub old_owner: AccountId, + pub new_owner: AccountId, +} + +#[ink::event] +pub struct ParamSet { + #[ink(topic)] + pub github_id: u64, + pub key: u8, + pub value: u64, +} + +#[ink::event] +pub struct LabelMultiplierSet { + #[ink(topic)] + pub github_id: u64, + pub label: String, + pub value: u64, +} + +#[ink::event] +pub struct LabelMultiplierRemoved { + #[ink(topic)] + pub github_id: u64, + pub label: String, +} + +#[ink::event] +pub struct BranchPatternsSet { + #[ink(topic)] + pub github_id: u64, + pub patterns: Vec, +} + +#[ink::event] +pub struct BasketSet { + #[ink(topic)] + pub hotkey: AccountId, + pub entries: Vec<(u64, u16)>, +} + +#[ink::event] +pub struct BasketCleared { + #[ink(topic)] + pub hotkey: AccountId, +} + +#[ink::event] +pub struct VoterAdded { + #[ink(topic)] + pub hotkey: AccountId, +} + +#[ink::event] +pub struct VoterRemoved { + #[ink(topic)] + pub hotkey: AccountId, +} + +#[ink::event] +pub struct BoundsSet { + #[ink(topic)] + pub key: u8, + pub min: u64, + pub max: u64, +} + +#[ink::event] +pub struct ConfigUpdated {} + +#[ink::event] +pub struct PausedSet { + pub paused: bool, +} + +#[ink::event] +pub struct OwnerChanged { + #[ink(topic)] + pub old_owner: AccountId, + #[ink(topic)] + pub new_owner: AccountId, +} + +#[ink::event] +pub struct CodeHashSet { + #[ink(topic)] + pub code_hash: Hash, +} diff --git a/smart-contracts/repos-v0/lib.rs b/smart-contracts/repos-v0/lib.rs new file mode 100644 index 000000000..c59134fb4 --- /dev/null +++ b/smart-contracts/repos-v0/lib.rs @@ -0,0 +1,847 @@ +#![cfg_attr(not(feature = "std"), no_std, no_main)] + +mod errors; +mod events; +mod price; +mod types; + +pub use errors::Error; +pub use types::*; + +// ============================================================================ +// Chain Extension for Subtensor Staking Operations +// ============================================================================ + +/// Subtensor chain extension (function ids from subtensor +/// chain-extensions/src/types.rs `FunctionId`). The runtime dispatches on the +/// function id only, so the extension id matches issues-v0's convention. +#[ink::chain_extension(extension = 5001)] +pub trait SubtensorExtension { + type ErrorCode = ExtStatus; + + /// GetStakeInfoForHotkeyColdkeyNetuidV1: pure read, always Success status. + #[ink(function = 0, handle_status = false)] + fn get_stake_info(hotkey: [u8; 32], coldkey: [u8; 32], netuid: u16) + -> Option; + + /// RecycleAlphaV1: destroys alpha staked on (hotkey, contract coldkey). + /// Origin = the contract account. Returns the amount actually recycled. + #[ink(function = 16)] + fn recycle_alpha(hotkey: [u8; 32], netuid: u16, amount: u64) -> u64; + + /// CallerTransferStakeV1: dispatches transfer_stake as the ORIGINAL tx + /// signer, moving their alpha on `hotkey` to `destination_coldkey`. + #[ink(function = 25)] + fn caller_transfer_stake( + destination_coldkey: [u8; 32], + hotkey: [u8; 32], + origin_netuid: u16, + destination_netuid: u16, + amount: u64, + ); +} + +/// Custom environment with Subtensor chain extension. +#[derive(Debug, Clone, PartialEq, Eq)] +#[ink::scale_derive(TypeInfo)] +pub enum CustomEnvironment {} + +impl ink::env::Environment for CustomEnvironment { + const MAX_EVENT_TOPICS: usize = 4; + type AccountId = ink::primitives::AccountId; + type Balance = u128; + type Hash = ink::primitives::Hash; + type Timestamp = u64; + type BlockNumber = u32; + type ChainExtension = SubtensorExtension; +} + +#[ink::contract(env = crate::CustomEnvironment)] +mod repo_registry { + use crate::events::*; + use crate::price; + use crate::types::*; + use crate::Error; + use ink::prelude::string::String; + use ink::prelude::vec::Vec; + use ink::storage::Mapping; + + const ZERO_ACCOUNT: [u8; 32] = [0u8; 32]; + + /// Repository registry: whitelist, per-repo hyperparams, validator baskets. + /// + /// # Storage layout (python childstate reader contract) + /// + /// Root cell = SCALE-encoded packed fields in declaration order. Fixed + /// byte offsets (all ints little-endian): + /// + /// | offset | field | type | + /// |--------|----------------------|-----------| + /// | 0 | owner | AccountId (32) | + /// | 32 | paused | bool (1) | + /// | 33 | netuid | u16 (2) | + /// | 35 | storage_version | u32 (4) | + /// | 39 | price_last | u64 (8) | + /// | 47 | price_last_block | u64 (8) | + /// | 55 | last_reg_block | u64 (8) | + /// | 63 | regs_in_block | u32 (4) | + /// | 67 | constants | Constants (68: max_repos u32, immunity_period u64, price_floor u64, price_ceiling u64, price_half_life u64, price_bump_q32 u64, max_regs_per_block u32, param_rate_limit_blocks u64, snapshot_interval u64, basket_cap u32) | + /// | 135 | repo_ids | Vec (compact len + n*8) | + /// | var | voters | Vec (compact len + n*32) | + /// | var | bound_keys | Vec (compact len + n) | + /// + /// Mappings are lazy cells under ink AutoKey; their root keys come from + /// metadata.json (generated in the python client task). Reads are pure + /// storage — dry-run/childstate safe. + #[ink(storage)] + pub struct RepoRegistry { + owner: AccountId, + paused: bool, + netuid: u16, + storage_version: u32, + price_last: u64, + price_last_block: u64, + last_reg_block: u64, + regs_in_block: u32, + constants: Constants, + /// Active repo ids, bounded by constants.max_repos (<= MAX_REPOS_HARD) + repo_ids: Vec, + /// Team whitelist of validator hotkeys (basket gate + prune counts) + voters: Vec, + /// Keys present in param_bounds, bounded by MAX_PARAM_KEYS + bound_keys: Vec, + repos: Mapping, + /// (key, value) pairs per repo; keys must exist in param_bounds + params: Mapping, + param_bounds: Mapping, + /// (label, multiplier) pairs per repo, <= MAX_LABELS + label_mults: Mapping, + /// <= MAX_PATTERNS validated patterns per repo + branch_patterns: Mapping>, + /// hotkey -> basket entries, <= constants.basket_cap, weights sum 65535 + baskets: Mapping, + /// (github_id, key) -> block of last change (rate limit) + last_param_change: Mapping<(u64, u8), u64>, + /// github_id -> block of last rename (rate limit) + last_rename: Mapping, + } + + impl RepoRegistry { + // ==================================================================== + // Constructor + // ==================================================================== + + #[ink(constructor)] + pub fn new(owner: AccountId, netuid: u16) -> Self { + let mut param_bounds = Mapping::default(); + let mut bound_keys = Vec::new(); + for (key, min, max) in DEFAULT_BOUNDS { + param_bounds.insert(key, &Bounds { min, max }); + bound_keys.push(key); + } + let constants = Constants::launch(); + Self { + owner, + paused: false, + netuid, + storage_version: 1, + price_last: constants.price_floor, + price_last_block: u64::from(Self::env().block_number()), + last_reg_block: 0, + regs_in_block: 0, + constants, + repo_ids: Vec::new(), + voters: Vec::new(), + bound_keys, + repos: Mapping::default(), + params: Mapping::default(), + param_bounds, + label_mults: Mapping::default(), + branch_patterns: Mapping::default(), + baskets: Mapping::default(), + last_param_change: Mapping::default(), + last_rename: Mapping::default(), + } + } + + // ==================================================================== + // Permissionless + // ==================================================================== + + /// Registers a repository, pulling the dynamic alpha fee from the + /// caller's stake on `fee_hotkey` and recycling it. Prunes the least + /// supported non-immune repo when the registry is full. + #[ink(message)] + pub fn register( + &mut self, + github_id: u64, + full_name: String, + fee_hotkey: AccountId, + ) -> Result<(), Error> { + self.ensure_not_paused()?; + if github_id == 0 { + return Err(Error::InvalidGithubId); + } + if !valid_full_name(&full_name) { + return Err(Error::InvalidFullName); + } + if fee_hotkey == AccountId::from(ZERO_ACCOUNT) { + return Err(Error::ZeroAddress); + } + let now = self.now(); + if now == self.last_reg_block && self.regs_in_block >= self.constants.max_regs_per_block + { + return Err(Error::TooManyRegistrationsThisBlock); + } + if let Some(existing) = self.repos.get(github_id) { + if existing.active { + return Err(Error::AlreadyRegistered); + } + } + // Prune feasibility BEFORE the fee: all-immune must reject with no fee taken + let full = u32::try_from(self.repo_ids.len()).unwrap_or(u32::MAX) + >= self.constants.max_repos; + let victim = if full { Some(self.select_victim(now)?) } else { None }; + + let fee = self.current_price(now); + self.pull_and_recycle_fee(fee_hotkey, fee)?; + + if let Some(victim_id) = victim { + if let Some(pruned) = self.remove_repo(victim_id) { + self.env().emit_event(RepositoryPruned { + github_id: victim_id, + full_name: pruned.full_name, + replaced_by: github_id, + }); + } + } + + let caller = self.env().caller(); + let repo = Repo { + github_id, + full_name: full_name.clone(), + owner: caller, + reg_block: now, + active: true, + }; + self.repos.insert(github_id, &repo); + self.repo_ids.push(github_id); + + let bumped = price::mul_by_q32(fee, self.constants.price_bump_q32); + self.price_last = core::cmp::max( + self.constants.price_floor, + core::cmp::min(bumped, self.constants.price_ceiling), + ); + self.price_last_block = now; + if now == self.last_reg_block { + self.regs_in_block = self.regs_in_block.saturating_add(1); + } else { + self.last_reg_block = now; + self.regs_in_block = 1; + } + + self.env().emit_event(RepositoryRegistered { + github_id, + full_name, + owner: caller, + fee_paid: fee, + reg_block: now, + }); + Ok(()) + } + + /// Current registration price quote (lazy decay, pure storage read). + #[ink(message)] + pub fn get_price(&self) -> u128 { + u128::from(self.current_price(self.now())) + } + + // ==================================================================== + // Repo owner + // ==================================================================== + + #[ink(message)] + pub fn set_param(&mut self, github_id: u64, key: u8, value: u64) -> Result<(), Error> { + self.ensure_not_paused()?; + let repo = self.active_repo(github_id)?; + self.ensure_repo_owner(&repo)?; + let bounds = self.param_bounds.get(key).ok_or(Error::UnknownParamKey)?; + if value < bounds.min || value > bounds.max { + return Err(Error::ValueOutOfBounds); + } + let now = self.now(); + self.check_rate_limit(self.last_param_change.get((github_id, key)), now)?; + + let mut entries = self.params.get(github_id).unwrap_or_default(); + match entries.iter_mut().find(|(k, _)| *k == key) { + Some(entry) => entry.1 = value, + None => entries.push((key, value)), + } + self.params.insert(github_id, &entries); + self.last_param_change.insert((github_id, key), &now); + self.env().emit_event(ParamSet { github_id, key, value }); + Ok(()) + } + + #[ink(message)] + pub fn set_label_multiplier( + &mut self, + github_id: u64, + label: String, + value: u64, + ) -> Result<(), Error> { + self.ensure_not_paused()?; + let repo = self.active_repo(github_id)?; + self.ensure_repo_owner(&repo)?; + if !valid_label(&label) { + return Err(Error::InvalidLabel); + } + if !(LABEL_MULT_MIN..=LABEL_MULT_MAX).contains(&value) { + return Err(Error::ValueOutOfBounds); + } + let mut entries = self.label_mults.get(github_id).unwrap_or_default(); + match entries.iter_mut().find(|(l, _)| *l == label) { + Some(entry) => entry.1 = value, + None => { + if entries.len() >= MAX_LABELS { + return Err(Error::TooManyLabels); + } + entries.push((label.clone(), value)); + } + } + self.label_mults.insert(github_id, &entries); + self.env().emit_event(LabelMultiplierSet { github_id, label, value }); + Ok(()) + } + + #[ink(message)] + pub fn remove_label_multiplier( + &mut self, + github_id: u64, + label: String, + ) -> Result<(), Error> { + self.ensure_not_paused()?; + let repo = self.active_repo(github_id)?; + self.ensure_repo_owner(&repo)?; + let mut entries = self.label_mults.get(github_id).unwrap_or_default(); + let pos = entries + .iter() + .position(|(l, _)| *l == label) + .ok_or(Error::LabelNotFound)?; + entries.remove(pos); + if entries.is_empty() { + self.label_mults.remove(github_id); + } else { + self.label_mults.insert(github_id, &entries); + } + self.env().emit_event(LabelMultiplierRemoved { github_id, label }); + Ok(()) + } + + /// Replaces the full pattern list. Empty list clears. + #[ink(message)] + pub fn set_branch_patterns( + &mut self, + github_id: u64, + patterns: Vec, + ) -> Result<(), Error> { + self.ensure_not_paused()?; + let repo = self.active_repo(github_id)?; + self.ensure_repo_owner(&repo)?; + if patterns.len() > MAX_PATTERNS { + return Err(Error::TooManyPatterns); + } + for (i, pattern) in patterns.iter().enumerate() { + if !valid_branch_pattern(pattern) { + return Err(Error::InvalidPattern); + } + if patterns.iter().take(i).any(|other| other == pattern) { + return Err(Error::DuplicatePattern); + } + } + if patterns.is_empty() { + self.branch_patterns.remove(github_id); + } else { + self.branch_patterns.insert(github_id, &patterns); + } + self.env().emit_event(BranchPatternsSet { github_id, patterns }); + Ok(()) + } + + /// Follows a GitHub rename; id stays stable. Rate-limited. + #[ink(message)] + pub fn update_full_name( + &mut self, + github_id: u64, + full_name: String, + ) -> Result<(), Error> { + self.ensure_not_paused()?; + let mut repo = self.active_repo(github_id)?; + self.ensure_repo_owner(&repo)?; + if !valid_full_name(&full_name) { + return Err(Error::InvalidFullName); + } + let now = self.now(); + self.check_rate_limit(self.last_rename.get(github_id), now)?; + let old_full_name = repo.full_name; + repo.full_name = full_name.clone(); + self.repos.insert(github_id, &repo); + self.last_rename.insert(github_id, &now); + self.env().emit_event(FullNameUpdated { + github_id, + old_full_name, + new_full_name: full_name, + }); + Ok(()) + } + + #[ink(message)] + pub fn transfer_ownership( + &mut self, + github_id: u64, + new_owner: AccountId, + ) -> Result<(), Error> { + self.ensure_not_paused()?; + let mut repo = self.active_repo(github_id)?; + self.ensure_repo_owner(&repo)?; + if new_owner == AccountId::from(ZERO_ACCOUNT) { + return Err(Error::ZeroAddress); + } + let old_owner = repo.owner; + repo.owner = new_owner; + self.repos.insert(github_id, &repo); + self.env().emit_event(RepoOwnershipTransferred { + github_id, + old_owner, + new_owner, + }); + Ok(()) + } + + /// Frees the slot. No refund. Re-registration pays a fresh fee. + #[ink(message)] + pub fn deregister(&mut self, github_id: u64) -> Result<(), Error> { + self.ensure_not_paused()?; + let repo = self.active_repo(github_id)?; + self.ensure_repo_owner(&repo)?; + self.remove_and_emit(github_id, false); + Ok(()) + } + + // ==================================================================== + // Whitelisted validator hotkey + // ==================================================================== + + /// All ids must be registered+active, no duplicates, weights sum 65535. + #[ink(message)] + pub fn set_basket(&mut self, entries: BasketEntries) -> Result<(), Error> { + self.ensure_not_paused()?; + let caller = self.env().caller(); + if !self.voters.contains(&caller) { + return Err(Error::NotWhitelistedVoter); + } + self.validate_basket(&entries)?; + self.baskets.insert(caller, &entries); + self.env().emit_event(BasketSet { hotkey: caller, entries }); + Ok(()) + } + + #[ink(message)] + pub fn clear_basket(&mut self) -> Result<(), Error> { + self.ensure_not_paused()?; + let caller = self.env().caller(); + if self.baskets.get(caller).is_some() { + self.baskets.remove(caller); + self.env().emit_event(BasketCleared { hotkey: caller }); + } + Ok(()) + } + + // ==================================================================== + // Team (contract owner) + // ==================================================================== + + #[ink(message)] + pub fn add_voter(&mut self, hotkey: AccountId) -> Result<(), Error> { + self.ensure_owner()?; + if hotkey == AccountId::from(ZERO_ACCOUNT) { + return Err(Error::ZeroAddress); + } + if self.voters.contains(&hotkey) { + return Err(Error::VoterAlreadyAdded); + } + if self.voters.len() >= MAX_VOTERS { + return Err(Error::TooManyVoters); + } + self.voters.push(hotkey); + self.env().emit_event(VoterAdded { hotkey }); + Ok(()) + } + + #[ink(message)] + pub fn remove_voter(&mut self, hotkey: AccountId) -> Result<(), Error> { + self.ensure_owner()?; + let pos = self + .voters + .iter() + .position(|voter| voter == &hotkey) + .ok_or(Error::VoterNotFound)?; + self.voters.remove(pos); + self.baskets.remove(hotkey); + self.env().emit_event(VoterRemoved { hotkey }); + Ok(()) + } + + #[ink(message)] + pub fn set_bounds(&mut self, key: u8, min: u64, max: u64) -> Result<(), Error> { + self.ensure_owner()?; + if min > max { + return Err(Error::InvalidBounds); + } + if !self.bound_keys.contains(&key) { + if self.bound_keys.len() >= MAX_PARAM_KEYS { + return Err(Error::TooManyParamKeys); + } + self.bound_keys.push(key); + } + self.param_bounds.insert(key, &Bounds { min, max }); + self.env().emit_event(BoundsSet { key, min, max }); + Ok(()) + } + + /// Replaces the adjustable constants. price_last is re-clamped into + /// the new [floor, ceiling] band. + #[ink(message)] + pub fn set_config(&mut self, constants: Constants) -> Result<(), Error> { + self.ensure_owner()?; + let valid = constants.max_repos >= 1 + && constants.max_repos <= MAX_REPOS_HARD + && constants.price_floor >= 1 + && constants.price_floor <= constants.price_ceiling + && constants.price_bump_q32 >= price::ONE_Q32 + && constants.max_regs_per_block >= 1 + && constants.basket_cap >= 1; + if !valid { + return Err(Error::InvalidConfig); + } + self.price_last = core::cmp::max( + constants.price_floor, + core::cmp::min(self.price_last, constants.price_ceiling), + ); + self.constants = constants; + self.env().emit_event(ConfigUpdated {}); + Ok(()) + } + + /// Team escape hatch (squat-guard mismatch, abuse). + #[ink(message)] + pub fn force_deregister(&mut self, github_id: u64) -> Result<(), Error> { + self.ensure_owner()?; + self.active_repo(github_id)?; + self.remove_and_emit(github_id, true); + Ok(()) + } + + #[ink(message)] + pub fn set_paused(&mut self, paused: bool) -> Result<(), Error> { + self.ensure_owner()?; + self.paused = paused; + self.env().emit_event(PausedSet { paused }); + Ok(()) + } + + #[ink(message)] + pub fn set_owner(&mut self, new_owner: AccountId) -> Result<(), Error> { + self.ensure_owner()?; + if new_owner == AccountId::from(ZERO_ACCOUNT) { + return Err(Error::ZeroAddress); + } + let old_owner = self.owner; + self.owner = new_owner; + self.env().emit_event(OwnerChanged { old_owner, new_owner }); + Ok(()) + } + + /// Upgrades the contract logic in place (storage preserved). + #[ink(message)] + pub fn set_code_hash(&mut self, code_hash: Hash) -> Result<(), Error> { + self.ensure_owner()?; + self.env() + .set_code_hash(&code_hash) + .map_err(|_| Error::SetCodeFailed)?; + self.env().emit_event(CodeHashSet { code_hash }); + Ok(()) + } + + // ==================================================================== + // Reads (all pure storage — dry-run safe) + // ==================================================================== + + #[ink(message)] + pub fn get_repo(&self, github_id: u64) -> Option { + self.repos.get(github_id) + } + + #[ink(message)] + pub fn get_all_repos(&self) -> Vec { + self.repo_ids + .iter() + .filter_map(|id| self.repos.get(id)) + .collect() + } + + #[ink(message)] + pub fn get_params(&self, github_id: u64) -> ParamEntries { + self.params.get(github_id).unwrap_or_default() + } + + #[ink(message)] + pub fn get_label_multipliers(&self, github_id: u64) -> LabelEntries { + self.label_mults.get(github_id).unwrap_or_default() + } + + #[ink(message)] + pub fn get_branch_patterns(&self, github_id: u64) -> Vec { + self.branch_patterns.get(github_id).unwrap_or_default() + } + + #[ink(message)] + pub fn get_basket(&self, hotkey: AccountId) -> Option { + self.baskets.get(hotkey) + } + + #[ink(message)] + pub fn get_all_baskets(&self) -> Vec<(AccountId, BasketEntries)> { + self.voters + .iter() + .filter_map(|voter| self.baskets.get(voter).map(|basket| (*voter, basket))) + .collect() + } + + #[ink(message)] + pub fn get_voters(&self) -> Vec { + self.voters.clone() + } + + #[ink(message)] + pub fn get_bounds(&self, key: u8) -> Option { + self.param_bounds.get(key) + } + + #[ink(message)] + pub fn get_all_bounds(&self) -> Vec<(u8, Bounds)> { + self.bound_keys + .iter() + .filter_map(|key| self.param_bounds.get(key).map(|bounds| (*key, bounds))) + .collect() + } + + #[ink(message)] + pub fn get_config(&self) -> RegistryInfo { + RegistryInfo { + owner: self.owner, + paused: self.paused, + netuid: self.netuid, + storage_version: self.storage_version, + price_last: self.price_last, + price_last_block: self.price_last_block, + repo_count: u32::try_from(self.repo_ids.len()).unwrap_or(u32::MAX), + constants: self.constants.clone(), + } + } + + // ==================================================================== + // Internal + // ==================================================================== + + fn now(&self) -> u64 { + u64::from(self.env().block_number()) + } + + fn ensure_owner(&self) -> Result<(), Error> { + if self.env().caller() != self.owner { + return Err(Error::NotOwner); + } + Ok(()) + } + + fn ensure_not_paused(&self) -> Result<(), Error> { + if self.paused { + return Err(Error::Paused); + } + Ok(()) + } + + fn ensure_repo_owner(&self, repo: &Repo) -> Result<(), Error> { + if self.env().caller() != repo.owner { + return Err(Error::NotRepoOwner); + } + Ok(()) + } + + fn active_repo(&self, github_id: u64) -> Result { + let repo = self.repos.get(github_id).ok_or(Error::RepoNotFound)?; + if !repo.active { + return Err(Error::RepoNotActive); + } + Ok(repo) + } + + fn check_rate_limit(&self, last: Option, now: u64) -> Result<(), Error> { + match last { + Some(block) + if now.saturating_sub(block) < self.constants.param_rate_limit_blocks => + { + Err(Error::RateLimited) + } + _ => Ok(()), + } + } + + fn current_price(&self, now: u64) -> u64 { + price::lazy_price( + self.price_last, + now.saturating_sub(self.price_last_block), + self.constants.price_half_life, + self.constants.price_floor, + self.constants.price_ceiling, + ) + } + + /// Pull `fee` alpha from the caller's stake on `hotkey` into the + /// contract, verify the delta (silent-cap guard), then recycle it. + /// Any shortfall reverts the whole message. + fn pull_and_recycle_fee(&mut self, hotkey: AccountId, fee: u64) -> Result<(), Error> { + if fee == 0 { + return Ok(()); + } + let contract: [u8; 32] = *self.env().account_id().as_ref(); + let hot: [u8; 32] = *hotkey.as_ref(); + let before = self.stake_of(hot, contract); + self.env() + .extension() + .caller_transfer_stake(contract, hot, self.netuid, self.netuid, fee) + .map_err(|_| Error::FeeTransferFailed)?; + let after = self.stake_of(hot, contract); + if after.saturating_sub(before) < fee { + return Err(Error::FeeShortfall); + } + let recycled = self + .env() + .extension() + .recycle_alpha(hot, self.netuid, fee) + .map_err(|_| Error::RecycleFailed)?; + if recycled < fee { + return Err(Error::RecycleFailed); + } + Ok(()) + } + + fn stake_of(&self, hotkey: [u8; 32], coldkey: [u8; 32]) -> u64 { + self.env() + .extension() + .get_stake_info(hotkey, coldkey, self.netuid) + .map(|info| info.stake.0) + .unwrap_or(0) + } + + /// 2a prune selection: victim = non-immune repo in fewest whitelisted + /// baskets; tie -> lowest reg_block. All immune -> NoPrunableSlot. + /// (2b swaps this to stake-weighted selection; storage unchanged.) + fn select_victim(&self, now: u64) -> Result { + let mut best: Option<(u32, u64, u64)> = None; + for &id in &self.repo_ids { + let Some(repo) = self.repos.get(id) else { continue }; + if now < repo.reg_block.saturating_add(self.constants.immunity_period) { + continue; + } + let count = self.basket_count(id); + let candidate = (count, repo.reg_block, id); + let better = match best { + None => true, + Some((best_count, best_block, _)) => { + count < best_count || (count == best_count && repo.reg_block < best_block) + } + }; + if better { + best = Some(candidate); + } + } + best.map(|(_, _, id)| id).ok_or(Error::NoPrunableSlot) + } + + /// Number of whitelisted baskets containing `github_id`. + fn basket_count(&self, github_id: u64) -> u32 { + let mut count = 0u32; + for voter in &self.voters { + if let Some(basket) = self.baskets.get(voter) { + if basket.iter().any(|(id, _)| *id == github_id) { + count = count.saturating_add(1); + } + } + } + count + } + + fn validate_basket(&self, entries: &[(u64, u16)]) -> Result<(), Error> { + if entries.is_empty() { + return Err(Error::EmptyBasket); + } + if entries.len() > self.constants.basket_cap as usize { + return Err(Error::BasketTooLarge); + } + let mut sum = 0u32; + for (i, (id, weight)) in entries.iter().enumerate() { + if *weight == 0 { + return Err(Error::ZeroWeight); + } + if entries.iter().take(i).any(|(other, _)| other == id) { + return Err(Error::DuplicateBasketEntry); + } + self.active_repo(*id)?; + sum = sum.saturating_add(u32::from(*weight)); + } + if sum != WEIGHT_SUM { + return Err(Error::WeightSumMismatch); + } + Ok(()) + } + + /// Deactivates a repo and clears its per-repo state. The Repo record + /// stays (active = false) for history; stale basket entries remain and + /// fail re-validation on the next set_basket. + fn remove_repo(&mut self, github_id: u64) -> Option { + let mut repo = self.repos.get(github_id)?; + repo.active = false; + self.repos.insert(github_id, &repo); + if let Some(pos) = self.repo_ids.iter().position(|&id| id == github_id) { + self.repo_ids.swap_remove(pos); + } + if let Some(entries) = self.params.get(github_id) { + for (key, _) in entries { + self.last_param_change.remove((github_id, key)); + } + self.params.remove(github_id); + } + self.label_mults.remove(github_id); + self.branch_patterns.remove(github_id); + self.last_rename.remove(github_id); + Some(repo) + } + + fn remove_and_emit(&mut self, github_id: u64, forced: bool) { + if let Some(removed) = self.remove_repo(github_id) { + self.env().emit_event(RepositoryDeregistered { + github_id, + full_name: removed.full_name, + forced, + }); + } + } + } + + #[cfg(test)] + mod tests { + include!("tests.rs"); + } +} diff --git a/smart-contracts/repos-v0/price.rs b/smart-contracts/repos-v0/price.rs new file mode 100644 index 000000000..cab0e59f1 --- /dev/null +++ b/smart-contracts/repos-v0/price.rs @@ -0,0 +1,70 @@ +//! Bittensor-style registration price decay: whole halvings by shift plus a Q32 +//! fractional multiply. Ports subtensor `mul_by_q32` / `pow_q32` / `decay_factor_q32` +//! (pallets/subtensor/src/utils/misc.rs) with u64 exponents for lazy evaluation. +//! All arithmetic is saturating/clamped — no panics. + +/// 1.0 in Q32 +pub const ONE_Q32: u64 = 1 << 32; +/// 0.5 in Q32 +pub const HALF_Q32: u64 = 1 << 31; + +/// Multiply an integer by a Q32 fixed-point factor, saturating into u64. +pub fn mul_by_q32(value: u64, factor_q32: u64) -> u64 { + let product = (value as u128).saturating_mul(factor_q32 as u128); + let shifted = product >> 32; + core::cmp::min(shifted, u64::MAX as u128) as u64 +} + +/// Exponentiation-by-squaring for Q32 values: `base_q32 ^ exp` in Q32. +pub fn pow_q32(base_q32: u64, exp: u64) -> u64 { + let mut result = ONE_Q32; + let mut factor = base_q32; + let mut power = exp; + while power > 0 { + if power & 1 == 1 { + result = mul_by_q32(result, factor); + } + power >>= 1; + if power > 0 { + factor = mul_by_q32(factor, factor); + } + } + result +} + +/// Per-block decay factor `f` in Q32 such that `f ^ half_life = 0.5` (bisection). +pub fn decay_factor_q32(half_life: u64) -> u64 { + if half_life == 0 { + return ONE_Q32; + } + let mut lo = 0u64; + let mut hi = ONE_Q32; + while lo.saturating_add(1) < hi { + let mid = lo.saturating_add(hi.saturating_sub(lo) >> 1); + if pow_q32(mid, half_life) > HALF_Q32 { + hi = mid; + } else { + lo = mid; + } + } + lo +} + +/// Lazy price quote: `clamp(last * 0.5^(delta/half_life), floor, ceiling)`. +/// Whole halvings apply as a right shift; the fractional remainder applies as +/// `f^frac` where `f = decay_factor_q32(half_life)`. +pub fn lazy_price(last: u64, delta_blocks: u64, half_life: u64, floor: u64, ceiling: u64) -> u64 { + let mut price = last; + if half_life > 0 && delta_blocks > 0 { + let whole = delta_blocks.checked_div(half_life).unwrap_or(0); + let frac = delta_blocks.checked_rem(half_life).unwrap_or(0); + price = u32::try_from(whole) + .ok() + .and_then(|shift| price.checked_shr(shift)) + .unwrap_or(0); + if frac > 0 { + price = mul_by_q32(price, pow_q32(decay_factor_q32(half_life), frac)); + } + } + core::cmp::max(floor, core::cmp::min(price, ceiling)) +} diff --git a/smart-contracts/repos-v0/tests.rs b/smart-contracts/repos-v0/tests.rs new file mode 100644 index 000000000..55a8ca3ed --- /dev/null +++ b/smart-contracts/repos-v0/tests.rs @@ -0,0 +1,1190 @@ +use super::*; +use ink::env::test; +use scale::{Decode, Encode}; + +const TEST_NETUID: u16 = 1; + +fn account(byte: u8) -> AccountId { + AccountId::from([byte; 32]) +} + +/// Convention: account(1) = contract owner, account(2) = fee hotkey, +/// account(3..) = repo owners / voters. +fn set_caller(caller: AccountId) { + test::set_caller::(caller); +} + +fn advance_blocks(n: u64) { + for _ in 0..n { + test::advance_block::(); + } +} + +// ============================================================================ +// Mock Chain Extension (ext 5001: funcs 0 / 16 / 25) +// ============================================================================ + +struct MockExt { + stake: u64, + transfer_status: u32, + /// Credited amount = min(requested, cap) — simulates a silent cap + transfer_credit_cap: u64, + recycle_status: u32, + /// Returned recycled amount = requested - shortfall + recycle_shortfall: u64, +} + +impl Default for MockExt { + fn default() -> Self { + Self { + stake: 0, + transfer_status: 0, + transfer_credit_cap: u64::MAX, + recycle_status: 0, + recycle_shortfall: 0, + } + } +} + +impl ink::env::test::ChainExtension for MockExt { + fn ext_id(&self) -> u16 { + 5001 + } + + fn call(&mut self, func_id: u16, input: &[u8], output: &mut Vec) -> u32 { + // The off-chain engine passes args with a compact length prefix + let mut args_buf = input; + let _len: scale::Compact = Decode::decode(&mut args_buf).expect("input len"); + match func_id { + 0 => { + let info = crate::StakeInfo { + hotkey: account(0), + coldkey: account(0), + netuid: scale::Compact(TEST_NETUID), + stake: scale::Compact(self.stake), + locked: scale::Compact(0u64), + emission: scale::Compact(0u64), + tao_emission: scale::Compact(0u64), + drain: scale::Compact(0u64), + is_registered: true, + }; + Some(info).encode_to(output); + 0 + } + 25 => { + if self.transfer_status == 0 { + let args: ([u8; 32], [u8; 32], u16, u16, u64) = + Decode::decode(&mut args_buf).expect("transfer args"); + self.stake = self + .stake + .saturating_add(core::cmp::min(args.4, self.transfer_credit_cap)); + } + self.transfer_status + } + 16 => { + if self.recycle_status == 0 { + let args: ([u8; 32], u16, u64) = + Decode::decode(&mut args_buf).expect("recycle args"); + args.2.saturating_sub(self.recycle_shortfall).encode_to(output); + } + self.recycle_status + } + _ => 1, + } + } +} + +fn register_mock() { + test::register_chain_extension(MockExt::default()); +} + +// ============================================================================ +// Setup helpers +// ============================================================================ + +fn create_contract() -> RepoRegistry { + set_caller(account(1)); + RepoRegistry::new(account(1), TEST_NETUID) +} + +fn small_constants() -> crate::Constants { + crate::Constants { + max_repos: 3, + immunity_period: 5, + price_floor: 100, + price_ceiling: 1_000_000, + price_half_life: 10, + price_bump_q32: 2 << 32, + max_regs_per_block: 100, + param_rate_limit_blocks: 5, + snapshot_interval: 5, + basket_cap: 3, + } +} + +/// Applies small constants, squashing price_last down to the small floor +/// first (set_config only re-clamps, launch price_last >> small ceiling). +fn apply_small_config(contract: &mut RepoRegistry) { + set_caller(account(1)); + let mut squash = small_constants(); + squash.price_ceiling = squash.price_floor; + contract.set_config(squash).expect("squash config"); + contract.set_config(small_constants()).expect("set_config"); +} + +/// Contract with mock extension and test-friendly constants. +fn create_small_contract() -> RepoRegistry { + register_mock(); + let mut contract = create_contract(); + apply_small_config(&mut contract); + contract +} + +fn register_repo(contract: &mut RepoRegistry, github_id: u64, name: &str, owner: AccountId) { + set_caller(owner); + contract + .register(github_id, name.into(), account(2)) + .expect("register"); +} + +// ============================================================================ +// Price curve — golden vectors (shared with the python client, task 2a-3) +// ============================================================================ + +const GOLDEN_HALF_LIFE: u64 = 100_800; +const GOLDEN_FLOOR: u64 = 500_000_000_000; +const GOLDEN_CEILING: u64 = 500_000_000_000_000; + +/// (price_last, delta_blocks, expected_quote) at launch constants. +/// Explicit constants — the python client must reproduce these byte-for-byte. +const PRICE_GOLDEN: &[(u64, u64, u64)] = &[ + (500_000_000_000, 0, 500_000_000_000), + (500_000_000_000, 100_800, 500_000_000_000), + (1_000_000_000_000, 100_800, 500_000_000_000), + (500_000_000_000_000, 1, 499_996_561_789_885), + (500_000_000_000_000, 50_400, 353_551_803_971_640), + (500_000_000_000_000, 100_800, 250_000_000_000_000), + (500_000_000_000_000, 201_600, 125_000_000_000_000), + (500_000_000_000_000, 252_000, 88_387_950_992_910), + (500_000_000_000_000, 6_451_200, 500_000_000_000), + (123_456_789_012_345, 12_345, 113_408_938_323_092), + (600_000_000_000_000, 0, 500_000_000_000_000), + (400_000_000_000, 0, 500_000_000_000), + (2_000_000_000_000, 33_600, 1_587_396_302_726), + (2_000_000_000_000, 302_400, 500_000_000_000), +]; + +/// decay_factor_q32(100_800) — golden constant shared with python. +const DECAY_FACTOR_GOLDEN: u64 = 4_294_937_762; + +#[ink::test] +fn price_golden_vectors() { + for &(last, delta, expected) in PRICE_GOLDEN { + assert_eq!( + crate::price::lazy_price(last, delta, GOLDEN_HALF_LIFE, GOLDEN_FLOOR, GOLDEN_CEILING), + expected, + "lazy_price({last}, {delta})" + ); + } +} + +#[ink::test] +fn price_decay_factor_golden() { + assert_eq!(crate::price::decay_factor_q32(GOLDEN_HALF_LIFE), DECAY_FACTOR_GOLDEN); +} + +#[ink::test] +fn price_mul_by_q32_basics() { + assert_eq!(crate::price::mul_by_q32(1_000, crate::price::ONE_Q32), 1_000); + assert_eq!(crate::price::mul_by_q32(1_000, crate::price::HALF_Q32), 500); + assert_eq!(crate::price::mul_by_q32(u64::MAX, u64::MAX), u64::MAX); + assert_eq!(crate::price::mul_by_q32(0, u64::MAX), 0); +} + +#[ink::test] +fn price_pow_q32_basics() { + assert_eq!(crate::price::pow_q32(crate::price::HALF_Q32, 0), crate::price::ONE_Q32); + assert_eq!(crate::price::pow_q32(crate::price::HALF_Q32, 1), crate::price::HALF_Q32); + assert_eq!(crate::price::pow_q32(crate::price::HALF_Q32, 3), 1 << 29); +} + +#[ink::test] +fn price_decay_factor_edge_cases() { + assert_eq!(crate::price::decay_factor_q32(0), crate::price::ONE_Q32); + assert_eq!(crate::price::decay_factor_q32(1), crate::price::HALF_Q32); +} + +#[ink::test] +fn price_lazy_zero_half_life_only_clamps() { + assert_eq!(crate::price::lazy_price(700, 1_000_000, 0, 100, 500), 500); + assert_eq!(crate::price::lazy_price(50, 1_000_000, 0, 100, 500), 100); +} + +#[ink::test] +fn price_lazy_huge_delta_hits_floor() { + let delta = GOLDEN_HALF_LIFE.saturating_mul(64); + assert_eq!( + crate::price::lazy_price(GOLDEN_CEILING, delta, GOLDEN_HALF_LIFE, GOLDEN_FLOOR, GOLDEN_CEILING), + GOLDEN_FLOOR + ); +} + +#[ink::test] +fn price_bump_doubles() { + assert_eq!(crate::price::mul_by_q32(1_000, 2 << 32), 2_000); +} + +// ============================================================================ +// Input validators +// ============================================================================ + +#[ink::test] +fn full_name_accepts_valid() { + for name in ["octo/repo", "a/b", "my-org/my.repo_x", "org7/.github", "a-b/c-d"] { + assert!(crate::valid_full_name(name), "{name}"); + } +} + +#[ink::test] +fn full_name_rejects_invalid() { + let too_long_owner = format!("{}/r", "a".repeat(40)); + let too_long = format!("o/{}", "r".repeat(120)); + for name in [ + "", "norepo", "/repo", "owner/", "a/b/c", "Upper/repo", "owner/Repo", + "-owner/repo", "own er/repo", "owner/re po", "owner/rep@", + too_long_owner.as_str(), too_long.as_str(), + ] { + assert!(!crate::valid_full_name(name), "{name}"); + } +} + +#[ink::test] +fn branch_pattern_accepts_valid() { + let max_core = "a".repeat(39); + let max_star = format!("{}*", "a".repeat(39)); + for pattern in ["main", "release/*", "v1.2", "feat/x_y-z", "0abc", max_core.as_str(), max_star.as_str()] { + assert!(crate::valid_branch_pattern(pattern), "{pattern}"); + } +} + +#[ink::test] +fn branch_pattern_rejects_invalid() { + let over = "a".repeat(41); + for pattern in ["", "*", "*main", "a*b", "Main", "-x", ".x", "a b", over.as_str()] { + assert!(!crate::valid_branch_pattern(pattern), "{pattern}"); + } +} + +#[ink::test] +fn label_validation() { + assert!(crate::valid_label("bug")); + assert!(crate::valid_label("good first issue")); + assert!(crate::valid_label("priorit\u{00e9}")); + assert!(!crate::valid_label("")); + assert!(!crate::valid_label("a\tb")); + assert!(!crate::valid_label(&"x".repeat(51))); +} + +// ============================================================================ +// Constructor / config +// ============================================================================ + +#[ink::test] +fn constructor_sets_defaults() { + let contract = create_contract(); + let info = contract.get_config(); + assert_eq!(info.owner, account(1)); + assert!(!info.paused); + assert_eq!(info.netuid, TEST_NETUID); + assert_eq!(info.storage_version, 1); + assert_eq!(info.repo_count, 0); + assert_eq!(info.constants, crate::Constants::launch()); + assert_eq!(info.price_last, 500_000_000_000); + assert_eq!(contract.get_price(), 500_000_000_000); + assert!(contract.get_all_repos().is_empty()); + assert!(contract.get_voters().is_empty()); +} + +#[ink::test] +fn constructor_seeds_bounds_table() { + let contract = create_contract(); + let all = contract.get_all_bounds(); + assert_eq!(all.len(), 26); + assert_eq!(contract.get_bounds(1), Some(crate::Bounds { min: 0, max: 1_000_000 })); + assert_eq!(contract.get_bounds(4), Some(crate::Bounds { min: 0, max: 200_000 })); + assert_eq!(contract.get_bounds(9), Some(crate::Bounds { min: 1, max: 10_000_000_000 })); + assert_eq!(contract.get_bounds(27), None); +} + +#[ink::test] +fn set_config_validates() { + let mut contract = create_contract(); + let mut bad = small_constants(); + bad.max_repos = 0; + assert_eq!(contract.set_config(bad), Err(Error::InvalidConfig)); + let mut bad = small_constants(); + bad.price_floor = bad.price_ceiling.saturating_add(1); + assert_eq!(contract.set_config(bad), Err(Error::InvalidConfig)); + let mut bad = small_constants(); + bad.price_bump_q32 = crate::price::ONE_Q32 - 1; + assert_eq!(contract.set_config(bad), Err(Error::InvalidConfig)); + let mut bad = small_constants(); + bad.max_regs_per_block = 0; + assert_eq!(contract.set_config(bad), Err(Error::InvalidConfig)); + let mut bad = small_constants(); + bad.basket_cap = 0; + assert_eq!(contract.set_config(bad), Err(Error::InvalidConfig)); + let mut bad = small_constants(); + bad.max_repos = crate::MAX_REPOS_HARD + 1; + assert_eq!(contract.set_config(bad), Err(Error::InvalidConfig)); + assert_eq!(contract.set_config(small_constants()), Ok(())); + assert_eq!(contract.get_config().constants, small_constants()); +} + +#[ink::test] +fn set_config_requires_owner_and_reclamps_price() { + let mut contract = create_contract(); + set_caller(account(4)); + assert_eq!(contract.set_config(small_constants()), Err(Error::NotOwner)); + set_caller(account(1)); + // Launch price_last = 500 alpha; small ceiling 1_000_000 < that -> reclamp + contract.set_config(small_constants()).unwrap(); + assert_eq!(contract.get_config().price_last, 1_000_000); +} + +// ============================================================================ +// Registration +// ============================================================================ + +#[ink::test] +fn register_happy_path() { + let mut contract = create_small_contract(); + set_caller(account(3)); + assert_eq!(contract.register(42, "octo/repo".into(), account(2)), Ok(())); + let repo = contract.get_repo(42).expect("repo"); + assert_eq!(repo.github_id, 42); + assert_eq!(repo.full_name, "octo/repo"); + assert_eq!(repo.owner, account(3)); + assert!(repo.active); + assert_eq!(contract.get_all_repos().len(), 1); + assert_eq!(contract.get_config().repo_count, 1); + // Fee was floor (100); price bumped x2 + assert_eq!(contract.get_price(), 200); +} + +#[ink::test] +fn register_rejects_duplicate_active() { + let mut contract = create_small_contract(); + register_repo(&mut contract, 42, "octo/repo", account(3)); + set_caller(account(4)); + assert_eq!( + contract.register(42, "other/name".into(), account(2)), + Err(Error::AlreadyRegistered) + ); +} + +#[ink::test] +fn register_rejects_bad_inputs() { + let mut contract = create_small_contract(); + set_caller(account(3)); + assert_eq!( + contract.register(0, "octo/repo".into(), account(2)), + Err(Error::InvalidGithubId) + ); + assert_eq!( + contract.register(1, "NotLower/repo".into(), account(2)), + Err(Error::InvalidFullName) + ); + assert_eq!( + contract.register(1, "octo/repo".into(), account(0)), + Err(Error::ZeroAddress) + ); +} + +#[ink::test] +fn register_blocked_when_paused() { + let mut contract = create_small_contract(); + set_caller(account(1)); + contract.set_paused(true).unwrap(); + set_caller(account(3)); + assert_eq!( + contract.register(1, "octo/repo".into(), account(2)), + Err(Error::Paused) + ); +} + +#[ink::test] +fn register_per_block_cap() { + register_mock(); + let mut contract = create_contract(); + // Launch constants: MAX_REGS_PER_BLOCK = 1, fee = floor + set_caller(account(3)); + assert_eq!(contract.register(1, "octo/one".into(), account(2)), Ok(())); + assert_eq!( + contract.register(2, "octo/two".into(), account(2)), + Err(Error::TooManyRegistrationsThisBlock) + ); + advance_blocks(1); + assert_eq!(contract.register(2, "octo/two".into(), account(2)), Ok(())); +} + +#[ink::test] +fn register_fee_silent_cap_shortfall_reverts() { + test::register_chain_extension(MockExt { + transfer_credit_cap: 99, // fee will be 100 + ..MockExt::default() + }); + let mut contract = create_contract(); + apply_small_config(&mut contract); + set_caller(account(3)); + assert_eq!( + contract.register(1, "octo/repo".into(), account(2)), + Err(Error::FeeShortfall) + ); + assert_eq!(contract.get_repo(1), None); + assert_eq!(contract.get_config().repo_count, 0); + assert_eq!(contract.get_price(), 100); // no bump +} + +#[ink::test] +fn register_fee_transfer_failure_reverts() { + test::register_chain_extension(MockExt { + transfer_status: 6, // NotEnoughStakeToWithdraw + ..MockExt::default() + }); + let mut contract = create_contract(); + apply_small_config(&mut contract); + set_caller(account(3)); + assert_eq!( + contract.register(1, "octo/repo".into(), account(2)), + Err(Error::FeeTransferFailed) + ); + assert_eq!(contract.get_repo(1), None); +} + +#[ink::test] +fn register_recycle_failure_reverts() { + test::register_chain_extension(MockExt { + recycle_status: 1, + ..MockExt::default() + }); + let mut contract = create_contract(); + apply_small_config(&mut contract); + set_caller(account(3)); + assert_eq!( + contract.register(1, "octo/repo".into(), account(2)), + Err(Error::RecycleFailed) + ); +} + +#[ink::test] +fn register_recycle_shortfall_reverts() { + test::register_chain_extension(MockExt { + recycle_shortfall: 1, + ..MockExt::default() + }); + let mut contract = create_contract(); + apply_small_config(&mut contract); + set_caller(account(3)); + assert_eq!( + contract.register(1, "octo/repo".into(), account(2)), + Err(Error::RecycleFailed) + ); +} + +#[ink::test] +fn register_price_bumps_and_decays() { + let mut contract = create_small_contract(); + register_repo(&mut contract, 1, "octo/one", account(3)); + assert_eq!(contract.get_price(), 200); + advance_blocks(1); + register_repo(&mut contract, 2, "octo/two", account(3)); + // Quote just below 200 after 1 block of decay, bumped x2 + let quote = crate::price::lazy_price(200, 1, 10, 100, 1_000_000); + let expected = crate::price::mul_by_q32(quote, 2 << 32); + assert_eq!(contract.get_price() as u64, crate::price::lazy_price(expected, 0, 10, 100, 1_000_000)); + // Full half-life returns halfway toward the floor + advance_blocks(10); + assert_eq!( + contract.get_price() as u64, + crate::price::lazy_price(expected, 10, 10, 100, 1_000_000) + ); +} + +#[ink::test] +fn register_price_decays_to_floor() { + let mut contract = create_small_contract(); + register_repo(&mut contract, 1, "octo/one", account(3)); + assert_eq!(contract.get_price(), 200); + advance_blocks(20); // two half-lives: 200 -> 50 -> clamped to floor 100 + assert_eq!(contract.get_price(), 100); +} + +#[ink::test] +fn reregister_after_deregister_gets_fresh_state() { + let mut contract = create_small_contract(); + register_repo(&mut contract, 1, "octo/one", account(3)); + set_caller(account(3)); + contract.set_param(1, 4, 100_000).unwrap(); + contract.deregister(1).unwrap(); + assert!(!contract.get_repo(1).expect("kept").active); + advance_blocks(1); + set_caller(account(4)); + contract.register(1, "octo/one".into(), account(2)).unwrap(); + let repo = contract.get_repo(1).expect("repo"); + assert!(repo.active); + assert_eq!(repo.owner, account(4)); + assert_eq!(repo.reg_block, 1); + assert!(contract.get_params(1).is_empty()); +} + +// ============================================================================ +// Pruning +// ============================================================================ + +fn fill_registry(contract: &mut RepoRegistry) { + register_repo(contract, 1, "octo/one", account(3)); + advance_blocks(1); + register_repo(contract, 2, "octo/two", account(3)); + advance_blocks(1); + register_repo(contract, 3, "octo/three", account(3)); +} + +#[ink::test] +fn prune_rejects_when_all_immune_without_fee() { + let mut contract = create_small_contract(); + fill_registry(&mut contract); + let price_before = contract.get_price(); + set_caller(account(4)); + assert_eq!( + contract.register(4, "octo/four".into(), account(2)), + Err(Error::NoPrunableSlot) + ); + assert_eq!(contract.get_config().repo_count, 3); + assert_eq!(contract.get_price(), price_before); // fee untouched, no bump +} + +#[ink::test] +fn prune_tie_breaks_to_oldest() { + let mut contract = create_small_contract(); + fill_registry(&mut contract); // regs at blocks 0, 1, 2; immunity 5 + advance_blocks(5); // block 7: all non-immune, no baskets -> tie -> oldest (id 1) + set_caller(account(4)); + contract.register(4, "octo/four".into(), account(2)).unwrap(); + assert!(!contract.get_repo(1).expect("pruned").active); + assert!(contract.get_repo(2).expect("kept").active); + assert!(contract.get_repo(3).expect("kept").active); + assert!(contract.get_repo(4).expect("new").active); +} + +#[ink::test] +fn prune_selects_fewest_basket_appearances() { + let mut contract = create_small_contract(); + fill_registry(&mut contract); + set_caller(account(1)); + contract.add_voter(account(10)).unwrap(); + contract.add_voter(account(11)).unwrap(); + set_caller(account(10)); + contract.set_basket(vec![(1, 65_535)]).unwrap(); + set_caller(account(11)); + contract.set_basket(vec![(1, 30_000), (2, 35_535)]).unwrap(); + // Counts: 1 -> 2, 2 -> 1, 3 -> 0 + advance_blocks(5); + set_caller(account(4)); + contract.register(4, "octo/four".into(), account(2)).unwrap(); + assert!(!contract.get_repo(3).expect("pruned").active); + assert!(contract.get_repo(1).expect("kept").active); + assert!(contract.get_repo(2).expect("kept").active); +} + +#[ink::test] +fn prune_skips_immune_repos() { + let mut contract = create_small_contract(); + register_repo(&mut contract, 1, "octo/one", account(3)); + advance_blocks(1); + register_repo(&mut contract, 2, "octo/two", account(3)); + advance_blocks(5); // block 6 + register_repo(&mut contract, 3, "octo/three", account(3)); // immune until 11 + // Block 6: repo 1 (reg 0) and repo 2 (reg 1) non-immune; repo 3 immune. + // Protect repo 1 via basket -> victim must be repo 2. + set_caller(account(1)); + contract.add_voter(account(10)).unwrap(); + set_caller(account(10)); + contract.set_basket(vec![(1, 65_535)]).unwrap(); + set_caller(account(4)); + contract.register(4, "octo/four".into(), account(2)).unwrap(); + assert!(contract.get_repo(1).expect("kept").active); + assert!(!contract.get_repo(2).expect("pruned").active); + assert!(contract.get_repo(3).expect("kept").active); +} + +#[ink::test] +fn prune_clears_victim_state() { + let mut contract = create_small_contract(); + fill_registry(&mut contract); + set_caller(account(3)); + contract.set_param(1, 4, 100_000).unwrap(); + contract + .set_label_multiplier(1, "bug".into(), 1_500_000) + .unwrap(); + contract.set_branch_patterns(1, vec!["main".into()]).unwrap(); + advance_blocks(5); + set_caller(account(4)); + contract.register(4, "octo/four".into(), account(2)).unwrap(); + assert!(contract.get_params(1).is_empty()); + assert!(contract.get_label_multipliers(1).is_empty()); + assert!(contract.get_branch_patterns(1).is_empty()); +} + +// ============================================================================ +// Hyperparams +// ============================================================================ + +#[ink::test] +fn set_param_happy_path() { + let mut contract = create_small_contract(); + register_repo(&mut contract, 1, "octo/one", account(3)); + set_caller(account(3)); + assert_eq!(contract.set_param(1, 4, 150_000), Ok(())); + assert_eq!(contract.get_params(1), vec![(4, 150_000)]); +} + +#[ink::test] +fn set_param_requires_repo_owner() { + let mut contract = create_small_contract(); + register_repo(&mut contract, 1, "octo/one", account(3)); + set_caller(account(4)); + assert_eq!(contract.set_param(1, 4, 150_000), Err(Error::NotRepoOwner)); +} + +#[ink::test] +fn set_param_unknown_key() { + let mut contract = create_small_contract(); + register_repo(&mut contract, 1, "octo/one", account(3)); + set_caller(account(3)); + assert_eq!(contract.set_param(1, 99, 1), Err(Error::UnknownParamKey)); +} + +#[ink::test] +fn set_param_enforces_bounds() { + let mut contract = create_small_contract(); + register_repo(&mut contract, 1, "octo/one", account(3)); + set_caller(account(3)); + // Key 2 default_label_multiplier: [500_000, 2_000_000] + assert_eq!(contract.set_param(1, 2, 499_999), Err(Error::ValueOutOfBounds)); + assert_eq!(contract.set_param(1, 2, 2_000_001), Err(Error::ValueOutOfBounds)); + assert_eq!(contract.set_param(1, 2, 500_000), Ok(())); + // Key 9 divisor: exclusive zero -> min 1 + advance_blocks(5); + assert_eq!(contract.set_param(1, 9, 0), Err(Error::ValueOutOfBounds)); +} + +#[ink::test] +fn set_param_rate_limited_per_key() { + let mut contract = create_small_contract(); + register_repo(&mut contract, 1, "octo/one", account(3)); + set_caller(account(3)); + contract.set_param(1, 4, 100_000).unwrap(); + assert_eq!(contract.set_param(1, 4, 110_000), Err(Error::RateLimited)); + // Different key unaffected + assert_eq!(contract.set_param(1, 6, 5), Ok(())); + advance_blocks(5); + assert_eq!(contract.set_param(1, 4, 110_000), Ok(())); + assert_eq!(contract.get_params(1), vec![(4, 110_000), (6, 5)]); +} + +#[ink::test] +fn set_param_rejects_inactive_repo() { + let mut contract = create_small_contract(); + register_repo(&mut contract, 1, "octo/one", account(3)); + set_caller(account(3)); + contract.deregister(1).unwrap(); + assert_eq!(contract.set_param(1, 4, 100_000), Err(Error::RepoNotActive)); + assert_eq!(contract.set_param(999, 4, 100_000), Err(Error::RepoNotFound)); +} + +#[ink::test] +fn set_param_blocked_when_paused() { + let mut contract = create_small_contract(); + register_repo(&mut contract, 1, "octo/one", account(3)); + set_caller(account(1)); + contract.set_paused(true).unwrap(); + set_caller(account(3)); + assert_eq!(contract.set_param(1, 4, 100_000), Err(Error::Paused)); +} + +// ============================================================================ +// Label multipliers +// ============================================================================ + +#[ink::test] +fn label_multiplier_lifecycle() { + let mut contract = create_small_contract(); + register_repo(&mut contract, 1, "octo/one", account(3)); + set_caller(account(3)); + contract.set_label_multiplier(1, "bug".into(), 1_500_000).unwrap(); + contract.set_label_multiplier(1, "bug".into(), 800_000).unwrap(); // update in place + contract.set_label_multiplier(1, "feature".into(), 2_000_000).unwrap(); + assert_eq!( + contract.get_label_multipliers(1), + vec![("bug".into(), 800_000), ("feature".into(), 2_000_000)] + ); + contract.remove_label_multiplier(1, "bug".into()).unwrap(); + assert_eq!(contract.get_label_multipliers(1), vec![("feature".into(), 2_000_000)]); + assert_eq!( + contract.remove_label_multiplier(1, "bug".into()), + Err(Error::LabelNotFound) + ); +} + +#[ink::test] +fn label_multiplier_enforces_bounds_and_validity() { + let mut contract = create_small_contract(); + register_repo(&mut contract, 1, "octo/one", account(3)); + set_caller(account(3)); + assert_eq!( + contract.set_label_multiplier(1, "bug".into(), 499_999), + Err(Error::ValueOutOfBounds) + ); + assert_eq!( + contract.set_label_multiplier(1, "bug".into(), 2_000_001), + Err(Error::ValueOutOfBounds) + ); + assert_eq!( + contract.set_label_multiplier(1, "".into(), 1_000_000), + Err(Error::InvalidLabel) + ); + assert_eq!( + contract.set_label_multiplier(1, "x".repeat(51), 1_000_000), + Err(Error::InvalidLabel) + ); +} + +#[ink::test] +fn label_multiplier_cap() { + let mut contract = create_small_contract(); + register_repo(&mut contract, 1, "octo/one", account(3)); + set_caller(account(3)); + for i in 0..crate::MAX_LABELS { + contract + .set_label_multiplier(1, format!("label-{i}"), 1_000_000) + .unwrap(); + } + assert_eq!( + contract.set_label_multiplier(1, "one-too-many".into(), 1_000_000), + Err(Error::TooManyLabels) + ); +} + +#[ink::test] +fn label_multiplier_requires_repo_owner() { + let mut contract = create_small_contract(); + register_repo(&mut contract, 1, "octo/one", account(3)); + set_caller(account(4)); + assert_eq!( + contract.set_label_multiplier(1, "bug".into(), 1_000_000), + Err(Error::NotRepoOwner) + ); + assert_eq!( + contract.remove_label_multiplier(1, "bug".into()), + Err(Error::NotRepoOwner) + ); +} + +// ============================================================================ +// Branch patterns +// ============================================================================ + +#[ink::test] +fn branch_patterns_lifecycle() { + let mut contract = create_small_contract(); + register_repo(&mut contract, 1, "octo/one", account(3)); + set_caller(account(3)); + contract + .set_branch_patterns(1, vec!["develop".into(), "release/*".into()]) + .unwrap(); + assert_eq!( + contract.get_branch_patterns(1), + vec![String::from("develop"), String::from("release/*")] + ); + contract.set_branch_patterns(1, vec![]).unwrap(); // clears + assert!(contract.get_branch_patterns(1).is_empty()); +} + +#[ink::test] +fn branch_patterns_validation() { + let mut contract = create_small_contract(); + register_repo(&mut contract, 1, "octo/one", account(3)); + set_caller(account(3)); + assert_eq!( + contract.set_branch_patterns(1, vec!["*".into()]), + Err(Error::InvalidPattern) + ); + assert_eq!( + contract.set_branch_patterns(1, vec!["main".into(), "main".into()]), + Err(Error::DuplicatePattern) + ); + let five = (0..5).map(|i| format!("branch-{i}")).collect(); + assert_eq!(contract.set_branch_patterns(1, five), Err(Error::TooManyPatterns)); + set_caller(account(4)); + assert_eq!( + contract.set_branch_patterns(1, vec!["main".into()]), + Err(Error::NotRepoOwner) + ); +} + +// ============================================================================ +// Rename / transfer / deregister +// ============================================================================ + +#[ink::test] +fn update_full_name_happy_and_rate_limited() { + let mut contract = create_small_contract(); + register_repo(&mut contract, 1, "octo/one", account(3)); + set_caller(account(3)); + contract.update_full_name(1, "octo/renamed".into()).unwrap(); + assert_eq!(contract.get_repo(1).expect("repo").full_name, "octo/renamed"); + assert_eq!( + contract.update_full_name(1, "octo/again".into()), + Err(Error::RateLimited) + ); + advance_blocks(5); + assert_eq!(contract.update_full_name(1, "octo/again".into()), Ok(())); + assert_eq!( + contract.update_full_name(1, "Bad Name".into()), + Err(Error::InvalidFullName) + ); +} + +#[ink::test] +fn transfer_ownership_happy_and_guards() { + let mut contract = create_small_contract(); + register_repo(&mut contract, 1, "octo/one", account(3)); + set_caller(account(4)); + assert_eq!(contract.transfer_ownership(1, account(4)), Err(Error::NotRepoOwner)); + set_caller(account(3)); + assert_eq!(contract.transfer_ownership(1, account(0)), Err(Error::ZeroAddress)); + contract.transfer_ownership(1, account(4)).unwrap(); + assert_eq!(contract.get_repo(1).expect("repo").owner, account(4)); + // New owner controls params now + set_caller(account(4)); + assert_eq!(contract.set_param(1, 4, 100_000), Ok(())); +} + +#[ink::test] +fn deregister_frees_slot_and_clears_state() { + let mut contract = create_small_contract(); + register_repo(&mut contract, 1, "octo/one", account(3)); + set_caller(account(3)); + contract.set_param(1, 4, 100_000).unwrap(); + contract.deregister(1).unwrap(); + assert!(!contract.get_repo(1).expect("kept").active); + assert_eq!(contract.get_config().repo_count, 0); + assert!(contract.get_all_repos().is_empty()); + assert!(contract.get_params(1).is_empty()); + assert_eq!(contract.deregister(1), Err(Error::RepoNotActive)); +} + +#[ink::test] +fn deregister_requires_repo_owner() { + let mut contract = create_small_contract(); + register_repo(&mut contract, 1, "octo/one", account(3)); + set_caller(account(4)); + assert_eq!(contract.deregister(1), Err(Error::NotRepoOwner)); +} + +#[ink::test] +fn force_deregister_team_only() { + let mut contract = create_small_contract(); + register_repo(&mut contract, 1, "octo/one", account(3)); + set_caller(account(4)); + assert_eq!(contract.force_deregister(1), Err(Error::NotOwner)); + set_caller(account(1)); + assert_eq!(contract.force_deregister(1), Ok(())); + assert!(!contract.get_repo(1).expect("kept").active); + assert_eq!(contract.force_deregister(1), Err(Error::RepoNotActive)); +} + +// ============================================================================ +// Baskets +// ============================================================================ + +fn setup_with_repos_and_voter() -> RepoRegistry { + let mut contract = create_small_contract(); + fill_registry(&mut contract); + set_caller(account(1)); + contract.add_voter(account(10)).unwrap(); + contract +} + +#[ink::test] +fn set_basket_happy_path() { + let mut contract = setup_with_repos_and_voter(); + set_caller(account(10)); + let entries = vec![(1u64, 30_000u16), (2, 20_000), (3, 15_535)]; + assert_eq!(contract.set_basket(entries.clone()), Ok(())); + assert_eq!(contract.get_basket(account(10)), Some(entries.clone())); + assert_eq!(contract.get_all_baskets(), vec![(account(10), entries)]); +} + +#[ink::test] +fn set_basket_requires_whitelist() { + let mut contract = setup_with_repos_and_voter(); + set_caller(account(11)); + assert_eq!( + contract.set_basket(vec![(1, 65_535)]), + Err(Error::NotWhitelistedVoter) + ); +} + +#[ink::test] +fn set_basket_validation_matrix() { + let mut contract = setup_with_repos_and_voter(); + set_caller(account(10)); + assert_eq!(contract.set_basket(vec![]), Err(Error::EmptyBasket)); + assert_eq!( + contract.set_basket(vec![(1, 30_000), (1, 35_535)]), + Err(Error::DuplicateBasketEntry) + ); + assert_eq!( + contract.set_basket(vec![(1, 65_535), (2, 0)]), + Err(Error::ZeroWeight) + ); + assert_eq!( + contract.set_basket(vec![(1, 30_000), (2, 30_000)]), + Err(Error::WeightSumMismatch) + ); + assert_eq!( + contract.set_basket(vec![(999, 65_535)]), + Err(Error::RepoNotFound) + ); +} + +#[ink::test] +fn set_basket_rejects_over_cap() { + let mut contract = create_small_contract(); + // Need > basket_cap distinct repos: raise max_repos, keep cap 3 + set_caller(account(1)); + let mut constants = small_constants(); + constants.max_repos = 5; + contract.set_config(constants).unwrap(); + for (id, name) in [(1, "o/a"), (2, "o/b"), (3, "o/c"), (4, "o/d")] { + register_repo(&mut contract, id, name, account(3)); + advance_blocks(1); + } + set_caller(account(1)); + contract.add_voter(account(10)).unwrap(); + set_caller(account(10)); + assert_eq!( + contract.set_basket(vec![(1, 20_000), (2, 20_000), (3, 20_000), (4, 5_535)]), + Err(Error::BasketTooLarge) + ); +} + +#[ink::test] +fn set_basket_rejects_inactive_repo() { + let mut contract = setup_with_repos_and_voter(); + set_caller(account(3)); + contract.deregister(2).unwrap(); + set_caller(account(10)); + assert_eq!( + contract.set_basket(vec![(2, 65_535)]), + Err(Error::RepoNotActive) + ); +} + +#[ink::test] +fn clear_basket_removes_entry() { + let mut contract = setup_with_repos_and_voter(); + set_caller(account(10)); + contract.set_basket(vec![(1, 65_535)]).unwrap(); + contract.clear_basket().unwrap(); + assert_eq!(contract.get_basket(account(10)), None); + assert!(contract.get_all_baskets().is_empty()); + assert_eq!(contract.clear_basket(), Ok(())); // idempotent +} + +#[ink::test] +fn basket_blocked_when_paused() { + let mut contract = setup_with_repos_and_voter(); + set_caller(account(1)); + contract.set_paused(true).unwrap(); + set_caller(account(10)); + assert_eq!(contract.set_basket(vec![(1, 65_535)]), Err(Error::Paused)); + assert_eq!(contract.clear_basket(), Err(Error::Paused)); +} + +// ============================================================================ +// Voters +// ============================================================================ + +#[ink::test] +fn voter_lifecycle() { + let mut contract = create_small_contract(); + set_caller(account(1)); + contract.add_voter(account(10)).unwrap(); + assert_eq!(contract.get_voters(), vec![account(10)]); + assert_eq!(contract.add_voter(account(10)), Err(Error::VoterAlreadyAdded)); + assert_eq!(contract.add_voter(account(0)), Err(Error::ZeroAddress)); + contract.remove_voter(account(10)).unwrap(); + assert!(contract.get_voters().is_empty()); + assert_eq!(contract.remove_voter(account(10)), Err(Error::VoterNotFound)); +} + +#[ink::test] +fn voter_admin_requires_owner() { + let mut contract = create_small_contract(); + set_caller(account(4)); + assert_eq!(contract.add_voter(account(10)), Err(Error::NotOwner)); + assert_eq!(contract.remove_voter(account(10)), Err(Error::NotOwner)); +} + +#[ink::test] +fn voter_cap() { + let mut contract = create_small_contract(); + set_caller(account(1)); + for i in 0..crate::MAX_VOTERS { + contract + .add_voter(AccountId::from([u8::try_from(i + 100).unwrap(); 32])) + .unwrap(); + } + assert_eq!(contract.add_voter(account(99)), Err(Error::TooManyVoters)); +} + +#[ink::test] +fn removed_voter_loses_basket_and_prune_weight() { + let mut contract = setup_with_repos_and_voter(); + set_caller(account(10)); + contract.set_basket(vec![(1, 65_535)]).unwrap(); + set_caller(account(1)); + contract.remove_voter(account(10)).unwrap(); + assert_eq!(contract.get_basket(account(10)), None); + // Repo 1 unprotected again: tie-break prunes oldest (repo 1) + advance_blocks(7); + set_caller(account(4)); + contract.register(4, "octo/four".into(), account(2)).unwrap(); + assert!(!contract.get_repo(1).expect("pruned").active); +} + +// ============================================================================ +// Bounds admin +// ============================================================================ + +#[ink::test] +fn set_bounds_new_key_enables_param() { + let mut contract = create_small_contract(); + register_repo(&mut contract, 1, "octo/one", account(3)); + set_caller(account(3)); + assert_eq!(contract.set_param(1, 27, 5), Err(Error::UnknownParamKey)); + set_caller(account(1)); + contract.set_bounds(27, 1, 10).unwrap(); + assert_eq!(contract.get_all_bounds().len(), 27); + set_caller(account(3)); + assert_eq!(contract.set_param(1, 27, 5), Ok(())); + assert_eq!(contract.set_param(1, 27, 11), Err(Error::ValueOutOfBounds)); +} + +#[ink::test] +fn set_bounds_guards() { + let mut contract = create_small_contract(); + set_caller(account(4)); + assert_eq!(contract.set_bounds(27, 1, 10), Err(Error::NotOwner)); + set_caller(account(1)); + assert_eq!(contract.set_bounds(27, 10, 1), Err(Error::InvalidBounds)); + // Updating an existing key does not consume a slot + contract.set_bounds(4, 0, 100_000).unwrap(); + assert_eq!(contract.get_all_bounds().len(), 26); + assert_eq!(contract.get_bounds(4), Some(crate::Bounds { min: 0, max: 100_000 })); +} + +#[ink::test] +fn set_bounds_key_cap() { + let mut contract = create_small_contract(); + set_caller(account(1)); + let seeded = u8::try_from(crate::DEFAULT_BOUNDS.len()).unwrap(); + let cap = u8::try_from(crate::MAX_PARAM_KEYS).unwrap(); + for key in (seeded + 1)..=cap { + contract.set_bounds(key, 0, 1).unwrap(); + } + assert_eq!(contract.set_bounds(cap + 1, 0, 1), Err(Error::TooManyParamKeys)); +} + +// ============================================================================ +// Pause / ownership / upgrade +// ============================================================================ + +#[ink::test] +fn pause_matrix() { + let mut contract = create_small_contract(); + register_repo(&mut contract, 1, "octo/one", account(3)); + set_caller(account(4)); + assert_eq!(contract.set_paused(true), Err(Error::NotOwner)); + set_caller(account(1)); + contract.set_paused(true).unwrap(); + assert!(contract.get_config().paused); + // Owner/repo-owner mutations blocked + set_caller(account(3)); + assert_eq!(contract.deregister(1), Err(Error::Paused)); + assert_eq!(contract.update_full_name(1, "octo/x".into()), Err(Error::Paused)); + assert_eq!(contract.transfer_ownership(1, account(5)), Err(Error::Paused)); + assert_eq!( + contract.set_label_multiplier(1, "bug".into(), 1_000_000), + Err(Error::Paused) + ); + assert_eq!(contract.set_branch_patterns(1, vec![]), Err(Error::Paused)); + // Team admin still works while paused + set_caller(account(1)); + assert_eq!(contract.add_voter(account(10)), Ok(())); + assert_eq!(contract.force_deregister(1), Ok(())); + contract.set_paused(false).unwrap(); + assert!(!contract.get_config().paused); +} + +#[ink::test] +fn set_owner_transfers_control() { + let mut contract = create_small_contract(); + set_caller(account(4)); + assert_eq!(contract.set_owner(account(4)), Err(Error::NotOwner)); + set_caller(account(1)); + assert_eq!(contract.set_owner(account(0)), Err(Error::ZeroAddress)); + contract.set_owner(account(5)).unwrap(); + assert_eq!(contract.get_config().owner, account(5)); + assert_eq!(contract.set_paused(true), Err(Error::NotOwner)); // old owner locked out + set_caller(account(5)); + assert_eq!(contract.set_paused(true), Ok(())); +} + +#[ink::test] +fn set_code_hash_requires_owner() { + let mut contract = create_small_contract(); + set_caller(account(4)); + assert_eq!( + contract.set_code_hash(Hash::from([7u8; 32])), + Err(Error::NotOwner) + ); +} + +// ============================================================================ +// Reads +// ============================================================================ + +#[ink::test] +fn get_all_repos_returns_active_only() { + let mut contract = create_small_contract(); + fill_registry(&mut contract); + set_caller(account(3)); + contract.deregister(2).unwrap(); + let ids: Vec = contract.get_all_repos().iter().map(|r| r.github_id).collect(); + assert_eq!(ids.len(), 2); + assert!(ids.contains(&1) && ids.contains(&3)); +} + +#[ink::test] +fn get_repo_none_for_unknown() { + let contract = create_contract(); + assert_eq!(contract.get_repo(1), None); + assert!(contract.get_params(1).is_empty()); + assert!(contract.get_label_multipliers(1).is_empty()); + assert!(contract.get_branch_patterns(1).is_empty()); + assert_eq!(contract.get_basket(account(10)), None); +} diff --git a/smart-contracts/repos-v0/types.rs b/smart-contracts/repos-v0/types.rs new file mode 100644 index 000000000..f35039367 --- /dev/null +++ b/smart-contracts/repos-v0/types.rs @@ -0,0 +1,219 @@ +use ink::prelude::string::String; +use ink::primitives::AccountId; +use scale::{Compact, Decode, Encode}; + +// ============================================================================ +// Hard caps (compile-time, not config) +// ============================================================================ + +pub const MAX_REPOS_HARD: u32 = 256; +pub const MAX_VOTERS: usize = 64; +pub const MAX_PARAM_KEYS: usize = 64; +pub const MAX_LABELS: usize = 20; +pub const MAX_PATTERNS: usize = 4; +pub const MAX_FULL_NAME_LEN: usize = 100; +pub const MAX_OWNER_LEN: usize = 39; +pub const MAX_LABEL_LEN: usize = 50; +pub const MAX_PATTERN_LEN: usize = 40; +pub const LABEL_MULT_MIN: u64 = 500_000; +pub const LABEL_MULT_MAX: u64 = 2_000_000; +pub const WEIGHT_SUM: u32 = 65_535; + +/// (github_id, weight) pairs; weights sum to WEIGHT_SUM +pub type BasketEntries = ink::prelude::vec::Vec<(u64, u16)>; +/// (param key, value) pairs; keys must exist in param_bounds +pub type ParamEntries = ink::prelude::vec::Vec<(u8, u64)>; +/// (label, multiplier) pairs, <= MAX_LABELS +pub type LabelEntries = ink::prelude::vec::Vec<(String, u64)>; + +/// Default bounds table: (key, min, max). Fixed-point FP6 where noted in the spec. +/// Keys 9/15/19 have exclusive-zero lower bounds -> min = 1. +pub const DEFAULT_BOUNDS: [(u8, u64, u64); 26] = [ + (1, 0, 1_000_000), // issue_discovery_share + (2, 500_000, 2_000_000), // default_label_multiplier + (3, 0, 25_000_000), // fixed_base_score (0 = unset) + (4, 0, 200_000), // maintainer_cut + (5, 0, 1), // trusted_label_pipeline + (6, 1, 20), // min_valid_merged_prs + (7, 300_000, 1_000_000), // min_credibility + (8, 0, 30), // excessive_pr_penalty_base_threshold + (9, 1, 10_000_000_000), // open_pr_threshold_token_score (divisor) + (10, 1, 50), // max_open_pr_threshold + (11, 1, 20), // min_valid_solved_issues + (12, 300_000, 1_000_000), // min_issue_credibility + (13, 0, 1_000_000_000), // min_token_score_for_valid_issue + (14, 0, 30), // open_issue_spam_base_threshold + (15, 1, 10_000_000_000), // open_issue_spam_token_score_per_slot (divisor) + (16, 1, 100), // max_open_issue_threshold + (17, 1, 90), // pr_lookback_days + (18, 0, 1_000_000), // open_pr_collateral_percent + (19, 1, 1_000_000), // review_penalty_rate + (20, 1_000_000, 5_000_000), // standard_issue_multiplier + (21, 1_000_000, 2_000_000), // maintainer_issue_multiplier + (22, 10_000_000, 500_000_000), // src_tok_saturation_scale + (23, 0, 168), // grace_period_hours + (24, 1_000_000, 90_000_000), // sigmoid_midpoint_days + (25, 10_000, 5_000_000), // sigmoid_steepness + (26, 0, 1_000_000), // time_decay_min_multiplier +]; + +// ============================================================================ +// Chain extension types +// ============================================================================ + +/// StakeInfo returned by chain extension function 0. +/// Must match subtensor's StakeInfo struct exactly for SCALE decoding. +#[derive(Debug, Clone, Decode, Encode)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +pub struct StakeInfo { + pub hotkey: AccountId, + pub coldkey: AccountId, + pub netuid: Compact, + pub stake: Compact, + pub locked: Compact, + pub emission: Compact, + pub tao_emission: Compact, + pub drain: Compact, + pub is_registered: bool, +} + +/// Non-zero chain extension status code. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +pub struct ExtStatus(pub u32); + +impl ink::env::chain_extension::FromStatusCode for ExtStatus { + fn from_status_code(status: u32) -> Result<(), Self> { + if status == 0 { + Ok(()) + } else { + Err(Self(status)) + } + } +} + +// ============================================================================ +// Registry types +// ============================================================================ + +/// A registered repository. Keyed by immutable GitHub numeric id. +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout))] +pub struct Repo { + pub github_id: u64, + /// "owner/repo" lowercase, owner-updatable across renames + pub full_name: String, + /// Registrant coldkey; transferable + pub owner: AccountId, + /// Immunity anchor + prune tie-break + pub reg_block: u64, + pub active: bool, +} + +/// Min/max bounds for a hyperparam key +#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout))] +pub struct Bounds { + pub min: u64, + pub max: u64, +} + +/// One-tx adjustable launch constants (no redeploy needed) +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout))] +pub struct Constants { + pub max_repos: u32, + pub immunity_period: u64, + pub price_floor: u64, + pub price_ceiling: u64, + pub price_half_life: u64, + /// Post-registration price bump multiplier in Q32 (2.0 = 2 << 32) + pub price_bump_q32: u64, + pub max_regs_per_block: u32, + pub param_rate_limit_blocks: u64, + pub snapshot_interval: u64, + pub basket_cap: u32, +} + +impl Constants { + /// Tight launch preset from the spec + pub const fn launch() -> Self { + Self { + max_repos: 32, + immunity_period: 216_000, + price_floor: 500_000_000_000, + price_ceiling: 500_000_000_000_000, + price_half_life: 100_800, + price_bump_q32: 2 << 32, + max_regs_per_block: 1, + param_rate_limit_blocks: 3_600, + snapshot_interval: 3_600, + basket_cap: 10, + } + } +} + +/// Full contract configuration returned by get_config() +#[derive(Debug, Clone, Encode, Decode)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +pub struct RegistryInfo { + pub owner: AccountId, + pub paused: bool, + pub netuid: u16, + pub storage_version: u32, + pub price_last: u64, + pub price_last_block: u64, + pub repo_count: u32, + pub constants: Constants, +} + +// ============================================================================ +// Input validation +// ============================================================================ + +/// Lowercase "owner/repo": owner [a-z0-9-] (no leading '-', <=39), +/// repo [a-z0-9._-], total <=100 bytes. +pub fn valid_full_name(name: &str) -> bool { + if !(3..=MAX_FULL_NAME_LEN).contains(&name.len()) { + return false; + } + let Some((owner, repo)) = name.split_once('/') else { + return false; + }; + if owner.is_empty() || owner.len() > MAX_OWNER_LEN || repo.is_empty() || repo.contains('/') { + return false; + } + let owner_ok = !owner.starts_with('-') + && owner + .bytes() + .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-'); + let repo_ok = repo + .bytes() + .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || matches!(b, b'-' | b'_' | b'.')); + owner_ok && repo_ok +} + +/// `^[a-z0-9][a-z0-9._/-]{0,38}\*?$` — bare '*' rejected, '*' only as suffix. +pub fn valid_branch_pattern(pattern: &str) -> bool { + let bytes = pattern.as_bytes(); + if bytes.is_empty() || bytes.len() > MAX_PATTERN_LEN { + return false; + } + let core = match bytes.split_last() { + Some((b'*', rest)) => rest, + _ => bytes, + }; + let Some((&first, tail)) = core.split_first() else { + return false; + }; + (first.is_ascii_lowercase() || first.is_ascii_digit()) + && tail.len() <= 38 + && tail + .iter() + .all(|&b| b.is_ascii_lowercase() || b.is_ascii_digit() || matches!(b, b'.' | b'_' | b'/' | b'-')) +} + +/// 1..=50 bytes, no ASCII control characters (multi-byte UTF-8 allowed). +pub fn valid_label(label: &str) -> bool { + !label.is_empty() && label.len() <= MAX_LABEL_LEN && label.bytes().all(|b| b >= 0x20) +}