diff --git a/Cargo.lock b/Cargo.lock index bb211c82e..f581a91d5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "Inflector" @@ -724,7 +724,7 @@ dependencies = [ "parachains-runtimes-test-utils", "parity-scale-codec", "sp-io 38.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "staging-parachain-info", "staging-xcm 14.2.0", "staging-xcm-builder", @@ -749,7 +749,7 @@ dependencies = [ "parity-scale-codec", "scale-info", "sp-api", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "staging-xcm 14.2.0", "staging-xcm-builder", "staging-xcm-executor", @@ -938,6 +938,61 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +[[package]] +name = "axum" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" +dependencies = [ + "async-trait", + "axum-core", + "bytes", + "futures-util", + "http 1.2.0", + "http-body 1.0.1", + "http-body-util", + "hyper 1.5.1", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper 1.0.2", + "tokio", + "tower 0.5.2", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http 1.2.0", + "http-body 1.0.1", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper 1.0.2", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "backoff" version = "0.4.0" @@ -1271,7 +1326,7 @@ dependencies = [ "serde", "sp-consensus-grandpa", "sp-core 34.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "sp-std", ] @@ -1306,7 +1361,7 @@ dependencies = [ "parity-scale-codec", "scale-info", "sp-core 34.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "sp-std", ] @@ -1339,7 +1394,7 @@ dependencies = [ "scale-info", "serde", "sp-core 34.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "sp-std", ] @@ -1358,7 +1413,7 @@ dependencies = [ "pallet-utility", "parity-scale-codec", "scale-info", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "sp-std", ] @@ -1379,7 +1434,7 @@ dependencies = [ "serde", "sp-core 34.0.0", "sp-io 38.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "sp-state-machine 0.43.0", "sp-std", "sp-trie 37.0.0", @@ -1402,7 +1457,7 @@ dependencies = [ "sp-application-crypto 38.0.0", "sp-consensus-grandpa", "sp-core 34.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "sp-std", "sp-trie 37.0.0", ] @@ -1434,7 +1489,7 @@ dependencies = [ "parity-scale-codec", "scale-info", "sp-core 34.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "staging-xcm 14.2.0", ] @@ -1451,7 +1506,7 @@ dependencies = [ "scale-info", "snowbridge-core", "sp-core 34.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "sp-std", "staging-xcm 14.2.0", ] @@ -1493,7 +1548,7 @@ dependencies = [ "sp-core 34.0.0", "sp-io 38.0.0", "sp-keyring", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "sp-tracing 17.0.1", "staging-xcm 14.2.0", "staging-xcm-builder", @@ -1525,7 +1580,7 @@ dependencies = [ "parity-scale-codec", "scale-info", "sp-io 38.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "sp-std", "sp-trie 37.0.0", "staging-xcm 14.2.0", @@ -1552,9 +1607,9 @@ dependencies = [ [[package]] name = "bstr" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a68f1f47cdf0ec8ee4b941b2eee2a80cb796db73118c0dd09ac63fbe405be22" +checksum = "786a307d683a5bf92e6fd5fd69a7eb613751668d1d8d67d802846dfe367c62c8" dependencies = [ "memchr", "regex-automata 0.4.9", @@ -1648,7 +1703,7 @@ checksum = "eee4243f1f26fc7a42710e7439c149e2b10b05472f88090acce52632f231a73a" dependencies = [ "camino", "cargo-platform", - "semver 1.0.23", + "semver 1.0.24", "serde", "serde_json", "thiserror 1.0.69", @@ -1662,7 +1717,7 @@ checksum = "8769706aad5d996120af43197bf46ef6ad0fda35216b4505f926a365a232d924" dependencies = [ "camino", "cargo-platform", - "semver 1.0.23", + "semver 1.0.24", "serde", "serde_json", "thiserror 2.0.6", @@ -1680,9 +1735,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.3" +version = "1.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27f657647bcff5394bf56c7317665bbf790a137a50eaaa5c6bfbb9e27a518f2d" +checksum = "9157bbaa6b165880c27a4293a474c91cdcf265cc68cc829bf10be0964a391caf" dependencies = [ "jobserver", "libc", @@ -1948,9 +2003,9 @@ checksum = "cd7e35aee659887cbfb97aaf227ac12cad1a9d7c71e55ff3376839ed4e282d08" [[package]] name = "contract-build" -version = "5.0.1" +version = "5.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "857769855bf40d230e41baf6575cc44bd5e6869f69f88f45f6791da793f49a0c" +checksum = "b014fa89030235ecd8bdeb061ec97df326281b484f89ad3e17a79f08759c2f52" dependencies = [ "anyhow", "blake2", @@ -1967,7 +2022,7 @@ dependencies = [ "parity-scale-codec", "regex", "rustc_version 0.4.1", - "semver 1.0.23", + "semver 1.0.24", "serde", "serde_json", "strum 0.26.3", @@ -1989,9 +2044,9 @@ dependencies = [ [[package]] name = "contract-extrinsics" -version = "5.0.1" +version = "5.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e77ad38bef6454f97ca33481e960e9b105cb97f3794656071cbf50229445a89d" +checksum = "67d0c91349c31caec4d5e3c544b4bc4fcc7c0468dc49ee84a96a24f915464401" dependencies = [ "anyhow", "blake2", @@ -2022,13 +2077,13 @@ dependencies = [ [[package]] name = "contract-metadata" -version = "5.0.1" +version = "5.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "733a6624ea05dd71050641c3cd9baff7a1445032a0082f0e55c800c078716424" +checksum = "83ae8bcb5f7c5ea033d05fa0bbffa4e762a5b69c0ce96e4188fb15385a01998b" dependencies = [ "anyhow", "impl-serde 0.5.0", - "semver 1.0.23", + "semver 1.0.24", "serde", "serde_json", "url", @@ -2036,9 +2091,9 @@ dependencies = [ [[package]] name = "contract-transcode" -version = "5.0.1" +version = "5.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd9131028be7b8eefdd9151a0a682ed428c1418d5d1ec142c35d2dbe6b9653c6" +checksum = "baca96dc859fd180eba5f15e468f59dc8c932a6b72f6b76f91b571b6743a9e7d" dependencies = [ "anyhow", "base58", @@ -2355,7 +2410,7 @@ dependencies = [ "scale-info", "sp-application-crypto 38.0.0", "sp-consensus-aura", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", ] [[package]] @@ -2372,7 +2427,7 @@ dependencies = [ "parity-scale-codec", "scale-info", "sp-io 38.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "staging-xcm 14.2.0", ] @@ -2403,7 +2458,7 @@ dependencies = [ "sp-externalities 0.29.0", "sp-inherents", "sp-io 38.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "sp-state-machine 0.43.0", "sp-std", "sp-trie 37.0.0", @@ -2436,7 +2491,7 @@ dependencies = [ "frame-system", "pallet-session", "parity-scale-codec", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", ] [[package]] @@ -2452,7 +2507,7 @@ dependencies = [ "parity-scale-codec", "polkadot-primitives 16.0.0", "scale-info", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", ] [[package]] @@ -2467,7 +2522,7 @@ dependencies = [ "parity-scale-codec", "scale-info", "sp-io 38.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "staging-xcm 14.2.0", ] @@ -2491,7 +2546,7 @@ dependencies = [ "scale-info", "sp-core 34.0.0", "sp-io 38.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "staging-xcm 14.2.0", "staging-xcm-builder", "staging-xcm-executor", @@ -2509,7 +2564,7 @@ dependencies = [ "frame-system", "parity-scale-codec", "scale-info", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "staging-xcm 14.2.0", ] @@ -2524,7 +2579,7 @@ dependencies = [ "polkadot-primitives 15.0.0", "sp-api", "sp-consensus-aura", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", ] [[package]] @@ -2539,7 +2594,7 @@ dependencies = [ "polkadot-primitives 16.0.0", "scale-info", "sp-api", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "sp-trie 37.0.0", "staging-xcm 14.2.0", ] @@ -2584,7 +2639,7 @@ dependencies = [ "log", "parity-scale-codec", "scale-info", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", ] [[package]] @@ -2610,7 +2665,7 @@ dependencies = [ "pallet-asset-conversion", "parity-scale-codec", "polkadot-runtime-common", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "staging-xcm 14.2.0", "staging-xcm-builder", "staging-xcm-executor", @@ -2625,7 +2680,7 @@ dependencies = [ "cumulus-primitives-core", "parity-scale-codec", "polkadot-primitives 16.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "sp-state-machine 0.43.0", "sp-trie 37.0.0", ] @@ -2672,9 +2727,9 @@ dependencies = [ [[package]] name = "cxx" -version = "1.0.133" +version = "1.0.134" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05e1ec88093d2abd9cf1b09ffd979136b8e922bf31cad966a8fe0d73233112ef" +checksum = "a5a32d755fe20281b46118ee4b507233311fb7a48a0cfd42f554b93640521a2f" dependencies = [ "cc", "cxxbridge-cmd", @@ -2686,9 +2741,9 @@ dependencies = [ [[package]] name = "cxx-build" -version = "1.0.133" +version = "1.0.134" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afa390d956ee7ccb41aeed7ed7856ab3ffb4fc587e7216be7e0f83e949b4e6c" +checksum = "11645536ada5d1c8804312cbffc9ab950f2216154de431de930da47ca6955199" dependencies = [ "cc", "codespan-reporting", @@ -2700,9 +2755,9 @@ dependencies = [ [[package]] name = "cxxbridge-cmd" -version = "1.0.133" +version = "1.0.134" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c23bfff654d6227cbc83de8e059d2f8678ede5fc3a6c5a35d5c379983cc61e6" +checksum = "ebcc9c78e3c7289665aab921a2b394eaffe8bdb369aa18d81ffc0f534fd49385" dependencies = [ "clap", "codespan-reporting", @@ -2713,15 +2768,15 @@ dependencies = [ [[package]] name = "cxxbridge-flags" -version = "1.0.133" +version = "1.0.134" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7c01b36e22051bc6928a78583f1621abaaf7621561c2ada1b00f7878fbe2caa" +checksum = "3a22a87bd9e78d7204d793261470a4c9d585154fddd251828d8aefbb5f74c3bf" [[package]] name = "cxxbridge-macro" -version = "1.0.133" +version = "1.0.134" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6e14013136fac689345d17b9a6df55977251f11d333c0a571e8d963b55e1f95" +checksum = "1dfdb020ff8787c5daf6e0dca743005cc8782868faeadfbabb8824ede5cb1c72" dependencies = [ "proc-macro2", "quote", @@ -3560,7 +3615,7 @@ dependencies = [ "sp-application-crypto 38.0.0", "sp-core 34.0.0", "sp-io 38.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "sp-runtime-interface 28.0.0", "sp-storage 21.0.0", "static_assertions", @@ -3578,7 +3633,7 @@ dependencies = [ "parity-scale-codec", "scale-info", "sp-io 38.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", ] [[package]] @@ -3621,7 +3676,7 @@ dependencies = [ "sp-arithmetic", "sp-core 34.0.0", "sp-npos-elections", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", ] [[package]] @@ -3639,7 +3694,7 @@ dependencies = [ "scale-info", "sp-core 34.0.0", "sp-io 38.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "sp-tracing 17.0.1", ] @@ -3680,7 +3735,7 @@ dependencies = [ "log", "parity-scale-codec", "scale-info", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", ] [[package]] @@ -3715,7 +3770,7 @@ dependencies = [ "sp-inherents", "sp-io 38.0.0", "sp-metadata-ir", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "sp-staking 36.0.0", "sp-state-machine 0.43.0", "sp-std", @@ -3784,7 +3839,7 @@ dependencies = [ "serde", "sp-core 34.0.0", "sp-io 38.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "sp-std", "sp-version", "sp-weights", @@ -3802,7 +3857,7 @@ dependencies = [ "parity-scale-codec", "scale-info", "sp-core 34.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", ] [[package]] @@ -3825,7 +3880,7 @@ dependencies = [ "frame-support", "parity-scale-codec", "sp-api", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", ] [[package]] @@ -4336,6 +4391,12 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "add0ab9360ddbd88cfeb3bd9574a1d85cfdfa14db10b3e21d3700dbc4328758f" +[[package]] +name = "http-range-header" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c" + [[package]] name = "httparse" version = "1.9.5" @@ -4449,7 +4510,7 @@ dependencies = [ "http 1.2.0", "hyper 1.5.1", "hyper-util", - "rustls 0.23.19", + "rustls 0.23.20", "rustls-pki-types", "tokio", "tokio-rustls 0.26.1", @@ -4975,6 +5036,15 @@ version = "2.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" +[[package]] +name = "is-docker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] + [[package]] name = "is-terminal" version = "0.4.13" @@ -4986,6 +5056,16 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "is-wsl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -5117,7 +5197,7 @@ dependencies = [ "http 1.2.0", "jsonrpsee-core", "pin-project", - "rustls 0.23.19", + "rustls 0.23.20", "rustls-pki-types", "rustls-platform-verifier", "soketto", @@ -5266,8 +5346,8 @@ dependencies = [ "tokio", "tokio-tungstenite", "tokio-util", - "tower", - "tower-http", + "tower 0.4.13", + "tower-http 0.4.4", "tracing", ] @@ -5716,6 +5796,12 @@ dependencies = [ "regex-automata 0.1.10", ] +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + [[package]] name = "matrixmultiply" version = "0.3.9" @@ -6126,6 +6212,17 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" +[[package]] +name = "open" +version = "5.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ecd52f0b8d15c40ce4820aa251ed5de032e5d91fab27f7db2f40d42a8bdf69c" +dependencies = [ + "is-wsl", + "libc", + "pathdiff", +] + [[package]] name = "openssl" version = "0.10.68" @@ -6238,7 +6335,7 @@ dependencies = [ "sp-core 34.0.0", "sp-crypto-hashing", "sp-io 38.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", ] [[package]] @@ -6257,7 +6354,7 @@ dependencies = [ "sp-arithmetic", "sp-core 34.0.0", "sp-io 38.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", ] [[package]] @@ -6276,7 +6373,7 @@ dependencies = [ "sp-arithmetic", "sp-core 34.0.0", "sp-io 38.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", ] [[package]] @@ -6291,7 +6388,7 @@ dependencies = [ "pallet-transaction-payment", "parity-scale-codec", "scale-info", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", ] [[package]] @@ -6306,7 +6403,7 @@ dependencies = [ "parity-scale-codec", "scale-info", "sp-core 34.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", ] [[package]] @@ -6324,7 +6421,7 @@ dependencies = [ "serde", "sp-core 34.0.0", "sp-io 38.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", ] [[package]] @@ -6341,7 +6438,7 @@ dependencies = [ "parity-scale-codec", "scale-info", "sp-core 34.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", ] [[package]] @@ -6357,7 +6454,7 @@ dependencies = [ "pallet-assets", "parity-scale-codec", "scale-info", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", ] [[package]] @@ -6372,7 +6469,7 @@ dependencies = [ "scale-info", "sp-core 34.0.0", "sp-io 38.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", ] [[package]] @@ -6389,7 +6486,7 @@ dependencies = [ "scale-info", "sp-application-crypto 38.0.0", "sp-consensus-aura", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", ] [[package]] @@ -6405,7 +6502,7 @@ dependencies = [ "scale-info", "sp-application-crypto 38.0.0", "sp-authority-discovery", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", ] [[package]] @@ -6419,7 +6516,7 @@ dependencies = [ "impl-trait-for-tuples", "parity-scale-codec", "scale-info", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", ] [[package]] @@ -6441,7 +6538,7 @@ dependencies = [ "sp-consensus-babe", "sp-core 34.0.0", "sp-io 38.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "sp-session", "sp-staking 36.0.0", ] @@ -6464,7 +6561,7 @@ dependencies = [ "scale-info", "sp-core 34.0.0", "sp-io 38.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "sp-tracing 17.0.1", ] @@ -6481,7 +6578,7 @@ dependencies = [ "log", "parity-scale-codec", "scale-info", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", ] [[package]] @@ -6499,7 +6596,7 @@ dependencies = [ "scale-info", "serde", "sp-consensus-beefy", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "sp-session", "sp-staking 36.0.0", ] @@ -6526,7 +6623,7 @@ dependencies = [ "sp-consensus-beefy", "sp-core 34.0.0", "sp-io 38.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "sp-state-machine 0.43.0", ] @@ -6545,7 +6642,7 @@ dependencies = [ "scale-info", "sp-core 34.0.0", "sp-io 38.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", ] [[package]] @@ -6564,7 +6661,7 @@ dependencies = [ "parity-scale-codec", "scale-info", "sp-consensus-grandpa", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "sp-std", ] @@ -6583,7 +6680,7 @@ dependencies = [ "log", "parity-scale-codec", "scale-info", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "sp-std", "sp-trie 37.0.0", ] @@ -6605,7 +6702,7 @@ dependencies = [ "pallet-bridge-grandpa", "parity-scale-codec", "scale-info", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "sp-std", ] @@ -6630,7 +6727,7 @@ dependencies = [ "parity-scale-codec", "scale-info", "sp-arithmetic", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "sp-std", ] @@ -6650,7 +6747,7 @@ dependencies = [ "sp-api", "sp-arithmetic", "sp-core 34.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", ] [[package]] @@ -6669,7 +6766,7 @@ dependencies = [ "scale-info", "sp-core 34.0.0", "sp-io 38.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", ] [[package]] @@ -6688,7 +6785,7 @@ dependencies = [ "parity-scale-codec", "rand", "scale-info", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "sp-staking 36.0.0", ] @@ -6706,7 +6803,7 @@ dependencies = [ "scale-info", "sp-core 34.0.0", "sp-io 38.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", ] [[package]] @@ -6721,7 +6818,7 @@ dependencies = [ "parity-scale-codec", "scale-info", "sp-core 34.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", ] [[package]] @@ -6749,7 +6846,7 @@ dependencies = [ "sp-api", "sp-core 34.0.0", "sp-io 38.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "sp-std", "staging-xcm 14.2.0", "staging-xcm-builder", @@ -6785,7 +6882,7 @@ dependencies = [ "sp-core 34.0.0", "sp-io 38.0.0", "sp-keystore 0.40.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "sp-tracing 17.0.1", "staging-xcm 14.2.0", "staging-xcm-builder", @@ -6855,7 +6952,7 @@ dependencies = [ "scale-info", "serde", "sp-io 38.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", ] [[package]] @@ -6874,7 +6971,7 @@ dependencies = [ "sp-arithmetic", "sp-core 34.0.0", "sp-io 38.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", ] [[package]] @@ -6889,7 +6986,7 @@ dependencies = [ "parity-scale-codec", "scale-info", "sp-io 38.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "sp-staking 36.0.0", ] @@ -6908,7 +7005,7 @@ dependencies = [ "serde", "sp-core 34.0.0", "sp-io 38.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", ] [[package]] @@ -6924,7 +7021,7 @@ dependencies = [ "parity-scale-codec", "scale-info", "sp-io 38.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", ] [[package]] @@ -6946,7 +7043,7 @@ dependencies = [ "sp-core 34.0.0", "sp-io 38.0.0", "sp-npos-elections", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "strum 0.26.3", ] @@ -6961,7 +7058,7 @@ dependencies = [ "frame-system", "parity-scale-codec", "sp-npos-elections", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", ] [[package]] @@ -6979,7 +7076,7 @@ dependencies = [ "sp-core 34.0.0", "sp-io 38.0.0", "sp-npos-elections", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "sp-staking 36.0.0", ] @@ -6998,7 +7095,7 @@ dependencies = [ "parity-scale-codec", "scale-info", "sp-io 38.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "sp-staking 36.0.0", ] @@ -7018,7 +7115,7 @@ dependencies = [ "sp-core 34.0.0", "sp-inherents", "sp-io 38.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", ] [[package]] @@ -7039,7 +7136,7 @@ dependencies = [ "sp-consensus-grandpa", "sp-core 34.0.0", "sp-io 38.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "sp-session", "sp-staking 36.0.0", ] @@ -7058,7 +7155,7 @@ dependencies = [ "parity-scale-codec", "scale-info", "sp-io 38.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", ] [[package]] @@ -7077,7 +7174,7 @@ dependencies = [ "sp-application-crypto 38.0.0", "sp-core 34.0.0", "sp-io 38.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "sp-staking 36.0.0", ] @@ -7095,7 +7192,7 @@ dependencies = [ "sp-core 34.0.0", "sp-io 38.0.0", "sp-keyring", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", ] [[package]] @@ -7109,7 +7206,7 @@ dependencies = [ "parity-scale-codec", "safe-mix", "scale-info", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", ] [[package]] @@ -7123,7 +7220,7 @@ dependencies = [ "frame-system", "parity-scale-codec", "scale-info", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", ] [[package]] @@ -7140,7 +7237,7 @@ dependencies = [ "scale-info", "sp-core 34.0.0", "sp-io 38.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", ] [[package]] @@ -7159,7 +7256,7 @@ dependencies = [ "sp-arithmetic", "sp-core 34.0.0", "sp-io 38.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "sp-weights", ] @@ -7178,7 +7275,7 @@ dependencies = [ "parity-scale-codec", "scale-info", "sp-core 34.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", ] [[package]] @@ -7198,7 +7295,7 @@ dependencies = [ "sp-arithmetic", "sp-io 38.0.0", "sp-mixnet", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", ] [[package]] @@ -7216,7 +7313,7 @@ dependencies = [ "sp-core 34.0.0", "sp-io 38.0.0", "sp-mmr-primitives", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", ] [[package]] @@ -7232,7 +7329,7 @@ dependencies = [ "parity-scale-codec", "scale-info", "sp-io 38.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", ] [[package]] @@ -7249,7 +7346,7 @@ dependencies = [ "pallet-nfts", "parity-scale-codec", "scale-info", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", ] [[package]] @@ -7267,7 +7364,7 @@ dependencies = [ "scale-info", "sp-core 34.0.0", "sp-io 38.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", ] [[package]] @@ -7294,7 +7391,7 @@ dependencies = [ "scale-info", "sp-arithmetic", "sp-core 34.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", ] [[package]] @@ -7310,7 +7407,7 @@ dependencies = [ "scale-info", "sp-core 34.0.0", "sp-io 38.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", ] [[package]] @@ -7327,7 +7424,7 @@ dependencies = [ "scale-info", "sp-core 34.0.0", "sp-io 38.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "sp-staking 36.0.0", "sp-tracing 17.0.1", ] @@ -7348,7 +7445,7 @@ dependencies = [ "pallet-staking", "parity-scale-codec", "scale-info", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "sp-runtime-interface 28.0.0", "sp-staking 36.0.0", ] @@ -7377,7 +7474,7 @@ dependencies = [ "parity-scale-codec", "scale-info", "serde", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "sp-staking 36.0.0", ] @@ -7401,7 +7498,7 @@ dependencies = [ "pallet-staking", "parity-scale-codec", "scale-info", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "sp-staking 36.0.0", ] @@ -7420,7 +7517,7 @@ dependencies = [ "sp-core 34.0.0", "sp-io 38.0.0", "sp-metadata-ir", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", ] [[package]] @@ -7438,7 +7535,7 @@ dependencies = [ "scale-info", "serde", "sp-core 34.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", ] [[package]] @@ -7455,7 +7552,7 @@ dependencies = [ "scale-info", "sp-core 34.0.0", "sp-io 38.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", ] [[package]] @@ -7470,7 +7567,7 @@ dependencies = [ "parity-scale-codec", "scale-info", "sp-io 38.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", ] [[package]] @@ -7489,7 +7586,7 @@ dependencies = [ "sp-arithmetic", "sp-core 34.0.0", "sp-io 38.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", ] [[package]] @@ -7504,7 +7601,7 @@ dependencies = [ "parity-scale-codec", "scale-info", "sp-io 38.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", ] [[package]] @@ -7522,7 +7619,7 @@ dependencies = [ "serde", "sp-arithmetic", "sp-io 38.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", ] [[package]] @@ -7539,7 +7636,7 @@ dependencies = [ "serde", "sp-core 34.0.0", "sp-io 38.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", ] [[package]] @@ -7567,7 +7664,7 @@ dependencies = [ "sp-api", "sp-core 34.0.0", "sp-io 38.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "sp-std", "staging-xcm 14.2.0", "staging-xcm-builder", @@ -7583,7 +7680,7 @@ dependencies = [ "frame-system", "parity-wasm", "polkavm-linker 0.10.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "tempfile", "toml 0.8.19", ] @@ -7615,7 +7712,7 @@ dependencies = [ "sp-core 34.0.0", "sp-io 38.0.0", "sp-keystore 0.40.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "sp-tracing 17.0.1", "staging-xcm 14.2.0", "staging-xcm-builder", @@ -7659,7 +7756,7 @@ dependencies = [ "pallet-staking", "parity-scale-codec", "scale-info", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "sp-staking 36.0.0", ] @@ -7675,7 +7772,7 @@ dependencies = [ "scale-info", "sp-core 34.0.0", "sp-io 38.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", ] [[package]] @@ -7694,7 +7791,7 @@ dependencies = [ "parity-scale-codec", "scale-info", "sp-arithmetic", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", ] [[package]] @@ -7713,7 +7810,7 @@ dependencies = [ "sp-arithmetic", "sp-core 34.0.0", "sp-io 38.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", ] [[package]] @@ -7730,7 +7827,7 @@ dependencies = [ "parity-scale-codec", "scale-info", "sp-io 38.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "sp-weights", ] @@ -7745,7 +7842,7 @@ dependencies = [ "parity-scale-codec", "scale-info", "sp-io 38.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", ] [[package]] @@ -7763,7 +7860,7 @@ dependencies = [ "scale-info", "sp-core 34.0.0", "sp-io 38.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "sp-session", "sp-staking 36.0.0", "sp-state-machine 0.43.0", @@ -7783,7 +7880,7 @@ dependencies = [ "pallet-staking", "parity-scale-codec", "rand", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "sp-session", ] @@ -7797,7 +7894,7 @@ dependencies = [ "frame-system", "parity-scale-codec", "scale-info", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", ] [[package]] @@ -7815,7 +7912,7 @@ dependencies = [ "scale-info", "sp-arithmetic", "sp-io 38.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", ] [[package]] @@ -7836,7 +7933,7 @@ dependencies = [ "serde", "sp-application-crypto 38.0.0", "sp-io 38.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "sp-staking 36.0.0", ] @@ -7875,7 +7972,7 @@ dependencies = [ "scale-info", "sp-core 34.0.0", "sp-io 38.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", ] [[package]] @@ -7892,7 +7989,7 @@ dependencies = [ "sp-api", "sp-core 34.0.0", "sp-io 38.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "sp-statement-store", ] @@ -7909,7 +8006,7 @@ dependencies = [ "parity-scale-codec", "scale-info", "sp-io 38.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", ] [[package]] @@ -7927,7 +8024,7 @@ dependencies = [ "scale-info", "sp-inherents", "sp-io 38.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "sp-storage 21.0.0", "sp-timestamp", ] @@ -7948,7 +8045,7 @@ dependencies = [ "serde", "sp-core 34.0.0", "sp-io 38.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", ] [[package]] @@ -7964,7 +8061,7 @@ dependencies = [ "serde", "sp-core 34.0.0", "sp-io 38.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", ] [[package]] @@ -7976,7 +8073,7 @@ dependencies = [ "pallet-transaction-payment", "parity-scale-codec", "sp-api", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "sp-weights", ] @@ -7996,7 +8093,7 @@ dependencies = [ "serde", "sp-inherents", "sp-io 38.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "sp-transaction-storage-proof", ] @@ -8016,7 +8113,7 @@ dependencies = [ "scale-info", "serde", "sp-core 34.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", ] [[package]] @@ -8034,7 +8131,7 @@ dependencies = [ "pallet-utility", "parity-scale-codec", "scale-info", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", ] [[package]] @@ -8049,7 +8146,7 @@ dependencies = [ "log", "parity-scale-codec", "scale-info", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", ] [[package]] @@ -8065,7 +8162,7 @@ dependencies = [ "scale-info", "sp-core 34.0.0", "sp-io 38.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", ] [[package]] @@ -8080,7 +8177,7 @@ dependencies = [ "log", "parity-scale-codec", "scale-info", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", ] [[package]] @@ -8095,7 +8192,7 @@ dependencies = [ "parity-scale-codec", "scale-info", "sp-api", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", ] [[package]] @@ -8115,7 +8212,7 @@ dependencies = [ "serde", "sp-core 34.0.0", "sp-io 38.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "staging-xcm 14.2.0", "staging-xcm-builder", "staging-xcm-executor", @@ -8136,7 +8233,7 @@ dependencies = [ "parity-scale-codec", "scale-info", "sp-io 38.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "staging-xcm 14.2.0", "staging-xcm-builder", "staging-xcm-executor", @@ -8158,7 +8255,7 @@ dependencies = [ "parity-scale-codec", "scale-info", "sp-core 34.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "sp-std", "staging-xcm 14.2.0", "staging-xcm-builder", @@ -8179,7 +8276,7 @@ dependencies = [ "parity-scale-codec", "scale-info", "sp-core 34.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "sp-std", "staging-xcm 14.2.0", "staging-xcm-builder", @@ -8209,7 +8306,7 @@ dependencies = [ "sp-consensus-aura", "sp-core 34.0.0", "sp-io 38.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "staging-parachain-info", "staging-xcm 14.2.0", "staging-xcm-executor", @@ -8239,7 +8336,7 @@ dependencies = [ "sp-consensus-aura", "sp-core 34.0.0", "sp-io 38.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "sp-tracing 17.0.1", "staging-parachain-info", "staging-xcm 14.2.0", @@ -8374,6 +8471,12 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + [[package]] name = "pbkdf2" version = "0.12.2" @@ -8524,7 +8627,7 @@ dependencies = [ "parity-scale-codec", "scale-info", "sp-core 34.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", ] [[package]] @@ -8540,7 +8643,7 @@ dependencies = [ "scale-info", "serde", "sp-core 34.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "sp-weights", ] @@ -8567,7 +8670,7 @@ dependencies = [ "sp-inherents", "sp-io 38.0.0", "sp-keystore 0.40.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "sp-staking 34.0.0", ] @@ -8594,7 +8697,7 @@ dependencies = [ "sp-inherents", "sp-io 38.0.0", "sp-keystore 0.40.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "sp-staking 36.0.0", ] @@ -8639,7 +8742,7 @@ dependencies = [ "sp-inherents", "sp-io 38.0.0", "sp-npos-elections", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "sp-session", "sp-staking 36.0.0", "staging-xcm 14.2.0", @@ -8702,7 +8805,7 @@ dependencies = [ "sp-inherents", "sp-io 38.0.0", "sp-keystore 0.40.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "sp-session", "sp-staking 36.0.0", "sp-std", @@ -8917,7 +9020,7 @@ dependencies = [ "sp-mmr-primitives", "sp-npos-elections", "sp-offchain", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "sp-runtime-interface 28.0.0", "sp-session", "sp-staking 36.0.0", @@ -8968,7 +9071,7 @@ dependencies = [ "sp-inherents", "sp-io 38.0.0", "sp-offchain", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "sp-session", "sp-storage 21.0.0", "sp-transaction-pool", @@ -9251,13 +9354,16 @@ version = "0.5.0" dependencies = [ "anyhow", "assert_cmd", + "axum", "clap", "cliclack", "console", + "contract-extrinsics", "dirs", "duct", "env_logger 0.11.5", "git2", + "open", "os_info", "pop-common", "pop-contracts", @@ -9265,13 +9371,17 @@ dependencies = [ "pop-telemetry", "predicates", "reqwest 0.12.9", + "serde", "serde_json", "sp-core 32.0.0", "sp-weights", "strum 0.26.3", "strum_macros 0.26.4", + "subxt", + "subxt-signer", "tempfile", "tokio", + "tower-http 0.6.2", "url", ] @@ -9328,6 +9438,8 @@ dependencies = [ "sp-weights", "strum 0.26.3", "strum_macros 0.26.4", + "subxt", + "subxt-signer", "tar", "tempfile", "thiserror 1.0.69", @@ -9346,7 +9458,6 @@ dependencies = [ "duct", "flate2", "glob", - "hex", "indexmap 2.7.0", "mockito", "pop-common", @@ -9354,6 +9465,7 @@ dependencies = [ "scale-info", "scale-value", "serde_json", + "sp-core 32.0.0", "strum 0.26.3", "strum_macros 0.26.4", "subxt", @@ -9710,9 +9822,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.7" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" dependencies = [ "bitflags 2.6.0", ] @@ -9990,7 +10102,7 @@ dependencies = [ "polkadot-runtime-common", "smallvec", "sp-core 34.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "sp-weights", "staging-xcm 14.2.0", "staging-xcm-builder", @@ -10090,7 +10202,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ - "semver 1.0.23", + "semver 1.0.24", ] [[package]] @@ -10134,9 +10246,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.19" +version = "0.23.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "934b404430bb06b3fae2cba809eb45a1ab1aecd64491213d7c3301b88393f8d1" +checksum = "5065c3f250cbd332cd894be57c40fa52387247659b14a2d6041d121547903b1b" dependencies = [ "log", "once_cell", @@ -10192,9 +10304,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.10.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b" +checksum = "d2bf47e6ff922db3825eb750c4e2ff784c6ff8fb9e13046ef6a1d1c5401b0b37" [[package]] name = "rustls-platform-verifier" @@ -10207,7 +10319,7 @@ dependencies = [ "jni", "log", "once_cell", - "rustls 0.23.19", + "rustls 0.23.20", "rustls-native-certs 0.7.3", "rustls-platform-verifier-android", "rustls-webpki 0.102.8", @@ -10842,9 +10954,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.23" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" +checksum = "3cb6eb87a131f756572d7fb904f6e7b68633f09cca868c5df1c4b8d1a694bbba" dependencies = [ "serde", ] @@ -10936,6 +11048,16 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" +dependencies = [ + "itoa", + "serde", +] + [[package]] name = "serde_repr" version = "0.1.19" @@ -11185,7 +11307,7 @@ dependencies = [ "enumn", "parity-scale-codec", "paste", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", ] [[package]] @@ -11334,7 +11456,7 @@ dependencies = [ "snowbridge-milagro-bls", "sp-core 34.0.0", "sp-io 38.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "sp-std", "ssz_rs", "ssz_rs_derive", @@ -11358,7 +11480,7 @@ dependencies = [ "sp-arithmetic", "sp-core 34.0.0", "sp-io 38.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "sp-std", "staging-xcm 14.2.0", "staging-xcm-builder", @@ -11381,7 +11503,7 @@ dependencies = [ "serde", "serde-big-array", "sp-io 38.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "sp-std", ] @@ -11409,7 +11531,7 @@ dependencies = [ "parity-scale-codec", "scale-info", "sp-core 34.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", ] [[package]] @@ -11446,7 +11568,7 @@ dependencies = [ "snowbridge-pallet-ethereum-client-fixtures", "sp-core 34.0.0", "sp-io 38.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "sp-std", "static_assertions", ] @@ -11486,7 +11608,7 @@ dependencies = [ "snowbridge-router-primitives", "sp-core 34.0.0", "sp-io 38.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "sp-std", "staging-xcm 14.2.0", "staging-xcm-executor", @@ -11524,7 +11646,7 @@ dependencies = [ "sp-arithmetic", "sp-core 34.0.0", "sp-io 38.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "sp-std", ] @@ -11543,7 +11665,7 @@ dependencies = [ "snowbridge-core", "sp-core 34.0.0", "sp-io 38.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "sp-std", "staging-xcm 14.2.0", "staging-xcm-executor", @@ -11563,7 +11685,7 @@ dependencies = [ "snowbridge-core", "sp-core 34.0.0", "sp-io 38.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "sp-std", "staging-xcm 14.2.0", "staging-xcm-executor", @@ -11612,7 +11734,7 @@ dependencies = [ "sp-core 34.0.0", "sp-io 38.0.0", "sp-keyring", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "staging-parachain-info", "staging-xcm 14.2.0", "staging-xcm-executor", @@ -11671,7 +11793,7 @@ dependencies = [ "sp-core 34.0.0", "sp-externalities 0.29.0", "sp-metadata-ir", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "sp-runtime-interface 28.0.0", "sp-state-machine 0.43.0", "sp-trie 37.0.0", @@ -11747,7 +11869,7 @@ dependencies = [ "scale-info", "sp-api", "sp-application-crypto 38.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", ] [[package]] @@ -11758,7 +11880,7 @@ checksum = "74738809461e3d4bd707b5b94e0e0c064a623a74a6a8fe5c98514417a02858dd" dependencies = [ "sp-api", "sp-inherents", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", ] [[package]] @@ -11774,7 +11896,7 @@ dependencies = [ "sp-application-crypto 38.0.0", "sp-consensus-slots", "sp-inherents", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "sp-timestamp", ] @@ -11793,7 +11915,7 @@ dependencies = [ "sp-consensus-slots", "sp-core 34.0.0", "sp-inherents", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "sp-timestamp", ] @@ -11814,7 +11936,7 @@ dependencies = [ "sp-io 38.0.0", "sp-keystore 0.40.0", "sp-mmr-primitives", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "sp-weights", "strum 0.26.3", ] @@ -11834,7 +11956,7 @@ dependencies = [ "sp-application-crypto 38.0.0", "sp-core 34.0.0", "sp-keystore 0.40.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", ] [[package]] @@ -11846,7 +11968,7 @@ dependencies = [ "parity-scale-codec", "sp-api", "sp-core 34.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", ] [[package]] @@ -12112,7 +12234,7 @@ dependencies = [ "scale-info", "serde_json", "sp-api", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", ] [[package]] @@ -12125,7 +12247,7 @@ dependencies = [ "impl-trait-for-tuples", "parity-scale-codec", "scale-info", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "thiserror 1.0.69", ] @@ -12190,7 +12312,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c0e20624277f578b27f44ecfbe2ebc2e908488511ee2c900c5281599f700ab3" dependencies = [ "sp-core 34.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "strum 0.26.3", ] @@ -12265,7 +12387,7 @@ dependencies = [ "sp-api", "sp-core 34.0.0", "sp-debug-derive", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "thiserror 1.0.69", ] @@ -12280,7 +12402,7 @@ dependencies = [ "serde", "sp-arithmetic", "sp-core 34.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", ] [[package]] @@ -12291,7 +12413,7 @@ checksum = "2d9de237d72ecffd07f90826eef18360208b16d8de939d54e61591fac0fcbf99" dependencies = [ "sp-api", "sp-core 34.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", ] [[package]] @@ -12332,9 +12454,9 @@ dependencies = [ [[package]] name = "sp-runtime" -version = "39.0.2" +version = "39.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "658f23be7c79a85581029676a73265c107c5469157e3444c8c640fdbaa8bfed0" +checksum = "ef567865c042b9002dfa44b8fc850fe611038acdf1e382e539495015f60f692f" dependencies = [ "docify", "either", @@ -12442,7 +12564,7 @@ dependencies = [ "sp-api", "sp-core 34.0.0", "sp-keystore 0.40.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "sp-staking 36.0.0", ] @@ -12457,7 +12579,7 @@ dependencies = [ "scale-info", "serde", "sp-core 34.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", ] [[package]] @@ -12471,7 +12593,7 @@ dependencies = [ "scale-info", "serde", "sp-core 34.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", ] [[package]] @@ -12535,7 +12657,7 @@ dependencies = [ "sp-core 34.0.0", "sp-crypto-hashing", "sp-externalities 0.29.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "sp-runtime-interface 28.0.0", "thiserror 1.0.69", "x25519-dalek", @@ -12583,7 +12705,7 @@ dependencies = [ "async-trait", "parity-scale-codec", "sp-inherents", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "thiserror 1.0.69", ] @@ -12619,7 +12741,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc4bf251059485a7dd38fe4afeda8792983511cc47f342ff4695e2dcae6b5247" dependencies = [ "sp-api", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", ] [[package]] @@ -12633,7 +12755,7 @@ dependencies = [ "scale-info", "sp-core 34.0.0", "sp-inherents", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "sp-trie 37.0.0", ] @@ -12697,7 +12819,7 @@ dependencies = [ "scale-info", "serde", "sp-crypto-hashing-proc-macro", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "sp-std", "sp-version-proc-macro", "thiserror 1.0.69", @@ -12828,7 +12950,7 @@ dependencies = [ "frame-system", "parity-scale-codec", "scale-info", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", ] [[package]] @@ -12865,7 +12987,7 @@ dependencies = [ "parity-scale-codec", "scale-info", "serde", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "sp-weights", "xcm-procedural 10.1.0", ] @@ -12887,7 +13009,7 @@ dependencies = [ "scale-info", "sp-arithmetic", "sp-io 38.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "sp-weights", "staging-xcm 14.2.0", "staging-xcm-executor", @@ -12908,7 +13030,7 @@ dependencies = [ "sp-arithmetic", "sp-core 34.0.0", "sp-io 38.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "sp-weights", "staging-xcm 14.2.0", "tracing", @@ -13397,7 +13519,7 @@ dependencies = [ "polkadot-core-primitives", "rococo-runtime-constants", "smallvec", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "staging-xcm 14.2.0", "westend-runtime-constants", ] @@ -13593,7 +13715,7 @@ version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37" dependencies = [ - "rustls 0.23.19", + "rustls 0.23.20", "tokio", ] @@ -13733,6 +13855,22 @@ dependencies = [ "tracing", ] +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper 1.0.2", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "tower-http" version = "0.4.4" @@ -13746,9 +13884,34 @@ dependencies = [ "futures-util", "http 0.2.12", "http-body 0.4.6", - "http-range-header", + "http-range-header 0.3.1", + "mime", + "pin-project-lite", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "403fa3b783d4b626a8ad51d766ab03cb6d2dbfc46b1c5d4448395e6628dc9697" +dependencies = [ + "bitflags 2.6.0", + "bytes", + "futures-util", + "http 1.2.0", + "http-body 1.0.1", + "http-body-util", + "http-range-header 0.4.2", + "httpdate", "mime", + "mime_guess", + "percent-encoding", "pin-project-lite", + "tokio", + "tokio-util", "tower-layer", "tower-service", "tracing", @@ -14399,7 +14562,7 @@ dependencies = [ "bitflags 2.6.0", "hashbrown 0.14.5", "indexmap 2.7.0", - "semver 1.0.23", + "semver 1.0.24", "serde", ] @@ -14647,7 +14810,7 @@ dependencies = [ "polkadot-runtime-common", "smallvec", "sp-core 34.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "sp-weights", "staging-xcm 14.2.0", "staging-xcm-builder", @@ -15092,7 +15255,7 @@ dependencies = [ "polkadot-runtime-parachains", "scale-info", "sp-io 38.0.0", - "sp-runtime 39.0.2", + "sp-runtime 39.0.3", "sp-std", "staging-xcm 14.2.0", "staging-xcm-builder", @@ -15236,9 +15399,9 @@ dependencies = [ [[package]] name = "zombienet-configuration" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22860eef7e651d6e0aa7e37fc3bcba3c2c8b7bd1c140d7ea929caacf2b7fc726" +checksum = "d716b3ff8112d98ced15f53b0c72454f8cde533fe2b68bb04379228961efbd80" dependencies = [ "anyhow", "lazy_static", @@ -15256,9 +15419,9 @@ dependencies = [ [[package]] name = "zombienet-orchestrator" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b19b1b2fd2db3153155f21cb84cdd8e5d6faefc3043353b8c90661c44f4660da" +checksum = "4098a7d33b729b59e32c41a87aa4d484bd1b8771a059bbd4edfb4d430b3b2d74" dependencies = [ "anyhow", "async-trait", @@ -15289,9 +15452,9 @@ dependencies = [ [[package]] name = "zombienet-prom-metrics-parser" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61ce9c6b2d43be864ad34328d05794079381807f5d77c737a062486966347f" +checksum = "961e30be45b34f6ebeabf29ee2f47b0cd191ea62e40c064752572207509a6f5c" dependencies = [ "pest", "pest_derive", @@ -15300,9 +15463,9 @@ dependencies = [ [[package]] name = "zombienet-provider" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c99cc7c143f1145bda2b2f5a945b8040a898a85fc029dba51f220395a7188d9d" +checksum = "ab0f7f01780b7c99a6c40539d195d979f234305f32808d547438b50829d44262" dependencies = [ "anyhow", "async-trait", @@ -15331,9 +15494,9 @@ dependencies = [ [[package]] name = "zombienet-sdk" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09e5abdad4ad32c1c06cb8fdc4507db026f65987cb5c46ae4224118cc496097b" +checksum = "99a3c5f2d657235b3ab7dc384677e63cde21983029e99106766ecd49e9f8d7f3" dependencies = [ "async-trait", "futures", @@ -15349,9 +15512,9 @@ dependencies = [ [[package]] name = "zombienet-support" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e3310631948f8bb4d394c160c4b063889a4e0c6beadd6b95f9b62d1616ab93c" +checksum = "296f887ea88e07edd771f8e1d0dec5297a58b422f4b884a6292a21ebe03277cb" dependencies = [ "anyhow", "async-trait", diff --git a/Cargo.toml b/Cargo.toml index 86a7a928d..44dd1fbd3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,13 +50,12 @@ subxt = "0.38.0" ink_env = "5.0.0" sp-core = "32.0.0" sp-weights = "31.0.0" -contract-build = "5.0.0" -contract-extrinsics = "5.0.0" -contract-transcode = "5.0.0" scale-info = { version = "2.11.4", default-features = false, features = ["derive"] } scale-value = { version = "0.17.0", default-features = false, features = ["from-string", "parser-ss58"] } +contract-build = "5.0.2" +contract-extrinsics = "5.0.2" +contract-transcode = "5.0.2" heck = "0.5.0" -hex = { version = "0.4.3", default-features = false } # parachains askama = "0.12" @@ -77,3 +76,8 @@ console = "0.15" os_info = { version = "3", default-features = false } strum = "0.26" strum_macros = "0.26" + +# wallet-integration +axum = "0.7.9" +open = "5.3.1" +tower-http = "0.6.2" diff --git a/crates/pop-cli/Cargo.toml b/crates/pop-cli/Cargo.toml index c9761b642..34e51111d 100644 --- a/crates/pop-cli/Cargo.toml +++ b/crates/pop-cli/Cargo.toml @@ -18,6 +18,7 @@ duct.workspace = true env_logger.workspace = true os_info.workspace = true reqwest.workspace = true +serde = { workspace = true, features = ["derive"] } serde_json.workspace = true tempfile.workspace = true tokio.workspace = true @@ -27,12 +28,12 @@ url.workspace = true clap.workspace = true cliclack.workspace = true console.workspace = true +sp-core.workspace = true strum.workspace = true strum_macros.workspace = true # contracts pop-contracts = { path = "../pop-contracts", version = "0.5.0", optional = true } -sp-core = { workspace = true, optional = true } sp-weights = { workspace = true, optional = true } # parachains @@ -46,12 +47,21 @@ pop-telemetry = { path = "../pop-telemetry", version = "0.5.0", optional = true # common pop-common = { path = "../pop-common", version = "0.5.0" } +# wallet-integration +axum.workspace = true +open.workspace = true +tower-http = { workspace = true, features = ["fs", "cors"] } + [dev-dependencies] assert_cmd.workspace = true +contract-extrinsics.workspace = true predicates.workspace = true +subxt.workspace = true +subxt-signer.workspace = true +sp-weights.workspace = true [features] default = ["contract", "parachain", "telemetry"] -contract = ["dep:pop-contracts", "dep:sp-core", "dep:sp-weights", "dep:dirs"] +contract = ["dep:pop-contracts", "dep:sp-weights", "dep:dirs"] parachain = ["dep:pop-parachains", "dep:dirs"] telemetry = ["dep:pop-telemetry"] diff --git a/crates/pop-cli/src/assets/index.html b/crates/pop-cli/src/assets/index.html new file mode 100644 index 000000000..d1d5a8ef7 --- /dev/null +++ b/crates/pop-cli/src/assets/index.html @@ -0,0 +1,1024 @@ + + + + + + + + Pop CLI Signing Portal + + + + + +
+ + + diff --git a/crates/pop-cli/src/cli.rs b/crates/pop-cli/src/cli.rs index 6c6bb3bce..f98c934e9 100644 --- a/crates/pop-cli/src/cli.rs +++ b/crates/pop-cli/src/cli.rs @@ -150,7 +150,6 @@ impl traits::Confirm for Confirm { /// A input prompt using cliclack. struct Input(cliclack::Input); - impl traits::Input for Input { /// Sets the default value for the input. fn default_input(mut self, value: &str) -> Self { diff --git a/crates/pop-cli/src/commands/call/chain.rs b/crates/pop-cli/src/commands/call/chain.rs index 36b4dbd1e..41348826f 100644 --- a/crates/pop-cli/src/commands/call/chain.rs +++ b/crates/pop-cli/src/commands/call/chain.rs @@ -2,14 +2,17 @@ use std::path::Path; -use crate::cli::{self, traits::*}; +use crate::{ + cli::{self, traits::*}, + common::wallet::{prompt_to_use_wallet, request_signature}, +}; 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, supported_actions, Action, CallData, DynamicPayload, Function, - OnlineClient, Pallet, Param, SubstrateConfig, + sign_and_submit_extrinsic, submit_signed_extrinsic, supported_actions, Action, CallData, + DynamicPayload, Function, OnlineClient, Pallet, Param, Payload, SubstrateConfig, }; use url::Url; @@ -40,6 +43,15 @@ pub struct CallChainCommand { /// - with a password "//Alice///SECRET_PASSWORD" #[arg(short, long)] suri: Option, + /// Use a browser extension wallet to sign the extrinsic. + #[arg( + name = "use-wallet", + short = 'w', + long, + default_value = "false", + conflicts_with = "suri" + )] + use_wallet: bool, /// SCALE encoded bytes representing the call data of the extrinsic. #[arg(name = "call", short, long, conflicts_with_all = ["pallet", "function", "args"])] call_data: Option, @@ -95,7 +107,14 @@ impl CallChainCommand { }; // Sign and submit the extrinsic. - if let Err(e) = call.submit_extrinsic(&chain.client, &chain.url, xt, &mut cli).await { + let result = if self.use_wallet { + let call_data = xt.encode_call_data(&chain.client.metadata())?; + submit_extrinsic_with_wallet(&chain.client, &chain.url, call_data, &mut cli).await + } else { + call.submit_extrinsic(&chain.client, &chain.url, xt, &mut cli).await + }; + + if let Err(e) = result { display_message(&e.to_string(), false, &mut cli)?; break; } @@ -197,12 +216,8 @@ impl CallChainCommand { // 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()?, - }; + let (use_wallet, suri) = self.determine_signing_method(cli)?; + self.use_wallet = use_wallet; return Ok(Call { function: function.clone(), @@ -210,6 +225,7 @@ impl CallChainCommand { suri, skip_confirm: self.skip_confirm, sudo: self.sudo, + use_wallet: self.use_wallet, }); } } @@ -222,11 +238,18 @@ impl CallChainCommand { 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()?, - }; + let (use_wallet, suri) = self.determine_signing_method(cli)?; + + // Perform signing steps with wallet integration and return early. + if use_wallet { + let call_data_bytes = + decode_call_data(call_data).map_err(|err| anyhow!("{}", format!("{err:?}")))?; + submit_extrinsic_with_wallet(client, url, call_data_bytes, cli) + .await + .map_err(|err| anyhow!("{}", format!("{err:?}")))?; + display_message("Call complete.", true, cli)?; + return Ok(()); + } cli.info(format!("Encoded call data: {}", call_data))?; if !self.skip_confirm && !cli.confirm("Do you want to submit the extrinsic?") @@ -244,7 +267,7 @@ impl CallChainCommand { 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(client, url, CallData::new(call_data_bytes), suri) + let result = sign_and_submit_extrinsic(client, url, CallData::new(call_data_bytes), &suri) .await .map_err(|err| anyhow!("{}", format!("{err:?}")))?; @@ -253,6 +276,29 @@ impl CallChainCommand { Ok(()) } + // Resolve who is signing the extrinsic. If a `suri` was provided via the command line, + // skip the prompt. + fn determine_signing_method(&self, cli: &mut impl Cli) -> Result<(bool, String)> { + let mut use_wallet = self.use_wallet; + let suri = match self.suri.as_ref() { + Some(suri) => suri.clone(), + None => + if !self.use_wallet { + if prompt_to_use_wallet(cli)? { + use_wallet = true; + DEFAULT_URI.to_string() + } else { + cli.input("Signer of the extrinsic:") + .default_input(DEFAULT_URI) + .interact()? + } + } else { + DEFAULT_URI.to_string() + }, + }; + Ok((use_wallet, suri)) + } + // 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<()> { @@ -283,6 +329,7 @@ impl CallChainCommand { self.function = None; self.args.clear(); self.sudo = false; + self.use_wallet = false; } // Function to check if all required fields are specified. @@ -334,6 +381,8 @@ struct Call { /// - for a dev account "//Alice" /// - with a password "//Alice///SECRET_PASSWORD" suri: String, + /// Whether to use your browser wallet to sign the extrinsic. + use_wallet: bool, /// Whether to automatically sign and submit the extrinsic without prompting for confirmation. skip_confirm: bool, /// Whether to dispatch the function call with `Root` origin. @@ -411,7 +460,12 @@ impl Call { .collect(); full_message.push_str(&format!(" --args {}", args.join(" "))); } - full_message.push_str(&format!(" --url {} --suri {}", chain.url, self.suri)); + full_message.push_str(&format!(" --url {}", chain.url)); + if self.use_wallet { + full_message.push_str(" --use-wallet"); + } else { + full_message.push_str(&format!(" --suri {}", self.suri)); + } if self.sudo { full_message.push_str(" --sudo"); } @@ -419,6 +473,32 @@ impl Call { } } +// Sign and submit an extrinsic using wallet integration. +async fn submit_extrinsic_with_wallet( + client: &OnlineClient, + url: &Url, + call_data: Vec, + cli: &mut impl Cli, +) -> Result<()> { + let maybe_payload = request_signature(call_data, url.to_string()).await?; + if let Some(payload) = maybe_payload { + cli.success("Signed payload received.")?; + let spinner = cliclack::spinner(); + spinner.start( + "Submitting the extrinsic and then waiting for finalization, please be patient...", + ); + + let result = submit_signed_extrinsic(client.clone(), payload) + .await + .map_err(|err| anyhow!("{}", format!("{err:?}")))?; + + spinner.stop(format!("Extrinsic submitted with hash: {:?}", result)); + } else { + display_message("No signed payload received.", false, cli)?; + } + Ok(()) +} + // 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 { @@ -589,7 +669,7 @@ fn parse_function_name(name: &str) -> Result { #[cfg(test)] mod tests { use super::*; - use crate::cli::MockCli; + use crate::{cli::MockCli, common::wallet::USE_WALLET_PROMPT}; use tempfile::tempdir; use url::Url; @@ -642,7 +722,7 @@ mod tests { ) .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()); + .expect_confirm(USE_WALLET_PROMPT, true); let chain = call_config.configure_chain(&mut cli).await?; assert_eq!(chain.url, Url::parse(POP_NETWORK_TESTNET_URL)?); @@ -651,9 +731,10 @@ mod tests { 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_eq!(call_chain.suri, "//Alice"); // Default value + assert!(call_chain.use_wallet); 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"); + assert_eq!(call_chain.display(&chain), "pop call chain --pallet System --function remark --args \"0x11\" --url wss://rpc1.paseo.popnetwork.xyz/ --use-wallet --sudo"); cli.verify() } @@ -714,6 +795,7 @@ mod tests { }, args: vec!["0x11".to_string()].to_vec(), suri: DEFAULT_URI.to_string(), + use_wallet: false, skip_confirm: false, sudo: false, }; @@ -753,6 +835,7 @@ mod tests { function: find_dispatchable_by_name(&pallets, "System", "remark")?.clone(), args: vec!["0x11".to_string()].to_vec(), suri: DEFAULT_URI.to_string(), + use_wallet: false, skip_confirm: false, sudo: false, }; @@ -776,11 +859,13 @@ mod tests { args: vec![].to_vec(), url: Some(Url::parse(POP_NETWORK_TESTNET_URL)?), suri: None, + use_wallet: false, skip_confirm: false, call_data: Some("0x00000411".to_string()), sudo: false, }; let mut cli = MockCli::new() + .expect_confirm(USE_WALLET_PROMPT, false) .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."); @@ -803,8 +888,9 @@ mod tests { pallet: None, function: None, args: vec![].to_vec(), - url: Some(Url::parse("wss://polkadot-rpc.publicnode.com")?), + url: Some(Url::parse(POLKADOT_NETWORK_URL)?), suri: Some("//Alice".to_string()), + use_wallet: false, skip_confirm: false, call_data: Some("0x00000411".to_string()), sudo: true, @@ -836,6 +922,7 @@ mod tests { function: Some("remark".to_string()), args: vec!["0x11".to_string()].to_vec(), url: Some(Url::parse(POP_NETWORK_TESTNET_URL)?), + use_wallet: true, suri: Some(DEFAULT_URI.to_string()), skip_confirm: false, call_data: None, @@ -846,6 +933,7 @@ mod tests { assert_eq!(call_config.function, None); assert_eq!(call_config.args.len(), 0); assert!(!call_config.sudo); + assert!(!call_config.use_wallet); Ok(()) } @@ -857,6 +945,7 @@ mod tests { args: vec!["0x11".to_string()].to_vec(), url: Some(Url::parse(POP_NETWORK_TESTNET_URL)?), suri: Some(DEFAULT_URI.to_string()), + use_wallet: false, skip_confirm: false, call_data: None, sudo: false, @@ -875,6 +964,7 @@ mod tests { 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()), + use_wallet: false, call_data: None, skip_confirm: false, sudo: false, diff --git a/crates/pop-cli/src/commands/call/contract.rs b/crates/pop-cli/src/commands/call/contract.rs index e83a871d2..ab7a826fd 100644 --- a/crates/pop-cli/src/commands/call/contract.rs +++ b/crates/pop-cli/src/commands/call/contract.rs @@ -2,17 +2,22 @@ use crate::{ cli::{self, traits::*}, - common::contracts::has_contract_been_built, + common::{ + contracts::has_contract_been_built, + wallet::{prompt_to_use_wallet, request_signature}, + }, }; use anyhow::{anyhow, Result}; use clap::Args; use cliclack::spinner; +use pop_common::{DefaultConfig, Keypair}; use pop_contracts::{ - build_smart_contract, call_smart_contract, dry_run_call, dry_run_gas_estimate_call, - get_messages, parse_account, set_up_call, CallOpts, Verbosity, + build_smart_contract, call_smart_contract, call_smart_contract_from_signed_payload, + dry_run_call, dry_run_gas_estimate_call, get_call_payload, get_message, get_messages, + parse_account, set_up_call, CallExec, CallOpts, DefaultEnvironment, Verbosity, }; use sp_weights::Weight; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; const DEFAULT_URL: &str = "ws://localhost:9944/"; const DEFAULT_URI: &str = "//Alice"; @@ -54,6 +59,15 @@ pub struct CallContractCommand { /// - with a password "//Alice///SECRET_PASSWORD" #[arg(short, long, default_value = DEFAULT_URI)] suri: String, + /// Use a browser extension wallet to sign the extrinsic. + #[arg( + name = "use-wallet", + long, + short = 'w', + default_value = "false", + conflicts_with = "suri" + )] + use_wallet: bool, /// Submit an extrinsic for on-chain execution. #[arg(short = 'x', long)] execute: bool, @@ -115,14 +129,19 @@ impl CallContractCommand { full_message.push_str(&format!(" --gas {}", gas_limit)); } if let Some(proof_size) = self.proof_size { - full_message.push_str(&format!(" --proof_size {}", proof_size)); + full_message.push_str(&format!(" --proof-size {}", proof_size)); + } + full_message.push_str(&format!(" --url {}", self.url)); + if self.use_wallet { + full_message.push_str(" --use-wallet"); + } else { + full_message.push_str(&format!(" --suri {}", self.suri)); } - full_message.push_str(&format!(" --url {} --suri {}", self.url, self.suri)); if self.execute { full_message.push_str(" --execute"); } if self.dry_run { - full_message.push_str(" --dry_run"); + full_message.push_str(" --dry-run"); } full_message } @@ -165,7 +184,7 @@ impl CallContractCommand { fn is_contract_build_required(&self) -> bool { self.path .as_ref() - .map(|p| p.is_dir() && !has_contract_been_built(Some(&p))) + .map(|p| p.is_dir() && !has_contract_been_built(Some(p))) .unwrap_or_default() } @@ -303,18 +322,22 @@ impl CallContractCommand { self.proof_size = proof_size_input.parse::().ok(); // If blank or bad input, estimate it. } - // Resolve who is calling the contract. - if self.suri == DEFAULT_URI { - // Prompt for uri. - self.suri = cli - .input("Signer calling the contract:") - .placeholder("//Alice") - .default_input("//Alice") - .interact()?; - }; + // Resolve who is calling the contract. If a `suri` was provided via the command line, skip + // the prompt. + if self.suri == DEFAULT_URI && !self.use_wallet && message.mutates { + if prompt_to_use_wallet(cli)? { + self.use_wallet = true; + } else { + self.suri = cli + .input("Signer calling the contract:") + .placeholder("//Alice") + .default_input("//Alice") + .interact()?; + }; + } // Finally prompt for confirmation. - let is_call_confirmed = if message.mutates && !self.dev_mode { + let is_call_confirmed = if message.mutates && !self.dev_mode && !self.use_wallet { cli.confirm("Do you want to execute the call? (Selecting 'No' will perform a dry run)") .initial_value(true) .interact()? @@ -340,6 +363,14 @@ impl CallContractCommand { return Err(anyhow!("Please specify the message to call.")); }, }; + // Disable wallet signing and display warning if the call is read-only. + let message_metadata = + get_message(self.path.as_deref().unwrap_or_else(|| Path::new("./")), &message)?; + if !message_metadata.mutates && self.use_wallet { + cli.warning("NOTE: Signing is not required for this read-only call. The '--use-wallet' flag will be ignored.")?; + self.use_wallet = false; + } + let contract = match &self.contract { Some(contract) => contract.to_string(), None => { @@ -366,6 +397,12 @@ impl CallContractCommand { }, }; + // Perform signing steps with wallet integration, skipping secure signing for query-only + // operations. + if self.use_wallet { + self.execute_with_wallet(call_exec, cli).await?; + return self.finalize_execute_call(cli, prompt_to_repeat_call).await; + } if self.dry_run { let spinner = spinner(); spinner.start("Doing a dry run to estimate the gas..."); @@ -386,6 +423,7 @@ impl CallContractCommand { let spinner = spinner(); spinner.start("Calling the contract..."); let call_dry_run_result = dry_run_call(&call_exec).await?; + spinner.stop(""); cli.info(format!("Result: {}", call_dry_run_result))?; cli.warning("Your call has not been executed.")?; } else { @@ -414,7 +452,15 @@ impl CallContractCommand { cli.info(call_result)?; } + self.finalize_execute_call(cli, prompt_to_repeat_call).await + } + /// Finalize the current call, prompting the user to repeat or conclude the process. + async fn finalize_execute_call( + &mut self, + cli: &mut impl Cli, + prompt_to_repeat_call: bool, + ) -> Result<()> { // Prompt for any additional calls. if !prompt_to_repeat_call { display_message("Call completed successfully!", true, cli)?; @@ -435,12 +481,56 @@ impl CallContractCommand { } } + /// Execute the smart contract call using wallet integration. + async fn execute_with_wallet( + &self, + call_exec: CallExec, + cli: &mut impl Cli, + ) -> Result<()> { + let call_data = self.get_contract_data(&call_exec).map_err(|err| { + anyhow!("An error occurred getting the call data: {}", err.to_string()) + })?; + + let maybe_payload = request_signature(call_data, self.url.to_string()).await?; + if let Some(payload) = maybe_payload { + cli.success("Signed payload received.")?; + let spinner = spinner(); + spinner + .start("Calling the contract and waiting for finalization, please be patient..."); + + let call_result = + call_smart_contract_from_signed_payload(call_exec, payload, &self.url) + .await + .map_err(|err| anyhow!("{} {}", "ERROR:", format!("{err:?}")))?; + + cli.info(call_result)?; + } else { + display_message("No signed payload received.", false, cli)?; + } + Ok(()) + } + + // Get the call data. + fn get_contract_data( + &self, + call_exec: &CallExec, + ) -> anyhow::Result> { + let weight_limit = if self.gas_limit.is_some() && self.proof_size.is_some() { + Weight::from_parts(self.gas_limit.unwrap(), self.proof_size.unwrap()) + } else { + Weight::zero() + }; + let call_data = get_call_payload(call_exec, weight_limit)?; + Ok(call_data) + } + /// Resets message specific fields to default values for a new call. fn reset_for_new_call(&mut self) { self.message = None; self.value = DEFAULT_PAYABLE_VALUE.to_string(); self.gas_limit = None; self.proof_size = None; + self.use_wallet = false; } } @@ -456,7 +546,7 @@ fn display_message(message: &str, success: bool, cli: &mut impl Cli) -> Result<( #[cfg(test)] mod tests { use super::*; - use crate::cli::MockCli; + use crate::{cli::MockCli, common::wallet::USE_WALLET_PROMPT}; use pop_contracts::{mock_build_process, new_environment}; use std::{env, fs::write}; use url::Url; @@ -482,6 +572,7 @@ mod tests { proof_size: None, url: Url::parse("wss://rpc1.paseo.popnetwork.xyz")?, suri: "//Alice".to_string(), + use_wallet: false, dry_run: false, execute: false, dev_mode: false, @@ -517,13 +608,14 @@ mod tests { proof_size: Some(10), url: Url::parse("wss://rpc1.paseo.popnetwork.xyz")?, suri: "//Alice".to_string(), + use_wallet: false, dry_run: true, execute: false, dev_mode: false, }; call_config.configure(&mut cli, false).await?; assert_eq!(call_config.display(), format!( - "pop call contract --path {} --contract 15XausWjFLBBFLDXUSBRfSfZk25warm4wZRV4ZxhZbfvjrJm --message flip --gas 100 --proof_size 10 --url wss://rpc1.paseo.popnetwork.xyz/ --suri //Alice --dry_run", + "pop call contract --path {} --contract 15XausWjFLBBFLDXUSBRfSfZk25warm4wZRV4ZxhZbfvjrJm --message flip --gas 100 --proof-size 10 --url wss://rpc1.paseo.popnetwork.xyz/ --suri //Alice --dry-run", temp_dir.path().join("testing").display().to_string(), )); // Contract deployed on Pop Network testnet, test dry-run @@ -553,13 +645,14 @@ mod tests { proof_size: Some(10), url: Url::parse("wss://rpc1.paseo.popnetwork.xyz")?, suri: "//Alice".to_string(), + use_wallet: false, dry_run: true, execute: false, dev_mode: false, }; call_config.configure(&mut cli, false).await?; assert_eq!(call_config.display(), format!( - "pop call contract --path {} --contract 15XausWjFLBBFLDXUSBRfSfZk25warm4wZRV4ZxhZbfvjrJm --message flip --gas 100 --proof_size 10 --url wss://rpc1.paseo.popnetwork.xyz/ --suri //Alice --dry_run", + "pop call contract --path {} --contract 15XausWjFLBBFLDXUSBRfSfZk25warm4wZRV4ZxhZbfvjrJm --message flip --gas 100 --proof-size 10 --url wss://rpc1.paseo.popnetwork.xyz/ --suri //Alice --dry-run", current_dir.join("pop-contracts/tests/files/testing.contract").display().to_string(), )); // Contract deployed on Pop Network testnet, test dry-run @@ -569,7 +662,7 @@ mod tests { call_config.path = Some(current_dir.join("pop-contracts/tests/files/testing.json")); call_config.configure(&mut cli, false).await?; assert_eq!(call_config.display(), format!( - "pop call contract --path {} --contract 15XausWjFLBBFLDXUSBRfSfZk25warm4wZRV4ZxhZbfvjrJm --message flip --gas 100 --proof_size 10 --url wss://rpc1.paseo.popnetwork.xyz/ --suri //Alice --dry_run", + "pop call contract --path {} --contract 15XausWjFLBBFLDXUSBRfSfZk25warm4wZRV4ZxhZbfvjrJm --message flip --gas 100 --proof-size 10 --url wss://rpc1.paseo.popnetwork.xyz/ --suri //Alice --dry-run", current_dir.join("pop-contracts/tests/files/testing.json").display().to_string(), )); @@ -577,7 +670,7 @@ mod tests { call_config.path = Some(current_dir.join("pop-contracts/tests/files/testing.wasm")); call_config.configure(&mut cli, false).await?; assert_eq!(call_config.display(), format!( - "pop call contract --path {} --contract 15XausWjFLBBFLDXUSBRfSfZk25warm4wZRV4ZxhZbfvjrJm --message flip --gas 100 --proof_size 10 --url wss://rpc1.paseo.popnetwork.xyz/ --suri //Alice --dry_run", + "pop call contract --path {} --contract 15XausWjFLBBFLDXUSBRfSfZk25warm4wZRV4ZxhZbfvjrJm --message flip --gas 100 --proof-size 10 --url wss://rpc1.paseo.popnetwork.xyz/ --suri //Alice --dry-run", current_dir.join("pop-contracts/tests/files/testing.wasm").display().to_string(), )); // Contract deployed on Pop Network testnet, test dry-run @@ -608,10 +701,6 @@ mod tests { "Do you want to perform another call using the existing smart contract?", true, ) - .expect_confirm( - "Do you want to perform another call using the existing smart contract?", - false, - ) .expect_select( "Select the message to call:", Some(false), @@ -619,12 +708,16 @@ mod tests { Some(items), 1, // "get" message ) - .expect_input("Signer calling the contract:", "//Alice".into()) .expect_info(format!( "pop call contract --path {} --contract 15XausWjFLBBFLDXUSBRfSfZk25warm4wZRV4ZxhZbfvjrJm --message get --url wss://rpc1.paseo.popnetwork.xyz/ --suri //Alice", temp_dir.path().join("testing").display().to_string(), )) + .expect_warning("NOTE: Signing is not required for this read-only call. The '--use-wallet' flag will be ignored.") .expect_warning("Your call has not been executed.") + .expect_confirm( + "Do you want to perform another call using the existing smart contract?", + false, + ) .expect_outro("Contract calling complete."); // Contract deployed on Pop Network testnet, test get @@ -638,6 +731,7 @@ mod tests { proof_size: None, url: Url::parse("wss://rpc1.paseo.popnetwork.xyz")?, suri: "//Alice".to_string(), + use_wallet: true, dry_run: false, execute: false, dev_mode: false, @@ -688,7 +782,6 @@ mod tests { "Provide the on-chain contract address:", "15XausWjFLBBFLDXUSBRfSfZk25warm4wZRV4ZxhZbfvjrJm".into(), ) - .expect_input("Signer calling the contract:", "//Alice".into()) .expect_info(format!( "pop call contract --path {} --contract 15XausWjFLBBFLDXUSBRfSfZk25warm4wZRV4ZxhZbfvjrJm --message get --url wss://rpc1.paseo.popnetwork.xyz/ --suri //Alice", temp_dir.path().join("testing").display().to_string(), @@ -704,6 +797,7 @@ mod tests { proof_size: None, url: Url::parse(DEFAULT_URL)?, suri: DEFAULT_URI.to_string(), + use_wallet: false, dry_run: false, execute: false, dev_mode: false, @@ -750,14 +844,6 @@ mod tests { ]; // The inputs are processed in reverse order. let mut cli = MockCli::new() - .expect_confirm("Do you want to execute the call? (Selecting 'No' will perform a dry run)", true) - .expect_select( - "Select the message to call:", - Some(false), - true, - Some(items), - 2, // "specific_flip" message - ) .expect_input( "Where is your project or contract artifact located?", temp_dir.path().join("testing").display().to_string(), @@ -770,14 +856,21 @@ mod tests { "Provide the on-chain contract address:", "15XausWjFLBBFLDXUSBRfSfZk25warm4wZRV4ZxhZbfvjrJm".into(), ) + .expect_select( + "Select the message to call:", + Some(false), + true, + Some(items), + 2, // "specific_flip" message + ) .expect_input("Enter the value for the parameter: new_value", "true".into()) // Args for specific_flip .expect_input("Enter the value for the parameter: number", "2".into()) // Args for specific_flip .expect_input("Value to transfer to the call:", "50".into()) // Only if payable .expect_input("Enter the gas limit:", "".into()) // Only if call .expect_input("Enter the proof size limit:", "".into()) // Only if call - .expect_input("Signer calling the contract:", "//Alice".into()) + .expect_confirm(USE_WALLET_PROMPT, true) .expect_info(format!( - "pop call contract --path {} --contract 15XausWjFLBBFLDXUSBRfSfZk25warm4wZRV4ZxhZbfvjrJm --message specific_flip --args \"true\", \"2\" --value 50 --url wss://rpc1.paseo.popnetwork.xyz/ --suri //Alice --execute", + "pop call contract --path {} --contract 15XausWjFLBBFLDXUSBRfSfZk25warm4wZRV4ZxhZbfvjrJm --message specific_flip --args \"true\", \"2\" --value 50 --url wss://rpc1.paseo.popnetwork.xyz/ --use-wallet --execute", temp_dir.path().join("testing").display().to_string(), )); @@ -791,6 +884,7 @@ mod tests { proof_size: None, url: Url::parse(DEFAULT_URL)?, suri: DEFAULT_URI.to_string(), + use_wallet: false, dry_run: false, execute: false, dev_mode: false, @@ -809,10 +903,11 @@ mod tests { assert_eq!(call_config.proof_size, None); assert_eq!(call_config.url.to_string(), "wss://rpc1.paseo.popnetwork.xyz/"); assert_eq!(call_config.suri, "//Alice"); + assert!(call_config.use_wallet); assert!(call_config.execute); assert!(!call_config.dry_run); assert_eq!(call_config.display(), format!( - "pop call contract --path {} --contract 15XausWjFLBBFLDXUSBRfSfZk25warm4wZRV4ZxhZbfvjrJm --message specific_flip --args \"true\", \"2\" --value 50 --url wss://rpc1.paseo.popnetwork.xyz/ --suri //Alice --execute", + "pop call contract --path {} --contract 15XausWjFLBBFLDXUSBRfSfZk25warm4wZRV4ZxhZbfvjrJm --message specific_flip --args \"true\", \"2\" --value 50 --url wss://rpc1.paseo.popnetwork.xyz/ --use-wallet --execute", temp_dir.path().join("testing").display().to_string(), )); @@ -877,6 +972,7 @@ mod tests { proof_size: None, url: Url::parse(DEFAULT_URL)?, suri: DEFAULT_URI.to_string(), + use_wallet: false, dry_run: false, execute: false, dev_mode: true, @@ -935,6 +1031,7 @@ mod tests { proof_size: None, url: Url::parse("wss://rpc1.paseo.popnetwork.xyz")?, suri: "//Alice".to_string(), + use_wallet: false, dry_run: false, execute: false, dev_mode: false, @@ -984,6 +1081,7 @@ mod tests { proof_size: None, url: Url::parse("wss://rpc1.paseo.popnetwork.xyz")?, suri: "//Alice".to_string(), + use_wallet: false, dry_run: false, execute: false, dev_mode: false, @@ -1002,6 +1100,7 @@ mod tests { proof_size: None, url: Url::parse("wss://rpc1.paseo.popnetwork.xyz")?, suri: "//Alice".to_string(), + use_wallet: false, dry_run: false, execute: false, dev_mode: false, @@ -1025,6 +1124,7 @@ mod tests { proof_size: None, url: Url::parse("wss://rpc1.paseo.popnetwork.xyz")?, suri: "//Alice".to_string(), + use_wallet: false, dry_run: false, execute: false, dev_mode: false, @@ -1055,6 +1155,7 @@ mod tests { proof_size: None, url: Url::parse("wss://rpc1.paseo.popnetwork.xyz")?, suri: "//Alice".to_string(), + use_wallet: false, dry_run: false, execute: false, dev_mode: false, diff --git a/crates/pop-cli/src/commands/test/contract.rs b/crates/pop-cli/src/commands/test/contract.rs index 190d9ac64..05e842eca 100644 --- a/crates/pop-cli/src/commands/test/contract.rs +++ b/crates/pop-cli/src/commands/test/contract.rs @@ -48,7 +48,13 @@ impl TestContractCommand { sleep(Duration::from_secs(3)).await; } - self.node = match check_contracts_node_and_prompt(self.skip_confirm).await { + self.node = match check_contracts_node_and_prompt( + &mut Cli, + &crate::cache()?, + self.skip_confirm, + ) + .await + { Ok(binary_path) => Some(binary_path), Err(_) => { warning("🚫 substrate-contracts-node is necessary to run e2e tests. Will try to run tests anyway...")?; diff --git a/crates/pop-cli/src/commands/up/contract.rs b/crates/pop-cli/src/commands/up/contract.rs index 40e158032..f46c0776f 100644 --- a/crates/pop-cli/src/commands/up/contract.rs +++ b/crates/pop-cli/src/commands/up/contract.rs @@ -2,23 +2,25 @@ use crate::{ cli::{traits::Cli as _, Cli}, - common::contracts::{check_contracts_node_and_prompt, has_contract_been_built}, + common::{ + contracts::{check_contracts_node_and_prompt, has_contract_been_built, terminate_node}, + wallet::request_signature, + }, style::style, }; use clap::Args; -use cliclack::{confirm, log, log::error, spinner}; +use cliclack::{confirm, log, log::error, spinner, ProgressBar}; use console::{Emoji, Style}; use pop_contracts::{ build_smart_contract, dry_run_gas_estimate_instantiate, dry_run_upload, - instantiate_smart_contract, is_chain_alive, parse_hex_bytes, run_contracts_node, - set_up_deployment, set_up_upload, upload_smart_contract, UpOpts, Verbosity, + get_code_hash_from_event, get_contract_code, get_instantiate_payload, get_upload_payload, + instantiate_contract_signed, instantiate_smart_contract, is_chain_alive, parse_hex_bytes, + run_contracts_node, set_up_deployment, set_up_upload, upload_contract_signed, + upload_smart_contract, UpOpts, Verbosity, }; use sp_core::Bytes; use sp_weights::Weight; -use std::{ - path::PathBuf, - process::{Child, Command}, -}; +use std::path::PathBuf; use tempfile::NamedTempFile; use url::Url; @@ -64,6 +66,15 @@ pub struct UpContractCommand { /// - with a password "//Alice///SECRET_PASSWORD" #[clap(short, long, default_value = "//Alice")] suri: String, + /// Use a browser extension wallet to sign the extrinsic. + #[clap( + name = "use-wallet", + long, + default_value = "false", + short('w'), + conflicts_with = "suri" + )] + use_wallet: bool, /// Perform a dry-run via RPC to estimate the gas usage. This does not submit a transaction. #[clap(short = 'D', long)] dry_run: bool, @@ -128,7 +139,13 @@ impl UpContractCommand { let log = NamedTempFile::new()?; // uses the cache location - let binary_path = match check_contracts_node_and_prompt(self.skip_confirm).await { + let binary_path = match check_contracts_node_and_prompt( + &mut Cli, + &crate::cache()?, + self.skip_confirm, + ) + .await + { Ok(binary_path) => binary_path, Err(_) => { Cli.outro_cancel( @@ -164,10 +181,89 @@ impl UpContractCommand { None }; + // Run steps for signing with wallet integration. Returns early. + if self.use_wallet { + let (call_data, hash) = match self.get_contract_data().await { + Ok(data) => data, + Err(e) => { + error(format!("An error occurred getting the call data: {e}"))?; + terminate_node(&mut Cli, process)?; + Cli.outro_cancel(FAILED)?; + return Ok(()); + }, + }; + + let maybe_payload = request_signature(call_data, self.url.to_string()).await?; + if let Some(payload) = maybe_payload { + log::success("Signed payload received.")?; + let spinner = spinner(); + spinner.start( + "Uploading the contract and waiting for finalization, please be patient...", + ); + + if self.upload_only { + let upload_result = match upload_contract_signed(self.url.as_str(), payload) + .await + { + Err(e) => { + spinner + .error(format!("An error occurred uploading your contract: {e}")); + terminate_node(&mut Cli, process)?; + Cli.outro_cancel(FAILED)?; + return Ok(()); + }, + Ok(result) => result, + }; + + match get_code_hash_from_event(&upload_result, hash) { + Ok(r) => { + spinner.stop(format!("Contract uploaded: The code hash is {:?}", r)); + }, + Err(e) => { + spinner + .error(format!("An error occurred uploading your contract: {e}")); + }, + }; + } else { + let contract_info = + match instantiate_contract_signed(self.url.as_str(), payload).await { + Err(e) => { + spinner.error(format!( + "An error occurred uploading your contract: {e}" + )); + terminate_node(&mut Cli, process)?; + Cli.outro_cancel(FAILED)?; + return Ok(()); + }, + Ok(result) => result, + }; + + let hash = contract_info.code_hash.map(|code_hash| format!("{:?}", code_hash)); + display_contract_info( + &spinner, + contract_info.contract_address.to_string(), + hash, + ); + }; + + if self.upload_only { + log::warning("NOTE: The contract has not been instantiated.")?; + } + } else { + Cli.outro_cancel("Signed payload doesn't exist.")?; + terminate_node(&mut Cli, process)?; + return Ok(()); + } + + terminate_node(&mut Cli, process)?; + Cli.outro(COMPLETE)?; + return Ok(()); + } + // Check for upload only. if self.upload_only { let result = self.upload_contract().await; - Self::terminate_node(process)?; + terminate_node(&mut Cli, process)?; match result { Ok(_) => { Cli.outro(COMPLETE)?; @@ -180,23 +276,11 @@ impl UpContractCommand { } // Otherwise instantiate. - let instantiate_exec = match set_up_deployment(UpOpts { - path: self.path.clone(), - constructor: self.constructor.clone(), - args: self.args.clone(), - value: self.value.clone(), - gas_limit: self.gas_limit, - proof_size: self.proof_size, - salt: self.salt.clone(), - url: self.url.clone(), - suri: self.suri.clone(), - }) - .await - { + let instantiate_exec = match set_up_deployment(self.clone().into()).await { Ok(i) => i, Err(e) => { error(format!("An error occurred instantiating the contract: {e}"))?; - Self::terminate_node(process)?; + terminate_node(&mut Cli, process)?; Cli.outro_cancel(FAILED)?; return Ok(()); }, @@ -214,7 +298,7 @@ impl UpContractCommand { }, Err(e) => { spinner.error(format!("{e}")); - Self::terminate_node(process)?; + terminate_node(&mut Cli, process)?; Cli.outro_cancel(FAILED)?; return Ok(()); }, @@ -226,30 +310,13 @@ impl UpContractCommand { let spinner = spinner(); spinner.start("Uploading and instantiating the contract..."); let contract_info = instantiate_smart_contract(instantiate_exec, weight_limit).await?; - spinner.stop(format!( - "Contract deployed and instantiated:\n{}", - style(format!( - "{}\n{}", - style(format!( - "{} The contract address is {:?}", - console::Emoji("●", ">"), - contract_info.address - )) - .dim(), - contract_info - .code_hash - .map(|hash| style(format!( - "{} The contract code hash is {:?}", - console::Emoji("●", ">"), - hash - )) - .dim() - .to_string()) - .unwrap_or_default(), - )) - .dim() - )); - Self::terminate_node(process)?; + display_contract_info( + &spinner, + contract_info.address.to_string(), + contract_info.code_hash, + ); + + terminate_node(&mut Cli, process)?; Cli.outro(COMPLETE)?; } @@ -291,27 +358,25 @@ impl UpContractCommand { Ok(()) } - /// Handles the optional termination of a local running node. - fn terminate_node(process: Option<(Child, NamedTempFile)>) -> anyhow::Result<()> { - // Prompt to close any launched node - let Some((process, log)) = process else { - return Ok(()); - }; - if confirm("Would you like to terminate the local node?") - .initial_value(true) - .interact()? - { - // Stop the process contracts-node - Command::new("kill") - .args(["-s", "TERM", &process.id().to_string()]) - .spawn()? - .wait()?; + // get the call data and contract code hash + async fn get_contract_data(&self) -> anyhow::Result<(Vec, [u8; 32])> { + let contract_code = get_contract_code(self.path.as_ref())?; + let hash = contract_code.code_hash(); + if self.upload_only { + let call_data = get_upload_payload(contract_code, self.url.as_str()).await?; + Ok((call_data, hash)) } else { - log.keep()?; - log::warning(format!("NOTE: The node is running in the background with process ID {}. Please terminate it manually when done.", process.id()))?; - } + let instantiate_exec = set_up_deployment(self.clone().into()).await?; - Ok(()) + let weight_limit = if self.gas_limit.is_some() && self.proof_size.is_some() { + Weight::from_parts(self.gas_limit.unwrap(), self.proof_size.unwrap()) + } else { + // Frontend will do dry run and update call data. + Weight::zero() + }; + let call_data = get_instantiate_payload(instantiate_exec, weight_limit)?; + Ok((call_data, hash)) + } } } @@ -331,14 +396,44 @@ impl From for UpOpts { } } +fn display_contract_info(spinner: &ProgressBar, address: String, code_hash: Option) { + spinner.stop(format!( + "Contract deployed and instantiated:\n{}", + style(format!( + "{}\n{}", + style(format!("{} The contract address is {:?}", console::Emoji("●", ">"), address)) + .dim(), + code_hash + .map(|hash| style(format!( + "{} The contract code hash is {:?}", + console::Emoji("●", ">"), + hash + )) + .dim() + .to_string()) + .unwrap_or_default(), + )) + .dim() + )); +} + #[cfg(test)] mod tests { use super::*; + use pop_common::{find_free_port, set_executable_permission}; + use pop_contracts::{contracts_node_generator, mock_build_process, new_environment}; + use std::{ + env, + process::{Child, Command}, + time::Duration, + }; + use subxt::{tx::Payload, SubstrateConfig}; + use tempfile::TempDir; + use tokio::time::sleep; use url::Url; - #[test] - fn conversion_up_contract_command_to_up_opts_works() -> anyhow::Result<()> { - let command = UpContractCommand { + fn default_up_contract_command() -> UpContractCommand { + UpContractCommand { path: None, constructor: "new".to_string(), args: vec![], @@ -346,12 +441,40 @@ mod tests { gas_limit: None, proof_size: None, salt: None, - url: Url::parse("ws://localhost:9944")?, + url: Url::parse("ws://localhost:9944").expect("default url is valid"), suri: "//Alice".to_string(), dry_run: false, upload_only: false, skip_confirm: false, - }; + use_wallet: false, + } + } + + async fn start_test_environment() -> anyhow::Result<(Child, u16, TempDir)> { + let random_port = find_free_port(None); + let temp_dir = new_environment("testing")?; + let current_dir = env::current_dir().expect("Failed to get current directory"); + mock_build_process( + temp_dir.path().join("testing"), + current_dir.join("../pop-contracts/tests/files/testing.contract"), + current_dir.join("../pop-contracts/tests/files/testing.json"), + )?; + let cache = temp_dir.path().join(""); + let binary = contracts_node_generator(cache.clone(), None).await?; + binary.source(false, &(), true).await?; + set_executable_permission(binary.path())?; + let process = run_contracts_node(binary.path(), None, random_port).await?; + Ok((process, random_port, temp_dir)) + } + + fn stop_test_environment(id: &str) -> anyhow::Result<()> { + Command::new("kill").args(["-s", "TERM", id]).spawn()?.wait()?; + Ok(()) + } + + #[test] + fn conversion_up_contract_command_to_up_opts_works() -> anyhow::Result<()> { + let command = default_up_contract_command(); let opts: UpOpts = command.into(); assert_eq!( opts, @@ -369,4 +492,108 @@ mod tests { ); Ok(()) } + + #[tokio::test] + async fn get_upload_and_instantiate_call_data_works() -> anyhow::Result<()> { + let (contracts_node_process, port, temp_dir) = start_test_environment().await?; + sleep(Duration::from_secs(5)).await; + + get_upload_call_data_works(port, temp_dir.path().join("testing")).await?; + get_instantiate_call_data_works(port, temp_dir.path().join("testing")).await?; + + // Stop running contracts-node + stop_test_environment(&contracts_node_process.id().to_string())?; + Ok(()) + } + + async fn get_upload_call_data_works(port: u16, temp_dir: PathBuf) -> anyhow::Result<()> { + let localhost_url = format!("ws://127.0.0.1:{}", port); + + let up_contract_opts = UpContractCommand { + path: Some(temp_dir), + constructor: "new".to_string(), + args: vec![], + value: "0".to_string(), + gas_limit: None, + proof_size: None, + salt: None, + url: Url::parse(&localhost_url).expect("given url is valid"), + suri: "//Alice".to_string(), + dry_run: false, + upload_only: true, + skip_confirm: true, + use_wallet: true, + }; + + let rpc_client = subxt::backend::rpc::RpcClient::from_url(&up_contract_opts.url).await?; + let client = subxt::OnlineClient::::from_rpc_client(rpc_client).await?; + + // Retrieve call data based on the above command options. + let (retrieved_call_data, _) = match up_contract_opts.get_contract_data().await { + Ok(data) => data, + Err(e) => { + error(format!("An error occurred getting the call data: {e}"))?; + return Err(e); + }, + }; + // We have retrieved some payload. + assert!(!retrieved_call_data.is_empty()); + + // Craft encoded call data for an upload code call. + let contract_code = get_contract_code(up_contract_opts.path.as_ref())?; + let storage_deposit_limit: Option = None; + let upload_code = contract_extrinsics::extrinsic_calls::UploadCode::new( + contract_code, + storage_deposit_limit, + contract_extrinsics::upload::Determinism::Enforced, + ); + let expected_call_data = upload_code.build(); + let mut encoded_expected_call_data = Vec::::new(); + expected_call_data + .encode_call_data_to(&client.metadata(), &mut encoded_expected_call_data)?; + + // Retrieved call data and calculated match. + assert_eq!(retrieved_call_data, encoded_expected_call_data); + Ok(()) + } + + async fn get_instantiate_call_data_works(port: u16, temp_dir: PathBuf) -> anyhow::Result<()> { + let localhost_url = format!("ws://127.0.0.1:{}", port); + + let up_contract_opts = UpContractCommand { + path: Some(temp_dir), + constructor: "new".to_string(), + args: vec!["false".to_string()], + value: "0".to_string(), + gas_limit: Some(200_000_000), + proof_size: Some(30_000), + salt: None, + url: Url::parse(&localhost_url).expect("given url is valid"), + suri: "//Alice".to_string(), + dry_run: false, + upload_only: false, + skip_confirm: true, + use_wallet: true, + }; + + // Retrieve call data based on the above command options. + let (retrieved_call_data, _) = match up_contract_opts.get_contract_data().await { + Ok(data) => data, + Err(e) => { + error(format!("An error occurred getting the call data: {e}"))?; + return Err(e); + }, + }; + // We have retrieved some payload. + assert!(!retrieved_call_data.is_empty()); + + // Craft instantiate call data. + let weight = Weight::from_parts(200_000_000, 30_000); + let expected_call_data = + get_instantiate_payload(set_up_deployment(up_contract_opts.into()).await?, weight)?; + // Retrieved call data matches the one crafted above. + assert_eq!(retrieved_call_data, expected_call_data); + + Ok(()) + } } diff --git a/crates/pop-cli/src/common/contracts.rs b/crates/pop-cli/src/common/contracts.rs index 3d0be11f6..a3b8f878c 100644 --- a/crates/pop-cli/src/common/contracts.rs +++ b/crates/pop-cli/src/common/contracts.rs @@ -1,22 +1,33 @@ // SPDX-License-Identifier: GPL-3.0 -use cliclack::{confirm, log::warning, spinner}; +use crate::cli::traits::*; +use cliclack::spinner; use pop_common::{manifest::from_path, sourcing::set_executable_permission}; use pop_contracts::contracts_node_generator; -use std::path::{Path, PathBuf}; +use std::{ + path::{Path, PathBuf}, + process::{Child, Command}, +}; +use tempfile::NamedTempFile; /// Checks the status of the `substrate-contracts-node` binary, sources it if necessary, and /// prompts the user to update it if the existing binary is not the latest version. /// /// # Arguments +/// * `cli`: Command line interface. +/// * `cache_path`: The cache directory path. /// * `skip_confirm`: A boolean indicating whether to skip confirmation prompts. -pub async fn check_contracts_node_and_prompt(skip_confirm: bool) -> anyhow::Result { - let cache_path: PathBuf = crate::cache()?; - let mut binary = contracts_node_generator(cache_path, None).await?; +pub async fn check_contracts_node_and_prompt( + cli: &mut impl Cli, + cache_path: &Path, + skip_confirm: bool, +) -> anyhow::Result { + let mut binary = contracts_node_generator(PathBuf::from(cache_path), None).await?; let mut node_path = binary.path(); if !binary.exists() { - warning("⚠️ The substrate-contracts-node binary is not found.")?; - if confirm("📦 Would you like to source it automatically now?") + cli.warning("⚠️ The substrate-contracts-node binary is not found.")?; + if cli + .confirm("📦 Would you like to source it automatically now?") .initial_value(true) .interact()? { @@ -33,14 +44,14 @@ pub async fn check_contracts_node_and_prompt(skip_confirm: bool) -> anyhow::Resu } } if binary.stale() { - warning(format!( + cli.warning(format!( "ℹ️ There is a newer version of {} available:\n {} -> {}", binary.name(), binary.version().unwrap_or("None"), binary.latest().unwrap_or("None") ))?; let latest = if !skip_confirm { - confirm( + cli.confirm( "📦 Would you like to source it automatically now? It may take some time..." .to_string(), ) @@ -68,6 +79,36 @@ pub async fn check_contracts_node_and_prompt(skip_confirm: bool) -> anyhow::Resu Ok(node_path) } +/// Handles the optional termination of a local running node. +/// # Arguments +/// * `cli`: Command line interface. +/// * `process`: Tuple identifying the child process to terminate and its log file. +pub fn terminate_node( + cli: &mut impl Cli, + process: Option<(Child, NamedTempFile)>, +) -> anyhow::Result<()> { + // Prompt to close any launched node + let Some((process, log)) = process else { + return Ok(()); + }; + if cli + .confirm("Would you like to terminate the local node?") + .initial_value(true) + .interact()? + { + // Stop the process contracts-node + Command::new("kill") + .args(["-s", "TERM", &process.id().to_string()]) + .spawn()? + .wait()?; + } else { + log.keep()?; + cli.warning(format!("NOTE: The node is running in the background with process ID {}. Please terminate it manually when done.", process.id()))?; + } + + Ok(()) +} + /// Checks if a contract has been built by verifying the existence of the build directory and the /// .contract file. /// @@ -88,8 +129,12 @@ pub fn has_contract_been_built(path: Option<&Path>) -> bool { #[cfg(test)] mod tests { use super::*; + use crate::cli::MockCli; use duct::cmd; + use pop_common::find_free_port; + use pop_contracts::{is_chain_alive, run_contracts_node}; use std::fs::{self, File}; + use url::Url; #[test] fn has_contract_been_built_works() -> anyhow::Result<()> { @@ -111,4 +156,38 @@ mod tests { assert!(has_contract_been_built(Some(&path.join(name)))); Ok(()) } + + #[tokio::test] + async fn check_contracts_node_and_prompt_works() -> anyhow::Result<()> { + let cache_path = tempfile::tempdir().expect("Could create temp dir"); + let mut cli = MockCli::new() + .expect_warning("⚠️ The substrate-contracts-node binary is not found.") + .expect_confirm("📦 Would you like to source it automatically now?", true) + .expect_warning("⚠️ The substrate-contracts-node binary is not found."); + + let node_path = check_contracts_node_and_prompt(&mut cli, cache_path.path(), false).await?; + // Binary path is at least equal to the cache path + "substrate-contracts-node". + assert!(node_path + .to_str() + .unwrap() + .starts_with(&cache_path.path().join("substrate-contracts-node").to_str().unwrap())); + cli.verify() + } + + #[tokio::test] + async fn node_is_terminated() -> anyhow::Result<()> { + let cache = tempfile::tempdir().expect("Could not create temp dir"); + let binary = contracts_node_generator(PathBuf::from(cache.path()), None).await?; + binary.source(false, &(), true).await?; + set_executable_permission(binary.path())?; + let port = find_free_port(None); + let process = run_contracts_node(binary.path(), None, port).await?; + let log = NamedTempFile::new()?; + // Terminate the process. + let mut cli = + MockCli::new().expect_confirm("Would you like to terminate the local node?", true); + assert!(terminate_node(&mut cli, Some((process, log))).is_ok()); + assert_eq!(is_chain_alive(Url::parse(&format!("ws://localhost:{}", port))?).await?, false); + cli.verify() + } } diff --git a/crates/pop-cli/src/common/mod.rs b/crates/pop-cli/src/common/mod.rs index 1cb3ee579..4a89036e5 100644 --- a/crates/pop-cli/src/common/mod.rs +++ b/crates/pop-cli/src/common/mod.rs @@ -3,3 +3,4 @@ #[cfg(feature = "contract")] pub mod contracts; pub mod helpers; +pub mod wallet; diff --git a/crates/pop-cli/src/common/wallet.rs b/crates/pop-cli/src/common/wallet.rs new file mode 100644 index 000000000..1aedd0294 --- /dev/null +++ b/crates/pop-cli/src/common/wallet.rs @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: GPL-3.0 + +use crate::{ + cli::traits::Cli, + wallet_integration::{FrontendFromString, TransactionData, WalletIntegrationManager}, +}; +use cliclack::{log, spinner}; + +/// The prompt to ask the user if they want to use the wallet for signing. +pub const USE_WALLET_PROMPT: &str = "Do you want to use your browser wallet to sign the extrinsic? (Selecting 'No' will prompt you to manually enter the secret key URI for signing, e.g., '//Alice')"; + +/// Launches the wallet integration for in-browser signing. Blocks until the signature is received. +/// +/// # Arguments +/// * `call_data` - The call data to be signed. +/// * `url` - Chain rpc. +/// # Returns +/// * The signed payload, if it exists. +pub async fn request_signature(call_data: Vec, rpc: String) -> anyhow::Result> { + let ui = FrontendFromString::new(include_str!("../assets/index.html").to_string()); + + let transaction_data = TransactionData::new(rpc, call_data); + // Starts server with port 9090. + let mut wallet = WalletIntegrationManager::new(ui, transaction_data, Some(9090)); + let url = format!("http://{}", &wallet.server_url); + log::step(format!("Wallet signing portal started at {url}."))?; + + let spinner = spinner(); + spinner.start(format!("Opening browser to {url}")); + if let Err(e) = open::that(url) { + spinner.error(format!("Failed to launch browser. Please open link manually. {e}")); + } + + spinner.start("Waiting for signature... Press Ctrl+C to terminate early."); + loop { + // Display error, if any. + if let Some(error) = wallet.take_error().await { + log::error(format!("Signing portal error: {error}"))?; + } + + let state = wallet.state.lock().await; + // If the payload is submitted we terminate the frontend. + if !wallet.is_running() || state.signed_payload.is_some() { + wallet.task_handle.await??; + break; + } + } + spinner.stop(""); + + let signed_payload = wallet.state.lock().await.signed_payload.take(); + Ok(signed_payload) +} + +/// Prompts the user to use the wallet for signing. +/// # Arguments +/// * `cli` - The CLI instance. +/// # Returns +/// * `true` if the user wants to use the wallet, `false` otherwise. +pub fn prompt_to_use_wallet(cli: &mut impl Cli) -> anyhow::Result { + use crate::cli::traits::Confirm; + + if cli.confirm(USE_WALLET_PROMPT).initial_value(true).interact()? { + Ok(true) + } else { + Ok(false) + } +} diff --git a/crates/pop-cli/src/main.rs b/crates/pop-cli/src/main.rs index 9a41fd7c1..f8e3504e5 100644 --- a/crates/pop-cli/src/main.rs +++ b/crates/pop-cli/src/main.rs @@ -19,6 +19,7 @@ mod cli; mod commands; mod common; mod style; +mod wallet_integration; #[tokio::main] async fn main() -> Result<()> { diff --git a/crates/pop-cli/src/wallet_integration.rs b/crates/pop-cli/src/wallet_integration.rs new file mode 100644 index 000000000..92e3395ec --- /dev/null +++ b/crates/pop-cli/src/wallet_integration.rs @@ -0,0 +1,579 @@ +use axum::{ + http::HeaderValue, + response::Html, + routing::{get, post}, + Router, +}; +use pop_common::find_free_port; +use serde::Serialize; +use std::{path::PathBuf, sync::Arc}; +use tokio::{ + sync::{oneshot, Mutex}, + task::JoinHandle, +}; +use tower_http::{cors::Any, services::ServeDir}; + +/// Make frontend sourcing more flexible by allowing a custom route to be defined. +pub trait Frontend { + /// Serves the content via a [Router]. + fn serve_content(&self) -> Router; +} + +/// Transaction payload to be sent to frontend for signing. +#[derive(Serialize, Debug)] +#[cfg_attr(test, derive(serde::Deserialize, Clone))] +pub struct TransactionData { + chain_rpc: String, + call_data: Vec, +} + +impl TransactionData { + /// Create a new transaction payload. + /// # Arguments + /// * `chain_rpc`: The RPC of the chain. + /// * `call_data`: the call data. + /// # Returns + /// The transaction payload to be sent to frontend for signing. + pub fn new(chain_rpc: String, call_data: Vec) -> Self { + Self { chain_rpc, call_data } + } +} + +/// Shared state between routes. Serves two purposes: +/// - Maintains a channel to signal shutdown to the main app. +/// - Stores the signed payload received from the wallet. +#[derive(Default)] +pub struct StateHandler { + /// Channel to signal shutdown to the main app. + shutdown_tx: Option>, + /// Received from UI. + pub signed_payload: Option, + /// Holds a single error message. + /// Only method for consuming error removes (takes) it from state. + error: Option, +} + +/// Manages the wallet integration for secure signing of transactions. +pub struct WalletIntegrationManager { + pub server_url: String, + /// Shared state between routes. + pub state: Arc>, + /// Web server task handle. + pub task_handle: JoinHandle>, +} + +impl WalletIntegrationManager { + /// Launches a server for hosting the wallet integration. Server launched in separate task. + /// # Arguments + /// * `frontend`: A frontend with custom route to serve content. + /// * `payload`: Payload to be sent to the frontend for signing. + /// * `maybe_port`: Optional port for server to bind to. `None` will result in a random port. + /// + /// # Returns + /// A `WalletIntegrationManager` instance, with access to the state and task handle for the + /// server. + pub fn new( + frontend: F, + payload: TransactionData, + maybe_port: Option, + ) -> Self { + let port = find_free_port(maybe_port); + Self::new_with_address(frontend, payload, format!("127.0.0.1:{}", port)) + } + + /// Same as `new`, but allows specifying the address to bind to. + /// # Arguments + /// * `frontend`: A frontend with custom route to serve content. + /// * `payload`: Payload to be sent to the frontend for signing. + /// * `server_url`: The address to bind to. + /// + /// # Returns + /// A `WalletIntegrationManager` instance, with access to the state and task handle for the + pub fn new_with_address( + frontend: F, + payload: TransactionData, + server_url: String, + ) -> Self { + // Channel to signal shutdown. + let (tx, rx) = oneshot::channel(); + + let state = Arc::new(Mutex::new(StateHandler { + shutdown_tx: Some(tx), + signed_payload: None, + error: None, + })); + + let payload = Arc::new(payload); + + let cors = tower_http::cors::CorsLayer::new() + .allow_origin(server_url.parse::().expect("invalid server url")) + .allow_methods(Any) // Allow any HTTP method + .allow_headers(Any); // Allow any headers (like 'Content-Type') + + let app = Router::new() + .route("/payload", get(routes::get_payload_handler).with_state(payload)) + .route("/submit", post(routes::submit_handler).with_state(state.clone())) + .route("/error", post(routes::error_handler).with_state(state.clone())) + .route("/terminate", post(routes::terminate_handler).with_state(state.clone())) + .merge(frontend.serve_content()) // Custom route for serving frontend. + .layer(cors); + + let url_owned = server_url.to_string(); + + // Will shut down when the signed payload is received. + let task_handle = tokio::spawn(async move { + let listener = tokio::net::TcpListener::bind(&url_owned) + .await + .map_err(|e| anyhow::anyhow!("Failed to bind to {}: {}", url_owned, e))?; + + axum::serve(listener, app) + .with_graceful_shutdown(async move { + let _ = rx.await.ok(); + }) + .await + .map_err(|e| anyhow::anyhow!("Server encountered an error: {}", e))?; + Ok(()) + }); + + Self { state, server_url, task_handle } + } + + /// Signals the wallet integration server to shut down. + #[allow(dead_code)] + pub async fn terminate(&mut self) -> anyhow::Result<()> { + terminate_helper(&self.state).await + } + + /// Checks if the server task is still running. + pub fn is_running(&self) -> bool { + !self.task_handle.is_finished() + } + + /// Takes the error from the state if it exists. + pub async fn take_error(&mut self) -> Option { + self.state.lock().await.error.take() + } +} + +mod routes { + use super::{terminate_helper, Arc, Mutex, StateHandler, TransactionData}; + use anyhow::Error; + use axum::{ + extract::State, + http::StatusCode, + response::{IntoResponse, Response}, + Json, + }; + use serde_json::json; + + pub(super) struct ApiError(Error); + + impl From for ApiError { + fn from(err: Error) -> Self { + ApiError(err) + } + } + + // Implementing IntoResponse for ApiError allows us to return it directly from a route handler. + impl IntoResponse for ApiError { + fn into_response(self) -> Response { + let body = json!({ + "error": self.0.to_string(), + }); + (StatusCode::INTERNAL_SERVER_ERROR, Json(body)).into_response() + } + } + + /// Responds with the serialized JSON data for signing. + pub(super) async fn get_payload_handler( + State(payload): State>, + ) -> Result, ApiError> { + // Error should never occur. + let json_payload = serde_json::to_value(&*payload) + .map_err(|e| anyhow::anyhow!("Failed to serialize payload: {}", e))?; + Ok(Json(json_payload)) + } + + /// Receives the signed payload from the wallet. + /// Will signal for shutdown on success. + pub(super) async fn submit_handler( + State(state): State>>, + Json(payload): Json, + ) -> Result, ApiError> { + // Signal shutdown. + let res = terminate_helper(&state).await; + + let mut state_locked = state.lock().await; + state_locked.signed_payload = Some(payload); + + res?; + + // Graceful shutdown ensures response is sent before shutdown. + Ok(Json(json!({"status": "success"}))) + } + + /// Receives an error message from the wallet. + pub(super) async fn error_handler( + State(state): State>>, + Json(error): Json, + ) { + let mut state = state.lock().await; + state.error = Some(error); + } + + /// Allows the server to be terminated from the frontend. + pub(super) async fn terminate_handler( + State(state): State>>, + ) -> Result<(), ApiError> { + Ok(terminate_helper(&state).await?) + } +} + +async fn terminate_helper(handle: &Arc>) -> anyhow::Result<()> { + if let Some(shutdown_tx) = handle.lock().await.shutdown_tx.take() { + shutdown_tx + .send(()) + .map_err(|_| anyhow::anyhow!("Failed to send shutdown signal"))?; + } + Ok(()) +} + +/// Serves static files from a directory. +pub struct FrontendFromDir { + content: PathBuf, +} +#[allow(dead_code)] +impl FrontendFromDir { + /// A new static server. + /// # Arguments + /// * `content`: A directory path. + pub fn new(content: PathBuf) -> Self { + Self { content } + } +} + +impl Frontend for FrontendFromDir { + fn serve_content(&self) -> Router { + Router::new().nest_service("/", ServeDir::new(self.content.clone())) + } +} + +/// Serves a hard-coded HTML string as the frontend. +pub struct FrontendFromString { + content: String, +} + +#[allow(dead_code)] +impl FrontendFromString { + /// A new static server. + /// # Arguments + /// * `content`: A hard-coded HTML string + pub fn new(content: String) -> Self { + Self { content } + } +} + +impl Frontend for FrontendFromString { + fn serve_content(&self) -> Router { + let content = self.content.clone(); + Router::new().route("/", get(move || async { Html(content) })) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + const TEST_HTML: &str = "Hello, world!"; + + // Wait for server to launch. + async fn wait() { + tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; + } + + fn default_payload() -> TransactionData { + TransactionData { chain_rpc: "localhost:9944".to_string(), call_data: vec![1, 2, 3] } + } + + #[tokio::test] + async fn new_works() { + let frontend = FrontendFromString::new(TEST_HTML.to_string()); + let mut wim = WalletIntegrationManager::new(frontend, default_payload(), Some(9190)); + + assert_eq!(wim.server_url, "127.0.0.1:9190"); + assert_eq!(wim.is_running(), true); + assert!(wim.state.lock().await.shutdown_tx.is_some()); + assert!(wim.state.lock().await.signed_payload.is_none()); + + // Terminate the server and make sure result is ok. + wim.terminate().await.expect("Termination should not fail."); + assert!(wim.task_handle.await.is_ok()); + } + + #[tokio::test] + async fn new_with_random_port_works() { + let servers = (0..3) + .map(|_| { + let frontend = FrontendFromString::new(TEST_HTML.to_string()); + WalletIntegrationManager::new(frontend, default_payload(), None) + }) + .collect::>(); + + // Ensure all server URLs are unique + for i in 0..servers.len() { + for j in (i + 1)..servers.len() { + assert_ne!(servers[i].server_url, servers[j].server_url); + } + } + + assert!(servers.iter().all(|server| server.is_running())); + for mut server in servers.into_iter() { + assert!(server.state.lock().await.shutdown_tx.is_some()); + assert!(server.state.lock().await.signed_payload.is_none()); + server.terminate().await.expect("Server termination should not fail"); + + let task_result = server.task_handle.await; + assert!(task_result.is_ok()); + } + } + + #[test] + fn new_transaction_data_works() { + let chain_rpc = "localhost:9944".to_string(); + let call_data = vec![1, 2, 3]; + let transaction_data = TransactionData::new(chain_rpc.clone(), call_data.clone()); + + assert_eq!(transaction_data.chain_rpc, chain_rpc); + assert_eq!(transaction_data.call_data, call_data); + } + + #[tokio::test] + async fn take_error_works() { + let frontend = FrontendFromString::new(TEST_HTML.to_string()); + let mut wim = WalletIntegrationManager::new(frontend, default_payload(), None); + + assert_eq!(wim.take_error().await, None); + + let error = "An error occurred".to_string(); + wim.state.lock().await.error = Some(error.clone()); + + let taken_error = wim.take_error().await; + assert_eq!(taken_error, Some(error)); + } + + #[tokio::test] + async fn payload_handler_works() { + // offset port per test to avoid conflicts + let frontend = FrontendFromString::new(TEST_HTML.to_string()); + let expected_payload = + TransactionData { chain_rpc: "localhost:9944".to_string(), call_data: vec![1, 2, 3] }; + let mut wim = WalletIntegrationManager::new(frontend, expected_payload.clone(), None); + wait().await; + + let addr = format!("http://{}", wim.server_url); + let actual_payload = reqwest::get(&format!("{}/payload", addr)) + .await + .expect("Failed to get payload") + .json::() + .await + .expect("Failed to parse payload"); + + assert_eq!(actual_payload.chain_rpc, expected_payload.chain_rpc); + assert_eq!(actual_payload.call_data, expected_payload.call_data); + + wim.terminate().await.expect("Termination should not fail"); + assert!(wim.task_handle.await.is_ok()); + } + + #[tokio::test] + async fn submit_handler_works() { + let frontend = FrontendFromString::new(TEST_HTML.to_string()); + let mut wim = WalletIntegrationManager::new(frontend, default_payload(), None); + wait().await; + + let addr = format!("http://{}", wim.server_url); + let response = reqwest::Client::new() + .post(&format!("{}/submit", addr)) + .json(&"0xDEADBEEF") + .send() + .await + .expect("Failed to submit payload") + .text() + .await + .expect("Failed to parse response"); + + assert_eq!(response, json!({"status": "success"}).to_string()); + assert_eq!(wim.state.lock().await.signed_payload, Some("0xDEADBEEF".to_string())); + assert_eq!(wim.is_running(), false); + + wim.terminate().await.expect("Termination should not fail"); + assert!(wim.task_handle.await.is_ok()); + } + + #[tokio::test] + async fn error_handler_works() { + let frontend = FrontendFromString::new(TEST_HTML.to_string()); + let mut wim = WalletIntegrationManager::new(frontend, default_payload(), None); + wait().await; + + let addr = format!("http://{}", wim.server_url); + let response = reqwest::Client::new() + .post(&format!("{}/error", addr)) + .json(&"an error occurred") + .send() + .await + .expect("Failed to submit error") + .text() + .await + .expect("Failed to parse response"); + + // no response expected + assert_eq!(response.len(), 0); + assert_eq!(wim.state.lock().await.error, Some("an error occurred".to_string())); + assert_eq!(wim.is_running(), true); + + wim.terminate().await.expect("Termination should not fail"); + assert!(wim.task_handle.await.is_ok()); + } + + #[tokio::test] + async fn terminate_handler_works() { + let frontend = FrontendFromString::new(TEST_HTML.to_string()); + + let wim = WalletIntegrationManager::new(frontend, default_payload(), None); + wait().await; + + let addr = format!("http://{}", wim.server_url); + let response = reqwest::Client::new() + .post(&format!("{}/terminate", addr)) + .send() + .await + .expect("Failed to terminate") + .text() + .await + .expect("Failed to parse response"); + + // No response expected. + assert_eq!(response.len(), 0); + assert_eq!(wim.is_running(), false); + + assert!(wim.task_handle.await.is_ok()); + } + + #[tokio::test] + async fn wallet_terminate_works() { + let frontend = FrontendFromString::new(TEST_HTML.to_string()); + + let mut wim = WalletIntegrationManager::new(frontend, default_payload(), None); + assert_eq!(wim.is_running(), true); + wim.terminate().await.expect("Termination should not fail"); + wait().await; + assert_eq!(wim.is_running(), false); + + wim.terminate().await.expect("Termination should not fail"); + assert!(wim.task_handle.await.is_ok()); + } + + #[tokio::test] + async fn frontend_from_string_works() { + let frontend = FrontendFromString::new(TEST_HTML.to_string()); + let mut wim = WalletIntegrationManager::new(frontend, default_payload(), None); + wait().await; + + let actual_content = reqwest::get(&format!("http://{}", wim.server_url)) + .await + .expect("Failed to get web page") + .text() + .await + .expect("Failed to parse page"); + + assert_eq!(actual_content, TEST_HTML); + + wim.terminate().await.expect("Termination should not fail"); + assert!(wim.task_handle.await.is_ok()); + } + + #[tokio::test] + async fn frontend_from_dir_works() { + use std::fs; + use tempfile::tempdir; + + let temp_dir = tempdir().expect("Failed to create temp directory"); + let index_file_path = temp_dir.path().join("index.html"); + + let test_html = "Hello, world from Directory!"; + fs::write(&index_file_path, test_html).expect("Failed to write index.html"); + + let frontend = FrontendFromDir::new(temp_dir.path().to_path_buf()); + let mut wim = WalletIntegrationManager::new(frontend, default_payload(), None); + wait().await; + + let actual_content = reqwest::get(&format!("http://{}", wim.server_url)) + .await + .expect("Failed to get web page") + .text() + .await + .expect("Failed to parse page"); + + assert_eq!(actual_content, test_html); + + wim.terminate().await.expect("Termination should not fail"); + assert!(wim.task_handle.await.is_ok()); + } + + #[tokio::test] + async fn large_payload_works() { + let frontend = FrontendFromString::new(TEST_HTML.to_string()); + + let call_data_5mb = vec![99u8; 5 * 1024 * 1024]; + + let expected_payload = TransactionData { + chain_rpc: "localhost:9944".to_string(), + call_data: call_data_5mb.clone(), + }; + let mut wim = WalletIntegrationManager::new(frontend, expected_payload.clone(), None); + wait().await; + + let addr = format!("http://{}", wim.server_url); + let actual_payload = reqwest::get(&format!("{}/payload", addr)) + .await + .expect("Failed to get payload") + .json::() + .await + .expect("Failed to parse payload"); + + assert_eq!(actual_payload.chain_rpc, expected_payload.chain_rpc); + assert_eq!(actual_payload.call_data, call_data_5mb); + + wim.terminate().await.expect("Termination should not fail."); + assert!(wim.task_handle.await.is_ok()); + } + + #[tokio::test] + async fn new_with_conflicting_address_fails() { + // offset port per test to avoid conflicts + let addr = "127.0.0.1:9099".to_string(); + + let frontend = FrontendFromString::new(TEST_HTML.to_string()); + let wim = + WalletIntegrationManager::new_with_address(frontend, default_payload(), addr.clone()); + wait().await; + + assert_eq!(wim.is_running(), true); + + let frontend = FrontendFromString::new(TEST_HTML.to_string()); + let wim_conflict = + WalletIntegrationManager::new_with_address(frontend, default_payload(), addr.clone()); + wait().await; + + assert_eq!(wim_conflict.is_running(), false); + let task_result = wim_conflict.task_handle.await.unwrap(); + match task_result { + Err(e) => assert!(e + .to_string() + .starts_with(&format!("Failed to bind to {}: Address already in use", addr))), + Ok(_) => panic!("Expected error, but task succeeded"), + } + } +} diff --git a/crates/pop-cli/tests/contract.rs b/crates/pop-cli/tests/contract.rs index 77c8dba5d..1af21b385 100644 --- a/crates/pop-cli/tests/contract.rs +++ b/crates/pop-cli/tests/contract.rs @@ -2,19 +2,57 @@ use anyhow::Result; use assert_cmd::Command; -use pop_common::{set_executable_permission, templates::Template}; +use pop_common::{find_free_port, set_executable_permission, templates::Template}; use pop_contracts::{ contracts_node_generator, dry_run_gas_estimate_instantiate, instantiate_smart_contract, run_contracts_node, set_up_deployment, Contract, UpOpts, }; -use std::{path::Path, process::Command as Cmd}; +use serde::{Deserialize, Serialize}; +use std::{path::Path, process::Command as Cmd, time::Duration}; use strum::VariantArray; +use subxt::{config::DefaultExtrinsicParamsBuilder as Params, tx::Payload, utils::to_hex}; +use subxt_signer::sr25519::dev; +use tokio::time::sleep; use url::Url; +// 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(()) + } +} + +// TransactionData has been copied from wallet_integration.rs +/// Transaction payload to be sent to frontend for signing. +#[derive(Serialize, Debug)] +#[cfg_attr(test, derive(Deserialize, Clone))] +pub struct TransactionData { + chain_rpc: String, + call_data: Vec, +} +impl TransactionData { + pub fn new(chain_rpc: String, call_data: Vec) -> Self { + Self { chain_rpc, call_data } + } + pub fn call_data(&self) -> Vec { + self.call_data.clone() + } +} + /// Test the contract lifecycle: new, build, up, call #[tokio::test] async fn contract_lifecycle() -> Result<()> { - const DEFAULT_PORT: u16 = 9944; + const WALLET_INT_URI: &str = "http://127.0.0.1:9090"; + const WAIT_SECS: u64 = 240; + let endpoint_port = find_free_port(None); + let default_endpoint: &str = &format!("ws://127.0.0.1:{}", endpoint_port); let temp = tempfile::tempdir().unwrap(); let temp_dir = temp.path(); //let temp_dir = Path::new("./"); //For testing locally @@ -46,14 +84,15 @@ async fn contract_lifecycle() -> Result<()> { let binary = contracts_node_generator(temp_dir.to_path_buf().clone(), None).await?; binary.source(false, &(), true).await?; set_executable_permission(binary.path())?; - let process = run_contracts_node(binary.path(), None, DEFAULT_PORT).await?; + let process = run_contracts_node(binary.path(), None, endpoint_port).await?; + sleep(Duration::from_secs(5)).await; // Only upload the contract // pop up contract --upload-only Command::cargo_bin("pop") .unwrap() .current_dir(&temp_dir.join("test_contract")) - .args(&["up", "contract", "--upload-only"]) + .args(&["up", "contract", "--upload-only", "--url", default_endpoint]) .assert() .success(); // Instantiate contract, only dry-run @@ -70,6 +109,8 @@ async fn contract_lifecycle() -> Result<()> { "--suri", "//Alice", "--dry-run", + "--url", + default_endpoint, ]) .assert() .success(); @@ -83,7 +124,7 @@ async fn contract_lifecycle() -> Result<()> { gas_limit: None, proof_size: None, salt: None, - url: Url::parse("ws://127.0.0.1:9944")?, + url: Url::parse(default_endpoint)?, suri: "//Alice".to_string(), }) .await?; @@ -103,6 +144,8 @@ async fn contract_lifecycle() -> Result<()> { "get", "--suri", "//Alice", + "--url", + default_endpoint, ]) .assert() .success(); @@ -122,10 +165,70 @@ async fn contract_lifecycle() -> Result<()> { "--suri", "//Alice", "-x", + "--url", + default_endpoint, ]) .assert() .success(); + // pop up contract --upload-only --use-wallet + // Will run http server for wallet integration. + // Using `cargo run --` as means for the CI to pass. + // Possibly there's room for improvement here. + let _ = tokio::process::Command::new("cargo") + .args(&[ + "run", + "--", + "up", + "contract", + "--upload-only", + "--use-wallet", + "--skip-confirm", + "--dry-run", + "-p", + temp_dir.join("test_contract").to_str().expect("to_str"), + "--url", + default_endpoint, + ]) + .spawn()?; + // Wait a moment for node and server to be up. + sleep(Duration::from_secs(WAIT_SECS)).await; + + // Request payload from server. + let response = reqwest::get(&format!("{}/payload", WALLET_INT_URI)) + .await + .expect("Failed to get payload") + .json::() + .await + .expect("Failed to parse payload"); + // We have received some payload. + assert!(!response.call_data().is_empty()); + + let rpc_client = subxt::backend::rpc::RpcClient::from_url(default_endpoint).await?; + let client = subxt::OnlineClient::::from_rpc_client(rpc_client).await?; + + // Sign payload. + let signer = dev::alice(); + let payload = CallData(response.call_data()); + let ext_params = Params::new().build(); + let signed = client.tx().create_signed(&payload, &signer, ext_params).await?; + + // Submit signed payload. This kills the wallet integration server. + let _ = reqwest::Client::new() + .post(&format!("{}/submit", WALLET_INT_URI)) + .json(&to_hex(signed.encoded())) + .send() + .await + .expect("Failed to submit payload") + .text() + .await + .expect("Failed to parse JSON response"); + + // Request payload from server after signed payload has been sent. + // Server should not be running! + let response = reqwest::get(&format!("{}/payload", WALLET_INT_URI)).await; + assert!(response.is_err()); + // Stop the process contracts-node Cmd::new("kill") .args(["-s", "TERM", &process.id().to_string()]) diff --git a/crates/pop-cli/tests/parachain.rs b/crates/pop-cli/tests/parachain.rs index 67132e6d4..31ad6bded 100644 --- a/crates/pop-cli/tests/parachain.rs +++ b/crates/pop-cli/tests/parachain.rs @@ -95,7 +95,7 @@ async fn parachain_lifecycle() -> Result<()> { // 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 random_port = find_free_port(None); let localhost_url = format!("ws://127.0.0.1:{}", random_port); fs::write( &network_toml_path, diff --git a/crates/pop-common/src/lib.rs b/crates/pop-common/src/lib.rs index 70dbcc8f8..b4c0c1973 100644 --- a/crates/pop-common/src/lib.rs +++ b/crates/pop-common/src/lib.rs @@ -64,12 +64,20 @@ 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 { +/// Checks if preferred port is available, otherwise returns a random available port. +pub fn find_free_port(preferred_port: Option) -> u16 { + // Try to bind to preferred port if provided. + if let Some(port) = preferred_port { + if TcpListener::bind(format!("127.0.0.1:{}", port)).is_ok() { + return port; + } + } + + // Else, fallback to a random available port TcpListener::bind("127.0.0.1:0") .expect("Failed to bind to an available port") .local_addr() - .expect("Failed to retrieve local address") + .expect("Failed to retrieve local address. This should never occur.") .port() } @@ -105,7 +113,7 @@ mod test { #[test] fn find_free_port_works() -> Result<()> { - let port = find_free_port(); + let port = find_free_port(None); let addr = format!("127.0.0.1:{}", port); // Constructs the TcpListener from the above port let listener = TcpListener::bind(&addr); diff --git a/crates/pop-contracts/Cargo.toml b/crates/pop-contracts/Cargo.toml index b3ab82cb8..f44332299 100644 --- a/crates/pop-contracts/Cargo.toml +++ b/crates/pop-contracts/Cargo.toml @@ -27,11 +27,13 @@ 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 contract-extrinsics.workspace = true -contract-transcode.workspace = true +contract-transcode.workspace = true scale-info.workspace = true # pop pop-common = { path = "../pop-common", version = "0.5.0" } @@ -39,4 +41,4 @@ pop-common = { path = "../pop-common", version = "0.5.0" } [dev-dependencies] dirs.workspace = true mockito.workspace = true -tokio-test.workspace = true +tokio-test.workspace = true \ No newline at end of file diff --git a/crates/pop-contracts/src/call.rs b/crates/pop-contracts/src/call.rs index 03effcb0a..2ffbd3258 100644 --- a/crates/pop-contracts/src/call.rs +++ b/crates/pop-contracts/src/call.rs @@ -2,6 +2,7 @@ use crate::{ errors::Error, + submit_signed_payload, utils::{ get_manifest_path, metadata::{process_function_args, FunctionType}, @@ -11,16 +12,18 @@ use crate::{ use anyhow::Context; use contract_build::Verbosity; use contract_extrinsics::{ - BalanceVariant, CallCommandBuilder, CallExec, ContractArtifacts, DisplayEvents, ErrorVariant, - ExtrinsicOptsBuilder, TokenMetadata, + extrinsic_calls::Call, BalanceVariant, CallCommandBuilder, CallExec, ContractArtifacts, + DisplayEvents, ErrorVariant, 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::{tx::Payload, SubstrateConfig}; use url::Url; /// Attributes for the `call` command. +#[derive(Clone, Debug, PartialEq)] pub struct CallOpts { /// Path to the contract build directory. pub path: Option, @@ -173,6 +176,53 @@ pub async fn call_smart_contract( Ok(output) } +/// Executes a smart contract call using a signed payload. +/// +/// # Arguments +/// +/// * `call_exec` - A struct containing the details of the contract call. +/// * `payload` - The signed payload string to be submitted for executing the call. +/// * `url` - The endpoint of the node where the call is executed. +pub async fn call_smart_contract_from_signed_payload( + call_exec: CallExec, + payload: String, + url: &Url, +) -> anyhow::Result { + let token_metadata = TokenMetadata::query::(url).await?; + let metadata = call_exec.client().metadata(); + let events = submit_signed_payload(url.as_str(), payload).await?; + let display_events = DisplayEvents::from_events::( + &events, None, &metadata, + )?; + + let output = + display_events.display_events::(Verbosity::Default, &token_metadata)?; + Ok(output) +} + +/// Generates the payload for executing a smart contract call. +/// +/// # Arguments +/// * `call_exec` - A struct containing the details of the contract call. +/// * `gas_limit` - The maximum amount of gas allocated for executing the contract call. +pub fn get_call_payload( + call_exec: &CallExec, + gas_limit: Weight, +) -> anyhow::Result> { + let storage_deposit_limit: Option = call_exec.opts().storage_deposit_limit(); + let mut encoded_data = Vec::::new(); + Call::new( + call_exec.contract().into(), + call_exec.value(), + gas_limit, + storage_deposit_limit.as_ref(), + call_exec.call_data().clone(), + ) + .build() + .encode_call_data_to(&call_exec.client().metadata(), &mut encoded_data)?; + Ok(encoded_data) +} + #[cfg(test)] mod tests { use super::*; @@ -335,7 +385,7 @@ mod tests { #[tokio::test] async fn call_works() -> Result<()> { - let random_port = find_free_port(); + let random_port = find_free_port(None); let localhost_url = format!("ws://127.0.0.1:{}", random_port); let temp_dir = new_environment("testing")?; let current_dir = env::current_dir().expect("Failed to get current directory"); diff --git a/crates/pop-contracts/src/lib.rs b/crates/pop-contracts/src/lib.rs index c5e76e2f1..6dded2fe0 100644 --- a/crates/pop-contracts/src/lib.rs +++ b/crates/pop-contracts/src/lib.rs @@ -14,7 +14,8 @@ mod utils; pub use build::{build_smart_contract, is_supported, Verbosity}; pub use call::{ - call_smart_contract, dry_run_call, dry_run_gas_estimate_call, set_up_call, CallOpts, + call_smart_contract, call_smart_contract_from_signed_payload, dry_run_call, + dry_run_gas_estimate_call, get_call_payload, set_up_call, CallOpts, }; pub use new::{create_smart_contract, is_valid_contract_name}; pub use node::{contracts_node_generator, is_chain_alive, run_contracts_node}; @@ -22,10 +23,15 @@ pub use templates::{Contract, ContractType}; pub use test::{test_e2e_smart_contract, test_smart_contract}; 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, + dry_run_gas_estimate_instantiate, dry_run_upload, get_code_hash_from_event, get_contract_code, + get_instantiate_payload, get_upload_payload, instantiate_contract_signed, + instantiate_smart_contract, set_up_deployment, set_up_upload, submit_signed_payload, + upload_contract_signed, upload_smart_contract, ContractInfo, UpOpts, }; pub use utils::{ - metadata::{get_messages, ContractFunction}, + metadata::{get_message, get_messages, ContractFunction}, parse_account, parse_hex_bytes, }; +// External exports +pub use contract_extrinsics::CallExec; +pub use ink_env::DefaultEnvironment; diff --git a/crates/pop-contracts/src/node/mod.rs b/crates/pop-contracts/src/node.rs similarity index 91% rename from crates/pop-contracts/src/node/mod.rs rename to crates/pop-contracts/src/node.rs index be5680a04..158865d40 100644 --- a/crates/pop-contracts/src/node/mod.rs +++ b/crates/pop-contracts/src/node.rs @@ -19,9 +19,11 @@ use std::{ process::{Child, Command, Stdio}, time::Duration, }; +use subxt::{dynamic::Value, SubstrateConfig}; use tokio::time::sleep; const BIN_NAME: &str = "substrate-contracts-node"; +const STARTUP: Duration = Duration::from_millis(20_000); /// Checks if the specified node is alive and responsive. /// @@ -131,8 +133,24 @@ pub async fn run_contracts_node( let process = command.spawn()?; - // Wait 5 secs until the node is ready - sleep(Duration::from_millis(5000)).await; + // Wait until the node is ready + sleep(STARTUP).await; + + let data = Value::from_bytes(subxt::utils::to_hex("initialize contracts node")); + let payload = subxt::dynamic::tx("System", "remark", [data].to_vec()); + + let client = subxt::client::OnlineClient::::from_url(format!( + "ws://127.0.0.1:{}", + port + )) + .await + .map_err(|e| Error::AnyhowError(e.into()))?; + client + .tx() + .sign_and_submit_default(&payload, &subxt_signer::sr25519::dev::alice()) + .await + .map_err(|e| Error::AnyhowError(e.into()))?; + Ok(process) } @@ -222,7 +240,7 @@ mod tests { #[ignore = "Works fine locally but is causing issues when running tests in parallel in the CI environment."] #[tokio::test] async fn run_contracts_node_works() -> Result<(), Error> { - let random_port = find_free_port(); + let random_port = find_free_port(None); let localhost_url = format!("ws://127.0.0.1:{}", random_port); let local_url = url::Url::parse(&localhost_url)?; diff --git a/crates/pop-contracts/src/up.rs b/crates/pop-contracts/src/up.rs index bed7dfa45..3ed614c7e 100644 --- a/crates/pop-contracts/src/up.rs +++ b/crates/pop-contracts/src/up.rs @@ -1,4 +1,5 @@ // SPDX-License-Identifier: GPL-3.0 + use crate::{ errors::Error, utils::{ @@ -8,17 +9,29 @@ use crate::{ }, }; use contract_extrinsics::{ - BalanceVariant, ErrorVariant, ExtrinsicOptsBuilder, InstantiateCommandBuilder, InstantiateExec, - TokenMetadata, UploadCommandBuilder, UploadExec, + events::{CodeStored, ContractInstantiated}, + extrinsic_calls::{Instantiate, InstantiateWithCode, UploadCode}, + upload::Determinism, + BalanceVariant, Code, ErrorVariant, ExtrinsicOptsBuilder, InstantiateCommandBuilder, + InstantiateExec, InstantiateExecResult, TokenMetadata, UploadCommandBuilder, UploadExec, + UploadResult, WasmCode, }; use ink_env::{DefaultEnvironment, Environment}; use pop_common::{create_signer, DefaultConfig, Keypair}; -use sp_core::Bytes; +use sp_core::{bytes::from_hex, Bytes}; use sp_weights::Weight; -use std::{fmt::Write, path::PathBuf}; +use std::{ + fmt::Write, + path::{Path, PathBuf}, +}; +use subxt::{ + blocks::ExtrinsicEvents, + tx::{Payload, SubmittableExtrinsic}, + Config, SubstrateConfig, +}; /// Attributes for the `up` command -#[derive(Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq)] pub struct UpOpts { /// Path to the contract build directory. pub path: Option, @@ -100,14 +113,177 @@ pub async fn set_up_upload( let upload_exec: UploadExec = UploadCommandBuilder::new(extrinsic_opts).done().await?; + Ok(upload_exec) } +/// Gets the encoded payload call data for contract upload (not instantiate). +/// +/// # Arguments +/// * `code` - contract code to upload. +/// * `url` - the rpc of the chain node. +pub async fn get_upload_payload(code: WasmCode, url: &str) -> anyhow::Result> { + let storage_deposit_limit: Option = None; + let upload_code = UploadCode::new(code, storage_deposit_limit, Determinism::Enforced); + + let rpc_client = subxt::backend::rpc::RpcClient::from_url(url).await?; + let client = subxt::OnlineClient::::from_rpc_client(rpc_client).await?; + + let call_data = upload_code.build(); + let mut encoded_data = Vec::::new(); + call_data.encode_call_data_to(&client.metadata(), &mut encoded_data)?; + Ok(encoded_data) +} + +/// Gets the encoded payload call data for a contract instantiation. +/// +/// # Arguments +/// * `instantiate_exec` - arguments for contract instantiate. +/// * `gas_limit` - max amount of gas to be used for instantiation. +pub fn get_instantiate_payload( + instantiate_exec: InstantiateExec, + gas_limit: Weight, +) -> anyhow::Result> { + let storage_deposit_limit: Option = None; + let mut encoded_data = Vec::::new(); + let args = instantiate_exec.args(); + match args.code() { + Code::Upload(code) => InstantiateWithCode::new( + args.value(), + gas_limit, + storage_deposit_limit, + code.clone(), + args.data().into(), + args.salt().into(), + ) + .build() + .encode_call_data_to(&instantiate_exec.client().metadata(), &mut encoded_data), + Code::Existing(hash) => Instantiate::new( + args.value(), + gas_limit, + storage_deposit_limit, + hash, + args.data().into(), + args.salt().into(), + ) + .build() + .encode_call_data_to(&instantiate_exec.client().metadata(), &mut encoded_data), + }?; + + Ok(encoded_data) +} + +/// Reads the contract code from contract file. +/// +/// # Arguments +/// * `path` - path to the contract file. +pub fn get_contract_code(path: Option<&PathBuf>) -> anyhow::Result { + let manifest_path = get_manifest_path(path.map(|p| p as &Path))?; + + // signer does not matter for this + let signer = create_signer("//Alice")?; + let extrinsic_opts = + ExtrinsicOptsBuilder::::new(signer) + .manifest_path(Some(manifest_path)) + .done(); + let artifacts = extrinsic_opts.contract_artifacts()?; + + let artifacts_path = artifacts.artifact_path().to_path_buf(); + let code = artifacts.code.ok_or_else(|| { + Error::UploadContractError(format!( + "Contract code not found from artifact file {}", + artifacts_path.display() + )) + })?; + Ok(code) +} + +/// Submit a pre-signed payload for uploading a contract. +/// +/// # Arguments +/// * `url` - rpc for chain. +/// * `payload` - the signed payload to submit (encoded call data). +pub async fn upload_contract_signed( + url: &str, + payload: String, +) -> anyhow::Result> { + let events = submit_signed_payload(url, payload).await?; + + let code_stored = events.find_first::>()?; + + Ok(UploadResult { code_stored, events }) +} + +/// Submit a pre-signed payload for instantiating a contract. +/// +/// # Arguments +/// * `url` - rpc for chain. +/// * `payload` - the signed payload to submit (encoded call data). +pub async fn instantiate_contract_signed( + url: &str, + payload: String, +) -> anyhow::Result> { + let events = submit_signed_payload(url, payload).await?; + + // The CodeStored event is only raised if the contract has not already been + // uploaded. + let code_hash = events + .find_first::>()? + .map(|code_stored| code_stored.code_hash); + + let instantiated = events + .find_first::>()? + .ok_or_else(|| { + Error::InstantiateContractError("Failed to find Instantiated event".to_string()) + })?; + + Ok(InstantiateExecResult { events, code_hash, contract_address: instantiated.contract }) +} + +/// Submit a pre-signed payload. +/// +/// # Arguments +/// * `url` - rpc for chain. +/// * `payload` - the signed payload to submit (encoded call data). +pub async fn submit_signed_payload( + url: &str, + payload: String, +) -> anyhow::Result> { + let rpc_client = subxt::backend::rpc::RpcClient::from_url(url).await?; + let client = subxt::OnlineClient::::from_rpc_client(rpc_client).await?; + + let hex_encoded = from_hex(&payload)?; + + let extrinsic = SubmittableExtrinsic::from_bytes(client, hex_encoded); + + // src: https://github.com/use-ink/cargo-contract/blob/68691b9b6cdb7c6ec52ea441b3dc31fcb1ce08e0/crates/extrinsics/src/lib.rs#L143 + + use subxt::{ + error::{RpcError, TransactionError}, + tx::TxStatus, + }; + + let mut tx = extrinsic.submit_and_watch().await?; + + while let Some(status) = tx.next().await { + match status? { + TxStatus::InFinalizedBlock(tx_in_block) => { + let events = tx_in_block.wait_for_success().await?; + return Ok(events) + }, + TxStatus::Error { message } => return Err(TransactionError::Error(message).into()), + TxStatus::Invalid { message } => return Err(TransactionError::Invalid(message).into()), + TxStatus::Dropped { message } => return Err(TransactionError::Dropped(message).into()), + _ => continue, + } + } + Err(RpcError::SubscriptionDropped.into()) +} + /// Estimate the gas required for instantiating a contract without modifying the state of the /// blockchain. /// /// # Arguments -/// /// * `instantiate_exec` - the preprocessed data to instantiate a contract. pub async fn dry_run_gas_estimate_instantiate( instantiate_exec: &InstantiateExec, @@ -136,14 +312,15 @@ pub async fn dry_run_gas_estimate_instantiate( /// Result of a dry-run upload of a smart contract. pub struct UploadDryRunResult { + /// The key under which the new code is stored. pub code_hash: String, + /// The deposit that was reserved at the caller. Is zero when the code already existed. pub deposit: String, } /// Performs a dry-run for uploading a contract without modifying the state of the blockchain. /// /// # Arguments -/// /// * `upload_exec` - the preprocessed data to upload a contract. pub async fn dry_run_upload( upload_exec: &UploadExec, @@ -175,7 +352,6 @@ pub struct ContractInfo { /// Instantiate a contract. /// /// # Arguments -/// /// * `instantiate_exec` - the preprocessed data to instantiate a contract. /// * `gas_limit` - maximum amount of gas to be used for this call. pub async fn instantiate_smart_contract( @@ -195,7 +371,6 @@ pub async fn instantiate_smart_contract( /// Upload a contract. /// /// # Arguments -/// /// * `upload_exec` - the preprocessed data to upload a contract. pub async fn upload_smart_contract( upload_exec: &UploadExec, @@ -204,14 +379,26 @@ pub async fn upload_smart_contract( .upload_code() .await .map_err(|error_variant| Error::UploadContractError(format!("{:?}", error_variant)))?; - if let Some(code_stored) = upload_result.code_stored { + get_code_hash_from_event(&upload_result, upload_exec.code().code_hash()) +} + +/// Get the code hash of a contract from the upload event. +/// +/// # Arguments +/// * `upload_result` - the result of uploading the contract. +/// * `metadata_code_hash` - the code hash from the metadata Used only for error reporting. +pub fn get_code_hash_from_event( + upload_result: &UploadResult, + // used for error reporting + metadata_code_hash: [u8; 32], +) -> Result { + if let Some(code_stored) = upload_result.code_stored.as_ref() { Ok(format!("{:?}", code_stored.code_hash)) } else { - let code_hash: String = - upload_exec.code().code_hash().iter().fold(String::new(), |mut output, b| { - write!(output, "{:02x}", b).expect("expected to write to string"); - output - }); + let code_hash: String = metadata_code_hash.iter().fold(String::new(), |mut output, b| { + write!(output, "{:02x}", b).expect("expected to write to string"); + output + }); Err(Error::UploadContractError(format!( "This contract has already been uploaded with code hash: 0x{code_hash}" ))) @@ -228,6 +415,10 @@ mod tests { use anyhow::Result; use pop_common::{find_free_port, set_executable_permission}; use std::{env, process::Command, time::Duration}; + use subxt::{ + config::{substrate::BlakeTwo256, Hasher}, + utils::H256, + }; use tokio::time::sleep; use url::Url; @@ -281,6 +472,43 @@ mod tests { Ok(()) } + #[tokio::test] + async fn get_payload_works() -> Result<()> { + let temp_dir = new_environment("testing")?; + let current_dir = env::current_dir().expect("Failed to get current directory"); + mock_build_process( + temp_dir.path().join("testing"), + current_dir.join("./tests/files/testing.contract"), + current_dir.join("./tests/files/testing.json"), + )?; + let up_opts = UpOpts { + path: Some(temp_dir.path().join("testing")), + constructor: "new".to_string(), + args: ["false".to_string()].to_vec(), + value: "1000".to_string(), + gas_limit: None, + proof_size: None, + salt: None, + url: Url::parse(CONTRACTS_NETWORK_URL)?, + suri: "//Alice".to_string(), + }; + let contract_code = get_contract_code(up_opts.path.as_ref())?; + let call_data = get_upload_payload(contract_code, CONTRACTS_NETWORK_URL).await?; + let payload_hash = BlakeTwo256::hash(&call_data); + // We know that for the above opts the payload hash should be: + // 0x98c24584107b3a01d12e8e02c0bb634d15dc86123c44d186206813ede42f478d + let hex_bytes = + from_hex("98c24584107b3a01d12e8e02c0bb634d15dc86123c44d186206813ede42f478d") + .expect("Invalid hex string"); + + let hex_array: [u8; 32] = hex_bytes.try_into().expect("Expected 32-byte array"); + + // Create `H256` from the `[u8; 32]` array + let expected_hash = H256::from(hex_array); + assert_eq!(expected_hash, payload_hash); + Ok(()) + } + #[tokio::test] async fn dry_run_gas_estimate_instantiate_works() -> Result<()> { let temp_dir = new_environment("testing")?; @@ -365,7 +593,7 @@ mod tests { #[tokio::test] async fn instantiate_and_upload() -> Result<()> { - let random_port = find_free_port(); + let random_port = find_free_port(None); let localhost_url = format!("ws://127.0.0.1:{}", random_port); let temp_dir = new_environment("testing")?; let current_dir = env::current_dir().expect("Failed to get current directory"); diff --git a/crates/pop-contracts/src/utils/metadata.rs b/crates/pop-contracts/src/utils/metadata.rs index 111468f0e..0c2d26d6d 100644 --- a/crates/pop-contracts/src/utils/metadata.rs +++ b/crates/pop-contracts/src/utils/metadata.rs @@ -117,7 +117,7 @@ fn get_contract_functions( /// # Arguments /// * `path` - Location path of the project or contract artifact. /// * `message` - The label of the contract message. -fn get_message

(path: P, message: &str) -> Result +pub fn get_message

(path: P, message: &str) -> Result where P: AsRef, { diff --git a/crates/pop-contracts/tests/files/testing.wasm b/crates/pop-contracts/tests/files/testing.wasm index 430888669..e69de29bb 100644 Binary files a/crates/pop-contracts/tests/files/testing.wasm and b/crates/pop-contracts/tests/files/testing.wasm differ diff --git a/crates/pop-parachains/Cargo.toml b/crates/pop-parachains/Cargo.toml index 64bef2d67..6380fbd10 100644 --- a/crates/pop-parachains/Cargo.toml +++ b/crates/pop-parachains/Cargo.toml @@ -24,11 +24,11 @@ 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 +sp-core.workspace = true subxt.workspace = true symlink.workspace = true toml_edit.workspace = true diff --git a/crates/pop-parachains/src/call/metadata/mod.rs b/crates/pop-parachains/src/call/metadata/mod.rs index 1f53f523c..5a9a9a2cb 100644 --- a/crates/pop-parachains/src/call/metadata/mod.rs +++ b/crates/pop-parachains/src/call/metadata/mod.rs @@ -4,7 +4,7 @@ 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}; +use subxt::{dynamic::Value, utils::to_hex, Metadata, OnlineClient, SubstrateConfig}; pub mod action; pub mod params; @@ -179,7 +179,7 @@ pub fn parse_dispatchable_arguments( .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)) + to_hex(&raw_param) } else { raw_param }; @@ -199,6 +199,7 @@ mod tests { use crate::{call::tests::POP_NETWORK_TESTNET_URL, set_up_client}; use anyhow::Result; + use sp_core::bytes::from_hex; use subxt::ext::scale_bits; #[tokio::test] @@ -283,7 +284,7 @@ mod tests { ] .to_vec(); let addr: Vec<_> = - hex::decode("8eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48") + from_hex("8eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48") .unwrap() .into_iter() .map(|b| Value::u128(b as u128)) diff --git a/crates/pop-parachains/src/call/mod.rs b/crates/pop-parachains/src/call/mod.rs index 1f7e61b22..f5bb05b71 100644 --- a/crates/pop-parachains/src/call/mod.rs +++ b/crates/pop-parachains/src/call/mod.rs @@ -5,12 +5,12 @@ use pop_common::{ call::{DefaultEnvironment, DisplayEvents, TokenMetadata, Verbosity}, create_signer, }; +use sp_core::bytes::{from_hex, to_hex}; use subxt::{ dynamic::Value, - tx::{DynamicPayload, Payload}, + tx::{DynamicPayload, Payload, SubmittableExtrinsic}, OnlineClient, SubstrateConfig, }; - pub mod metadata; /// Sets up an [OnlineClient] instance for connecting to a blockchain. @@ -82,6 +82,28 @@ pub async fn sign_and_submit_extrinsic( Ok(format!("Extrinsic Submitted with hash: {:?}\n\n{}", result.extrinsic_hash(), events)) } +/// Submits a signed extrinsic. +/// +/// # Arguments +/// * `client` - The client used to interact with the chain. +/// * `payload` - The signed payload string to be submitted. +pub async fn submit_signed_extrinsic( + client: OnlineClient, + payload: String, +) -> Result { + let hex_encoded = + from_hex(&payload).map_err(|e| Error::CallDataDecodingError(e.to_string()))?; + let extrinsic = SubmittableExtrinsic::from_bytes(client, hex_encoded); + let result = extrinsic + .submit_and_watch() + .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 @@ -94,7 +116,7 @@ pub fn encode_call_data( let call_data = xt .encode_call_data(&client.metadata()) .map_err(|e| Error::CallDataEncodingError(e.to_string()))?; - Ok(format!("0x{}", hex::encode(call_data))) + Ok(to_hex(&call_data, false)) } /// Decodes a hex-encoded string into a vector of bytes representing the call data. @@ -102,8 +124,7 @@ pub fn encode_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())) + from_hex(call_data).map_err(|e| Error::CallDataDecodingError(e.to_string())) } /// This struct implements the [`Payload`] trait and is used to submit diff --git a/crates/pop-parachains/src/lib.rs b/crates/pop-parachains/src/lib.rs index 5bba81542..7e70c2149 100644 --- a/crates/pop-parachains/src/lib.rs +++ b/crates/pop-parachains/src/lib.rs @@ -24,14 +24,17 @@ pub use call::{ params::Param, parse_chain_metadata, Function, Pallet, }, - set_up_client, sign_and_submit_extrinsic, CallData, + set_up_client, sign_and_submit_extrinsic, submit_signed_extrinsic, CallData, }; 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 subxt::{ + tx::{DynamicPayload, Payload}, + OnlineClient, SubstrateConfig, +}; pub use templates::{Config, Parachain, Provider}; pub use up::Zombienet; pub use utils::helpers::is_initial_endowment_valid;