diff --git a/Cargo.lock b/Cargo.lock index 220042045..6ff7331d9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -355,7 +355,7 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.77", + "syn 2.0.89", ] [[package]] @@ -533,7 +533,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.89", ] [[package]] @@ -550,7 +550,7 @@ checksum = "a27b8a3a6e1a44fa4c8baf1f653e4172e81486d4941f2237e20dc2d0cf4ddff1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.89", ] [[package]] @@ -848,7 +848,7 @@ dependencies = [ "proc-macro-crate 3.2.0", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.89", "syn_derive", ] @@ -1075,7 +1075,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.89", ] [[package]] @@ -1505,7 +1505,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.89", ] [[package]] @@ -1532,7 +1532,7 @@ dependencies = [ "proc-macro2", "quote", "scratch", - "syn 2.0.77", + "syn 2.0.89", ] [[package]] @@ -1549,7 +1549,7 @@ checksum = "98532a60dedaebc4848cb2cba5023337cc9ea3af16a5b062633fabfd9f18fb60" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.89", ] [[package]] @@ -1597,7 +1597,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.11.1", - "syn 2.0.77", + "syn 2.0.89", ] [[package]] @@ -1619,7 +1619,7 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core 0.20.10", "quote", - "syn 2.0.77", + "syn 2.0.89", ] [[package]] @@ -1687,7 +1687,7 @@ checksum = "d65d7ce8132b7c0e54497a4d9a55a1c2a0912a0d786cf894472ba818fba45762" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.89", ] [[package]] @@ -1698,7 +1698,7 @@ checksum = "62d671cc41a825ebabc75757b62d3d168c577f9149b2d49ece1dad1f72119d25" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.89", ] [[package]] @@ -1709,7 +1709,7 @@ checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.89", ] [[package]] @@ -1722,7 +1722,27 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.77", + "syn 2.0.89", +] + +[[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.89", ] [[package]] @@ -1793,7 +1813,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.89", ] [[package]] @@ -1823,7 +1843,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "syn 2.0.77", + "syn 2.0.89", "termcolor", "toml 0.8.19", "walkdir", @@ -2078,7 +2098,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.89", ] [[package]] @@ -2310,7 +2330,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.89", ] [[package]] @@ -2897,6 +2917,124 @@ dependencies = [ "cc", ] +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.89", +] + [[package]] name = "ident_case" version = "1.0.1" @@ -2905,12 +3043,23 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" -version = "0.5.0" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" dependencies = [ - "unicode-bidi", - "unicode-normalization", + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +dependencies = [ + "icu_normalizer", + "icu_properties", ] [[package]] @@ -3005,7 +3154,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f357e2e867f4e222ffc4015a6e61d1073548de89f70a4e36a8b0385562777fa" dependencies = [ "blake2", - "derive_more", + "derive_more 0.99.18", "ink_primitives", "pallet-contracts-uapi-next", "parity-scale-codec", @@ -3023,7 +3172,7 @@ dependencies = [ "blake2", "cfg-if", "const_env", - "derive_more", + "derive_more 0.99.18", "ink_allocator", "ink_engine", "ink_prelude", @@ -3050,7 +3199,7 @@ version = "5.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a98fcc0ff9292ff68c7ee7b84c93533c9ff13859ec3b148faa822e2da9954fe6" dependencies = [ - "derive_more", + "derive_more 0.99.18", "impl-serde", "ink_prelude", "ink_primitives", @@ -3076,7 +3225,7 @@ version = "5.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11ec35ef7f45e67a53b6142d7e7f18e6d9292d76c3a2a1da14cf8423e481813d" dependencies = [ - "derive_more", + "derive_more 0.99.18", "ink_prelude", "parity-scale-codec", "scale-decode 0.10.0", @@ -3804,7 +3953,7 @@ checksum = "cb26336e6dc7cc76e7927d2c9e7e3bb376d7af65a6f56a0b16c47d18a9b1abc5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.89", ] [[package]] @@ -3819,6 +3968,12 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +[[package]] +name = "litemap" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" + [[package]] name = "lock_api" version = "0.4.12" @@ -4242,7 +4397,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.89", ] [[package]] @@ -4464,7 +4619,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.89", ] [[package]] @@ -4495,7 +4650,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.89", ] [[package]] @@ -4562,7 +4717,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6380dbe1fb03ecc74ad55d841cfc75480222d153ba69ddcb00977866cbdabdb8" dependencies = [ "polkavm-derive-impl 0.5.0", - "syn 2.0.77", + "syn 2.0.89", ] [[package]] @@ -4592,7 +4747,7 @@ dependencies = [ "polkavm-common 0.5.0", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.89", ] [[package]] @@ -4604,7 +4759,7 @@ dependencies = [ "polkavm-common 0.8.0", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.89", ] [[package]] @@ -4616,7 +4771,7 @@ dependencies = [ "polkavm-common 0.9.0", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.89", ] [[package]] @@ -4626,7 +4781,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "15e85319a0d5129dc9f021c62607e0804f5fb777a05cdda44d750ac0732def66" dependencies = [ "polkavm-derive-impl 0.8.0", - "syn 2.0.77", + "syn 2.0.89", ] [[package]] @@ -4636,7 +4791,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ba81f7b5faac81e528eb6158a6f3c9e0bb1008e0ffa19653bc8dea925ecb429" dependencies = [ "polkavm-derive-impl 0.9.0", - "syn 2.0.77", + "syn 2.0.89", ] [[package]] @@ -4708,10 +4863,13 @@ dependencies = [ "mockito", "regex", "reqwest 0.12.7", + "scale-info", "serde", "serde_json", "strum 0.26.3", "strum_macros 0.26.4", + "subxt", + "subxt-signer", "tar", "tempfile", "thiserror", @@ -4742,8 +4900,6 @@ dependencies = [ "sp-weights", "strum 0.26.3", "strum_macros 0.26.4", - "subxt", - "subxt-signer", "tar", "tempfile", "thiserror", @@ -4762,13 +4918,17 @@ dependencies = [ "duct", "flate2", "glob", + "hex", "indexmap 2.5.0", "mockito", "pop-common", "reqwest 0.12.7", + "scale-info", + "scale-value", "serde_json", "strum 0.26.3", "strum_macros 0.26.4", + "subxt", "symlink", "tar", "tempfile", @@ -4855,7 +5015,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479cf940fbbb3426c32c5d5176f62ad57549a0bb84773423ba8be9d089f5faba" dependencies = [ "proc-macro2", - "syn 2.0.77", + "syn 2.0.89", ] [[package]] @@ -4916,9 +5076,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.86" +version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" dependencies = [ "unicode-ident", ] @@ -5065,7 +5225,7 @@ checksum = "bcc303e793d3734489387d205e9b186fac9c6cfacedd98cbb2e8a5943595f3e6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.89", ] [[package]] @@ -5486,7 +5646,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "58c4eb8a81997cf040a091d1f7e1938aeab6749d3a0dfa73af43cdc32393483d" dependencies = [ "byteorder", - "derive_more", + "derive_more 0.99.18", "twox-hash", ] @@ -5544,7 +5704,7 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7caaf753f8ed1ab4752c6afb20174f03598c664724e0e32628e161c21000ff76" dependencies = [ - "derive_more", + "derive_more 0.99.18", "parity-scale-codec", "scale-bits 0.4.0", "scale-decode-derive 0.10.0", @@ -5558,7 +5718,7 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e98f3262c250d90e700bb802eb704e1f841e03331c2eb815e46516c4edbf5b27" dependencies = [ - "derive_more", + "derive_more 0.99.18", "parity-scale-codec", "primitive-types", "scale-bits 0.6.0", @@ -5598,7 +5758,7 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d70cb4b29360105483fac1ed567ff95d65224a14dd275b6303ed0a654c78de5" dependencies = [ - "derive_more", + "derive_more 0.99.18", "parity-scale-codec", "scale-encode-derive 0.5.0", "scale-info", @@ -5611,7 +5771,7 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ba0b9c48dc0eb20c60b083c29447c0c4617cb7c4a4c9fef72aa5c5bc539e15e" dependencies = [ - "derive_more", + "derive_more 0.99.18", "parity-scale-codec", "primitive-types", "scale-bits 0.6.0", @@ -5648,13 +5808,13 @@ dependencies = [ [[package]] name = "scale-info" -version = "2.11.3" +version = "2.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eca070c12893629e2cc820a9761bedf6ce1dcddc9852984d1dc734b8bd9bd024" +checksum = "346a3b32eba2640d17a9cb5927056b08f3de90f65b72fe09402c2ad07d684d0b" dependencies = [ "bitvec", "cfg-if", - "derive_more", + "derive_more 1.0.0", "parity-scale-codec", "scale-info-derive", "schemars", @@ -5663,14 +5823,14 @@ dependencies = [ [[package]] name = "scale-info-derive" -version = "2.11.3" +version = "2.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d35494501194174bda522a32605929eefc9ecf7e0a326c26db1fdd85881eb62" +checksum = "c6630024bf739e2179b91fb424b28898baf819414262c5d376677dbff1fe7ebf" dependencies = [ "proc-macro-crate 3.2.0", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.89", ] [[package]] @@ -5692,7 +5852,7 @@ dependencies = [ "proc-macro2", "quote", "scale-info", - "syn 2.0.77", + "syn 2.0.89", "thiserror", ] @@ -5704,7 +5864,7 @@ checksum = "ba4d772cfb7569e03868400344a1695d16560bf62b86b918604773607d39ec84" dependencies = [ "base58", "blake2", - "derive_more", + "derive_more 0.99.18", "either", "frame-metadata 15.1.0", "parity-scale-codec", @@ -5747,7 +5907,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.77", + "syn 2.0.89", ] [[package]] @@ -5920,7 +6080,7 @@ checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.89", ] [[package]] @@ -5931,7 +6091,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.89", ] [[package]] @@ -5955,7 +6115,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.89", ] [[package]] @@ -6225,7 +6385,7 @@ dependencies = [ "bs58", "chacha20", "crossbeam-queue", - "derive_more", + "derive_more 0.99.18", "ed25519-zebra 4.0.3", "either", "event-listener 4.0.3", @@ -6275,7 +6435,7 @@ dependencies = [ "async-lock", "base64 0.21.7", "blake2-rfc", - "derive_more", + "derive_more 0.99.18", "either", "event-listener 4.0.3", "fnv", @@ -6439,7 +6599,7 @@ checksum = "48d09fa0a5f7299fb81ee25ae3853d26200f7a348148aed6de76be905c007dbe" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.89", ] [[package]] @@ -6560,7 +6720,7 @@ dependencies = [ "proc-macro-crate 3.2.0", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.89", ] [[package]] @@ -6766,7 +6926,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.77", + "syn 2.0.89", ] [[package]] @@ -6840,7 +7000,7 @@ dependencies = [ "scale-info", "scale-typegen", "subxt-metadata", - "syn 2.0.77", + "syn 2.0.89", "thiserror", "tokio", ] @@ -6903,7 +7063,7 @@ dependencies = [ "quote", "scale-typegen", "subxt-codegen", - "syn 2.0.77", + "syn 2.0.89", ] [[package]] @@ -6960,9 +7120,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.77" +version = "2.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" +checksum = "44d46482f1c1c87acd84dea20c1bf5ebff4c757009ed6bf19cfd36fb10e92c4e" dependencies = [ "proc-macro2", "quote", @@ -6978,7 +7138,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.89", ] [[package]] @@ -6996,6 +7156,17 @@ dependencies = [ "futures-core", ] +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.89", +] + [[package]] name = "system-configuration" version = "0.5.1" @@ -7127,7 +7298,7 @@ checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.89", ] [[package]] @@ -7171,6 +7342,16 @@ dependencies = [ "time-core", ] +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + [[package]] name = "tinyvec" version = "1.8.0" @@ -7222,7 +7403,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.89", ] [[package]] @@ -7456,7 +7637,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.89", ] [[package]] @@ -7604,12 +7785,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "unicode-bidi" -version = "0.3.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" - [[package]] name = "unicode-ident" version = "1.0.13" @@ -7673,9 +7848,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.2" +version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" dependencies = [ "form_urlencoded", "idna", @@ -7689,6 +7864,18 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "utf8parse" version = "0.2.2" @@ -7818,7 +8005,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.89", "wasm-bindgen-shared", ] @@ -7852,7 +8039,7 @@ checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.89", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -8457,6 +8644,18 @@ version = "0.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + [[package]] name = "wyz" version = "0.5.1" @@ -8501,6 +8700,30 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff4524214bc4629eba08d78ceb1d6507070cc0bcbbed23af74e19e6e924a24cf" +[[package]] +name = "yoke" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.89", + "synstructure", +] + [[package]] name = "zerocopy" version = "0.7.35" @@ -8519,7 +8742,28 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.89", +] + +[[package]] +name = "zerofrom" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.89", + "synstructure", ] [[package]] @@ -8539,7 +8783,29 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.89", +] + +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.89", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index a5a8c7c1e..30c181caa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,7 +41,7 @@ toml = "0.5.0" # networking reqwest = { version = "0.12", features = ["json"] } tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] } -url = "2.5" +url = "2.5.4" # contracts subxt-signer = { version = "0.37.0", features = ["subxt", "sr25519"] } @@ -53,7 +53,9 @@ contract-build = "5.0.0-alpha" contract-extrinsics = "5.0.0-alpha" contract-transcode = "5.0.0-alpha" scale-info = { version = "2.11.3", default-features = false, features = ["derive"] } +scale-value = { version = "0.16.2", default-features = false, features = ["from-string", "parser-ss58"] } heck = "0.5.0" +hex = { version = "0.4.3", default-features = false } # parachains askama = "0.12" diff --git a/crates/pop-cli/src/commands/call/chain.rs b/crates/pop-cli/src/commands/call/chain.rs new file mode 100644 index 000000000..ce7050a29 --- /dev/null +++ b/crates/pop-cli/src/commands/call/chain.rs @@ -0,0 +1,1050 @@ +// SPDX-License-Identifier: GPL-3.0 + +use std::path::Path; + +use crate::cli::{self, traits::*}; +use anyhow::{anyhow, Result}; +use clap::Args; +use pop_parachains::{ + construct_extrinsic, construct_sudo_extrinsic, decode_call_data, encode_call_data, + find_dispatchable_by_name, find_pallet_by_name, parse_chain_metadata, set_up_client, + sign_and_submit_extrinsic, sign_and_submit_extrinsic_with_call_data, supported_actions, Action, + DynamicPayload, Function, OnlineClient, Pallet, Param, SubstrateConfig, +}; +use url::Url; + +const DEFAULT_URL: &str = "ws://localhost:9944/"; +const DEFAULT_URI: &str = "//Alice"; +const ENCODED_CALL_DATA_MAX_LEN: usize = 500; // Maximum length of encoded call data to display. + +/// Command to construct and execute extrinsics with configurable pallets, functions, arguments, and +/// signing options. +#[derive(Args, Clone, Default)] +pub struct CallChainCommand { + /// The pallet containing the dispatchable function to execute. + #[arg(short, long, value_parser = parse_pallet_name)] + pallet: Option, + /// The dispatchable function to execute within the specified pallet. + #[arg(short, long, value_parser = parse_function_name)] + function: Option, + /// The dispatchable function arguments, encoded as strings. + #[arg(short, long, num_args = 0..,)] + args: Vec, + /// Websocket endpoint of a node. + #[arg(short, long, value_parser)] + url: Option, + /// Secret key URI for the account signing the extrinsic. + /// + /// e.g. + /// - for a dev account "//Alice" + /// - with a password "//Alice///SECRET_PASSWORD" + #[arg(short, long)] + suri: Option, + /// SCALE encoded bytes representing the call data of the extrinsic. + #[arg(name = "call", short, long, conflicts_with_all = ["pallet", "function", "args"])] + call_data: Option, + /// Authenticates the sudo key and dispatches a function call with `Root` origin. + #[arg(short = 'S', long)] + sudo: bool, + /// Automatically signs and submits the extrinsic without prompting for confirmation. + #[arg(short('y'), long)] + skip_confirm: bool, +} + +impl CallChainCommand { + /// Executes the command. + pub(crate) async fn execute(mut self) -> Result<()> { + let mut cli = cli::Cli; + // Check if all fields are specified via the command line. + let prompt_to_repeat_call = self.requires_user_input(); + // Configure the chain. + let chain = self.configure_chain(&mut cli).await?; + // Execute the call if call_data is provided. + if let Some(call_data) = self.call_data.as_ref() { + if let Err(e) = self + .submit_extrinsic_from_call_data(&chain.client, call_data, &mut cli::Cli) + .await + { + display_message(&e.to_string(), false, &mut cli::Cli)?; + } + return Ok(()); + } + loop { + // Configure the call based on command line arguments/call UI. + let mut call = match self.configure_call(&chain, &mut cli) { + Ok(call) => call, + Err(e) => { + display_message(&e.to_string(), false, &mut cli)?; + break; + }, + }; + // Display the configured call. + cli.info(call.display(&chain))?; + // Prepare the extrinsic. + let xt = match call.prepare_extrinsic(&chain.client, &mut cli) { + Ok(payload) => payload, + Err(e) => { + display_message(&e.to_string(), false, &mut cli)?; + break; + }, + }; + + // Sign and submit the extrinsic. + if let Err(e) = call.submit_extrinsic(&chain.client, xt, &mut cli).await { + display_message(&e.to_string(), false, &mut cli)?; + break; + } + + if !prompt_to_repeat_call || + !cli.confirm("Do you want to perform another call?") + .initial_value(false) + .interact()? + { + display_message("Call complete.", true, &mut cli)?; + break; + } + self.reset_for_new_call(); + } + Ok(()) + } + + // Configures the chain by resolving the URL and fetching its metadata. + async fn configure_chain(&self, cli: &mut impl Cli) -> Result { + cli.intro("Call a chain")?; + // Resolve url. + let url = match &self.url { + Some(url) => url.clone(), + None => { + // Prompt for url. + let url: String = cli + .input("Which chain would you like to interact with?") + .default_input(DEFAULT_URL) + .interact()?; + Url::parse(&url)? + }, + }; + + // Parse metadata from chain url. + let client = set_up_client(url.as_str()).await?; + let mut pallets = parse_chain_metadata(&client).map_err(|e| { + anyhow!(format!("Unable to fetch the chain metadata: {}", e.to_string())) + })?; + // Sort by name for display. + pallets.sort_by(|a, b| a.name.cmp(&b.name)); + pallets.iter_mut().for_each(|p| p.functions.sort_by(|a, b| a.name.cmp(&b.name))); + Ok(Chain { url, client, pallets }) + } + + // Configure the call based on command line arguments/call UI. + fn configure_call(&mut self, chain: &Chain, cli: &mut impl Cli) -> Result { + loop { + // Resolve pallet. + let pallet = match self.pallet { + Some(ref pallet_name) => find_pallet_by_name(&chain.pallets, pallet_name)?, + None => { + // Specific predefined actions first. + if let Some(action) = prompt_predefined_actions(&chain.pallets, cli)? { + self.function = Some(action.function_name().to_string()); + find_pallet_by_name(&chain.pallets, action.pallet_name())? + } else { + let mut prompt = cli.select("Select the pallet to call:"); + for pallet_item in &chain.pallets { + prompt = prompt.item(pallet_item, &pallet_item.name, &pallet_item.docs); + } + prompt.interact()? + } + }, + }; + + // Resolve dispatchable function. + let function = match self.function { + Some(ref name) => find_dispatchable_by_name(&chain.pallets, &pallet.name, name)?, + None => { + let mut prompt = cli.select("Select the function to call:"); + for function in &pallet.functions { + prompt = prompt.item(function, &function.name, &function.docs); + } + prompt.interact()? + }, + }; + // Certain dispatchable functions are not supported yet due to complexity. + if !function.is_supported { + cli.outro_cancel( + "The selected function is not supported yet. Please choose another one.", + )?; + self.reset_for_new_call(); + continue; + } + + // Resolve dispatchable function arguments. + let args = if self.args.is_empty() { + let mut args = Vec::new(); + for param in &function.params { + let input = prompt_for_param(cli, param)?; + args.push(input); + } + args + } else { + self.expand_file_arguments()? + }; + + // If chain has sudo prompt the user to confirm if they want to execute the call via + // sudo. + self.configure_sudo(chain, cli)?; + + // Resolve who is signing the extrinsic. + let suri = match self.suri.as_ref() { + Some(suri) => suri.clone(), + None => + cli.input("Signer of the extrinsic:").default_input(DEFAULT_URI).interact()?, + }; + + return Ok(Call { + function: function.clone(), + args, + suri, + skip_confirm: self.skip_confirm, + sudo: self.sudo, + }); + } + } + + // Submits an extrinsic to the chain using the provided encoded call data. + async fn submit_extrinsic_from_call_data( + &self, + client: &OnlineClient, + call_data: &str, + cli: &mut impl Cli, + ) -> Result<()> { + // Resolve who is signing the extrinsic. + let suri = match self.suri.as_ref() { + Some(suri) => suri, + None => &cli.input("Signer of the extrinsic:").default_input(DEFAULT_URI).interact()?, + }; + cli.info(format!("Encoded call data: {}", call_data))?; + if !self.skip_confirm && + !cli.confirm("Do you want to submit the extrinsic?") + .initial_value(true) + .interact()? + { + display_message( + &format!("Extrinsic with call data {call_data} was not submitted."), + false, + cli, + )?; + return Ok(()); + } + let spinner = cliclack::spinner(); + spinner.start("Signing and submitting the extrinsic and then waiting for finalization, please be patient..."); + let call_data_bytes = + decode_call_data(call_data).map_err(|err| anyhow!("{}", format!("{err:?}")))?; + let result = sign_and_submit_extrinsic_with_call_data(client, call_data_bytes, suri) + .await + .map_err(|err| anyhow!("{}", format!("{err:?}")))?; + + spinner.stop(format!("Extrinsic submitted successfully with hash: {:?}", result)); + display_message("Call complete.", true, cli)?; + Ok(()) + } + + // Checks if the chain has the Sudo pallet and prompts the user to confirm if they want to + // execute the call via `sudo`. + fn configure_sudo(&mut self, chain: &Chain, cli: &mut impl Cli) -> Result<()> { + match find_dispatchable_by_name(&chain.pallets, "Sudo", "sudo") { + Ok(_) => + if !self.sudo { + self.sudo = cli + .confirm( + "Would you like to dispatch this function call with `Root` origin?", + ) + .initial_value(false) + .interact()?; + }, + Err(_) => + if self.sudo { + cli.warning( + "NOTE: sudo is not supported by the chain. Ignoring `--sudo` flag.", + )?; + self.sudo = false; + }, + } + Ok(()) + } + + // Resets specific fields to default values for a new call. + fn reset_for_new_call(&mut self) { + self.pallet = None; + self.function = None; + self.args.clear(); + self.sudo = false; + } + + // Function to check if all required fields are specified. + fn requires_user_input(&self) -> bool { + self.pallet.is_none() || + self.function.is_none() || + self.args.is_empty() || + self.url.is_none() || + self.suri.is_none() + } + + /// Replaces file arguments with their contents, leaving other arguments unchanged. + fn expand_file_arguments(&self) -> Result> { + self.args + .iter() + .map(|arg| { + if std::fs::metadata(arg).map(|m| m.is_file()).unwrap_or(false) { + std::fs::read_to_string(arg) + .map_err(|err| anyhow!("Failed to read file {}", err.to_string())) + } else { + Ok(arg.clone()) + } + }) + .collect() + } +} + +// Represents a chain, including its URL, client connection, and available pallets. +struct Chain { + // Websocket endpoint of the node. + url: Url, + // The client used to interact with the chain. + client: OnlineClient, + // A list of pallets available on the chain. + pallets: Vec, +} + +/// Represents a configured dispatchable function call, including the pallet, function, arguments, +/// and signing options. +#[derive(Clone)] +struct Call { + /// The dispatchable function to execute. + function: Function, + /// The dispatchable function arguments, encoded as strings. + args: Vec, + /// Secret key URI for the account signing the extrinsic. + /// + /// e.g. + /// - for a dev account "//Alice" + /// - with a password "//Alice///SECRET_PASSWORD" + suri: String, + /// Whether to automatically sign and submit the extrinsic without prompting for confirmation. + skip_confirm: bool, + /// Whether to dispatch the function call with `Root` origin. + sudo: bool, +} + +impl Call { + // Prepares the extrinsic. + fn prepare_extrinsic( + &self, + client: &OnlineClient, + cli: &mut impl Cli, + ) -> Result { + let xt = match construct_extrinsic(&self.function, self.args.clone()) { + Ok(tx) => tx, + Err(e) => { + return Err(anyhow!("Error: {}", e)); + }, + }; + // If sudo is required, wrap the call in a sudo call. + let xt = if self.sudo { construct_sudo_extrinsic(xt)? } else { xt }; + let encoded_data = encode_call_data(client, &xt)?; + // If the encoded call data is too long, don't display it all. + if encoded_data.len() < ENCODED_CALL_DATA_MAX_LEN { + cli.info(format!("Encoded call data: {}", encode_call_data(client, &xt)?))?; + } + Ok(xt) + } + + // Sign and submit an extrinsic. + async fn submit_extrinsic( + &mut self, + client: &OnlineClient, + tx: DynamicPayload, + cli: &mut impl Cli, + ) -> Result<()> { + if !self.skip_confirm && + !cli.confirm("Do you want to submit the extrinsic?") + .initial_value(true) + .interact()? + { + display_message( + &format!("Extrinsic for `{}` was not submitted.", self.function.name), + false, + cli, + )?; + return Ok(()); + } + let spinner = cliclack::spinner(); + spinner.start("Signing and submitting the extrinsic and then waiting for finalization, please be patient..."); + let result = sign_and_submit_extrinsic(client, tx, &self.suri) + .await + .map_err(|err| anyhow!("{}", format!("{err:?}")))?; + + spinner.stop(format!("Extrinsic submitted with hash: {:?}", result)); + Ok(()) + } + + fn display(&self, chain: &Chain) -> String { + let mut full_message = "pop call chain".to_string(); + full_message.push_str(&format!(" --pallet {}", self.function.pallet)); + full_message.push_str(&format!(" --function {}", self.function)); + if !self.args.is_empty() { + let args: Vec<_> = self + .args + .iter() + .map(|a| { + // If the argument is too long, don't show it all, truncate it. + if a.len() > ENCODED_CALL_DATA_MAX_LEN { + format!("\"{}...{}\"", &a[..20], &a[a.len() - 20..]) + } else { + format!("\"{a}\"") + } + }) + .collect(); + full_message.push_str(&format!(" --args {}", args.join(" "))); + } + full_message.push_str(&format!(" --url {} --suri {}", chain.url, self.suri)); + if self.sudo { + full_message.push_str(" --sudo"); + } + full_message + } +} + +// Displays a message to the user, with formatting based on the success status. +fn display_message(message: &str, success: bool, cli: &mut impl Cli) -> Result<()> { + if success { + cli.outro(message)?; + } else { + cli.outro_cancel(message)?; + } + Ok(()) +} + +// Prompts the user for some predefined actions. +fn prompt_predefined_actions(pallets: &[Pallet], cli: &mut impl Cli) -> Result> { + let mut predefined_action = cli.select("What would you like to do?"); + for action in supported_actions(pallets) { + predefined_action = predefined_action.item( + Some(action.clone()), + action.description(), + action.pallet_name(), + ); + } + predefined_action = predefined_action.item(None, "All", "Explore all pallets and functions"); + Ok(predefined_action.interact()?) +} + +// Prompts the user for the value of a parameter. +fn prompt_for_param(cli: &mut impl Cli, param: &Param) -> Result { + if param.is_optional { + if !cli + .confirm(format!( + "Do you want to provide a value for the optional parameter: {}?", + param.name + )) + .interact()? + { + return Ok("None()".to_string()); + } + let value = get_param_value(cli, param)?; + Ok(format!("Some({})", value)) + } else { + get_param_value(cli, param) + } +} + +// Resolves the value of a parameter based on its type. +fn get_param_value(cli: &mut impl Cli, param: &Param) -> Result { + if param.is_sequence { + prompt_for_sequence_param(cli, param) + } else if param.sub_params.is_empty() { + prompt_for_primitive_param(cli, param) + } else if param.is_variant { + prompt_for_variant_param(cli, param) + } else if param.is_tuple { + prompt_for_tuple_param(cli, param) + } else { + prompt_for_composite_param(cli, param) + } +} + +// Prompt for the value when it is a sequence. +fn prompt_for_sequence_param(cli: &mut impl Cli, param: &Param) -> Result { + let input_value = cli + .input(format!( + "The value for `{}` might be too large to enter. You may enter the path to a file instead.", + param.name + )) + .placeholder(&format!( + "Enter a value of type {} or provide a file path (e.g. /path/to/your/file)", + param.type_name + )) + .interact()?; + if Path::new(&input_value).is_file() { + return std::fs::read_to_string(&input_value) + .map_err(|err| anyhow!("Failed to read file {}", err.to_string())); + } + Ok(input_value) +} + +// Prompt for the value when it is a primitive. +fn prompt_for_primitive_param(cli: &mut impl Cli, param: &Param) -> Result { + Ok(cli + .input(format!("Enter the value for the parameter: {}", param.name)) + .placeholder(&format!("Type required: {}", param.type_name)) + .interact()?) +} + +// Prompt the user to select the value of the variant parameter and recursively prompt for nested +// fields. Output example: `Id(5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY)` for the `Id` +// variant. +fn prompt_for_variant_param(cli: &mut impl Cli, param: &Param) -> Result { + let selected_variant = { + let mut select = cli.select(format!("Select the value for the parameter: {}", param.name)); + for option in ¶m.sub_params { + select = select.item(option, &option.name, &option.type_name); + } + select.interact()? + }; + + if !selected_variant.sub_params.is_empty() { + let mut field_values = Vec::new(); + for field_arg in &selected_variant.sub_params { + let field_value = prompt_for_param(cli, field_arg)?; + field_values.push(field_value); + } + Ok(format!("{}({})", selected_variant.name, field_values.join(", "))) + } else { + Ok(format!("{}()", selected_variant.name)) + } +} + +// Recursively prompt the user for all the nested fields in a composite type. +// Example of a composite definition: +// Param { +// name: "Id", +// type_name: "AccountId32 ([u8;32])", +// is_optional: false, +// sub_params: [ +// Param { +// name: "Id", +// type_name: "[u8;32]", +// is_optional: false, +// sub_params: [], +// is_variant: false +// } +// ], +// is_variant: false +// } +fn prompt_for_composite_param(cli: &mut impl Cli, param: &Param) -> Result { + let mut field_values = Vec::new(); + for field_arg in ¶m.sub_params { + let field_value = prompt_for_param(cli, field_arg)?; + if param.sub_params.len() == 1 && param.name == param.sub_params[0].name { + field_values.push(field_value); + } else { + field_values.push(format!("{}: {}", field_arg.name, field_value)); + } + } + if param.sub_params.len() == 1 && param.name == param.sub_params[0].name { + Ok(field_values.join(", ").to_string()) + } else { + Ok(format!("{{{}}}", field_values.join(", "))) + } +} + +// Recursively prompt the user for the tuple values. +fn prompt_for_tuple_param(cli: &mut impl Cli, param: &Param) -> Result { + let mut tuple_values = Vec::new(); + for tuple_param in param.sub_params.iter() { + let tuple_value = prompt_for_param(cli, tuple_param)?; + tuple_values.push(tuple_value); + } + Ok(format!("({})", tuple_values.join(", "))) +} + +// Parser to capitalize the first letter of the pallet name. +fn parse_pallet_name(name: &str) -> Result { + let mut chars = name.chars(); + match chars.next() { + Some(c) => Ok(c.to_ascii_uppercase().to_string() + chars.as_str()), + None => Err("Pallet cannot be empty".to_string()), + } +} + +// Parser to convert the function name to lowercase. +fn parse_function_name(name: &str) -> Result { + Ok(name.to_ascii_lowercase()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::cli::MockCli; + use tempfile::tempdir; + use url::Url; + + const BOB_SURI: &str = "//Bob"; + const POP_NETWORK_TESTNET_URL: &str = "wss://rpc1.paseo.popnetwork.xyz"; + const POLKADOT_NETWORK_URL: &str = "wss://polkadot-rpc.publicnode.com"; + + #[tokio::test] + async fn configure_chain_works() -> Result<()> { + let call_config = + CallChainCommand { suri: Some(DEFAULT_URI.to_string()), ..Default::default() }; + let mut cli = MockCli::new().expect_intro("Call a chain").expect_input( + "Which chain would you like to interact with?", + POP_NETWORK_TESTNET_URL.into(), + ); + let chain = call_config.configure_chain(&mut cli).await?; + assert_eq!(chain.url, Url::parse(POP_NETWORK_TESTNET_URL)?); + cli.verify() + } + + #[tokio::test] + async fn guide_user_to_call_chain_works() -> Result<()> { + let mut call_config = + CallChainCommand { pallet: Some("System".to_string()), ..Default::default() }; + + let mut cli = MockCli::new() + .expect_intro("Call a chain") + .expect_input("Which chain would you like to interact with?", POP_NETWORK_TESTNET_URL.into()) + .expect_select( + "Select the function to call:", + Some(true), + true, + Some( + [ + ("apply_authorized_upgrade".to_string(), "Provide the preimage (runtime binary) `code` for an upgrade that has been authorized. If the authorization required a version check, this call will ensure the spec name remains unchanged and that the spec version has increased. Depending on the runtime's `OnSetCode` configuration, this function may directly apply the new `code` in the same block or attempt to schedule the upgrade. All origins are allowed.".to_string()), + ("authorize_upgrade".to_string(), "Authorize an upgrade to a given `code_hash` for the runtime. The runtime can be supplied later. This call requires Root origin.".to_string()), + ("authorize_upgrade_without_checks".to_string(), "Authorize an upgrade to a given `code_hash` for the runtime. The runtime can be supplied later. WARNING: This authorizes an upgrade that will take place without any safety checks, for example that the spec name remains the same and that the version number increases. Not recommended for normal use. Use `authorize_upgrade` instead. This call requires Root origin.".to_string()), + ("kill_prefix".to_string(), "Kill all storage items with a key that starts with the given prefix. **NOTE:** We rely on the Root origin to provide us the number of subkeys under the prefix we are removing to accurately calculate the weight of this function.".to_string()), + ("kill_storage".to_string(), "Kill some items from storage.".to_string()), + ("remark".to_string(), "Make some on-chain remark. Can be executed by every `origin`.".to_string()), + ("remark_with_event".to_string(), "Make some on-chain remark and emit event.".to_string()), + ("set_code".to_string(), "Set the new runtime code.".to_string()), + ("set_code_without_checks".to_string(), "Set the new runtime code without doing any checks of the given `code`. Note that runtime upgrades will not run if this is called with a not-increasing spec version!".to_string()), + ("set_heap_pages".to_string(), "Set the number of pages in the WebAssembly environment's heap.".to_string()), + ("set_storage".to_string(), "Set some items of storage.".to_string()), + ] + .to_vec(), + ), + 5, // "remark" dispatchable function + ) + .expect_input("The value for `remark` might be too large to enter. You may enter the path to a file instead.", "0x11".into()) + .expect_confirm("Would you like to dispatch this function call with `Root` origin?", true) + .expect_input("Signer of the extrinsic:", "//Bob".into()); + + let chain = call_config.configure_chain(&mut cli).await?; + assert_eq!(chain.url, Url::parse(POP_NETWORK_TESTNET_URL)?); + + let call_chain = call_config.configure_call(&chain, &mut cli)?; + assert_eq!(call_chain.function.pallet, "System"); + assert_eq!(call_chain.function.name, "remark"); + assert_eq!(call_chain.args, ["0x11".to_string()].to_vec()); + assert_eq!(call_chain.suri, "//Bob"); + assert!(call_chain.sudo); + assert_eq!(call_chain.display(&chain), "pop call chain --pallet System --function remark --args \"0x11\" --url wss://rpc1.paseo.popnetwork.xyz/ --suri //Bob --sudo"); + cli.verify() + } + + #[tokio::test] + async fn guide_user_to_configure_predefined_action_works() -> Result<()> { + let mut call_config = CallChainCommand::default(); + + let mut cli = MockCli::new().expect_intro("Call a chain").expect_input( + "Which chain would you like to interact with?", + POLKADOT_NETWORK_URL.into(), + ); + let chain = call_config.configure_chain(&mut cli).await?; + assert_eq!(chain.url, Url::parse(POLKADOT_NETWORK_URL)?); + cli.verify()?; + + let mut cli = MockCli::new() + .expect_select( + "What would you like to do?", + Some(true), + true, + Some( + supported_actions(&chain.pallets) + .into_iter() + .map(|action| { + (action.description().to_string(), action.pallet_name().to_string()) + }) + .chain(std::iter::once(( + "All".to_string(), + "Explore all pallets and functions".to_string(), + ))) + .collect::>(), + ), + 1, // "Purchase on-demand coretime" action + ) + .expect_input("Enter the value for the parameter: max_amount", "10000".into()) + .expect_input("Enter the value for the parameter: para_id", "2000".into()) + .expect_input("Signer of the extrinsic:", BOB_SURI.into()); + + let call_chain = call_config.configure_call(&chain, &mut cli)?; + + assert_eq!(call_chain.function.pallet, "OnDemand"); + assert_eq!(call_chain.function.name, "place_order_allow_death"); + assert_eq!(call_chain.args, ["10000".to_string(), "2000".to_string()].to_vec()); + assert_eq!(call_chain.suri, "//Bob"); + assert!(!call_chain.sudo); + assert_eq!(call_chain.display(&chain), "pop call chain --pallet OnDemand --function place_order_allow_death --args \"10000\" \"2000\" --url wss://polkadot-rpc.publicnode.com/ --suri //Bob"); + cli.verify() + } + + #[tokio::test] + async fn prepare_extrinsic_works() -> Result<()> { + let client = set_up_client(POP_NETWORK_TESTNET_URL).await?; + let mut call_config = Call { + function: Function { + pallet: "WrongName".to_string(), + name: "WrongName".to_string(), + ..Default::default() + }, + args: vec!["0x11".to_string()].to_vec(), + suri: DEFAULT_URI.to_string(), + skip_confirm: false, + sudo: false, + }; + let mut cli = MockCli::new(); + // Error, wrong name of the pallet. + assert!(matches!( + call_config.prepare_extrinsic(&client, &mut cli), + Err(message) + if message.to_string().contains("Failed to encode call data. Metadata Error: Pallet with name WrongName not found"))); + let pallets = parse_chain_metadata(&client)?; + call_config.function.pallet = "System".to_string(); + // Error, wrong name of the function. + assert!(matches!( + call_config.prepare_extrinsic(&client, &mut cli), + Err(message) + if message.to_string().contains("Failed to encode call data. Metadata Error: Call with name WrongName not found"))); + // Success, pallet and dispatchable function specified. + cli = MockCli::new().expect_info("Encoded call data: 0x00000411"); + call_config.function = find_dispatchable_by_name(&pallets, "System", "remark")?.clone(); + let xt = call_config.prepare_extrinsic(&client, &mut cli)?; + assert_eq!(xt.call_name(), "remark"); + assert_eq!(xt.pallet_name(), "System"); + + // Prepare extrinsic wrapped in sudo works. + cli = MockCli::new().expect_info("Encoded call data: 0x0f0000000411"); + call_config.sudo = true; + call_config.prepare_extrinsic(&client, &mut cli)?; + + cli.verify() + } + + #[tokio::test] + async fn user_cancel_submit_extrinsic_works() -> Result<()> { + let client = set_up_client(POP_NETWORK_TESTNET_URL).await?; + let pallets = parse_chain_metadata(&client)?; + let mut call_config = Call { + function: find_dispatchable_by_name(&pallets, "System", "remark")?.clone(), + args: vec!["0x11".to_string()].to_vec(), + suri: DEFAULT_URI.to_string(), + skip_confirm: false, + sudo: false, + }; + let mut cli = MockCli::new() + .expect_confirm("Do you want to submit the extrinsic?", false) + .expect_outro_cancel("Extrinsic for `remark` was not submitted."); + let xt = call_config.prepare_extrinsic(&client, &mut cli)?; + call_config.submit_extrinsic(&client, xt, &mut cli).await?; + + cli.verify() + } + + #[tokio::test] + async fn user_cancel_submit_extrinsic_from_call_data_works() -> Result<()> { + let client = set_up_client("wss://rpc1.paseo.popnetwork.xyz").await?; + let call_config = CallChainCommand { + pallet: None, + function: None, + args: vec![].to_vec(), + url: Some(Url::parse("wss://rpc1.paseo.popnetwork.xyz")?), + suri: None, + skip_confirm: false, + call_data: Some("0x00000411".to_string()), + sudo: false, + }; + let mut cli = MockCli::new() + .expect_input("Signer of the extrinsic:", "//Bob".into()) + .expect_confirm("Do you want to submit the extrinsic?", false) + .expect_outro_cancel("Extrinsic with call data 0x00000411 was not submitted."); + call_config + .submit_extrinsic_from_call_data(&client, "0x00000411", &mut cli) + .await?; + + cli.verify() + } + + #[tokio::test] + async fn configure_sudo_works() -> Result<()> { + // Test when sudo pallet doesn't exist. + let mut call_config = CallChainCommand { + pallet: None, + function: None, + args: vec![].to_vec(), + url: Some(Url::parse("wss://polkadot-rpc.publicnode.com")?), + suri: Some("//Alice".to_string()), + skip_confirm: false, + call_data: Some("0x00000411".to_string()), + sudo: true, + }; + let mut cli = MockCli::new() + .expect_intro("Call a chain") + .expect_warning("NOTE: sudo is not supported by the chain. Ignoring `--sudo` flag."); + let chain = call_config.configure_chain(&mut cli).await?; + call_config.configure_sudo(&chain, &mut cli)?; + assert!(!call_config.sudo); + cli.verify()?; + + // Test when sudo pallet exist. + cli = MockCli::new().expect_intro("Call a chain").expect_confirm( + "Would you like to dispatch this function call with `Root` origin?", + true, + ); + call_config.url = Some(Url::parse("wss://rpc1.paseo.popnetwork.xyz")?); + let chain = call_config.configure_chain(&mut cli).await?; + call_config.configure_sudo(&chain, &mut cli)?; + assert!(call_config.sudo); + cli.verify() + } + + #[test] + fn reset_for_new_call_works() -> Result<()> { + let mut call_config = CallChainCommand { + pallet: Some("System".to_string()), + function: Some("remark".to_string()), + args: vec!["0x11".to_string()].to_vec(), + url: Some(Url::parse(POP_NETWORK_TESTNET_URL)?), + suri: Some(DEFAULT_URI.to_string()), + skip_confirm: false, + call_data: None, + sudo: true, + }; + call_config.reset_for_new_call(); + assert_eq!(call_config.pallet, None); + assert_eq!(call_config.function, None); + assert_eq!(call_config.args.len(), 0); + assert!(!call_config.sudo); + Ok(()) + } + + #[test] + fn requires_user_input_works() -> Result<()> { + let mut call_config = CallChainCommand { + pallet: Some("System".to_string()), + function: Some("remark".to_string()), + args: vec!["0x11".to_string()].to_vec(), + url: Some(Url::parse(POP_NETWORK_TESTNET_URL)?), + suri: Some(DEFAULT_URI.to_string()), + skip_confirm: false, + call_data: None, + sudo: false, + }; + assert!(!call_config.requires_user_input()); + call_config.pallet = None; + assert!(call_config.requires_user_input()); + Ok(()) + } + + #[test] + fn expand_file_arguments_works() -> Result<()> { + let mut call_config = CallChainCommand { + pallet: Some("Registrar".to_string()), + function: Some("register".to_string()), + args: vec!["2000".to_string(), "0x1".to_string(), "0x12".to_string()].to_vec(), + url: Some(Url::parse(POP_NETWORK_TESTNET_URL)?), + suri: Some(DEFAULT_URI.to_string()), + call_data: None, + skip_confirm: false, + sudo: false, + }; + assert_eq!( + call_config.expand_file_arguments()?, + vec!["2000".to_string(), "0x1".to_string(), "0x12".to_string()] + ); + // Temporal file for testing when the input is a file. + let temp_dir = tempdir()?; + let genesis_file = temp_dir.path().join("genesis_file.json"); + std::fs::write(&genesis_file, "genesis_file_content")?; + let wasm_file = temp_dir.path().join("wasm_file.json"); + std::fs::write(&wasm_file, "wasm_file_content")?; + call_config.args = vec![ + "2000".to_string(), + genesis_file.display().to_string(), + wasm_file.display().to_string(), + ]; + assert_eq!( + call_config.expand_file_arguments()?, + vec![ + "2000".to_string(), + "genesis_file_content".to_string(), + "wasm_file_content".to_string() + ] + ); + Ok(()) + } + + #[test] + fn display_message_works() -> Result<()> { + let mut cli = MockCli::new().expect_outro(&"Call completed successfully!"); + display_message("Call completed successfully!", true, &mut cli)?; + cli.verify()?; + let mut cli = MockCli::new().expect_outro_cancel("Call failed."); + display_message("Call failed.", false, &mut cli)?; + cli.verify() + } + + #[tokio::test] + async fn prompt_predefined_actions_works() -> Result<()> { + let client = set_up_client(POP_NETWORK_TESTNET_URL).await?; + let pallets = parse_chain_metadata(&client)?; + let mut cli = MockCli::new().expect_select( + "What would you like to do?", + Some(true), + true, + Some( + supported_actions(&pallets) + .into_iter() + .map(|action| { + (action.description().to_string(), action.pallet_name().to_string()) + }) + .chain(std::iter::once(( + "All".to_string(), + "Explore all pallets and functions".to_string(), + ))) + .collect::>(), + ), + 2, // "Mint an Asset" action + ); + let action = prompt_predefined_actions(&pallets, &mut cli)?; + assert_eq!(action, Some(Action::MintAsset)); + cli.verify() + } + + #[tokio::test] + async fn prompt_for_param_works() -> Result<()> { + let client = set_up_client(POP_NETWORK_TESTNET_URL).await?; + let pallets = parse_chain_metadata(&client)?; + // Using NFT mint dispatchable function to test the majority of sub-functions. + let function = find_dispatchable_by_name(&pallets, "Nfts", "mint")?; + let mut cli = MockCli::new() + .expect_input("Enter the value for the parameter: collection", "0".into()) + .expect_input("Enter the value for the parameter: item", "0".into()) + .expect_select( + "Select the value for the parameter: mint_to", + Some(true), + true, + Some( + [ + ("Id".to_string(), "".to_string()), + ("Index".to_string(), "".to_string()), + ("Raw".to_string(), "".to_string()), + ("Address32".to_string(), "".to_string()), + ("Address20".to_string(), "".to_string()), + ] + .to_vec(), + ), + 0, // "Id" action + ) + .expect_input( + "Enter the value for the parameter: Id", + "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty".into(), + ) + .expect_confirm( + "Do you want to provide a value for the optional parameter: witness_data?", + true, + ) + .expect_confirm( + "Do you want to provide a value for the optional parameter: owned_item?", + false, + ) + .expect_confirm( + "Do you want to provide a value for the optional parameter: mint_price?", + true, + ) + .expect_input("Enter the value for the parameter: mint_price", "1000".into()); + + // Test all the function params. + let mut params: Vec = Vec::new(); + for param in &function.params { + params.push(prompt_for_param(&mut cli, ¶m)?); + } + assert_eq!(params.len(), 4); + assert_eq!(params[0], "0".to_string()); // collection: test primitive + assert_eq!(params[1], "0".to_string()); // item: test primitive + assert_eq!(params[2], "Id(5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty)".to_string()); // mint_to: test variant + assert_eq!(params[3], "Some({owned_item: None(), mint_price: Some(1000)})".to_string()); // witness_data: test composite + cli.verify()?; + + // Using Scheduler set_retry dispatchable function to test the tuple params. + let function = find_dispatchable_by_name(&pallets, "Scheduler", "set_retry")?; + let mut cli = MockCli::new() + .expect_input( + "Enter the value for the parameter: Index 0 of the tuple task", + "0".into(), + ) + .expect_input( + "Enter the value for the parameter: Index 1 of the tuple task", + "0".into(), + ) + .expect_input("Enter the value for the parameter: retries", "0".into()) + .expect_input("Enter the value for the parameter: period", "0".into()); + + // Test all the extrinsic params + let mut params: Vec = Vec::new(); + for param in &function.params { + params.push(prompt_for_param(&mut cli, ¶m)?); + } + assert_eq!(params.len(), 3); + assert_eq!(params[0], "(0, 0)".to_string()); // task: test tuples + assert_eq!(params[1], "0".to_string()); // retries: test primitive + assert_eq!(params[2], "0".to_string()); // period: test primitive + cli.verify()?; + + // Using System remark dispatchable function to test the sequence params. + let function = find_dispatchable_by_name(&pallets, "System", "remark")?; + // Temporal file for testing the input. + let temp_dir = tempdir()?; + let file = temp_dir.path().join("file.json"); + std::fs::write(&file, "testing")?; + + let mut cli = MockCli::new() + .expect_input( + "The value for `remark` might be too large to enter. You may enter the path to a file instead.", + file.display().to_string(), + ); + + // Test all the function params + let mut params: Vec = Vec::new(); + for param in &function.params { + params.push(prompt_for_param(&mut cli, ¶m)?); + } + assert_eq!(params.len(), 1); + assert_eq!(params[0], "testing".to_string()); // remark: test sequence from file + cli.verify() + } + + #[test] + fn parse_pallet_name_works() -> Result<()> { + assert_eq!(parse_pallet_name("system").unwrap(), "System"); + assert_eq!(parse_pallet_name("balances").unwrap(), "Balances"); + assert_eq!(parse_pallet_name("nfts").unwrap(), "Nfts"); + Ok(()) + } + + #[test] + fn parse_function_name_works() -> Result<()> { + assert_eq!(parse_function_name("Remark").unwrap(), "remark"); + assert_eq!(parse_function_name("Force_transfer").unwrap(), "force_transfer"); + assert_eq!(parse_function_name("MINT").unwrap(), "mint"); + Ok(()) + } +} diff --git a/crates/pop-cli/src/commands/call/mod.rs b/crates/pop-cli/src/commands/call/mod.rs index 6cb137f4b..dd1a2318c 100644 --- a/crates/pop-cli/src/commands/call/mod.rs +++ b/crates/pop-cli/src/commands/call/mod.rs @@ -2,6 +2,8 @@ use clap::{Args, Subcommand}; +#[cfg(feature = "parachain")] +pub(crate) mod chain; #[cfg(feature = "contract")] pub(crate) mod contract; @@ -13,9 +15,13 @@ pub(crate) struct CallArgs { pub command: Command, } -/// Call a smart contract. +/// Call a chain or a smart contract. #[derive(Subcommand)] pub(crate) enum Command { + /// Call a chain + #[cfg(feature = "parachain")] + #[clap(alias = "p", visible_aliases = ["parachain"])] + Chain(chain::CallChainCommand), /// Call a contract #[cfg(feature = "contract")] #[clap(alias = "c")] diff --git a/crates/pop-cli/src/commands/mod.rs b/crates/pop-cli/src/commands/mod.rs index aa385f28e..9af86e3f4 100644 --- a/crates/pop-cli/src/commands/mod.rs +++ b/crates/pop-cli/src/commands/mod.rs @@ -26,9 +26,9 @@ pub(crate) enum Command { #[clap(alias = "b", about = about_build())] #[cfg(any(feature = "parachain", feature = "contract"))] Build(build::BuildArgs), - /// Call a smart contract. + /// Call a chain or a smart contract. #[clap(alias = "c")] - #[cfg(feature = "contract")] + #[cfg(any(feature = "parachain", feature = "contract"))] Call(call::CallArgs), /// Launch a local network or deploy a smart contract. #[clap(alias = "u")] @@ -96,8 +96,11 @@ impl Command { build::Command::Spec(cmd) => cmd.execute().await.map(|_| Value::Null), }, }, - #[cfg(feature = "contract")] + #[cfg(any(feature = "parachain", feature = "contract"))] Self::Call(args) => match args.command { + #[cfg(feature = "parachain")] + call::Command::Chain(cmd) => cmd.execute().await.map(|_| Value::Null), + #[cfg(feature = "contract")] call::Command::Contract(cmd) => cmd.execute().await.map(|_| Value::Null), }, #[cfg(any(feature = "parachain", feature = "contract"))] diff --git a/crates/pop-cli/tests/parachain.rs b/crates/pop-cli/tests/parachain.rs index b9baaac58..67132e6d4 100644 --- a/crates/pop-cli/tests/parachain.rs +++ b/crates/pop-cli/tests/parachain.rs @@ -2,13 +2,13 @@ use anyhow::Result; use assert_cmd::{cargo::cargo_bin, Command}; -use pop_common::templates::Template; +use pop_common::{find_free_port, templates::Template}; use pop_parachains::Parachain; use std::{fs, path::Path, process::Command as Cmd}; use strum::VariantArray; use tokio::time::{sleep, Duration}; -/// Test the parachain lifecycle: new, build, up +/// Test the parachain lifecycle: new, build, up, call. #[tokio::test] async fn parachain_lifecycle() -> Result<()> { let temp = tempfile::tempdir().unwrap(); @@ -92,14 +92,86 @@ async fn parachain_lifecycle() -> Result<()> { assert!(content.contains("\"protocolId\": \"pop-protocol\"")); assert!(content.contains("\"id\": \"local_testnet\"")); - // pop up parachain -p "./test_parachain" + // Overwrite the config file to manually set the port to test pop call parachain. + let network_toml_path = temp_parachain_dir.join("network.toml"); + fs::create_dir_all(&temp_parachain_dir)?; + let random_port = find_free_port(); + let localhost_url = format!("ws://127.0.0.1:{}", random_port); + fs::write( + &network_toml_path, + format!( + r#"[relaychain] +chain = "paseo-local" + +[[relaychain.nodes]] +name = "alice" +rpc_port = {} +validator = true + +[[relaychain.nodes]] +name = "bob" +validator = true + +[[parachains]] +id = 2000 +default_command = "./target/release/parachain-template-node" + +[[parachains.collators]] +name = "collator-01" +"#, + random_port + ), + )?; + + // `pop up parachain -f ./network.toml --skip-confirm` let mut cmd = Cmd::new(cargo_bin("pop")) .current_dir(&temp_parachain_dir) .args(&["up", "parachain", "-f", "./network.toml", "--skip-confirm"]) .spawn() .unwrap(); - // If after 20 secs is still running probably execution is ok, or waiting for user response - sleep(Duration::from_secs(20)).await; + + // Wait for the networks to initialize. Increased timeout to accommodate CI environment delays. + sleep(Duration::from_secs(50)).await; + + // `pop call chain --pallet System --function remark --args "0x11" --url + // ws://127.0.0.1:random_port --suri //Alice --skip-confirm` + Command::cargo_bin("pop") + .unwrap() + .args(&[ + "call", + "chain", + "--pallet", + "System", + "--function", + "remark", + "--args", + "0x11", + "--url", + &localhost_url, + "--suri", + "//Alice", + "--skip-confirm", + ]) + .assert() + .success(); + + // pop call chain --call 0x00000411 --url ws://127.0.0.1:random_port --suri //Alice + // --skip-confirm + Command::cargo_bin("pop") + .unwrap() + .args(&[ + "call", + "chain", + "--call", + "0x00000411", + "--url", + &localhost_url, + "--suri", + "//Alice", + "--skip-confirm", + ]) + .assert() + .success(); assert!(cmd.try_wait().unwrap().is_none(), "the process should still be running"); // Stop the process diff --git a/crates/pop-common/Cargo.toml b/crates/pop-common/Cargo.toml index 87074a601..03e5035d7 100644 --- a/crates/pop-common/Cargo.toml +++ b/crates/pop-common/Cargo.toml @@ -16,10 +16,13 @@ git2.workspace = true git2_credentials.workspace = true regex.workspace = true reqwest.workspace = true +scale-info.workspace = true serde_json.workspace = true serde.workspace = true strum.workspace = true strum_macros.workspace = true +subxt.workspace = true +subxt-signer.workspace = true tar.workspace = true tempfile.workspace = true thiserror.workspace = true diff --git a/crates/pop-common/src/build.rs b/crates/pop-common/src/build.rs index f871078ad..85d1eba5a 100644 --- a/crates/pop-common/src/build.rs +++ b/crates/pop-common/src/build.rs @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: GPL-3.0 + use std::{ fmt, path::{Path, PathBuf}, diff --git a/crates/pop-common/src/errors.rs b/crates/pop-common/src/errors.rs index 02fb0c9b1..ceef11500 100644 --- a/crates/pop-common/src/errors.rs +++ b/crates/pop-common/src/errors.rs @@ -3,6 +3,7 @@ use crate::{sourcing, templates}; use thiserror::Error; +/// Represents the various errors that can occur in the crate. #[derive(Error, Debug)] pub enum Error { #[error("Anyhow error: {0}")] @@ -13,12 +14,19 @@ pub enum Error { Git(String), #[error("IO error: {0}")] IO(#[from] std::io::Error), - #[error("Failed to get manifest path: {0}")] - ManifestPath(String), + /// An error occurred while attempting to create a keypair from the provided URI. + #[error("Failed to create keypair from URI: {0}")] + KeyPairCreation(String), #[error("Manifest error: {0}")] ManifestError(#[from] cargo_toml::Error), + /// An error occurred while attempting to retrieve the manifest path. + #[error("Failed to get manifest path: {0}")] + ManifestPath(String), #[error("ParseError error: {0}")] ParseError(#[from] url::ParseError), + /// An error occurred while parsing the provided secret URI. + #[error("Failed to parse secret URI: {0}")] + ParseSecretURI(String), #[error("SourceError error: {0}")] SourceError(#[from] sourcing::Error), #[error("TemplateError error: {0}")] diff --git a/crates/pop-common/src/lib.rs b/crates/pop-common/src/lib.rs index b70dd121b..6f1991960 100644 --- a/crates/pop-common/src/lib.rs +++ b/crates/pop-common/src/lib.rs @@ -1,20 +1,32 @@ -pub mod build; -pub mod errors; -pub mod git; -pub mod helpers; -pub mod manifest; -pub mod polkadot_sdk; -pub mod sourcing; -pub mod templates; +// SPDX-License-Identifier: GPL-3.0 + +use std::net::TcpListener; pub use build::Profile; pub use errors::Error; pub use git::{Git, GitHub, Release}; pub use helpers::{get_project_name_from_path, prefix_with_current_dir_if_needed, replace_in_file}; pub use manifest::{add_crate_to_workspace, find_workspace_toml}; +pub use metadata::format_type; +pub use signer::create_signer; pub use sourcing::set_executable_permission; +pub use subxt::{Config, PolkadotConfig as DefaultConfig}; +pub use subxt_signer::sr25519::Keypair; pub use templates::extractor::extract_template_files; +pub mod build; +pub mod errors; +pub mod git; +pub mod helpers; +pub mod manifest; +/// Provides functionality for formatting and resolving metadata types. +pub mod metadata; +pub mod polkadot_sdk; +/// Provides functionality for creating a signer from a secret URI. +pub mod signer; +pub mod sourcing; +pub mod templates; + static APP_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")); /// Trait for observing status updates. @@ -52,6 +64,15 @@ pub fn target() -> Result<&'static str, Error> { Err(Error::UnsupportedPlatform { arch: ARCH, os: OS }) } +/// Finds an available port by binding to port 0 and retrieving the assigned port. +pub fn find_free_port() -> u16 { + TcpListener::bind("127.0.0.1:0") + .expect("Failed to bind to an available port") + .local_addr() + .expect("Failed to retrieve local address") + .port() +} + #[cfg(test)] mod test { use super::*; @@ -71,4 +92,14 @@ mod test { assert_eq!(target()?, target_expected); Ok(()) } + + #[test] + fn find_free_port_works() -> Result<()> { + let port = find_free_port(); + let addr = format!("127.0.0.1:{}", port); + // Constructs the TcpListener from the above port + let listener = TcpListener::bind(&addr); + assert!(listener.is_ok()); + Ok(()) + } } diff --git a/crates/pop-common/src/metadata.rs b/crates/pop-common/src/metadata.rs new file mode 100644 index 000000000..48f46da28 --- /dev/null +++ b/crates/pop-common/src/metadata.rs @@ -0,0 +1,228 @@ +// SPDX-License-Identifier: GPL-3.0 + +use scale_info::{form::PortableForm, PortableRegistry, Type, TypeDef, TypeDefPrimitive}; + +/// Formats a specified type, using the registry to output its full type representation. +/// +/// # Arguments +/// * `ty`: The type to format, containing metadata like name, parameters, and definition. +/// * `registry`: The registry used to resolve type dependencies and provides details for complex +/// types. +pub fn format_type(ty: &Type, registry: &PortableRegistry) -> String { + let mut name = ty + .path + .segments + .last() + .map(|s| s.to_owned()) + .unwrap_or_else(|| ty.path.to_string()); + + if !ty.type_params.is_empty() { + let params: Vec<_> = ty + .type_params + .iter() + .filter_map(|p| { + if let Some(ty) = p.ty { + registry.resolve(ty.id) + } else { + None // Ignore if p.ty is None + } + }) + .map(|t| format_type(t, registry)) + .collect(); + name = format!("{name}<{}>", params.join(",")); + } + name = format!( + "{name}{}", + match &ty.type_def { + TypeDef::Composite(composite) => { + if composite.fields.is_empty() { + return "".to_string(); + } + + let mut named = false; + let fields: Vec<_> = composite + .fields + .iter() + .filter_map(|f| match f.name.as_ref() { + None => registry.resolve(f.ty.id).map(|t| format_type(t, registry)), + Some(field) => { + named = true; + f.type_name.as_ref().map(|t| format!("{field}: {t}")) + }, + }) + .collect(); + match named { + true => format!(" {{ {} }}", fields.join(", ")), + false => format!(" ({})", fields.join(", ")), + } + }, + TypeDef::Variant(variant) => { + let variants: Vec<_> = variant + .variants + .iter() + .map(|v| { + if v.fields.is_empty() { + return v.name.clone(); + } + + let name = v.name.as_str(); + let mut named = false; + let fields: Vec<_> = v + .fields + .iter() + .filter_map(|f| match f.name.as_ref() { + None => registry.resolve(f.ty.id).map(|t| format_type(t, registry)), + Some(field) => { + named = true; + f.type_name.as_ref().map(|t| format!("{field}: {t}")) + }, + }) + .collect(); + format!( + "{name}{}", + match named { + true => format!("{{ {} }}", fields.join(", ")), + false => format!("({})", fields.join(", ")), + } + ) + }) + .collect(); + format!(": {}", variants.join(", ")) + }, + TypeDef::Sequence(sequence) => { + format!( + "[{}]", + format_type( + registry.resolve(sequence.type_param.id).expect("sequence type not found"), + registry + ) + ) + }, + TypeDef::Array(array) => { + format!( + "[{};{}]", + format_type( + registry.resolve(array.type_param.id).expect("array type not found"), + registry + ), + array.len + ) + }, + TypeDef::Tuple(tuple) => { + let fields: Vec<_> = tuple + .fields + .iter() + .filter_map(|p| registry.resolve(p.id)) + .map(|t| format_type(t, registry)) + .collect(); + format!("({})", fields.join(",")) + }, + TypeDef::Primitive(primitive) => { + use TypeDefPrimitive::*; + match primitive { + Bool => "bool", + Char => "char", + Str => "str", + U8 => "u8", + U16 => "u16", + U32 => "u32", + U64 => "u64", + U128 => "u128", + U256 => "u256", + I8 => "i8", + I16 => "i16", + I32 => "i32", + I64 => "i64", + I128 => "i128", + I256 => "i256", + } + .to_string() + }, + TypeDef::Compact(compact) => { + format!( + "Compact<{}>", + format_type( + registry.resolve(compact.type_param.id).expect("compact type not found"), + registry + ) + ) + }, + TypeDef::BitSequence(_) => { + "BitSequence".to_string() + }, + } + ); + + name +} + +#[cfg(test)] +mod tests { + use super::*; + use anyhow::Result; + use subxt::{OnlineClient, SubstrateConfig}; + + const POP_NETWORK_TESTNET_URL: &str = "wss://rpc1.paseo.popnetwork.xyz"; + + #[tokio::test] + async fn format_type_works() -> Result<()> { + let client = OnlineClient::::from_url(POP_NETWORK_TESTNET_URL).await?; + let metadata = client.metadata(); + let registry = metadata.types(); + + // Validate `Nfts::mint` extrinsic types cover most of cases. + let nfts_mint_extrinsic = + metadata.pallet_by_name("Nfts").unwrap().call_variant_by_name("mint").unwrap(); + let nfts_mint_types: Vec = nfts_mint_extrinsic + .fields + .iter() + .map(|field| { + let type_info = registry.resolve(field.ty.id).unwrap(); + format_type(&type_info, registry) + }) + .collect(); + assert_eq!(nfts_mint_types.len(), 4); + assert_eq!(nfts_mint_types[0], "u32"); // collection + assert_eq!(nfts_mint_types[1], "u32"); // item + assert_eq!(nfts_mint_types[2], "MultiAddress: Id(AccountId32 ([u8;32])), Index(Compact<()>), Raw([u8]), Address32([u8;32]), Address20([u8;20])"); // mint_to + assert_eq!(nfts_mint_types[3], "Option { owned_item: Option, mint_price: Option }>: None, Some(MintWitness { owned_item: Option, mint_price: Option })"); // witness_data + + // Validate `System::remark` to cover Sequences. + let system_remark_extrinsic = metadata + .pallet_by_name("System") + .unwrap() + .call_variant_by_name("remark") + .unwrap(); + let system_remark_types: Vec = system_remark_extrinsic + .fields + .iter() + .map(|field| { + let type_info = registry.resolve(field.ty.id).unwrap(); + format_type(&type_info, registry) + }) + .collect(); + assert_eq!(system_remark_types.len(), 1); + assert_eq!(system_remark_types[0], "[u8]"); // remark + + // Extrinsic Scheduler::set_retry, cover tuples. + let scheduler_set_retry_extrinsic = metadata + .pallet_by_name("Scheduler") + .unwrap() + .call_variant_by_name("set_retry") + .unwrap(); + let scheduler_set_retry_types: Vec = scheduler_set_retry_extrinsic + .fields + .iter() + .map(|field| { + let type_info = registry.resolve(field.ty.id).unwrap(); + format_type(&type_info, registry) + }) + .collect(); + assert_eq!(scheduler_set_retry_types.len(), 3); + assert_eq!(scheduler_set_retry_types[0], "(u32,u32)"); // task + assert_eq!(scheduler_set_retry_types[1], "u8"); // retries + assert_eq!(scheduler_set_retry_types[2], "u32"); // period + + Ok(()) + } +} diff --git a/crates/pop-contracts/src/utils/signer.rs b/crates/pop-common/src/signer.rs similarity index 52% rename from crates/pop-contracts/src/utils/signer.rs rename to crates/pop-common/src/signer.rs index 51fc44b0e..e542d0417 100644 --- a/crates/pop-contracts/src/utils/signer.rs +++ b/crates/pop-common/src/signer.rs @@ -1,24 +1,19 @@ // SPDX-License-Identifier: GPL-3.0 use crate::errors::Error; -use contract_build::util::decode_hex; -use sp_core::Bytes; use subxt_signer::{sr25519::Keypair, SecretUri}; -/// Create a Signer from a secret URI. -pub(crate) fn create_signer(suri: &str) -> Result { +/// Create a keypair from a secret URI. +/// +/// # Arguments +/// `suri` - Secret URI string used to generate the `Keypair`. +pub fn create_signer(suri: &str) -> Result { let uri = ::from_str(suri) .map_err(|e| Error::ParseSecretURI(format!("{}", e)))?; let keypair = Keypair::from_uri(&uri).map_err(|e| Error::KeyPairCreation(format!("{}", e)))?; Ok(keypair) } -/// Parse hex encoded bytes. -pub fn parse_hex_bytes(input: &str) -> Result { - let bytes = decode_hex(input).map_err(|e| Error::HexParsing(format!("{}", e)))?; - Ok(bytes.into()) -} - #[cfg(test)] mod tests { use super::*; @@ -39,18 +34,4 @@ mod tests { assert!(matches!(create_signer("11111"), Err(Error::KeyPairCreation(..)))); Ok(()) } - - #[test] - fn parse_hex_bytes_works() -> Result<(), Error> { - let input_in_hex = "48656c6c6f"; - let result = parse_hex_bytes(input_in_hex)?; - assert_eq!(result, Bytes(vec![72, 101, 108, 108, 111])); - Ok(()) - } - - #[test] - fn parse_hex_bytes_fails_wrong_input() -> Result<(), Error> { - assert!(matches!(parse_hex_bytes("wronghexvalue"), Err(Error::HexParsing(..)))); - Ok(()) - } } diff --git a/crates/pop-contracts/Cargo.toml b/crates/pop-contracts/Cargo.toml index 3d4f15fb1..b3ab82cb8 100644 --- a/crates/pop-contracts/Cargo.toml +++ b/crates/pop-contracts/Cargo.toml @@ -27,8 +27,6 @@ sp-core.workspace = true sp-weights.workspace = true strum.workspace = true strum_macros.workspace = true -subxt-signer.workspace = true -subxt.workspace = true # cargo-contracts contract-build.workspace = true diff --git a/crates/pop-contracts/src/build.rs b/crates/pop-contracts/src/build.rs index dcbec25a8..df9cc4a3f 100644 --- a/crates/pop-contracts/src/build.rs +++ b/crates/pop-contracts/src/build.rs @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-3.0 -use crate::{errors::Error, utils::helpers::get_manifest_path}; +use crate::{errors::Error, utils::get_manifest_path}; pub use contract_build::Verbosity; use contract_build::{execute, BuildMode, BuildResult, ExecuteArgs}; use std::path::Path; diff --git a/crates/pop-contracts/src/call.rs b/crates/pop-contracts/src/call.rs index 1886f3be2..03effcb0a 100644 --- a/crates/pop-contracts/src/call.rs +++ b/crates/pop-contracts/src/call.rs @@ -3,9 +3,9 @@ use crate::{ errors::Error, utils::{ - helpers::{get_manifest_path, parse_account, parse_balance}, + get_manifest_path, metadata::{process_function_args, FunctionType}, - signer::create_signer, + parse_account, parse_balance, }, }; use anyhow::Context; @@ -15,10 +15,9 @@ use contract_extrinsics::{ ExtrinsicOptsBuilder, TokenMetadata, }; use ink_env::{DefaultEnvironment, Environment}; +use pop_common::{create_signer, Config, DefaultConfig, Keypair}; use sp_weights::Weight; use std::path::PathBuf; -use subxt::{Config, PolkadotConfig as DefaultConfig}; -use subxt_signer::sr25519::Keypair; use url::Url; /// Attributes for the `call` command. @@ -180,10 +179,10 @@ mod tests { use crate::{ contracts_node_generator, dry_run_gas_estimate_instantiate, errors::Error, instantiate_smart_contract, mock_build_process, new_environment, run_contracts_node, - set_up_deployment, testing::find_free_port, UpOpts, + set_up_deployment, UpOpts, }; use anyhow::Result; - use pop_common::set_executable_permission; + use pop_common::{find_free_port, set_executable_permission}; use sp_core::Bytes; use std::{env, process::Command, time::Duration}; use tokio::time::sleep; diff --git a/crates/pop-contracts/src/errors.rs b/crates/pop-contracts/src/errors.rs index b59031a06..e7ddec21b 100644 --- a/crates/pop-contracts/src/errors.rs +++ b/crates/pop-contracts/src/errors.rs @@ -3,13 +3,14 @@ use pop_common::sourcing::Error as SourcingError; use thiserror::Error; +/// Represents the various errors that can occur in the crate. #[derive(Error, Debug)] #[allow(clippy::enum_variant_names)] pub enum Error { - #[error("Anyhow error: {0}")] - AnyhowError(#[from] anyhow::Error), #[error("Failed to parse account address: {0}")] AccountAddressParsing(String), + #[error("Anyhow error: {0}")] + AnyhowError(#[from] anyhow::Error), #[error("Failed to parse balance: {0}")] BalanceParsing(String), #[error("{0}")] @@ -38,8 +39,6 @@ pub enum Error { InvalidName(String), #[error("IO error: {0}")] IO(#[from] std::io::Error), - #[error("Failed to create keypair from URI: {0}")] - KeyPairCreation(String), #[error("Failed to get manifest path: {0}")] ManifestPath(String), #[error("Argument {0} is required")] @@ -48,8 +47,6 @@ pub enum Error { NewContract(String), #[error("ParseError error: {0}")] ParseError(#[from] url::ParseError), - #[error("Failed to parse secret URI: {0}")] - ParseSecretURI(String), #[error("The `Repository` property is missing from the template variant")] RepositoryMissing, #[error("Sourcing error {0}")] diff --git a/crates/pop-contracts/src/lib.rs b/crates/pop-contracts/src/lib.rs index 005ec91ac..c5e76e2f1 100644 --- a/crates/pop-contracts/src/lib.rs +++ b/crates/pop-contracts/src/lib.rs @@ -20,13 +20,12 @@ pub use new::{create_smart_contract, is_valid_contract_name}; pub use node::{contracts_node_generator, is_chain_alive, run_contracts_node}; pub use templates::{Contract, ContractType}; pub use test::{test_e2e_smart_contract, test_smart_contract}; -pub use testing::{find_free_port, mock_build_process, new_environment}; +pub use testing::{mock_build_process, new_environment}; pub use up::{ dry_run_gas_estimate_instantiate, dry_run_upload, instantiate_smart_contract, set_up_deployment, set_up_upload, upload_smart_contract, UpOpts, }; pub use utils::{ - helpers::parse_account, metadata::{get_messages, ContractFunction}, - signer::parse_hex_bytes, + parse_account, parse_hex_bytes, }; diff --git a/crates/pop-contracts/src/new.rs b/crates/pop-contracts/src/new.rs index 8e11fbd16..6d4d7f125 100644 --- a/crates/pop-contracts/src/new.rs +++ b/crates/pop-contracts/src/new.rs @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-3.0 -use crate::{errors::Error, utils::helpers::canonicalized_path, Contract}; +use crate::{errors::Error, utils::canonicalized_path, Contract}; use anyhow::Result; use contract_build::new_contract_project; use heck::ToUpperCamelCase; diff --git a/crates/pop-contracts/src/node/mod.rs b/crates/pop-contracts/src/node/mod.rs index 77ac97cd5..be5680a04 100644 --- a/crates/pop-contracts/src/node/mod.rs +++ b/crates/pop-contracts/src/node/mod.rs @@ -166,10 +166,9 @@ fn release_directory_by_target(tag: Option<&str>) -> Result<&'static str, Error> #[cfg(test)] mod tests { - use crate::testing::find_free_port; - use super::*; use anyhow::{Error, Result}; + use pop_common::find_free_port; use std::process::Command; #[tokio::test] diff --git a/crates/pop-contracts/src/testing.rs b/crates/pop-contracts/src/testing.rs index c10bd4e5f..6a8abfcd9 100644 --- a/crates/pop-contracts/src/testing.rs +++ b/crates/pop-contracts/src/testing.rs @@ -4,7 +4,6 @@ use crate::{create_smart_contract, Contract}; use anyhow::Result; use std::{ fs::{copy, create_dir}, - net::TcpListener, path::Path, }; @@ -38,12 +37,3 @@ where copy(metadata_file, target_contract_dir.join("ink/testing.json"))?; Ok(()) } - -/// Finds an available port by binding to port 0 and retrieving the assigned port. -pub fn find_free_port() -> u16 { - TcpListener::bind("127.0.0.1:0") - .expect("Failed to bind to an available port") - .local_addr() - .expect("Failed to retrieve local address") - .port() -} diff --git a/crates/pop-contracts/src/up.rs b/crates/pop-contracts/src/up.rs index 90e0b35a3..bed7dfa45 100644 --- a/crates/pop-contracts/src/up.rs +++ b/crates/pop-contracts/src/up.rs @@ -2,9 +2,9 @@ use crate::{ errors::Error, utils::{ - helpers::{get_manifest_path, parse_balance}, + get_manifest_path, metadata::{process_function_args, FunctionType}, - signer::create_signer, + parse_balance, }, }; use contract_extrinsics::{ @@ -12,11 +12,10 @@ use contract_extrinsics::{ TokenMetadata, UploadCommandBuilder, UploadExec, }; use ink_env::{DefaultEnvironment, Environment}; +use pop_common::{create_signer, DefaultConfig, Keypair}; use sp_core::Bytes; use sp_weights::Weight; use std::{fmt::Write, path::PathBuf}; -use subxt::PolkadotConfig as DefaultConfig; -use subxt_signer::sr25519::Keypair; /// Attributes for the `up` command #[derive(Debug, PartialEq)] @@ -224,10 +223,10 @@ mod tests { use super::*; use crate::{ contracts_node_generator, errors::Error, mock_build_process, new_environment, - run_contracts_node, testing::find_free_port, + run_contracts_node, }; use anyhow::Result; - use pop_common::set_executable_permission; + use pop_common::{find_free_port, set_executable_permission}; use std::{env, process::Command, time::Duration}; use tokio::time::sleep; use url::Url; diff --git a/crates/pop-contracts/src/utils/helpers.rs b/crates/pop-contracts/src/utils/helpers.rs deleted file mode 100644 index d0797015c..000000000 --- a/crates/pop-contracts/src/utils/helpers.rs +++ /dev/null @@ -1,112 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 - -use crate::errors::Error; -use contract_build::ManifestPath; -use contract_extrinsics::BalanceVariant; -use ink_env::{DefaultEnvironment, Environment}; -use std::{ - path::{Path, PathBuf}, - str::FromStr, -}; -use subxt::{Config, PolkadotConfig as DefaultConfig}; - -pub fn get_manifest_path(path: Option<&Path>) -> Result { - if let Some(path) = path { - let full_path = PathBuf::from(path.to_string_lossy().to_string() + "/Cargo.toml"); - ManifestPath::try_from(Some(full_path)) - .map_err(|e| Error::ManifestPath(format!("Failed to get manifest path: {}", e))) - } else { - ManifestPath::try_from(path.as_ref()) - .map_err(|e| Error::ManifestPath(format!("Failed to get manifest path: {}", e))) - } -} - -pub fn parse_balance( - balance: &str, -) -> Result::Balance>, Error> { - BalanceVariant::from_str(balance).map_err(|e| Error::BalanceParsing(format!("{}", e))) -} - -pub fn parse_account(account: &str) -> Result<::AccountId, Error> { - ::AccountId::from_str(account) - .map_err(|e| Error::AccountAddressParsing(format!("{}", e))) -} - -/// Canonicalizes the given path to ensure consistency and resolve any symbolic links. -/// -/// # Arguments -/// -/// * `target` - A reference to the `Path` to be canonicalized. -pub fn canonicalized_path(target: &Path) -> Result { - // Canonicalize the target path to ensure consistency and resolve any symbolic links. - target - .canonicalize() - // If an I/O error occurs during canonicalization, convert it into an Error enum variant. - .map_err(Error::IO) -} - -#[cfg(test)] -mod tests { - use super::*; - use anyhow::{Error, Result}; - use std::fs; - - fn setup_test_environment() -> Result { - let temp_dir = tempfile::tempdir().expect("Could not create temp dir"); - let temp_contract_dir = temp_dir.path().join("test_contract"); - fs::create_dir(&temp_contract_dir)?; - crate::create_smart_contract( - "test_contract", - temp_contract_dir.as_path(), - &crate::Contract::Standard, - )?; - Ok(temp_dir) - } - - #[test] - fn test_get_manifest_path() -> Result<(), Error> { - let temp_dir = setup_test_environment()?; - get_manifest_path(Some(&PathBuf::from(temp_dir.path().join("test_contract"))))?; - Ok(()) - } - - #[test] - fn test_canonicalized_path() -> Result<(), Error> { - let temp_dir = tempfile::tempdir()?; - // Error case - let error_directory = canonicalized_path(&temp_dir.path().join("my_directory")); - assert!(error_directory.is_err()); - // Success case - canonicalized_path(temp_dir.path())?; - Ok(()) - } - - #[test] - fn parse_balance_works() -> Result<(), Error> { - let balance = parse_balance("100000")?; - assert_eq!(balance, BalanceVariant::Default(100000)); - Ok(()) - } - - #[test] - fn parse_balance_fails_wrong_balance() -> Result<(), Error> { - assert!(matches!(parse_balance("wrongbalance"), Err(super::Error::BalanceParsing(..)))); - Ok(()) - } - - #[test] - fn parse_account_works() -> Result<(), Error> { - let account = parse_account("5CLPm1CeUvJhZ8GCDZCR7nWZ2m3XXe4X5MtAQK69zEjut36A")?; - assert_eq!(account.to_string(), "5CLPm1CeUvJhZ8GCDZCR7nWZ2m3XXe4X5MtAQK69zEjut36A"); - Ok(()) - } - - #[test] - fn parse_account_fails_wrong_value() -> Result<(), Error> { - assert!(matches!( - parse_account("wrongaccount"), - Err(super::Error::AccountAddressParsing(..)) - )); - Ok(()) - } -} diff --git a/crates/pop-contracts/src/utils/metadata.rs b/crates/pop-contracts/src/utils/metadata.rs index 38cdc12d7..111468f0e 100644 --- a/crates/pop-contracts/src/utils/metadata.rs +++ b/crates/pop-contracts/src/utils/metadata.rs @@ -3,7 +3,8 @@ use crate::errors::Error; use contract_extrinsics::ContractArtifacts; use contract_transcode::ink_metadata::MessageParamSpec; -use scale_info::{form::PortableForm, PortableRegistry, Type, TypeDef, TypeDefPrimitive}; +use pop_common::format_type; +use scale_info::{form::PortableForm, PortableRegistry}; use std::path::Path; /// Describes a parameter. @@ -156,150 +157,6 @@ fn process_args( args } -// Formats a specified type, using the registry to output its full type representation. -fn format_type(ty: &Type, registry: &PortableRegistry) -> String { - let mut name = ty - .path - .segments - .last() - .map(|s| s.to_owned()) - .unwrap_or_else(|| ty.path.to_string()); - - if !ty.type_params.is_empty() { - let params: Vec<_> = ty - .type_params - .iter() - .filter_map(|p| registry.resolve(p.ty.unwrap().id)) - .map(|t| format_type(t, registry)) - .collect(); - name = format!("{name}<{}>", params.join(",")); - } - - name = format!( - "{name}{}", - match &ty.type_def { - TypeDef::Composite(composite) => { - if composite.fields.is_empty() { - return "".to_string(); - } - - let mut named = false; - let fields: Vec<_> = composite - .fields - .iter() - .filter_map(|f| match f.name.as_ref() { - None => registry.resolve(f.ty.id).map(|t| format_type(t, registry)), - Some(field) => { - named = true; - f.type_name.as_ref().map(|t| format!("{field}: {t}")) - }, - }) - .collect(); - match named { - true => format!(" {{ {} }}", fields.join(", ")), - false => format!(" ({})", fields.join(", ")), - } - }, - TypeDef::Variant(variant) => { - let variants: Vec<_> = variant - .variants - .iter() - .map(|v| { - if v.fields.is_empty() { - return v.name.clone(); - } - - let name = v.name.as_str(); - let mut named = false; - let fields: Vec<_> = v - .fields - .iter() - .filter_map(|f| match f.name.as_ref() { - None => registry.resolve(f.ty.id).map(|t| format_type(t, registry)), - Some(field) => { - named = true; - f.type_name.as_ref().map(|t| format!("{field}: {t}")) - }, - }) - .collect(); - format!( - "{name}{}", - match named { - true => format!("{{ {} }}", fields.join(", ")), - false => format!("({})", fields.join(", ")), - } - ) - }) - .collect(); - format!(": {}", variants.join(", ")) - }, - TypeDef::Sequence(sequence) => { - format!( - "[{}]", - format_type( - registry.resolve(sequence.type_param.id).expect("sequence type not found"), - registry - ) - ) - }, - TypeDef::Array(array) => { - format!( - "[{};{}]", - format_type( - registry.resolve(array.type_param.id).expect("array type not found"), - registry - ), - array.len - ) - }, - TypeDef::Tuple(tuple) => { - let fields: Vec<_> = tuple - .fields - .iter() - .filter_map(|p| registry.resolve(p.id)) - .map(|t| format_type(t, registry)) - .collect(); - format!("({})", fields.join(",")) - }, - TypeDef::Primitive(primitive) => { - use TypeDefPrimitive::*; - match primitive { - Bool => "bool", - Char => "char", - Str => "str", - U8 => "u8", - U16 => "u16", - U32 => "u32", - U64 => "u64", - U128 => "u128", - U256 => "u256", - I8 => "i8", - I16 => "i16", - I32 => "i32", - I64 => "i64", - I128 => "i128", - I256 => "i256", - } - .to_string() - }, - TypeDef::Compact(compact) => { - format!( - "Compact<{}>", - format_type( - registry.resolve(compact.type_param.id).expect("compact type not found"), - registry - ) - ) - }, - TypeDef::BitSequence(_) => { - unimplemented!("bit sequence not currently supported") - }, - } - ); - - name -} - /// Processes a list of argument values for a specified contract function, /// wrapping each value in `Some(...)` or replacing it with `None` if the argument is optional. /// diff --git a/crates/pop-contracts/src/utils/mod.rs b/crates/pop-contracts/src/utils/mod.rs index ad49e2dc6..a3a99323b 100644 --- a/crates/pop-contracts/src/utils/mod.rs +++ b/crates/pop-contracts/src/utils/mod.rs @@ -1,5 +1,149 @@ // SPDX-License-Identifier: GPL-3.0 -pub mod helpers; +use crate::errors::Error; +use contract_build::{util::decode_hex, ManifestPath}; +use contract_extrinsics::BalanceVariant; +use ink_env::{DefaultEnvironment, Environment}; +use pop_common::{Config, DefaultConfig}; +use sp_core::Bytes; +use std::{ + path::{Path, PathBuf}, + str::FromStr, +}; + pub mod metadata; -pub mod signer; + +/// Retrieves the manifest path for a contract project. +/// +/// # Arguments +/// * `path` - An optional path to the project directory. +pub fn get_manifest_path(path: Option<&Path>) -> Result { + if let Some(path) = path { + let full_path = PathBuf::from(path.to_string_lossy().to_string() + "/Cargo.toml"); + ManifestPath::try_from(Some(full_path)) + .map_err(|e| Error::ManifestPath(format!("Failed to get manifest path: {}", e))) + } else { + ManifestPath::try_from(path.as_ref()) + .map_err(|e| Error::ManifestPath(format!("Failed to get manifest path: {}", e))) + } +} + +/// Parses a balance value from a string representation. +/// +/// # Arguments +/// * `balance` - A string representing the balance value to parse. +pub fn parse_balance( + balance: &str, +) -> Result::Balance>, Error> { + BalanceVariant::from_str(balance).map_err(|e| Error::BalanceParsing(format!("{}", e))) +} + +/// Parses an account ID from its string representation. +/// +/// # Arguments +/// * `account` - A string representing the account ID to parse. +pub fn parse_account(account: &str) -> Result<::AccountId, Error> { + ::AccountId::from_str(account) + .map_err(|e| Error::AccountAddressParsing(format!("{}", e))) +} + +/// Parse hex encoded bytes. +/// +/// # Arguments +/// * `input` - A string containing hex-encoded bytes. +pub fn parse_hex_bytes(input: &str) -> Result { + let bytes = decode_hex(input).map_err(|e| Error::HexParsing(format!("{}", e)))?; + Ok(bytes.into()) +} + +/// Canonicalizes the given path to ensure consistency and resolve any symbolic links. +/// +/// # Arguments +/// * `target` - A reference to the `Path` to be canonicalized. +pub fn canonicalized_path(target: &Path) -> Result { + // Canonicalize the target path to ensure consistency and resolve any symbolic links. + target + .canonicalize() + // If an I/O error occurs during canonicalization, convert it into an Error enum variant. + .map_err(Error::IO) +} + +#[cfg(test)] +mod tests { + use super::*; + use anyhow::Result; + use std::fs; + + fn setup_test_environment() -> Result { + let temp_dir = tempfile::tempdir().expect("Could not create temp dir"); + let temp_contract_dir = temp_dir.path().join("test_contract"); + fs::create_dir(&temp_contract_dir)?; + crate::create_smart_contract( + "test_contract", + temp_contract_dir.as_path(), + &crate::Contract::Standard, + )?; + Ok(temp_dir) + } + + #[test] + fn test_get_manifest_path() -> Result<(), Error> { + let temp_dir = setup_test_environment()?; + get_manifest_path(Some(&PathBuf::from(temp_dir.path().join("test_contract"))))?; + Ok(()) + } + + #[test] + fn test_canonicalized_path() -> Result<(), Error> { + let temp_dir = tempfile::tempdir()?; + // Error case + let error_directory = canonicalized_path(&temp_dir.path().join("my_directory")); + assert!(error_directory.is_err()); + // Success case + canonicalized_path(temp_dir.path())?; + Ok(()) + } + + #[test] + fn parse_balance_works() -> Result<(), Error> { + let balance = parse_balance("100000")?; + assert_eq!(balance, BalanceVariant::Default(100000)); + Ok(()) + } + + #[test] + fn parse_balance_fails_wrong_balance() -> Result<(), Error> { + assert!(matches!(parse_balance("wrongbalance"), Err(super::Error::BalanceParsing(..)))); + Ok(()) + } + + #[test] + fn parse_account_works() -> Result<(), Error> { + let account = parse_account("5CLPm1CeUvJhZ8GCDZCR7nWZ2m3XXe4X5MtAQK69zEjut36A")?; + assert_eq!(account.to_string(), "5CLPm1CeUvJhZ8GCDZCR7nWZ2m3XXe4X5MtAQK69zEjut36A"); + Ok(()) + } + + #[test] + fn parse_account_fails_wrong_value() -> Result<(), Error> { + assert!(matches!( + parse_account("wrongaccount"), + Err(super::Error::AccountAddressParsing(..)) + )); + Ok(()) + } + + #[test] + fn parse_hex_bytes_works() -> Result<(), Error> { + let input_in_hex = "48656c6c6f"; + let result = parse_hex_bytes(input_in_hex)?; + assert_eq!(result, Bytes(vec![72, 101, 108, 108, 111])); + Ok(()) + } + + #[test] + fn parse_hex_bytes_fails_wrong_input() -> Result<()> { + assert!(matches!(parse_hex_bytes("wronghexvalue"), Err(Error::HexParsing(..)))); + Ok(()) + } +} diff --git a/crates/pop-parachains/Cargo.toml b/crates/pop-parachains/Cargo.toml index dd6406824..64bef2d67 100644 --- a/crates/pop-parachains/Cargo.toml +++ b/crates/pop-parachains/Cargo.toml @@ -24,8 +24,12 @@ tokio.workspace = true url.workspace = true askama.workspace = true +hex.workspace = true indexmap.workspace = true reqwest.workspace = true +scale-info.workspace = true +scale-value.workspace = true +subxt.workspace = true symlink.workspace = true toml_edit.workspace = true walkdir.workspace = true diff --git a/crates/pop-parachains/src/call/metadata/action.rs b/crates/pop-parachains/src/call/metadata/action.rs new file mode 100644 index 000000000..cd47e1279 --- /dev/null +++ b/crates/pop-parachains/src/call/metadata/action.rs @@ -0,0 +1,207 @@ +// SPDX-License-Identifier: GPL-3.0 + +use super::{find_dispatchable_by_name, Pallet}; +use strum::{EnumMessage as _, EnumProperty as _, VariantArray as _}; +use strum_macros::{AsRefStr, Display, EnumMessage, EnumProperty, EnumString, VariantArray}; + +/// Enum representing various predefined actions supported. +#[derive( + AsRefStr, + Clone, + Debug, + Display, + EnumMessage, + EnumString, + EnumProperty, + Eq, + Hash, + PartialEq, + VariantArray, +)] +pub enum Action { + /// Transfer balance. + #[strum( + serialize = "transfer", + message = "transfer_allow_death", + detailed_message = "Transfer balance", + props(Pallet = "Balances") + )] + Transfer, + /// Create an asset. + #[strum( + serialize = "create", + message = "create", + detailed_message = "Create an asset", + props(Pallet = "Assets") + )] + CreateAsset, + /// Mint an asset. + #[strum( + serialize = "mint", + message = "mint", + detailed_message = "Mint an asset", + props(Pallet = "Assets") + )] + MintAsset, + /// Create an NFT collection. + #[strum( + serialize = "create_nft", + message = "create", + detailed_message = "Create an NFT collection", + props(Pallet = "Nfts") + )] + CreateCollection, + /// Mint an NFT. + #[strum( + serialize = "mint_nft", + message = "mint", + detailed_message = "Mint an NFT", + props(Pallet = "Nfts") + )] + MintNFT, + /// Purchase on-demand coretime. + #[strum( + serialize = "place_order_allow_death", + message = "place_order_allow_death", + detailed_message = "Purchase on-demand coretime", + props(Pallet = "OnDemand") + )] + PurchaseOnDemandCoretime, + /// Reserve a parachain ID. + #[strum( + serialize = "reserve", + message = "reserve", + detailed_message = "Reserve a parachain ID", + props(Pallet = "Registrar") + )] + Reserve, + /// Register a parachain ID with genesis state and code. + #[strum( + serialize = "register", + message = "register", + detailed_message = "Register a parachain ID with genesis state and code", + props(Pallet = "Registrar") + )] + Register, +} + +impl Action { + /// Get the description of the action. + pub fn description(&self) -> &str { + self.get_detailed_message().unwrap_or_default() + } + + /// Get the dispatchable function name corresponding to the action. + pub fn function_name(&self) -> &str { + self.get_message().unwrap_or_default() + } + + /// Get the associated pallet name for the action. + pub fn pallet_name(&self) -> &str { + self.get_str("Pallet").unwrap_or_default() + } +} + +/// Fetch the list of supported actions based on available pallets. +/// +/// # Arguments +/// * `pallets`: Supported pallets. +pub fn supported_actions(pallets: &[Pallet]) -> Vec { + let mut actions = Vec::new(); + for action in Action::VARIANTS.iter() { + if find_dispatchable_by_name(pallets, action.pallet_name(), action.function_name()).is_ok() + { + actions.push(action.clone()); + } + } + actions +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{call::tests::POP_NETWORK_TESTNET_URL, parse_chain_metadata, set_up_client}; + use anyhow::Result; + use std::collections::HashMap; + + const POLKADOT_NETWORK_URL: &str = "wss://polkadot-rpc.publicnode.com"; + + #[test] + fn action_descriptions_are_correct() { + let descriptions = HashMap::from([ + (Action::CreateAsset, "Create an asset"), + (Action::MintAsset, "Mint an asset"), + (Action::CreateCollection, "Create an NFT collection"), + (Action::MintNFT, "Mint an NFT"), + (Action::PurchaseOnDemandCoretime, "Purchase on-demand coretime"), + (Action::Transfer, "Transfer balance"), + (Action::Register, "Register a parachain ID with genesis state and code"), + (Action::Reserve, "Reserve a parachain ID"), + ]); + + for action in Action::VARIANTS.iter() { + assert_eq!(&action.description(), descriptions.get(action).unwrap()); + } + } + + #[test] + fn pallet_names_are_correct() { + let pallets = HashMap::from([ + (Action::CreateAsset, "Assets"), + (Action::MintAsset, "Assets"), + (Action::CreateCollection, "Nfts"), + (Action::MintNFT, "Nfts"), + (Action::PurchaseOnDemandCoretime, "OnDemand"), + (Action::Transfer, "Balances"), + (Action::Register, "Registrar"), + (Action::Reserve, "Registrar"), + ]); + + for action in Action::VARIANTS.iter() { + assert_eq!(&action.pallet_name(), pallets.get(action).unwrap(),); + } + } + + #[test] + fn function_names_are_correct() { + let pallets = HashMap::from([ + (Action::CreateAsset, "create"), + (Action::MintAsset, "mint"), + (Action::CreateCollection, "create"), + (Action::MintNFT, "mint"), + (Action::PurchaseOnDemandCoretime, "place_order_allow_death"), + (Action::Transfer, "transfer_allow_death"), + (Action::Register, "register"), + (Action::Reserve, "reserve"), + ]); + + for action in Action::VARIANTS.iter() { + assert_eq!(&action.function_name(), pallets.get(action).unwrap(),); + } + } + + #[tokio::test] + async fn supported_actions_works() -> Result<()> { + // Test Pop Parachain. + let mut client: subxt::OnlineClient = + set_up_client(POP_NETWORK_TESTNET_URL).await?; + let mut actions = supported_actions(&parse_chain_metadata(&client)?); + assert_eq!(actions.len(), 5); + assert_eq!(actions[0], Action::Transfer); + assert_eq!(actions[1], Action::CreateAsset); + assert_eq!(actions[2], Action::MintAsset); + assert_eq!(actions[3], Action::CreateCollection); + assert_eq!(actions[4], Action::MintNFT); + + // Test Polkadot Relay Chain. + client = set_up_client(POLKADOT_NETWORK_URL).await?; + actions = supported_actions(&parse_chain_metadata(&client)?); + assert_eq!(actions.len(), 4); + assert_eq!(actions[0], Action::Transfer); + assert_eq!(actions[1], Action::PurchaseOnDemandCoretime); + assert_eq!(actions[2], Action::Reserve); + assert_eq!(actions[3], Action::Register); + + Ok(()) + } +} diff --git a/crates/pop-parachains/src/call/metadata/mod.rs b/crates/pop-parachains/src/call/metadata/mod.rs new file mode 100644 index 000000000..1f53f523c --- /dev/null +++ b/crates/pop-parachains/src/call/metadata/mod.rs @@ -0,0 +1,334 @@ +// SPDX-License-Identifier: GPL-3.0 + +use crate::errors::Error; +use params::Param; +use scale_value::stringify::custom_parsers; +use std::fmt::{Display, Formatter}; +use subxt::{dynamic::Value, Metadata, OnlineClient, SubstrateConfig}; + +pub mod action; +pub mod params; + +/// Represents a pallet in the blockchain, including its dispatchable functions. +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct Pallet { + /// The name of the pallet. + pub name: String, + /// The index of the pallet within the runtime. + pub index: u8, + /// The documentation of the pallet. + pub docs: String, + /// The dispatchable functions of the pallet. + pub functions: Vec, +} + +impl Display for Pallet { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.name) + } +} + +/// Represents a dispatchable function. +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct Function { + /// The pallet containing the dispatchable function. + pub pallet: String, + /// The name of the function. + pub name: String, + /// The index of the function within the pallet. + pub index: u8, + /// The documentation of the function. + pub docs: String, + /// The parameters of the function. + pub params: Vec, + /// Whether this function is supported (no recursive or unsupported types like `RuntimeCall`). + pub is_supported: bool, +} + +impl Display for Function { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.name) + } +} + +/// Parses the chain metadata to extract information about pallets and their dispatchable functions. +/// +/// # Arguments +/// * `client`: The client to interact with the chain. +/// +/// NOTE: pallets are ordered by their index within the runtime by default. +pub fn parse_chain_metadata(client: &OnlineClient) -> Result, Error> { + let metadata: Metadata = client.metadata(); + + let pallets = metadata + .pallets() + .map(|pallet| { + let functions = pallet + .call_variants() + .map(|variants| { + variants + .iter() + .map(|variant| { + let mut is_supported = true; + + // Parse parameters for the dispatchable function. + let params = { + let mut parsed_params = Vec::new(); + for field in &variant.fields { + match params::field_to_param(&metadata, field) { + Ok(param) => parsed_params.push(param), + Err(_) => { + // If an error occurs while parsing the values, mark the + // dispatchable function as unsupported rather than + // error. + is_supported = false; + parsed_params.clear(); + break; + }, + } + } + parsed_params + }; + + Ok(Function { + pallet: pallet.name().to_string(), + name: variant.name.clone(), + index: variant.index, + docs: if is_supported { + // Filter out blank lines and then flatten into a single value. + variant + .docs + .iter() + .filter(|l| !l.is_empty()) + .cloned() + .collect::>() + .join(" ") + } else { + // To display the message in the UI + "Function Not Supported".to_string() + }, + params, + is_supported, + }) + }) + .collect::, Error>>() + }) + .unwrap_or_else(|| Ok(vec![]))?; + + Ok(Pallet { + name: pallet.name().to_string(), + index: pallet.index(), + docs: pallet.docs().join(" "), + functions, + }) + }) + .collect::, Error>>()?; + + Ok(pallets) +} + +/// Finds a specific pallet by name and retrieves its details from metadata. +/// +/// # Arguments +/// * `pallets`: List of pallets available within the chain's runtime. +/// * `pallet_name`: The name of the pallet to find. +pub fn find_pallet_by_name<'a>( + pallets: &'a [Pallet], + pallet_name: &str, +) -> Result<&'a Pallet, Error> { + if let Some(pallet) = pallets.iter().find(|p| p.name == pallet_name) { + Ok(pallet) + } else { + Err(Error::PalletNotFound(pallet_name.to_string())) + } +} + +/// Finds a specific dispatchable function by name and retrieves its details from metadata. +/// +/// # Arguments +/// * `pallets`: List of pallets available within the chain's runtime. +/// * `pallet_name`: The name of the pallet. +/// * `function_name`: Name of the dispatchable function to locate. +pub fn find_dispatchable_by_name<'a>( + pallets: &'a [Pallet], + pallet_name: &str, + function_name: &str, +) -> Result<&'a Function, Error> { + let pallet = find_pallet_by_name(pallets, pallet_name)?; + if let Some(function) = pallet.functions.iter().find(|&e| e.name == function_name) { + Ok(function) + } else { + Err(Error::FunctionNotSupported) + } +} + +/// Parses and processes raw string parameter values for a dispatchable function, mapping them to +/// `Value` types. +/// +/// # Arguments +/// * `params`: The metadata definition for each parameter of the corresponding dispatchable +/// function. +/// * `raw_params`: A vector of raw string arguments for the dispatchable function. +pub fn parse_dispatchable_arguments( + params: &[Param], + raw_params: Vec, +) -> Result, Error> { + params + .iter() + .zip(raw_params) + .map(|(param, raw_param)| { + // Convert sequence parameters to hex if is_sequence + let processed_param = if param.is_sequence && !raw_param.starts_with("0x") { + format!("0x{}", hex::encode(raw_param)) + } else { + raw_param + }; + scale_value::stringify::from_str_custom() + .add_custom_parser(custom_parsers::parse_hex) + .add_custom_parser(custom_parsers::parse_ss58) + .parse(&processed_param) + .0 + .map_err(|_| Error::ParamProcessingError) + }) + .collect() +} + +#[cfg(test)] +mod tests { + use super::*; + + use crate::{call::tests::POP_NETWORK_TESTNET_URL, set_up_client}; + use anyhow::Result; + use subxt::ext::scale_bits; + + #[tokio::test] + async fn parse_chain_metadata_works() -> Result<()> { + let client = set_up_client(POP_NETWORK_TESTNET_URL).await?; + let pallets = parse_chain_metadata(&client)?; + // Test the first pallet is parsed correctly + let first_pallet = pallets.first().unwrap(); + assert_eq!(first_pallet.name, "System"); + assert_eq!(first_pallet.index, 0); + assert_eq!(first_pallet.docs, ""); + assert_eq!(first_pallet.functions.len(), 11); + let first_function = first_pallet.functions.first().unwrap(); + assert_eq!(first_function.name, "remark"); + assert_eq!(first_function.index, 0); + assert_eq!( + first_function.docs, + "Make some on-chain remark. Can be executed by every `origin`." + ); + assert!(first_function.is_supported); + assert_eq!(first_function.params.first().unwrap().name, "remark"); + assert_eq!(first_function.params.first().unwrap().type_name, "[u8]"); + assert_eq!(first_function.params.first().unwrap().sub_params.len(), 0); + assert!(!first_function.params.first().unwrap().is_optional); + assert!(!first_function.params.first().unwrap().is_tuple); + assert!(!first_function.params.first().unwrap().is_variant); + assert!(first_function.params.first().unwrap().is_sequence); + Ok(()) + } + + #[tokio::test] + async fn find_pallet_by_name_works() -> Result<()> { + let client = set_up_client(POP_NETWORK_TESTNET_URL).await?; + let pallets = parse_chain_metadata(&client)?; + assert!(matches!( + find_pallet_by_name(&pallets, "WrongName"), + Err(Error::PalletNotFound(pallet)) if pallet == "WrongName".to_string())); + let pallet = find_pallet_by_name(&pallets, "Balances")?; + assert_eq!(pallet.name, "Balances"); + assert_eq!(pallet.functions.len(), 9); + Ok(()) + } + + #[tokio::test] + async fn find_dispatchable_by_name_works() -> Result<()> { + let client = set_up_client(POP_NETWORK_TESTNET_URL).await?; + let pallets = parse_chain_metadata(&client)?; + assert!(matches!( + find_dispatchable_by_name(&pallets, "WrongName", "wrong_name"), + Err(Error::PalletNotFound(pallet)) if pallet == "WrongName".to_string())); + assert!(matches!( + find_dispatchable_by_name(&pallets, "Balances", "wrong_name"), + Err(Error::FunctionNotSupported) + )); + let function = find_dispatchable_by_name(&pallets, "Balances", "force_transfer")?; + assert_eq!(function.name, "force_transfer"); + assert_eq!(function.docs, "Exactly as `transfer_allow_death`, except the origin must be root and the source account may be specified."); + assert_eq!(function.is_supported, true); + assert_eq!(function.params.len(), 3); + Ok(()) + } + + #[test] + fn parse_dispatchable_arguments_works() -> Result<()> { + // Values for testing from: https://docs.rs/scale-value/0.18.0/scale_value/stringify/fn.from_str.html + // and https://docs.rs/scale-value/0.18.0/scale_value/stringify/fn.from_str_custom.html + let args = [ + "1".to_string(), + "-1".to_string(), + "true".to_string(), + "'a'".to_string(), + "\"hi\"".to_string(), + "{ a: true, b: \"hello\" }".to_string(), + "MyVariant { a: true, b: \"hello\" }".to_string(), + "<0101>".to_string(), + "(1,2,0x030405)".to_string(), + r#"{ + name: "Alice", + address: 5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty + }"# + .to_string(), + ] + .to_vec(); + let addr: Vec<_> = + hex::decode("8eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48") + .unwrap() + .into_iter() + .map(|b| Value::u128(b as u128)) + .collect(); + // Define mock dispatchable function parameters for testing. + let params = vec![ + Param { type_name: "u128".to_string(), ..Default::default() }, + Param { type_name: "i128".to_string(), ..Default::default() }, + Param { type_name: "bool".to_string(), ..Default::default() }, + Param { type_name: "char".to_string(), ..Default::default() }, + Param { type_name: "string".to_string(), ..Default::default() }, + Param { type_name: "compostie".to_string(), ..Default::default() }, + Param { type_name: "variant".to_string(), is_variant: true, ..Default::default() }, + Param { type_name: "bit_sequence".to_string(), ..Default::default() }, + Param { type_name: "tuple".to_string(), is_tuple: true, ..Default::default() }, + Param { type_name: "composite".to_string(), ..Default::default() }, + ]; + assert_eq!( + parse_dispatchable_arguments(¶ms, args)?, + [ + Value::u128(1), + Value::i128(-1), + Value::bool(true), + Value::char('a'), + Value::string("hi"), + Value::named_composite(vec![ + ("a", Value::bool(true)), + ("b", Value::string("hello")) + ]), + Value::named_variant( + "MyVariant", + vec![("a", Value::bool(true)), ("b", Value::string("hello"))] + ), + Value::bit_sequence(scale_bits::Bits::from_iter([false, true, false, true])), + Value::unnamed_composite(vec![ + Value::u128(1), + Value::u128(2), + Value::unnamed_composite(vec![Value::u128(3), Value::u128(4), Value::u128(5),]) + ]), + Value::named_composite(vec![ + ("name", Value::string("Alice")), + ("address", Value::unnamed_composite(addr)) + ]) + ] + ); + Ok(()) + } +} diff --git a/crates/pop-parachains/src/call/metadata/params.rs b/crates/pop-parachains/src/call/metadata/params.rs new file mode 100644 index 000000000..9d8521f9c --- /dev/null +++ b/crates/pop-parachains/src/call/metadata/params.rs @@ -0,0 +1,238 @@ +// SPDX-License-Identifier: GPL-3.0 + +use crate::errors::Error; +use pop_common::format_type; +use scale_info::{form::PortableForm, Field, PortableRegistry, TypeDef}; +use subxt::Metadata; + +/// Describes a parameter of a dispatchable function. +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct Param { + /// The name of the parameter. + pub name: String, + /// The type of the parameter. + pub type_name: String, + /// Nested parameters for composite, variants, types or tuples. + pub sub_params: Vec, + /// Indicates if the parameter is optional (`Option`). + pub is_optional: bool, + /// Indicates if the parameter is a Tuple. + pub is_tuple: bool, + /// Indicates if the parameter is a Variant. + pub is_variant: bool, + /// Indicates if the parameter is a Sequence. + pub is_sequence: bool, +} + +/// Transforms a metadata field into its `Param` representation. +/// +/// # Arguments +/// * `metadata`: The chain metadata. +/// * `field`: A parameter of a dispatchable function (as [Field]). +pub fn field_to_param(metadata: &Metadata, field: &Field) -> Result { + let registry = metadata.types(); + if let Some(name) = field.type_name.as_deref() { + if name.contains("RuntimeCall") { + return Err(Error::FunctionNotSupported); + } + } + let name = field.name.as_deref().unwrap_or("Unnamed"); //It can be unnamed field + type_to_param(name, registry, field.ty.id) +} + +/// Converts a type's metadata into a `Param` representation. +/// +/// # Arguments +/// * `name`: The name of the parameter. +/// * `registry`: Type registry containing all types used in the metadata. +/// * `type_id`: The ID of the type to be converted. +fn type_to_param(name: &str, registry: &PortableRegistry, type_id: u32) -> Result { + let type_info = registry + .resolve(type_id) + .ok_or_else(|| Error::MetadataParsingError(name.to_string()))?; + for param in &type_info.type_params { + if param.name.contains("RuntimeCall") { + return Err(Error::FunctionNotSupported); + } + } + if type_info.path.segments == ["Option"] { + if let Some(sub_type_id) = type_info.type_params.first().and_then(|param| param.ty) { + // Recursive for the sub parameters + let sub_param = type_to_param(name, registry, sub_type_id.id)?; + Ok(Param { + name: name.to_string(), + type_name: sub_param.type_name, + sub_params: sub_param.sub_params, + is_optional: true, + ..Default::default() + }) + } else { + Err(Error::MetadataParsingError(name.to_string())) + } + } else { + // Determine the formatted type name. + let type_name = format_type(type_info, registry); + match &type_info.type_def { + TypeDef::Primitive(_) | TypeDef::Array(_) | TypeDef::Compact(_) => + Ok(Param { name: name.to_string(), type_name, ..Default::default() }), + TypeDef::Composite(composite) => { + let sub_params = composite + .fields + .iter() + .map(|field| { + // Recursive for the sub parameters of composite type. + type_to_param(field.name.as_deref().unwrap_or(name), registry, field.ty.id) + }) + .collect::, Error>>()?; + + Ok(Param { name: name.to_string(), type_name, sub_params, ..Default::default() }) + }, + TypeDef::Variant(variant) => { + let variant_params = variant + .variants + .iter() + .map(|variant_param| { + let variant_sub_params = variant_param + .fields + .iter() + .map(|field| { + // Recursive for the sub parameters of variant type. + type_to_param( + field.name.as_deref().unwrap_or(&variant_param.name), + registry, + field.ty.id, + ) + }) + .collect::, Error>>()?; + Ok(Param { + name: variant_param.name.clone(), + type_name: "".to_string(), + sub_params: variant_sub_params, + is_variant: true, + ..Default::default() + }) + }) + .collect::, Error>>()?; + + Ok(Param { + name: name.to_string(), + type_name, + sub_params: variant_params, + is_variant: true, + ..Default::default() + }) + }, + TypeDef::Sequence(_) => Ok(Param { + name: name.to_string(), + type_name, + is_sequence: true, + ..Default::default() + }), + TypeDef::Tuple(tuple) => { + let sub_params = tuple + .fields + .iter() + .enumerate() + .map(|(index, field_id)| { + type_to_param( + &format!("Index {index} of the tuple {name}"), + registry, + field_id.id, + ) + }) + .collect::, Error>>()?; + + Ok(Param { + name: name.to_string(), + type_name, + sub_params, + is_tuple: true, + ..Default::default() + }) + }, + _ => Err(Error::MetadataParsingError(name.to_string())), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{call::tests::POP_NETWORK_TESTNET_URL, set_up_client}; + use anyhow::Result; + + #[tokio::test] + async fn field_to_param_works() -> Result<()> { + let client = set_up_client(POP_NETWORK_TESTNET_URL).await?; + let metadata = client.metadata(); + // Test a supported dispatchable function. + let function = metadata + .pallet_by_name("Balances") + .unwrap() + .call_variant_by_name("force_transfer") + .unwrap(); + let mut params = Vec::new(); + for field in &function.fields { + params.push(field_to_param(&metadata, field)?) + } + assert_eq!(params.len(), 3); + assert_eq!(params.first().unwrap().name, "source"); + assert_eq!(params.first().unwrap().type_name, "MultiAddress: Id(AccountId32 ([u8;32])), Index(Compact<()>), Raw([u8]), Address32([u8;32]), Address20([u8;20])"); + assert_eq!(params.first().unwrap().sub_params.len(), 5); + assert_eq!(params.first().unwrap().sub_params.first().unwrap().name, "Id"); + assert_eq!(params.first().unwrap().sub_params.first().unwrap().type_name, ""); + assert_eq!( + params + .first() + .unwrap() + .sub_params + .first() + .unwrap() + .sub_params + .first() + .unwrap() + .name, + "Id" + ); + assert_eq!( + params + .first() + .unwrap() + .sub_params + .first() + .unwrap() + .sub_params + .first() + .unwrap() + .type_name, + "AccountId32 ([u8;32])" + ); + // Test some dispatchable functions that are not supported. + let function = + metadata.pallet_by_name("Sudo").unwrap().call_variant_by_name("sudo").unwrap(); + assert!(matches!( + field_to_param(&metadata, &function.fields.first().unwrap()), + Err(Error::FunctionNotSupported) + )); + let function = metadata + .pallet_by_name("Utility") + .unwrap() + .call_variant_by_name("batch") + .unwrap(); + assert!(matches!( + field_to_param(&metadata, &function.fields.first().unwrap()), + Err(Error::FunctionNotSupported) + )); + let function = metadata + .pallet_by_name("PolkadotXcm") + .unwrap() + .call_variant_by_name("execute") + .unwrap(); + assert!(matches!( + field_to_param(&metadata, &function.fields.first().unwrap()), + Err(Error::FunctionNotSupported) + )); + + Ok(()) + } +} diff --git a/crates/pop-parachains/src/call/mod.rs b/crates/pop-parachains/src/call/mod.rs new file mode 100644 index 000000000..be9b4f69f --- /dev/null +++ b/crates/pop-parachains/src/call/mod.rs @@ -0,0 +1,241 @@ +// SPDX-License-Identifier: GPL-3.0 + +use crate::{errors::Error, Function}; +use pop_common::create_signer; +use subxt::{ + dynamic::Value, + tx::{DynamicPayload, Payload}, + OnlineClient, SubstrateConfig, +}; + +pub mod metadata; + +/// Sets up an [OnlineClient] instance for connecting to a blockchain. +/// +/// # Arguments +/// * `url` - Endpoint of the node. +pub async fn set_up_client(url: &str) -> Result, Error> { + OnlineClient::::from_url(url) + .await + .map_err(|e| Error::ConnectionFailure(e.to_string())) +} + +/// Constructs a dynamic extrinsic payload for a specified dispatchable function. +/// +/// # Arguments +/// * `function` - A dispatchable function. +/// * `args` - A vector of string arguments to be passed to construct the extrinsic. +pub fn construct_extrinsic( + function: &Function, + args: Vec, +) -> Result { + let parsed_args: Vec = metadata::parse_dispatchable_arguments(&function.params, args)?; + Ok(subxt::dynamic::tx(function.pallet.clone(), function.name.clone(), parsed_args)) +} + +/// Constructs a Sudo extrinsic. +/// +/// # Arguments +/// * `xt`: The extrinsic representing the dispatchable function call to be dispatched with `Root` +/// privileges. +pub fn construct_sudo_extrinsic(xt: DynamicPayload) -> Result { + Ok(subxt::dynamic::tx("Sudo", "sudo", [xt.into_value()].to_vec())) +} + +/// Signs and submits a given extrinsic. +/// +/// # Arguments +/// * `client` - The client used to interact with the chain. +/// * `xt` - The extrinsic to be signed and submitted. +/// * `suri` - The secret URI (e.g., mnemonic or private key) for signing the extrinsic. +pub async fn sign_and_submit_extrinsic( + client: &OnlineClient, + xt: DynamicPayload, + suri: &str, +) -> Result { + let signer = create_signer(suri)?; + let result = client + .tx() + .sign_and_submit_then_watch_default(&xt, &signer) + .await + .map_err(|e| Error::ExtrinsicSubmissionError(format!("{:?}", e)))? + .wait_for_finalized_success() + .await + .map_err(|e| Error::ExtrinsicSubmissionError(format!("{:?}", e)))?; + Ok(format!("{:?}", result.extrinsic_hash())) +} + +/// Encodes the call data for a given extrinsic into a hexadecimal string. +/// +/// # Arguments +/// * `client` - The client used to interact with the chain. +/// * `xt` - The extrinsic whose call data will be encoded and returned. +pub fn encode_call_data( + client: &OnlineClient, + xt: &DynamicPayload, +) -> Result { + let call_data = xt + .encode_call_data(&client.metadata()) + .map_err(|e| Error::CallDataEncodingError(e.to_string()))?; + Ok(format!("0x{}", hex::encode(call_data))) +} + +/// Decodes a hex-encoded string into a vector of bytes representing the call data. +/// +/// # Arguments +/// * `call_data` - The hex-encoded string representing call data. +pub fn decode_call_data(call_data: &str) -> Result, Error> { + hex::decode(call_data.trim_start_matches("0x")) + .map_err(|e| Error::CallDataDecodingError(e.to_string())) +} + +// This struct implements the [`Payload`] trait and is used to submit +// pre-encoded SCALE call data directly, without the dynamic construction of transactions. +struct CallData(Vec); + +impl Payload for CallData { + fn encode_call_data_to( + &self, + _: &subxt::Metadata, + out: &mut Vec, + ) -> Result<(), subxt::ext::subxt_core::Error> { + out.extend_from_slice(&self.0); + Ok(()) + } +} + +/// Signs and submits a given extrinsic. +/// +/// # Arguments +/// * `client` - Reference to an `OnlineClient` connected to the chain. +/// * `call_data` - SCALE encoded bytes representing the extrinsic's call data. +/// * `suri` - The secret URI (e.g., mnemonic or private key) for signing the extrinsic. +pub async fn sign_and_submit_extrinsic_with_call_data( + client: &OnlineClient, + call_data: Vec, + suri: &str, +) -> Result { + let signer = create_signer(suri)?; + let payload = CallData(call_data); + let result = client + .tx() + .sign_and_submit_then_watch_default(&payload, &signer) + .await + .map_err(|e| Error::ExtrinsicSubmissionError(format!("{:?}", e)))? + .wait_for_finalized_success() + .await + .map_err(|e| Error::ExtrinsicSubmissionError(format!("{:?}", e)))?; + Ok(format!("{:?}", result.extrinsic_hash())) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{find_dispatchable_by_name, parse_chain_metadata, set_up_client}; + use anyhow::Result; + + const ALICE_SURI: &str = "//Alice"; + pub(crate) const POP_NETWORK_TESTNET_URL: &str = "wss://rpc1.paseo.popnetwork.xyz"; + + #[tokio::test] + async fn set_up_client_works() -> Result<()> { + assert!(matches!( + set_up_client("wss://wronguri.xyz").await, + Err(Error::ConnectionFailure(_)) + )); + set_up_client(POP_NETWORK_TESTNET_URL).await?; + Ok(()) + } + + #[tokio::test] + async fn construct_extrinsic_works() -> Result<()> { + let client = set_up_client(POP_NETWORK_TESTNET_URL).await?; + let pallets = parse_chain_metadata(&client)?; + let transfer_allow_death = + find_dispatchable_by_name(&pallets, "Balances", "transfer_allow_death")?; + + // Wrong parameters + assert!(matches!( + construct_extrinsic( + &transfer_allow_death, + vec![ALICE_SURI.to_string(), "100".to_string()], + ), + Err(Error::ParamProcessingError) + )); + // Valid parameters + let xt = construct_extrinsic( + &transfer_allow_death, + vec![ + "Id(5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty)".to_string(), + "100".to_string(), + ], + )?; + assert_eq!(xt.call_name(), "transfer_allow_death"); + assert_eq!(xt.pallet_name(), "Balances"); + Ok(()) + } + + #[tokio::test] + async fn encode_call_data_works() -> Result<()> { + let client = set_up_client(POP_NETWORK_TESTNET_URL).await?; + let pallets = parse_chain_metadata(&client)?; + let remark = find_dispatchable_by_name(&pallets, "System", "remark")?; + let xt = construct_extrinsic(&remark, vec!["0x11".to_string()])?; + assert_eq!(encode_call_data(&client, &xt)?, "0x00000411"); + let xt = construct_extrinsic(&remark, vec!["123".to_string()])?; + assert_eq!(encode_call_data(&client, &xt)?, "0x00000c313233"); + let xt = construct_extrinsic(&remark, vec!["test".to_string()])?; + assert_eq!(encode_call_data(&client, &xt)?, "0x00001074657374"); + Ok(()) + } + + #[tokio::test] + async fn decode_call_data_works() -> Result<()> { + assert!(matches!(decode_call_data("wrongcalldata"), Err(Error::CallDataDecodingError(..)))); + let client = set_up_client(POP_NETWORK_TESTNET_URL).await?; + let pallets = parse_chain_metadata(&client)?; + let remark = find_dispatchable_by_name(&pallets, "System", "remark")?; + let xt = construct_extrinsic(&remark, vec!["0x11".to_string()])?; + let expected_call_data = xt.encode_call_data(&client.metadata())?; + assert_eq!(decode_call_data("0x00000411")?, expected_call_data); + Ok(()) + } + + #[tokio::test] + async fn sign_and_submit_wrong_extrinsic_fails() -> Result<()> { + let client = set_up_client(POP_NETWORK_TESTNET_URL).await?; + let function = Function { + pallet: "WrongPallet".to_string(), + name: "wrong_extrinsic".to_string(), + index: 0, + docs: "documentation".to_string(), + is_supported: true, + ..Default::default() + }; + let xt = construct_extrinsic(&function, vec!["0x11".to_string()])?; + assert!(matches!( + sign_and_submit_extrinsic(&client, xt, ALICE_SURI).await, + Err(Error::ExtrinsicSubmissionError(message)) if message.contains("PalletNameNotFound(\"WrongPallet\"))") + )); + Ok(()) + } + + #[tokio::test] + async fn construct_sudo_extrinsic_works() -> Result<()> { + let client = set_up_client(POP_NETWORK_TESTNET_URL).await?; + let pallets = parse_chain_metadata(&client)?; + let force_transfer = find_dispatchable_by_name(&pallets, "Balances", "force_transfer")?; + let xt = construct_extrinsic( + &force_transfer, + vec![ + "Id(5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty)".to_string(), + "Id(5DAAnrj7VHTznn2AWBemMuyBwZWs6FNFjdyVXUeYum3PTXFy)".to_string(), + "100".to_string(), + ], + )?; + let xt = construct_sudo_extrinsic(xt)?; + assert_eq!(xt.call_name(), "sudo"); + assert_eq!(xt.pallet_name(), "Sudo"); + Ok(()) + } +} diff --git a/crates/pop-parachains/src/errors.rs b/crates/pop-parachains/src/errors.rs index 3eff7bbfc..8bd97434a 100644 --- a/crates/pop-parachains/src/errors.rs +++ b/crates/pop-parachains/src/errors.rs @@ -3,24 +3,43 @@ use thiserror::Error; use zombienet_sdk::OrchestratorError; +/// Represents the various errors that can occur in the crate. #[derive(Error, Debug)] pub enum Error { #[error("User aborted due to existing target directory.")] Aborted, #[error("Anyhow error: {0}")] AnyhowError(#[from] anyhow::Error), + /// An error occurred while decoding the call data. + #[error("Failed to decode call data. {0}")] + CallDataDecodingError(String), + /// An error occurred while encoding the call data. + #[error("Failed to encode call data. {0}")] + CallDataEncodingError(String), #[error("{0}")] CommonError(#[from] pop_common::Error), + /// An error occurred while attempting to establish a connection to the endpoint. + #[error("Failed to establish a connection to: {0}")] + ConnectionFailure(String), #[error("Configuration error: {0}")] Config(String), #[error("Failed to access the current directory")] CurrentDirAccess, #[error("Failed to parse the endowment value")] EndowmentError, + /// An error occurred during the submission of an extrinsic. + #[error("Extrinsic submission error: {0}")] + ExtrinsicSubmissionError(String), + /// The dispatchable function is not supported. + #[error("The dispatchable function is not supported")] + FunctionNotSupported, #[error("IO error: {0}")] IO(#[from] std::io::Error), #[error("JSON error: {0}")] JsonError(#[from] serde_json::Error), + /// An error occurred while parsing metadata of a parameter. + #[error("Error parsing metadata for parameter {0}")] + MetadataParsingError(String), #[error("Missing binary: {0}")] MissingBinary(String), #[error("Missing chain spec file at: {0}")] @@ -31,6 +50,12 @@ pub enum Error { OrchestratorError(#[from] OrchestratorError), #[error("Failed to create pallet directory")] PalletDirCreation, + /// The specified pallet could not be found. + #[error("Failed to find the pallet {0}")] + PalletNotFound(String), + /// An error occurred while processing the arguments provided by the user. + #[error("Failed to process the arguments provided by the user.")] + ParamProcessingError, #[error("Invalid path")] PathError, #[error("Failed to execute rustfmt")] diff --git a/crates/pop-parachains/src/lib.rs b/crates/pop-parachains/src/lib.rs index 06bee8f4a..31a86ade7 100644 --- a/crates/pop-parachains/src/lib.rs +++ b/crates/pop-parachains/src/lib.rs @@ -2,6 +2,8 @@ #![doc = include_str!("../README.md")] mod build; +/// Provides functionality to construct, encode, sign, and submit chain extrinsics. +mod call; mod errors; mod generator; mod new_pallet; @@ -14,10 +16,22 @@ pub use build::{ binary_path, build_parachain, export_wasm_file, generate_genesis_state_file, generate_plain_chain_spec, generate_raw_chain_spec, is_supported, ChainSpec, }; +pub use call::{ + construct_extrinsic, construct_sudo_extrinsic, decode_call_data, encode_call_data, + metadata::{ + action::{supported_actions, Action}, + find_dispatchable_by_name, find_pallet_by_name, + params::Param, + parse_chain_metadata, Function, Pallet, + }, + set_up_client, sign_and_submit_extrinsic, sign_and_submit_extrinsic_with_call_data, +}; pub use errors::Error; pub use indexmap::IndexSet; pub use new_pallet::{create_pallet_template, new_pallet_options::*, TemplatePalletConfig}; pub use new_parachain::instantiate_template_dir; +// External export from subxt. +pub use subxt::{tx::DynamicPayload, OnlineClient, SubstrateConfig}; pub use templates::{Config, Parachain, Provider}; pub use up::Zombienet; pub use utils::helpers::is_initial_endowment_valid; diff --git a/deny.toml b/deny.toml index 5c295de57..53c6d37e6 100644 --- a/deny.toml +++ b/deny.toml @@ -20,6 +20,7 @@ allow = [ "GPL-3.0", "MIT", "MPL-2.0", + "Unicode-3.0", "Unicode-DFS-2016", "Unlicense" ]