├── metadata ├── kusama.scale ├── polkadot.scale ├── kusama_people.scale ├── kusama_asset_hub.scale ├── kusama_coretime.scale ├── kusama_encointer.scale ├── polkadot_people.scale ├── kusama_bridge_hub.scale ├── polkadot_asset_hub.scale ├── polkadot_bridge_hub.scale ├── polkadot_coretime.scale └── polkadot_collectives.scale ├── .gitignore ├── rustfmt.toml ├── Cargo.toml ├── src ├── functions.rs ├── main.rs ├── types.rs ├── tests.rs ├── build_upgrade.rs └── submit_referendum.rs ├── .github └── workflows │ └── rust.yml ├── LICENSE └── README.md /metadata/kusama.scale: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joepetrowski/opengov-cli/HEAD/metadata/kusama.scale -------------------------------------------------------------------------------- /metadata/polkadot.scale: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joepetrowski/opengov-cli/HEAD/metadata/polkadot.scale -------------------------------------------------------------------------------- /metadata/kusama_people.scale: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joepetrowski/opengov-cli/HEAD/metadata/kusama_people.scale -------------------------------------------------------------------------------- /metadata/kusama_asset_hub.scale: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joepetrowski/opengov-cli/HEAD/metadata/kusama_asset_hub.scale -------------------------------------------------------------------------------- /metadata/kusama_coretime.scale: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joepetrowski/opengov-cli/HEAD/metadata/kusama_coretime.scale -------------------------------------------------------------------------------- /metadata/kusama_encointer.scale: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joepetrowski/opengov-cli/HEAD/metadata/kusama_encointer.scale -------------------------------------------------------------------------------- /metadata/polkadot_people.scale: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joepetrowski/opengov-cli/HEAD/metadata/polkadot_people.scale -------------------------------------------------------------------------------- /metadata/kusama_bridge_hub.scale: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joepetrowski/opengov-cli/HEAD/metadata/kusama_bridge_hub.scale -------------------------------------------------------------------------------- /metadata/polkadot_asset_hub.scale: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joepetrowski/opengov-cli/HEAD/metadata/polkadot_asset_hub.scale -------------------------------------------------------------------------------- /metadata/polkadot_bridge_hub.scale: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joepetrowski/opengov-cli/HEAD/metadata/polkadot_bridge_hub.scale -------------------------------------------------------------------------------- /metadata/polkadot_coretime.scale: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joepetrowski/opengov-cli/HEAD/metadata/polkadot_coretime.scale -------------------------------------------------------------------------------- /metadata/polkadot_collectives.scale: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joepetrowski/opengov-cli/HEAD/metadata/polkadot_collectives.scale -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /upgrade* 3 | *.call 4 | *.wasm 5 | collectives_types.rs 6 | kusama_types.rs 7 | kusama_ah_types.rs 8 | polkadot_types.rs 9 | .DS_Store 10 | self-notes.md 11 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | hard_tabs = true 2 | max_width = 100 3 | use_small_heuristics = "Max" 4 | reorder_imports = true 5 | newline_style = "Unix" 6 | match_arm_leading_pipes = "Preserve" 7 | match_arm_blocks = false 8 | match_block_trailing_comma = true 9 | trailing_comma = "Vertical" 10 | trailing_semicolon = false 11 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "opengov-cli" 3 | version = "0.2.0" 4 | edition = "2021" 5 | authors = ["joepetrowski"] 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | parity-scale-codec = "3.7.5" 11 | sp-core = { git = "https://github.com/paritytech/polkadot-sdk", branch = "master" } 12 | subxt = "0.42.0" 13 | hex = "0.4.3" 14 | tokio = { version = "1", features = ["macros", "rt-multi-thread"] } 15 | clap = { version = "4.4.18", features = ["derive", "cargo"] } 16 | reqwest = "0.12.3" 17 | -------------------------------------------------------------------------------- /src/functions.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | 3 | // Check what the user entered for the proposal. If it is just call data, return it back. Otherwise, 4 | // we expect a path to a file that contains the call data. Read that in and return it. 5 | pub(crate) fn get_proposal_bytes(proposal: String) -> Vec { 6 | let proposal = proposal.as_str(); 7 | if proposal.starts_with("0x") { 8 | // This is just call data 9 | hex::decode(proposal.trim_start_matches("0x")).expect("Valid proposal") 10 | } else { 11 | // This is a file path 12 | let contents = fs::read_to_string(proposal).expect("Should give a valid file path"); 13 | hex::decode(contents.as_str().trim_start_matches("0x")).expect("Valid proposal") 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod types; 2 | use crate::types::*; 3 | mod functions; 4 | use crate::functions::*; 5 | mod build_upgrade; 6 | use crate::build_upgrade::{build_upgrade, UpgradeArgs}; 7 | mod submit_referendum; 8 | use crate::submit_referendum::{submit_referendum, ReferendumArgs}; 9 | use clap::Parser as ClapParser; 10 | 11 | #[cfg(test)] 12 | mod tests; 13 | 14 | /// Utilities for submitting OpenGov referenda and constructing tedious calls. 15 | #[derive(Debug, ClapParser)] 16 | enum Command { 17 | BuildUpgrade(UpgradeArgs), 18 | SubmitReferendum(ReferendumArgs), 19 | } 20 | 21 | #[tokio::main] 22 | async fn main() { 23 | let args = Command::parse(); 24 | match args { 25 | Command::BuildUpgrade(prefs) => build_upgrade(prefs).await, 26 | Command::SubmitReferendum(prefs) => submit_referendum(prefs).await, 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | # Cancel a currently running workflow from the same PR, branch or tag when a new workflow is 4 | # triggered (ref https://stackoverflow.com/a/72408109) 5 | concurrency: 6 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 7 | cancel-in-progress: true 8 | 9 | on: 10 | push: 11 | branches: ["master"] 12 | pull_request: 13 | branches: ["master"] 14 | 15 | env: 16 | CARGO_TERM_COLOR: always 17 | TOOLCHAIN_LINT: nightly-2023-11-13 18 | 19 | jobs: 20 | build: 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@v3 24 | - name: Build 25 | env: 26 | SKIP_WASM_BUILD: 1 27 | run: cargo build --verbose 28 | 29 | lint: 30 | runs-on: ubuntu-20.04 31 | steps: 32 | - uses: actions/checkout@v3 33 | 34 | - name: Free Disk Space 35 | uses: jlumbroso/free-disk-space@v1.3.1 36 | with: 37 | # this might remove tools that are actually needed, 38 | # if set to "true" but frees about 6 GB 39 | tool-cache: false 40 | 41 | # all of these default to true, but feel free to set to 42 | # "false" if necessary for your workflow 43 | android: true 44 | dotnet: true 45 | haskell: true 46 | large-packages: true 47 | docker-images: true 48 | swap-storage: false 49 | 50 | - name: Increase swap 51 | run: | 52 | sudo swapoff -a 53 | sudo fallocate -l 16G /swapfile 54 | sudo chmod 600 /swapfile 55 | sudo mkswap /swapfile 56 | sudo swapon /swapfile 57 | sudo swapon --show 58 | 59 | - name: Install nightly toolchain 60 | uses: actions-rs/toolchain@v1 61 | with: 62 | toolchain: ${{ env.TOOLCHAIN_LINT }} 63 | components: rustfmt, clippy, rust-src 64 | override: true 65 | 66 | - name: Fmt 67 | run: cargo +${{ env.TOOLCHAIN_LINT }} fmt --all -- --check 68 | 69 | - name: Clippy 70 | env: 71 | SKIP_WASM_BUILD: 1 72 | uses: actions-rs-plus/clippy-check@v2 73 | with: 74 | toolchain: ${{ env.TOOLCHAIN_LINT }} 75 | args: --all-targets --all-features -- -D warnings 76 | 77 | test: 78 | runs-on: ubuntu-latest 79 | steps: 80 | - uses: actions/checkout@v3 81 | - name: Run tests 82 | run: cargo test --verbose 83 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OpenGov CLI 2 | 3 | This program's primary purpose is to construct all the needed calls to submit a proposal as an OpenGov referendum on Kusama or Polkadot. It assumes that you construct the proposal (i.e., the privileged call you want to execute) elsewhere (e.g. Polkadot JS Apps UI Extrinsics tab). It will return all the calls that you will need to sign and submit (also using, e.g., the Apps UI Extrinsics tab). Note that you may need to submit calls on multiple chains. 4 | 5 | It also provides a utility to construct a runtime upgrade call that will batch the upgrades of the Kusama or Polkadot Relay Chain with the upgrades of all their respective system parachains. 6 | 7 | ## CLI 8 | 9 | This is a CLI program. To get started: 10 | 11 | ``` 12 | $ git clone https://github.com/joepetrowski/opengov-cli.git 13 | $ cd opengov-cli 14 | $ cargo build 15 | $ ./target/debug/opengov-cli --help 16 | Utilities for submitting OpenGov referenda and constructing tedious calls 17 | 18 | Usage: opengov-cli 19 | 20 | Commands: 21 | build-upgrade Generate a single call that will upgrade a Relay Chain and all of its system parachains 22 | submit-referendum Generate all the calls needed to submit a proposal as a referendum in OpenGov 23 | help Print this message or the help of the given subcommand(s) 24 | 25 | Options: 26 | -h, --help Print help 27 | ``` 28 | 29 | ### Submit Referendum 30 | 31 | The `submit-referendum` subcommand will take a proposal and some parameters and create all the necessary calls. Note that they can actually be submitted in any order. The preimages do not need to be submitted in order to start the referenda, but they will eventually in order to enact. 32 | 33 | ``` 34 | $ ./target/debug/opengov-cli submit-referendum --help 35 | Generate all the calls needed to submit a proposal as a referendum in OpenGov 36 | 37 | Usage: opengov-cli submit-referendum [OPTIONS] --proposal --network --track 38 | 39 | Options: 40 | -p, --proposal 41 | The encoded proposal that we want to submit. This can either be the call data itself, e.g. "0x0102...", or a file path that contains the data, e.g. "./my_proposal.call" 42 | -n, --network 43 | Network on which to submit the referendum. `polkadot` or `kusama` 44 | -t, --track 45 | Track on which to submit the referendum 46 | --at 47 | Optional: Enact at a particular block number 48 | --after 49 | Optional: Enact after a given number of blocks 50 | --output-len-limit 51 | Output length limit. Defaults to 1,000 52 | --no-batch 53 | Do not print batch calls. Defaults to false 54 | --output 55 | Form of output. `AppsUiLink` or `CallData`. Defaults to Apps UI 56 | --light-client 57 | Use light client endpoints instead of RPC for PAPI links 58 | -h, --help 59 | Print help 60 | ``` 61 | 62 | ### Build Upgrade 63 | 64 | The `build-upgrade` subcommand will take a Relay Chain name and version and construct a single call to upgrade the Relay Chain and all of its system parachains. 65 | 66 | ``` 67 | $ ./target/debug/opengov-cli build-upgrade --help 68 | Generate a single call that will upgrade a Relay Chain and all of its system parachains 69 | 70 | Usage: opengov-cli build-upgrade [OPTIONS] --network --relay-version 71 | 72 | Options: 73 | -n, --network Network on which to submit the referendum. `polkadot` or `kusama` 74 | --relay-version The Fellowship release version. Should be semver and correspond to the release published 75 | --asset-hub Optional. The runtime version of Asset Hub to which to upgrade. If not provided, it will use the Relay Chain's version 76 | --bridge-hub Optional. The runtime version of Bridge Hub to which to upgrade. If not provided, it will use the Relay Chain's version 77 | --collectives Optional. The runtime version of Collectives to which to upgrade. If not provided, it will use the Relay Chain's version 78 | --filename Name of the file to which to write the output. If not provided, a default will be constructed 79 | --additional Some additional call that you want executed on the Relay Chain along with the upgrade 80 | -h, --help Print help 81 | ``` 82 | 83 | ## Examples 84 | 85 | ### Build Upgrade 86 | 87 | ``` 88 | $ ./target/release/opengov-cli build-upgrade --network polkadot --relay-version "1.0.0" 89 | 90 | Downloading runtimes. 91 | 92 | Downloading... polkadot_runtime-v1000000.compact.compressed.wasm 93 | Downloading... asset_hub_polkadot_runtime-v1000000.compact.compressed.wasm 94 | Downloading... collectives_polkadot_runtime-v1000000.compact.compressed.wasm 95 | Downloading... bridge_hub_polkadot_runtime-v1000000.compact.compressed.wasm 96 | 97 | Generating parachain authorization calls. The runtime hashes are logged if you would like to verify them with srtool. 98 | 99 | Polkadot Asset Hub Runtime Hash: 0x52c2f520914514a196059fc8cc74f516a004f2463ba11d7385b5241bb5d50ee4 100 | Polkadot Collectives Runtime Hash: 0xb061815642328374a62b3282c78fa8bef5a27cd313d4ac79cbd49e43e0a4b879 101 | Polkadot Bridge Hub Runtime Hash: 0xf43e890a5eca0230a7eaaf88a60b8cc5ccb0ef157986f628650b071e47f7d323 102 | 103 | Generating Relay Chain upgrade call. The runtime hash is logged if you would like to verify it with srtool. 104 | 105 | Polkadot Relay Chain Runtime Hash: 0x17e1c7023134c196678f202daf8071a25f63f3e7b3937d8632a7474c618dc9a4 106 | 107 | Batching calls. 108 | 109 | Success! The call data was written to ./upgrade-polkadot-1.0.0/polkadot-1.0.0.call 110 | To submit this as a referendum in OpenGov, run: 111 | 112 | opengov-cli submit-referendum \ 113 | --proposal "./upgrade-polkadot-1.0.0/polkadot-1.0.0.call" \ 114 | --network "polkadot" --track <"root" or "whitelistedcaller"> 115 | ``` 116 | 117 | ### Submit a Referendum on Kusama 118 | 119 | As a proposal, send an [XCM to Kusama Asset Hub](https://dev.papi.how/extrinsics#data=0x630001000100a10f0204060202286bee880102957f0c9b47bc84d11116aef273e61565cf893801e7db0223aeea112e53922a4a&networkId=kusama&endpoint=wss%3A%2F%2Fkusama-rpc.dwellir.com) to authorize an upgrade. 120 | 121 | Call data: 122 | ``` 123 | 0x630001000100a10f0204060202286bee880102957f0c9b47bc84d11116aef273e61565cf893801e7db0223aeea112e53922a4a 124 | ``` 125 | 126 | This has a call hash of `0x4149bf15976cd3c0c244ca0cd43d59fed76f4bb936b186cc18bd88dee6edd986`. 127 | 128 | ``` 129 | $ ./target/debug/opengov-cli submit-referendum \ 130 | --proposal "0x630001000100a10f0204060202286bee880102957f0c9b47bc84d11116aef273e61565cf893801e7db0223aeea112e53922a4a" \ 131 | --network "kusama" --track "whitelistedcaller" \ 132 | --after "10" 133 | 134 | Submit the preimage for the Fellowship referendum: 135 | https://dev.papi.how/extrinsics#data=0x2000882c004149bf15976cd3c0c244ca0cd43d59fed76f4bb936b186cc18bd88dee6edd986&networkId=kusama&endpoint=wss%3A%2F%2Fkusama-rpc.dwellir.com 136 | 137 | Open a Fellowship referendum to whitelist the call: 138 | https://dev.papi.how/extrinsics#data=0x17002b0f024c02d09f7b5e4b71e357780baf8cb2d625dca6efaba2ee777516eaf72e5a14a022000000010a000000&networkId=kusama&endpoint=wss%3A%2F%2Fkusama-rpc.dwellir.com 139 | 140 | Submit the preimage for the public referendum: 141 | https://dev.papi.how/extrinsics#data=0x2000d42c03630001000100a10f0204060202286bee880102957f0c9b47bc84d11116aef273e61565cf893801e7db0223aeea112e53922a4a&networkId=kusama&endpoint=wss%3A%2F%2Fkusama-rpc.dwellir.com 142 | 143 | Open a public referendum to dispatch the call: 144 | https://dev.papi.how/extrinsics#data=0x15002b0d02022022c662d88f6b0f84c1771134e69b4412aff9e08a99e2a2da2794b5725fbe35000000010a000000&networkId=kusama&endpoint=wss%3A%2F%2Fkusama-rpc.dwellir.com 145 | 146 | Batch to submit on Kusama Relay Chain: 147 | https://dev.papi.how/extrinsics#data=0x1804102000882c004149bf15976cd3c0c244ca0cd43d59fed76f4bb936b186cc18bd88dee6edd98617002b0f024c02d09f7b5e4b71e357780baf8cb2d625dca6efaba2ee777516eaf72e5a14a022000000010a0000002000d42c03630001000100a10f0204060202286bee880102957f0c9b47bc84d11116aef273e61565cf893801e7db0223aeea112e53922a4a15002b0d02022022c662d88f6b0f84c1771134e69b4412aff9e08a99e2a2da2794b5725fbe35000000010a000000&networkId=kusama&endpoint=wss%3A%2F%2Fkusama-rpc.dwellir.com 148 | ``` 149 | 150 | This will return either two or four calls, the latter if the origin is `WhitelistedCaller`, which will require a preimage and referendum for the Fellowship. It also returns a batch call if you want to submit them all at once (you can hide this with `--no-batch "true"`). 151 | 152 | ### Submit a Referendum on Polkadot 153 | 154 | For Polkadot, we will use a proposal of `0x0000645468652046656c6c6f777368697020736179732068656c6c6f`, which is a `system.remark` call. We will use the Fellowship to whitelist it. 155 | 156 | The Fellowship is on the Collectives parachain, so this will require a referendum on the Collectives chain for the Fellowship to whitelist a call, and a referendum on the Relay Chain for it to pass public vote. Notice the WSS nodes pointing to different chains in the output. 157 | 158 | ``` 159 | $ ./target/debug/opengov-cli submit-referendum \ 160 | --proposal "0x0000645468652046656c6c6f777368697020736179732068656c6c6f" \ 161 | --network "polkadot" --track "whitelistedcaller" \ 162 | --after "10" 163 | 164 | Submit the preimage for the Fellowship referendum: 165 | https://dev.papi.how/extrinsics#data=0x2b00d41f0003010003082f00000603c2695e6d216f8817000363631d09c4ac33f2960d5d26b02f8ec89ac7a986c0bdab2a3a9f354acb6167&networkId=polkadot_collectives&endpoint=wss%3A%2F%2Fpolkadot-collectives-rpc.polkadot.io 166 | 167 | Open a Fellowship referendum to whitelist the call: 168 | https://dev.papi.how/extrinsics#data=0x3d003e0102664da7c8fb74a75e641b8aca751297fff57c5aee8014b3570e08f1454c06a88b35000000010a000000&networkId=polkadot_collectives&endpoint=wss%3A%2F%2Fpolkadot-collectives-rpc.polkadot.io 169 | 170 | Submit the preimage for the public referendum: 171 | https://dev.papi.how/extrinsics#data=0x0a007817030000645468652046656c6c6f777368697020736179732068656c6c6f&networkId=polkadot&endpoint=wss%3A%2F%2Fpolkadot-rpc.dwellir.com 172 | 173 | Open a public referendum to dispatch the call: 174 | https://dev.papi.how/extrinsics#data=0x1500160d022d1d8846a18770fc07a5b03383045d965aad65abb1077d0306142e60551813141e000000010a000000&networkId=polkadot&endpoint=wss%3A%2F%2Fpolkadot-rpc.dwellir.com 175 | 176 | Batch to submit on Polkadot Relay Chain: 177 | https://dev.papi.how/extrinsics#data=0x1a04080a007817030000645468652046656c6c6f777368697020736179732068656c6c6f1500160d022d1d8846a18770fc07a5b03383045d965aad65abb1077d0306142e60551813141e000000010a000000&networkId=polkadot&endpoint=wss%3A%2F%2Fpolkadot-rpc.dwellir.com 178 | 179 | Batch to submit on Polkadot Collectives Chain: 180 | https://dev.papi.how/extrinsics#data=0x2804082b00d41f0003010003082f00000603c2695e6d216f8817000363631d09c4ac33f2960d5d26b02f8ec89ac7a986c0bdab2a3a9f354acb61673d003e0102664da7c8fb74a75e641b8aca751297fff57c5aee8014b3570e08f1454c06a88b35000000010a000000&networkId=polkadot_collectives&endpoint=wss%3A%2F%2Fpolkadot-collectives-rpc.polkadot.io 181 | ``` 182 | 183 | ### Checking the Results 184 | 185 | We will use the Kusama referendum example to check the output. Let's check each of these calls and ensure they match our expectations. 186 | 187 | The **first one** just gives us some bytes wrapped in a `note_preimage` call: 188 | 189 | ![note-preimage](https://i.imgur.com/vfMq3MS.png) 190 | 191 | Let's of course look at those bytes: 192 | 193 | ![whitelist-call](https://i.imgur.com/VUEZcQk.png) 194 | 195 | This whitelists a call with the hash of our proposal, `0x4149bf15976cd3c0c244ca0cd43d59fed76f4bb936b186cc18bd88dee6edd986`, good. 196 | 197 | The **second call** starts a Fellowship referendum: 198 | 199 | ![fellowship-referendum](https://i.imgur.com/g1msmrV.png) 200 | 201 | Note that the hash is for the `whitelist_call` instruction, _not_ our actual proposal. 202 | 203 | The **third call** submits another preimage, this time a `dispatch_whitelisted_call_with_preimage` wrapping our actual proposal. The Fellowship referendum will have had to have passed before this executes. 204 | 205 | ![note-second-preimage](https://i.imgur.com/ECFTdDS.png) 206 | 207 | Let's again inspect the noted bytes. We should find that they contain our original proposal! 208 | 209 | ![dispatch-whitelisted](https://i.imgur.com/WvAeHLZ.png) 210 | 211 | It does, wrapped in the dispatch instruction. 212 | 213 | Finally, the **fourth call** submits a referendum to the public: 214 | 215 | ![start-referendum](https://i.imgur.com/hGN9YHG.png) 216 | 217 | Again, note that the hash of the referendum is the hash of the _dispatch_ instructions that contain our actual proposal. 218 | -------------------------------------------------------------------------------- /src/types.rs: -------------------------------------------------------------------------------- 1 | pub(super) use parity_scale_codec::Encode as _; 2 | pub(super) use sp_core::blake2_256; 3 | pub(super) use subxt::utils::H256; 4 | 5 | // Kusama Chains ----------------------------------------------------------------------------------- 6 | 7 | #[subxt::subxt( 8 | runtime_metadata_path = "metadata/kusama.scale", 9 | derive_for_all_types = "PartialEq, Clone" 10 | )] 11 | pub mod kusama_relay {} 12 | pub(super) use kusama_relay::runtime_types::staging_kusama_runtime::{ 13 | governance::origins::pallet_custom_origins::Origin as KusamaOpenGovOrigin, 14 | OriginCaller as KusamaOriginCaller, RuntimeCall as KusamaRuntimeCall, 15 | }; 16 | 17 | #[subxt::subxt( 18 | runtime_metadata_path = "metadata/kusama_asset_hub.scale", 19 | derive_for_all_types = "PartialEq, Clone" 20 | )] 21 | pub mod kusama_asset_hub {} 22 | pub(super) use kusama_asset_hub::runtime_types::asset_hub_kusama_runtime::{ 23 | governance::origins::pallet_custom_origins::Origin as KusamaAssetHubOpenGovOrigin, 24 | OriginCaller as KusamaAssetHubOriginCaller, RuntimeCall as KusamaAssetHubRuntimeCall, 25 | }; 26 | 27 | #[subxt::subxt(runtime_metadata_path = "metadata/kusama_bridge_hub.scale")] 28 | pub mod kusama_bridge_hub {} 29 | pub(super) use kusama_bridge_hub::runtime_types::bridge_hub_kusama_runtime::RuntimeCall as KusamaBridgeHubRuntimeCall; 30 | 31 | #[subxt::subxt(runtime_metadata_path = "metadata/kusama_encointer.scale")] 32 | pub mod kusama_encointer {} 33 | pub(super) use kusama_encointer::runtime_types::encointer_kusama_runtime::RuntimeCall as KusamaEncointerRuntimeCall; 34 | 35 | #[subxt::subxt(runtime_metadata_path = "metadata/kusama_people.scale")] 36 | pub mod kusama_people {} 37 | pub(super) use kusama_people::runtime_types::people_kusama_runtime::RuntimeCall as KusamaPeopleRuntimeCall; 38 | 39 | #[subxt::subxt(runtime_metadata_path = "metadata/kusama_coretime.scale")] 40 | pub mod kusama_coretime {} 41 | pub(super) use kusama_coretime::runtime_types::coretime_kusama_runtime::RuntimeCall as KusamaCoretimeRuntimeCall; 42 | 43 | // Polkadot Chains --------------------------------------------------------------------------------- 44 | 45 | #[subxt::subxt( 46 | runtime_metadata_path = "metadata/polkadot.scale", 47 | derive_for_all_types = "PartialEq, Clone" 48 | )] 49 | pub mod polkadot_relay {} 50 | pub(super) use polkadot_relay::runtime_types::polkadot_runtime::RuntimeCall as PolkadotRuntimeCall; 51 | 52 | #[subxt::subxt( 53 | runtime_metadata_path = "metadata/polkadot_asset_hub.scale", 54 | derive_for_all_types = "PartialEq, Clone" 55 | )] 56 | pub mod polkadot_asset_hub {} 57 | pub(super) use polkadot_asset_hub::runtime_types::asset_hub_polkadot_runtime::{ 58 | governance::origins::pallet_custom_origins::Origin as PolkadotAssetHubOpenGovOrigin, 59 | OriginCaller as PolkadotAssetHubOriginCaller, RuntimeCall as PolkadotAssetHubRuntimeCall, 60 | }; 61 | 62 | #[subxt::subxt(runtime_metadata_path = "metadata/polkadot_collectives.scale")] 63 | pub mod polkadot_collectives {} 64 | pub(super) use polkadot_collectives::runtime_types::collectives_polkadot_runtime::{ 65 | fellowship::origins::pallet_origins::Origin as FellowshipOrigins, 66 | RuntimeCall as CollectivesRuntimeCall, 67 | }; 68 | 69 | #[subxt::subxt(runtime_metadata_path = "metadata/polkadot_bridge_hub.scale")] 70 | pub mod polkadot_bridge_hub {} 71 | pub(super) use polkadot_bridge_hub::runtime_types::bridge_hub_polkadot_runtime::RuntimeCall as PolkadotBridgeHubRuntimeCall; 72 | 73 | #[subxt::subxt(runtime_metadata_path = "metadata/polkadot_people.scale")] 74 | pub mod polkadot_people {} 75 | pub(super) use polkadot_people::runtime_types::people_polkadot_runtime::RuntimeCall as PolkadotPeopleRuntimeCall; 76 | 77 | #[subxt::subxt(runtime_metadata_path = "metadata/polkadot_coretime.scale")] 78 | pub mod polkadot_coretime {} 79 | pub(super) use polkadot_coretime::runtime_types::coretime_polkadot_runtime::RuntimeCall as PolkadotCoretimeRuntimeCall; 80 | 81 | #[derive(Clone, Debug, PartialEq)] 82 | pub(super) enum Network { 83 | Kusama, 84 | KusamaAssetHub, 85 | KusamaEncointer, 86 | KusamaBridgeHub, 87 | KusamaPeople, 88 | KusamaCoretime, 89 | Polkadot, 90 | PolkadotAssetHub, 91 | PolkadotCollectives, 92 | PolkadotBridgeHub, 93 | PolkadotPeople, 94 | PolkadotCoretime, 95 | } 96 | 97 | impl Network { 98 | /// Return the `ParaId` of a given network. Returns an error if the network is not a parachain. 99 | pub(super) fn get_para_id(&self) -> Result { 100 | use Network::*; 101 | match &self { 102 | // Kusama 103 | Kusama => Err("relay chain"), 104 | KusamaAssetHub => Ok(1_000), 105 | KusamaBridgeHub => Ok(1_002), 106 | KusamaPeople => Ok(1_004), 107 | KusamaCoretime => Ok(1_005), 108 | KusamaEncointer => Ok(1_001), 109 | // Polkadot 110 | Polkadot => Err("relay chain"), 111 | PolkadotAssetHub => Ok(1_000), 112 | PolkadotBridgeHub => Ok(1_002), 113 | PolkadotCollectives => Ok(1_001), 114 | PolkadotPeople => Ok(1_004), 115 | PolkadotCoretime => Ok(1_005), 116 | } 117 | } 118 | } 119 | 120 | // Info and preferences provided by the user for proposal submission. 121 | pub(super) struct ProposalDetails { 122 | // The proposal, generated elsewhere and pasted here. 123 | pub(super) proposal: String, 124 | // The track to submit on. 125 | pub(super) track: NetworkTrack, 126 | // When do you want this to enact. `At(block)` or `After(blocks)`. 127 | pub(super) dispatch: DispatchTimeWrapper, 128 | // How you would like to view the output. 129 | pub(super) output: Output, 130 | // Cutoff length in bytes for printing the output. If too long, it will print the hash of the 131 | // call you would need to submit so that you can verify before submission. 132 | pub(super) output_len_limit: u32, 133 | // Whether or not to group all calls into a batch. Uses `force_batch` in case the account does 134 | // not have funds for pre-image deposits or is not a fellow. 135 | pub(super) print_batch: bool, 136 | // Whether to use light client endpoints in PAPI links (default true). 137 | pub(super) use_light_client: bool, 138 | } 139 | 140 | // Info and preferences provided by the user for runtime upgrade construction. 141 | pub(super) struct UpgradeDetails { 142 | // The Relay Network for this upgrade, Polkadot or Kusama. 143 | pub(super) relay: Network, 144 | // All networks to upgrade. 145 | pub(super) networks: Vec, 146 | // The directory into which to write information needed. 147 | pub(super) directory: String, 148 | // The filename of the output. 149 | pub(super) output_file: String, 150 | // An additional call to be enacted in the same batch as the system upgrade. 151 | pub(super) additional: Option, 152 | } 153 | 154 | // A network and the version to which it will upgrade. 155 | #[derive(Debug, PartialEq)] 156 | pub(super) struct VersionedNetwork { 157 | // A network identifier. 158 | pub(super) network: Network, 159 | // A runtime version number (i.e. "9430", not "0.9.43"). 160 | pub(super) version: String, 161 | } 162 | 163 | // The network and OpenGov track this proposal should be voted on. 164 | pub(super) enum NetworkTrack { 165 | KusamaRoot, 166 | Kusama(KusamaAssetHubOpenGovOrigin), 167 | PolkadotRoot, 168 | Polkadot(PolkadotAssetHubOpenGovOrigin), 169 | } 170 | 171 | // A runtime call wrapped in the network it should execute on. 172 | pub(super) enum NetworkRuntimeCall { 173 | Kusama(KusamaRuntimeCall), 174 | KusamaAssetHub(KusamaAssetHubRuntimeCall), 175 | KusamaBridgeHub(KusamaBridgeHubRuntimeCall), 176 | KusamaPeople(KusamaPeopleRuntimeCall), 177 | KusamaCoretime(KusamaCoretimeRuntimeCall), 178 | KusamaEncointer(KusamaEncointerRuntimeCall), 179 | Polkadot(PolkadotRuntimeCall), 180 | PolkadotAssetHub(PolkadotAssetHubRuntimeCall), 181 | PolkadotCollectives(CollectivesRuntimeCall), 182 | PolkadotBridgeHub(PolkadotBridgeHubRuntimeCall), 183 | PolkadotPeople(PolkadotPeopleRuntimeCall), 184 | PolkadotCoretime(PolkadotCoretimeRuntimeCall), 185 | } 186 | 187 | // How the user would like to see the output of the program. 188 | pub(super) enum Output { 189 | // Print just the call data (e.g. 0x1234). 190 | CallData, 191 | // Print a clickable link to view the decoded call on Polkadot JS Apps UI. 192 | AppsUiLink, 193 | } 194 | 195 | // Local concrete type to use in each runtime's `DispatchTime` 196 | pub(super) enum DispatchTimeWrapper { 197 | At(u32), 198 | After(u32), 199 | } 200 | 201 | // A call or a hash. Used for printing (or rather, to avoid printing large calls). 202 | // The Hash variant is only used when calls exceed the output length limit, which is rare. 203 | #[allow(clippy::large_enum_variant)] 204 | pub(super) enum CallOrHash { 205 | Call(NetworkRuntimeCall), 206 | Hash([u8; 32]), 207 | } 208 | 209 | // All the info associated with a call in the forms you may need it in. 210 | #[derive(Clone)] 211 | pub(super) struct CallInfo { 212 | pub(super) network: Network, 213 | pub(super) encoded: Vec, 214 | pub(super) hash: [u8; 32], 215 | pub(super) length: u32, 216 | } 217 | 218 | impl CallInfo { 219 | // Construct `Self` from a `NetworkRuntimeCall`. 220 | pub(super) fn from_runtime_call(call: NetworkRuntimeCall) -> Self { 221 | let (network, encoded) = match &call { 222 | NetworkRuntimeCall::Kusama(cc) => (Network::Kusama, cc.encode()), 223 | NetworkRuntimeCall::KusamaAssetHub(cc) => (Network::KusamaAssetHub, cc.encode()), 224 | NetworkRuntimeCall::KusamaBridgeHub(cc) => (Network::KusamaBridgeHub, cc.encode()), 225 | NetworkRuntimeCall::KusamaPeople(cc) => (Network::KusamaPeople, cc.encode()), 226 | NetworkRuntimeCall::KusamaCoretime(cc) => (Network::KusamaCoretime, cc.encode()), 227 | NetworkRuntimeCall::KusamaEncointer(cc) => (Network::KusamaEncointer, cc.encode()), 228 | NetworkRuntimeCall::Polkadot(cc) => (Network::Polkadot, cc.encode()), 229 | NetworkRuntimeCall::PolkadotAssetHub(cc) => (Network::PolkadotAssetHub, cc.encode()), 230 | NetworkRuntimeCall::PolkadotCollectives(cc) => 231 | (Network::PolkadotCollectives, cc.encode()), 232 | NetworkRuntimeCall::PolkadotBridgeHub(cc) => (Network::PolkadotBridgeHub, cc.encode()), 233 | NetworkRuntimeCall::PolkadotPeople(cc) => (Network::PolkadotPeople, cc.encode()), 234 | NetworkRuntimeCall::PolkadotCoretime(cc) => (Network::PolkadotCoretime, cc.encode()), 235 | }; 236 | let hash = blake2_256(&encoded); 237 | let length: u32 = (encoded.len()).try_into().unwrap(); 238 | Self { network, encoded: encoded.to_vec(), hash, length } 239 | } 240 | 241 | // Construct `Self` for some `network` given some `encoded` bytes. 242 | pub(super) fn from_bytes(encoded: &[u8], network: Network) -> Self { 243 | let hash = blake2_256(encoded); 244 | let length = (encoded.len()).try_into().unwrap(); 245 | Self { network, encoded: encoded.to_vec(), hash, length } 246 | } 247 | 248 | // Strip the outer enum and return a Kusama Relay `RuntimeCall`. 249 | pub(super) fn get_kusama_call(&self) -> Result { 250 | match &self.network { 251 | Network::Kusama => { 252 | let bytes = &self.encoded; 253 | Ok(::decode(&mut &bytes[..]) 254 | .unwrap()) 255 | }, 256 | _ => Err("not a kusama call"), 257 | } 258 | } 259 | 260 | // Strip the outer enum and return a Kusama Asset Hub `RuntimeCall`. 261 | #[allow(dead_code)] 262 | pub(super) fn get_kusama_asset_hub_call( 263 | &self, 264 | ) -> Result { 265 | match &self.network { 266 | Network::KusamaAssetHub => { 267 | let bytes = &self.encoded; 268 | Ok(::decode( 269 | &mut &bytes[..], 270 | ) 271 | .unwrap()) 272 | }, 273 | _ => Err("not a kusama asset hub call"), 274 | } 275 | } 276 | 277 | // Strip the outer enum and return a Kusama Bridge Hub `RuntimeCall`. 278 | #[allow(dead_code)] 279 | pub(super) fn get_kusama_bridge_hub_call( 280 | &self, 281 | ) -> Result { 282 | match &self.network { 283 | Network::KusamaBridgeHub => { 284 | let bytes = &self.encoded; 285 | Ok(::decode( 286 | &mut &bytes[..], 287 | ) 288 | .unwrap()) 289 | }, 290 | _ => Err("not a kusama bridge hub call"), 291 | } 292 | } 293 | 294 | // Strip the outer enum and return a Kusama Encointer `RuntimeCall`. 295 | #[allow(dead_code)] 296 | pub(super) fn get_kusama_encointer_call( 297 | &self, 298 | ) -> Result { 299 | match &self.network { 300 | Network::KusamaEncointer => { 301 | let bytes = &self.encoded; 302 | Ok(::decode( 303 | &mut &bytes[..], 304 | ) 305 | .unwrap()) 306 | }, 307 | _ => Err("not a kusama encointer call"), 308 | } 309 | } 310 | 311 | // Strip the outer enum and return a Kusama People `RuntimeCall`. 312 | #[allow(dead_code)] 313 | pub(super) fn get_kusama_people_call(&self) -> Result { 314 | match &self.network { 315 | Network::KusamaPeople => { 316 | let bytes = &self.encoded; 317 | Ok(::decode(&mut &bytes[..]) 318 | .unwrap()) 319 | }, 320 | _ => Err("not a kusama people call"), 321 | } 322 | } 323 | 324 | // Strip the outer enum and return a Kusama Coretime `RuntimeCall`. 325 | #[allow(dead_code)] 326 | pub(super) fn get_kusama_coretime_call( 327 | &self, 328 | ) -> Result { 329 | match &self.network { 330 | Network::KusamaCoretime => { 331 | let bytes = &self.encoded; 332 | Ok(::decode( 333 | &mut &bytes[..], 334 | ) 335 | .unwrap()) 336 | }, 337 | _ => Err("not a kusama coretime call"), 338 | } 339 | } 340 | 341 | // Strip the outer enum and return a Polkadot Relay `RuntimeCall`. 342 | pub(super) fn get_polkadot_call(&self) -> Result { 343 | match &self.network { 344 | Network::Polkadot => { 345 | let bytes = &self.encoded; 346 | Ok(::decode(&mut &bytes[..]) 347 | .unwrap()) 348 | }, 349 | _ => Err("not a polkadot call"), 350 | } 351 | } 352 | 353 | // Strip the outer enum and return a Polkadot Asset Hub `RuntimeCall`. 354 | #[allow(dead_code)] 355 | pub(super) fn get_polkadot_asset_hub_call( 356 | &self, 357 | ) -> Result { 358 | match &self.network { 359 | Network::PolkadotAssetHub => { 360 | let bytes = &self.encoded; 361 | Ok(::decode( 362 | &mut &bytes[..], 363 | ) 364 | .unwrap()) 365 | }, 366 | _ => Err("not a polkadot asset hub call"), 367 | } 368 | } 369 | 370 | // Strip the outer enum and return a Polkadot Collectives `RuntimeCall`. 371 | pub(super) fn get_polkadot_collectives_call( 372 | &self, 373 | ) -> Result { 374 | match &self.network { 375 | Network::PolkadotCollectives => { 376 | let bytes = &self.encoded; 377 | Ok(::decode(&mut &bytes[..]) 378 | .unwrap()) 379 | }, 380 | _ => Err("not a polkadot collectives call"), 381 | } 382 | } 383 | 384 | // Strip the outer enum and return a Polkadot Bridge Hub `RuntimeCall`. 385 | #[allow(dead_code)] 386 | pub(super) fn get_polkadot_bridge_hub_call( 387 | &self, 388 | ) -> Result { 389 | match &self.network { 390 | Network::PolkadotBridgeHub => { 391 | let bytes = &self.encoded; 392 | Ok(::decode( 393 | &mut &bytes[..], 394 | ) 395 | .unwrap()) 396 | }, 397 | _ => Err("not a polkadot bridge hub call"), 398 | } 399 | } 400 | 401 | // Strip the outer enum and return a Polkadot People `RuntimeCall`. 402 | #[allow(dead_code)] 403 | pub(super) fn get_polkadot_people_call( 404 | &self, 405 | ) -> Result { 406 | match &self.network { 407 | Network::PolkadotPeople => { 408 | let bytes = &self.encoded; 409 | Ok(::decode( 410 | &mut &bytes[..], 411 | ) 412 | .unwrap()) 413 | }, 414 | _ => Err("not a polkadot people call"), 415 | } 416 | } 417 | 418 | // Strip the outer enum and return a Polkadot Coretime `RuntimeCall`. 419 | #[allow(dead_code)] 420 | pub(super) fn get_polkadot_coretime_call( 421 | &self, 422 | ) -> Result { 423 | match &self.network { 424 | Network::PolkadotCoretime => { 425 | let bytes = &self.encoded; 426 | Ok(::decode( 427 | &mut &bytes[..], 428 | ) 429 | .unwrap()) 430 | }, 431 | _ => Err("not a polkadot coretime call"), 432 | } 433 | } 434 | 435 | // Take `Self` and a length limit as input. If the call length exceeds the limit, just return 436 | // its hash. Call length is recomputed and will be 2 bytes longer than the actual preimage 437 | // length. This is because the call is `preimage.note_preimage(call)`, so the outer pallet/call 438 | // indices have a length of 2 bytes. 439 | pub(super) fn create_print_output(&self, length_limit: u32) -> (CallOrHash, u32) { 440 | let print_output = if self.length > length_limit { 441 | CallOrHash::Hash(self.hash) 442 | } else { 443 | match &self.network { 444 | Network::Kusama => { 445 | let kusama_call = self.get_kusama_call().expect("kusama"); 446 | CallOrHash::Call(NetworkRuntimeCall::Kusama(kusama_call)) 447 | }, 448 | Network::KusamaAssetHub => { 449 | let kusama_asset_hub_call = 450 | self.get_kusama_asset_hub_call().expect("kusama asset hub"); 451 | CallOrHash::Call(NetworkRuntimeCall::KusamaAssetHub(kusama_asset_hub_call)) 452 | }, 453 | Network::Polkadot => { 454 | let polkadot_call = self.get_polkadot_call().expect("polkadot"); 455 | CallOrHash::Call(NetworkRuntimeCall::Polkadot(polkadot_call)) 456 | }, 457 | Network::PolkadotAssetHub => { 458 | let polkadot_asset_hub_call = 459 | self.get_polkadot_asset_hub_call().expect("polkadot asset hub"); 460 | CallOrHash::Call(NetworkRuntimeCall::PolkadotAssetHub(polkadot_asset_hub_call)) 461 | }, 462 | Network::PolkadotCollectives => { 463 | let collectives_call = 464 | self.get_polkadot_collectives_call().expect("collectives"); 465 | CallOrHash::Call(NetworkRuntimeCall::PolkadotCollectives(collectives_call)) 466 | }, 467 | _ => panic!("to do"), 468 | } 469 | }; 470 | (print_output, self.length) 471 | } 472 | } 473 | 474 | // The set of calls that some user will need to sign and submit to initiate a referendum. 475 | pub(super) struct PossibleCallsToSubmit { 476 | // `Some` if using the Fellowship to Whitelist a call. The second value is the length of the 477 | // call, which may be relevant to the print output. 478 | // 479 | // ``` 480 | // preimage.note(whitelist.whitelist_call(hash(proposal))); 481 | // ``` 482 | pub(super) preimage_for_whitelist_call: Option<(CallOrHash, u32)>, 483 | // The preimage for the public referendum. Should always be `Some`. When not using the 484 | // Whitelist, this will just be the proposal itself. When using the Whitelist, it will be the 485 | // proposal nested in a call to dispatch via Whitelist. The second value is the length of the 486 | // call, which may be relevant to the print output. 487 | // 488 | // ``` 489 | // // Without Fellowship 490 | // preimage.note(proposal); 491 | // 492 | // // With Fellowship 493 | // preimage.note(whitelist.dispatch_whitelisted_call_with_preimage(proposal)); 494 | // ``` 495 | pub(super) preimage_for_public_referendum: Option<(CallOrHash, u32)>, 496 | // The actual submission of the Fellowship referendum to Whitelist a call. `None` when not using 497 | // Whitelist. 498 | pub(super) fellowship_referendum_submission: Option, 499 | // The actual submission of the public referendum. The `proposal` is the proposal itself when 500 | // not using the Whitelist, or the dispatch call with nested proposal when using the Whitelist. 501 | pub(super) public_referendum_submission: Option, 502 | } 503 | -------------------------------------------------------------------------------- /src/tests.rs: -------------------------------------------------------------------------------- 1 | use crate::get_proposal_bytes; 2 | use crate::polkadot_asset_hub::runtime_types::frame_system::pallet::Call as PolkadotAssetHubSystemCall; 3 | use crate::polkadot_relay::runtime_types::frame_system::pallet::Call as PolkadotRelaySystemCall; 4 | use crate::{ 5 | build_upgrade, submit_referendum::generate_calls, CallInfo, CallOrHash, 6 | KusamaAssetHubOpenGovOrigin, Network, NetworkRuntimeCall, PolkadotAssetHubOpenGovOrigin, 7 | PolkadotAssetHubRuntimeCall, PolkadotRuntimeCall, ProposalDetails, UpgradeArgs, 8 | VersionedNetwork, 9 | }; 10 | 11 | fn polkadot_whitelist_remark_user_input() -> ProposalDetails { 12 | use crate::DispatchTimeWrapper::*; 13 | use crate::NetworkTrack::*; 14 | use crate::Output::*; 15 | ProposalDetails { 16 | // `system.remark("opengov-submit test")` 17 | proposal: String::from("0x00004c6f70656e676f762d7375626d69742074657374"), 18 | track: Polkadot(PolkadotAssetHubOpenGovOrigin::WhitelistedCaller), 19 | dispatch: After(10), 20 | output: AppsUiLink, 21 | output_len_limit: 1_000, 22 | print_batch: true, 23 | use_light_client: false, 24 | } 25 | } 26 | 27 | fn polkadot_staking_validator_user_input() -> ProposalDetails { 28 | use crate::DispatchTimeWrapper::*; 29 | use crate::NetworkTrack::*; 30 | use crate::Output::*; 31 | ProposalDetails { 32 | // `staking.increase_validator_count(50)` 33 | proposal: String::from("0x070ac8"), 34 | track: Polkadot(PolkadotAssetHubOpenGovOrigin::StakingAdmin), 35 | dispatch: After(10), 36 | output: AppsUiLink, 37 | output_len_limit: 1_000, 38 | print_batch: true, 39 | use_light_client: false, 40 | } 41 | } 42 | 43 | fn polkadot_root_remark_user_input() -> ProposalDetails { 44 | use crate::DispatchTimeWrapper::*; 45 | use crate::NetworkTrack::*; 46 | use crate::Output::*; 47 | ProposalDetails { 48 | // `system.remark("opengov-submit test")` 49 | proposal: String::from("0x00004c6f70656e676f762d7375626d69742074657374"), 50 | track: PolkadotRoot, 51 | dispatch: After(10), 52 | output: AppsUiLink, 53 | output_len_limit: 1_000, 54 | print_batch: true, 55 | use_light_client: false, 56 | } 57 | } 58 | 59 | fn kusama_whitelist_remark_user_input() -> ProposalDetails { 60 | use crate::DispatchTimeWrapper::*; 61 | use crate::NetworkTrack::*; 62 | use crate::Output::*; 63 | ProposalDetails { 64 | // `system.remark("opengov-submit test")` 65 | proposal: String::from("0x00004c6f70656e676f762d7375626d69742074657374"), 66 | track: Kusama(KusamaAssetHubOpenGovOrigin::WhitelistedCaller), 67 | dispatch: At(100_000_000), 68 | output: AppsUiLink, 69 | output_len_limit: 1_000, 70 | print_batch: true, 71 | use_light_client: false, 72 | } 73 | } 74 | 75 | fn kusama_staking_validator_user_input() -> ProposalDetails { 76 | use crate::DispatchTimeWrapper::*; 77 | use crate::NetworkTrack::*; 78 | use crate::Output::*; 79 | ProposalDetails { 80 | // `staking.increase_validator_count(50)` 81 | proposal: String::from("0x060ac8"), 82 | track: Kusama(KusamaAssetHubOpenGovOrigin::StakingAdmin), 83 | dispatch: At(100_000_000), 84 | output: AppsUiLink, 85 | output_len_limit: 1_000, 86 | print_batch: true, 87 | use_light_client: false, 88 | } 89 | } 90 | 91 | fn kusama_root_remark_user_input() -> ProposalDetails { 92 | use crate::DispatchTimeWrapper::*; 93 | use crate::NetworkTrack::*; 94 | use crate::Output::*; 95 | ProposalDetails { 96 | // `system.remark("opengov-submit test")` 97 | proposal: String::from("0x00004c6f70656e676f762d7375626d69742074657374"), 98 | track: KusamaRoot, 99 | dispatch: After(10), 100 | output: AppsUiLink, 101 | output_len_limit: 1_000, 102 | print_batch: true, 103 | use_light_client: false, 104 | } 105 | } 106 | 107 | fn limited_length_user_input() -> ProposalDetails { 108 | use crate::DispatchTimeWrapper::*; 109 | use crate::NetworkTrack::*; 110 | use crate::Output::*; 111 | ProposalDetails { 112 | // `system.remark("opengov-submit test")` 113 | proposal: String::from("0x00004c6f70656e676f762d7375626d69742074657374"), 114 | track: Polkadot(PolkadotAssetHubOpenGovOrigin::StakingAdmin), 115 | dispatch: After(10), 116 | output: AppsUiLink, 117 | output_len_limit: 5, // very limiting 118 | print_batch: true, 119 | use_light_client: false, 120 | } 121 | } 122 | 123 | fn upgrade_args_for_only_relay() -> UpgradeArgs { 124 | UpgradeArgs { 125 | network: String::from("polkadot"), 126 | only: true, 127 | local: false, 128 | relay_version: Some(String::from("v1.2.0")), 129 | asset_hub: None, 130 | bridge_hub: None, 131 | collectives: None, 132 | encointer: None, 133 | people: None, 134 | coretime: None, 135 | filename: None, 136 | additional: None, 137 | } 138 | } 139 | 140 | fn upgrade_args_for_only_asset_hub() -> UpgradeArgs { 141 | UpgradeArgs { 142 | network: String::from("polkadot"), 143 | only: true, 144 | local: false, 145 | relay_version: None, 146 | asset_hub: Some(String::from("v1.2.0")), 147 | bridge_hub: None, 148 | collectives: None, 149 | encointer: None, 150 | people: None, 151 | coretime: None, 152 | filename: None, 153 | additional: None, 154 | } 155 | } 156 | 157 | fn upgrade_args_for_all() -> UpgradeArgs { 158 | UpgradeArgs { 159 | network: String::from("polkadot"), 160 | only: false, 161 | local: false, 162 | relay_version: Some(String::from("v1.2.0")), 163 | asset_hub: None, 164 | bridge_hub: None, 165 | collectives: None, 166 | encointer: None, 167 | people: None, 168 | coretime: None, 169 | filename: None, 170 | additional: None, 171 | } 172 | } 173 | 174 | fn upgrade_args_with_additional() -> UpgradeArgs { 175 | UpgradeArgs { 176 | network: String::from("polkadot"), 177 | only: true, 178 | local: false, 179 | relay_version: Some(String::from("v1.2.0")), 180 | asset_hub: None, 181 | bridge_hub: None, 182 | collectives: None, 183 | encointer: None, 184 | people: None, 185 | coretime: None, 186 | filename: None, 187 | // `system.remark("test")` on Polkadot Asset Hub 188 | additional: Some(String::from("0x00001074657374")), 189 | } 190 | } 191 | 192 | #[test] 193 | fn call_info_from_bytes_works() { 194 | let proposal_details = polkadot_whitelist_remark_user_input(); 195 | let proposal_bytes = get_proposal_bytes(proposal_details.proposal); 196 | let proposal_call_info = CallInfo::from_bytes(&proposal_bytes, Network::Polkadot); 197 | 198 | let remark_to_verify = PolkadotRuntimeCall::System(PolkadotRelaySystemCall::remark { 199 | remark: b"opengov-submit test".to_vec(), 200 | }); 201 | 202 | let hash_to_verify = "0x8821e8db19b8e34b62ee8bc618a5ed3eecb9761d7d81349b00aa5ce5dfca2534"; 203 | let verification_bytes = 204 | hex::decode(hash_to_verify.trim_start_matches("0x")).expect("Valid hash"); 205 | 206 | assert_eq!(proposal_call_info.get_polkadot_call().expect("polkadot"), remark_to_verify); 207 | assert_eq!(proposal_call_info.encoded, proposal_bytes); 208 | assert_eq!(proposal_call_info.hash, &verification_bytes[..]); 209 | assert_eq!(proposal_call_info.length, 22u32); 210 | 211 | let bad_remark = PolkadotRuntimeCall::System(PolkadotRelaySystemCall::remark { 212 | remark: b"another remark".to_vec(), 213 | }); 214 | let bad_remark_hash = "0x8821e8db19b8e34b62ee8bc618a5ed3eecb9761d7d81349b00aa5ce5dfca2534"; 215 | let bad_verification = 216 | hex::decode(bad_remark_hash.trim_start_matches("0x")).expect("Valid hash"); 217 | assert_ne!(proposal_call_info.get_polkadot_call().expect("polkadot"), bad_remark); 218 | assert_eq!(proposal_call_info.hash, &bad_verification[..]); 219 | } 220 | 221 | #[test] 222 | fn call_info_from_runtime_call_works() { 223 | let remark_to_verify = PolkadotRuntimeCall::System(PolkadotRelaySystemCall::remark { 224 | remark: b"opengov-submit test".to_vec(), 225 | }); 226 | let call_info = CallInfo::from_runtime_call(NetworkRuntimeCall::Polkadot(remark_to_verify)); 227 | 228 | let encoded_to_verify = 229 | hex::decode("0x00004c6f70656e676f762d7375626d69742074657374".trim_start_matches("0x")) 230 | .expect("Valid encoded"); 231 | 232 | let hash_to_verify = "0x8821e8db19b8e34b62ee8bc618a5ed3eecb9761d7d81349b00aa5ce5dfca2534"; 233 | let verification_bytes = 234 | hex::decode(hash_to_verify.trim_start_matches("0x")).expect("Valid hash"); 235 | 236 | assert_eq!(call_info.encoded, encoded_to_verify); 237 | assert_eq!(call_info.hash, &verification_bytes[..]); 238 | assert_eq!(call_info.length, 22u32); 239 | } 240 | 241 | #[tokio::test] 242 | async fn it_starts_polkadot_non_fellowship_referenda_correctly() { 243 | let proposal_details = polkadot_staking_validator_user_input(); 244 | let calls = generate_calls(&proposal_details).await; 245 | 246 | let public_preimage = 247 | hex::decode("0x05000c070ac8".trim_start_matches("0x")).expect("Valid call"); 248 | let public_referendum = hex::decode("0x3e003f0002439a93279b25a49bf366c9fe1b06d4fc342f46b5a3b2734dcffe0c56c12b28ef03000000010a000000".trim_start_matches("0x")).expect("Valid call"); 249 | 250 | assert!(calls.preimage_for_whitelist_call.is_none(), "it must not generate this call"); 251 | assert!(calls.fellowship_referendum_submission.is_none(), "it must not generate this call"); 252 | 253 | assert!(calls.preimage_for_public_referendum.is_some(), "it must generate this call"); 254 | if let Some((coh, length)) = calls.preimage_for_public_referendum { 255 | match coh { 256 | CallOrHash::Call(public_preimage_generated) => { 257 | let call_info = CallInfo::from_runtime_call(public_preimage_generated); 258 | assert_eq!(call_info.encoded, public_preimage); 259 | assert_eq!(length, 6u32); 260 | }, 261 | CallOrHash::Hash(_) => panic!("call length within the limit"), 262 | } 263 | } 264 | 265 | assert!(calls.public_referendum_submission.is_some(), "it must generate this call"); 266 | if let Some(public_referendum_generated) = calls.public_referendum_submission { 267 | let call_info = CallInfo::from_runtime_call(public_referendum_generated); 268 | assert_eq!(call_info.encoded, public_referendum); 269 | } 270 | } 271 | 272 | #[tokio::test] 273 | async fn it_starts_polkadot_fellowship_referenda_correctly() { 274 | // Fellowship is on Collectives, send XCM to Asset Hub to whitelist. 275 | let proposal_details = polkadot_whitelist_remark_user_input(); 276 | let calls = generate_calls(&proposal_details).await; 277 | 278 | let public_preimage = hex::decode( 279 | "0x050060400300004c6f70656e676f762d7375626d69742074657374".trim_start_matches("0x"), 280 | ) 281 | .expect("Valid call"); 282 | // Fellowship referendum now uses Inline, so it contains the XCM call directly. 283 | let fellowship_referendum = hex::decode("0x3d003e0201cc1f0005010100a10f05082f00000603008840008821e8db19b8e34b62ee8bc618a5ed3eecb9761d7d81349b00aa5ce5dfca2534010a000000".trim_start_matches("0x")).expect("Valid call"); 284 | let public_referendum = hex::decode("0x3e003f0d02a322f65fd03ba368587f997b14e306211f6fb3c30b06a5be472f2f96b3b27e1e18000000010a000000".trim_start_matches("0x")).expect("Valid call"); 285 | 286 | // No preimage needed for whitelist call - it's inlined in the fellowship referendum. 287 | assert!(calls.preimage_for_whitelist_call.is_none(), "should be None with Inline"); 288 | 289 | assert!(calls.preimage_for_public_referendum.is_some(), "it must generate this call"); 290 | if let Some((coh, length)) = calls.preimage_for_public_referendum { 291 | match coh { 292 | CallOrHash::Call(public_preimage_generated) => { 293 | let call_info = CallInfo::from_runtime_call(public_preimage_generated); 294 | assert_eq!(call_info.encoded, public_preimage); 295 | assert_eq!(length, 27u32); 296 | }, 297 | CallOrHash::Hash(_) => panic!("call length within the limit"), 298 | } 299 | } 300 | 301 | assert!(calls.fellowship_referendum_submission.is_some(), "it must generate this call"); 302 | if let Some(fellowship_referendum_generated) = calls.fellowship_referendum_submission { 303 | let call_info = CallInfo::from_runtime_call(fellowship_referendum_generated); 304 | assert_eq!(call_info.encoded, fellowship_referendum); 305 | } 306 | 307 | assert!(calls.public_referendum_submission.is_some(), "it must generate this call"); 308 | if let Some(public_referendum_generated) = calls.public_referendum_submission { 309 | let call_info = CallInfo::from_runtime_call(public_referendum_generated); 310 | assert_eq!(call_info.encoded, public_referendum); 311 | } 312 | } 313 | 314 | #[tokio::test] 315 | async fn it_starts_polkadot_root_referenda_correctly() { 316 | let proposal_details = polkadot_root_remark_user_input(); 317 | let calls = generate_calls(&proposal_details).await; 318 | 319 | let public_preimage = hex::decode( 320 | "0x05005800004c6f70656e676f762d7375626d69742074657374".trim_start_matches("0x"), 321 | ) 322 | .expect("Valid call"); 323 | let public_referendum = hex::decode("0x3e000000028821e8db19b8e34b62ee8bc618a5ed3eecb9761d7d81349b00aa5ce5dfca253416000000010a000000".trim_start_matches("0x")).expect("Valid call"); 324 | 325 | assert!(calls.preimage_for_whitelist_call.is_none(), "it must not generate this call"); 326 | assert!(calls.fellowship_referendum_submission.is_none(), "it must not generate this call"); 327 | 328 | assert!(calls.preimage_for_public_referendum.is_some(), "it must generate this call"); 329 | if let Some((coh, length)) = calls.preimage_for_public_referendum { 330 | match coh { 331 | CallOrHash::Call(public_preimage_generated) => { 332 | let call_info = CallInfo::from_runtime_call(public_preimage_generated); 333 | assert_eq!(call_info.encoded, public_preimage); 334 | assert_eq!(length, 25u32); 335 | }, 336 | CallOrHash::Hash(_) => panic!("call length within the limit"), 337 | } 338 | } 339 | 340 | assert!(calls.public_referendum_submission.is_some(), "it must generate this call"); 341 | if let Some(public_referendum_generated) = calls.public_referendum_submission { 342 | let call_info = CallInfo::from_runtime_call(public_referendum_generated); 343 | assert_eq!(call_info.encoded, public_referendum); 344 | } 345 | } 346 | 347 | #[tokio::test] 348 | async fn it_starts_kusama_non_fellowship_referenda_correctly() { 349 | let proposal_details = kusama_staking_validator_user_input(); 350 | let calls = generate_calls(&proposal_details).await; 351 | 352 | let public_preimage = 353 | hex::decode("0x06000c060ac8".trim_start_matches("0x")).expect("Valid call"); 354 | let public_referendum = hex::decode("0x5c005d00028fd8848a8f93980f5cea2de1c11f29ed7dced592aa207218a2e0ae5b78b9fffb030000000000e1f505".trim_start_matches("0x")).expect("Valid call"); 355 | 356 | assert!(calls.preimage_for_whitelist_call.is_none(), "it must not generate this call"); 357 | assert!(calls.fellowship_referendum_submission.is_none(), "it must not generate this call"); 358 | 359 | assert!(calls.preimage_for_public_referendum.is_some(), "it must generate this call"); 360 | if let Some((coh, length)) = calls.preimage_for_public_referendum { 361 | match coh { 362 | CallOrHash::Call(public_preimage_generated) => { 363 | let call_info = CallInfo::from_runtime_call(public_preimage_generated); 364 | assert_eq!(call_info.encoded, public_preimage); 365 | assert_eq!(length, 6u32); 366 | }, 367 | CallOrHash::Hash(_) => panic!("call length within the limit"), 368 | } 369 | } 370 | 371 | assert!(calls.public_referendum_submission.is_some(), "it must generate this call"); 372 | if let Some(public_referendum_generated) = calls.public_referendum_submission { 373 | let call_info = CallInfo::from_runtime_call(public_referendum_generated); 374 | assert_eq!(call_info.encoded, public_referendum); 375 | } 376 | } 377 | 378 | #[tokio::test] 379 | async fn it_starts_kusama_fellowship_referenda_correctly() { 380 | let proposal_details = kusama_whitelist_remark_user_input(); 381 | let calls = generate_calls(&proposal_details).await; 382 | 383 | // On Kusama, the fellowship is on the Relay Chain and uses inline calls, 384 | // so preimage_for_whitelist_call is None. The fellowship referendum is submitted 385 | // on the Relay Chain and sends XCM to Asset Hub to whitelist. 386 | let public_preimage = hex::decode( 387 | "0x0600605e0300004c6f70656e676f762d7375626d69742074657374".trim_start_matches("0x"), 388 | ) 389 | .expect("Valid call"); 390 | let fellowship_referendum = hex::decode("0x17002b0f01cc630005000100a10f05082f0000060300885e008821e8db19b8e34b62ee8bc618a5ed3eecb9761d7d81349b00aa5ce5dfca2534010a000000".trim_start_matches("0x")).expect("Valid call"); 391 | let public_referendum = hex::decode("0x5c005d0d02dd86316423e1bc1ca2ac30b36d9384c7edea7b4e033a2b81c1a45c75091c2f15180000000000e1f505".trim_start_matches("0x")).expect("Valid call"); 392 | 393 | // Kusama fellowship uses inline preimage, so no separate preimage note call 394 | assert!( 395 | calls.preimage_for_whitelist_call.is_none(), 396 | "kusama uses inline preimages for fellowship" 397 | ); 398 | 399 | assert!(calls.preimage_for_public_referendum.is_some(), "it must generate this call"); 400 | if let Some((coh, length)) = calls.preimage_for_public_referendum { 401 | match coh { 402 | CallOrHash::Call(public_preimage_generated) => { 403 | let call_info = CallInfo::from_runtime_call(public_preimage_generated); 404 | assert_eq!(call_info.encoded, public_preimage); 405 | assert_eq!(length, 27u32); 406 | }, 407 | CallOrHash::Hash(_) => panic!("call length within the limit"), 408 | } 409 | } 410 | 411 | assert!(calls.fellowship_referendum_submission.is_some(), "it must generate this call"); 412 | if let Some(fellowship_referendum_generated) = calls.fellowship_referendum_submission { 413 | let call_info = CallInfo::from_runtime_call(fellowship_referendum_generated); 414 | assert_eq!(call_info.encoded, fellowship_referendum); 415 | } 416 | 417 | assert!(calls.public_referendum_submission.is_some(), "it must generate this call"); 418 | if let Some(public_referendum_generated) = calls.public_referendum_submission { 419 | let call_info = CallInfo::from_runtime_call(public_referendum_generated); 420 | assert_eq!(call_info.encoded, public_referendum); 421 | } 422 | } 423 | 424 | #[tokio::test] 425 | async fn it_starts_kusama_root_referenda_correctly() { 426 | let proposal_details = kusama_root_remark_user_input(); 427 | let calls = generate_calls(&proposal_details).await; 428 | 429 | let public_preimage = hex::decode( 430 | "0x06005800004c6f70656e676f762d7375626d69742074657374".trim_start_matches("0x"), 431 | ) 432 | .expect("Valid call"); 433 | let public_referendum = hex::decode("0x5c000000028821e8db19b8e34b62ee8bc618a5ed3eecb9761d7d81349b00aa5ce5dfca253416000000010a000000".trim_start_matches("0x")).expect("Valid call"); 434 | 435 | assert!(calls.preimage_for_whitelist_call.is_none(), "it must not generate this call"); 436 | assert!(calls.fellowship_referendum_submission.is_none(), "it must not generate this call"); 437 | 438 | assert!(calls.preimage_for_public_referendum.is_some(), "it must generate this call"); 439 | if let Some((coh, length)) = calls.preimage_for_public_referendum { 440 | match coh { 441 | CallOrHash::Call(public_preimage_generated) => { 442 | let call_info = CallInfo::from_runtime_call(public_preimage_generated); 443 | assert_eq!(call_info.encoded, public_preimage); 444 | assert_eq!(length, 25u32); 445 | }, 446 | CallOrHash::Hash(_) => panic!("call length within the limit"), 447 | } 448 | } 449 | 450 | assert!(calls.public_referendum_submission.is_some(), "it must generate this call"); 451 | if let Some(public_referendum_generated) = calls.public_referendum_submission { 452 | let call_info = CallInfo::from_runtime_call(public_referendum_generated); 453 | assert_eq!(call_info.encoded, public_referendum); 454 | } 455 | } 456 | 457 | #[test] 458 | fn only_relay_chain() { 459 | let args = upgrade_args_for_only_relay(); 460 | let details = build_upgrade::parse_inputs(args); 461 | assert_eq!(details.relay, Network::Polkadot); 462 | let expected_networks = 463 | vec![VersionedNetwork { network: Network::Polkadot, version: String::from("1.2.0") }]; 464 | assert_eq!(details.networks, expected_networks); 465 | assert!(details.additional.is_none()); 466 | } 467 | 468 | #[test] 469 | fn only_asset_hub() { 470 | let args = upgrade_args_for_only_asset_hub(); 471 | let details = build_upgrade::parse_inputs(args); 472 | assert_eq!(details.relay, Network::Polkadot); 473 | let expected_networks = vec![VersionedNetwork { 474 | network: Network::PolkadotAssetHub, 475 | version: String::from("1.2.0"), 476 | }]; 477 | assert_eq!(details.networks, expected_networks); 478 | assert!(details.additional.is_none()); 479 | } 480 | 481 | #[test] 482 | fn upgrade_everything_works_with_just_relay_version() { 483 | let args = upgrade_args_for_all(); 484 | let details = build_upgrade::parse_inputs(args); 485 | assert_eq!(details.relay, Network::Polkadot); 486 | let expected_networks = vec![ 487 | VersionedNetwork { network: Network::Polkadot, version: String::from("1.2.0") }, 488 | VersionedNetwork { network: Network::PolkadotAssetHub, version: String::from("1.2.0") }, 489 | VersionedNetwork { network: Network::PolkadotCollectives, version: String::from("1.2.0") }, 490 | VersionedNetwork { network: Network::PolkadotBridgeHub, version: String::from("1.2.0") }, 491 | VersionedNetwork { network: Network::PolkadotPeople, version: String::from("1.2.0") }, 492 | VersionedNetwork { network: Network::PolkadotCoretime, version: String::from("1.2.0") }, 493 | ]; 494 | assert_eq!(details.networks, expected_networks); 495 | assert!(details.additional.is_none()); 496 | } 497 | 498 | #[test] 499 | fn additional_call_decodes_correctly() { 500 | let args = upgrade_args_with_additional(); 501 | let details = build_upgrade::parse_inputs(args); 502 | 503 | assert!(details.additional.is_some(), "additional should be set"); 504 | let additional = details.additional.unwrap(); 505 | 506 | // Verify the call decodes to the expected remark on Asset Hub 507 | let expected_remark = PolkadotAssetHubRuntimeCall::System(PolkadotAssetHubSystemCall::remark { 508 | remark: b"test".to_vec(), 509 | }); 510 | assert_eq!( 511 | additional.get_polkadot_asset_hub_call().expect("polkadot asset hub call"), 512 | expected_remark 513 | ); 514 | 515 | // Verify length (0x00001074657374 = 7 bytes) 516 | assert_eq!(additional.length, 7u32); 517 | } 518 | 519 | #[test] 520 | fn it_creates_constrained_print_output() { 521 | let proposal_details = limited_length_user_input(); 522 | let proposal_bytes = get_proposal_bytes(proposal_details.proposal); 523 | let proposal_call_info = CallInfo::from_bytes(&proposal_bytes, Network::Polkadot); 524 | let (coh, length) = proposal_call_info.create_print_output(proposal_details.output_len_limit); 525 | 526 | let expected_hash = hex::decode( 527 | "0x8821e8db19b8e34b62ee8bc618a5ed3eecb9761d7d81349b00aa5ce5dfca2534" 528 | .trim_start_matches("0x"), 529 | ) 530 | .expect("Valid hash"); 531 | 532 | match coh { 533 | CallOrHash::Call(_) => panic!("this should not have a call"), 534 | CallOrHash::Hash(h) => { 535 | assert_eq!(h, &expected_hash[..]); 536 | }, 537 | } 538 | assert_eq!(length, proposal_call_info.length); 539 | } 540 | -------------------------------------------------------------------------------- /src/build_upgrade.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | use clap::Parser as ClapParser; 3 | use std::fs; 4 | use std::path::Path; 5 | 6 | /// Generate a single call that will upgrade all system chains in a given network. 7 | #[derive(Debug, ClapParser)] 8 | pub(crate) struct UpgradeArgs { 9 | /// Network on which to submit the referendum. `polkadot` or `kusama`. 10 | #[clap(long = "network", short)] 11 | pub(crate) network: String, 12 | 13 | /// Only include the runtimes explicitly specified. 14 | #[clap(long = "only")] 15 | pub(crate) only: bool, 16 | 17 | /// Use local WASM files instead of downloading from GitHub. Files are assumed to already be in 18 | /// the upgrade directory. 19 | #[clap(long = "local")] 20 | pub(crate) local: bool, 21 | 22 | /// The Fellowship release version. Should be semver and correspond to the release published. 23 | #[clap(long = "relay-version")] 24 | pub(crate) relay_version: Option, 25 | 26 | /// Optional. The runtime version of Asset Hub to which to upgrade. If not provided, it will use 27 | /// the Relay Chain's version. 28 | #[clap(long = "asset-hub")] 29 | pub(crate) asset_hub: Option, 30 | 31 | /// Optional. The runtime version of Bridge Hub to which to upgrade. If not provided, it will use 32 | /// the Relay Chain's version. 33 | #[clap(long = "bridge-hub")] 34 | pub(crate) bridge_hub: Option, 35 | 36 | /// Optional. The runtime version of Collectives to which to upgrade. If not provided, it will 37 | /// use the Relay Chain's version. 38 | #[clap(long = "collectives")] 39 | pub(crate) collectives: Option, 40 | 41 | /// Optional. The runtime version of Encointer to which to upgrade. If not provided, it will use 42 | /// the Relay Chain's version. 43 | #[clap(long = "encointer")] 44 | pub(crate) encointer: Option, 45 | 46 | /// Optional. The runtime version of People to which to upgrade. If not provided, it will use 47 | /// the Relay Chain's version. 48 | #[clap(long = "people")] 49 | pub(crate) people: Option, 50 | 51 | /// Optional. The runtime version of Coretime to which to upgrade. If not provided, it will use 52 | /// the Relay Chain's version. 53 | #[clap(long = "coretime")] 54 | pub(crate) coretime: Option, 55 | 56 | /// Name of the file to which to write the output. If not provided, a default will be 57 | /// constructed. 58 | #[clap(long = "filename")] 59 | pub(crate) filename: Option, 60 | 61 | /// Some additional call that you want executed on the Relay Chain along with the upgrade. 62 | #[clap(long = "additional")] 63 | pub(crate) additional: Option, 64 | } 65 | 66 | // The sub-command's "main" function. 67 | pub(crate) async fn build_upgrade(prefs: UpgradeArgs) { 68 | // 0. Find out what to do. 69 | let use_local = prefs.local; 70 | let upgrade_details = parse_inputs(prefs); 71 | 72 | // 1. Download all the Wasm files needed from the release pages (unless using local files). 73 | if use_local { 74 | println!("\nUsing local WASM files from {}\n", upgrade_details.directory); 75 | } else { 76 | download_runtimes(&upgrade_details).await; 77 | } 78 | 79 | // 2. Construct the `authorize_upgrade` call on each chain. 80 | let authorization_calls = generate_authorize_upgrade_calls(&upgrade_details); 81 | 82 | // 3. Construct a `force_batch` call with everything. 83 | let batch = construct_batch(&upgrade_details, authorization_calls).await; 84 | 85 | // 4. Write this call as a file that can then be passed to `submit_referendum`. 86 | write_batch(&upgrade_details, batch); 87 | } 88 | 89 | fn chain_version(chain: Option, default: Option, only: bool) -> Option { 90 | // if the user specified a version for this particular chain, use it 91 | if let Some(v) = chain { 92 | Some(String::from(v.trim_start_matches('v'))) 93 | } else { 94 | // if the user only wants to upgrade specific chains, and have not specified this one, then 95 | // return None so that it will not be added to the batch of upgrades 96 | if only { 97 | None 98 | // otherwise, use the default version 99 | } else { 100 | assert!(default.is_some(), "no version specified"); 101 | default 102 | } 103 | } 104 | } 105 | 106 | // Parse the CLI inputs and return a typed struct with all the details needed. 107 | pub(crate) fn parse_inputs(prefs: UpgradeArgs) -> UpgradeDetails { 108 | let mut networks = Vec::new(); 109 | let only = prefs.only; 110 | 111 | if !only { 112 | assert!( 113 | prefs.relay_version.is_some(), 114 | "relay-version must be specified unless using --only" 115 | ); 116 | } 117 | let relay_version = chain_version(prefs.relay_version, None, only); 118 | let asset_hub_version = chain_version(prefs.asset_hub, relay_version.clone(), only); 119 | let bridge_hub_version = chain_version(prefs.bridge_hub, relay_version.clone(), only); 120 | let people_version = chain_version(prefs.people, relay_version.clone(), only); 121 | let coretime_version = chain_version(prefs.coretime, relay_version.clone(), only); 122 | let encointer_version = chain_version(prefs.encointer, relay_version.clone(), only); 123 | let collectives_version = chain_version(prefs.collectives, relay_version.clone(), only); 124 | 125 | let relay = match prefs.network.to_ascii_lowercase().as_str() { 126 | "polkadot" => { 127 | if let Some(v) = relay_version.clone() { 128 | networks.push(VersionedNetwork { network: Network::Polkadot, version: v }); 129 | } 130 | if let Some(v) = asset_hub_version.clone() { 131 | networks.push(VersionedNetwork { network: Network::PolkadotAssetHub, version: v }); 132 | } 133 | if let Some(v) = collectives_version.clone() { 134 | networks 135 | .push(VersionedNetwork { network: Network::PolkadotCollectives, version: v }); 136 | } 137 | if let Some(v) = bridge_hub_version.clone() { 138 | networks.push(VersionedNetwork { network: Network::PolkadotBridgeHub, version: v }); 139 | } 140 | if let Some(v) = people_version.clone() { 141 | networks.push(VersionedNetwork { network: Network::PolkadotPeople, version: v }); 142 | } 143 | if let Some(v) = coretime_version.clone() { 144 | networks.push(VersionedNetwork { network: Network::PolkadotCoretime, version: v }); 145 | } 146 | Network::Polkadot 147 | }, 148 | "kusama" => { 149 | if let Some(v) = relay_version.clone() { 150 | networks.push(VersionedNetwork { network: Network::Kusama, version: v }); 151 | } 152 | if let Some(v) = asset_hub_version.clone() { 153 | networks.push(VersionedNetwork { network: Network::KusamaAssetHub, version: v }); 154 | } 155 | if let Some(v) = encointer_version.clone() { 156 | networks.push(VersionedNetwork { network: Network::KusamaEncointer, version: v }); 157 | } 158 | if let Some(v) = bridge_hub_version.clone() { 159 | networks.push(VersionedNetwork { network: Network::KusamaBridgeHub, version: v }); 160 | } 161 | if let Some(v) = people_version.clone() { 162 | networks.push(VersionedNetwork { network: Network::KusamaPeople, version: v }); 163 | } 164 | if let Some(v) = coretime_version.clone() { 165 | networks.push(VersionedNetwork { network: Network::KusamaCoretime, version: v }); 166 | } 167 | Network::Kusama 168 | }, 169 | _ => panic!("`network` must be `polkadot` or `kusama`"), 170 | }; 171 | 172 | let additional = match prefs.additional { 173 | Some(c) => { 174 | let additional_bytes = get_proposal_bytes(c.clone()); 175 | match relay { 176 | // This match isn't as intuitive post-ahm, as these are AH calls. 177 | Network::Polkadot => 178 | Some(CallInfo::from_bytes(&additional_bytes, Network::PolkadotAssetHub)), 179 | Network::Kusama => 180 | Some(CallInfo::from_bytes(&additional_bytes, Network::KusamaAssetHub)), 181 | _ => panic!("`network` must be `polkadot` or `kusama`"), 182 | } 183 | }, 184 | None => None, 185 | }; 186 | 187 | // Get a version from one of the args. (This still feels dirty.) 188 | let version = relay_version.clone().unwrap_or(asset_hub_version.unwrap_or( 189 | bridge_hub_version.unwrap_or(encointer_version.unwrap_or(collectives_version.unwrap_or( 190 | coretime_version.unwrap_or(people_version.unwrap_or(String::from("no-version"))), 191 | ))), 192 | )); 193 | 194 | // Set up a directory to store information fetched/written during this program. 195 | let directory = format!("./upgrade-{}-{}/", &prefs.network, &version); 196 | let output_file = if let Some(user_filename) = prefs.filename { 197 | format!("{directory}{user_filename}") 198 | } else { 199 | let network = &prefs.network; 200 | format!("{directory}{network}-{version}.call") 201 | }; 202 | 203 | make_version_directory(directory.as_str()); 204 | 205 | UpgradeDetails { relay, networks, directory, output_file, additional } 206 | } 207 | 208 | // Create a directory into which to place runtime blobs and the final call data. 209 | fn make_version_directory(dir_name: &str) { 210 | if !Path::new(dir_name).is_dir() { 211 | fs::create_dir_all(dir_name).expect("it makes a dir"); 212 | } 213 | } 214 | 215 | // Convert a semver version (e.g. "1.2.3") to an integer runtime version (e.g. 1002003). 216 | fn semver_to_intver(semver: &str) -> String { 217 | // M.m.p => M_mmm_ppp 218 | let points = 219 | semver.bytes().enumerate().filter(|(_, b)| *b == b'.').map(|(i, _)| i).collect::>(); 220 | 221 | assert!(points.len() == 2, "not semver"); 222 | 223 | let major = &semver[..points[0]]; 224 | let minor = &semver[points[0] + 1..points[1]]; 225 | let patch = &semver[points[1] + 1..]; 226 | 227 | format!("{major}{minor:0>3}{patch:0>3}") 228 | } 229 | 230 | // Fetch all the runtime Wasm blobs from a Fellowship release. 231 | async fn download_runtimes(upgrade_details: &UpgradeDetails) { 232 | // Relay Form 233 | // https://github.com/polkadot-fellows/runtimes/releases/download/v1.0.0/polkadot_runtime-v1000000.compact.compressed.wasm 234 | // 235 | // Parachains Form 236 | // https://github.com/polkadot-fellows/runtimes/releases/download/v1.0.0/asset_hub_kusama_runtime-v1000000.compact.compressed.wasm 237 | 238 | println!("\nDownloading runtimes.\n"); 239 | for chain in &upgrade_details.networks { 240 | let chain_name = match chain.network { 241 | Network::Kusama => "kusama", 242 | Network::Polkadot => "polkadot", 243 | Network::KusamaAssetHub => "asset-hub-kusama", 244 | Network::KusamaBridgeHub => "bridge-hub-kusama", 245 | Network::KusamaPeople => "people-kusama", 246 | Network::KusamaCoretime => "coretime-kusama", 247 | Network::KusamaEncointer => "encointer-kusama", 248 | Network::PolkadotAssetHub => "asset-hub-polkadot", 249 | Network::PolkadotCollectives => "collectives-polkadot", 250 | Network::PolkadotBridgeHub => "bridge-hub-polkadot", 251 | Network::PolkadotPeople => "people-polkadot", 252 | Network::PolkadotCoretime => "coretime-polkadot", 253 | }; 254 | let runtime_version = semver_to_intver(&chain.version); 255 | let fname = format!("{chain_name}_runtime-v{runtime_version}.compact.compressed.wasm"); 256 | 257 | let version = &chain.version; 258 | let download_url = format!( 259 | "https://github.com/polkadot-fellows/runtimes/releases/download/v{version}/{fname}" 260 | ); 261 | 262 | let download_url = download_url.as_str(); 263 | let directory = &upgrade_details.directory; 264 | let path_name = format!("{directory}{fname}"); 265 | println!("Downloading... {fname}"); 266 | let response = reqwest::get(download_url).await.expect("we need files to work"); 267 | let runtime = response.bytes().await.expect("need bytes"); 268 | // todo: we could actually just hash the file, mutate UpgradeDetails, and not write it. 269 | // saving it may be more convenient anyway though, since someone needs to upload it after 270 | // the referendum enacts. 271 | fs::write(path_name, runtime).expect("we can write"); 272 | } 273 | } 274 | 275 | // Generate the `authorize_upgrade` calls that will need to execute on each parachain. 276 | fn generate_authorize_upgrade_calls(upgrade_details: &UpgradeDetails) -> Vec { 277 | println!("\nGenerating parachain authorization calls. The runtime hashes are logged if you would like to verify them with srtool.\n"); 278 | let mut authorization_calls = Vec::new(); 279 | for chain in &upgrade_details.networks { 280 | let runtime_version = semver_to_intver(&chain.version); 281 | match chain.network { 282 | Network::Kusama => { 283 | use kusama_relay::runtime_types::frame_system::pallet::Call as SystemCall; 284 | let path = format!( 285 | "{}kusama_runtime-v{}.compact.compressed.wasm", 286 | upgrade_details.directory, runtime_version 287 | ); 288 | let runtime = fs::read(path).expect("Should give a valid file path"); 289 | let runtime_hash = blake2_256(&runtime); 290 | println!("Kusama Relay Chain Runtime Hash: 0x{}", hex::encode(runtime_hash)); 291 | 292 | let call = CallInfo::from_runtime_call(NetworkRuntimeCall::Kusama( 293 | KusamaRuntimeCall::System(SystemCall::authorize_upgrade { 294 | code_hash: H256(runtime_hash), 295 | }), 296 | )); 297 | authorization_calls.push(call); 298 | }, 299 | Network::KusamaAssetHub => { 300 | use kusama_asset_hub::runtime_types::frame_system::pallet::Call; 301 | let path = format!( 302 | "{}asset-hub-kusama_runtime-v{}.compact.compressed.wasm", 303 | upgrade_details.directory, runtime_version 304 | ); 305 | let runtime = fs::read(path).expect("Should give a valid file path"); 306 | let runtime_hash = blake2_256(&runtime); 307 | println!("Kusama Asset Hub Runtime Hash: 0x{}", hex::encode(runtime_hash)); 308 | 309 | let call = CallInfo::from_runtime_call(NetworkRuntimeCall::KusamaAssetHub( 310 | KusamaAssetHubRuntimeCall::System(Call::authorize_upgrade { 311 | code_hash: H256(runtime_hash), 312 | }), 313 | )); 314 | authorization_calls.push(call); 315 | }, 316 | Network::KusamaBridgeHub => { 317 | use kusama_bridge_hub::runtime_types::frame_system::pallet::Call; 318 | let path = format!( 319 | "{}bridge-hub-kusama_runtime-v{}.compact.compressed.wasm", 320 | upgrade_details.directory, runtime_version 321 | ); 322 | let runtime = fs::read(path).expect("Should give a valid file path"); 323 | let runtime_hash = blake2_256(&runtime); 324 | println!("Kusama Bridge Hub Runtime Hash: 0x{}", hex::encode(runtime_hash)); 325 | 326 | let call = CallInfo::from_runtime_call(NetworkRuntimeCall::KusamaBridgeHub( 327 | KusamaBridgeHubRuntimeCall::System(Call::authorize_upgrade { 328 | code_hash: H256(runtime_hash), 329 | }), 330 | )); 331 | authorization_calls.push(call); 332 | }, 333 | Network::KusamaPeople => { 334 | use kusama_people::runtime_types::frame_system::pallet::Call; 335 | let path = format!( 336 | "{}people-kusama_runtime-v{}.compact.compressed.wasm", 337 | upgrade_details.directory, runtime_version 338 | ); 339 | let runtime = fs::read(path).expect("Should give a valid file path"); 340 | let runtime_hash = blake2_256(&runtime); 341 | println!("Kusama People Runtime Hash: 0x{}", hex::encode(runtime_hash)); 342 | 343 | let call = CallInfo::from_runtime_call(NetworkRuntimeCall::KusamaPeople( 344 | KusamaPeopleRuntimeCall::System(Call::authorize_upgrade { 345 | code_hash: H256(runtime_hash), 346 | }), 347 | )); 348 | authorization_calls.push(call); 349 | }, 350 | Network::KusamaCoretime => { 351 | use kusama_coretime::runtime_types::frame_system::pallet::Call; 352 | let path = format!( 353 | "{}coretime-kusama_runtime-v{}.compact.compressed.wasm", 354 | upgrade_details.directory, runtime_version 355 | ); 356 | let runtime = fs::read(path).expect("Should give a valid file path"); 357 | let runtime_hash = blake2_256(&runtime); 358 | println!("Kusama Coretime Runtime Hash: 0x{}", hex::encode(runtime_hash)); 359 | 360 | let call = CallInfo::from_runtime_call(NetworkRuntimeCall::KusamaCoretime( 361 | KusamaCoretimeRuntimeCall::System(Call::authorize_upgrade { 362 | code_hash: H256(runtime_hash), 363 | }), 364 | )); 365 | authorization_calls.push(call); 366 | }, 367 | Network::KusamaEncointer => { 368 | use kusama_encointer::runtime_types::frame_system::pallet::Call; 369 | let path = format!( 370 | "{}encointer-kusama_runtime-v{}.compact.compressed.wasm", 371 | upgrade_details.directory, runtime_version 372 | ); 373 | let runtime = fs::read(path).expect("Should give a valid file path"); 374 | let runtime_hash = blake2_256(&runtime); 375 | println!("Kusama Encointer Runtime Hash: 0x{}", hex::encode(runtime_hash)); 376 | 377 | let call = CallInfo::from_runtime_call(NetworkRuntimeCall::KusamaEncointer( 378 | KusamaEncointerRuntimeCall::System(Call::authorize_upgrade { 379 | code_hash: H256(runtime_hash), 380 | }), 381 | )); 382 | authorization_calls.push(call); 383 | }, 384 | Network::Polkadot => { 385 | use polkadot_relay::runtime_types::frame_system::pallet::Call; 386 | let path = format!( 387 | "{}polkadot_runtime-v{}.compact.compressed.wasm", 388 | upgrade_details.directory, runtime_version 389 | ); 390 | let runtime = fs::read(path).expect("Should give a valid file path"); 391 | let runtime_hash = blake2_256(&runtime); 392 | println!("Polkadot Relay Chain Runtime Hash: 0x{}", hex::encode(runtime_hash)); 393 | 394 | let call = CallInfo::from_runtime_call(NetworkRuntimeCall::Polkadot( 395 | PolkadotRuntimeCall::System(Call::authorize_upgrade { 396 | code_hash: H256(runtime_hash), 397 | }), 398 | )); 399 | authorization_calls.push(call); 400 | }, 401 | Network::PolkadotAssetHub => { 402 | use polkadot_asset_hub::runtime_types::frame_system::pallet::Call; 403 | let path = format!( 404 | "{}asset-hub-polkadot_runtime-v{}.compact.compressed.wasm", 405 | upgrade_details.directory, runtime_version 406 | ); 407 | let runtime = fs::read(path).expect("Should give a valid file path"); 408 | let runtime_hash = blake2_256(&runtime); 409 | println!("Polkadot Asset Hub Runtime Hash: 0x{}", hex::encode(runtime_hash)); 410 | 411 | let call = CallInfo::from_runtime_call(NetworkRuntimeCall::PolkadotAssetHub( 412 | PolkadotAssetHubRuntimeCall::System(Call::authorize_upgrade { 413 | code_hash: H256(runtime_hash), 414 | }), 415 | )); 416 | authorization_calls.push(call); 417 | }, 418 | Network::PolkadotCollectives => { 419 | use polkadot_collectives::runtime_types::frame_system::pallet::Call; 420 | let path = format!( 421 | "{}collectives-polkadot_runtime-v{}.compact.compressed.wasm", 422 | upgrade_details.directory, runtime_version 423 | ); 424 | let runtime = fs::read(path).expect("Should give a valid file path"); 425 | let runtime_hash = blake2_256(&runtime); 426 | println!("Polkadot Collectives Runtime Hash: 0x{}", hex::encode(runtime_hash)); 427 | 428 | let call = CallInfo::from_runtime_call(NetworkRuntimeCall::PolkadotCollectives( 429 | CollectivesRuntimeCall::System(Call::authorize_upgrade { 430 | code_hash: H256(runtime_hash), 431 | }), 432 | )); 433 | authorization_calls.push(call); 434 | }, 435 | Network::PolkadotBridgeHub => { 436 | use polkadot_bridge_hub::runtime_types::frame_system::pallet::Call; 437 | let path = format!( 438 | "{}bridge-hub-polkadot_runtime-v{}.compact.compressed.wasm", 439 | upgrade_details.directory, runtime_version 440 | ); 441 | let runtime = fs::read(path).expect("Should give a valid file path"); 442 | let runtime_hash = blake2_256(&runtime); 443 | println!("Polkadot Bridge Hub Runtime Hash: 0x{}", hex::encode(runtime_hash)); 444 | 445 | let call = CallInfo::from_runtime_call(NetworkRuntimeCall::PolkadotBridgeHub( 446 | PolkadotBridgeHubRuntimeCall::System(Call::authorize_upgrade { 447 | code_hash: H256(runtime_hash), 448 | }), 449 | )); 450 | authorization_calls.push(call); 451 | }, 452 | Network::PolkadotPeople => { 453 | use polkadot_people::runtime_types::frame_system::pallet::Call; 454 | let path = format!( 455 | "{}people-polkadot_runtime-v{}.compact.compressed.wasm", 456 | upgrade_details.directory, runtime_version 457 | ); 458 | let runtime = fs::read(path).expect("Should give a valid file path"); 459 | let runtime_hash = blake2_256(&runtime); 460 | println!("Polkadot People Runtime Hash: 0x{}", hex::encode(runtime_hash)); 461 | 462 | let call = CallInfo::from_runtime_call(NetworkRuntimeCall::PolkadotPeople( 463 | PolkadotPeopleRuntimeCall::System(Call::authorize_upgrade { 464 | code_hash: H256(runtime_hash), 465 | }), 466 | )); 467 | authorization_calls.push(call); 468 | }, 469 | Network::PolkadotCoretime => { 470 | use polkadot_coretime::runtime_types::frame_system::pallet::Call; 471 | let path = format!( 472 | "{}coretime-polkadot_runtime-v{}.compact.compressed.wasm", 473 | upgrade_details.directory, runtime_version 474 | ); 475 | let runtime = fs::read(path).expect("Should give a valid file path"); 476 | let runtime_hash = blake2_256(&runtime); 477 | println!("Polkadot Coretime Runtime Hash: 0x{}", hex::encode(runtime_hash)); 478 | 479 | let call = CallInfo::from_runtime_call(NetworkRuntimeCall::PolkadotCoretime( 480 | PolkadotCoretimeRuntimeCall::System(Call::authorize_upgrade { 481 | code_hash: H256(runtime_hash), 482 | }), 483 | )); 484 | authorization_calls.push(call); 485 | }, 486 | }; 487 | } 488 | authorization_calls 489 | } 490 | 491 | // Take the parachain authorization calls and the Relay Chain call, and batch them into one call 492 | // that can be executed on the Relay Chain. The call returned here is the proposal to put to 493 | // referendum. 494 | async fn construct_batch(upgrade_details: &UpgradeDetails, calls: Vec) -> CallInfo { 495 | println!("\nBatching calls."); 496 | match upgrade_details.relay { 497 | Network::Kusama => construct_kusama_batch(calls, upgrade_details.additional.clone()).await, 498 | Network::Polkadot => 499 | construct_polkadot_batch(calls, upgrade_details.additional.clone()).await, 500 | _ => panic!("Not a Relay Chain"), 501 | } 502 | } 503 | 504 | // Construct the batch needed on Kusama. 505 | async fn construct_kusama_batch(calls: Vec, additional: Option) -> CallInfo { 506 | use kusama_asset_hub::runtime_types::pallet_utility::pallet::Call as UtilityCall; 507 | 508 | let mut batch_calls = Vec::new(); 509 | for auth in calls { 510 | dbg!(&auth.network); 511 | if matches!(auth.network, Network::KusamaAssetHub) { 512 | batch_calls.push(auth.get_kusama_asset_hub_call().expect("We just constructed this")); 513 | } else { 514 | let send_auth = send_as_superuser_kusama(&auth).await; 515 | batch_calls.push(send_auth); 516 | } 517 | } 518 | if let Some(a) = additional { 519 | batch_calls.push(a.get_kusama_asset_hub_call().expect("kusama call")) 520 | } 521 | match &batch_calls.len() { 522 | 0 => panic!("no calls"), 523 | 1 => 524 | CallInfo::from_runtime_call(NetworkRuntimeCall::KusamaAssetHub(batch_calls[0].clone())), 525 | _ => CallInfo::from_runtime_call(NetworkRuntimeCall::KusamaAssetHub( 526 | KusamaAssetHubRuntimeCall::Utility(UtilityCall::force_batch { calls: batch_calls }), 527 | )), 528 | } 529 | } 530 | 531 | // Construct the batch needed on Polkadot. 532 | async fn construct_polkadot_batch(calls: Vec, additional: Option) -> CallInfo { 533 | use polkadot_asset_hub::runtime_types::pallet_utility::pallet::Call as UtilityCall; 534 | 535 | let mut batch_calls = Vec::new(); 536 | for auth in calls { 537 | if matches!(auth.network, Network::PolkadotAssetHub) { 538 | batch_calls.push(auth.get_polkadot_asset_hub_call().expect("We just constructed this")); 539 | } else { 540 | let send_auth = send_as_superuser_polkadot(&auth).await; 541 | batch_calls.push(send_auth); 542 | } 543 | } 544 | if let Some(a) = additional { 545 | batch_calls.push(a.get_polkadot_asset_hub_call().expect("polkadot call")) 546 | } 547 | match &batch_calls.len() { 548 | 0 => panic!("no calls"), 549 | 1 => CallInfo::from_runtime_call(NetworkRuntimeCall::PolkadotAssetHub( 550 | batch_calls[0].clone(), 551 | )), 552 | _ => CallInfo::from_runtime_call(NetworkRuntimeCall::PolkadotAssetHub( 553 | PolkadotAssetHubRuntimeCall::Utility(UtilityCall::force_batch { calls: batch_calls }), 554 | )), 555 | } 556 | } 557 | 558 | // Take a call, which includes its intended destination, and wrap it in XCM instructions to `send` 559 | // it from Kusama Asset Hub, with `Root` origin, and have it execute on its destination. 560 | async fn send_as_superuser_kusama(auth: &CallInfo) -> KusamaAssetHubRuntimeCall { 561 | use kusama_asset_hub::runtime_types::{ 562 | pallet_xcm::pallet::Call as XcmCall, 563 | staging_xcm::v5::{ 564 | junction::Junction::Parachain, junctions::Junctions::Here, junctions::Junctions::X1, 565 | location::Location, Instruction, Xcm, 566 | }, 567 | xcm::{ 568 | double_encoded::DoubleEncoded, v3::OriginKind, v3::WeightLimit, VersionedLocation, 569 | VersionedXcm::V5, 570 | }, 571 | }; 572 | 573 | let location = match auth.network.get_para_id() { 574 | Ok(para_id) => Location { parents: 1, interior: X1([Parachain(para_id)]) }, 575 | Err(_) => Location { parents: 1, interior: Here }, 576 | }; 577 | 578 | KusamaAssetHubRuntimeCall::PolkadotXcm(XcmCall::send { 579 | dest: Box::new(VersionedLocation::V5(location)), 580 | message: Box::new(V5(Xcm(vec![ 581 | Instruction::UnpaidExecution { 582 | weight_limit: WeightLimit::Unlimited, 583 | check_origin: None, 584 | }, 585 | Instruction::Transact { 586 | origin_kind: OriginKind::Superuser, 587 | fallback_max_weight: None, 588 | call: DoubleEncoded { encoded: auth.encoded.clone() }, 589 | }, 590 | ]))), 591 | }) 592 | } 593 | 594 | // Take a call, which includes its intended destination, and wrap it in XCM instructions to `send` 595 | // it from the Polkadot Relay Chain, with `Root` origin, and have it execute on its destination. 596 | async fn send_as_superuser_polkadot(auth: &CallInfo) -> PolkadotAssetHubRuntimeCall { 597 | use polkadot_asset_hub::runtime_types::{ 598 | pallet_xcm::pallet::Call as XcmCall, 599 | staging_xcm::v5::{ 600 | junction::Junction::Parachain, junctions::Junctions::Here, junctions::Junctions::X1, 601 | location::Location, Instruction, Xcm, 602 | }, 603 | xcm::{ 604 | double_encoded::DoubleEncoded, v3::OriginKind, v3::WeightLimit, VersionedLocation, 605 | VersionedXcm::V5, 606 | }, 607 | }; 608 | 609 | let location = match auth.network.get_para_id() { 610 | Ok(para_id) => Location { parents: 1, interior: X1([Parachain(para_id)]) }, 611 | Err(_) => Location { parents: 1, interior: Here }, 612 | }; 613 | 614 | PolkadotAssetHubRuntimeCall::PolkadotXcm(XcmCall::send { 615 | dest: Box::new(VersionedLocation::V5(location)), 616 | message: Box::new(V5(Xcm(vec![ 617 | Instruction::UnpaidExecution { 618 | weight_limit: WeightLimit::Unlimited, 619 | check_origin: None, 620 | }, 621 | Instruction::Transact { 622 | origin_kind: OriginKind::Superuser, 623 | fallback_max_weight: None, 624 | call: DoubleEncoded { encoded: auth.encoded.clone() }, 625 | }, 626 | ]))), 627 | }) 628 | } 629 | 630 | // Write the call needed to disk and provide instructions to the user about how to propose it. 631 | fn write_batch(upgrade_details: &UpgradeDetails, batch: CallInfo) { 632 | let fname = upgrade_details.output_file.as_str(); 633 | let mut info_to_write = "0x".to_owned(); 634 | info_to_write.push_str(hex::encode(batch.encoded).as_str()); 635 | fs::write(fname, info_to_write).expect("it should write"); 636 | 637 | println!("\nSuccess! The call data was written to {fname}"); 638 | println!("To submit this as a referendum in OpenGov, run:"); 639 | let network = match upgrade_details.relay { 640 | Network::Kusama => "kusama", 641 | Network::Polkadot => "polkadot", 642 | _ => panic!("not a relay network"), 643 | }; 644 | println!("\nopengov-cli submit-referendum \\"); 645 | println!(" --proposal \"{fname}\" \\"); 646 | println!(" --network \"{network}\" --track <\"root\" or \"whitelistedcaller\">"); 647 | } 648 | -------------------------------------------------------------------------------- /src/submit_referendum.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | use clap::Parser as ClapParser; 3 | use std::fs; 4 | 5 | /// Generate all the calls needed to submit a proposal as a referendum in OpenGov. 6 | #[derive(Debug, ClapParser)] 7 | pub(crate) struct ReferendumArgs { 8 | /// The encoded proposal that we want to submit. This can either be the call data itself, 9 | /// e.g. "0x0102...", or a file path that contains the data, e.g. "./my_proposal.call". 10 | #[clap(long = "proposal", short)] 11 | proposal: String, 12 | 13 | /// Network on which to submit the referendum. `polkadot` or `kusama`. 14 | #[clap(long = "network", short)] 15 | network: String, 16 | 17 | /// Track on which to submit the referendum. 18 | #[clap(long = "track", short)] 19 | track: String, 20 | 21 | /// Optional: Enact at a particular block number. 22 | #[clap(long = "at")] 23 | at: Option, 24 | 25 | /// Optional: Enact after a given number of blocks. 26 | #[clap(long = "after")] 27 | after: Option, 28 | 29 | /// Output length limit. Defaults to 1,000. 30 | #[clap(long = "output-len-limit")] 31 | output_len_limit: Option, 32 | 33 | /// Do not print batch calls. Defaults to false. 34 | #[clap(long = "no-batch")] 35 | no_batch: bool, 36 | 37 | /// Form of output. `AppsUiLink` or `CallData`. Defaults to Apps UI. 38 | #[clap(long = "output")] 39 | output: Option, 40 | 41 | /// Use light client endpoints instead of RPC for PAPI links. 42 | #[clap(long = "light-client")] 43 | light_client: bool, 44 | } 45 | 46 | // The sub-command's "main" function. 47 | pub(crate) async fn submit_referendum(prefs: ReferendumArgs) { 48 | // Find out what the user wants to do. 49 | let proposal_details = parse_inputs(prefs); 50 | // Generate the calls necessary. 51 | let calls = generate_calls(&proposal_details).await; 52 | // Tell the user what to do. 53 | deliver_output(proposal_details, calls); 54 | } 55 | 56 | // Parse the CLI inputs and return a typed struct with all the details needed. 57 | fn parse_inputs(prefs: ReferendumArgs) -> ProposalDetails { 58 | use DispatchTimeWrapper::*; 59 | use NetworkTrack::*; 60 | use Output::*; 61 | 62 | let proposal = prefs.proposal; 63 | 64 | let track = match prefs.network.to_ascii_lowercase().as_str() { 65 | "polkadot" => match prefs.track.to_ascii_lowercase().as_str() { 66 | "root" => PolkadotRoot, 67 | "whitelisted-caller" | "whitelistedcaller" => 68 | Polkadot(PolkadotAssetHubOpenGovOrigin::WhitelistedCaller), 69 | "staking-admin" | "stakingadmin" => Polkadot(PolkadotAssetHubOpenGovOrigin::StakingAdmin), 70 | "treasurer" => Polkadot(PolkadotAssetHubOpenGovOrigin::Treasurer), 71 | "lease-admin" | "leaseadmin" => Polkadot(PolkadotAssetHubOpenGovOrigin::LeaseAdmin), 72 | "fellowship-admin" | "fellowshipadmin" => 73 | Polkadot(PolkadotAssetHubOpenGovOrigin::FellowshipAdmin), 74 | "general-admin" | "generaladmin" => Polkadot(PolkadotAssetHubOpenGovOrigin::GeneralAdmin), 75 | "auction-admin" | "auctionadmin" => Polkadot(PolkadotAssetHubOpenGovOrigin::AuctionAdmin), 76 | "referendum-killer" | "referendumkiller" => 77 | Polkadot(PolkadotAssetHubOpenGovOrigin::ReferendumKiller), 78 | "referendum-canceller" | "referendumcanceller" => 79 | Polkadot(PolkadotAssetHubOpenGovOrigin::ReferendumCanceller), 80 | _ => panic!("Unsupported track! Tracks should be in the form `general-admin` or `generaladmin`."), 81 | }, 82 | "kusama" => match prefs.track.to_ascii_lowercase().as_str() { 83 | "root" => KusamaRoot, 84 | "whitelisted-caller" | "whitelistedcaller" => 85 | Kusama(KusamaAssetHubOpenGovOrigin::WhitelistedCaller), 86 | "staking-admin" | "stakingadmin" => Kusama(KusamaAssetHubOpenGovOrigin::StakingAdmin), 87 | "treasurer" => Kusama(KusamaAssetHubOpenGovOrigin::Treasurer), 88 | "lease-admin" | "leaseadmin" => Kusama(KusamaAssetHubOpenGovOrigin::LeaseAdmin), 89 | "fellowship-admin" | "fellowshipadmin" => Kusama(KusamaAssetHubOpenGovOrigin::FellowshipAdmin), 90 | "general-admin" | "generaladmin" => Kusama(KusamaAssetHubOpenGovOrigin::GeneralAdmin), 91 | "auction-admin" | "auctionadmin" => Kusama(KusamaAssetHubOpenGovOrigin::AuctionAdmin), 92 | "referendum-killer" | "referendumkiller" => 93 | Kusama(KusamaAssetHubOpenGovOrigin::ReferendumKiller), 94 | "referendum-canceller" | "referendumcanceller" => 95 | Kusama(KusamaAssetHubOpenGovOrigin::ReferendumCanceller), 96 | _ => panic!("Unsupported track! Tracks should be in the form `general-admin` or `generaladmin`."), 97 | }, 98 | _ => panic!("`network` must be `polkadot` or `kusama`"), 99 | }; 100 | 101 | let dispatch = match (prefs.at, prefs.after) { 102 | (None, None) => { 103 | println!("\nNo enactment time specified. Defaulting to `After(10)`."); 104 | println!("Specify an enactment time with `--at ` or `--after `.\n"); 105 | After(10) 106 | }, 107 | (Some(_), Some(_)) => { 108 | panic!("\nBoth `At` and `After` dispatch times provided. You can only use one.\n"); 109 | }, 110 | (Some(at), None) => At(at), 111 | (None, Some(after)) => After(after), 112 | }; 113 | 114 | let output_len_limit = prefs.output_len_limit.unwrap_or(1_000); 115 | 116 | let print_batch = !prefs.no_batch; 117 | 118 | let output = if let Some(input) = prefs.output { 119 | match input.to_ascii_lowercase().as_str() { 120 | "calldata" | "call-data" => CallData, 121 | "appsuilink" | "apps-ui-link" => AppsUiLink, 122 | _ => panic!("`output` must be `calldata` or `appsuilink`. If not specified, the default is `appsuilink`."), 123 | } 124 | } else { 125 | AppsUiLink 126 | }; 127 | 128 | let use_light_client = prefs.light_client; 129 | 130 | ProposalDetails { 131 | proposal, 132 | track, 133 | dispatch, 134 | output, 135 | output_len_limit, 136 | print_batch, 137 | use_light_client, 138 | } 139 | } 140 | 141 | // Generate all the calls needed. 142 | pub(crate) async fn generate_calls(proposal_details: &ProposalDetails) -> PossibleCallsToSubmit { 143 | match &proposal_details.track { 144 | // Kusama Root Origin. Since the Root origin is not part of `OpenGovOrigin`, we match it 145 | // specially. 146 | NetworkTrack::KusamaRoot => { 147 | use kusama_asset_hub::runtime_types::frame_support::dispatch::RawOrigin; 148 | kusama_non_fellowship_referenda( 149 | proposal_details, 150 | KusamaAssetHubOriginCaller::system(RawOrigin::Root), 151 | ) 152 | }, 153 | 154 | // All special Kusama origins. 155 | NetworkTrack::Kusama(kusama_track) => { 156 | match kusama_track { 157 | // Whitelisted calls are special. 158 | KusamaAssetHubOpenGovOrigin::WhitelistedCaller => 159 | kusama_fellowship_referenda(proposal_details).await, 160 | 161 | // All other Kusama origins. 162 | _ => kusama_non_fellowship_referenda( 163 | proposal_details, 164 | KusamaAssetHubOriginCaller::Origins(kusama_track.clone()), 165 | ), 166 | } 167 | }, 168 | 169 | // Same for Polkadot Root origin. It is not part of OpenGovOrigins, so it gets its own arm. 170 | NetworkTrack::PolkadotRoot => { 171 | use polkadot_asset_hub::runtime_types::frame_support::dispatch::RawOrigin; 172 | polkadot_non_fellowship_referenda( 173 | proposal_details, 174 | PolkadotAssetHubOriginCaller::system(RawOrigin::Root), 175 | ) 176 | }, 177 | 178 | // All special Polkadot origins. 179 | NetworkTrack::Polkadot(polkadot_track) => { 180 | match polkadot_track { 181 | PolkadotAssetHubOpenGovOrigin::WhitelistedCaller => 182 | polkadot_fellowship_referenda(proposal_details).await, 183 | 184 | // All other Polkadot origins. 185 | _ => polkadot_non_fellowship_referenda( 186 | proposal_details, 187 | PolkadotAssetHubOriginCaller::Origins(polkadot_track.clone()), 188 | ), 189 | } 190 | }, 191 | } 192 | } 193 | 194 | // Generate the calls needed for a proposal to pass through the Fellowship. 195 | async fn kusama_fellowship_referenda(proposal_details: &ProposalDetails) -> PossibleCallsToSubmit { 196 | use kusama_asset_hub::runtime_types::{ 197 | frame_support::traits::{preimages::Bounded::Lookup, schedule::DispatchTime}, 198 | pallet_preimage::pallet::Call as PreimageCall, 199 | pallet_referenda::pallet::Call as ReferendaCall, 200 | pallet_whitelist::pallet::Call as WhitelistCall, 201 | }; 202 | use kusama_relay::runtime_types::bounded_collections::bounded_vec::BoundedVec; 203 | use kusama_relay::runtime_types::{ 204 | frame_support::traits::{ 205 | preimages::Bounded::Inline, schedule::DispatchTime as KusamaDispatchTime, 206 | }, 207 | // Since the Relay Chain and Asset Hub may be on different versions of Preimage, 208 | // Referenda, and XCM pallets, we need to define their `Call` enum separately. 209 | pallet_referenda::pallet::Call as KusamaReferendaCall, 210 | pallet_xcm::pallet::Call as KusamaXcmCall, 211 | staging_xcm::v5::{ 212 | junction::Junction::Parachain, junctions::Junctions::X1, location::Location, 213 | Instruction, Xcm, 214 | }, 215 | xcm::{ 216 | double_encoded::DoubleEncoded, v3::OriginKind, v3::WeightLimit, VersionedLocation, 217 | VersionedXcm::V5, 218 | }, 219 | }; 220 | // Fellowship is on the Relay Chain, so things are a bit different here. 221 | // 222 | // 1. Create a whitelist call on Asset Hub: 223 | // 224 | // let whitelist_call = 225 | // KusamaAssetHubRuntimeCall::Whitelist(WhitelistCall::whitelist_call { 226 | // call_hash: H256(proposal_hash), 227 | // }); 228 | // 229 | // 2. Create an XCM to send this from the Relay Chain: 230 | // let send_whitelist = KusamaRuntimeCall::XcmPallet( 231 | // PolkadotXcmCall::send { 232 | // dest: Location { parents: 0, interior: X1([Parachain(1000)]) }, 233 | // message: vec![UnpaidExecution, Transact {call: whitelist_call, ..}], 234 | // } 235 | // ); 236 | // 237 | // 3. Make a Fellowship referendum for `send_whitelist`. 238 | // 239 | // 4. Make a public referendum on Asset Hub. 240 | let proposal_bytes = get_proposal_bytes(proposal_details.proposal.clone()); 241 | let proposal_call_info = CallInfo::from_bytes(&proposal_bytes, Network::KusamaAssetHub); 242 | 243 | let public_referendum_dispatch_time = match proposal_details.dispatch { 244 | DispatchTimeWrapper::At(block) => DispatchTime::At(block), 245 | DispatchTimeWrapper::After(block) => DispatchTime::After(block), 246 | }; 247 | 248 | let whitelist_call = CallInfo::from_runtime_call(NetworkRuntimeCall::KusamaAssetHub( 249 | KusamaAssetHubRuntimeCall::Whitelist(WhitelistCall::whitelist_call { 250 | call_hash: H256(proposal_call_info.hash), 251 | }), 252 | )); 253 | 254 | // This is what the Fellowship will actually vote on enacting. 255 | let whitelist_over_xcm = CallInfo::from_runtime_call(NetworkRuntimeCall::Kusama( 256 | KusamaRuntimeCall::XcmPallet(KusamaXcmCall::send { 257 | dest: Box::new(VersionedLocation::V5(Location { 258 | parents: 0, 259 | interior: X1([Parachain(1000)]), 260 | })), 261 | message: Box::new(V5(Xcm(vec![ 262 | Instruction::UnpaidExecution { 263 | weight_limit: WeightLimit::Unlimited, 264 | check_origin: None, 265 | }, 266 | Instruction::Transact { 267 | origin_kind: OriginKind::Xcm, 268 | fallback_max_weight: None, 269 | call: DoubleEncoded { encoded: whitelist_call.encoded }, 270 | }, 271 | ]))), 272 | }), 273 | )); 274 | 275 | // The Inline limit is 128 bytes. 276 | assert!( 277 | whitelist_over_xcm.length <= 128, 278 | "Fellowship proposal exceeds Inline limit of 128 bytes ({} bytes). There is no longer a preimage pallet on Kusama Relay Chain, try again as a root ref.", 279 | whitelist_over_xcm.length 280 | ); 281 | 282 | // The actual Fellowship referendum submission. 283 | let fellowship_proposal = CallInfo::from_runtime_call(NetworkRuntimeCall::Kusama( 284 | KusamaRuntimeCall::FellowshipReferenda(KusamaReferendaCall::submit { 285 | proposal_origin: Box::new(KusamaOriginCaller::Origins(KusamaOpenGovOrigin::Fellows)), 286 | proposal: Inline(BoundedVec(whitelist_over_xcm.encoded)), 287 | enactment_moment: KusamaDispatchTime::After(10u32), 288 | }), 289 | )); 290 | 291 | // Now we put together the public referendum part. This still needs separate logic because the 292 | // actual proposal gets wrapped in a Whitelist call. 293 | let dispatch_whitelisted_call = CallInfo::from_runtime_call( 294 | NetworkRuntimeCall::KusamaAssetHub(KusamaAssetHubRuntimeCall::Whitelist( 295 | WhitelistCall::dispatch_whitelisted_call_with_preimage { 296 | call: Box::new( 297 | proposal_call_info 298 | .get_kusama_asset_hub_call() 299 | .expect("it is a kusama asset hub call"), 300 | ), 301 | }, 302 | )), 303 | ); 304 | 305 | let preimage_for_dispatch_whitelisted_call = CallInfo::from_runtime_call( 306 | NetworkRuntimeCall::KusamaAssetHub(KusamaAssetHubRuntimeCall::Preimage( 307 | PreimageCall::note_preimage { bytes: dispatch_whitelisted_call.encoded.clone() }, 308 | )), 309 | ); 310 | let public_proposal = CallInfo::from_runtime_call(NetworkRuntimeCall::KusamaAssetHub( 311 | KusamaAssetHubRuntimeCall::Referenda(ReferendaCall::submit { 312 | proposal_origin: Box::new(KusamaAssetHubOriginCaller::Origins( 313 | KusamaAssetHubOpenGovOrigin::WhitelistedCaller, 314 | )), 315 | proposal: Lookup { 316 | hash: H256(dispatch_whitelisted_call.hash), 317 | len: dispatch_whitelisted_call.length, 318 | }, 319 | enactment_moment: public_referendum_dispatch_time, 320 | }), 321 | )); 322 | 323 | // Check the lengths and prepare preimages for printing. 324 | let (dispatch_preimage_print, dispatch_preimage_print_len) = 325 | preimage_for_dispatch_whitelisted_call 326 | .create_print_output(proposal_details.output_len_limit); 327 | 328 | // If it's a hash, let's write the data to a file you can upload. 329 | match dispatch_preimage_print { 330 | CallOrHash::Call(_) => (), 331 | CallOrHash::Hash(_) => { 332 | let mut info_to_write = "0x".to_owned(); 333 | info_to_write.push_str(hex::encode(dispatch_whitelisted_call.encoded).as_str()); 334 | fs::write("kusama_asset_hub_public_referendum_preimage_to_note.call", info_to_write) 335 | .expect("it should write"); 336 | }, 337 | } 338 | 339 | PossibleCallsToSubmit { 340 | preimage_for_whitelist_call: None, 341 | preimage_for_public_referendum: Some(( 342 | dispatch_preimage_print, 343 | dispatch_preimage_print_len, 344 | )), 345 | fellowship_referendum_submission: Some(NetworkRuntimeCall::Kusama( 346 | fellowship_proposal.get_kusama_call().expect("kusama relay"), 347 | )), 348 | public_referendum_submission: Some(NetworkRuntimeCall::KusamaAssetHub( 349 | public_proposal.get_kusama_asset_hub_call().expect("kusama asset hub"), 350 | )), 351 | } 352 | } 353 | 354 | // Generate the calls needed for a proposal to pass on Kusama without the Fellowship. 355 | fn kusama_non_fellowship_referenda( 356 | proposal_details: &ProposalDetails, 357 | origin: KusamaAssetHubOriginCaller, 358 | ) -> PossibleCallsToSubmit { 359 | use kusama_asset_hub::runtime_types::{ 360 | frame_support::traits::{preimages::Bounded::Lookup, schedule::DispatchTime}, 361 | pallet_preimage::pallet::Call as PreimageCall, 362 | pallet_referenda::pallet::Call as ReferendaCall, 363 | }; 364 | 365 | let proposal_bytes = get_proposal_bytes(proposal_details.proposal.clone()); 366 | let proposal_call_info = CallInfo::from_bytes(&proposal_bytes, Network::KusamaAssetHub); 367 | 368 | let public_referendum_dispatch_time = match proposal_details.dispatch { 369 | DispatchTimeWrapper::At(block) => DispatchTime::At(block), 370 | DispatchTimeWrapper::After(block) => DispatchTime::After(block), 371 | }; 372 | 373 | let note_proposal_preimage = CallInfo::from_runtime_call(NetworkRuntimeCall::KusamaAssetHub( 374 | KusamaAssetHubRuntimeCall::Preimage(PreimageCall::note_preimage { bytes: proposal_bytes }), 375 | )); 376 | let public_proposal = CallInfo::from_runtime_call(NetworkRuntimeCall::KusamaAssetHub( 377 | KusamaAssetHubRuntimeCall::Referenda(ReferendaCall::submit { 378 | proposal_origin: Box::new(origin), 379 | proposal: Lookup { 380 | hash: H256(proposal_call_info.hash), 381 | len: proposal_call_info.length, 382 | }, 383 | enactment_moment: public_referendum_dispatch_time, 384 | }), 385 | )); 386 | let (preimage_print, preimage_print_len) = 387 | note_proposal_preimage.create_print_output(proposal_details.output_len_limit); 388 | 389 | PossibleCallsToSubmit { 390 | preimage_for_whitelist_call: None, 391 | preimage_for_public_referendum: Some((preimage_print, preimage_print_len)), 392 | fellowship_referendum_submission: None, 393 | public_referendum_submission: Some(NetworkRuntimeCall::KusamaAssetHub( 394 | public_proposal.get_kusama_asset_hub_call().expect("kusama asset hub"), 395 | )), 396 | } 397 | } 398 | 399 | // Generate the calls needed for a proposal to pass through the Polkadot Fellowship. 400 | async fn polkadot_fellowship_referenda( 401 | proposal_details: &ProposalDetails, 402 | ) -> PossibleCallsToSubmit { 403 | use polkadot_asset_hub::runtime_types::{ 404 | frame_support::traits::{preimages::Bounded::Lookup, schedule::DispatchTime}, 405 | pallet_preimage::pallet::Call as PreimageCall, 406 | pallet_referenda::pallet::Call as ReferendaCall, 407 | pallet_whitelist::pallet::Call as WhitelistCall, 408 | }; 409 | use polkadot_collectives::runtime_types::{ 410 | bounded_collections::bounded_vec::BoundedVec as CollectivesBoundedVec, 411 | collectives_polkadot_runtime::OriginCaller as CollectivesOriginCaller, 412 | frame_support::traits::{ 413 | preimages::Bounded::{Inline as CollectivesInline, Lookup as CollectivesLookup}, 414 | schedule::DispatchTime as CollectivesDispatchTime, 415 | }, 416 | // Since Asset Hub and Collectives may be on different versions of Preimage, 417 | // Referenda, and XCM pallets, we need to define their `Call` enum separately. 418 | pallet_preimage::pallet::Call as CollectivesPreimageCall, 419 | pallet_referenda::pallet::Call as CollectivesReferendaCall, 420 | pallet_xcm::pallet::Call as CollectivesXcmCall, 421 | staging_xcm::v5::{ 422 | junction::Junction::Parachain, junctions::Junctions::X1, location::Location, 423 | Instruction, Xcm, 424 | }, 425 | xcm::{ 426 | double_encoded::DoubleEncoded, v3::OriginKind, v3::WeightLimit, VersionedLocation, 427 | VersionedXcm::V5, 428 | }, 429 | }; 430 | // Fellowship is on the Collectives chain, so things are a bit different here. 431 | // 432 | // 1. Create a whitelist call on Asset Hub: 433 | // 434 | // let whitelist_call = 435 | // PolkadotAssetHubRuntimeCall::Whitelist(WhitelistCall::whitelist_call { 436 | // call_hash: H256(proposal_hash), 437 | // }); 438 | // 439 | // 2. Create an XCM send call on the Collectives chain to Transact this on Asset Hub: 440 | // 441 | // let send_whitelist = CollectivesRuntimeCall::PolkadotXcm( 442 | // PolkadotXcmCall::send { 443 | // dest: Location { parents: 1, interior: X1([Parachain(1000)]) }, 444 | // message: vec![UnpaidExecution, Transact {call: whitelist_call, ..}], 445 | // } 446 | // ); 447 | // 448 | // 3. Make a Fellowship referendum for `send_whitelist`. 449 | // 450 | // 4. Make a public referendum on Asset Hub. 451 | let proposal_bytes = get_proposal_bytes(proposal_details.proposal.clone()); 452 | let proposal_call_info = CallInfo::from_bytes(&proposal_bytes, Network::PolkadotAssetHub); 453 | 454 | let public_referendum_dispatch_time = match proposal_details.dispatch { 455 | DispatchTimeWrapper::At(block) => DispatchTime::At(block), 456 | DispatchTimeWrapper::After(block) => DispatchTime::After(block), 457 | }; 458 | // Whitelist the call on the Relay Chain. 459 | let whitelist_call = CallInfo::from_runtime_call(NetworkRuntimeCall::PolkadotAssetHub( 460 | PolkadotAssetHubRuntimeCall::Whitelist(WhitelistCall::whitelist_call { 461 | call_hash: H256(proposal_call_info.hash), 462 | }), 463 | )); 464 | 465 | // This is what the Fellowship will actually vote on enacting. 466 | let whitelist_over_xcm = CallInfo::from_runtime_call(NetworkRuntimeCall::PolkadotCollectives( 467 | CollectivesRuntimeCall::PolkadotXcm(CollectivesXcmCall::send { 468 | dest: Box::new(VersionedLocation::V5(Location { 469 | parents: 1, 470 | interior: X1([Parachain(1000)]), 471 | })), 472 | message: Box::new(V5(Xcm(vec![ 473 | Instruction::UnpaidExecution { 474 | weight_limit: WeightLimit::Unlimited, 475 | check_origin: None, 476 | }, 477 | Instruction::Transact { 478 | origin_kind: OriginKind::Xcm, 479 | fallback_max_weight: None, 480 | call: DoubleEncoded { encoded: whitelist_call.encoded }, 481 | }, 482 | ]))), 483 | }), 484 | )); 485 | 486 | // The Inline limit is 128 bytes. Use Inline if within limit, otherwise fall back to Lookup. 487 | let (fellowship_proposal, preimage_for_whitelist_over_xcm) = if whitelist_over_xcm.length <= 128 488 | { 489 | let proposal = CallInfo::from_runtime_call(NetworkRuntimeCall::PolkadotCollectives( 490 | CollectivesRuntimeCall::FellowshipReferenda(CollectivesReferendaCall::submit { 491 | proposal_origin: Box::new(CollectivesOriginCaller::FellowshipOrigins( 492 | FellowshipOrigins::Fellows, 493 | )), 494 | proposal: CollectivesInline(CollectivesBoundedVec(whitelist_over_xcm.encoded)), 495 | enactment_moment: CollectivesDispatchTime::After(10u32), 496 | }), 497 | )); 498 | (proposal, None) 499 | } else { 500 | let preimage = CallInfo::from_runtime_call(NetworkRuntimeCall::PolkadotCollectives( 501 | CollectivesRuntimeCall::Preimage(CollectivesPreimageCall::note_preimage { 502 | bytes: whitelist_over_xcm.encoded.clone(), 503 | }), 504 | )); 505 | let proposal = CallInfo::from_runtime_call(NetworkRuntimeCall::PolkadotCollectives( 506 | CollectivesRuntimeCall::FellowshipReferenda(CollectivesReferendaCall::submit { 507 | proposal_origin: Box::new(CollectivesOriginCaller::FellowshipOrigins( 508 | FellowshipOrigins::Fellows, 509 | )), 510 | proposal: CollectivesLookup { 511 | hash: H256(whitelist_over_xcm.hash), 512 | len: whitelist_over_xcm.length, 513 | }, 514 | enactment_moment: CollectivesDispatchTime::After(10u32), 515 | }), 516 | )); 517 | (proposal, Some(preimage)) 518 | }; 519 | 520 | // Now we put together the public referendum part. This still needs separate logic because the 521 | // actual proposal gets wrapped in a Whitelist call. 522 | let dispatch_whitelisted_call = CallInfo::from_runtime_call( 523 | NetworkRuntimeCall::PolkadotAssetHub(PolkadotAssetHubRuntimeCall::Whitelist( 524 | WhitelistCall::dispatch_whitelisted_call_with_preimage { 525 | call: Box::new( 526 | proposal_call_info 527 | .get_polkadot_asset_hub_call() 528 | .expect("it is a polkadot asset hub call"), 529 | ), 530 | }, 531 | )), 532 | ); 533 | 534 | let preimage_for_dispatch_whitelisted_call = CallInfo::from_runtime_call( 535 | NetworkRuntimeCall::PolkadotAssetHub(PolkadotAssetHubRuntimeCall::Preimage( 536 | PreimageCall::note_preimage { bytes: dispatch_whitelisted_call.encoded.clone() }, 537 | )), 538 | ); 539 | let public_proposal = CallInfo::from_runtime_call(NetworkRuntimeCall::PolkadotAssetHub( 540 | PolkadotAssetHubRuntimeCall::Referenda(ReferendaCall::submit { 541 | proposal_origin: Box::new(PolkadotAssetHubOriginCaller::Origins( 542 | PolkadotAssetHubOpenGovOrigin::WhitelistedCaller, 543 | )), 544 | proposal: Lookup { 545 | hash: H256(dispatch_whitelisted_call.hash), 546 | len: dispatch_whitelisted_call.length, 547 | }, 548 | enactment_moment: public_referendum_dispatch_time, 549 | }), 550 | )); 551 | 552 | // Check the lengths and prepare preimages for printing. 553 | let whitelist_preimage_print = preimage_for_whitelist_over_xcm 554 | .map(|p| p.create_print_output(proposal_details.output_len_limit)); 555 | let (dispatch_preimage_print, dispatch_preimage_print_len) = 556 | preimage_for_dispatch_whitelisted_call 557 | .create_print_output(proposal_details.output_len_limit); 558 | 559 | // If it's a hash, let's write the data to a file you can upload. 560 | match dispatch_preimage_print { 561 | CallOrHash::Call(_) => (), 562 | CallOrHash::Hash(_) => { 563 | let mut info_to_write = "0x".to_owned(); 564 | info_to_write.push_str(hex::encode(dispatch_whitelisted_call.encoded).as_str()); 565 | fs::write("polkadot_asset_hub_public_referendum_preimage_to_note.call", info_to_write) 566 | .expect("it should write"); 567 | }, 568 | } 569 | 570 | PossibleCallsToSubmit { 571 | preimage_for_whitelist_call: whitelist_preimage_print, 572 | preimage_for_public_referendum: Some(( 573 | dispatch_preimage_print, 574 | dispatch_preimage_print_len, 575 | )), 576 | fellowship_referendum_submission: Some(NetworkRuntimeCall::PolkadotCollectives( 577 | fellowship_proposal.get_polkadot_collectives_call().expect("polkadot collectives"), 578 | )), 579 | public_referendum_submission: Some(NetworkRuntimeCall::PolkadotAssetHub( 580 | public_proposal.get_polkadot_asset_hub_call().expect("polkadot asset hub"), 581 | )), 582 | } 583 | } 584 | 585 | // Generate the calls needed for a proposal to pass on Polkadot without the Fellowship. 586 | fn polkadot_non_fellowship_referenda( 587 | proposal_details: &ProposalDetails, 588 | origin: PolkadotAssetHubOriginCaller, 589 | ) -> PossibleCallsToSubmit { 590 | use polkadot_asset_hub::runtime_types::{ 591 | frame_support::traits::{preimages::Bounded::Lookup, schedule::DispatchTime}, 592 | pallet_preimage::pallet::Call as PreimageCall, 593 | pallet_referenda::pallet::Call as ReferendaCall, 594 | }; 595 | 596 | let proposal_bytes = get_proposal_bytes(proposal_details.proposal.clone()); 597 | let proposal_call_info = CallInfo::from_bytes(&proposal_bytes, Network::PolkadotAssetHub); 598 | 599 | let public_referendum_dispatch_time = match proposal_details.dispatch { 600 | DispatchTimeWrapper::At(block) => DispatchTime::At(block), 601 | DispatchTimeWrapper::After(block) => DispatchTime::After(block), 602 | }; 603 | 604 | let note_proposal_preimage = CallInfo::from_runtime_call(NetworkRuntimeCall::PolkadotAssetHub( 605 | PolkadotAssetHubRuntimeCall::Preimage(PreimageCall::note_preimage { 606 | bytes: proposal_bytes, 607 | }), 608 | )); 609 | let public_proposal = CallInfo::from_runtime_call(NetworkRuntimeCall::PolkadotAssetHub( 610 | PolkadotAssetHubRuntimeCall::Referenda(ReferendaCall::submit { 611 | proposal_origin: Box::new(origin), 612 | proposal: Lookup { 613 | hash: H256(proposal_call_info.hash), 614 | len: proposal_call_info.length, 615 | }, 616 | enactment_moment: public_referendum_dispatch_time, 617 | }), 618 | )); 619 | let (preimage_print, preimage_print_len) = 620 | note_proposal_preimage.create_print_output(proposal_details.output_len_limit); 621 | 622 | PossibleCallsToSubmit { 623 | preimage_for_whitelist_call: None, 624 | preimage_for_public_referendum: Some((preimage_print, preimage_print_len)), 625 | fellowship_referendum_submission: None, 626 | public_referendum_submission: Some(NetworkRuntimeCall::PolkadotAssetHub( 627 | public_proposal.get_polkadot_asset_hub_call().expect("polkadot asset hub"), 628 | )), 629 | } 630 | } 631 | 632 | // Takes all the `calls` needed to submit and logs them according to the user's preferences. 633 | fn deliver_output(proposal_details: ProposalDetails, calls: PossibleCallsToSubmit) { 634 | let mut batch_of_calls = Vec::new(); 635 | 636 | if let Some((call_or_hash, len)) = calls.preimage_for_whitelist_call { 637 | match call_or_hash { 638 | CallOrHash::Call(c) => { 639 | println!("\nSubmit the preimage for the Fellowship referendum:"); 640 | print_output(&proposal_details.output, &c, proposal_details.use_light_client); 641 | batch_of_calls.push(c); 642 | }, 643 | CallOrHash::Hash(h) => { 644 | println!( 645 | "\nPreimage for the public whitelist call too large ({len} bytes). Not included in batch." 646 | ); 647 | println!("Submission should have the hash: 0x{}", hex::encode(h)); 648 | }, 649 | } 650 | } 651 | if let Some(c) = calls.fellowship_referendum_submission { 652 | println!("\nOpen a Fellowship referendum to whitelist the call:"); 653 | print_output(&proposal_details.output, &c, proposal_details.use_light_client); 654 | batch_of_calls.push(c); 655 | } 656 | if let Some((call_or_hash, len)) = calls.preimage_for_public_referendum { 657 | match call_or_hash { 658 | CallOrHash::Call(c) => { 659 | println!("\nSubmit the preimage for the public referendum:"); 660 | print_output(&proposal_details.output, &c, proposal_details.use_light_client); 661 | batch_of_calls.push(c); 662 | }, 663 | CallOrHash::Hash(h) => { 664 | println!( 665 | "\nPreimage for the public referendum too large ({len} bytes). Not included in batch." 666 | ); 667 | println!("A file was created that you can upload in `preimage.note_preimage` in Apps UI."); 668 | println!("Submission should have the hash: 0x{}", hex::encode(h)); 669 | }, 670 | } 671 | } 672 | if let Some(c) = calls.public_referendum_submission { 673 | println!("\nOpen a public referendum to dispatch the call:"); 674 | print_output(&proposal_details.output, &c, proposal_details.use_light_client); 675 | batch_of_calls.push(c); 676 | } 677 | 678 | if proposal_details.print_batch { 679 | handle_batch_of_calls( 680 | &proposal_details.output, 681 | batch_of_calls, 682 | proposal_details.use_light_client, 683 | ); 684 | } 685 | } 686 | 687 | // Takes a vec of calls, which could be intended for use on different networks, sorts them into the 688 | // appropriate network, and provides a single batch call for each network. 689 | fn handle_batch_of_calls(output: &Output, batch: Vec, use_light_client: bool) { 690 | use kusama_asset_hub::runtime_types::pallet_utility::pallet::Call as KusamaAssetHubUtilityCall; 691 | use kusama_relay::runtime_types::pallet_utility::pallet::Call as KusamaUtilityCall; 692 | use polkadot_asset_hub::runtime_types::pallet_utility::pallet::Call as PolkadotAssetHubUtilityCall; 693 | use polkadot_collectives::runtime_types::pallet_utility::pallet::Call as CollectivesUtilityCall; 694 | use polkadot_relay::runtime_types::pallet_utility::pallet::Call as PolkadotRelayUtilityCall; 695 | 696 | let mut kusama_relay_batch = Vec::new(); 697 | let mut kusama_asset_hub_batch = Vec::new(); 698 | let mut polkadot_asset_hub_batch = Vec::new(); 699 | let mut polkadot_relay_batch = Vec::new(); 700 | let mut polkadot_collectives_batch = Vec::new(); 701 | 702 | for network_call in batch { 703 | match network_call { 704 | NetworkRuntimeCall::Kusama(cc) => kusama_relay_batch.push(cc), 705 | NetworkRuntimeCall::KusamaAssetHub(cc) => kusama_asset_hub_batch.push(cc), 706 | NetworkRuntimeCall::Polkadot(cc) => polkadot_relay_batch.push(cc), 707 | NetworkRuntimeCall::PolkadotAssetHub(cc) => polkadot_asset_hub_batch.push(cc), 708 | NetworkRuntimeCall::PolkadotCollectives(cc) => polkadot_collectives_batch.push(cc), 709 | _ => panic!("no other chains are needed for this"), 710 | } 711 | } 712 | if !kusama_relay_batch.is_empty() { 713 | let batch = KusamaRuntimeCall::Utility(KusamaUtilityCall::force_batch { 714 | calls: kusama_relay_batch, 715 | }); 716 | println!("\nBatch to submit on Kusama Relay Chain:"); 717 | print_output(output, &NetworkRuntimeCall::Kusama(batch), use_light_client); 718 | } 719 | if !kusama_asset_hub_batch.is_empty() { 720 | let batch = KusamaAssetHubRuntimeCall::Utility(KusamaAssetHubUtilityCall::force_batch { 721 | calls: kusama_asset_hub_batch, 722 | }); 723 | println!("\nBatch to submit on Kusama Asset Hub:"); 724 | print_output(output, &NetworkRuntimeCall::KusamaAssetHub(batch), use_light_client); 725 | } 726 | if !polkadot_relay_batch.is_empty() { 727 | let batch = PolkadotRuntimeCall::Utility(PolkadotRelayUtilityCall::force_batch { 728 | calls: polkadot_relay_batch, 729 | }); 730 | println!("\nBatch to submit on Polkadot Relay Chain:"); 731 | print_output(output, &NetworkRuntimeCall::Polkadot(batch), use_light_client); 732 | } 733 | if !polkadot_asset_hub_batch.is_empty() { 734 | let batch = 735 | PolkadotAssetHubRuntimeCall::Utility(PolkadotAssetHubUtilityCall::force_batch { 736 | calls: polkadot_asset_hub_batch, 737 | }); 738 | println!("\nBatch to submit on Polkadot Asset Hub:"); 739 | print_output(output, &NetworkRuntimeCall::PolkadotAssetHub(batch), use_light_client); 740 | } 741 | if !polkadot_collectives_batch.is_empty() { 742 | let batch = CollectivesRuntimeCall::Utility(CollectivesUtilityCall::force_batch { 743 | calls: polkadot_collectives_batch, 744 | }); 745 | println!("\nBatch to submit on Polkadot Collectives Chain:"); 746 | print_output(output, &NetworkRuntimeCall::PolkadotCollectives(batch), use_light_client); 747 | } 748 | } 749 | 750 | // Format the data to print to console. 751 | fn print_output(output: &Output, network_call: &NetworkRuntimeCall, use_light_client: bool) { 752 | match network_call { 753 | NetworkRuntimeCall::Kusama(call) => { 754 | let endpoint = if use_light_client { 755 | "light-client" 756 | } else { 757 | "wss%3A%2F%2Fkusama-rpc.dwellir.com" 758 | }; 759 | let network_id = "kusama"; 760 | match output { 761 | Output::CallData => println!("0x{}", hex::encode(call.encode())), 762 | Output::AppsUiLink => println!( 763 | "https://dev.papi.how/extrinsics#data=0x{}&networkId={network_id}&endpoint={endpoint}", 764 | hex::encode(call.encode()) 765 | ), 766 | } 767 | }, 768 | NetworkRuntimeCall::KusamaAssetHub(call) => { 769 | let endpoint = if use_light_client { 770 | "light-client" 771 | } else { 772 | "wss%3A%2F%2Fasset-hub-kusama-rpc.dwellir.com" 773 | }; 774 | let network_id = "kusama_asset_hub"; 775 | match output { 776 | Output::CallData => println!("0x{}", hex::encode(call.encode())), 777 | Output::AppsUiLink => println!( 778 | "https://dev.papi.how/extrinsics#data=0x{}&networkId={network_id}&endpoint={endpoint}", 779 | hex::encode(call.encode()) 780 | ), 781 | } 782 | }, 783 | NetworkRuntimeCall::Polkadot(call) => { 784 | let endpoint = if use_light_client { 785 | "light-client" 786 | } else { 787 | "wss%3A%2F%2Fpolkadot-rpc.dwellir.com" 788 | }; 789 | let network_id = "polkadot"; 790 | match output { 791 | Output::CallData => println!("0x{}", hex::encode(call.encode())), 792 | Output::AppsUiLink => println!( 793 | "https://dev.papi.how/extrinsics#data=0x{}&networkId={network_id}&endpoint={endpoint}", 794 | hex::encode(call.encode()) 795 | ), 796 | } 797 | }, 798 | NetworkRuntimeCall::PolkadotAssetHub(call) => { 799 | let endpoint = if use_light_client { 800 | "light-client" 801 | } else { 802 | "wss%3A%2F%2Fasset-hub-polkadot-rpc.dwellir.com" 803 | }; 804 | let network_id = "polkadot_asset_hub"; 805 | match output { 806 | Output::CallData => println!("0x{}", hex::encode(call.encode())), 807 | Output::AppsUiLink => println!( 808 | "https://dev.papi.how/extrinsics#data=0x{}&networkId={network_id}&endpoint={endpoint}", 809 | hex::encode(call.encode()) 810 | ), 811 | } 812 | }, 813 | NetworkRuntimeCall::PolkadotCollectives(call) => { 814 | let endpoint = if use_light_client { 815 | "light-client" 816 | } else { 817 | "wss%3A%2F%2Fpolkadot-collectives-rpc.polkadot.io" 818 | }; 819 | let network_id = "polkadot_collectives"; 820 | match output { 821 | Output::CallData => println!("0x{}", hex::encode(call.encode())), 822 | Output::AppsUiLink => println!( 823 | "https://dev.papi.how/extrinsics#data=0x{}&networkId={network_id}&endpoint={endpoint}", 824 | hex::encode(call.encode()) 825 | ), 826 | } 827 | }, 828 | _ => panic!("no other chains are needed for this"), 829 | } 830 | } 831 | --------------------------------------------------------------------------------